nansen-cli 1.32.0 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/api.js +4 -4
- package/src/rpc-urls.js +3 -0
- package/src/x402-evm.js +145 -1
- package/src/x402.js +98 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.33.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#457](https://github.com/nansen-ai/nansen-cli/pull/457) [`8149564`](https://github.com/nansen-ai/nansen-cli/commit/8149564f181dc7bfc9c66488ba2373df6f1aab5d) Thanks [@gulshngill](https://github.com/gulshngill)! - x402 on BNB Smart Chain: support all four stablecoins the API now advertises (U, USD1, USDT, USDC) and add Permit2 payment signing. Payments route on the 402's `extra.assetTransferMethod` — `eip3009` keeps the existing gasless flow (U, USD1), while `permit2-exact` (USDT, USDC on BSC) signs a Permit2 `PermitWitnessTransferFrom` against the spender contract advertised in the 402. Permit2 entries are skipped with an actionable message when the wallet hasn't made the one-time `approve(Permit2, …)` for the token. Post-payment balance warnings now check the exact token paid with (per-token decimals) instead of one hardcoded token per network.
|
|
8
|
+
|
|
9
|
+
## 1.32.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#455](https://github.com/nansen-ai/nansen-cli/pull/455) [`875fabb`](https://github.com/nansen-ai/nansen-cli/commit/875fabb3f86736b03c874904e27a898315ef4bfa) Thanks [@gulshngill](https://github.com/gulshngill)! - 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.
|
|
14
|
+
|
|
3
15
|
## 1.32.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -493,7 +493,7 @@ export class NansenAPI {
|
|
|
493
493
|
* an attemptX402Payment() method so adding a new payment provider only requires
|
|
494
494
|
* touching that one method, not hunting inside the retry loop.
|
|
495
495
|
*/
|
|
496
|
-
async _x402Retry(signature, walletLabel, network, url, body, options = {}) {
|
|
496
|
+
async _x402Retry(signature, walletLabel, network, url, body, options = {}, asset = null) {
|
|
497
497
|
const paidResponse = await fetch(url, {
|
|
498
498
|
method: 'POST',
|
|
499
499
|
headers: {
|
|
@@ -514,7 +514,7 @@ export class NansenAPI {
|
|
|
514
514
|
if (network) {
|
|
515
515
|
try {
|
|
516
516
|
const { checkX402Balance } = await import('./x402.js');
|
|
517
|
-
const result = await checkX402Balance(network);
|
|
517
|
+
const result = await checkX402Balance(network, asset);
|
|
518
518
|
if (result !== null && result.balance < 0.25) {
|
|
519
519
|
console.error(`[x402] Warning: ${result.symbol} balance low ($${result.balance.toFixed(2)}). Fund your wallet to avoid interruptions.`);
|
|
520
520
|
}
|
|
@@ -651,8 +651,8 @@ export class NansenAPI {
|
|
|
651
651
|
// 1. Try local wallet with fallback across payment networks
|
|
652
652
|
try {
|
|
653
653
|
const { createPaymentSignatures } = await import('./x402.js');
|
|
654
|
-
for await (const { signature, network } of createPaymentSignatures(response, url)) {
|
|
655
|
-
const result = await this._x402Retry(signature, `local wallet ${defaultWalletName}`, network, url, body, options);
|
|
654
|
+
for await (const { signature, network, asset } of createPaymentSignatures(response, url)) {
|
|
655
|
+
const result = await this._x402Retry(signature, `local wallet ${defaultWalletName}`, network, url, body, options, asset);
|
|
656
656
|
if (result !== null) return result;
|
|
657
657
|
// This payment option was rejected, try next
|
|
658
658
|
}
|
package/src/rpc-urls.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Override env vars:
|
|
9
9
|
* NANSEN_EVM_RPC Custom Ethereum RPC (also used as generic EVM fallback)
|
|
10
10
|
* NANSEN_BASE_RPC Custom Base RPC
|
|
11
|
+
* NANSEN_BSC_RPC Custom BNB Smart Chain RPC
|
|
11
12
|
* NANSEN_XLAYER_RPC Custom X Layer RPC
|
|
12
13
|
* NANSEN_SOLANA_RPC Custom Solana RPC
|
|
13
14
|
*
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
|
|
21
22
|
const DEFAULT_EVM_RPC = 'https://eth.public-rpc.com';
|
|
22
23
|
const DEFAULT_BASE_RPC = 'https://mainnet.base.org';
|
|
24
|
+
const DEFAULT_BSC_RPC = 'https://bsc-dataseed.binance.org';
|
|
23
25
|
const DEFAULT_XLAYER_RPC = 'https://rpc.xlayer.tech';
|
|
24
26
|
const DEFAULT_SOLANA_RPC = 'https://api.mainnet-beta.solana.com';
|
|
25
27
|
|
|
@@ -27,6 +29,7 @@ export const CHAIN_RPCS = {
|
|
|
27
29
|
ethereum: process.env.NANSEN_EVM_RPC || DEFAULT_EVM_RPC,
|
|
28
30
|
evm: process.env.NANSEN_EVM_RPC || DEFAULT_EVM_RPC, // generic EVM fallback
|
|
29
31
|
base: process.env.NANSEN_BASE_RPC || process.env.NANSEN_RPC_BASE || DEFAULT_BASE_RPC,
|
|
32
|
+
bsc: process.env.NANSEN_BSC_RPC || DEFAULT_BSC_RPC,
|
|
30
33
|
xlayer: process.env.NANSEN_XLAYER_RPC || DEFAULT_XLAYER_RPC,
|
|
31
34
|
solana: process.env.NANSEN_SOLANA_RPC || DEFAULT_SOLANA_RPC,
|
|
32
35
|
};
|
package/src/x402-evm.js
CHANGED
|
@@ -25,6 +25,39 @@ const AUTHORIZATION_TYPES = [
|
|
|
25
25
|
{ name: 'nonce', type: 'bytes32' },
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
+
// ============= Permit2 (permit2-exact transfer method) =============
|
|
29
|
+
// Some tokens (e.g. USDT/USDC on BNB Smart Chain) predate EIP-3009, so
|
|
30
|
+
// facilitators settle them through Uniswap's canonical Permit2 contract
|
|
31
|
+
// instead: the payer signs a PermitWitnessTransferFrom and the facilitator's
|
|
32
|
+
// proxy (requirements.extra.spenderAddress) executes the transfer. Requires a
|
|
33
|
+
// one-time on-chain `token.approve(PERMIT2_ADDRESS, ...)` from the payer.
|
|
34
|
+
|
|
35
|
+
export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
|
|
36
|
+
|
|
37
|
+
// Permit2's EIP-712 domain has no version field.
|
|
38
|
+
const PERMIT2_DOMAIN_TYPES = [
|
|
39
|
+
{ name: 'name', type: 'string' },
|
|
40
|
+
{ name: 'chainId', type: 'uint256' },
|
|
41
|
+
{ name: 'verifyingContract', type: 'address' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const TOKEN_PERMISSIONS_TYPES = [
|
|
45
|
+
{ name: 'token', type: 'address' },
|
|
46
|
+
{ name: 'amount', type: 'uint256' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const WITNESS_TYPES = [
|
|
50
|
+
{ name: 'to', type: 'address' },
|
|
51
|
+
{ name: 'validAfter', type: 'uint256' },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// encodeType for a nested primary type: referenced struct types are appended
|
|
55
|
+
// in alphabetical order per EIP-712 (TokenPermissions before Witness).
|
|
56
|
+
const PERMIT_WITNESS_TYPE_STRING =
|
|
57
|
+
'PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Witness witness)' +
|
|
58
|
+
'TokenPermissions(address token,uint256 amount)' +
|
|
59
|
+
'Witness(address to,uint256 validAfter)';
|
|
60
|
+
|
|
28
61
|
/**
|
|
29
62
|
* Encode a type string for EIP-712 typeHash.
|
|
30
63
|
* e.g. "TransferWithAuthorization(address from,address to,uint256 value,...)"
|
|
@@ -110,6 +143,38 @@ export function hashTypedData(domain, primaryType, fields, message) {
|
|
|
110
143
|
]));
|
|
111
144
|
}
|
|
112
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Compute the EIP-712 digest for a Permit2 PermitWitnessTransferFrom.
|
|
148
|
+
*
|
|
149
|
+
* Hand-rolled because the generic helpers above only support flat structs:
|
|
150
|
+
* the primary typeHash must cover the full type string including referenced
|
|
151
|
+
* structs, and struct-typed fields encode as their structHash.
|
|
152
|
+
*
|
|
153
|
+
* @param {number} chainId - EVM chain id (Permit2 domain field)
|
|
154
|
+
* @param {object} message - { permitted: {token, amount}, spender, nonce, deadline, witness: {to, validAfter} }
|
|
155
|
+
* @returns {Buffer} 32-byte digest to sign
|
|
156
|
+
*/
|
|
157
|
+
export function hashPermit2WitnessTransfer(chainId, message) {
|
|
158
|
+
const domainSeparator = hashStruct('EIP712Domain', PERMIT2_DOMAIN_TYPES, {
|
|
159
|
+
name: 'Permit2',
|
|
160
|
+
chainId,
|
|
161
|
+
verifyingContract: PERMIT2_ADDRESS,
|
|
162
|
+
});
|
|
163
|
+
const structHash = keccak256(Buffer.concat([
|
|
164
|
+
keccak256(Buffer.from(PERMIT_WITNESS_TYPE_STRING, 'utf8')),
|
|
165
|
+
hashStruct('TokenPermissions', TOKEN_PERMISSIONS_TYPES, message.permitted),
|
|
166
|
+
encodeValue('address', message.spender),
|
|
167
|
+
encodeValue('uint256', message.nonce),
|
|
168
|
+
encodeValue('uint256', message.deadline),
|
|
169
|
+
hashStruct('Witness', WITNESS_TYPES, message.witness),
|
|
170
|
+
]));
|
|
171
|
+
return keccak256(Buffer.concat([
|
|
172
|
+
Buffer.from([0x19, 0x01]),
|
|
173
|
+
domainSeparator,
|
|
174
|
+
structHash,
|
|
175
|
+
]));
|
|
176
|
+
}
|
|
177
|
+
|
|
113
178
|
// ============= x402 EVM Payment =============
|
|
114
179
|
|
|
115
180
|
/**
|
|
@@ -123,7 +188,13 @@ function getChainId(network) {
|
|
|
123
188
|
}
|
|
124
189
|
|
|
125
190
|
/**
|
|
126
|
-
* Create an x402 payment payload for EVM
|
|
191
|
+
* Create an x402 payment payload for EVM.
|
|
192
|
+
*
|
|
193
|
+
* Routes on requirements.extra.assetTransferMethod: absent or "eip3009" signs
|
|
194
|
+
* an EIP-3009 TransferWithAuthorization (gasless); "permit2-exact" signs a
|
|
195
|
+
* Permit2 PermitWitnessTransferFrom (requires a prior one-time
|
|
196
|
+
* token.approve(PERMIT2_ADDRESS, ...)). Other methods throw so the fallback
|
|
197
|
+
* loop tries the next accepts entry.
|
|
127
198
|
*
|
|
128
199
|
* @param {object} requirements - Parsed PaymentRequirements from 402 response
|
|
129
200
|
* @param {string} privateKeyHex - 32-byte EVM private key as hex
|
|
@@ -135,6 +206,14 @@ export function createEvmPaymentPayload(requirements, privateKeyHex, walletAddre
|
|
|
135
206
|
const chainId = getChainId(requirements.network);
|
|
136
207
|
const extra = requirements.extra || {};
|
|
137
208
|
|
|
209
|
+
const method = extra.assetTransferMethod;
|
|
210
|
+
if (method === 'permit2-exact') {
|
|
211
|
+
return createPermit2ExactPayload(requirements, privateKeyHex, walletAddress, resource);
|
|
212
|
+
}
|
|
213
|
+
if (method && method !== 'eip3009') {
|
|
214
|
+
throw new Error(`Unsupported assetTransferMethod: ${method}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
138
217
|
// Token name and version from requirements.extra (set by server/facilitator)
|
|
139
218
|
const tokenName = extra.name;
|
|
140
219
|
const tokenVersion = extra.version || '1';
|
|
@@ -199,6 +278,71 @@ export function createEvmPaymentPayload(requirements, privateKeyHex, walletAddre
|
|
|
199
278
|
return Buffer.from(JSON.stringify(payload)).toString('base64');
|
|
200
279
|
}
|
|
201
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Create an x402 payment payload via Permit2 PermitWitnessTransferFrom
|
|
283
|
+
* (assetTransferMethod "permit2-exact").
|
|
284
|
+
*
|
|
285
|
+
* The spender is the facilitator's Permit2 proxy advertised in
|
|
286
|
+
* requirements.extra.spenderAddress; the witness binds the transfer to the
|
|
287
|
+
* merchant wallet (requirements.payTo). Wire-format numeric fields are
|
|
288
|
+
* decimal strings.
|
|
289
|
+
*
|
|
290
|
+
* @param {object} requirements - Parsed PaymentRequirements from 402 response
|
|
291
|
+
* @param {string} privateKeyHex - 32-byte EVM private key as hex
|
|
292
|
+
* @param {string} walletAddress - Signer's EVM address
|
|
293
|
+
* @param {string} resource - Original request URL
|
|
294
|
+
* @returns {string} Base64-encoded PaymentPayload for Payment-Signature header
|
|
295
|
+
*/
|
|
296
|
+
export function createPermit2ExactPayload(requirements, privateKeyHex, walletAddress, resource) {
|
|
297
|
+
const chainId = getChainId(requirements.network);
|
|
298
|
+
const extra = requirements.extra || {};
|
|
299
|
+
const spender = extra.spenderAddress;
|
|
300
|
+
if (!spender) {
|
|
301
|
+
throw new Error('spenderAddress missing from requirements.extra (required for permit2-exact)');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const payTo = requirements.pay_to || requirements.payTo;
|
|
305
|
+
const now = Math.floor(Date.now() / 1000);
|
|
306
|
+
// 256-bit random nonce — Permit2 uses an unordered nonce bitmap.
|
|
307
|
+
const nonce = BigInt('0x' + crypto.randomBytes(32).toString('hex')).toString();
|
|
308
|
+
const deadline = String(now + 3600);
|
|
309
|
+
const validAfter = String(now - 60); // allow clock skew
|
|
310
|
+
|
|
311
|
+
const message = {
|
|
312
|
+
permitted: { token: requirements.asset, amount: BigInt(requirements.amount) },
|
|
313
|
+
spender,
|
|
314
|
+
nonce: BigInt(nonce),
|
|
315
|
+
deadline: BigInt(deadline),
|
|
316
|
+
witness: { to: payTo, validAfter: BigInt(validAfter) },
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const msgHash = hashPermit2WitnessTransfer(chainId, message);
|
|
320
|
+
const { r, s, v } = signSecp256k1(msgHash, Buffer.from(privateKeyHex, 'hex'));
|
|
321
|
+
const signature = '0x' + r.toString('hex') + s.toString('hex') + (27 + v).toString(16);
|
|
322
|
+
|
|
323
|
+
const payload = {
|
|
324
|
+
x402Version: 2,
|
|
325
|
+
payload: {
|
|
326
|
+
permit2Authorization: {
|
|
327
|
+
permitted: { token: requirements.asset, amount: String(requirements.amount) },
|
|
328
|
+
from: walletAddress,
|
|
329
|
+
spender,
|
|
330
|
+
nonce,
|
|
331
|
+
deadline,
|
|
332
|
+
witness: { to: payTo, validAfter },
|
|
333
|
+
},
|
|
334
|
+
signature,
|
|
335
|
+
},
|
|
336
|
+
accepted: requirements,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
if (resource) {
|
|
340
|
+
payload.resource = { url: resource };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return Buffer.from(JSON.stringify(payload)).toString('base64');
|
|
344
|
+
}
|
|
345
|
+
|
|
202
346
|
/**
|
|
203
347
|
* Check if a network string is an EVM network.
|
|
204
348
|
*/
|
package/src/x402.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Supports EVM (EIP-3009 on Base) and Solana (SPL TransferChecked).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createEvmPaymentPayload, isEvmNetwork } from './x402-evm.js';
|
|
7
|
+
import { createEvmPaymentPayload, isEvmNetwork, PERMIT2_ADDRESS } from './x402-evm.js';
|
|
8
8
|
import {
|
|
9
9
|
createSvmPaymentPayload,
|
|
10
10
|
isSvmNetwork,
|
|
@@ -55,12 +55,63 @@ function rankRequirements(requirements) {
|
|
|
55
55
|
return ranked;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// ERC-20 allowance(owner, spender) selector for the Permit2 preflight.
|
|
59
|
+
const ALLOWANCE_SELECTOR = '0xdd62ed3e';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check whether `owner` has approved Permit2 to spend at least `amount` of
|
|
63
|
+
* `token`. Permit2-based payments are doomed without sufficient allowance
|
|
64
|
+
* (never approved, or a finite approval now below the payment amount), so
|
|
65
|
+
* skip those entries early instead of burning a failed verify round-trip.
|
|
66
|
+
* Returns true when the allowance is unknown (RPC failure) — let the server
|
|
67
|
+
* decide rather than block a possibly-valid payment.
|
|
68
|
+
*/
|
|
69
|
+
async function hasPermit2Allowance(network, token, owner, amount) {
|
|
70
|
+
const rpc = getEvmRpcUrl(network);
|
|
71
|
+
if (!rpc) return true;
|
|
72
|
+
try {
|
|
73
|
+
const ownerArg = owner.replace(/^0x/, '').toLowerCase().padStart(64, '0');
|
|
74
|
+
const spenderArg = PERMIT2_ADDRESS.replace(/^0x/, '').toLowerCase().padStart(64, '0');
|
|
75
|
+
const resp = await fetch(rpc, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
jsonrpc: '2.0', id: 1,
|
|
80
|
+
method: 'eth_call',
|
|
81
|
+
params: [{ to: token, data: `${ALLOWANCE_SELECTOR}${ownerArg}${spenderArg}` }, 'latest'],
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
const data = await resp.json();
|
|
85
|
+
if (typeof data.result !== 'string') return true;
|
|
86
|
+
return BigInt(data.result) >= BigInt(amount);
|
|
87
|
+
} catch {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
58
92
|
/**
|
|
59
93
|
* Build a payment signature for a single requirement.
|
|
60
94
|
* @returns {string|null} Base64 payment signature, or null on failure
|
|
61
95
|
*/
|
|
62
96
|
async function buildPaymentForRequirement(requirement, exported, url) {
|
|
63
97
|
if (isEvmNetwork(requirement.network)) {
|
|
98
|
+
if ((requirement.extra || {}).assetTransferMethod === 'permit2-exact') {
|
|
99
|
+
const approved = await hasPermit2Allowance(
|
|
100
|
+
requirement.network,
|
|
101
|
+
requirement.asset,
|
|
102
|
+
exported.evm.address,
|
|
103
|
+
requirement.amount,
|
|
104
|
+
);
|
|
105
|
+
if (!approved) {
|
|
106
|
+
console.error(
|
|
107
|
+
`[x402] Skipping ${requirement.network} permit2 option: Permit2 ` +
|
|
108
|
+
`(${PERMIT2_ADDRESS}) allowance for token ${requirement.asset} is ` +
|
|
109
|
+
`missing or below the payment amount (${requirement.amount}). ` +
|
|
110
|
+
`Send approve(${PERMIT2_ADDRESS}, <amount>) from the wallet to enable it.`,
|
|
111
|
+
);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
64
115
|
return createEvmPaymentPayload(
|
|
65
116
|
requirement,
|
|
66
117
|
exported.evm.privateKey,
|
|
@@ -132,7 +183,7 @@ export async function* createPaymentSignatures(response, url, options = {}) {
|
|
|
132
183
|
for (const req of ranked) {
|
|
133
184
|
try {
|
|
134
185
|
const sig = await buildPaymentForRequirement(req, exported, url);
|
|
135
|
-
if (sig) yield { signature: sig, network: req.network };
|
|
186
|
+
if (sig) yield { signature: sig, network: req.network, asset: req.asset };
|
|
136
187
|
} catch {
|
|
137
188
|
// This payment option failed to build, try next
|
|
138
189
|
continue;
|
|
@@ -156,11 +207,48 @@ export async function createPaymentSignature(response, url, options = {}) {
|
|
|
156
207
|
return null;
|
|
157
208
|
}
|
|
158
209
|
|
|
210
|
+
/**
|
|
211
|
+
* x402 payment tokens per EVM network, matching the stablecoins the API
|
|
212
|
+
* advertises in 402 `accepts` entries. `decimals` matters: USDT on BNB Smart
|
|
213
|
+
* Chain uses 18 decimals, unlike the 6-decimal tokens on Base and X Layer.
|
|
214
|
+
*/
|
|
215
|
+
// RPC endpoint per supported x402 EVM network.
|
|
216
|
+
export const EVM_X402_RPCS = {
|
|
217
|
+
'eip155:8453': CHAIN_RPCS.base,
|
|
218
|
+
'eip155:196': CHAIN_RPCS.xlayer,
|
|
219
|
+
'eip155:56': CHAIN_RPCS.bsc,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
function getEvmRpcUrl(network) {
|
|
223
|
+
return EVM_X402_RPCS[network] || null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Known payment tokens per network, in the order servers typically advertise
|
|
227
|
+
// them. A network can accept several stablecoins (BSC accepts four); `decimals`
|
|
228
|
+
// is per token — every BSC stablecoin is an 18-decimal BEP-20 deployment,
|
|
229
|
+
// unlike the 6-decimal tokens on Base and X Layer.
|
|
230
|
+
export const EVM_X402_TOKENS = {
|
|
231
|
+
'eip155:8453': [
|
|
232
|
+
{ token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', symbol: 'USDC', decimals: 6 }, // Base USDC
|
|
233
|
+
],
|
|
234
|
+
'eip155:196': [
|
|
235
|
+
{ token: '0x779Ded0c9e1022225f8E0630b35a9b54bE713736', symbol: 'USDT0', decimals: 6 }, // X Layer USDT0
|
|
236
|
+
],
|
|
237
|
+
'eip155:56': [
|
|
238
|
+
{ token: '0xcE24439F2D9C6a2289F741120FE202248B666666', symbol: 'U', decimals: 18 }, // United Stables
|
|
239
|
+
{ token: '0x8d0d000ee44948fc98c9b98a4fa4921476f08b0d', symbol: 'USD1', decimals: 18 }, // World Liberty Financial USD
|
|
240
|
+
{ token: '0x55d398326f99059fF775485246999027B3197955', symbol: 'USDT', decimals: 18 }, // Tether USD
|
|
241
|
+
{ token: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', symbol: 'USDC', decimals: 18 }, // Binance-Peg USD Coin
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
|
|
159
245
|
/**
|
|
160
246
|
* Check stablecoin balance for x402 payment wallet on the given network.
|
|
247
|
+
* Pass the token contract paid with (`asset`) to check that specific token;
|
|
248
|
+
* otherwise the network's first known token is checked.
|
|
161
249
|
* Returns `{ balance, symbol }` (USD amount + token symbol) or null if check fails.
|
|
162
250
|
*/
|
|
163
|
-
export async function checkX402Balance(network) {
|
|
251
|
+
export async function checkX402Balance(network, asset = null) {
|
|
164
252
|
try {
|
|
165
253
|
const { listWallets, exportWallet: _exportWallet } = await import('./wallet.js');
|
|
166
254
|
const wallets = listWallets();
|
|
@@ -192,13 +280,13 @@ export async function checkX402Balance(network) {
|
|
|
192
280
|
}
|
|
193
281
|
|
|
194
282
|
if (network.startsWith('eip155:')) {
|
|
195
|
-
// Per-network token + RPC. Both tokens are 6-decimals.
|
|
196
283
|
// Default to Base USDC if the network is unknown so existing wallets keep working.
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const { token,
|
|
284
|
+
const tokens = EVM_X402_TOKENS[network] || EVM_X402_TOKENS['eip155:8453'];
|
|
285
|
+
const entry = (asset
|
|
286
|
+
&& tokens.find(t => t.token.toLowerCase() === asset.toLowerCase()))
|
|
287
|
+
|| tokens[0];
|
|
288
|
+
const { token, symbol, decimals } = entry;
|
|
289
|
+
const rpc = getEvmRpcUrl(network) || EVM_X402_RPCS['eip155:8453'];
|
|
202
290
|
const addr = walletInfo.evm.replace('0x', '').toLowerCase().padStart(64, '0');
|
|
203
291
|
const resp = await fetch(rpc, {
|
|
204
292
|
method: 'POST',
|
|
@@ -210,7 +298,7 @@ export async function checkX402Balance(network) {
|
|
|
210
298
|
}),
|
|
211
299
|
});
|
|
212
300
|
const data = await resp.json();
|
|
213
|
-
return { balance: parseInt(data.result, 16) /
|
|
301
|
+
return { balance: parseInt(data.result, 16) / 10 ** decimals, symbol };
|
|
214
302
|
}
|
|
215
303
|
|
|
216
304
|
return null;
|