Skip to content
Merged
Show file tree
Hide file tree
Changes from 197 commits
Commits
Show all changes
212 commits
Select commit Hold shift + click to select a range
0a133ae
[Extension] Swap to New Token design doc
CassioMG Jun 24, 2026
e351df6
[Extension] Drop Paste button from Swap to New Token design doc
CassioMG Jun 24, 2026
bb5be7e
[Extension] Omit Popular tokens on custom networks in Swap design doc
CassioMG Jun 24, 2026
26112a5
[Extension] Rename isNew -> requiresTrustline in Swap design doc
CassioMG Jun 24, 2026
f6d73a4
[Extension] Add destinationTokenDetails to transactionSubmission slice
CassioMG Jun 24, 2026
3707af7
[Extension] Default swap slippage to 2% to match mobile
CassioMG Jun 24, 2026
c6b4981
[Extension] Add shared PercentageButtons component
CassioMG Jun 24, 2026
47a094b
[Extension] Add shared AmountCard component with optional security badge
CassioMG Jun 24, 2026
190e79e
[Extension] Migrate SendAmount to shared AmountCard + PercentageButtons
CassioMG Jun 24, 2026
adf9578
[Extension] Remove Send-local InputWidthContext now that AmountCard o…
CassioMG Jun 24, 2026
eec672d
[Extension] Add fetchTrendingAssets stellar.expert helper for swap Po…
CassioMG Jun 24, 2026
f228bcb
[Extension] Add Redux cache slots for swap Popular tokens + asset sca…
CassioMG Jun 24, 2026
dc8050f
[Extension] Add useSwapTokenLookup destination-discovery hook for swa…
CassioMG Jun 24, 2026
e661d1a
[Extension] Add SwapTokenRow for swap picker (held + non-held variants)
CassioMG Jun 24, 2026
c076158
[Extension] Add Verified/Unverified token info sheets for swap picker
CassioMG Jun 24, 2026
4002e02
[Extension] Add SwapPickerSections (sections, info icons, empty/fallb…
CassioMG Jun 24, 2026
81268ec
[Extension] Parameterize SwapAsset with selectionType (source/destina…
CassioMG Jun 24, 2026
775eccc
Extract buildChangeTrustOperation from getManageAssetXDR
CassioMG Jun 24, 2026
10a5792
Build atomic changeTrust+pathPayment swap tx with total-across-ops fee
CassioMG Jun 24, 2026
6e28b54
Add quote-expiry op-code classification helper
CassioMG Jun 24, 2026
73b8434
Add shouldShowXlmReservePreflight predicate
CassioMG Jun 24, 2026
8c557fd
Add horizonGetBestReceivePath for XLM-reserve prefill
CassioMG Jun 24, 2026
4998ae7
Add XlmReserveSheet (swap for 0.5 XLM, copy address, why-XLM link)
CassioMG Jun 24, 2026
5da1848
[Extension] Add swap-to-new-token telemetry event names
CassioMG Jun 24, 2026
939c6c7
[Extension] Add calculateSwapRate helper for swap review rate row
CassioMG Jun 24, 2026
1fc903d
[Extension] Add trustline banner + info sheet for swap review
CassioMG Jun 24, 2026
9c6dbc1
[Extension] Wire trustline banner, info sheet & rate row into ReviewTx
CassioMG Jun 24, 2026
7b12a23
[Extension] Render SwapAmount on shared AmountCard + PercentageButton…
CassioMG Jun 24, 2026
ff72c8c
[Extension] Add CTA state machine, post-scan gate & reserve pre-fligh…
CassioMG Jun 24, 2026
f7c230e
[Extension] Wire picker selectionType, destination details & swap tel…
CassioMG Jun 24, 2026
7f48804
[Extension] Add swap-to-new-token i18n strings (en + pt) with parity …
CassioMG Jun 24, 2026
741eaf0
[Extension] Add swap-to-new-token Playwright E2E spec
CassioMG Jun 24, 2026
aa07004
[Extension] Reset swap amount input to crypto for priceless source asset
CassioMG Jun 24, 2026
50c3ee6
[Extension] Fix wrong asset on "Sent!" screen after switching token m…
CassioMG Jun 25, 2026
90363c1
Polish Swap home + destination picker to match Figma
CassioMG Jun 25, 2026
cc6d33e
Refine Swap home + picker details to match Figma
CassioMG Jun 25, 2026
c0846e6
Conform Swap changes to AGENTS.md best practices
CassioMG Jun 25, 2026
2029643
Add setInputType to the priceless-asset effect deps
CassioMG Jun 25, 2026
910a746
Add performancePlugin no-op to the Amplitude autocapture stub
CassioMG Jun 25, 2026
4dc95c1
Mock runtime.onMessage/connect in the dev webextension-polyfill shim
CassioMG Jun 25, 2026
8736023
Use a plain plus icon for the empty swap Select picker
CassioMG Jun 25, 2026
bfb42f1
Match the empty swap Select pill dimensions to the token picker
CassioMG Jun 25, 2026
0868e93
Add the dropdown chevron to the empty swap Select picker
CassioMG Jun 25, 2026
156ac75
Darken the swap direction toggle so it's visible on the card seam
CassioMG Jun 25, 2026
ab092cd
Enlarge the swap confirm button to match the Send flow
CassioMG Jun 25, 2026
53cb2e3
Mute the Send flow "Fee:" label to match the Swap flow
CassioMG Jun 25, 2026
14eca11
Widen the AmountCard input click target
CassioMG Jun 25, 2026
4d9bf73
Style the empty Select plus as a small badge on a lighter circle
CassioMG Jun 25, 2026
f189637
Mute the Select plus icon to the "You receive" label color
CassioMG Jun 25, 2026
8028144
Shrink the swap direction toggle to 36px
CassioMG Jun 25, 2026
19e406a
Lighten the swap direction toggle so it doesn't melt into the background
CassioMG Jun 25, 2026
96afc00
Add shared AssetListRow + use it for the Swap discover lists
CassioMG Jun 25, 2026
07d3b62
Migrate Add-a-token rows to the shared AssetListRow
CassioMG Jun 25, 2026
ec0ca42
Space the Swap discover rows so they aren't glued together
CassioMG Jun 25, 2026
78a1018
Use a shared BalanceRow for Swap "Your tokens"; retire SwapTokenRow
CassioMG Jun 25, 2026
2d552e0
Render token icons as perfect circles regardless of source aspect ratio
CassioMG Jun 25, 2026
1d920fd
Share BalanceRow on the account-home balances list
CassioMG Jun 25, 2026
22017aa
Give BalanceRow lists consistent row spacing
CassioMG Jun 25, 2026
08ca478
Show the same "Your tokens" list in the Swap-from picker
CassioMG Jun 25, 2026
0e239f3
Fix "Your tokens" icons stuck loading in the Swap picker
CassioMG Jun 25, 2026
92ba6e4
Restyle Swap Verified/Unverified info sheets to match Figma
CassioMG Jun 25, 2026
9145652
Restyle trustline banner and info sheet to match Figma
CassioMG Jun 26, 2026
02e66f1
Filter the Swap-from list from the first character typed
CassioMG Jun 26, 2026
db4de25
Make Swap "Your tokens" classic-only, fiat-sorted, and always visible
CassioMG Jun 26, 2026
aca6604
Seed recommendedFee in XLM so Swap shows 0.00001, not "100 XLM"
CassioMG Jun 26, 2026
bc09db2
Show the picked non-held token's logo in the Swap receive picker
CassioMG Jun 26, 2026
f2490f4
Rework the Swap direction toggle and reset the amount on source change
CassioMG Jun 26, 2026
c5822d1
Inline-bold the reserve amount in the trustline info sheet
CassioMG Jun 26, 2026
4695fa2
Style the Swap picker empty state as a filled card
CassioMG Jun 26, 2026
d6f63fe
Reset the other picker to "(+) Select" when the same token is picked
CassioMG Jun 26, 2026
30c82f7
Never hide the fiat line; keep the swap fee in XLM
CassioMG Jun 26, 2026
fb72fcc
Use defined SDS font-weight vars in the info sheet
CassioMG Jun 26, 2026
e88a65f
Render the trustline info sheet as a slide-up sheet, not a review pane
CassioMG Jun 26, 2026
16bd79c
Price non-held swap destination tokens (token-prices + spot fallback)
CassioMG Jun 26, 2026
8485596
Remove dead Blockaid asset-scan Redux cache
CassioMG Jun 26, 2026
71151a9
Surface swap token-discovery outages instead of silent empty results
CassioMG Jun 26, 2026
d487072
Swap review: show conversion Rate, drop Minimum received + Memo rows
CassioMG Jun 26, 2026
2806c47
Reserve the 0.5 XLM trustline bump up-front when swapping XLM to a ne…
CassioMG Jun 26, 2026
e66ca34
Scale the default swap fee by operation count
CassioMG Jun 26, 2026
6a078eb
Wire up the "Swap for 0.5 XLM" reserve-recovery action
CassioMG Jun 27, 2026
382cd0f
Recover from a quote that expires at swap submit instead of dead-ending
CassioMG Jun 27, 2026
6ee060d
Fix conditional useMemo in SwapAmount (rules-of-hooks)
CassioMG Jun 27, 2026
8528d47
Gate the swap CTA on no-path and insufficient XLM for fees
CassioMG Jun 27, 2026
6a5c9eb
Show the Soroban hint when a pasted contract id returns no results
CassioMG Jun 27, 2026
4b30754
Simplify the swap record filter to honestly mirror mobile
CassioMG Jun 27, 2026
90dcc0a
Fold the destination token's Blockaid verdict into the swap review gate
CassioMG Jun 27, 2026
6450d41
Wire the swap source token's Blockaid verdict into the review gate
CassioMG Jun 27, 2026
344583a
Emit swap success + trustline-added telemetry post-confirmation
CassioMG Jun 27, 2026
0a10d36
Cache the last idle swap lookup in memory for instant repaint + stale…
CassioMG Jun 27, 2026
9382116
Persist the swap top-tokens list across popup sessions
CassioMG Jun 27, 2026
92e30aa
Pre-warm the swap top-tokens cache from the account screen
CassioMG Jun 27, 2026
5a23b48
Route swap top-tokens persistence through the background message hand…
CassioMG Jun 28, 2026
d039de2
Improve the swap amount-screen default state + "Select a token" CTA
CassioMG Jun 28, 2026
986a2ab
Resolve a pasted SAC to the held token it wraps on the swap-from picker
CassioMG Jun 28, 2026
9c2a4b5
Keep the Blockaid warning badge on the selected swap token pills
CassioMG Jun 28, 2026
512f581
Show a loader while the swap-to search is in flight
CassioMG Jun 28, 2026
e068684
Give the swap review Rate row a content-width label and a fitted value
CassioMG Jun 28, 2026
e3785a8
Keep a new token's icon through the Swapping/Swapped! states
CassioMG Jun 28, 2026
830a96e
Fix the swap-to picker loader getting stuck after a search
CassioMG Jun 29, 2026
c423a6e
Show the generic empty state for an unmatched address on the swap-fro…
CassioMG Jun 29, 2026
351e7cd
Enable the swap "Enter an amount" CTA and focus the sell input on tap
CassioMG Jun 29, 2026
b894df8
Fix the swap fiat-toggle first-tap glitches
CassioMG Jun 29, 2026
72d8583
Show a single Blockaid banner on the swap review + badge the flagged …
CassioMG Jun 29, 2026
17e9685
Hide the swap review body while the trustline info sheet is open
CassioMG Jun 29, 2026
3377ea6
Surface the swap quote-expired notice as a transient toast
CassioMG Jun 29, 2026
93bdf77
Rebuild the XLM-reserve sheet to match Figma + fix the help link
CassioMG Jun 29, 2026
fd13f8e
Don't let an unscannable tx hide a malicious-token banner on review
CassioMG Jun 29, 2026
24bc945
Re-measure the amount input width when the font-size class changes
CassioMG Jun 29, 2026
80df6d6
Render the trustline explanation in-flow so it isn't clipped on the r…
CassioMG Jun 29, 2026
a9e1a02
Shrink the swap direction toggle so it clears the fiat-toggle button
CassioMG Jun 29, 2026
4d54e6c
Disable the swap "Enter an amount" CTA while the sell input is focused
CassioMG Jun 29, 2026
fee8cec
Let the fiat amount input be fully erased to empty
CassioMG Jun 29, 2026
d08d298
Use mobile's insufficient-balance copy with the max spendable amount
CassioMG Jun 29, 2026
159efcc
Fetch swap token prices for held + selected tokens so prices survive …
CassioMG Jun 29, 2026
ca3cf4f
Recalculate the fiat amount when swapping for the 0.5 XLM reserve
CassioMG Jun 29, 2026
1c2e3d5
Polish the XLM-reserve sheet buttons + icon colors toward Figma
CassioMG Jun 29, 2026
1ffb6ab
Show friendly Blockaid reasons (not the raw validation string) on tx …
CassioMG Jun 29, 2026
c8812d3
Use "token" instead of "asset" in Blockaid security copy
CassioMG Jun 29, 2026
7467ffc
Speed up the swap-to search: in-session cache + earlier loader hand-off
CassioMG Jun 29, 2026
bc44578
Stop the i18n scanner splitting colon keys into stray namespace files
CassioMG Jun 29, 2026
144851f
Fix stale comment on the swap direction toggle size
CassioMG Jun 29, 2026
681d755
Fix XLM-sheet body color/line-height and swap the button variants
CassioMG Jun 29, 2026
24abfe1
Fix the in-flow trustline sheet's double padding and small "Got it" b…
CassioMG Jun 29, 2026
2b4825b
Shrink the direction toggle to 30px and keep the fiat toggle transparent
CassioMG Jun 29, 2026
da1270c
Size the reserve-swap prefill slightly above 0.5 XLM
CassioMG Jun 29, 2026
4c939c0
List swap token-scan Blockaid reasons next to the tx-scan reasons
CassioMG Jun 29, 2026
f346876
Confirm directly from the swap "Do not proceed" pane
CassioMG Jun 29, 2026
720f00d
Drop superseded swap-lookup dispatches to kill the search flash
CassioMG Jun 29, 2026
d63977e
Gate the swap amount screen on the network fee being ready
CassioMG Jun 29, 2026
1bd728d
Make swap token-scan reasons reachable on a token-only flag
CassioMG Jun 29, 2026
0833a79
Render the swap Blockaid sheet in-flow + banner/CTA polish
CassioMG Jun 29, 2026
396325c
Scope the white CTA + Blockaid-banner font; cover the in-flow sheet
CassioMG Jun 29, 2026
6d46dd0
Fix the InfoSheet CTA (bare text β†’ solid button) and match banner sizes
CassioMG Jun 29, 2026
456d833
Unify review banner spacing with a single flex gap
CassioMG Jun 29, 2026
2cca80f
Match the banner-block-to-Details gap to the inter-banner gap (12px)
CassioMG Jun 29, 2026
bcbba57
Cache swap Blockaid scans in-session + recover pick-time verdicts
CassioMG Jun 29, 2026
da890bf
Render the fee + memo review sheets in-flow (not side-slide)
CassioMG Jun 29, 2026
920e658
Make the whole swap Verified/Unverified section title clickable
CassioMG Jun 29, 2026
7649842
Unify add-token verified/unverified UX with the swap picker
CassioMG Jun 30, 2026
f941405
Add the "Transaction details" banner + breakdown to Send/Swap review
CassioMG Jun 30, 2026
3602d91
Review fixes: scan-on-select race, e2e copy, redundant scans, hardening
CassioMG Jun 30, 2026
f157ba4
Add 20px bottom padding to the transaction-details sheet
CassioMG Jun 30, 2026
05d66a9
Stop the slow-feeStats hang behind the swap fullscreen spinner
CassioMG Jun 30, 2026
2e033df
Review/swap polish: drop XDR row, fix memo padding, match section-tit…
CassioMG Jun 30, 2026
286bd10
Space the Send "Sending" card 8px below the recipient, 12px above %
CassioMG Jun 30, 2026
5efa213
Drop the unnecessary scanAssets dependency from the Operations scan e…
CassioMG Jun 30, 2026
6664c7a
Present Blockaid detail sheets in-flow instead of sliding from the side
CassioMG Jun 30, 2026
10417b6
Use a back arrow on the Send amount screen and tighten the account-ca…
CassioMG Jun 30, 2026
4bb5fed
Show the back arrow on the Send flow's initial asset-picker screen
CassioMG Jun 30, 2026
6e7e284
Add 20px of breathing room below the swap review Confirm-anyway button
CassioMG Jun 30, 2026
5e654c1
Relax SEP-0042 asset-list validation and keep partially-valid assets
CassioMG Jun 30, 2026
0136046
Align Swap/Send transaction-details labels and amounts with mobile
CassioMG Jun 30, 2026
fa9edc3
Fix operation types in the tx-details parity tests
CassioMG Jun 30, 2026
8f60e8f
Add a single reusable BlockaidBanner component
CassioMG Jun 30, 2026
073a01f
Warn when an asset list is rejected for failing schema validation
CassioMG Jun 30, 2026
319eba4
Style the swap path as numbered cards to match mobile
CassioMG Jun 30, 2026
523c025
Render the swap review's Blockaid warning via the unified BlockaidBanner
CassioMG Jun 30, 2026
4d783e0
Match BlockaidBanner icon size and font to the trustline banner
CassioMG Jun 30, 2026
ae9212d
Render the Blockaid banner text with SDS Text so its size applies
CassioMG Jun 30, 2026
0abcdeb
Tighten the transaction-details sheet to match mobile
CassioMG Jun 30, 2026
25bea26
Match the Blockaid banner text and icon to the trustline banner exactly
CassioMG Jun 30, 2026
c0bc227
Match the Blockaid banner height to the trustline banner
CassioMG Jun 30, 2026
75ed2ac
Refresh the swap Popular list on a network switch; hide it gracefully…
CassioMG Jun 30, 2026
22316fd
Render the swap Path as its own section with one card per hop
CassioMG Jun 30, 2026
2c8a146
Use isTestnet() for swap discovery support instead of a raw network-k…
CassioMG Jun 30, 2026
f1f368a
Make the swap path hop badges perfect circles
CassioMG Jun 30, 2026
19d951c
Clear the cached verified token lists when the network changes
CassioMG Jun 30, 2026
2ebee1e
Use the same Rate-row icon as mobile (SwitchHorizontal01)
CassioMG Jun 30, 2026
bad9a4c
Don't reopen the asset/collectible detail when closing the Send flow
CassioMG Jun 30, 2026
dcd6ae7
Show the Rate row above the Fee row on the swap review
CassioMG Jun 30, 2026
240306e
Stop the swap CTA flashing enabledβ†’disabled after picking the destina…
CassioMG Jun 30, 2026
27058bd
Revert "Don't reopen the asset/collectible detail when closing the Se…
CassioMG Jun 30, 2026
3478643
Fix double/uneven padding on the ChangeTrust sheets
CassioMG Jun 30, 2026
2c456a5
Show a Copied tooltip on the swap picker menu and red quote-expired t…
CassioMG Jun 30, 2026
137ee97
Fix the swap picker "Copied" tooltip not appearing
CassioMG Jun 30, 2026
281e125
Unify the Blockaid security UX: verdict-colored badges and one banner
CassioMG Jul 1, 2026
93bf840
Merge branch 'master' into feature/swap-to-new-token
CassioMG Jul 1, 2026
94978dd
Pass networkDetails to the extra token-price fetch after the master m…
CassioMG Jul 1, 2026
39a5c70
Use "Token Code"/"Token Issuer" in the ChangeTrust transaction details
CassioMG Jul 1, 2026
787c25e
Update useGetTokenPrices tests for network-keyed token prices
CassioMG Jul 1, 2026
2f6d720
Refine the swap token-discovery notice spacing and weight
CassioMG Jul 1, 2026
a57ed64
Update E2E specs for the unified Blockaid banner
CassioMG Jul 1, 2026
d20ba1c
Expand and repair swap E2E coverage
CassioMG Jul 1, 2026
b89f8fe
Add Tier-2 swap/Blockaid E2E coverage
CassioMG Jul 1, 2026
11d5d00
Repair blockaid swap E2E tests for the new picker flow
CassioMG Jul 1, 2026
00566ac
Migrate sendPayment "Swap" E2E tests to the new picker flow
CassioMG Jul 1, 2026
2550f51
Fix Send fiat-toggle E2E tests for the refactored AmountCard
CassioMG Jul 1, 2026
d170214
Fix addAssetContractSearch tests for the name-vs-code row display
CassioMG Jul 1, 2026
dd66bf0
Correct the displayCode comment in ManageAssetRows
CassioMG Jul 1, 2026
9a3e6cb
Stop Jest from treating __tests__/fixtures as test suites
CassioMG Jul 1, 2026
9606471
Fix addAssetIntegration tests for the manage-assets row refactor
CassioMG Jul 1, 2026
ed72373
Extract SwapAmount business logic into hooks + helpers
CassioMG Jul 1, 2026
9322da1
Show "--" (not "$0.00") for a token with no USD price
CassioMG Jul 1, 2026
70709a3
Clean up comments added in this PR: drop stale refs, tighten wording
CassioMG Jul 1, 2026
625f026
Type the swap top-tokens cache as TrendingAsset[] instead of unknown[]
CassioMG Jul 2, 2026
07b7cff
Fix cacheSwapTopTokens handler returning undefined
CassioMG Jul 2, 2026
6e76412
Default swap-sell-card and 'Swap to' E2E waits to the standard 5s tim…
CassioMG Jul 2, 2026
bbc8169
Move AmountCard mirror-span styles into a CSS class
CassioMG Jul 2, 2026
1942136
Consolidate amount font-size scales into a shared fontScale module
CassioMG Jul 2, 2026
d1e21ab
Extract a formatUsdValue helper in getSwapDerivedData
CassioMG Jul 2, 2026
acb199d
Share MAX_ASSETS_TO_SCAN from the Blockaid helpers
CassioMG Jul 2, 2026
8911a5f
Name the swap amount default literals
CassioMG Jul 2, 2026
c40fd69
Memoize the SEP-0042 schema fetch across asset-list validations
CassioMG Jul 2, 2026
c172156
Make swap percentage buttons always derive from the crypto balance
CassioMG Jul 2, 2026
e4262ff
Reuse the previewed quote at swap review instead of re-fetching
CassioMG Jul 2, 2026
45d9712
Show the true crypto amount in fiat mode, not the rounded-USD derivation
CassioMG Jul 2, 2026
51c3ceb
Show the same sell fiat figure on the swap review as the amount screen
CassioMG Jul 2, 2026
1eb26da
Make the crypto amount the source of truth in the Send amount screen
CassioMG Jul 2, 2026
e5126d3
Reset the memoized SEP-0042 schema between searchAsset test cases
CassioMG Jul 2, 2026
7635667
Fix Blockaid destination-token fail-open on the swap flow
CassioMG Jul 2, 2026
d568194
Return the bulk-scan shape from stubScanAssetSafe (fixes swap safe E2E)
CassioMG Jul 2, 2026
4ecb82a
Reconcile swap-to-new-token design doc against the as-built code
CassioMG Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions @shared/api/helpers/__tests__/fixtures/sep0042-assetlist.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Upstream SEP-0042 asset-list JSON schema, pinned verbatim for deterministic tests.
/* eslint-disable */
export const SEP0042_ASSETLIST_SCHEMA = {
$schema: "http://json-schema.org/draft-07/schema#",
title: "Stellar Asset List",
description: "Schema for Stellar Asset Lists (SAL)",
type: "object",
definitions: {
CleanString: {
type: "string",
pattern: "^[\\w\\u0020.,-@]*$",
},
},
properties: {
name: {
$ref: "#/definitions/CleanString",
maxLength: 30,
minLength: 5,
description: "Short descriptive title of the list",
},
network: {
type: "string",
enum: ["public", "testnet"],
},
provider: {
$ref: "#/definitions/CleanString",
maxLength: 50,
minLength: 5,
description: "Organization or entity that put together the list",
},
description: {
type: "string",
maxLength: 400,
description:
"Text description of the list to display alongside with the name",
},
version: {
type: "string",
pattern: "^\\d{1,4}\\.\\d{1,4}$",
description: "Current list revision",
},
feedback: {
type: "string",
format: "uri",
maxLength: 100,
description:
"URL or Github repository address where users can report bad actors or request addition of new assets",
},
assets: {
type: "array",
minItems: 1,
maxItems: 1000,
description: "Array of asset metadata entries",
items: {
type: "object",
properties: {
name: {
$ref: "#/definitions/CleanString",
maxLength: 30,
minLength: 5,
description: "Array of asset metadata entries",
},
contract: {
type: "string",
pattern: "^C[A-Z0-9]{55}$",
description:
"Asset contract address in StrKey encoding (for Soroban assets)",
},
code: {
type: "string",
pattern: "^[A-Za-z0-9]{1,12}$",
description: "Asset code (for Classic assets)",
},
issuer: {
type: "string",
pattern: "^G[A-Z0-9]{55}$",
description: "Asset issuer address (for Classic assets)",
},
org: {
$ref: "#/definitions/CleanString",
maxLength: 30,
minLength: 5,
description: "Issuer organization/company",
},
domain: {
type: "string",
pattern:
"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$",
description:
"FQDN of the site that hosts asset-related stellar.toml format",
},
icon: {
type: "string",
oneOf: [
{
format: "uri",
},
{
pattern: "^baf[a-zA-Z0-9]+$",
},
],
description:
"Icon URL (only HTTPS protocol is supported) or IPFS hash",
},
decimals: {
type: "integer",
minimum: 0,
maximum: 38,
description: "Number of decimals to display",
},
comment: {
type: "string",
maxLength: 150,
description:
"Alerts, messages, or other additional information specified by the provider",
},
},
required: ["name", "org"],
anyOf: [
{
required: ["contract"],
},
{
required: ["code", "issuer"],
},
],
additionalProperties: false,
},
},
},
required: ["name", "provider", "version", "assets"],
additionalProperties: false,
} as const;
125 changes: 125 additions & 0 deletions @shared/api/helpers/__tests__/token-list.schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { schemaValidatedAssetList } from "../token-list";
import { AssetListResponse } from "@shared/constants/soroban/asset-list";
import { SEP0042_ASSETLIST_SCHEMA } from "./fixtures/sep0042-assetlist.schema";

jest.mock("@sentry/browser", () => ({
captureException: jest.fn(),
}));

import { captureException } from "@sentry/browser";

const VALID_CONTRACT = `C${"A".repeat(55)}`; // ^C[A-Z0-9]{55}$
const VALID_ISSUER = `G${"A".repeat(55)}`; // ^G[A-Z0-9]{55}$

const baseAsset = (overrides: Record<string, unknown> = {}) => ({
name: "Test Asset",
org: "Test Org",
code: "TEST",
issuer: VALID_ISSUER,
contract: VALID_CONTRACT,
domain: "test.com",
icon: "https://test.com/icon.png",
decimals: 7,
...overrides,
});

const baseList = (overrides: Record<string, unknown> = {}): AssetListResponse =>
({
name: "Test List",
provider: "Test Provider",
description: "A test list",
version: "1.0",
network: "public",
assets: [baseAsset()],
...overrides,
}) as AssetListResponse;

describe("schemaValidatedAssetList", () => {
beforeEach(() => {
jest.clearAllMocks();
// The SEP-0042 schema is fetched over the network; return the fixture
// so validation is deterministic and offline.
global.fetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => SEP0042_ASSETLIST_SCHEMA,
}) as jest.Mock;
});

it("accepts 'mainnet' as a network value", async () => {
const result = await schemaValidatedAssetList(
baseList({ network: "mainnet" }),
);
expect(result.errors).toBeNull();
expect(result.assets).toHaveLength(1);
});

it("accepts a three-segment version", async () => {
const result = await schemaValidatedAssetList(
baseList({ version: "1.4.4" }),
);
expect(result.errors).toBeNull();
expect(result.assets).toHaveLength(1);
});

it("strips an invalid name but keeps the asset and its other fields", async () => {
const result = await schemaValidatedAssetList(
baseList({ assets: [baseAsset({ name: "Carbon tCOβ‚‚e offset" })] }),
);
expect(result.errors).toBeNull();
expect(result.assets).toHaveLength(1);
expect(result.assets[0].name).toBeUndefined();
expect(result.assets[0].code).toBe("TEST");
expect(result.assets[0].contract).toBe(VALID_CONTRACT);
});

it("strips an invalid contract but keeps the asset via code+issuer", async () => {
const result = await schemaValidatedAssetList(
baseList({
assets: [baseAsset({ contract: "deadbeef-not-a-contract" })],
}),
);
expect(result.errors).toBeNull();
expect(result.assets).toHaveLength(1);
expect(result.assets[0].contract).toBeUndefined();
expect(result.assets[0].code).toBe("TEST");
expect(result.assets[0].issuer).toBe(VALID_ISSUER);
});

it("tolerates assets missing both name and org", async () => {
const asset = baseAsset();
delete (asset as Record<string, unknown>).name;
delete (asset as Record<string, unknown>).org;
const result = await schemaValidatedAssetList(
baseList({ assets: [asset] }),
);
expect(result.errors).toBeNull();
expect(result.assets).toHaveLength(1);
});

it("still rejects a list with a non-relaxed violation (bad domain)", async () => {
const result = await schemaValidatedAssetList(
baseList({ assets: [baseAsset({ domain: "NOT A DOMAIN!!" })] }),
);
expect(result.assets).toHaveLength(0);
expect(result.errors).not.toBeNull();
expect(result.errors!.length).toBeGreaterThan(0);
});

it("returns empty assets and null errors when the schema cannot be fetched", async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: false, status: 500 });
const result = await schemaValidatedAssetList(baseList());
expect(result.assets).toHaveLength(0);
expect(result.errors).toBeNull();
expect(captureException).toHaveBeenCalled();
});

