nansen-cli 1.16.1 → 1.17.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 +24 -0
- package/package.json +1 -1
- package/src/api.js +69 -71
- package/src/cli.js +5 -0
- package/src/rpc-urls.js +29 -0
- package/src/schema.json +237 -1447
- package/src/trading.js +26 -12
- package/src/transfer.js +1 -11
- package/src/wallet.js +2 -1
- package/src/x402.js +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.17.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#279](https://github.com/nansen-ai/nansen-cli/pull/279) [`174a3d6`](https://github.com/nansen-ai/nansen-cli/commit/174a3d612b3f198c5e5b979c269d896f9d906704) Thanks [@kome12](https://github.com/kome12)! - Add `nansen account` command to verify API key and check credit balance
|
|
8
|
+
|
|
9
|
+
Users can now run `nansen account` to confirm their API key is valid and see
|
|
10
|
+
their current plan and remaining credits — without consuming any credits.
|
|
11
|
+
|
|
12
|
+
This calls the new `GET /api/v1/account` endpoint (ECINT-6365).
|
|
13
|
+
|
|
14
|
+
- [#234](https://github.com/nansen-ai/nansen-cli/pull/234) [`10a5ced`](https://github.com/nansen-ai/nansen-cli/commit/10a5ced9f1f751666d034cecfc431e335183741c) Thanks [@kome12](https://github.com/kome12)! - Reduced schema.json to a minimal format (~66% smaller).
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- [#272](https://github.com/nansen-ai/nansen-cli/pull/272) [`50213c1`](https://github.com/nansen-ai/nansen-cli/commit/50213c1d82375a153aa1bad3a53bbd7059cd9f5b) Thanks [@TimNooren](https://github.com/TimNooren)! - fix: show human-readable error when trade fails due to insufficient ETH
|
|
19
|
+
|
|
20
|
+
When a wallet has no ETH and a trade is attempted, the raw Ethereum RPC
|
|
21
|
+
error ("insufficient funds for gas \* price + value: ... have 0 want
|
|
22
|
+
400000000000000 (supplied gas 600000000)") is now translated into a
|
|
23
|
+
user-friendly message showing amounts in ETH with a funding hint, e.g.
|
|
24
|
+
"Insufficient ETH: wallet has 0.000000 ETH but this trade needs ~0.000400
|
|
25
|
+
ETH (amount + gas). Send ETH to 0x... before trading."
|
|
26
|
+
|
|
3
27
|
## 1.16.1
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -418,6 +418,52 @@ export class NansenAPI {
|
|
|
418
418
|
);
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Retry a POST request with a payment signature.
|
|
423
|
+
* Returns parsed JSON if the paid request succeeds, or null if still rejected.
|
|
424
|
+
* Logs the payment and warns about low balance when walletLabel and network are given.
|
|
425
|
+
*
|
|
426
|
+
* @param {string} signature - Payment-Signature header value
|
|
427
|
+
* @param {string|null} walletLabel - Display label for logging, e.g. "local wallet alice"
|
|
428
|
+
* @param {string|null} network - x402 network string for balance check, e.g. "eip155:8453"
|
|
429
|
+
* @param {string} url - Request URL
|
|
430
|
+
* @param {object} body - Request body (will be cleaned)
|
|
431
|
+
* @param {object} [options={}] - Request options (may include .headers)
|
|
432
|
+
* @returns {Promise<object|null>} Parsed JSON on success, null if rejected
|
|
433
|
+
*
|
|
434
|
+
* TODO: full fix — extract the entire x402 provider dispatch from request() into
|
|
435
|
+
* an attemptX402Payment() method so adding a new payment provider only requires
|
|
436
|
+
* touching that one method, not hunting inside the retry loop.
|
|
437
|
+
*/
|
|
438
|
+
async _x402Retry(signature, walletLabel, network, url, body, options = {}) {
|
|
439
|
+
const paidResponse = await fetch(url, {
|
|
440
|
+
method: 'POST',
|
|
441
|
+
headers: {
|
|
442
|
+
'Content-Type': 'application/json',
|
|
443
|
+
'X-Client-Type': 'nansen-cli',
|
|
444
|
+
'X-Client-Version': packageVersion,
|
|
445
|
+
'Payment-Signature': signature,
|
|
446
|
+
...this.defaultHeaders,
|
|
447
|
+
...options.headers,
|
|
448
|
+
},
|
|
449
|
+
body: JSON.stringify(NansenAPI.cleanBody(body)),
|
|
450
|
+
});
|
|
451
|
+
if (!paidResponse.ok) return null;
|
|
452
|
+
if (walletLabel) {
|
|
453
|
+
console.error(`[x402] Paid via ${walletLabel}${network ? ` (${network})` : ''}`);
|
|
454
|
+
}
|
|
455
|
+
if (network) {
|
|
456
|
+
try {
|
|
457
|
+
const { checkX402Balance } = await import('./x402.js');
|
|
458
|
+
const balance = await checkX402Balance(network);
|
|
459
|
+
if (balance !== null && balance < 0.25) {
|
|
460
|
+
console.error(`[x402] Warning: USDC balance low ($${balance.toFixed(2)}). Fund your wallet to avoid interruptions.`);
|
|
461
|
+
}
|
|
462
|
+
} catch { /* balance check is best-effort */ }
|
|
463
|
+
}
|
|
464
|
+
return await paidResponse.json();
|
|
465
|
+
}
|
|
466
|
+
|
|
421
467
|
async request(endpoint, body = {}, options = {}) {
|
|
422
468
|
const url = `${this.baseUrl}${endpoint}`;
|
|
423
469
|
const { maxRetries, baseDelayMs, maxDelayMs, retryOnStatus } = this.retryOptions;
|
|
@@ -439,17 +485,19 @@ export class NansenAPI {
|
|
|
439
485
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
440
486
|
let response;
|
|
441
487
|
try {
|
|
488
|
+
const method = options.method || 'POST';
|
|
489
|
+
const isGet = method === 'GET';
|
|
442
490
|
response = await fetch(url, {
|
|
443
|
-
method
|
|
491
|
+
method,
|
|
444
492
|
headers: {
|
|
445
|
-
'Content-Type': 'application/json',
|
|
493
|
+
...(!isGet && { 'Content-Type': 'application/json' }),
|
|
446
494
|
'X-Client-Type': 'nansen-cli',
|
|
447
495
|
'X-Client-Version': packageVersion,
|
|
448
496
|
...(this.apiKey ? { 'apikey': this.apiKey } : {}),
|
|
449
497
|
...this.defaultHeaders,
|
|
450
498
|
...options.headers
|
|
451
499
|
},
|
|
452
|
-
body: JSON.stringify(NansenAPI.cleanBody(body))
|
|
500
|
+
...(!isGet && { body: JSON.stringify(NansenAPI.cleanBody(body)) })
|
|
453
501
|
});
|
|
454
502
|
} catch (err) {
|
|
455
503
|
// Network-level errors - retry these too
|
|
@@ -524,29 +572,8 @@ export class NansenAPI {
|
|
|
524
572
|
try {
|
|
525
573
|
const { createPrivyPaymentSignatures } = await import('./privy.js');
|
|
526
574
|
for await (const { signature, network } of createPrivyPaymentSignatures(response, url)) {
|
|
527
|
-
const
|
|
528
|
-
|
|
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
|
-
}
|
|
575
|
+
const result = await this._x402Retry(signature, `Privy wallet ${defaultWalletName}`, network, url, body, options);
|
|
576
|
+
if (result !== null) return result;
|
|
550
577
|
}
|
|
551
578
|
} catch (privyErr) {
|
|
552
579
|
message = `x402 Privy payment failed: ${privyErr.message}`;
|
|
@@ -557,30 +584,8 @@ export class NansenAPI {
|
|
|
557
584
|
try {
|
|
558
585
|
const { createPaymentSignatures } = await import('./x402.js');
|
|
559
586
|
for await (const { signature, network } of createPaymentSignatures(response, url)) {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
headers: {
|
|
563
|
-
'Content-Type': 'application/json',
|
|
564
|
-
'X-Client-Type': 'nansen-cli',
|
|
565
|
-
'X-Client-Version': packageVersion,
|
|
566
|
-
'Payment-Signature': signature,
|
|
567
|
-
...this.defaultHeaders,
|
|
568
|
-
...options.headers,
|
|
569
|
-
},
|
|
570
|
-
body: JSON.stringify(NansenAPI.cleanBody(body)),
|
|
571
|
-
});
|
|
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 */ }
|
|
582
|
-
return await paidResponse.json();
|
|
583
|
-
}
|
|
587
|
+
const result = await this._x402Retry(signature, `local wallet ${defaultWalletName}`, network, url, body, options);
|
|
588
|
+
if (result !== null) return result;
|
|
584
589
|
// This payment option was rejected, try next
|
|
585
590
|
}
|
|
586
591
|
} catch { /* local wallet unavailable, try WalletConnect */ }
|
|
@@ -604,21 +609,8 @@ export class NansenAPI {
|
|
|
604
609
|
try {
|
|
605
610
|
const { handleX402Payment } = await import('./walletconnect-x402.js');
|
|
606
611
|
const paymentSignature = await handleX402Payment(paymentRequirements);
|
|
607
|
-
const
|
|
608
|
-
|
|
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
|
-
}
|
|
612
|
+
const result = await this._x402Retry(paymentSignature, 'WalletConnect', null, url, body, options);
|
|
613
|
+
if (result !== null) return result;
|
|
622
614
|
} catch (x402Err) {
|
|
623
615
|
if (!this.apiKey) {
|
|
624
616
|
message = 'No API key configured. Two ways to authenticate:\n' +
|
|
@@ -674,6 +666,12 @@ export class NansenAPI {
|
|
|
674
666
|
throw lastError;
|
|
675
667
|
}
|
|
676
668
|
|
|
669
|
+
// ============= Account Endpoint =============
|
|
670
|
+
|
|
671
|
+
async getAccount() {
|
|
672
|
+
return this.request('/api/v1/account', {}, { method: 'GET', cache: false });
|
|
673
|
+
}
|
|
674
|
+
|
|
677
675
|
// ============= Smart Money Endpoints =============
|
|
678
676
|
|
|
679
677
|
async smartMoneyNetflow(params = {}) {
|
|
@@ -1145,7 +1143,7 @@ export class NansenAPI {
|
|
|
1145
1143
|
|
|
1146
1144
|
async pmOhlcv(params = {}) {
|
|
1147
1145
|
const { marketId, sort, pagination } = params;
|
|
1148
|
-
if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
|
|
1146
|
+
if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
|
|
1149
1147
|
return this.request('/api/v1/prediction-market/ohlcv', {
|
|
1150
1148
|
market_id: marketId,
|
|
1151
1149
|
sort,
|
|
@@ -1155,7 +1153,7 @@ export class NansenAPI {
|
|
|
1155
1153
|
|
|
1156
1154
|
async pmOrderbook(params = {}) {
|
|
1157
1155
|
const { marketId, pagination } = params;
|
|
1158
|
-
if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
|
|
1156
|
+
if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
|
|
1159
1157
|
return this.request('/api/v1/prediction-market/orderbook', {
|
|
1160
1158
|
market_id: marketId,
|
|
1161
1159
|
pagination
|
|
@@ -1164,7 +1162,7 @@ export class NansenAPI {
|
|
|
1164
1162
|
|
|
1165
1163
|
async pmTopHolders(params = {}) {
|
|
1166
1164
|
const { marketId, sort, pagination } = params;
|
|
1167
|
-
if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
|
|
1165
|
+
if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
|
|
1168
1166
|
return this.request('/api/v1/prediction-market/top-holders', {
|
|
1169
1167
|
market_id: marketId,
|
|
1170
1168
|
sort,
|
|
@@ -1174,7 +1172,7 @@ export class NansenAPI {
|
|
|
1174
1172
|
|
|
1175
1173
|
async pmTradesByMarket(params = {}) {
|
|
1176
1174
|
const { marketId, pagination } = params;
|
|
1177
|
-
if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
|
|
1175
|
+
if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
|
|
1178
1176
|
return this.request('/api/v1/prediction-market/trades-by-market', {
|
|
1179
1177
|
market_id: marketId,
|
|
1180
1178
|
pagination
|
|
@@ -1214,7 +1212,7 @@ export class NansenAPI {
|
|
|
1214
1212
|
|
|
1215
1213
|
async pmPnlByMarket(params = {}) {
|
|
1216
1214
|
const { marketId, pagination } = params;
|
|
1217
|
-
if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
|
|
1215
|
+
if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
|
|
1218
1216
|
return this.request('/api/v1/prediction-market/pnl-by-market', {
|
|
1219
1217
|
market_id: marketId,
|
|
1220
1218
|
pagination
|
|
@@ -1234,7 +1232,7 @@ export class NansenAPI {
|
|
|
1234
1232
|
|
|
1235
1233
|
async pmPositionDetail(params = {}) {
|
|
1236
1234
|
const { marketId, pagination } = params;
|
|
1237
|
-
if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
|
|
1235
|
+
if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
|
|
1238
1236
|
return this.request('/api/v1/prediction-market/position-detail', {
|
|
1239
1237
|
market_id: marketId,
|
|
1240
1238
|
pagination
|
package/src/cli.js
CHANGED
|
@@ -670,6 +670,7 @@ COMMANDS:
|
|
|
670
670
|
research smart-money, profiler, token, search, perp, portfolio, points
|
|
671
671
|
trade quote, execute
|
|
672
672
|
wallet create, list, show, export, default, delete, forget-password
|
|
673
|
+
account Show API key status, plan, and remaining credits
|
|
673
674
|
login Save API key (--api-key <key> or NANSEN_API_KEY env var)
|
|
674
675
|
logout Remove saved API key
|
|
675
676
|
schema JSON schema for all commands (use "nansen schema <cmd>" for one)
|
|
@@ -757,6 +758,10 @@ export function buildCommands(deps = {}) {
|
|
|
757
758
|
} = deps;
|
|
758
759
|
|
|
759
760
|
const cmds = {
|
|
761
|
+
'account': async (_args, apiInstance, _flags, _options) => {
|
|
762
|
+
return apiInstance.getAccount();
|
|
763
|
+
},
|
|
764
|
+
|
|
760
765
|
'login': async (args, apiInstance, flags, options) => {
|
|
761
766
|
if (flags.help || flags.h) {
|
|
762
767
|
log('nansen login - Save your Nansen API key\n');
|
package/src/rpc-urls.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for chain RPC endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Both trading.js and transfer.js read from here so that:
|
|
5
|
+
* (a) adding a new chain only requires one edit, and
|
|
6
|
+
* (b) env-var overrides work consistently across all commands.
|
|
7
|
+
*
|
|
8
|
+
* Override env vars:
|
|
9
|
+
* NANSEN_EVM_RPC Custom Ethereum RPC (also used as generic EVM fallback)
|
|
10
|
+
* NANSEN_BASE_RPC Custom Base RPC
|
|
11
|
+
* NANSEN_SOLANA_RPC Custom Solana RPC
|
|
12
|
+
*
|
|
13
|
+
* Backward-compat aliases (deprecated — prefer the forms above):
|
|
14
|
+
* NANSEN_RPC_BASE Old name for NANSEN_BASE_RPC; trading.js previously read this
|
|
15
|
+
* but transfer.js never did, so the two commands were inconsistent.
|
|
16
|
+
* Both forms are now accepted here so existing .env files keep
|
|
17
|
+
* working while new code uses the standardised NANSEN_BASE_RPC name.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const DEFAULT_EVM_RPC = 'https://eth.public-rpc.com';
|
|
21
|
+
const DEFAULT_BASE_RPC = 'https://mainnet.base.org';
|
|
22
|
+
const DEFAULT_SOLANA_RPC = 'https://api.mainnet-beta.solana.com';
|
|
23
|
+
|
|
24
|
+
export const CHAIN_RPCS = {
|
|
25
|
+
ethereum: process.env.NANSEN_EVM_RPC || DEFAULT_EVM_RPC,
|
|
26
|
+
evm: process.env.NANSEN_EVM_RPC || DEFAULT_EVM_RPC, // generic EVM fallback
|
|
27
|
+
base: process.env.NANSEN_BASE_RPC || process.env.NANSEN_RPC_BASE || DEFAULT_BASE_RPC,
|
|
28
|
+
solana: process.env.NANSEN_SOLANA_RPC || DEFAULT_SOLANA_RPC,
|
|
29
|
+
};
|