Skip to content

Price & token sources: list, order and extend where Sage reads prices - #897

Open
awizardxch wants to merge 4 commits into
xch-dev:mainfrom
awizardxch:feat/token-index-source
Open

awizardxch wants to merge 4 commits into
xch-dev:mainfrom
awizardxch:feat/token-index-source

Conversation

@awizardxch

@awizardxch awizardxch commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Sage prices CATs from Dexie's v3/prices/tickers, takes the XCH/USD rate from CoinGecko, and names CATs from Dexie's v1/assets. None of this is visible or adjustable. A CAT that Dexie has no price for (it trades elsewhere, such as an AMM, or it is not registered with Dexie) shows no value, and an unregistered CAT shows as "Unknown CAT", however actively it trades. The user can't see where a number came from, can't turn a source off, and can't add another one.

Change

A new section, Settings → Network → Price & Token Sources, lists the sources in their order of priority:

Source Provides Default
Dexie CAT prices; names and icons (saved in the wallet by Sage's own sync) On, first
CoinGecko XCH/USD rate On, second
Custom token index (any https URL the user adds) CAT prices, XCH/USD rate, and names, tickers and icons a token lacks Added by the user, last

Each row has a switch, up/down buttons to change its priority (plain buttons, keyboard accessible, with labels such as "Move Dexie up"), and custom rows have a remove button. An input below the list adds a custom token index: one https JSON document (format below).

Priority and fallback. For each value, the first enabled source that has it wins; later sources only fill gaps:

  • A CAT's price in XCH: the first enabled source with a price for that asset id (Dexie's last_price, an index's price_xch).
  • XCH/USD: the first enabled source with a positive rate (CoinGecko, an index's xch_usd).
  • Labels: each of name, ticker and icon is suggested by the first enabled index that has it, only where the token has none: a CAT the wallet already named but that has no ticker or icon still gets them. These suggestions are display-only (token list, token page header and the token page's card; its Edit dialog starts from, and saves, the wallet's own record). A name the wallet already has, from the user, Dexie or chia_updateCat, is never replaced, and nothing is written to the wallet's database: turn the index off and the suggestion is gone. To keep a suggested name, edit the token.

Defaults reproduce today's behavior exactly. Dexie on and first, CoinGecko on, no custom sources. Nothing is stored until the user changes the list, so a user who never opens the section makes exactly the requests Sage makes on main, with no new default host. Turning Dexie off stops the price request only. Dexie's names and icons come from the Rust CAT queue (crates/sage-wallet/src/queues/cat_queue.rs) and are not affected; the row says so.

Fetching. Custom indexes are fetched on the existing 60-second price refresh, beside Dexie and CoinGecko. Reordering doesn't refetch. A document whose version is not 1, or whose network does not match the wallet's (mainnet / testnet11), is ignored. Within it, each entry and field is checked on its own and a bad one is dropped: the asset id must be 64 lowercase hex characters (not the native coin), the name 1–64 characters, the ticker 1–16, the icon https and at most 512 characters (the same bounds as chia_updateCat), and prices finite and positive. A failed fetch keeps the last good copy.

What's not listed. Spacescan isn't in the list. Sage uses it only for explorer links (spacescanCoinUrl / spacescanNftUrl / spacescanCollectionUrl in src/lib/urls.ts, used by coin, NFT, collection and option pages). It doesn't fetch prices or token data from it. Its per-token API doesn't fit a one-call index either.

Files:

  • src/lib/priceSources.ts: the built-in sources, the stored list (normalizePriceSources, add, move, the legacy migration), activePriceSources for the network, and the priority rules (firstCatPriceXch, firstXchUsd, firstLabel).
  • src/lib/priceSources.test.ts: defaults, ordering, fallback, enablement, network matching and migration.
  • src/lib/tokenIndex.ts, src/lib/tokenIndex.test.ts: the index format, parseTokenIndex (validation), withSuggestedLabel, isValidTokenIndexUrl.
  • src/hooks/usePriceSources.ts: the setting, in local storage like the other preferences (priceSources). It reads once the single tokenIndexUrl key that an earlier build of this branch stored.
  • src/contexts/PriceContext.tsx: fetches only the enabled sources, applies the order, and exposes getSuggestedLabel.
  • src/pages/Settings.tsx: the section.
  • src/pages/TokenList.tsx, src/pages/Token.tsx: the display-only label suggestion.
  • src/locales/*/messages.po: pnpm extract for the 14 new strings (a separate "Extract" commit; the rest of that diff is reference lines whose line numbers moved). Without it a production build shows the strings' message ids. Translations for de-DE, es-MX and zh-CN are left empty for translators.

No Rust changes, no new dependency, no new default host.

Side finding, out of scope: Sage's hourly Dexie refresh upserts every Dexie asset with name = COALESCE(excluded.name, name) (crates/sage-database/src/tables/assets/asset.rs). So for a CAT Dexie names, a name the user typed (or set through chia_updateCat) is overwritten by Dexie's within the hour. This PR doesn't change that. It only means the "never overwrite" rule above holds for index suggestions but not, today, for the user's own edits against Dexie.

Token index format (version 1)

Any provider can serve it: an exchange, an indexer, a community list. Nothing in it is specific to one provider.

{
  "version": 1,
  "provider": { "name": "Example Index", "url": "https://index.example" },
  "network": "mainnet",
  "generated_at": "2026-10-17T07:24:54.208Z",
  "ttl_seconds": 300,
  "xch_usd": 1.62,
  "tokens": [
    {
      "asset_id": "ab558b1b841365a24d1ff2264c55982e55664a8b6e45bc107446b7e667bb463b",
      "name": "SPROUT",
      "ticker": "🌱",
      "icon_url": "https://index.example/icons/sprout.webp",
      "decimals": 3,
      "price_xch": 0.000016181075,
      "price_usd": 0.0000262133,
      "liquidity_xch": 91.857983,
      "source": "amm"
    }
  ]
}
Field Rule
version 1. Any other value: the document is ignored.
provider { name, url }, informational.
network mainnet or testnet11; must match the wallet's network.
generated_at ISO 8601 time.
ttl_seconds 60–86400; how long the document may be cached.
xch_usd USD per XCH, or null.
tokens[].asset_id Required. 64 lowercase hex characters; never the native coin.
tokens[].name 1–64 characters.
tokens[].ticker 1–16 characters.
tokens[].icon_url https://, at most 512 characters.
tokens[].description At most 280 characters (not used by this PR).
tokens[].decimals 0–18 (not used by this PR; CATs are 3).
tokens[].price_xch XCH per whole token, positive and finite.
tokens[].price_usd USD per whole token (not used by this PR; Sage computes USD from its own rate).
tokens[].liquidity_xch XCH of liquidity behind the price (informational).
tokens[].source Informational, such as amm or orderbook.

A provider should serve it like this: a GET over https with Access-Control-Allow-Origin: * and no key or cookie, one document per network at a stable URL, and Cache-Control with s-maxage equal to ttl_seconds so that a CDN answers wallets' polls. It should list a price only for markets deep enough that a few mojos can't move it.

How to test

  1. pnpm test:unit (src/lib/priceSources.test.ts, src/lib/tokenIndex.test.ts), pnpm lint, pnpm tsc -b, pnpm prettier --check on the touched files.
  2. Run Sage without opening the section. The token list, the token page and the network requests match main: only Dexie and CoinGecko are fetched, every 60 s.
  3. Open Settings → Network → Price & Token Sources. Dexie (1) and CoinGecko (2) are on. Tab to the up/down buttons and switches and use them from the keyboard.
  4. Type http://example.com and press Add: it's refused with a message. Add an https index for the wallet's network (such as the example above): it appears third, on.
  5. Hold a CAT that Dexie has no price for but the index does. Its USD value and price appear in the token list, sorted by value. A CAT that Dexie prices keeps Dexie's price. Move the index above Dexie: the index's price now wins for that CAT. Move it back.
  6. Hold an unnamed CAT ("Unknown CAT") that the index names. The list, the token page header and its card show the index's name, ticker and icon. A CAT named in the wallet but without a ticker or icon gets just those. Rename it with Edit: your name is shown. Turn the index off: the suggestion disappears.
  7. Turn Dexie off: no request to api.dexie.space/v3/prices/tickers, and CATs are priced by the index only. Turn CoinGecko off with an index that has xch_usd: the index's rate is used.
  8. Add an index for the other network, or one with "version": 2: it's ignored (a console warning), and Sage shows what it shows without it. Remove it.

🤖 Generated with Claude Code

awizardxch and others added 2 commits October 17, 2026 01:10
A new Settings > Network > Price & Token Sources section lists the sources
Sage already uses, with what each provides and a switch: Dexie (CAT prices;
its names and icons are saved by the wallet's own sync) and CoinGecko (the
XCH/USD rate). The user can add custom token indexes (one https JSON document,
version 1, listing CATs with a name, ticker, https icon and price in XCH) and
reorder all sources with up/down buttons.

For each value, the first enabled source that has it wins and later sources
only fill gaps: a CAT's price in XCH, the XCH/USD rate, and a display-only
label for a CAT with no name. Index labels never replace a name the wallet
has (from the user, Dexie or chia_updateCat) and are never written to it.
Indexes are fetched on the existing 60-second price refresh, validated
(version, network, text bounds matching chia_updateCat, https icons, finite
positive prices), and a failed fetch keeps the last good copy.

Nothing is stored until the user changes the list, and the defaults (Dexie
then CoinGecko, both on, no custom sources) make exactly the requests Sage
made before. The single tokenIndexUrl key from an earlier build of this
branch is read once into the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@awizardxch

awizardxch commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

A note on the red checks: the two iOS jobs and Android fail at the signing step, because GitHub doesn't pass this repo's signing secrets to a PR from a fork. The iOS log shows APPLE_CERTIFICATE empty and xcodebuild: error: The flag -authenticationKeyID is required when specifying -authenticationKeyPath; the Android log writes an empty keyAlias, password and keystore.jks. Every job that builds or tests the code passes: Tests, Linux x64 and ARM, Windows x64 and ARM, macOS and Validate package.

awizardxch and others added 2 commits October 17, 2026 12:04
…oken page

A token the wallet already named (for example through chia_updateCat) kept
"CAT" and no icon, because a suggestion was made only for a token with no
name. Each of name, ticker and icon is now suggested where the token has
none, and an existing value is still never replaced. The token page's card
also shows the suggested ticker and icon; its edit dialog keeps starting
from, and saving, the wallet's own record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant