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 +21 -0
- package/README.md +1 -1
- package/package.json +2 -1
- package/src/api.js +107 -64
- package/src/chain-ids.js +2 -3
- package/src/cli.js +1 -1
- package/src/privy.js +359 -0
- package/src/schema.json +42 -2
- package/src/trading.js +290 -108
- package/src/transfer.js +151 -26
- package/src/wallet.js +112 -39
- package/src/walletconnect-trading.js +73 -9
- package/src/x402-svm.js +43 -24
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` `
|
|
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.
|
|
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
|
-
//
|
|
507
|
+
// Determine payment method from default wallet's provider
|
|
508
|
+
let defaultWalletProvider = 'local';
|
|
509
|
+
let defaultWalletName = 'unknown';
|
|
508
510
|
try {
|
|
509
|
-
const {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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 {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
{
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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':
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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', '
|
|
29
|
-
'sei', 'plasma', 'sonic', '
|
|
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,
|
|
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
|
+
}
|