it("does not mutate the input list when stripping fields", async () => {
const list = baseList({
assets: [baseAsset({ contract: "deadbeef-not-a-contract" })],
});
const snapshot = JSON.stringify(list);
await schemaValidatedAssetList(list);
expect(JSON.stringify(list)).toBe(snapshot);
});
});
85 changes: 72 additions & 13 deletions @shared/api/helpers/token-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,47 @@ import {
} from "@shared/constants/stellar";
import { CUSTOM_NETWORK } from "@shared/helpers/stellar";

const SEP_0042_SCHEMA_URL =
"https://raw.githubusercontent.com/orbitlens/stellar-protocol/sep-0042-token-lists/contents/sep-0042/assetlist.schema.json";

// Per-asset fields we treat as best-effort: when one of these fails validation
// we strip just that field and keep the rest of the asset. Matches error paths
// like "instance.assets[3].contract".
const STRIPPABLE_ASSET_FIELD =
/^instance\.assets\[(\d+)\]\.(name|contract|org)$/;

// Relax the upstream SEP-0042 schema to the subset of rules Freighter needs so a
// single odd field doesn't void an otherwise-useful list:
// - allow "mainnet" as a network value (in addition to public/testnet)
// - allow an optional third version segment (e.g. "1.4.4")
// - make name/org optional (contract is already optional via the schema's
// "contract OR code+issuer" anyOf)
// Operates on a clone so the caller's schema object is never mutated.
const relaxAssetListSchema = (rawSchema: unknown) => {
const schema = JSON.parse(JSON.stringify(rawSchema));
const properties = schema?.properties ?? {};

if (
Array.isArray(properties.network?.enum) &&
!properties.network.enum.includes("mainnet")
) {
properties.network.enum.push("mainnet");
}

if (typeof properties.version?.pattern === "string") {
properties.version.pattern = "^\\d{1,4}\\.\\d{1,4}(\\.\\d{1,4})?$";
}

const assetItems = properties.assets?.items;
if (Array.isArray(assetItems?.required)) {
assetItems.required = assetItems.required.filter(
(field: string) => field !== "name" && field !== "org",
);
}

return schema;
};

