Skip to content

Commit 38b272f

Browse files
committed
feat: add DOGE, XRP, ADA, BNB, USDT token support
- Add derivation for DOGE (BIP44 coin type 3) - Add derivation for XRP (coin type 144, Ripple base58) - Add derivation for ADA/Cardano (CIP-1852, bech32) - Add derivation for BNB (EVM-compatible) - Add USDT_ETH, USDT_POL, USDT_SOL token chains - Create DERIVABLE_CHAINS single source of truth - Add /api/web-wallet/supported-chains endpoint - Simplify ChainSelector to use DERIVABLE_CHAINS - Add 48 comprehensive tests for all chains - Update getMissingChains to fetch from API - Add USDT contract addresses for balance fetching Total: 15 chains now supported (was 8)
1 parent 4f2db93 commit 38b272f

File tree

10 files changed

+826
-322
lines changed

10 files changed

+826
-322
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { NextResponse } from 'next/server';
2+
import { DERIVABLE_CHAINS, DERIVABLE_CHAIN_INFO } from '@/lib/web-wallet/keys';
3+
4+
/**
5+
* GET /api/web-wallet/supported-chains
6+
*
7+
* Returns the list of chains that can be derived from a BIP39 mnemonic.
8+
* This is the single source of truth for what chains the web wallet supports.
9+
*
10+
* Response:
11+
* {
12+
* chains: ['BTC', 'BCH', ...],
13+
* chainInfo: { BTC: { name: 'Bitcoin', symbol: 'BTC' }, ... }
14+
* }
15+
*/
16+
export async function GET() {
17+
return NextResponse.json({
18+
chains: DERIVABLE_CHAINS,
19+
chainInfo: DERIVABLE_CHAIN_INFO,
20+
});
21+
}

src/app/web-wallet/create/page.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,12 @@ import { SeedDisplay } from '@/components/web-wallet/SeedDisplay';
99
import { ChainMultiSelect } from '@/components/web-wallet/ChainSelector';
1010
import { checkPasswordStrength } from '@/lib/web-wallet/client-crypto';
1111
import { downloadEncryptedSeedPhrase } from '@/lib/web-wallet/seedphrase-backup';
12+
import { DERIVABLE_CHAINS } from '@/lib/web-wallet/keys';
1213

1314
type Step = 'password' | 'seed' | 'verify';
1415

15-
const DEFAULT_CHAINS = [
16-
'BTC', 'BCH', 'ETH', 'POL', 'SOL',
17-
'DOGE', 'XRP', 'ADA', 'BNB',
18-
'USDT', 'USDC',
19-
'USDC_ETH', 'USDC_POL', 'USDC_SOL',
20-
];
16+
// Default to all derivable chains
17+
const DEFAULT_CHAINS = [...DERIVABLE_CHAINS];
2118

