diff --git a/.changeset/x402-bsc-payments.md b/.changeset/x402-bsc-payments.md new file mode 100644 index 00000000..07bf9983 --- /dev/null +++ b/.changeset/x402-bsc-payments.md @@ -0,0 +1,5 @@ +--- +"nansen-cli": patch +--- + +Support x402 payments with USDT on BNB Smart Chain (`eip155:56`), which the Nansen API now advertises as a payment option. Payment signing already handled any EVM network generically; this adds BSC to the post-payment balance check with the correct token contract and 18-decimal precision (Base USDC and X Layer USDT0 use 6), plus a `bsc` entry in the shared RPC registry with a `NANSEN_BSC_RPC` override. diff --git a/src/__tests__/x402-balance.test.js b/src/__tests__/x402-balance.test.js new file mode 100644 index 00000000..2f04f775 --- /dev/null +++ b/src/__tests__/x402-balance.test.js @@ -0,0 +1,38 @@ +/** + * Tests for the x402 EVM payment-token registry used by balance checks. + */ + +import { describe, it, expect } from 'vitest'; +import { EVM_X402_TOKENS } from '../x402.js'; +import { CHAIN_RPCS } from '../rpc-urls.js'; + +describe('EVM_X402_TOKENS registry', () => { + it('covers Base, X Layer, and BNB Smart Chain', () => { + expect(Object.keys(EVM_X402_TOKENS).sort()).toEqual( + ['eip155:196', 'eip155:56', 'eip155:8453'], + ); + }); + + it('uses 18 decimals for BSC USDT and 6 for the others', () => { + expect(EVM_X402_TOKENS['eip155:56'].decimals).toBe(18); + expect(EVM_X402_TOKENS['eip155:8453'].decimals).toBe(6); + expect(EVM_X402_TOKENS['eip155:196'].decimals).toBe(6); + }); + + it('pins the expected token contracts', () => { + expect(EVM_X402_TOKENS['eip155:8453'].token) + .toBe('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'); // Base USDC + expect(EVM_X402_TOKENS['eip155:56'].token) + .toBe('0x55d398326f99059fF775485246999027B3197955'); // BSC USDT + }); + + it('resolves an RPC URL for every network', () => { + for (const [network, cfg] of Object.entries(EVM_X402_TOKENS)) { + expect(cfg.rpc, `rpc for ${network}`).toMatch(/^https?:\/\//); + } + }); + + it('has a BSC entry in the shared RPC registry', () => { + expect(CHAIN_RPCS.bsc).toMatch(/^https?:\/\//); + }); +}); diff --git a/src/rpc-urls.js b/src/rpc-urls.js index ec1dd551..b91f1c40 100644 --- a/src/rpc-urls.js +++ b/src/rpc-urls.js @@ -8,6 +8,7 @@ * Override env vars: * NANSEN_EVM_RPC Custom Ethereum RPC (also used as generic EVM fallback) * NANSEN_BASE_RPC Custom Base RPC + * NANSEN_BSC_RPC Custom BNB Smart Chain RPC * NANSEN_XLAYER_RPC Custom X Layer RPC * NANSEN_SOLANA_RPC Custom Solana RPC * @@ -20,6 +21,7 @@ const DEFAULT_EVM_RPC = 'https://eth.public-rpc.com'; const DEFAULT_BASE_RPC = 'https://mainnet.base.org'; +const DEFAULT_BSC_RPC = 'https://bsc-dataseed.binance.org'; const DEFAULT_XLAYER_RPC = 'https://rpc.xlayer.tech'; const DEFAULT_SOLANA_RPC = 'https://api.mainnet-beta.solana.com'; @@ -27,6 +29,7 @@ export const CHAIN_RPCS = { ethereum: process.env.NANSEN_EVM_RPC || DEFAULT_EVM_RPC, evm: process.env.NANSEN_EVM_RPC || DEFAULT_EVM_RPC, // generic EVM fallback base: process.env.NANSEN_BASE_RPC || process.env.NANSEN_RPC_BASE || DEFAULT_BASE_RPC, + bsc: process.env.NANSEN_BSC_RPC || DEFAULT_BSC_RPC, xlayer: process.env.NANSEN_XLAYER_RPC || DEFAULT_XLAYER_RPC, solana: process.env.NANSEN_SOLANA_RPC || DEFAULT_SOLANA_RPC, }; diff --git a/src/x402.js b/src/x402.js index a462740f..0b8c8835 100644 --- a/src/x402.js +++ b/src/x402.js @@ -156,6 +156,17 @@ export async function createPaymentSignature(response, url, options = {}) { return null; } +/** + * x402 payment tokens per EVM network, matching the stablecoins the API + * advertises in 402 `accepts` entries. `decimals` matters: USDT on BNB Smart + * Chain uses 18 decimals, unlike the 6-decimal tokens on Base and X Layer. + */ +export const EVM_X402_TOKENS = { + 'eip155:8453': { token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', rpc: CHAIN_RPCS.base, symbol: 'USDC', decimals: 6 }, // Base USDC + 'eip155:196': { token: '0x779Ded0c9e1022225f8E0630b35a9b54bE713736', rpc: CHAIN_RPCS.xlayer, symbol: 'USDT0', decimals: 6 }, // X Layer USDT0 + 'eip155:56': { token: '0x55d398326f99059fF775485246999027B3197955', rpc: CHAIN_RPCS.bsc, symbol: 'USDT', decimals: 18 }, // BSC USDT +}; + /** * Check stablecoin balance for x402 payment wallet on the given network. * Returns `{ balance, symbol }` (USD amount + token symbol) or null if check fails. @@ -192,13 +203,9 @@ export async function checkX402Balance(network) { } if (network.startsWith('eip155:')) { - // Per-network token + RPC. Both tokens are 6-decimals. // Default to Base USDC if the network is unknown so existing wallets keep working. - const EVM_NETWORKS = { - 'eip155:8453': { token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', rpc: CHAIN_RPCS.base, symbol: 'USDC' }, // Base USDC - 'eip155:196': { token: '0x779Ded0c9e1022225f8E0630b35a9b54bE713736', rpc: CHAIN_RPCS.xlayer, symbol: 'USDT0' }, // X Layer USDT0 - }; - const { token, rpc, symbol } = EVM_NETWORKS[network] || EVM_NETWORKS['eip155:8453']; + const { token, rpc, symbol, decimals } = + EVM_X402_TOKENS[network] || EVM_X402_TOKENS['eip155:8453']; const addr = walletInfo.evm.replace('0x', '').toLowerCase().padStart(64, '0'); const resp = await fetch(rpc, { method: 'POST', @@ -210,7 +217,7 @@ export async function checkX402Balance(network) { }), }); const data = await resp.json(); - return { balance: parseInt(data.result, 16) / 1e6, symbol }; + return { balance: parseInt(data.result, 16) / 10 ** decimals, symbol }; } return null;