morphit/docs/ADDING-A-COIN.md

639 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Adding a coin to Morphit
This is the developer-facing playbook for adding a new
cryptocurrency to Morphit. It covers two phases:
1. **What we need from the coin's community** before any code
is written.
2. **The file-by-file checklist** for actually doing the work.
Sibling maintainer docs: [`ADDING-A-WORKSPACE.md`](./ADDING-A-WORKSPACE.md)
for adding `apps/*` or `packages/*`; [`LOCALE-GRADUATION.md`](./LOCALE-GRADUATION.md)
for graduating a `PLANNED_LOCALES` entry to `SUPPORTED_LOCALES`.
If you maintain or represent a cryptocurrency project and want
yours added: read part 1, send us the requested data, and we
take it from there. Reach the Agorise team via the contact
URL in your local Morphit instance's footer.
If you're a Morphit developer about to add a coin: skim part 1
to understand what fixed inputs you have, then work through
part 2.
---
## Part 1 — What we need from the coin community
A request to add a coin should arrive with the following
information. No surprises later, no "let me check on the
icon" mid-implementation.
### Required
1. **Ticker symbol.** The 3-5 character all-caps form (BTC,
XMR, BLURT, ARRR). Must not collide with an already-listed
coin or with a fiat currency code (we exclude USD, EUR, JPY,
etc.).
2. **Full name.** How the coin is known in prose ("Bitcoin",
"Monero", "Pirate Chain"). Used in pickers, tooltips,
FAQ entries, and brag list. Pirate Chain is real as of cp41
(see ADR-0032), Decred is real as of cp43
(see ADR-0033), Solana is real as of cp45
(see ADR-0034), Ethereum is real as of cp47
(see ADR-0035), and Ripple is real as of cp49
(see ADR-0036); all five were hypothetical examples earlier.
announcements.
3. **One-line description.** Maximum 120 characters. Plain
prose, no slogans. This is what users see when picking a
coin in the trade form. Examples already in the registry:
- "Privacy-focused cryptocurrency. Default and recommended
on Morphit."
- "The original cryptocurrency. Recommend SegWit (bc1...)
addresses."
4. **Logo SVG.** Two variants:
- **Mono** — single-color silhouette. We re-color this for
dark-mode and themed rendering. Should look correct at
16×16 px.
- **Color** — full brand-color version. Used in marketing
pages and orderbook list items.
Both must be vector, valid SVG 1.1, and free of embedded
raster images. No `<text>` elements (font fallback issues);
convert text-in-logo to outlined paths. Square viewBox.
Origin (0, 0) top-left.
5. **Address format specification.** Enough detail to
construct a regex-based shape check:
- First-character constraint (e.g., starts with "1", "3",
or "bc1" for BTC; "4" or "8" for XMR; "zc" for ZEC
shielded; etc.).
- Length range (BTC: 2662 chars; XMR: 95 or 106).
- Allowed character set (Base58, Base32-bech, hex, etc.).
We do not perform checksum verification at the address-
shape layer (that would require bundling per-coin libraries
and is the recipient wallet's job). Cheap shape check only.
6. **TxID format.** How the coin's confirmed-transaction
identifier looks.
- Length (BTC: 64 hex; XMR: 64 hex; etc.).
- Character set.
- Whether confirmations are needed before display (and how
many is "safe enough" for our verifier).
7. **Smallest-unit decimals.** How many decimal places a
transfer amount carries on chain. BTC: 8 (satoshi). XMR:
12 (piconero). BLURT: 3. Used for amount-display rounding
and form input precision.
### Conditional
8. **Memo / payment-id support.** Whether the coin's base
transaction can carry an arbitrary memo or payment-id field
that's visible to both sender and recipient.
- Yes (BLURT, XLM): we expose a memo input in the address-
share modal.
- No (BTC, modern XMR): we omit it.
- "Yes but discouraged" (legacy XMR payment-IDs): we omit
it; subaddresses are the path forward.
9. **Public block-explorer JSON API endpoint.** If the coin
has a public, maintained, no-API-key explorer that returns
transaction details (sender, recipient, amount, txid,
confirmations) as JSON, we can wire automatic verification
for fee-payments in this asset. Without this, fee-payments
in this coin require manual operator confirmation.
What we need:
- URL template: `https://explorer.example/api/tx/{TXID}`
- Documented response schema (what field carries the
amount, recipient address, etc.).
- Rate limits.
- Funding model — is the explorer maintained by the coin's
foundation or by a third party? Affects how comfortable
we are depending on it.
10. **Color theme accent.** Hex code or Tailwind palette name
matching the coin's brand identity. We use this for
border accents, dot indicators, and the chip background.
Example values: BTC `text-amber-500`, XMR `text-orange-500`,
BLURT `text-morphit-emerald`.
### Out of scope (don't send these)
- Marketing copy. We write our own.
- Banner ads or sponsorship offers. Not what we do.
- Fiat conversion rates. We don't handle those; users
agree on prices peer-to-peer.
- Wallet recommendations. We may suggest wallets in support
docs, but that's case-by-case.
---
## Part 2 — File-by-file checklist
This section assumes you have all the data above on hand.
Time estimate for an experienced Morphit developer: half a day
of code, plus a translation pass for the user-facing strings
(another day if any new strings need translation across all 10
locales).
### Step 1 — Bundle the logo
```
apps/web/static/coins/<lower-ticker>.svg
```
Mono variant. Viewbox 0 0 24 24 or similar square.
Test: open the SVG in a browser at multiple sizes (16, 24, 48,
96 px) and confirm it stays crisp and recognizable.
### Step 2 — Extend the chain-payload schema
If the new coin will be used as a payment method in chat
(address-share, funds-sent) or as a listing-fee currency,
extend the on-chain payload schema:
**File:** `apps/web/src/lib/chat/payload.ts`
Add to the `PaymentMethod` type union:
```ts
export type PaymentMethod = 'btc' | 'xmr' | 'blurt' | '<new>';
```
**IMPORTANT — PROTOCOL VERSION:**
Existing indexers will REJECT any chain op carrying an unknown
`method` field as "schema violation". This means adding a new
method requires either:
- (a) Bumping the op version (currently `v: 1` for chat
payloads, `v: 1` for orders) so old indexers see it as a
newer-version op they ignore (forward-compat fallthrough);
OR
- (b) Coordinating a federation-wide indexer rollout before
any frontend can broadcast the new method.
Option (a) is preferred when possible — it preserves backward
compatibility. Option (b) is acceptable for tightly-
coordinated rollouts.
**File:** `apps/web/src/lib/orders/payload.ts`
Add to the `fee_method` union:
```ts
readonly fee_method?: 'blurt' | 'waived_first_buy' | 'btc' | 'xmr' | '<new>';
```
Same protocol-version considerations apply.
### Step 3 — Frontend asset registry
**File:** `apps/web/src/lib/assets/registry.ts`
Add a new entry to `ASSETS`:
```ts
{
ticker: '<new>',
displayTicker: '<NEW>',
displayName: '<Full Name>',
oneLineDescription: '...',
logoSvgPath: '/coins/<new>.svg',
accentClass: 'text-<color>-<shade>',
decimals: <integer>,
supportsMemo: <bool>,
addressValidator: validators.<new>,
canBeUsedForListingFee: <bool>,
canBeTraded: <bool>
}
```
Add the validator regex above the `validators` object. Keep
it permissive (cheap shape check); see existing entries for
style.
Run the smoke:
```
tsx apps/indexer/scripts/asset-registry-smoke.ts
```
Then add new scenario(s) for the new coin's address validator
(positive and negative cases).
### Step 4 — Indexer chain-payload validation
**File:** `apps/indexer/src/indexer/handlers/order.ts`
Find the `asset:` and `fee_method:` zod schemas. Add the new
ticker. Same for any per-method conditional checks (e.g.
"externalTxId is required when method is btc/xmr/<new>").
**File:** `apps/indexer/src/indexer/handlers/chat.ts` (if
chat-payload methods extended)
Same — add the new method to validation.
### Step 5 — Database schema (if explorer-verifier wired)
If the new coin gets explorer verification (per Part 1, item 9):
**File:** `apps/indexer/src/db/schema.sql`
Append a new migration section at the bottom of the file in
the same style as the existing v29/v30/v31 blocks:
```sql
-- ─── Migration vN — <new coin> explorer-verified payments ───────
```
Pre-Phase-3 the convention was a separate `schema-v<N>.sql`
file in `apps/indexer/src/db/`; that approach was collapsed
in May 2026 (see `MIGRATIONS` in
`apps/indexer/src/db/migrations.ts` — historical per-version
files are now archived under
`apps/indexer/src/db/historical/`). All new schema changes
go inline in `schema.sql` and register as additive migration
entries in `MIGRATIONS`. See `docs/adr/0001-...` for the
migration contract.
Pattern for the verifier code: see how
`bitcoinExplorerVerifier.ts` and `moneroProofVerifier.ts`
write their state. (Monero uses per-payment tx_proof
verification rather than view-key-based explorer scraping
since Part 108++; the BTC verifier remains the canonical
explorer-style template.)
**File:** `apps/indexer/src/indexer/fee/<new>ExplorerVerifier.ts`
New verifier module. Use the existing BTC and XMR verifiers as
templates. Implements the `Verifier` interface defined in
`fee/verifier.ts`.
### Step 6 — Order form + chat modals
**File:** `apps/web/src/lib/components/AddressShareModal.svelte`
Find the per-method branches (current code switches on
`method === 'btc'` / `'xmr'` / `'blurt'`). Replace with calls
through `getAsset(method)` from the registry — picks up the
new entry automatically once the registry is updated.
**File:** `apps/web/src/lib/components/FundsSentModal.svelte`
Same pattern — switch hard-coded branches to registry lookups
where reasonable.
**File:** `apps/web/src/routes/[lang]/post/+page.svelte`
The order-creation form. Find the asset-picker UI. Replace
hardcoded `<button>BTC</button> <button>XMR</button>` triples
with `{#each tradeableAssets() as a}` loops over the registry.
### Step 7 — i18n (NEW STRINGS)
If any new user-visible strings are needed (e.g., a coin-
specific hint or warning), add them across all 10 locales:
```
apps/web/src/lib/i18n/locales/{en,es,fr,de,it,pl,ru,fa,zh-CN,zh-HK}.json
```
Run the parity check:
```python
python3 -c "
import json, glob
def flat(d, p=''): yield from (yield_recurse(d, p))
# (see existing locale-parity check in scripts/run-smokes.sh)
"
```
Make sure no locale lags behind English.
### Step 8 — Tests
For every change above, add or update smoke scenarios:
- `apps/indexer/scripts/asset-registry-smoke.ts` — registry
shape and validators.
- `apps/indexer/scripts/order-handler-smoke.ts` (if exists) —
the new asset accepted by the indexer.
- `apps/indexer/scripts/listener-dispatch-smoke.ts` — if the
new coin has chat-payload paths.
- Visual: open the `/post` form, the orderbook, and the chat
address-share modal in dev and confirm the new coin appears
in pickers and renders its accent color correctly.
### Step 9 — Documentation update
- Add a brief mention of the new coin in `docs/ARCHITECTURE.md`
(the "What runs where" section).
- Update `docs/OPERATIONS.md` if operators need to do anything
per-coin (e.g., set up an explorer API key).
- Bump the changelog entry.
### Step 10 — Coordination
- Open a PR with all of the above.
- Ping the requesting coin's contact for review of the
description, logo rendering, and address-validation behavior.
- Get review from at least one other Morphit core developer.
- Stage on a non-canonical instance for at least 72h before
enabling on the canonical morphit.io.
---
## What we will NOT do
- Add a coin without all the Part 1 inputs.
- Add a coin whose address format is unstable or under active
consensus debate (we'd have to keep updating the validator).
- Add a coin that requires us to bundle and ship a per-coin
library larger than 50 KB (we keep the bundle small).
- Add a coin whose primary use is a centralized exchange's
internal token.
## Future infrastructure: Matrix bridge bot
Coin communities reach us via Matrix today. We're planning a
Matrix↔Morphit-chat bridge bot so coin-community discussions
about Morphit features can flow naturally between protocols.
Out of scope for the current iteration; tracked separately.
---
## 2026-05-13 architectural update (Part 121) — trade-only assets + multi-network coins
Memory #23 established a hard architectural invariant that
clarifies what kinds of assets can be added in each role:
**Listing fees can ONLY be paid in BLURT, XMR, or BTC.** This is
not a configuration knob; it's a wire-format-frozen decision.
The indexer's `fee_method` enum at
`apps/indexer/src/indexer/handlers/order.ts` is exactly the
4-member set `'blurt' | 'waived_first_buy' | 'btc' | 'xmr'`. Two
sentinel-grep smokes enforce this (see "Smoke coverage" below).
This split breaks coin additions into two categories:
### Category A — full-citizen coin (rare, requires deep operator
trust)
Used **both** as a tradable asset AND as a fee-payment method.
Adding one is a HARD breaking change because the wire-format
fee_method enum expands. Every operator must agree to verify
this coin's payments before federation can continue without
divergence. In practice we expect this category to be closed
at BLURT/XMR/BTC and not reopen. If a future case arises,
treat it as a charter-level decision, not a routine PR.
### Category B — trade-only coin (the common case for new
additions)
Used **only** for peer-to-peer trading between users. Cannot
be used for listing fees, cold-message fees, or featured-slot
bids. Adding one is a much smaller change:
1. Single entry in `packages/asset-registry/src/index.ts` with
`canPayListingFee: false` AND `canBeTraded: true`. The
asset-registry-smoke validates the invariant that
`canPayListingFee: true → ticker ∈ {BLURT, BTC, XMR}`, so a
miswired entry fails CI loudly.
2. Mirror in `apps/web/src/lib/assets/registry.ts` with
`canBeUsedForListingFee: false`.
3. No fee-verifier needed (the asset can't pay fees).
4. Standard logo + i18n + address validator + ADDING-A-COIN
Part 1 inputs.
The two new sentinel-grep smokes guarantee a Category B coin
cannot accidentally leak into the fee path:
- `packages/asset-registry/scripts/fee-method-enum-frozen-smoke.ts`
— asserts the indexer's `fee_method` field type union stays
exactly the 4-member frozen set. Belt + suspenders against
someone adding `'usdt'` to the enum out of habit.
- `packages/asset-registry/scripts/first-buy-waiver-payment-agnostic-smoke.ts`
— asserts the first-buy waiver gate fires on (side=buy,
asset=BLURT) regardless of `payment_methods`, so a new
user's first BLURT buy still gets the waiver even if they
pay their counterparty in USDT.
### Multi-network coins (USDT, USDC, DAI; possible future ETH, ARRR, etc.)
A new asset-registry field `supportedNetworks: readonly string[]`
declares which networks an asset exists on. Single-network
coins (BTC, XMR, BLURT, BCH, LTC, DASH, DOGE, ZEC, ARRR, DCR, SOL, ETH, XRP) declare `['mainnet']`.
Multi-network coins list each network explicitly. As of Part
122 cp31 three multi-network assets are shipped:
1. **USDT** (Part 121 cp3) — `supportedNetworks: ['erc20',
'trc20', 'spl', 'bep20']`. Each network has a visually
distinct address format (TRC-20 starts with `T`, ERC-20/BEP-20
start with `0x`, SPL is base58 32-44 chars), so users can
*usually* tell at a glance which network an address belongs
to.
2. **USDC** (Part 122 cp30) — `supportedNetworks: ['erc20',
'spl', 'base', 'polygon']`. WATCH OUT: three of the four
(ERC-20, Base, Polygon) all use the EVM `0x[40 hex]` address
format — they are visually IDENTICAL. An address that's
valid on Polygon is also a valid address on Ethereum AND on
Base, but each chain holds its own separate USDC balance for
that address. The network discriminator field is the ONLY
thing telling the sender's wallet which chain to broadcast
on. Cross-network-mis-send is more dangerous on USDC than
on USDT for this reason; the per-message cross-network
warning in `ChatMessage.svelte` reflects this. See ADR-0028
§"Why surface the EVM identical-address warning so prominently?".
3. **DAI** (Part 122 cp31) — `supportedNetworks: ['erc20',
'polygon', 'base', 'arbitrum']`. EVEN MORE DANGEROUS than
USDC for cross-network confusion: ALL FOUR DAI networks
share the EVM `0x[40 hex]` format (no Solana SPL variant
for DAI — SPL/TRC-20/BEP-20 versions of DAI are bridged/
wrapped, not Maker-native, and Morphit intentionally excludes
them per ADR-0029 §1). DAI is the highest cross-network
address-confusion surface on Morphit; the network picker
carries the strongest warning. Unlike USDT/USDC, DAI's
token contract has no admin freeze function (MakerDAO can't
blacklist), but is partly USDC-backed via the Peg Stability
Module — distinct `dai_partly_centralized` privacy-warning
class (vs `usdt_centralized` / `usdc_centralized`).
The **canonical reference** is the actual USDT, USDC, and DAI
entries at `packages/asset-registry/src/index.ts`. USDT:
```ts
{
ticker: 'USDT',
decimals: 6,
isCoordinationChain: false,
canBeTraded: true,
canPayListingFee: false, // Category B
supportedNetworks: ['erc20', 'trc20', 'spl', 'bep20'],
defaultNetwork: null, // force explicit user choice
privacyWarningKey: 'usdt_centralized',
addressShape:
/^(0x[a-fA-F0-9]{40}|T[1-9A-HJ-NP-Za-km-z]{33}|[1-9A-HJ-NP-Za-km-z]{32,44})$/,
privacyFeatures: { // Part 122 cp26 — required
freshAddressAdvice: 'hd-derived',
optInPrivacyTech: null, // USDT has no chain-level opt-in
privacyGuideKey: 'usdt'
}
}
```
USDC mirrors the structure (note the simpler regex — no Tron
T-prefix branch because Circle doesn't issue natively on Tron):
```ts
{
ticker: 'USDC',
decimals: 6,
isCoordinationChain: false,
canBeTraded: true,
canPayListingFee: false, // Category B (same as USDT)
supportedNetworks: ['erc20', 'spl', 'base', 'polygon'],
defaultNetwork: null, // force explicit user choice
privacyWarningKey: 'usdc_centralized',
addressShape:
/^(0x[a-fA-F0-9]{40}|[1-9A-HJ-NP-Za-km-z]{32,44})$/,
privacyFeatures: {
freshAddressAdvice: 'hd-derived',
optInPrivacyTech: null, // USDC has no chain-level opt-in
privacyGuideKey: 'usdc'
}
}
```
Setting `defaultNetwork: null` forces the post-order form to
require an explicit network pick on every trade — the safest
stance for cross-chain-mis-send-prone assets. Per-network
metadata (regexes, fee hints, bundled explorer URLs) lives
separately in `apps/web/src/lib/assets/networks.ts`; adding a
new network to an existing multi-network asset is a single
entry there.
**Indexer wire-format wiring** — both the indexer's `InstanceResponse`
interface AND the indexer-client mirror declare a per-network
`chat_link_urls.<asset>` sub-map (Part 122 cp30-DD-10/11 — when
adding a new multi-network asset, do NOT just add the field to
the indexer-client mirror. The indexer-side body construction
must ALSO populate it from the corresponding Config field + Zod
env-var schema entry). The 4 canonical wire-format surfaces
that must ALL be updated for any new multi-network asset:
1. Frontend store interface + defensive `?? {…}` fallback +
fetch normalization (`apps/web/src/lib/stores/instance.ts`)
2. Indexer-side InstanceResponse interface + body construction
(`apps/indexer/src/api/instance.ts`)
3. Indexer-client mirror (`packages/indexer-client/src/index.ts`)
4. Matrix-bot api-response-shape-smoke ChatLinkUrlsSchema
(`apps/matrix-bot/scripts/api-response-shape-smoke.ts`)
Plus the Config schema fields + env-var Zod entries in
`apps/indexer/src/config/index.ts`, the example env file at
`ops/env/indexer.env.example`, and the ops-cli wizard's
`ChatLinkExplorersResult` + step prompts + render emission.
The frontend address-share modal validates the address against
the chosen network's regex (not just the registry's combined
regex), and the `<ChatMessage>` component renders a bold-network
prefix + amber-warning (USDT) or blue-warning (USDC) aside
above the address: "Tron (TRC-20) USDT address — send USDT on
Tron only. Sending USDT on any other network to this address
loses your funds permanently."
Full architectural rationale: `docs/adr/0023-usdt-multi-network.md`,
`docs/adr/0028-usdc-multi-network-trade-only-addition.md`, and
`docs/adr/0029-dai-multi-network-trade-only-addition.md`.
### Privacy warning chip
A new field `privacyWarningKey: string | null` opts an asset
into rendering a localized privacy/decentralization warning in
the post-order form and address-share modal. `null` means no
warning (BTC, XMR, BLURT, BCH, LTC, DASH, DOGE, ZEC, ARRR, DCR,
SOL, ETH, XRP all have null — they're either private,
decentralized, or transparent-but-non-custodial enough that no
warning is needed). Non-null is an i18n key looked up under
`assets.privacy_warnings.<key>` in the locale JSON.
The three stablecoins ship a warning:
- USDT's warning (Part 121 cp3,
`assets.privacy_warnings.usdt_centralized`)
- USDC's warning (Part 122 cp30,
`assets.privacy_warnings.usdc_centralized`)
- DAI's warning (Part 122 cp31,
`assets.privacy_warnings.dai_partly_decentralized`)
USDT/USDC explain:
- The issuer (Tether Inc. for USDT, Circle for USDC) can freeze
any address on any supported network.
- Transactions are public on whichever network the user chose
(no on-chain privacy).
- Morphit can't make centralized stablecoins private — only XMR
and ARRR have meaningful on-chain privacy by construction.
DAI explains the partial-decentralization nuance: the Maker
contract has no admin-controlled freeze function, but Dai is
partly backed by USDC via the Peg Stability Module — so Circle's
freeze power transitively affects DAI's redemption mechanics
(documented in ADR-0029 §3).
This warning is required by Memory #19 (privacy is priority
#1): users must be told when an asset they're considering
fails the privacy bar.
### Privacy framework (`privacyFeatures` struct)
Every `AssetEntry` carries a `privacyFeatures` struct (shipped in
Part 122 cp26 — see `docs/adr/0026-transparent-chain-privacy-framework.md`).
The struct drives four user-facing surfaces simultaneously:
amount-jitter, address-reuse warnings, opt-in privacy-tech
listings, and the per-asset privacy guide page at
`/[lang]/privacy/{key}`.
Three fields, all required:
- **`freshAddressAdvice`** — one of `'subaddress'` (XMR-style),
`'hd-derived'` (BTC and forks, transparent UTXO chains), or
`'account-reuse'` (account-model chains like BLURT where the
address IS the account name).
- **`optInPrivacyTech`** — `null` if the chain has no in-protocol
opt-in privacy tech, OR an array of protocol-standard
identifiers from the fixed enum:
`'mweb' | 'cashfusion' | 'coinjoin' | 'payjoin' | 'privatesend'`.
These are PROTOCOL NAMES not wallet names — Morphit never
endorses specific wallets.
- **`privacyGuideKey`** — lowercase i18n key prefix. Pages live
at `/[lang]/privacy/{key}` and pull from `privacy.guides.{key}.*`
i18n strings.
If a new coin has a privacy tech not in the enum, **extend the
enum** in `packages/asset-registry/src/index.ts` AND in
`docs/adr/0026-transparent-chain-privacy-framework.md`'s table.
Then add localized copy under `privacy.opt_in_tech.{tech}.{name,explain}`
× 10 locales. Same path DASH took to add `'privatesend'` (cp27,
ADR-0027 §7).
Required i18n keys per new asset (× 10 locales):
- `privacy.guides.{key}.one_line` — under-80-char summary shown
on the `/privacy` index page next to the asset icon.
- `privacy.guides.{key}.intro` — 2-3 sentence intro paragraph
shown on the per-asset guide page.
- `privacy.guides.{key}.meta_description` — `<meta name=description>`
for SEO + social cards.
- `privacy.guides.{key}.caveats` — honest disclosure of where
the asset's privacy story falls short and what stronger
alternatives exist on Morphit (typically XMR).
Skip the caveats key if the asset has no caveats worth flagging
(rare — every asset has some compromise).