nansen-cli 1.6.0 → 1.7.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/AGENTS.md +222 -0
- package/README.md +19 -1
- package/SKILL.md +25 -21
- package/TODO.md +17 -0
- package/package.json +1 -1
- package/src/api.js +40 -1
- package/src/cli.js +23 -10
- package/src/crypto.js +215 -0
- package/src/trading.js +1081 -0
- package/src/transfer.js +723 -0
- package/src/wallet.js +764 -0
- package/src/x402-evm.js +207 -0
- package/src/x402-svm.js +474 -0
- package/src/x402.js +205 -0
package/src/x402.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nansen CLI - x402 Auto-Payment Handler
|
|
3
|
+
* Detects 402 responses and auto-signs payment using local wallet.
|
|
4
|
+
* Supports EVM (EIP-3009 on Base) and Solana (SPL TransferChecked).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createEvmPaymentPayload, isEvmNetwork } from './x402-evm.js';
|
|
8
|
+
import {
|
|
9
|
+
createSvmPaymentPayload,
|
|
10
|
+
isSvmNetwork,
|
|
11
|
+
fetchRecentBlockhash,
|
|
12
|
+
getSolanaRpcUrl,
|
|
13
|
+
} from './x402-svm.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse PaymentRequirements from a 402 response.
|
|
17
|
+
* @param {Response} response - The 402 HTTP response
|
|
18
|
+
* @returns {object|null} Parsed requirements or null
|
|
19
|
+
*/
|
|
20
|
+
export function parsePaymentRequirements(response) {
|
|
21
|
+
const header = response.headers.get('payment-required');
|
|
22
|
+
if (!header) return null;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const decoded = JSON.parse(atob(header));
|
|
26
|
+
// V2 format: { accepts: [...], ... }
|
|
27
|
+
if (decoded.accepts && Array.isArray(decoded.accepts)) {
|
|
28
|
+
return decoded.accepts;
|
|
29
|
+
}
|
|
30
|
+
// Can be a single object or array of requirements
|
|
31
|
+
return Array.isArray(decoded) ? decoded : [decoded];
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Rank payment requirements. Prefers EVM (gasless) over Solana.
|
|
39
|
+
* Returns all supported requirements in priority order.
|
|
40
|
+
*/
|
|
41
|
+
function rankRequirements(requirements) {
|
|
42
|
+
const ranked = [];
|
|
43
|
+
// EVM first (gasless for client)
|
|
44
|
+
for (const r of requirements) {
|
|
45
|
+
if (isEvmNetwork(r.network)) ranked.push(r);
|
|
46
|
+
}
|
|
47
|
+
// Then Solana
|
|
48
|
+
for (const r of requirements) {
|
|
49
|
+
if (isSvmNetwork(r.network)) ranked.push(r);
|
|
50
|
+
}
|
|
51
|
+
return ranked;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build a payment signature for a single requirement.
|
|
56
|
+
* @returns {string|null} Base64 payment signature, or null on failure
|
|
57
|
+
*/
|
|
58
|
+
async function buildPaymentForRequirement(requirement, exported, url) {
|
|
59
|
+
if (isEvmNetwork(requirement.network)) {
|
|
60
|
+
return createEvmPaymentPayload(
|
|
61
|
+
requirement,
|
|
62
|
+
exported.evm.privateKey,
|
|
63
|
+
exported.evm.address,
|
|
64
|
+
url,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (isSvmNetwork(requirement.network)) {
|
|
69
|
+
const rpcUrl = getSolanaRpcUrl(requirement.network);
|
|
70
|
+
const blockhash = await fetchRecentBlockhash(rpcUrl);
|
|
71
|
+
return createSvmPaymentPayload(
|
|
72
|
+
requirement,
|
|
73
|
+
exported.solana.privateKey,
|
|
74
|
+
exported.solana.address,
|
|
75
|
+
url,
|
|
76
|
+
blockhash,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generate payment signatures for all viable payment options, in priority order.
|
|
85
|
+
* Yields { signature, network } objects. Caller should try each until one succeeds.
|
|
86
|
+
*
|
|
87
|
+
* @param {Response} response - The 402 HTTP response
|
|
88
|
+
* @param {string} url - The original request URL
|
|
89
|
+
* @param {object} options - { password, walletName }
|
|
90
|
+
* @returns {AsyncGenerator<{ signature: string, network: string }>}
|
|
91
|
+
*/
|
|
92
|
+
export async function* createPaymentSignatures(response, url, options = {}) {
|
|
93
|
+
const requirements = parsePaymentRequirements(response);
|
|
94
|
+
if (!requirements || requirements.length === 0) return;
|
|
95
|
+
|
|
96
|
+
const ranked = rankRequirements(requirements);
|
|
97
|
+
if (ranked.length === 0) return;
|
|
98
|
+
|
|
99
|
+
const password = options.password || process.env.NANSEN_WALLET_PASSWORD;
|
|
100
|
+
if (!password) return;
|
|
101
|
+
|
|
102
|
+
let exportWallet, listWallets;
|
|
103
|
+
try {
|
|
104
|
+
const walletMod = await import('./wallet.js');
|
|
105
|
+
exportWallet = walletMod.exportWallet;
|
|
106
|
+
listWallets = walletMod.listWallets;
|
|
107
|
+
} catch {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const wallets = listWallets();
|
|
112
|
+
if (wallets.wallets.length === 0) return;
|
|
113
|
+
|
|
114
|
+
const walletName = options.walletName || wallets.defaultWallet;
|
|
115
|
+
if (!walletName) return;
|
|
116
|
+
|
|
117
|
+
let exported;
|
|
118
|
+
try {
|
|
119
|
+
exported = exportWallet(walletName, password);
|
|
120
|
+
} catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const req of ranked) {
|
|
125
|
+
try {
|
|
126
|
+
const sig = await buildPaymentForRequirement(req, exported, url);
|
|
127
|
+
if (sig) yield { signature: sig, network: req.network };
|
|
128
|
+
} catch {
|
|
129
|
+
// This payment option failed to build, try next
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Attempt to auto-pay a 402 response (single-shot, returns first viable signature).
|
|
137
|
+
* For fallback support, use createPaymentSignatures() instead.
|
|
138
|
+
*
|
|
139
|
+
* @param {Response} response - The 402 HTTP response
|
|
140
|
+
* @param {string} url - The original request URL
|
|
141
|
+
* @param {object} options - { password, walletName }
|
|
142
|
+
* @returns {string|null} Payment-Signature header value, or null if can't pay
|
|
143
|
+
*/
|
|
144
|
+
export async function createPaymentSignature(response, url, options = {}) {
|
|
145
|
+
for await (const { signature } of createPaymentSignatures(response, url, options)) {
|
|
146
|
+
return signature;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check USDC balance for x402 payment wallet.
|
|
153
|
+
* Returns balance in USD (number) or null if check fails.
|
|
154
|
+
*/
|
|
155
|
+
export async function checkX402Balance(network) {
|
|
156
|
+
try {
|
|
157
|
+
const { listWallets, exportWallet } = await import('./wallet.js');
|
|
158
|
+
const wallets = listWallets();
|
|
159
|
+
if (!wallets.defaultWallet) return null;
|
|
160
|
+
|
|
161
|
+
// Find wallet addresses without needing password
|
|
162
|
+
const walletInfo = wallets.wallets.find(w => w.name === wallets.defaultWallet);
|
|
163
|
+
if (!walletInfo) return null;
|
|
164
|
+
|
|
165
|
+
if (network.startsWith('solana:')) {
|
|
166
|
+
const { getSolanaRpcUrl } = await import('./x402-svm.js');
|
|
167
|
+
const rpcUrl = getSolanaRpcUrl(network);
|
|
168
|
+
const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
|
169
|
+
const resp = await fetch(rpcUrl, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'Content-Type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
jsonrpc: '2.0', id: 1,
|
|
174
|
+
method: 'getTokenAccountsByOwner',
|
|
175
|
+
params: [walletInfo.solana, { mint: USDC_MINT }, { encoding: 'jsonParsed' }],
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
const data = await resp.json();
|
|
179
|
+
const accounts = data.result?.value || [];
|
|
180
|
+
if (accounts.length === 0) return 0;
|
|
181
|
+
return parseFloat(accounts[0].account.data.parsed.info.tokenAmount.uiAmountString || '0');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (network.startsWith('eip155:')) {
|
|
185
|
+
// Base USDC balance check
|
|
186
|
+
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
187
|
+
const addr = walletInfo.evm.replace('0x', '').toLowerCase().padStart(64, '0');
|
|
188
|
+
const resp = await fetch('https://mainnet.base.org', {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: { 'Content-Type': 'application/json' },
|
|
191
|
+
body: JSON.stringify({
|
|
192
|
+
jsonrpc: '2.0', id: 1,
|
|
193
|
+
method: 'eth_call',
|
|
194
|
+
params: [{ to: USDC_BASE, data: `0x70a08231${addr}` }, 'latest'],
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
const data = await resp.json();
|
|
198
|
+
return parseInt(data.result, 16) / 1e6;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return null;
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|