nansen-cli 1.26.0 → 1.27.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/src/wallet.js CHANGED
@@ -9,6 +9,7 @@ import path from 'path';
9
9
  import * as readline from 'readline';
10
10
  import { base58 } from '@scure/base';
11
11
  import { storePassword, retrievePassword, deletePassword, deleteCredentialsFile } from './keychain.js';
12
+ import { CommandError } from './api.js';
12
13
 
13
14
  // ============= Constants =============
14
15
 
@@ -246,6 +247,14 @@ function getWalletFile(name) {
246
247
  return path.join(getWalletsDir(), `${name}.json`);
247
248
  }
248
249
 
250
+ function requireWalletFile(name) {
251
+ const walletFile = getWalletFile(name);
252
+ if (!fs.existsSync(walletFile)) {
253
+ throw new Error(`Wallet "${name}" not found`);
254
+ }
255
+ return walletFile;
256
+ }
257
+
249
258
  /**
250
259
  * Verify the global password against stored hash.
251
260
  */
@@ -335,8 +344,8 @@ function resolveWalletPassword() {
335
344
  *
336
345
  * @param {object} config - wallet config (needs config.passwordHash)
337
346
  * @param {object} flags - CLI flags
338
- * @param {object} deps - { promptFn, log, exit }
339
- * @returns {{ password: string|null, error: string|null }}
347
+ * @param {object} deps - { promptFn, log }
348
+ * @returns {{ password: string|null, error: object|null }}
340
349
  */
341
350
  async function resolvePasswordForCommand(config, flags, deps) {
342
351
  if (!config.passwordHash) {
@@ -353,14 +362,14 @@ async function resolvePasswordForCommand(config, flags, deps) {
353
362
 
354
363
  return {
355
364
  password: null,
356
- error: JSON.stringify({
365
+ error: {
357
366
  error: 'PASSWORD_REQUIRED',
358
367
  message: 'Wallet is encrypted and no password was found.',
359
368
  resolution: [
360
369
  'Set NANSEN_WALLET_PASSWORD environment variable',
361
370
  'Or re-run wallet create with the password (it will be persisted for future use)',
362
371
  ],
363
- }),
372
+ },
364
373
  };
365
374
  }
366
375
 
@@ -465,10 +474,7 @@ export function createWallet(name, password) {
465
474
  * Show wallet details (addresses only, no keys).
466
475
  */
467
476
  export function showWallet(name) {
468
- const walletFile = getWalletFile(name);
469
- if (!fs.existsSync(walletFile)) {
470
- throw new Error(`Wallet "${name}" not found`);
471
- }
477
+ const walletFile = requireWalletFile(name);
472
478
 
473
479
  const data = JSON.parse(fs.readFileSync(walletFile, 'utf8'));
474
480
  const config = getWalletConfig();
@@ -494,10 +500,7 @@ export function showWallet(name) {
494
500
  * Export private keys for a wallet (requires password).
495
501
  */
496
502
  export function exportWallet(name, password) {
497
- const walletFile = getWalletFile(name);
498
- if (!fs.existsSync(walletFile)) {
499
- throw new Error(`Wallet "${name}" not found`);
500
- }
503
+ const walletFile = requireWalletFile(name);
501
504
 
502
505
  const data = JSON.parse(fs.readFileSync(walletFile, 'utf8'));
503
506
  if (data.provider && data.provider !== 'local') {
@@ -526,10 +529,7 @@ export function exportWallet(name, password) {
526
529
  * Set the default wallet.
527
530
  */
528
531
  export function setDefaultWallet(name) {
529
- const walletFile = getWalletFile(name);
530
- if (!fs.existsSync(walletFile)) {
531
- throw new Error(`Wallet "${name}" not found`);
532
- }
532
+ requireWalletFile(name);
533
533
 
534
534
  const config = getWalletConfig();
535
535
  config.defaultWallet = name;
@@ -542,10 +542,7 @@ export function setDefaultWallet(name) {
542
542
  * Delete a wallet.
543
543
  */
544
544
  export async function deleteWallet(name, password) {
545
- const walletFile = getWalletFile(name);
546
- if (!fs.existsSync(walletFile)) {
547
- throw new Error(`Wallet "${name}" not found`);
548
- }
545
+ const walletFile = requireWalletFile(name);
549
546
 
550
547
  const data = JSON.parse(fs.readFileSync(walletFile, 'utf8'));
551
548
  const config = getWalletConfig();
@@ -580,7 +577,7 @@ export async function deleteWallet(name, password) {
580
577
  * Build wallet command handlers for integration into CLI.
581
578
  */
582
579
  export function buildWalletCommands(deps = {}) {
583
- const { log = console.log, promptFn: _promptFn, exit = process.exit } = deps;
580
+ const { log = console.log } = deps;
584
581
 
585
582
  return {
586
583
  'wallet': async (args, apiInstance, flags, options) => {
@@ -599,9 +596,7 @@ export function buildWalletCommands(deps = {}) {
599
596
  log('');
600
597
  return;
601
598
  } catch (err) {
602
- log(`❌ ${err.message}`);
603
- exit(1);
604
- return;
599
+ throw new CommandError(`❌ ${err.message}`, 'PRIVY_ERROR');
605
600
  }
606
601
  }
607
602
  // All other subcommands fall through to unified handlers below
@@ -612,8 +607,8 @@ export function buildWalletCommands(deps = {}) {
612
607
  const handlers = {
613
608
  'create': async () => {
614
609
  // Privy wallets are handled above — if we reach here with provider=privy,
615
- // the privy path already failed and called exit(). Guard against environments
616
- // where exit() does not terminate (agent frameworks, test harnesses).
610
+ // the privy path already threw CommandError. Guard against environments
611
+ // where throw does not propagate (agent frameworks, test harnesses).
617
612
  if (isPrivy) return;
618
613
 
619
614
  const name = options.name || args[1] || 'default';
@@ -628,28 +623,22 @@ export function buildWalletCommands(deps = {}) {
628
623
 
629
624
  // Step 2: If --human flag, allow interactive prompt (requires TTY)
630
625
  if (!password && flags.human && !process.stdin.isTTY && !deps.promptFn) {
631
- log(JSON.stringify({
626
+ throw new CommandError('--human requires an interactive terminal. Set NANSEN_WALLET_PASSWORD env var instead.', 'NOT_A_TTY', {
632
627
  error: 'NOT_A_TTY',
633
628
  message: '--human requires an interactive terminal. Set NANSEN_WALLET_PASSWORD env var instead.',
634
- }));
635
- exit(1);
636
- return;
629
+ });
637
630
  }
638
631
  if (!password && flags.human && (process.stdin.isTTY || deps.promptFn)) {
639
632
  password = await promptPassword('Enter wallet password: ', deps);
640
633
  if (password && password.length < 12) {
641
- log('❌ Password must be at least 12 characters');
642
- exit(1);
643
- return;
634
+ throw new CommandError('❌ Password must be at least 12 characters', 'INVALID_INPUT');
644
635
  }
645
636
  if (password) {
646
637
  const config = getWalletConfig();
647
638
  if (!config.passwordHash) {
648
639
  const confirm = await promptPassword('Confirm password: ', deps);
649
640
  if (password !== confirm) {
650
- log('❌ Passwords do not match');
651
- exit(1);
652
- return;
641
+ throw new CommandError('❌ Passwords do not match', 'INVALID_INPUT');
653
642
  }
654
643
  }
655
644
  }
@@ -657,20 +646,16 @@ export function buildWalletCommands(deps = {}) {
657
646
 
658
647
  // Step 3: No password available — return structured error for agents
659
648
  if (!password) {
660
- log(JSON.stringify({
649
+ throw new CommandError('A wallet password is required. Ask the user to provide one.', 'PASSWORD_REQUIRED', {
661
650
  error: 'PASSWORD_REQUIRED',
662
651
  message: 'A wallet password is required. Ask the user to provide one.',
663
652
  instructions: 'Re-run with: NANSEN_WALLET_PASSWORD=<password> nansen wallet create',
664
653
  note: 'Password must be at least 12 characters. After creation, the password is saved to the OS keychain automatically — future operations will not require it.',
665
- }));
666
- exit(1);
667
- return;
654
+ });
668
655
  }
669
656
 
670
657
  if (password.length < 12) {
671
- log('❌ Password must be at least 12 characters');
672
- exit(1);
673
- return;
658
+ throw new CommandError('❌ Password must be at least 12 characters', 'INVALID_INPUT');
674
659
  }
675
660
  }
676
661
 
@@ -678,9 +663,7 @@ export function buildWalletCommands(deps = {}) {
678
663
  if (password !== null) {
679
664
  const config = getWalletConfig();
680
665
  if (config.passwordHash && !verifyPassword(password, config)) {
681
- log('❌ Incorrect password — does not match existing wallets.');
682
- exit(1);
683
- return;
666
+ throw new CommandError('❌ Incorrect password — does not match existing wallets.', 'INCORRECT_PASSWORD');
684
667
  }
685
668
  }
686
669
 
@@ -725,8 +708,7 @@ export function buildWalletCommands(deps = {}) {
725
708
  log('');
726
709
  return;
727
710
  } catch (err) {
728
- log(`❌ ${err.message}`);
729
- exit(1);
711
+ throw new CommandError(`❌ ${err.message}`, 'CREATE_FAILED');
730
712
  }
731
713
  },
732
714
 
@@ -750,9 +732,7 @@ export function buildWalletCommands(deps = {}) {
750
732
  'show': async () => {
751
733
  const name = options.name || args[1];
752
734
  if (!name) {
753
- log('Usage: nansen wallet show <name>');
754
- exit(1);
755
- return;
735
+ throw new CommandError('Usage: nansen wallet show <name>', 'MISSING_ARGS');
756
736
  }
757
737
  try {
758
738
  const result = showWallet(name);
@@ -764,25 +744,20 @@ export function buildWalletCommands(deps = {}) {
764
744
  log(` Created: ${result.createdAt}\n`);
765
745
  return;
766
746
  } catch (err) {
767
- log(`❌ ${err.message}`);
768
- exit(1);
747
+ throw new CommandError(`❌ ${err.message}`, 'SHOW_FAILED');
769
748
  }
770
749
  },
771
750
 
772
751
  'export': async () => {
773
752
  const name = options.name || args[1];
774
753
  if (!name) {
775
- log('Usage: nansen wallet export <name>');
776
- exit(1);
777
- return;
754
+ throw new CommandError('Usage: nansen wallet export <name>', 'MISSING_ARGS');
778
755
  }
779
756
 
780
757
  const config = getWalletConfig();
781
758
  const { password, error } = await resolvePasswordForCommand(config, flags, deps);
782
759
  if (error) {
783
- log(error);
784
- exit(1);
785
- return;
760
+ throw new CommandError(error.message, 'PASSWORD_REQUIRED', error);
786
761
  }
787
762
  try {
788
763
  const result = exportWallet(name, password);
@@ -796,34 +771,28 @@ export function buildWalletCommands(deps = {}) {
796
771
  log('');
797
772
  return;
798
773
  } catch (err) {
799
- log(`❌ ${err.message}`);
800
- exit(1);
774
+ throw new CommandError(`❌ ${err.message}`, 'EXPORT_FAILED');
801
775
  }
802
776
  },
803
777
 
804
778
  'default': async () => {
805
779
  const name = options.name || args[1];
806
780
  if (!name) {
807
- log('Usage: nansen wallet default <name>');
808
- exit(1);
809
- return;
781
+ throw new CommandError('Usage: nansen wallet default <name>', 'MISSING_ARGS');
810
782
  }
811
783
  try {
812
784
  const result = setDefaultWallet(name);
813
785
  log(`✓ Default wallet set to "${result.defaultWallet}"`);
814
786
  return;
815
787
  } catch (err) {
816
- log(`❌ ${err.message}`);
817
- exit(1);
788
+ throw new CommandError(`❌ ${err.message}`, 'DEFAULT_FAILED');
818
789
  }
819
790
  },
820
791
 
821
792
  'delete': async () => {
822
793
  const name = options.name || args[1];
823
794
  if (!name) {
824
- log('Usage: nansen wallet delete <name>');
825
- exit(1);
826
- return;
795
+ throw new CommandError('Usage: nansen wallet delete <name>', 'MISSING_ARGS');
827
796
  }
828
797
 
829
798
  // Check if this is a Privy wallet (no password needed)
@@ -839,9 +808,7 @@ export function buildWalletCommands(deps = {}) {
839
808
  const config = getWalletConfig();
840
809
  const resolved = await resolvePasswordForCommand(config, flags, deps);
841
810
  if (resolved.error) {
842
- log(resolved.error);
843
- exit(1);
844
- return;
811
+ throw new CommandError(resolved.error.message, 'PASSWORD_REQUIRED', resolved.error);
845
812
  }
846
813
  password = resolved.password;
847
814
  }
@@ -857,8 +824,7 @@ export function buildWalletCommands(deps = {}) {
857
824
  }
858
825
  return;
859
826
  } catch (err) {
860
- log(`❌ ${err.message}`);
861
- exit(1);
827
+ throw new CommandError(`❌ ${err.message}`, 'DELETE_FAILED');
862
828
  }
863
829
  },
864
830
 
@@ -866,28 +832,20 @@ export function buildWalletCommands(deps = {}) {
866
832
  const { sendTokens } = await import('./transfer.js');
867
833
 
868
834
  if (!options.to) {
869
- log('--to <address> is required');
870
- exit(1);
871
- return;
835
+ throw new CommandError('--to <address> is required', 'MISSING_ARGS');
872
836
  }
873
837
 
874
838
  const isMax = flags.max || options.amount === 'max';
875
839
  if (!options.amount && !isMax) {
876
- log('--amount <number> or --max is required');
877
- exit(1);
878
- return;
840
+ throw new CommandError('--amount <number> or --max is required', 'MISSING_ARGS');
879
841
  }
880
842
 
881
843
  if (!options.chain) {
882
- log('--chain <evm|solana> is required');
883
- exit(1);
884
- return;
844
+ throw new CommandError('--chain <evm|solana> is required', 'MISSING_ARGS');
885
845
  }
886
846
 
887
847
  if (!['evm', 'solana', 'ethereum', 'base'].includes(options.chain)) {
888
- log('--chain must be one of: evm, solana, ethereum, base');
889
- exit(1);
890
- return;
848
+ throw new CommandError('--chain must be one of: evm, solana, ethereum, base', 'INVALID_INPUT');
891
849
  }
892
850
 
893
851
  const isWalletConnect = options.wallet === 'walletconnect' || options.wallet === 'wc';
@@ -912,9 +870,7 @@ export function buildWalletCommands(deps = {}) {
912
870
  const sendConfig = getWalletConfig();
913
871
  const resolved = await resolvePasswordForCommand(sendConfig, flags, deps);
914
872
  if (resolved.error) {
915
- log(resolved.error);
916
- exit(1);
917
- return;
873
+ throw new CommandError(resolved.error.message, 'PASSWORD_REQUIRED', resolved.error);
918
874
  }
919
875
  password = resolved.password;
920
876
  }
@@ -960,8 +916,7 @@ export function buildWalletCommands(deps = {}) {
960
916
  log('');
961
917
  return;
962
918
  } catch (err) {
963
- log(`Error: ${err.message}`);
964
- exit(1);
919
+ throw new CommandError(err.message, 'SEND_FAILED');
965
920
  }
966
921
  },
967
922
 
@@ -979,16 +934,14 @@ export function buildWalletCommands(deps = {}) {
979
934
  'secure': async () => {
980
935
  const { password, source } = retrievePassword();
981
936
  if (!password) {
982
- log(JSON.stringify({
937
+ throw new CommandError('No wallet password found in any store.', 'NO_PASSWORD_FOUND', {
983
938
  error: 'NO_PASSWORD_FOUND',
984
939
  message: 'No wallet password found in any store.',
985
940
  resolution: [
986
941
  'Set NANSEN_WALLET_PASSWORD and run: nansen wallet secure',
987
942
  'This will store it in the OS keychain.',
988
943
  ],
989
- }));
990
- exit(1);
991
- return;
944
+ });
992
945
  }
993
946
 
994
947
  if (source === 'keychain') {
@@ -999,20 +952,19 @@ export function buildWalletCommands(deps = {}) {
999
952
  // Verify password actually decrypts wallets before overwriting keychain
1000
953
  const walletConfig = getWalletConfig();
1001
954
  if (walletConfig.passwordHash && !verifyPassword(password, walletConfig)) {
1002
- log(JSON.stringify({
955
+ const resolution = source === 'file'
956
+ ? [
957
+ 'The password in ~/.nansen/wallets/.credentials is incorrect.',
958
+ 'Run: nansen wallet forget-password then re-run with the correct password: NANSEN_WALLET_PASSWORD=<pw> nansen wallet secure',
959
+ ]
960
+ : [
961
+ 'Unset NANSEN_WALLET_PASSWORD if it is stale, then re-run: nansen wallet secure',
962
+ ];
963
+ throw new CommandError(`Password from '${source}' does not match the wallet's stored hash.`, 'INCORRECT_PASSWORD', {
1003
964
  error: 'INCORRECT_PASSWORD',
1004
965
  message: `Password from '${source}' does not match the wallet's stored hash.`,
1005
- resolution: source === 'file'
1006
- ? [
1007
- 'The password in ~/.nansen/wallets/.credentials is incorrect.',
1008
- 'Run: nansen wallet forget-password then re-run with the correct password: NANSEN_WALLET_PASSWORD=<pw> nansen wallet secure',
1009
- ]
1010
- : [
1011
- 'Unset NANSEN_WALLET_PASSWORD if it is stale, then re-run: nansen wallet secure',
1012
- ],
1013
- }));
1014
- exit(1);
1015
- return;
966
+ resolution,
967
+ });
1016
968
  }
1017
969
 
1018
970
  // Try to migrate to keychain
@@ -1027,18 +979,17 @@ export function buildWalletCommands(deps = {}) {
1027
979
  log(' Removed ~/.nansen/wallets/.credentials.');
1028
980
  }
1029
981
  } else {
1030
- log(JSON.stringify({
982
+ const msg = source === 'file'
983
+ ? 'OS keychain is not available. Password remains in ~/.nansen/wallets/.credentials (insecure).'
984
+ : 'OS keychain is not available. Password is only in the NANSEN_WALLET_PASSWORD env var (not persisted).';
985
+ throw new CommandError(msg, 'KEYCHAIN_UNAVAILABLE', {
1031
986
  error: 'KEYCHAIN_UNAVAILABLE',
1032
- message: source === 'file'
1033
- ? 'OS keychain is not available. Password remains in ~/.nansen/wallets/.credentials (insecure).'
1034
- : 'OS keychain is not available. Password is only in the NANSEN_WALLET_PASSWORD env var (not persisted).',
987
+ message: msg,
1035
988
  resolution: [
1036
989
  'Set NANSEN_WALLET_PASSWORD in a secrets manager or system keyring',
1037
990
  'Use a containerized secrets agent (e.g. Vault, 1Password CLI)',
1038
991
  ],
1039
- }));
1040
- exit(1);
1041
- return;
992
+ });
1042
993
  }
1043
994
  },
1044
995
 
@@ -1104,9 +1055,7 @@ EXAMPLES:
1104
1055
  };
1105
1056
 
1106
1057
  if (!handlers[subcommand]) {
1107
- log(`Unknown subcommand: ${subcommand}. Available: ${Object.keys(handlers).join(', ')}`);
1108
- exit(1);
1109
- return;
1058
+ throw new CommandError(`Unknown subcommand: ${subcommand}. Available: ${Object.keys(handlers).join(', ')}`, 'UNKNOWN_COMMAND');
1110
1059
  }
1111
1060
 
1112
1061
  // --help on any wallet subcommand shows wallet help instead of executing
package/src/x402-svm.js CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  import crypto from 'crypto';
7
7
  import { base58Encode, base58DecodePubkey } from './wallet.js';
8
- import { encodeCompactU16, isOnEd25519Curve } from './transfer.js';
8
+ import { encodeCompactU16, deriveATA as _deriveATA } from './transfer.js';
9
9
 
10
10
  // ============= Constants =============
11
11
 
@@ -13,7 +13,6 @@ const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
13
13
  const _TOKEN_2022_PROGRAM = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
14
14
  const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
15
15
  const MEMO_PROGRAM = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
16
- const ATA_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
17
16
  const _SYSTEM_PROGRAM = '11111111111111111111111111111111';
18
17
 
19
18
  const DEFAULT_COMPUTE_UNIT_LIMIT = 20000;
@@ -23,29 +22,10 @@ const DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;
23
22
 
24
23
  /**
25
24
  * Derive Associated Token Account (ATA) address.
26
- * PDA seeds: [owner, tokenProgram, mint] with ATA program.
25
+ * Returns base58-encoded PDA. Delegates algorithm to transfer.js.
27
26
  */
28
27
  export function deriveATA(ownerBase58, mintBase58, tokenProgramBase58 = TOKEN_PROGRAM) {
29
- const owner = base58DecodePubkey(ownerBase58);
30
- const tokenProgram = base58DecodePubkey(tokenProgramBase58);
31
- const mint = base58DecodePubkey(mintBase58);
32
- const ataProgramKey = base58DecodePubkey(ATA_PROGRAM);
33
-
34
- // find_program_address: try nonce 255 down to 0
35
- // PDA = SHA256(seeds... || programId || "ProgramDerivedAddress")
36
- // A valid PDA must NOT be on the ed25519 curve.
37
- // Checking on-curve in pure JS without a full ed25519 implementation is hard.
38
- // We use the mathematical approach: decode y-coordinate, compute x², check QR.
39
- for (let nonce = 255; nonce >= 0; nonce--) {
40
- const hash = crypto.createHash('sha256')
41
- .update(Buffer.concat([owner, tokenProgram, mint, Buffer.from([nonce]), ataProgramKey, Buffer.from('ProgramDerivedAddress')]))
42
- .digest();
43
-
44
- if (!isOnEd25519Curve(hash)) {
45
- return base58Encode(hash);
46
- }
47
- }
48
- throw new Error('Could not derive ATA: no valid PDA found');
28
+ return base58Encode(_deriveATA(ownerBase58, mintBase58, tokenProgramBase58));
49
29
  }
50
30
 
51
31
  // ============= MessageV0 Builder =============