nansen-cli 1.20.0 → 1.22.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 +26 -0
- package/README.md +26 -1
- package/package.json +1 -1
- package/skills/nansen-trading/SKILL.md +14 -2
- package/skills/nansen-wallet-manager/SKILL.md +12 -0
- package/src/api.js +6 -5
- package/src/cli.js +36 -22
- package/src/commands/agent.js +344 -0
- package/src/cost-cache.js +67 -0
- package/src/schema.json +90 -1
- package/src/trading.js +129 -5
- package/src/wallet.js +7 -0
package/src/schema.json
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"portfolio": {
|
|
7
7
|
"subcommands": {
|
|
8
8
|
"defi": {
|
|
9
|
+
"endpoint": "/api/v1/portfolio/defi-holdings",
|
|
9
10
|
"description": "DeFi holdings across protocols",
|
|
10
11
|
"options": {
|
|
11
12
|
"wallet": {
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
"smart-money": {
|
|
20
21
|
"subcommands": {
|
|
21
22
|
"netflow": {
|
|
23
|
+
"endpoint": "/api/v1/smart-money/netflow",
|
|
22
24
|
"description": "Net capital flows (inflows vs outflows)",
|
|
23
25
|
"options": {
|
|
24
26
|
"chain": {
|
|
@@ -27,6 +29,7 @@
|
|
|
27
29
|
}
|
|
28
30
|
},
|
|
29
31
|
"dex-trades": {
|
|
32
|
+
"endpoint": "/api/v1/smart-money/dex-trades",
|
|
30
33
|
"description": "Real-time DEX trading activity",
|
|
31
34
|
"options": {
|
|
32
35
|
"chain": {
|
|
@@ -35,12 +38,15 @@
|
|
|
35
38
|
}
|
|
36
39
|
},
|
|
37
40
|
"perp-trades": {
|
|
41
|
+
"endpoint": "/api/v1/smart-money/perp-trades",
|
|
38
42
|
"description": "Perpetual trading on Hyperliquid"
|
|
39
43
|
},
|
|
40
44
|
"dcas": {
|
|
45
|
+
"endpoint": "/api/v1/smart-money/dcas",
|
|
41
46
|
"description": "DCA strategies on Jupiter"
|
|
42
47
|
},
|
|
43
48
|
"holdings": {
|
|
49
|
+
"endpoint": "/api/v1/smart-money/holdings",
|
|
44
50
|
"description": "Aggregated token balances",
|
|
45
51
|
"options": {
|
|
46
52
|
"chain": {
|
|
@@ -49,6 +55,7 @@
|
|
|
49
55
|
}
|
|
50
56
|
},
|
|
51
57
|
"historical-holdings": {
|
|
58
|
+
"endpoint": "/api/v1/smart-money/historical-holdings",
|
|
52
59
|
"description": "Historical holdings over time",
|
|
53
60
|
"options": {
|
|
54
61
|
"chain": {
|
|
@@ -65,6 +72,7 @@
|
|
|
65
72
|
"profiler": {
|
|
66
73
|
"subcommands": {
|
|
67
74
|
"transactions": {
|
|
75
|
+
"endpoint": "/api/v1/profiler/address/transactions",
|
|
68
76
|
"description": "Transaction history",
|
|
69
77
|
"options": {
|
|
70
78
|
"address": {
|
|
@@ -79,6 +87,7 @@
|
|
|
79
87
|
}
|
|
80
88
|
},
|
|
81
89
|
"pnl-summary": {
|
|
90
|
+
"endpoint": "/api/v1/profiler/address/pnl-summary",
|
|
82
91
|
"description": "Summarized PnL metrics",
|
|
83
92
|
"options": {
|
|
84
93
|
"address": {
|
|
@@ -93,6 +102,7 @@
|
|
|
93
102
|
}
|
|
94
103
|
},
|
|
95
104
|
"balance": {
|
|
105
|
+
"endpoint": "/api/v1/profiler/address/current-balance",
|
|
96
106
|
"description": "Current token holdings",
|
|
97
107
|
"options": {
|
|
98
108
|
"address": {
|
|
@@ -104,6 +114,7 @@
|
|
|
104
114
|
}
|
|
105
115
|
},
|
|
106
116
|
"counterparties": {
|
|
117
|
+
"endpoint": "/api/v1/profiler/address/counterparties",
|
|
107
118
|
"description": "Top counterparties by volume",
|
|
108
119
|
"options": {
|
|
109
120
|
"address": {
|
|
@@ -118,6 +129,7 @@
|
|
|
118
129
|
}
|
|
119
130
|
},
|
|
120
131
|
"historical-balances": {
|
|
132
|
+
"endpoint": "/api/v1/profiler/address/historical-balances",
|
|
121
133
|
"description": "Historical balances over time",
|
|
122
134
|
"options": {
|
|
123
135
|
"address": {
|
|
@@ -132,6 +144,7 @@
|
|
|
132
144
|
}
|
|
133
145
|
},
|
|
134
146
|
"related-wallets": {
|
|
147
|
+
"endpoint": "/api/v1/profiler/address/related-wallets",
|
|
135
148
|
"description": "Find wallets related to an address",
|
|
136
149
|
"options": {
|
|
137
150
|
"address": {
|
|
@@ -143,6 +156,7 @@
|
|
|
143
156
|
}
|
|
144
157
|
},
|
|
145
158
|
"pnl": {
|
|
159
|
+
"endpoint": "/api/v1/profiler/address/pnl",
|
|
146
160
|
"description": "PnL and trade performance",
|
|
147
161
|
"options": {
|
|
148
162
|
"address": {
|
|
@@ -157,6 +171,7 @@
|
|
|
157
171
|
}
|
|
158
172
|
},
|
|
159
173
|
"labels": {
|
|
174
|
+
"endpoint": "/api/v1/profiler/address/labels",
|
|
160
175
|
"description": "Behavioral and entity labels",
|
|
161
176
|
"options": {
|
|
162
177
|
"address": {
|
|
@@ -168,6 +183,7 @@
|
|
|
168
183
|
}
|
|
169
184
|
},
|
|
170
185
|
"perp-positions": {
|
|
186
|
+
"endpoint": "/api/v1/profiler/perp-positions",
|
|
171
187
|
"description": "Current perpetual positions",
|
|
172
188
|
"options": {
|
|
173
189
|
"address": {
|
|
@@ -176,6 +192,7 @@
|
|
|
176
192
|
}
|
|
177
193
|
},
|
|
178
194
|
"perp-trades": {
|
|
195
|
+
"endpoint": "/api/v1/profiler/perp-trades",
|
|
179
196
|
"description": "Perpetual trading history",
|
|
180
197
|
"options": {
|
|
181
198
|
"address": {
|
|
@@ -187,6 +204,7 @@
|
|
|
187
204
|
}
|
|
188
205
|
},
|
|
189
206
|
"search": {
|
|
207
|
+
"endpoint": "/api/v1/search/entity-name",
|
|
190
208
|
"description": "Search for entities by name",
|
|
191
209
|
"options": {
|
|
192
210
|
"query": {
|
|
@@ -251,6 +269,7 @@
|
|
|
251
269
|
"token": {
|
|
252
270
|
"subcommands": {
|
|
253
271
|
"flows": {
|
|
272
|
+
"endpoint": "/api/v1/tgm/flows",
|
|
254
273
|
"description": "Token flow metrics",
|
|
255
274
|
"options": {
|
|
256
275
|
"chain": {
|
|
@@ -261,10 +280,16 @@
|
|
|
261
280
|
},
|
|
262
281
|
"days": {
|
|
263
282
|
"default": 30
|
|
283
|
+
},
|
|
284
|
+
"label": {
|
|
285
|
+
"description": "Holder segment to filter flows by",
|
|
286
|
+
"enum": ["top_100_holders", "smart_money", "public_figure", "whale", "exchange"],
|
|
287
|
+
"default": "top_100_holders"
|
|
264
288
|
}
|
|
265
289
|
}
|
|
266
290
|
},
|
|
267
291
|
"who-bought-sold": {
|
|
292
|
+
"endpoint": "/api/v1/tgm/who-bought-sold",
|
|
268
293
|
"description": "Recent buyers and sellers",
|
|
269
294
|
"options": {
|
|
270
295
|
"chain": {
|
|
@@ -279,6 +304,7 @@
|
|
|
279
304
|
}
|
|
280
305
|
},
|
|
281
306
|
"dex-trades": {
|
|
307
|
+
"endpoint": "/api/v1/tgm/dex-trades",
|
|
282
308
|
"description": "DEX trading activity",
|
|
283
309
|
"options": {
|
|
284
310
|
"chain": {
|
|
@@ -293,6 +319,7 @@
|
|
|
293
319
|
}
|
|
294
320
|
},
|
|
295
321
|
"transfers": {
|
|
322
|
+
"endpoint": "/api/v1/tgm/transfers",
|
|
296
323
|
"description": "Token transfer history",
|
|
297
324
|
"options": {
|
|
298
325
|
"chain": {
|
|
@@ -307,6 +334,7 @@
|
|
|
307
334
|
}
|
|
308
335
|
},
|
|
309
336
|
"holders": {
|
|
337
|
+
"endpoint": "/api/v1/tgm/holders",
|
|
310
338
|
"description": "Token holder analysis",
|
|
311
339
|
"options": {
|
|
312
340
|
"chain": {
|
|
@@ -318,6 +346,7 @@
|
|
|
318
346
|
}
|
|
319
347
|
},
|
|
320
348
|
"pnl": {
|
|
349
|
+
"endpoint": "/api/v1/tgm/pnl-leaderboard",
|
|
321
350
|
"description": "PnL leaderboard",
|
|
322
351
|
"options": {
|
|
323
352
|
"chain": {
|
|
@@ -332,6 +361,7 @@
|
|
|
332
361
|
}
|
|
333
362
|
},
|
|
334
363
|
"perp-pnl-leaderboard": {
|
|
364
|
+
"endpoint": "/api/v1/tgm/perp-pnl-leaderboard",
|
|
335
365
|
"description": "Perp PnL leaderboard by token",
|
|
336
366
|
"options": {
|
|
337
367
|
"symbol": {
|
|
@@ -343,6 +373,7 @@
|
|
|
343
373
|
}
|
|
344
374
|
},
|
|
345
375
|
"perp-positions": {
|
|
376
|
+
"endpoint": "/api/v1/tgm/perp-positions",
|
|
346
377
|
"description": "Open perp positions by token symbol",
|
|
347
378
|
"options": {
|
|
348
379
|
"symbol": {
|
|
@@ -351,6 +382,7 @@
|
|
|
351
382
|
}
|
|
352
383
|
},
|
|
353
384
|
"perp-trades": {
|
|
385
|
+
"endpoint": "/api/v1/tgm/perp-trades",
|
|
354
386
|
"description": "Perp trades by token symbol",
|
|
355
387
|
"options": {
|
|
356
388
|
"symbol": {
|
|
@@ -362,6 +394,7 @@
|
|
|
362
394
|
}
|
|
363
395
|
},
|
|
364
396
|
"flow-intelligence": {
|
|
397
|
+
"endpoint": "/api/v1/tgm/flow-intelligence",
|
|
365
398
|
"description": "Detailed flow intelligence by label",
|
|
366
399
|
"options": {
|
|
367
400
|
"chain": {
|
|
@@ -376,6 +409,7 @@
|
|
|
376
409
|
}
|
|
377
410
|
},
|
|
378
411
|
"info": {
|
|
412
|
+
"endpoint": "/api/v1/tgm/token-information",
|
|
379
413
|
"description": "Get detailed information for a specific token",
|
|
380
414
|
"options": {
|
|
381
415
|
"chain": {
|
|
@@ -390,6 +424,7 @@
|
|
|
390
424
|
}
|
|
391
425
|
},
|
|
392
426
|
"indicators": {
|
|
427
|
+
"endpoint": "/api/v1/tgm/indicators",
|
|
393
428
|
"description": "Risk and reward indicators for a token (Nansen Score)",
|
|
394
429
|
"options": {
|
|
395
430
|
"chain": {
|
|
@@ -401,6 +436,7 @@
|
|
|
401
436
|
}
|
|
402
437
|
},
|
|
403
438
|
"ohlcv": {
|
|
439
|
+
"endpoint": "/api/v1/tgm/token-ohlcv",
|
|
404
440
|
"description": "OHLCV candle data for a token",
|
|
405
441
|
"options": {
|
|
406
442
|
"chain": {
|
|
@@ -415,6 +451,7 @@
|
|
|
415
451
|
}
|
|
416
452
|
},
|
|
417
453
|
"jup-dca": {
|
|
454
|
+
"endpoint": "/api/v1/tgm/jup-dca",
|
|
418
455
|
"description": "Jupiter DCA orders for token",
|
|
419
456
|
"options": {
|
|
420
457
|
"token": {
|
|
@@ -423,6 +460,7 @@
|
|
|
423
460
|
}
|
|
424
461
|
},
|
|
425
462
|
"screener": {
|
|
463
|
+
"endpoint": "/api/v1/token-screener",
|
|
426
464
|
"description": "Discover and filter tokens",
|
|
427
465
|
"options": {
|
|
428
466
|
"timeframe": {
|
|
@@ -430,6 +468,10 @@
|
|
|
430
468
|
},
|
|
431
469
|
"chain": {
|
|
432
470
|
"default": "solana"
|
|
471
|
+
},
|
|
472
|
+
"include-stablecoins": {
|
|
473
|
+
"description": "Whether to include stablecoins in screener results (default true on API side)",
|
|
474
|
+
"default": true
|
|
433
475
|
}
|
|
434
476
|
}
|
|
435
477
|
}
|
|
@@ -437,6 +479,7 @@
|
|
|
437
479
|
"description": "Token God Mode - deep analytics for any token"
|
|
438
480
|
},
|
|
439
481
|
"search": {
|
|
482
|
+
"endpoint": "/api/v1/search/general",
|
|
440
483
|
"description": "Search for tokens and entities across Nansen",
|
|
441
484
|
"options": {
|
|
442
485
|
"query": {
|
|
@@ -453,6 +496,7 @@
|
|
|
453
496
|
"perp": {
|
|
454
497
|
"subcommands": {
|
|
455
498
|
"screener": {
|
|
499
|
+
"endpoint": "/api/v1/perp-screener",
|
|
456
500
|
"description": "Screen perpetual futures contracts",
|
|
457
501
|
"options": {
|
|
458
502
|
"days": {
|
|
@@ -461,6 +505,7 @@
|
|
|
461
505
|
}
|
|
462
506
|
},
|
|
463
507
|
"leaderboard": {
|
|
508
|
+
"endpoint": "/api/v1/perp-leaderboard",
|
|
464
509
|
"description": "Perpetual futures PnL leaderboard",
|
|
465
510
|
"options": {
|
|
466
511
|
"days": {
|
|
@@ -474,6 +519,7 @@
|
|
|
474
519
|
"prediction-market": {
|
|
475
520
|
"subcommands": {
|
|
476
521
|
"ohlcv": {
|
|
522
|
+
"endpoint": "/api/v1/prediction-market/ohlcv",
|
|
477
523
|
"description": "Get Prediction Market OHLCV Candles",
|
|
478
524
|
"options": {
|
|
479
525
|
"market-id": {
|
|
@@ -482,6 +528,7 @@
|
|
|
482
528
|
}
|
|
483
529
|
},
|
|
484
530
|
"orderbook": {
|
|
531
|
+
"endpoint": "/api/v1/prediction-market/orderbook",
|
|
485
532
|
"description": "Get Prediction Market Orderbook",
|
|
486
533
|
"options": {
|
|
487
534
|
"market-id": {
|
|
@@ -490,6 +537,7 @@
|
|
|
490
537
|
}
|
|
491
538
|
},
|
|
492
539
|
"top-holders": {
|
|
540
|
+
"endpoint": "/api/v1/prediction-market/top-holders",
|
|
493
541
|
"description": "Get Prediction Market Top Holders",
|
|
494
542
|
"options": {
|
|
495
543
|
"market-id": {
|
|
@@ -498,6 +546,7 @@
|
|
|
498
546
|
}
|
|
499
547
|
},
|
|
500
548
|
"trades-by-market": {
|
|
549
|
+
"endpoint": "/api/v1/prediction-market/trades-by-market",
|
|
501
550
|
"description": "Get Prediction Market Trades by Market",
|
|
502
551
|
"options": {
|
|
503
552
|
"market-id": {
|
|
@@ -506,6 +555,7 @@
|
|
|
506
555
|
}
|
|
507
556
|
},
|
|
508
557
|
"market-screener": {
|
|
558
|
+
"endpoint": "/api/v1/prediction-market/market-screener",
|
|
509
559
|
"description": "Get Prediction Market Screener",
|
|
510
560
|
"options": {
|
|
511
561
|
"query": {
|
|
@@ -514,6 +564,7 @@
|
|
|
514
564
|
}
|
|
515
565
|
},
|
|
516
566
|
"event-screener": {
|
|
567
|
+
"endpoint": "/api/v1/prediction-market/event-screener",
|
|
517
568
|
"description": "Get Prediction Market Event Screener",
|
|
518
569
|
"options": {
|
|
519
570
|
"query": {
|
|
@@ -522,6 +573,7 @@
|
|
|
522
573
|
}
|
|
523
574
|
},
|
|
524
575
|
"pnl-by-market": {
|
|
576
|
+
"endpoint": "/api/v1/prediction-market/pnl-by-market",
|
|
525
577
|
"description": "Get Prediction Market PnL by Market",
|
|
526
578
|
"options": {
|
|
527
579
|
"market-id": {
|
|
@@ -530,6 +582,7 @@
|
|
|
530
582
|
}
|
|
531
583
|
},
|
|
532
584
|
"pnl-by-address": {
|
|
585
|
+
"endpoint": "/api/v1/prediction-market/pnl-by-address",
|
|
533
586
|
"description": "Get Prediction Market PnL by Address",
|
|
534
587
|
"options": {
|
|
535
588
|
"address": {
|
|
@@ -538,6 +591,7 @@
|
|
|
538
591
|
}
|
|
539
592
|
},
|
|
540
593
|
"position-detail": {
|
|
594
|
+
"endpoint": "/api/v1/prediction-market/position-detail",
|
|
541
595
|
"description": "Get Prediction Market Position Detail",
|
|
542
596
|
"options": {
|
|
543
597
|
"market-id": {
|
|
@@ -546,6 +600,7 @@
|
|
|
546
600
|
}
|
|
547
601
|
},
|
|
548
602
|
"trades-by-address": {
|
|
603
|
+
"endpoint": "/api/v1/prediction-market/trades-by-address",
|
|
549
604
|
"description": "Get Prediction Market Trades by Address",
|
|
550
605
|
"options": {
|
|
551
606
|
"address": {
|
|
@@ -554,6 +609,7 @@
|
|
|
554
609
|
}
|
|
555
610
|
},
|
|
556
611
|
"categories": {
|
|
612
|
+
"endpoint": "/api/v1/prediction-market/categories",
|
|
557
613
|
"description": "Get Prediction Market Categories"
|
|
558
614
|
}
|
|
559
615
|
},
|
|
@@ -715,7 +771,11 @@
|
|
|
715
771
|
"amount": {
|
|
716
772
|
"type": "string",
|
|
717
773
|
"required": true,
|
|
718
|
-
"description": "Amount to swap"
|
|
774
|
+
"description": "Amount to swap (base units by default, or token units with --amount-unit token)"
|
|
775
|
+
},
|
|
776
|
+
"amount-unit": {
|
|
777
|
+
"type": "string",
|
|
778
|
+
"description": "\"token\" to specify amount in token units (e.g. 0.5 SOL), or \"base\" for base units (default). Decimals are resolved locally and the API always receives base units."
|
|
719
779
|
},
|
|
720
780
|
"wallet": {
|
|
721
781
|
"type": "string",
|
|
@@ -826,6 +886,35 @@
|
|
|
826
886
|
]
|
|
827
887
|
}
|
|
828
888
|
}
|
|
889
|
+
},
|
|
890
|
+
"agent": {
|
|
891
|
+
"description": "Nansen AI research agent \u2014 ask questions about wallets, tokens, and on-chain activity",
|
|
892
|
+
"options": {
|
|
893
|
+
"expert": {
|
|
894
|
+
"type": "boolean",
|
|
895
|
+
"description": "Use expert mode (deeper analysis, slower). Default: fast mode."
|
|
896
|
+
},
|
|
897
|
+
"conversation-id": {
|
|
898
|
+
"type": "string",
|
|
899
|
+
"format": "uuid",
|
|
900
|
+
"description": "Continue a previous conversation by passing its UUID v4 ID"
|
|
901
|
+
},
|
|
902
|
+
"json": {
|
|
903
|
+
"type": "boolean",
|
|
904
|
+
"description": "Output raw JSON (conversation_id, mode, text, tool_calls) instead of formatted text"
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
"examples": [
|
|
908
|
+
"nansen agent \"What are the top smart money inflows on Ethereum today?\"",
|
|
909
|
+
"nansen agent \"Analyze wallet 0xd8dA...6045\" --expert",
|
|
910
|
+
"nansen agent \"Tell me more\" --conversation-id <id>"
|
|
911
|
+
],
|
|
912
|
+
"returns": [
|
|
913
|
+
"conversation_id",
|
|
914
|
+
"mode",
|
|
915
|
+
"text",
|
|
916
|
+
"tool_calls"
|
|
917
|
+
]
|
|
829
918
|
}
|
|
830
919
|
},
|
|
831
920
|
"globalOptions": {
|
package/src/trading.js
CHANGED
|
@@ -683,6 +683,101 @@ export function getWrappedNativeFromWarning(tokenAddress, chain) {
|
|
|
683
683
|
return null;
|
|
684
684
|
}
|
|
685
685
|
|
|
686
|
+
// ============= Token Decimal Resolution =============
|
|
687
|
+
|
|
688
|
+
// Hardcoded decimals for well-known tokens — avoids RPC calls in the common case.
|
|
689
|
+
const KNOWN_DECIMALS = {
|
|
690
|
+
// Solana
|
|
691
|
+
'So11111111111111111111111111111111111111112': 9, // SOL/WSOL
|
|
692
|
+
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': 6, // USDC
|
|
693
|
+
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': 6, // USDT
|
|
694
|
+
// Base (EVM) — lowercase for case-insensitive matching
|
|
695
|
+
'0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee': 18, // ETH native
|
|
696
|
+
'0x4200000000000000000000000000000000000006': 18, // WETH
|
|
697
|
+
'0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': 6, // USDC
|
|
698
|
+
'0xfde4c96c8593536e31f229ea8f37b2ada2699bb2': 6, // USDT
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Resolve the number of decimals for a token.
|
|
703
|
+
* Checks a hardcoded map first, then falls back to an RPC call.
|
|
704
|
+
* Solana: getAccountInfo with jsonParsed encoding.
|
|
705
|
+
* EVM: eth_call to decimals() selector 0x313ce567.
|
|
706
|
+
*/
|
|
707
|
+
export async function resolveTokenDecimals(tokenAddress, chainName) {
|
|
708
|
+
// Normalise for map lookup (EVM addresses are case-insensitive)
|
|
709
|
+
const key = tokenAddress.startsWith('0x') ? tokenAddress.toLowerCase() : tokenAddress;
|
|
710
|
+
if (KNOWN_DECIMALS[key] !== undefined) return KNOWN_DECIMALS[key];
|
|
711
|
+
|
|
712
|
+
const chain = chainName.toLowerCase();
|
|
713
|
+
const chainConfig = CHAIN_MAP[chain];
|
|
714
|
+
if (!chainConfig) throw new Error(`Unknown chain: ${chain}`);
|
|
715
|
+
|
|
716
|
+
// Validate address format before making RPC calls.
|
|
717
|
+
// A bare symbol like "SOL" that didn't resolve means it's not recognized on this chain.
|
|
718
|
+
if (chainConfig.type === 'solana') {
|
|
719
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(tokenAddress)) {
|
|
720
|
+
throw new Error(`"${tokenAddress}" is not a recognized token on ${chainName}. Use a valid Solana address (base58, 32-44 chars).`);
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(tokenAddress)) {
|
|
724
|
+
throw new Error(`"${tokenAddress}" is not a recognized token on ${chainName}. Use a valid EVM address (0x + 40 hex chars).`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (chainConfig.type === 'solana') {
|
|
729
|
+
const rpcUrl = CHAIN_RPCS.solana;
|
|
730
|
+
const res = await fetch(rpcUrl, {
|
|
731
|
+
method: 'POST',
|
|
732
|
+
headers: { 'Content-Type': 'application/json' },
|
|
733
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'getAccountInfo', params: [tokenAddress, { encoding: 'jsonParsed' }] }),
|
|
734
|
+
});
|
|
735
|
+
const body = await res.json();
|
|
736
|
+
const decimals = body.result?.value?.data?.parsed?.info?.decimals;
|
|
737
|
+
if (decimals === undefined) throw new Error(`Could not resolve decimals for Solana token ${tokenAddress}`);
|
|
738
|
+
return decimals;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// EVM — eth_call to decimals()
|
|
742
|
+
const result = await evmRpcCall(chain, 'eth_call', [{ to: tokenAddress, data: '0x313ce567' }, 'latest']);
|
|
743
|
+
const decimals = parseInt(result, 16);
|
|
744
|
+
if (isNaN(decimals) || decimals > 255) throw new Error(`Could not resolve decimals for EVM token ${tokenAddress}`);
|
|
745
|
+
return decimals;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Convert a human-readable token amount to base units using string math.
|
|
750
|
+
* Avoids floating-point precision issues by operating on digit strings.
|
|
751
|
+
* Example: convertToBaseUnits('0.5', 9) => '500000000'
|
|
752
|
+
*/
|
|
753
|
+
export function convertToBaseUnits(amount, decimals) {
|
|
754
|
+
const str = String(amount);
|
|
755
|
+
if (!/^\d+(\.\d+)?$/.test(str)) {
|
|
756
|
+
throw new Error(`Invalid amount: "${str}". Must be a non-negative number (e.g. "0.5", "100").`);
|
|
757
|
+
}
|
|
758
|
+
const dotIndex = str.indexOf('.');
|
|
759
|
+
if (dotIndex === -1) {
|
|
760
|
+
// Whole number — append zeros and strip leading zeros
|
|
761
|
+
const raw = str + '0'.repeat(decimals);
|
|
762
|
+
return raw.replace(/^0+/, '') || '0';
|
|
763
|
+
}
|
|
764
|
+
const whole = str.slice(0, dotIndex);
|
|
765
|
+
let frac = str.slice(dotIndex + 1);
|
|
766
|
+
if (frac.length > decimals) {
|
|
767
|
+
// Reject if meaningful (non-zero) digits would be lost
|
|
768
|
+
const excess = frac.slice(decimals);
|
|
769
|
+
if (/[1-9]/.test(excess)) {
|
|
770
|
+
throw new Error(`Amount "${str}" has more fractional digits than the token supports (${decimals} decimals). The smallest unit is ${decimals === 0 ? '1 token' : '0.' + '0'.repeat(decimals - 1) + '1'}.`);
|
|
771
|
+
}
|
|
772
|
+
frac = frac.slice(0, decimals);
|
|
773
|
+
} else {
|
|
774
|
+
frac = frac.padEnd(decimals, '0');
|
|
775
|
+
}
|
|
776
|
+
// Strip leading zeros from the combined result
|
|
777
|
+
const raw = (whole + frac).replace(/^0+/, '') || '0';
|
|
778
|
+
return raw;
|
|
779
|
+
}
|
|
780
|
+
|
|
686
781
|
/**
|
|
687
782
|
* Check if amount contains a decimal point (i.e. not in base units).
|
|
688
783
|
* Returns an error string if invalid, or null if OK. Pure function.
|
|
@@ -690,8 +785,12 @@ export function getWrappedNativeFromWarning(tokenAddress, chain) {
|
|
|
690
785
|
export function validateBaseUnitAmount(amount) {
|
|
691
786
|
if (!amount) return null;
|
|
692
787
|
const str = String(amount);
|
|
788
|
+
if (str.startsWith('-')) {
|
|
789
|
+
return 'Amount cannot be negative. Got: ' + str;
|
|
790
|
+
}
|
|
693
791
|
if (str.includes('.')) {
|
|
694
|
-
return 'Amount must be in base units (integer)
|
|
792
|
+
return 'Amount must be in base units (integer). ' +
|
|
793
|
+
'Use --amount-unit token to specify token amounts (e.g. --amount 0.5 --amount-unit token). ' +
|
|
695
794
|
'Examples: 1000000000 lamports = 1 SOL, 1000000000000000000 wei = 1 ETH, ' +
|
|
696
795
|
'1000000 = 1 USDC. Got: ' + str;
|
|
697
796
|
}
|
|
@@ -745,6 +844,7 @@ export function buildTradingCommands(deps = {}) {
|
|
|
745
844
|
const autoSlippage = flags['auto-slippage'] || flags.autoSlippage;
|
|
746
845
|
const maxAutoSlippage = options['max-auto-slippage'];
|
|
747
846
|
const swapMode = options['swap-mode'] || 'exactIn';
|
|
847
|
+
const amountUnit = options['amount-unit'];
|
|
748
848
|
|
|
749
849
|
if (!chain || !from || !to || !amount) {
|
|
750
850
|
log(`
|
|
@@ -760,6 +860,7 @@ OPTIONS:
|
|
|
760
860
|
--from <symbol|address> Input token (symbol like SOL, USDC or address)
|
|
761
861
|
--to <symbol|address> Output token (symbol like USDC, ETH or address)
|
|
762
862
|
--amount <units> Amount in BASE UNITS (e.g. lamports, wei)
|
|
863
|
+
--amount-unit <unit> "token" to specify amount in token units (e.g. 0.5 SOL)
|
|
763
864
|
--wallet <name> Wallet name (default: default wallet). Use "walletconnect" or "wc" for WalletConnect.
|
|
764
865
|
--slippage <pct> Slippage as decimal (e.g. 0.03 for 3%). Default: 0.03
|
|
765
866
|
--auto-slippage Enable auto slippage calculation
|
|
@@ -768,6 +869,7 @@ OPTIONS:
|
|
|
768
869
|
|
|
769
870
|
EXAMPLES:
|
|
770
871
|
nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
872
|
+
nansen trade quote --chain solana --from SOL --to USDC --amount 0.5 --amount-unit token
|
|
771
873
|
nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
|
|
772
874
|
nansen trade quote --chain solana --from So11111111111111111111111111111111111111112 --to EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v --amount 1000000000
|
|
773
875
|
`);
|
|
@@ -775,13 +877,35 @@ EXAMPLES:
|
|
|
775
877
|
return;
|
|
776
878
|
}
|
|
777
879
|
|
|
778
|
-
|
|
779
|
-
if (
|
|
780
|
-
log(`Error: ${
|
|
880
|
+
// Validate --amount-unit if provided
|
|
881
|
+
if (amountUnit && amountUnit !== 'token' && amountUnit !== 'base') {
|
|
882
|
+
log(`Error: Unknown --amount-unit "${amountUnit}". Supported values: token, base`);
|
|
781
883
|
exit(1);
|
|
782
884
|
return;
|
|
783
885
|
}
|
|
784
886
|
|
|
887
|
+
// When --amount-unit token is used, resolve decimals and convert to base units.
|
|
888
|
+
// Otherwise, validate that the amount is already in base units (integer).
|
|
889
|
+
let resolvedAmount = amount;
|
|
890
|
+
if (amountUnit === 'token') {
|
|
891
|
+
try {
|
|
892
|
+
const tokenForDecimals = swapMode === 'exactOut' ? to : from;
|
|
893
|
+
const decimals = await resolveTokenDecimals(tokenForDecimals, chain);
|
|
894
|
+
resolvedAmount = convertToBaseUnits(amount, decimals);
|
|
895
|
+
} catch (err) {
|
|
896
|
+
log(`Error resolving token decimals: ${err.message}`);
|
|
897
|
+
exit(1);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
} else {
|
|
901
|
+
const amountError = validateBaseUnitAmount(amount);
|
|
902
|
+
if (amountError) {
|
|
903
|
+
log(`Error: ${amountError}`);
|
|
904
|
+
exit(1);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
785
909
|
try {
|
|
786
910
|
const chainConfig = resolveChain(chain);
|
|
787
911
|
const chainType = chainConfig.type === 'evm' ? 'evm' : 'solana';
|
|
@@ -837,7 +961,7 @@ EXAMPLES:
|
|
|
837
961
|
chainIndex: chainConfig.index,
|
|
838
962
|
fromTokenAddress: from,
|
|
839
963
|
toTokenAddress: to,
|
|
840
|
-
amount,
|
|
964
|
+
amount: resolvedAmount,
|
|
841
965
|
userWalletAddress: walletAddress,
|
|
842
966
|
};
|
|
843
967
|
if (slippage) params.slippagePercent = slippage;
|
package/src/wallet.js
CHANGED
|
@@ -596,8 +596,15 @@ export function buildWalletCommands(deps = {}) {
|
|
|
596
596
|
// All other subcommands fall through to unified handlers below
|
|
597
597
|
}
|
|
598
598
|
|
|
599
|
+
const isPrivy = options.provider === 'privy' || process.env.NANSEN_WALLET_PROVIDER === 'privy';
|
|
600
|
+
|
|
599
601
|
const handlers = {
|
|
600
602
|
'create': async () => {
|
|
603
|
+
// Privy wallets are handled above — if we reach here with provider=privy,
|
|
604
|
+
// the privy path already failed and called exit(). Guard against environments
|
|
605
|
+
// where exit() does not terminate (agent frameworks, test harnesses).
|
|
606
|
+
if (isPrivy) return;
|
|
607
|
+
|
|
601
608
|
const name = options.name || args[1] || 'default';
|
|
602
609
|
|
|
603
610
|
let password;
|