export const schemaValidatedAssetList = async (
assetListJson: AssetListResponse,
): Promise<{
Expand All @@ -22,9 +63,7 @@ export const schemaValidatedAssetList = async (
}> => {
let schemaRes;
try {
schemaRes = await fetch(
"https://raw.githubusercontent.com/orbitlens/stellar-protocol/sep-0042-token-lists/contents/sep-0042/assetlist.schema.json",
);
schemaRes = await fetch(SEP_0042_SCHEMA_URL);
} catch (err) {
captureException("Error fetching SEP-0042 JSON schema");
return { assets: [] as AssetListReponseItem[], errors: null };
Expand All @@ -35,19 +74,39 @@ export const schemaValidatedAssetList = async (
return { assets: [] as AssetListReponseItem[], errors: null };
}

const schemaResJson = await schemaRes?.json();

// check against the SEP-0042 schema
const validatedList = validate(assetListJson, schemaResJson);
const schema = relaxAssetListSchema(await schemaRes.json());

// Validate against a copy so we never mutate the (possibly cached) source list.
const candidate = {
...assetListJson,
assets: (assetListJson.assets || []).map((asset) => ({ ...asset })),
};

// First pass: drop the individual name/contract/org fields that fail
// validation, keeping the rest of each asset intact.
const firstPass = validate(candidate, schema);
for (const error of firstPass.errors) {
const match = error.property.match(STRIPPABLE_ASSET_FIELD);
if (match) {
const [, index, field] = match;
delete candidate.assets[Number(index)][
field as "name" | "contract" | "org"
];
}
}

if (validatedList.errors.length) {
return {
assets: [] as AssetListReponseItem[],
errors: validatedList.errors,
};
// Second pass: anything still invalid is a rule we don't relax, so reject the
// whole list.
const finalPass = validate(candidate, schema);
if (finalPass.errors.length) {
console.warn(
`Rejecting asset list from provider "${assetListJson.provider || "Unknown Provider"}": failed SEP-0042 schema validation`,
finalPass.errors,
);
return { assets: [] as AssetListReponseItem[], errors: finalPass.errors };
}

return { assets: assetListJson.assets, errors: null };
return { assets: candidate.assets, errors: null };
};

export const getCombinedAssetListData = async ({
Expand Down
Loading
Loading