nansen-cli 1.14.0 → 1.16.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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#196](https://github.com/nansen-ai/nansen-cli/pull/196) [`0c286c2`](https://github.com/nansen-ai/nansen-cli/commit/0c286c2d75f977894da8ff18a105aaf21f55f9f2) Thanks [@arein](https://github.com/arein)! - Add Solana WalletConnect support for trading (quote and execute). Solana wallets like Phantom and Solflare can now sign DEX swap transactions via WalletConnect v2.
8
+
9
+ ## 1.15.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#216](https://github.com/nansen-ai/nansen-cli/pull/216) [`5b88241`](https://github.com/nansen-ai/nansen-cli/commit/5b882411134efc5ece44d640adc105c8dd8c5771) Thanks [@TimNooren](https://github.com/TimNooren)! - Unified wallet abstraction: Privy server wallets are first-class citizens.
14
+
15
+ - `wallet create --provider privy` creates EVM + Solana wallets via Privy and stores a local reference
16
+ - All wallet commands (list, show, delete, default, send) work by name regardless of provider
17
+ - Trading (quote + execute) supports Privy wallets with sign-only + Trading API broadcast
18
+ - x402 auto-payment routes through Privy when credentials are configured
19
+
20
+ ### Patch Changes
21
+
22
+ - [#232](https://github.com/nansen-ai/nansen-cli/pull/232) [`443aaad`](https://github.com/nansen-ai/nansen-cli/commit/443aaad15da051ac65e0999b4c4b09436050d0fe) Thanks [@kome12](https://github.com/kome12)! - Remove unsupported chains (zksync, unichain) from CLI
23
+
3
24
  ## 1.14.0
4
25
 
5
26
  ### Minor Changes
package/README.md CHANGED
@@ -55,7 +55,7 @@ Run `nansen schema --pretty` for the full subcommand and field reference.
55
55
 
56
56
  ## Supported Chains
57
57
 
58
- `ethereum` `solana` `base` `bnb` `arbitrum` `polygon` `optimism` `avalanche` `linea` `scroll` `zksync` `mantle` `ronin` `sei` `plasma` `sonic` `unichain` `monad` `hyperevm` `iotaevm`
58
+ `ethereum` `solana` `base` `bnb` `arbitrum` `polygon` `optimism` `avalanche` `linea` `scroll` `mantle` `ronin` `sei` `plasma` `sonic` `monad` `hyperevm` `iotaevm`
59
59
 
60
60
  > Run `nansen schema` to get the current chain list (source of truth).
61
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -21,6 +21,7 @@
21
21
  "test:live": "NANSEN_LIVE_TEST=1 vitest run",
22
22
  "test:trade": "vitest run --config vitest.e2e.config.js src/__tests__/trade.e2e.test.js",
23
23
  "test:send": "vitest run --config vitest.e2e.config.js src/__tests__/send.e2e.test.js",
24
+ "test:privy": "vitest run --config vitest.e2e.config.js src/__tests__/privy.e2e.test.js",
24
25
  "lint": "eslint .",
25
26
  "lint:fix": "eslint . --fix",
26
27
  "changeset": "changeset",
package/src/api.js CHANGED
@@ -504,90 +504,133 @@ export class NansenAPI {
504
504
  const hasManualSignature = !!(this.defaultHeaders['Payment-Signature'] || options.headers?.['Payment-Signature']);
505
505
 
506
506
  if (!hasManualSignature) {
507
- // 1. Try local wallet with fallback across payment networks
507
+ // Determine payment method from default wallet's provider
508
+ let defaultWalletProvider = 'local';
509
+ let defaultWalletName = 'unknown';
508
510
  try {
509
- const { createPaymentSignatures } = await import('./x402.js');
510
- for await (const { signature, network } of createPaymentSignatures(response, url)) {
511
- const paidResponse = await fetch(url, {
512
- method: 'POST',
513
- headers: {
514
- 'Content-Type': 'application/json',
515
- 'X-Client-Type': 'nansen-cli',
516
- 'X-Client-Version': packageVersion,
517
- 'Payment-Signature': signature,
518
- ...this.defaultHeaders,
519
- ...options.headers,
520
- },
521
- body: JSON.stringify(NansenAPI.cleanBody(body)),
522
- });
523
- if (paidResponse.ok) {
524
- const chain = network.startsWith('solana:') ? 'Solana' : 'Base';
525
- console.error(`[x402] Paid via ${chain} USDC`);
526
- // Check remaining balance and warn if low
527
- try {
528
- const { checkX402Balance } = await import('./x402.js');
529
- const balance = await checkX402Balance(network);
530
- if (balance !== null && balance < 0.25) {
531
- console.error(`[x402] Warning: USDC balance low ($${balance.toFixed(2)}). Fund your wallet to avoid interruptions.`);
532
- }
533
- } catch { /* balance check is best-effort */ }
534
- return await paidResponse.json();
535
- }
536
- // This payment option was rejected, try next
511
+ const { getWalletConfig, showWallet } = await import('./wallet.js');
512
+ const walletConfig = getWalletConfig();
513
+ if (walletConfig.defaultWallet) {
514
+ defaultWalletName = walletConfig.defaultWallet;
515
+ const wallet = showWallet(walletConfig.defaultWallet);
516
+ defaultWalletProvider = wallet.provider || 'local';
537
517
  }
538
- } catch { /* local wallet unavailable, try WalletConnect */ }
539
-
540
- // 2. Fall back to WalletConnect (walletconnect-x402.js)
541
- // (local wallet returns early on success above, so we always reach here if it failed)
542
- {
543
- let paymentRequirements;
544
- const paymentHeader = response.headers.get('payment-required');
545
- if (paymentHeader) {
546
- try {
547
- paymentRequirements = JSON.parse(atob(paymentHeader));
548
- } catch {
549
- data.paymentRequiredRaw = paymentHeader;
518
+ } catch (err) {
519
+ if (process.env.DEBUG) console.error(`[x402] Failed to detect wallet provider: ${err.message}`);
520
+ }
521
+
522
+ if (defaultWalletProvider === 'privy') {
523
+ // Default wallet is Privy: sign via Privy
524
+ try {
525
+ const { createPrivyPaymentSignatures } = await import('./privy.js');
526
+ for await (const { signature, network } of createPrivyPaymentSignatures(response, url)) {
527
+ const paidResponse = await fetch(url, {
528
+ method: 'POST',
529
+ headers: {
530
+ 'Content-Type': 'application/json',
531
+ 'X-Client-Type': 'nansen-cli',
532
+ 'X-Client-Version': packageVersion,
533
+ 'Payment-Signature': signature,
534
+ ...this.defaultHeaders,
535
+ ...options.headers,
536
+ },
537
+ body: JSON.stringify(NansenAPI.cleanBody(body)),
538
+ });
539
+ if (paidResponse.ok) {
540
+ console.error(`[x402] Paid via Privy wallet ${defaultWalletName} (${network})`);
541
+ try {
542
+ const { checkX402Balance } = await import('./x402.js');
543
+ const balance = await checkX402Balance(network);
544
+ if (balance !== null && balance < 0.25) {
545
+ console.error(`[x402] Warning: USDC balance low ($${balance.toFixed(2)}). Fund your wallet to avoid interruptions.`);
546
+ }
547
+ } catch { /* balance check is best-effort */ }
548
+ return await paidResponse.json();
549
+ }
550
550
  }
551
+ } catch (privyErr) {
552
+ message = `x402 Privy payment failed: ${privyErr.message}`;
551
553
  }
552
- if (!paymentRequirements && data.paymentRequirements) {
553
- paymentRequirements = data.paymentRequirements;
554
- }
555
-
556
- if (paymentRequirements) {
557
- try {
558
- const { handleX402Payment } = await import('./walletconnect-x402.js');
559
- const paymentSignature = await handleX402Payment(paymentRequirements);
554
+ } else {
555
+ // Local wallet or no wallet: existing local wallet + WalletConnect flow
556
+ // 1. Try local wallet with fallback across payment networks
557
+ try {
558
+ const { createPaymentSignatures } = await import('./x402.js');
559
+ for await (const { signature, network } of createPaymentSignatures(response, url)) {
560
560
  const paidResponse = await fetch(url, {
561
561
  method: 'POST',
562
562
  headers: {
563
563
  'Content-Type': 'application/json',
564
564
  'X-Client-Type': 'nansen-cli',
565
565
  'X-Client-Version': packageVersion,
566
- 'Payment-Signature': paymentSignature,
566
+ 'Payment-Signature': signature,
567
567
  ...this.defaultHeaders,
568
568
  ...options.headers,
569
569
  },
570
570
  body: JSON.stringify(NansenAPI.cleanBody(body)),
571
571
  });
572
572
  if (paidResponse.ok) {
573
+ console.error(`[x402] Paid via local wallet ${defaultWalletName} (${network})`);
574
+ // Check remaining balance and warn if low
575
+ try {
576
+ const { checkX402Balance } = await import('./x402.js');
577
+ const balance = await checkX402Balance(network);
578
+ if (balance !== null && balance < 0.25) {
579
+ console.error(`[x402] Warning: USDC balance low ($${balance.toFixed(2)}). Fund your wallet to avoid interruptions.`);
580
+ }
581
+ } catch { /* balance check is best-effort */ }
573
582
  return await paidResponse.json();
574
583
  }
575
- } catch (x402Err) {
576
- if (!this.apiKey) {
577
- // No API key and no payment wallet guide the user to login rather than
578
- // showing a confusing x402 payment dump they can't act on.
579
- // TODO: full fix would skip x402 entirely when no apiKey is set — see PR #<this PR number>
580
- message = 'No API key configured. Two ways to authenticate:\n' +
581
- ' 1. API key: nansen login --api-key <key> (get key at https://app.nansen.ai/api)\n' +
582
- ' 2. x402 micropayment: nansen wallet create + fund with USDC (no API key needed)';
583
- } else {
584
- message = `x402 auto-payment failed: ${x402Err.message}`;
584
+ // This payment option was rejected, try next
585
+ }
586
+ } catch { /* local wallet unavailable, try WalletConnect */ }
587
+
588
+ // 2. Fall back to WalletConnect (walletconnect-x402.js)
589
+ {
590
+ let paymentRequirements;
591
+ const paymentHeader = response.headers.get('payment-required');
592
+ if (paymentHeader) {
593
+ try {
594
+ paymentRequirements = JSON.parse(atob(paymentHeader));
595
+ } catch {
596
+ data.paymentRequiredRaw = paymentHeader;
585
597
  }
586
598
  }
587
- // Only include raw payment requirements in the error details when the user
588
- // has an API key — for unauthenticated users they add noise, not signal.
589
- if (this.apiKey) {
590
- data.paymentRequirements = paymentRequirements;
599
+ if (!paymentRequirements && data.paymentRequirements) {
600
+ paymentRequirements = data.paymentRequirements;
601
+ }
602
+
603
+ if (paymentRequirements) {
604
+ try {
605
+ const { handleX402Payment } = await import('./walletconnect-x402.js');
606
+ const paymentSignature = await handleX402Payment(paymentRequirements);
607
+ const paidResponse = await fetch(url, {
608
+ method: 'POST',
609
+ headers: {
610
+ 'Content-Type': 'application/json',
611
+ 'X-Client-Type': 'nansen-cli',
612
+ 'X-Client-Version': packageVersion,
613
+ 'Payment-Signature': paymentSignature,
614
+ ...this.defaultHeaders,
615
+ ...options.headers,
616
+ },
617
+ body: JSON.stringify(NansenAPI.cleanBody(body)),
618
+ });
619
+ if (paidResponse.ok) {
620
+ return await paidResponse.json();
621
+ }
622
+ } catch (x402Err) {
623
+ if (!this.apiKey) {
624
+ message = 'No API key configured. Two ways to authenticate:\n' +
625
+ ' 1. API key: nansen login --api-key <key> (get key at https://app.nansen.ai/api)\n' +
626
+ ' 2. x402 micropayment: nansen wallet create + fund with USDC (no API key needed)';
627
+ } else {
628
+ message = `x402 auto-payment failed: ${x402Err.message}`;
629
+ }
630
+ }
631
+ if (this.apiKey) {
632
+ data.paymentRequirements = paymentRequirements;
633
+ }
591
634
  }
592
635
  }
593
636
  }
package/src/chain-ids.js CHANGED
@@ -14,7 +14,6 @@ export const EVM_CHAIN_IDS = {
14
14
  bnb: 56,
15
15
  linea: 59144,
16
16
  scroll: 534352,
17
- zksync: 324,
18
17
  mantle: 5000,
19
18
  };
20
19
 
@@ -25,6 +24,6 @@ export const EVM_CHAIN_IDS = {
25
24
  */
26
25
  export const EVM_CHAINS = [
27
26
  'ethereum', 'arbitrum', 'base', 'bnb', 'polygon', 'optimism',
28
- 'avalanche', 'linea', 'scroll', 'zksync', 'mantle', 'ronin',
29
- 'sei', 'plasma', 'sonic', 'unichain', 'monad', 'hyperevm', 'iotaevm',
27
+ 'avalanche', 'linea', 'scroll', 'mantle', 'ronin',
28
+ 'sei', 'plasma', 'sonic', 'monad', 'hyperevm', 'iotaevm',
30
29
  ];
package/src/cli.js CHANGED
@@ -686,7 +686,7 @@ EXAMPLES:
686
686
  nansen research profiler balance --address 0x... --chain ethereum
687
687
  nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
688
688
 
689
- Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, zksync, mantle, ronin, sei, plasma, sonic, unichain, monad, hyperevm, iotaevm
689
+ Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, mantle, ronin, sei, plasma, sonic, monad, hyperevm, iotaevm
690
690
  Trade chains: solana, base
691
691
  Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
692
692
 
package/src/privy.js ADDED
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Privy Server Wallet Integration
3
+ *
4
+ * Two concerns:
5
+ * 1. PrivyClient - thin REST wrapper for Privy's server wallet API
6
+ * 2. createPrivyPaymentSignatures - x402 auto-payment via Privy signing
7
+ */
8
+
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { parsePaymentRequirements } from "./x402.js";
12
+ import { isEvmNetwork } from "./x402-evm.js";
13
+ import {
14
+ isSvmNetwork,
15
+ getSolanaRpcUrl,
16
+ fetchRecentBlockhash,
17
+ buildUnsignedSvmTransaction,
18
+ } from "./x402-svm.js";
19
+ import {
20
+ buildEIP712TypedData,
21
+ buildPaymentSignatureHeader,
22
+ } from "./walletconnect-x402.js";
23
+
24
+ // ============= Constants =============
25
+
26
+ const PRIVY_BASE_URL = "https://api.privy.io/v1";
27
+
28
+ // ============= PrivyClient =============
29
+
30
+ export class PrivyClient {
31
+ constructor(appId, appSecret) {
32
+ if (!appId || !appSecret) {
33
+ throw new Error(
34
+ "Privy credentials required. Set PRIVY_APP_ID and PRIVY_APP_SECRET environment variables. Get them at https://dashboard.privy.io"
35
+ );
36
+ }
37
+ this.appId = appId;
38
+ this.appSecret = appSecret;
39
+ this.baseUrl = PRIVY_BASE_URL;
40
+ }
41
+
42
+ async _request(method, endpoint, body = null) {
43
+ const auth = Buffer.from(`${this.appId}:${this.appSecret}`).toString(
44
+ "base64"
45
+ );
46
+ const headers = {
47
+ Authorization: `Basic ${auth}`,
48
+ "privy-app-id": this.appId,
49
+ "Content-Type": "application/json",
50
+ };
51
+
52
+ const opts = { method, headers };
53
+ if (body) opts.body = JSON.stringify(body);
54
+
55
+ const response = await fetch(`${this.baseUrl}${endpoint}`, opts);
56
+
57
+ if (!response.ok) {
58
+ let msg = `Privy API error: ${response.status}`;
59
+ try {
60
+ const data = await response.json();
61
+ msg = data.message || data.error || msg;
62
+ } catch { /* non-JSON error response (e.g. 502 from CDN) */ }
63
+ throw new Error(msg);
64
+ }
65
+
66
+ return await response.json();
67
+ }
68
+
69
+ async createWallet(chainType = "ethereum") {
70
+ return this._request("POST", "/wallets", { chain_type: chainType });
71
+ }
72
+
73
+ async listWallets() {
74
+ return this._request("GET", "/wallets");
75
+ }
76
+
77
+ async getWallet(walletId) {
78
+ return this._request("GET", `/wallets/${walletId}`);
79
+ }
80
+
81
+ async deleteWallet(walletId) {
82
+ return this._request("DELETE", `/wallets/${walletId}`);
83
+ }
84
+
85
+ async sendTransaction(walletId, { to, value, chainId, data: txData }) {
86
+ const caip2 = `eip155:${chainId}`;
87
+ return this._request("POST", `/wallets/${walletId}/rpc`, {
88
+ method: "eth_sendTransaction",
89
+ caip2,
90
+ params: {
91
+ transaction: {
92
+ to,
93
+ value,
94
+ ...(txData ? { data: txData } : {}),
95
+ },
96
+ },
97
+ });
98
+ }
99
+
100
+ async ethSignTypedDataV4(walletId, typedData) {
101
+ // Privy uses snake_case "primary_type" instead of "primaryType"
102
+ const privyTypedData = { ...typedData };
103
+ if (privyTypedData.primaryType && !privyTypedData.primary_type) {
104
+ privyTypedData.primary_type = privyTypedData.primaryType;
105
+ delete privyTypedData.primaryType;
106
+ }
107
+ return this._request("POST", `/wallets/${walletId}/rpc`, {
108
+ method: "eth_signTypedData_v4",
109
+ params: { typed_data: privyTypedData },
110
+ });
111
+ }
112
+
113
+ async signEvmTransaction(walletId, transaction) {
114
+ return this._request("POST", `/wallets/${walletId}/rpc`, {
115
+ method: "eth_signTransaction",
116
+ params: { transaction },
117
+ });
118
+ }
119
+
120
+ async signSolanaTransaction(walletId, transactionBase64) {
121
+ return this._request("POST", `/wallets/${walletId}/rpc`, {
122
+ method: "signTransaction",
123
+ chain_type: "solana",
124
+ params: { transaction: transactionBase64, encoding: "base64" },
125
+ });
126
+ }
127
+
128
+ }
129
+
130
+ // ============= Helpers =============
131
+
132
+ function getClient() {
133
+ return new PrivyClient(process.env.PRIVY_APP_ID, process.env.PRIVY_APP_SECRET);
134
+ }
135
+
136
+ /**
137
+ * Create both an EVM and Solana wallet via Privy and store a local reference file.
138
+ * Mirrors createWallet() in wallet.js but for Privy server wallets.
139
+ */
140
+ export async function createPrivyWalletPair(name) {
141
+ const WALLET_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
142
+ if (!name || !WALLET_NAME_RE.test(name)) {
143
+ throw new Error("Wallet name must be 1-64 characters: letters, numbers, hyphens, underscores only");
144
+ }
145
+
146
+ const walletsDir = path.join(process.env.HOME || process.env.USERPROFILE || "", ".nansen", "wallets");
147
+ const walletFile = path.join(walletsDir, `${name}.json`);
148
+
149
+ if (fs.existsSync(walletFile)) {
150
+ throw new Error(`Wallet "${name}" already exists`);
151
+ }
152
+
153
+ const client = getClient();
154
+ // Create wallets sequentially so we can clean up on partial failure
155
+ const evmResult = await client.createWallet("ethereum");
156
+ let solanaResult;
157
+ try {
158
+ solanaResult = await client.createWallet("solana");
159
+ } catch (err) {
160
+ // Clean up the EVM wallet we just created to avoid orphans
161
+ try { await client.deleteWallet(evmResult.id); } catch { /* best effort */ }
162
+ throw err;
163
+ }
164
+
165
+ const walletData = {
166
+ name,
167
+ provider: "privy",
168
+ evm: { privyWalletId: evmResult.id, address: evmResult.address },
169
+ solana: { privyWalletId: solanaResult.id, address: solanaResult.address },
170
+ createdAt: new Date().toISOString(),
171
+ };
172
+
173
+ if (!fs.existsSync(walletsDir)) {
174
+ fs.mkdirSync(walletsDir, { mode: 0o700, recursive: true });
175
+ }
176
+
177
+ // Write config before wallet file so a crash doesn't leave an orphan without a default entry
178
+ const configPath = path.join(walletsDir, "config.json");
179
+ let config = { defaultWallet: null, passwordHash: null };
180
+ if (fs.existsSync(configPath)) {
181
+ config = JSON.parse(fs.readFileSync(configPath, "utf8"));
182
+ }
183
+ if (!config.defaultWallet) {
184
+ config.defaultWallet = name;
185
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
186
+ }
187
+
188
+ fs.writeFileSync(walletFile, JSON.stringify(walletData, null, 2), { mode: 0o600 });
189
+
190
+ return walletData;
191
+ }
192
+
193
+ // ============= x402 Payment Signing =============
194
+
195
+ /**
196
+ * Resolve the EVM wallet for x402 payments.
197
+ * Priority: PRIVY_WALLET_ID env > default local wallet's privyWalletId > first Privy EVM wallet.
198
+ */
199
+ async function getPrivyEvmWallet(client) {
200
+ if (process.env.PRIVY_WALLET_ID) {
201
+ return client.getWallet(process.env.PRIVY_WALLET_ID);
202
+ }
203
+
204
+ // Prefer the wallet referenced by the local default wallet file
205
+ try {
206
+ const walletsDir = path.join(process.env.HOME || process.env.USERPROFILE || "", ".nansen", "wallets");
207
+ const configPath = path.join(walletsDir, "config.json");
208
+ if (fs.existsSync(configPath)) {
209
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
210
+ if (config.defaultWallet) {
211
+ const walletFile = path.join(walletsDir, `${config.defaultWallet}.json`);
212
+ if (fs.existsSync(walletFile)) {
213
+ const data = JSON.parse(fs.readFileSync(walletFile, "utf8"));
214
+ if (data.provider === "privy" && data.evm?.privyWalletId) {
215
+ return client.getWallet(data.evm.privyWalletId);
216
+ }
217
+ }
218
+ }
219
+ }
220
+ } catch (err) {
221
+ // Fall through to list-based detection
222
+ if (process.env.DEBUG) console.error(`[x402] Default wallet lookup failed: ${err.message}`);
223
+ }
224
+
225
+ const result = await client.listWallets();
226
+ const wallets = result.data || result.wallets || result;
227
+ if (!Array.isArray(wallets)) return null;
228
+ return wallets.find((w) => w.chain_type === "ethereum") || null;
229
+ }
230
+
231
+ /**
232
+ * Resolve the Solana wallet for x402 payments.
233
+ * Priority: default local wallet's solana.privyWalletId > first Privy Solana wallet.
234
+ */
235
+ async function getPrivySolanaWallet(client) {
236
+ try {
237
+ const walletsDir = path.join(process.env.HOME || process.env.USERPROFILE || "", ".nansen", "wallets");
238
+ const configPath = path.join(walletsDir, "config.json");
239
+ if (fs.existsSync(configPath)) {
240
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
241
+ if (config.defaultWallet) {
242
+ const walletFile = path.join(walletsDir, `${config.defaultWallet}.json`);
243
+ if (fs.existsSync(walletFile)) {
244
+ const data = JSON.parse(fs.readFileSync(walletFile, "utf8"));
245
+ if (data.provider === "privy" && data.solana?.privyWalletId) {
246
+ return client.getWallet(data.solana.privyWalletId);
247
+ }
248
+ }
249
+ }
250
+ }
251
+ } catch (err) {
252
+ if (process.env.DEBUG) console.error(`[x402] Solana wallet lookup failed: ${err.message}`);
253
+ }
254
+
255
+ const result = await client.listWallets();
256
+ const wallets = result.data || result.wallets || result;
257
+ if (!Array.isArray(wallets)) return null;
258
+ return wallets.find((w) => w.chain_type === "solana") || null;
259
+ }
260
+
261
+ /**
262
+ * Generate payment signatures for x402 using Privy server wallets.
263
+ * Same yield contract as createPaymentSignatures() in x402.js: { signature, network }
264
+ *
265
+ * @param {Response} response - The 402 HTTP response
266
+ * @param {string} url - The original request URL
267
+ * @returns {AsyncGenerator<{ signature: string, network: string }>}
268
+ */
269
+ export async function* createPrivyPaymentSignatures(response, url) {
270
+ const requirements = parsePaymentRequirements(response);
271
+ if (!requirements || requirements.length === 0) return;
272
+
273
+ const client = getClient();
274
+
275
+ // EVM requirements
276
+ const evmRequirements = requirements.filter((r) => isEvmNetwork(r.network));
277
+ if (evmRequirements.length > 0) {
278
+ const evmWallet = await getPrivyEvmWallet(client);
279
+ if (evmWallet) {
280
+ for (const requirement of evmRequirements) {
281
+ try {
282
+ const typedData = buildEIP712TypedData({
283
+ fromAddress: evmWallet.address,
284
+ requirement,
285
+ });
286
+
287
+ const signResult = await client.ethSignTypedDataV4(
288
+ evmWallet.id,
289
+ typedData
290
+ );
291
+ const signature = signResult.data?.signature || signResult.signature;
292
+
293
+ const authorization = {
294
+ from: evmWallet.address,
295
+ to: requirement.payTo,
296
+ value: (requirement.amount || requirement.maxAmountRequired).toString(),
297
+ validAfter: typedData.message.validAfter.toString(),
298
+ validBefore: typedData.message.validBefore.toString(),
299
+ nonce: typedData.message.nonce,
300
+ };
301
+
302
+ const header = buildPaymentSignatureHeader({
303
+ signature,
304
+ authorization,
305
+ resource: { url, description: "", mimeType: "" },
306
+ accepted: requirement,
307
+ });
308
+
309
+ yield { signature: header, network: requirement.network };
310
+ } catch (err) {
311
+ console.error(`[x402] Privy EVM signing failed for ${requirement.network}: ${err.message}`);
312
+ continue;
313
+ }
314
+ }
315
+ } else {
316
+ console.error('[x402] No Privy EVM wallet found for payment signing');
317
+ }
318
+ }
319
+
320
+ // Solana requirements
321
+ const svmRequirements = requirements.filter((r) => isSvmNetwork(r.network));
322
+ if (svmRequirements.length > 0) {
323
+ const solWallet = await getPrivySolanaWallet(client);
324
+ if (solWallet) {
325
+ for (const requirement of svmRequirements) {
326
+ try {
327
+ const rpcUrl = getSolanaRpcUrl(requirement.network);
328
+ const recentBlockhash = await fetchRecentBlockhash(rpcUrl);
329
+
330
+ const { txBase64 } = buildUnsignedSvmTransaction(
331
+ requirement,
332
+ solWallet.address,
333
+ recentBlockhash,
334
+ );
335
+
336
+ const signResult = await client.signSolanaTransaction(solWallet.id, txBase64);
337
+ const signedTx = signResult.data?.signed_transaction || signResult.signed_transaction;
338
+
339
+ const payload = {
340
+ x402Version: 2,
341
+ payload: { transaction: signedTx },
342
+ accepted: requirement,
343
+ };
344
+ if (url) {
345
+ payload.resource = { url };
346
+ }
347
+
348
+ const header = Buffer.from(JSON.stringify(payload)).toString("base64");
349
+ yield { signature: header, network: requirement.network };
350
+ } catch (err) {
351
+ console.error(`[x402] Privy Solana signing failed for ${requirement.network}: ${err.message}`);
352
+ continue;
353
+ }
354
+ }
355
+ } else {
356
+ console.error('[x402] No Privy Solana wallet found for payment signing');
357
+ }
358
+ }
359
+ }