2219
export default function CreateWalletPage() {
2320
const router = useRouter();

src/app/web-wallet/import/page.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,10 @@ import { SeedInput } from '@/components/web-wallet/SeedInput';
99
import { ChainMultiSelect } from '@/components/web-wallet/ChainSelector';
1010
import { checkPasswordStrength } from '@/lib/web-wallet/client-crypto';
1111
import { downloadEncryptedSeedPhrase } from '@/lib/web-wallet/seedphrase-backup';
12+
import { DERIVABLE_CHAINS } from '@/lib/web-wallet/keys';
1213

13-
const DEFAULT_CHAINS = [
14-
'BTC', 'BCH', 'ETH', 'POL', 'SOL',
15-
'DOGE', 'XRP', 'ADA', 'BNB',
16-
'USDT', 'USDC',
17-
'USDC_ETH', 'USDC_POL', 'USDC_SOL',
18-
];
14+
// Default to all derivable chains
15+
const DEFAULT_CHAINS = [...DERIVABLE_CHAINS];
1916

2017
export default function ImportWalletPage() {
2118
const router = useRouter();

src/components/web-wallet/ChainSelector.tsx

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,15 @@
11
'use client';
22

3-
const ALL_CHAINS = [
4-
{ id: 'BTC', name: 'Bitcoin', symbol: 'BTC' },
5-
{ id: 'BCH', name: 'Bitcoin Cash', symbol: 'BCH' },
6-
{ id: 'ETH', name: 'Ethereum', symbol: 'ETH' },
7-
{ id: 'POL', name: 'Polygon', symbol: 'POL' },
8-
{ id: 'SOL', name: 'Solana', symbol: 'SOL' },
9-
{ id: 'DOGE', name: 'Dogecoin', symbol: 'DOGE' },
10-
{ id: 'XRP', name: 'Ripple', symbol: 'XRP' },
11-
{ id: 'ADA', name: 'Cardano', symbol: 'ADA' },
12-
{ id: 'BNB', name: 'BNB Chain', symbol: 'BNB' },
13-
{ id: 'USDT', name: 'Tether', symbol: 'USDT' },
14-
{ id: 'USDC', name: 'USD Coin', symbol: 'USDC' },
15-
{ id: 'USDC_ETH', name: 'USDC (Ethereum)', symbol: 'USDC' },
16-
{ id: 'USDC_POL', name: 'USDC (Polygon)', symbol: 'USDC' },
17-
{ id: 'USDC_SOL', name: 'USDC (Solana)', symbol: 'USDC' },
18-
{ id: 'USDT_ETH', name: 'USDT (Ethereum)', symbol: 'USDT' },
19-
{ id: 'USDT_POL', name: 'USDT (Polygon)', symbol: 'USDT' },
20-
{ id: 'USDT_SOL', name: 'USDT (Solana)', symbol: 'USDT' },
21-
] as const;
3+
import { DERIVABLE_CHAINS, DERIVABLE_CHAIN_INFO, type DerivableChain } from '@/lib/web-wallet/keys';
4+
5+
/**
6+
* Build chain list from the single source of truth
7+
*/
8+
const CHAIN_LIST = DERIVABLE_CHAINS.map((id) => ({
9+
id,
10+
name: DERIVABLE_CHAIN_INFO[id].name,
11+
symbol: DERIVABLE_CHAIN_INFO[id].symbol,
12+
}));
2213

2314
interface BalanceInfo {
2415
balance: string;
@@ -43,8 +34,8 @@ export function ChainSelector({
4334
balances,
4435
}: ChainSelectorProps) {
4536
const available = chains
46-
? ALL_CHAINS.filter((c) => chains.includes(c.id))
47-
: ALL_CHAINS;
37+
? CHAIN_LIST.filter((c) => chains.includes(c.id))
38+
: CHAIN_LIST;
4839

4940
// Format balance for display
5041
const formatBalance = (chainId: string): string => {
@@ -111,7 +102,7 @@ export function ChainMultiSelect({
111102
</label>
112103
)}
113104
<div className="grid grid-cols-1 min-[400px]:grid-cols-2 gap-2">
114-
{ALL_CHAINS.map((chain) => {
105+
{CHAIN_LIST.map((chain) => {
115106
const selected = value.includes(chain.id);
116107
return (
117108
<button

src/components/web-wallet/__tests__/ChainSelector.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('ChainSelector', () => {
1111

1212
// Should have "Select chain" + 17 chain options
1313
const options = screen.getAllByRole('option');
14-
expect(options.length).toBe(18); // 1 placeholder + 17 chains
14+
expect(options.length).toBe(16); // 1 placeholder + 15 derivable chains
1515
});
1616

1717
it('should render with label', () => {

src/lib/wallet-sdk/wallet.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ export class Wallet {
7676
const chains: WalletChain[] = options.chains || [
7777
'BTC', 'BCH', 'ETH', 'POL', 'SOL',
7878
'DOGE', 'XRP', 'ADA', 'BNB',
79-
'USDT', 'USDC',
8079
'USDC_ETH', 'USDC_POL', 'USDC_SOL',
80+
'USDT_ETH', 'USDT_POL', 'USDT_SOL',
8181
];
8282
const mnemonic = generateMnemonic(options.words || 12);
8383
const bundle = await deriveWalletBundle(mnemonic, chains);
@@ -132,11 +132,12 @@ export class Wallet {
132132
}
133133

134134
const client = new WalletAPIClient(options);
135-
// Only include chains with implemented key derivation
136-
// DOGE, XRP, ADA, BNB, USDT, USDC key derivation not yet implemented
135+
// Default to all derivable chains
137136
const chains: WalletChain[] = options.chains || [
138137
'BTC', 'BCH', 'ETH', 'POL', 'SOL',
138+
'DOGE', 'XRP', 'ADA', 'BNB',
139139
'USDC_ETH', 'USDC_POL', 'USDC_SOL',
140+
'USDT_ETH', 'USDT_POL', 'USDT_SOL',
140141
];
141142
const bundle = await deriveWalletBundle(mnemonic, chains);
142143

@@ -322,7 +323,9 @@ export class Wallet {
322323
// Default chains that should exist
323324
const defaultChains: WalletChain[] = targetChains || [
324325
'BTC', 'BCH', 'ETH', 'POL', 'SOL',
326+
'DOGE', 'XRP', 'ADA', 'BNB',
325327
'USDC_ETH', 'USDC_POL', 'USDC_SOL',
328+
'USDT_ETH', 'USDT_POL', 'USDT_SOL',
326329
];
327330

328331
// Get current addresses
@@ -360,15 +363,35 @@ export class Wallet {
360363
async getMissingChains(
361364
targetChains?: WalletChain[]
362365
): Promise<WalletChain[]> {
363-
const defaultChains: WalletChain[] = targetChains || [
364-
'BTC', 'BCH', 'ETH', 'POL', 'SOL',
365-
'USDC_ETH', 'USDC_POL', 'USDC_SOL',
366-
];
366+
// Dynamically fetch current supported chains from the server
367+
// This ensures old wallets see newly added chains
368+
let supportedChains: WalletChain[];
369+
370+
if (targetChains) {
371+
supportedChains = targetChains;
372+
} else {
373+
try {
374+
// Fetch from API to get latest supported chains
375+
const resp = await this.client.request<{ chains: string[] }>({
376+
method: 'GET',
377+
path: '/api/web-wallet/supported-chains',
378+
});
379+
supportedChains = (resp.chains || []) as WalletChain[];
380+
} catch {
381+
// Fallback to hardcoded list if API fails
382+
supportedChains = [
383+
'BTC', 'BCH', 'ETH', 'POL', 'SOL',
384+
'DOGE', 'XRP', 'ADA', 'BNB',
385+
'USDC_ETH', 'USDC_POL', 'USDC_SOL',
386+
'USDT_ETH', 'USDT_POL', 'USDT_SOL',
387+
];
388+
}
389+
}
367390

368391
const currentAddresses = await this.getAddresses();
369392
const existingChains = new Set(currentAddresses.map((a) => a.chain));
370393

371-
return defaultChains.filter((c) => !existingChains.has(c));
394+
return supportedChains.filter((c) => !existingChains.has(c));
372395
}
373396

374397
// ── Balances ──

src/lib/web-wallet/balance.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,16 @@ const USDC_CONTRACTS: Record<string, string> = {
6363
// USDT contract addresses (ERC-20 compatible)
6464
const USDT_CONTRACTS: Record<string, string> = {
6565
USDT_ETH: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
66+
USDT_POL: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', // USDT on Polygon
6667
USDT_BNB: '0x55d398326f99059fF775485246999027B3197955',
6768
};
6869

6970
// USDC on Solana (SPL token mint)
7071
const USDC_SOL_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
7172

73+
// USDT on Solana (SPL token mint)
74+
const USDT_SOL_MINT = 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB';
75+
7276
// USDC/USDT have 6 decimals on all chains
7377
const USDC_DECIMALS = 6;
7478
const USDT_DECIMALS = 6;
@@ -436,17 +440,20 @@ export async function fetchBalance(address: string, chain: WalletChain): Promise
436440
return fetchADABalance(address);
437441

438442
// USDC variants
439-
case 'USDC':
440443
case 'USDC_ETH':
441444
return fetchERC20Balance(address, USDC_CONTRACTS.USDC_ETH, rpc.ETH, USDC_DECIMALS);
442445
case 'USDC_POL':
443446
return fetchERC20Balance(address, USDC_CONTRACTS.USDC_POL, rpc.POL, USDC_DECIMALS);
444447
case 'USDC_SOL':
445448
return fetchSPLTokenBalance(address, USDC_SOL_MINT, rpc.SOL, USDC_DECIMALS);
446449

447-
// USDT (defaults to ETH)
448-
case 'USDT':
450+
// USDT variants
451+
case 'USDT_ETH':
449452
return fetchERC20Balance(address, USDT_CONTRACTS.USDT_ETH, rpc.ETH, USDT_DECIMALS);
453+
case 'USDT_POL':
454+
return fetchERC20Balance(address, USDT_CONTRACTS.USDT_POL, rpc.POL, USDT_DECIMALS);
455+
case 'USDT_SOL':
456+
return fetchSPLTokenBalance(address, USDT_SOL_MINT, rpc.SOL, USDT_DECIMALS);
450457

451458
default:
452459
throw new Error(`Unsupported chain: ${chain}`);

src/lib/web-wallet/identity.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import { secp256k1 } from '@noble/curves/secp256k1';
1111
export type WalletChain =
1212
| 'BTC' | 'BCH' | 'ETH' | 'POL' | 'SOL'
1313
| 'DOGE' | 'XRP' | 'ADA' | 'BNB'
14-
| 'USDT' | 'USDC'
15-
| 'USDC_ETH' | 'USDC_POL' | 'USDC_SOL';
14+
| 'USDC_ETH' | 'USDC_POL' | 'USDC_SOL'
15+
| 'USDT_ETH' | 'USDT_POL' | 'USDT_SOL';
1616

1717
/** All valid chain values */
1818
export const VALID_CHAINS: WalletChain[] = [
1919
'BTC', 'BCH', 'ETH', 'POL', 'SOL',
2020
'DOGE', 'XRP', 'ADA', 'BNB',
21-
'USDT', 'USDC',
2221
'USDC_ETH', 'USDC_POL', 'USDC_SOL',
22+
'USDT_ETH', 'USDT_POL', 'USDT_SOL',
2323
];
2424

2525
/** BIP44 derivation path patterns per chain */
@@ -31,13 +31,14 @@ export const DERIVATION_PATHS: Record<string, string> = {
3131
SOL: "m/44'/501'",
3232
DOGE: "m/44'/3'/0'/0",
3333
XRP: "m/44'/144'/0'/0",
34-
ADA: "m/1852'/1815'/0'/0",
34+
ADA: "m/1852'/1815'/0'/0'",
3535
BNB: "m/44'/60'/0'/0",
36-
USDT: "m/44'/60'/0'/0",
37-
USDC: "m/44'/60'/0'/0",
3836
USDC_ETH: "m/44'/60'/0'/0",
3937
USDC_POL: "m/44'/60'/0'/0",
4038
USDC_SOL: "m/44'/501'",
39+
USDT_ETH: "m/44'/60'/0'/0",
40+
USDT_POL: "m/44'/60'/0'/0",
41+
USDT_SOL: "m/44'/501'",
4142
};
4243

4344
/**
@@ -99,14 +100,15 @@ export function validateAddress(address: string, chain: WalletChain): boolean {
99100
case 'ETH':
100101
case 'POL':
101102
case 'BNB':
102-
case 'USDT':
103-
case 'USDC':
104103
case 'USDC_ETH':
105104
case 'USDC_POL':
105+
case 'USDT_ETH':
106+
case 'USDT_POL':
106107
// EVM address: 0x followed by 40 hex chars
107108
return /^0x[0-9a-fA-F]{40}$/.test(address);
108109
case 'SOL':
109110
case 'USDC_SOL':
111+
case 'USDT_SOL':
110112
// Solana: base58 encoded, 32-44 chars
111113
return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address);
112114
case 'DOGE':
@@ -132,11 +134,16 @@ export function validateDerivationPath(path: string, chain: WalletChain): boolea
132134
const basePath = DERIVATION_PATHS[chain];
133135
if (!basePath) return false;
134136

135-
if (chain === 'SOL' || chain === 'USDC_SOL') {
137+
if (chain === 'SOL' || chain === 'USDC_SOL' || chain === 'USDT_SOL') {
136138
// Solana uses: m/44'/501'/n'/0'
137139
return /^m\/44'\/501'\/\d+'\/0'$/.test(path);
138140
}
139141

142+
if (chain === 'ADA') {
143+
// Cardano CIP-1852 (all hardened for Ed25519): m/1852'/1815'/account'/role'/index'
144+
return /^m\/1852'\/1815'\/\d+'\/\d+'\/\d+'$/.test(path);
145+
}
146+
140147
// BTC/BCH/ETH/POL: m/44'/coinType'/0'/0/n
141148
const escapedBase = basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
142149
const regex = new RegExp(`^${escapedBase}/\\d+$`);
@@ -155,27 +162,28 @@ export function isValidChain(chain: string): chain is WalletChain {
155162
*/
156163
export function buildDerivationPath(chain: WalletChain, index: number): string {
157164
switch (chain) {
158-
case 'SOL':
159-
case 'USDC_SOL':
160-
return `m/44'/501'/${index}'/0'`;
161165
case 'BTC':
162166
return `m/44'/0'/0'/0/${index}`;
163167
case 'BCH':
164168
return `m/44'/145'/0'/0/${index}`;
165169
case 'ETH':
166170
case 'POL':
167171
case 'BNB':
168-
case 'USDT':
169-
case 'USDC':
170172
case 'USDC_ETH':
171173
case 'USDC_POL':
174+
case 'USDT_ETH':
175+
case 'USDT_POL':
172176
return `m/44'/60'/0'/0/${index}`;
177+
case 'SOL':
178+
case 'USDC_SOL':
179+
case 'USDT_SOL':
180+
return `m/44'/501'/${index}'/0'`;
173181
case 'DOGE':
174182
return `m/44'/3'/0'/0/${index}`;
175183
case 'XRP':
176184
return `m/44'/144'/0'/0/${index}`;
177185
case 'ADA':
178-
return `m/1852'/1815'/0'/0/${index}`;
186+
return `m/1852'/1815'/0'/0'/${index}'`;
179187
default:
180188
throw new Error(`Unsupported chain: ${chain}`);
181189
}

0 commit comments

Comments
 (0)