nansen-cli 1.26.0 → 1.26.1
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 +6 -0
- package/package.json +1 -1
- package/src/api.js +23 -0
- package/src/cli.js +29 -30
- package/src/telemetry.js +8 -6
- package/src/trade-validation.js +27 -0
- package/src/trading.js +55 -93
- package/src/wallet.js +54 -101
- package/src/x402-svm.js +3 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.26.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#392](https://github.com/nansen-ai/nansen-cli/pull/392) [`025993d`](https://github.com/nansen-ai/nansen-cli/commit/025993df798ddb406340511e0dada1f9a962be56) Thanks [@TimNooren](https://github.com/TimNooren)! - Add gas balance validation: rejects trades when the wallet lacks sufficient native token for gas fees.
|
|
8
|
+
|
|
3
9
|
## 1.26.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -60,6 +60,29 @@ export const ErrorCode = {
|
|
|
60
60
|
UNKNOWN: 'UNKNOWN', // Unclassified error
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Error thrown by command handlers for user-facing failures (validation errors,
|
|
65
|
+
* missing args, etc.).
|
|
66
|
+
*
|
|
67
|
+
* Handlers must throw rather than calling log() + exit() directly, because
|
|
68
|
+
* direct exits bypass runCLI's catch block and skip telemetry tracking.
|
|
69
|
+
*
|
|
70
|
+
* runCLI outputs CommandError.message as plain text (not JSON-formatted like
|
|
71
|
+
* NansenError), then fires trackCommandFailed before exiting.
|
|
72
|
+
*
|
|
73
|
+
* When `data` is provided, runCLI outputs JSON.stringify(data) instead of the
|
|
74
|
+
* plain message — this preserves structured JSON output for errors that agents
|
|
75
|
+
* parse (e.g. PASSWORD_REQUIRED, API_KEY_REQUIRED).
|
|
76
|
+
*/
|
|
77
|
+
export class CommandError extends Error {
|
|
78
|
+
constructor(message, code = 'COMMAND_ERROR', data = null) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.name = 'CommandError';
|
|
81
|
+
this.code = code;
|
|
82
|
+
this.data = data;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
63
86
|
/**
|
|
64
87
|
* Custom error class with structured error codes
|
|
65
88
|
*/
|
package/src/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Extracted from index.js for coverage
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { NansenAPI, NansenError, ErrorCode, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir, validateAddress, normalizeAddress, sleep } from './api.js';
|
|
6
|
+
import { NansenAPI, NansenError, CommandError, ErrorCode, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir, validateAddress, normalizeAddress, sleep } from './api.js';
|
|
7
7
|
import { buildWalletCommands } from './wallet.js';
|
|
8
8
|
import { buildTradingCommands } from './trading.js';
|
|
9
9
|
import { formatAlertsTable, buildAlertsCommands } from './commands/alerts.js';
|
|
@@ -793,7 +793,6 @@ export function buildCommands(deps = {}) {
|
|
|
793
793
|
saveConfigFn = saveConfig,
|
|
794
794
|
deleteConfigFn = deleteConfig,
|
|
795
795
|
getConfigFileFn = getConfigFile,
|
|
796
|
-
exit = process.exit,
|
|
797
796
|
isTTY = process.stdin.isTTY
|
|
798
797
|
} = deps;
|
|
799
798
|
|
|
@@ -895,12 +894,10 @@ export function buildCommands(deps = {}) {
|
|
|
895
894
|
|
|
896
895
|
if (!apiKey && flags.human) {
|
|
897
896
|
if (!isTTY) {
|
|
898
|
-
|
|
897
|
+
throw new CommandError('--human requires an interactive terminal. Use --api-key or NANSEN_API_KEY env var instead.', 'NOT_A_TTY', {
|
|
899
898
|
error: 'NOT_A_TTY',
|
|
900
899
|
message: '--human requires an interactive terminal. Use --api-key or NANSEN_API_KEY env var instead.',
|
|
901
|
-
})
|
|
902
|
-
exit(1);
|
|
903
|
-
return;
|
|
900
|
+
});
|
|
904
901
|
}
|
|
905
902
|
log('Nansen CLI Login\n');
|
|
906
903
|
log('Get your API key at: https://app.nansen.ai/auth/agent-setup\n');
|
|
@@ -908,7 +905,7 @@ export function buildCommands(deps = {}) {
|
|
|
908
905
|
}
|
|
909
906
|
|
|
910
907
|
if (!apiKey || apiKey.trim().length === 0) {
|
|
911
|
-
|
|
908
|
+
throw new CommandError('No API key provided.', 'API_KEY_REQUIRED', {
|
|
912
909
|
error: 'API_KEY_REQUIRED',
|
|
913
910
|
message: 'No API key provided.',
|
|
914
911
|
resolution: [
|
|
@@ -916,9 +913,7 @@ export function buildCommands(deps = {}) {
|
|
|
916
913
|
'Or set NANSEN_API_KEY environment variable',
|
|
917
914
|
'Get your API key at: https://app.nansen.ai/auth/agent-setup',
|
|
918
915
|
],
|
|
919
|
-
})
|
|
920
|
-
exit(1);
|
|
921
|
-
return;
|
|
916
|
+
});
|
|
922
917
|
}
|
|
923
918
|
|
|
924
919
|
// Verify API key before saving
|
|
@@ -933,20 +928,17 @@ export function buildCommands(deps = {}) {
|
|
|
933
928
|
accountInfo = await testApi.getAccount();
|
|
934
929
|
} catch (error) {
|
|
935
930
|
if (error.code === ErrorCode.UNAUTHORIZED) {
|
|
936
|
-
|
|
931
|
+
throw new CommandError('The API key is not valid.', 'INVALID_API_KEY', {
|
|
937
932
|
error: 'INVALID_API_KEY',
|
|
938
933
|
message: 'The API key is not valid.',
|
|
939
|
-
resolution: ['Check your key at https://app.nansen.ai/auth/agent-setup']
|
|
940
|
-
})
|
|
941
|
-
} else {
|
|
942
|
-
log(JSON.stringify({
|
|
943
|
-
error: 'VERIFICATION_FAILED',
|
|
944
|
-
message: `Could not verify API key: ${error.message}`,
|
|
945
|
-
resolution: ['Check your internet connection', 'Try again']
|
|
946
|
-
}));
|
|
934
|
+
resolution: ['Check your key at https://app.nansen.ai/auth/agent-setup'],
|
|
935
|
+
});
|
|
947
936
|
}
|
|
948
|
-
|
|
949
|
-
|
|
937
|
+
throw new CommandError(`Could not verify API key: ${error.message}`, 'VERIFICATION_FAILED', {
|
|
938
|
+
error: 'VERIFICATION_FAILED',
|
|
939
|
+
message: `Could not verify API key: ${error.message}`,
|
|
940
|
+
resolution: ['Check your internet connection', 'Try again'],
|
|
941
|
+
});
|
|
950
942
|
}
|
|
951
943
|
|
|
952
944
|
// Key is valid - now save
|
|
@@ -1732,7 +1724,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1732
1724
|
};
|
|
1733
1725
|
const formatted = formatOutput(errorData, { pretty, table });
|
|
1734
1726
|
output(formatted.text);
|
|
1735
|
-
trackCommandFailed({ command: fullCommand, duration_ms: Date.now() - startTime, error_code: 'UNKNOWN_COMMAND', flags: usedFlags, chain });
|
|
1727
|
+
await trackCommandFailed({ command: fullCommand, duration_ms: Date.now() - startTime, error_code: 'UNKNOWN_COMMAND', flags: usedFlags, chain });
|
|
1736
1728
|
exit(1);
|
|
1737
1729
|
return { type: 'error', data: errorData };
|
|
1738
1730
|
}
|
|
@@ -1759,7 +1751,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1759
1751
|
|
|
1760
1752
|
// Commands that handle their own output return undefined
|
|
1761
1753
|
if (result === undefined) {
|
|
1762
|
-
trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
1754
|
+
await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
1763
1755
|
return { type: 'no-output', command };
|
|
1764
1756
|
}
|
|
1765
1757
|
|
|
@@ -1767,7 +1759,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1767
1759
|
if (command === 'schema') {
|
|
1768
1760
|
const formatted = formatOutput(result, { pretty, table: false });
|
|
1769
1761
|
output(formatted.text);
|
|
1770
|
-
trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
1762
|
+
await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
1771
1763
|
return { type: 'schema', data: result };
|
|
1772
1764
|
}
|
|
1773
1765
|
|
|
@@ -1780,6 +1772,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1780
1772
|
// Alerts list with --table uses custom table format
|
|
1781
1773
|
if (command === 'alerts' && subcommand === 'list' && table) {
|
|
1782
1774
|
output(formatAlertsTable(result));
|
|
1775
|
+
await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
1783
1776
|
return { type: 'success', data: result };
|
|
1784
1777
|
}
|
|
1785
1778
|
|
|
@@ -1790,20 +1783,26 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1790
1783
|
if (streamOutput) {
|
|
1791
1784
|
output(streamOutput);
|
|
1792
1785
|
}
|
|
1793
|
-
trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
1786
|
+
await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
1794
1787
|
return { type: 'stream', data: result };
|
|
1795
1788
|
}
|
|
1796
1789
|
|
|
1797
1790
|
const successData = { success: true, data: result };
|
|
1798
1791
|
const formatted = formatOutput(successData, { pretty, table, csv });
|
|
1799
1792
|
output(formatted.text);
|
|
1800
|
-
trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
1793
|
+
await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
1801
1794
|
return { type: csv ? 'csv' : 'success', data: result };
|
|
1802
1795
|
} catch (error) {
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1796
|
+
let errorData;
|
|
1797
|
+
if (error instanceof CommandError) {
|
|
1798
|
+
output(error.data ? JSON.stringify(error.data) : error.message);
|
|
1799
|
+
errorData = { error: error.message, code: error.code };
|
|
1800
|
+
} else {
|
|
1801
|
+
errorData = formatError(error);
|
|
1802
|
+
const formatted = formatOutput(errorData, { pretty, table, csv });
|
|
1803
|
+
output(formatted.text);
|
|
1804
|
+
}
|
|
1805
|
+
await trackCommandFailed({
|
|
1807
1806
|
command: fullCommand,
|
|
1808
1807
|
duration_ms: Date.now() - startTime,
|
|
1809
1808
|
error_code: error.code || 'UNKNOWN',
|
package/src/telemetry.js
CHANGED
|
@@ -22,7 +22,7 @@ const { version: cliVersion } = JSON.parse(
|
|
|
22
22
|
const TELEMETRY_URL =
|
|
23
23
|
'https://bi-data-sources.nansen.ai/events-service-68ifmnpsx2uq7cgab8dw/v2/event';
|
|
24
24
|
|
|
25
|
-
const TIMEOUT_MS =
|
|
25
|
+
const TIMEOUT_MS = 1000;
|
|
26
26
|
|
|
27
27
|
// ─── opt-out ──────────────────────────────────────────────
|
|
28
28
|
|
|
@@ -122,15 +122,17 @@ export function getSessionId() {
|
|
|
122
122
|
// ─── send ──────────────────────────────────────────────────
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Send a telemetry event.
|
|
125
|
+
* Send a telemetry event. Returns a promise that resolves when the request
|
|
126
|
+
* completes (or fails/times out). Never rejects — errors are swallowed.
|
|
127
|
+
* Callers that need to ensure delivery before process.exit can await this.
|
|
126
128
|
*/
|
|
127
129
|
function sendEvent(event) {
|
|
128
|
-
if (TELEMETRY_DISABLED) return;
|
|
130
|
+
if (TELEMETRY_DISABLED) return Promise.resolve();
|
|
129
131
|
const controller = new AbortController();
|
|
130
132
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
131
133
|
timer.unref();
|
|
132
134
|
|
|
133
|
-
fetch(TELEMETRY_URL, {
|
|
135
|
+
return fetch(TELEMETRY_URL, {
|
|
134
136
|
method: 'POST',
|
|
135
137
|
headers: { 'Content-Type': 'application/json' },
|
|
136
138
|
body: JSON.stringify(event),
|
|
@@ -178,7 +180,7 @@ export function trackCommandSucceeded({
|
|
|
178
180
|
flags = [],
|
|
179
181
|
chain = null,
|
|
180
182
|
}) {
|
|
181
|
-
sendEvent({
|
|
183
|
+
return sendEvent({
|
|
182
184
|
event: 'cli_command_succeeded',
|
|
183
185
|
event_source: getEventSource(),
|
|
184
186
|
event_id: crypto.randomUUID(),
|
|
@@ -216,7 +218,7 @@ export function trackCommandFailed({
|
|
|
216
218
|
flags = [],
|
|
217
219
|
chain = null,
|
|
218
220
|
}) {
|
|
219
|
-
sendEvent({
|
|
221
|
+
return sendEvent({
|
|
220
222
|
event: 'cli_command_failed',
|
|
221
223
|
event_source: getEventSource(),
|
|
222
224
|
event_id: crypto.randomUUID(),
|
package/src/trade-validation.js
CHANGED
|
@@ -115,6 +115,7 @@ const NATIVE_TOKEN_ADDRESSES = {
|
|
|
115
115
|
// Native token symbols for error messages.
|
|
116
116
|
const NATIVE_SYMBOLS = { solana: 'SOL', base: 'ETH' };
|
|
117
117
|
|
|
118
|
+
const MIN_GAS_AMOUNTS = { solana: 0.01, base: 0.000024 };
|
|
118
119
|
const FEE_BUFFER = { solana: 0.005, base: 0.00004 };
|
|
119
120
|
const HIGH_PERCENTAGE_THRESHOLD = 95;
|
|
120
121
|
const AUTO_ADJUST_THRESHOLD_PERCENT = 2;
|
|
@@ -269,6 +270,32 @@ export async function resolvePercentAmount({ chain, from, walletAddress, percent
|
|
|
269
270
|
return String(parseFloat(tokenAmount.toFixed(decimals)));
|
|
270
271
|
}
|
|
271
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Validate that the wallet has enough native token for gas fees.
|
|
275
|
+
*
|
|
276
|
+
* Returns { hasSufficientNative } or throws on validation failure.
|
|
277
|
+
* Best-effort: if RPC fails, returns passing result.
|
|
278
|
+
*/
|
|
279
|
+
export async function validateGasBalance({ chain, walletAddress }) {
|
|
280
|
+
const normalizedChain = chain.toLowerCase();
|
|
281
|
+
const minGas = MIN_GAS_AMOUNTS[normalizedChain];
|
|
282
|
+
if (minGas === undefined) return { hasSufficientNative: true };
|
|
283
|
+
|
|
284
|
+
const balance = await fetchNativeBalance(normalizedChain, walletAddress);
|
|
285
|
+
|
|
286
|
+
// RPC failure — proceed without validation.
|
|
287
|
+
if (balance === null) return { hasSufficientNative: true };
|
|
288
|
+
|
|
289
|
+
if (balance >= minGas) {
|
|
290
|
+
return { hasSufficientNative: true };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const symbol = NATIVE_SYMBOLS[normalizedChain] || 'native token';
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Insufficient ${symbol} for gas fees. Wallet has ${balance} ${symbol} but needs at least ${minGas} ${symbol}. Fund the wallet before trading.`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
272
299
|
/**
|
|
273
300
|
* Fetch an ERC-20 or SPL token balance for a wallet.
|
|
274
301
|
* Returns balance in human-readable token units, or null on RPC failure.
|
package/src/trading.js
CHANGED
|
@@ -13,12 +13,14 @@ import { base58Decode } from './transfer.js';
|
|
|
13
13
|
import { keccak256, signSecp256k1, rlpEncode } from './crypto.js';
|
|
14
14
|
import { getWalletConnectAddress, sendTransactionViaWalletConnect, sendSolanaTransactionViaWalletConnect, sendApprovalViaWalletConnect } from './walletconnect-trading.js';
|
|
15
15
|
import { retrievePassword } from './keychain.js';
|
|
16
|
-
import { validateQuoteInput, validateBalance, resolvePercentAmount } from './trade-validation.js';
|
|
16
|
+
import { validateQuoteInput, validateBalance, resolvePercentAmount, validateGasBalance } from './trade-validation.js';
|
|
17
17
|
import { CHAIN_RPCS } from './rpc-urls.js';
|
|
18
|
+
import { packageVersion, CommandError } from './api.js';
|
|
18
19
|
|
|
19
20
|
// ============= Constants =============
|
|
20
21
|
|
|
21
22
|
const TRADING_API_URL = process.env.NANSEN_TRADING_API_URL || 'https://trading-api.nansen.ai';
|
|
23
|
+
const CLIENT_USER_AGENT = `nansen-cli/${packageVersion}`;
|
|
22
24
|
|
|
23
25
|
const CHAIN_MAP = {
|
|
24
26
|
solana: { index: '501', type: 'solana', chainId: 501, name: 'Solana', explorer: 'https://solscan.io/tx/', lifiChainId: '1151111081099710' },
|
|
@@ -115,7 +117,7 @@ export async function getQuote(params) {
|
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
const headers = { 'Accept': 'application/json' };
|
|
120
|
+
const headers = { 'Accept': 'application/json', 'User-Agent': CLIENT_USER_AGENT };
|
|
119
121
|
|
|
120
122
|
const res = await fetch(url.toString(), { headers });
|
|
121
123
|
|
|
@@ -153,6 +155,7 @@ export async function executeTransaction(params, { retries = 2, retryDelayMs = 1
|
|
|
153
155
|
const headers = {
|
|
154
156
|
'Content-Type': 'application/json',
|
|
155
157
|
'Accept': 'application/json',
|
|
158
|
+
'User-Agent': CLIENT_USER_AGENT,
|
|
156
159
|
};
|
|
157
160
|
let lastError;
|
|
158
161
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
@@ -216,7 +219,7 @@ export async function getBridgeStatus(txHash, fromChain, toChain) {
|
|
|
216
219
|
url.searchParams.set('fromChain', fromConfig.lifiChainId || fromConfig.index);
|
|
217
220
|
url.searchParams.set('toChain', toConfig.lifiChainId || toConfig.index);
|
|
218
221
|
|
|
219
|
-
const res = await fetch(url.toString(), { headers: { 'Accept': 'application/json' } });
|
|
222
|
+
const res = await fetch(url.toString(), { headers: { 'Accept': 'application/json', 'User-Agent': CLIENT_USER_AGENT } });
|
|
220
223
|
const text = await res.text();
|
|
221
224
|
let body;
|
|
222
225
|
try {
|
|
@@ -940,7 +943,7 @@ export function formatQuote(quote, index) {
|
|
|
940
943
|
* Build trading command handlers for CLI integration.
|
|
941
944
|
*/
|
|
942
945
|
export function buildTradingCommands(deps = {}) {
|
|
943
|
-
const { log = console.log
|
|
946
|
+
const { log = console.log } = deps;
|
|
944
947
|
|
|
945
948
|
return {
|
|
946
949
|
'quote': async (args, apiInstance, flags, options) => {
|
|
@@ -961,7 +964,7 @@ export function buildTradingCommands(deps = {}) {
|
|
|
961
964
|
const amountUnit = options['amount-unit'];
|
|
962
965
|
|
|
963
966
|
if (!chain || !from || !to || !amount) {
|
|
964
|
-
|
|
967
|
+
throw new CommandError(`
|
|
965
968
|
Usage: nansen trade quote --chain <chain> --from <token> --to <token> --amount <baseUnits>
|
|
966
969
|
|
|
967
970
|
PREREQUISITE:
|
|
@@ -991,23 +994,17 @@ EXAMPLES:
|
|
|
991
994
|
nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
|
|
992
995
|
nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
|
|
993
996
|
nansen trade quote --chain solana --to-chain base --from SOL --to ETH --amount 1000000000
|
|
994
|
-
|
|
995
|
-
exit(1);
|
|
996
|
-
return;
|
|
997
|
+
`, 'MISSING_ARGS');
|
|
997
998
|
}
|
|
998
999
|
|
|
999
1000
|
// Validate --amount-unit if provided
|
|
1000
1001
|
if (amountUnit && amountUnit !== 'token' && amountUnit !== 'base' && amountUnit !== 'usd' && amountUnit !== 'percent') {
|
|
1001
|
-
|
|
1002
|
-
exit(1);
|
|
1003
|
-
return;
|
|
1002
|
+
throw new CommandError(`Error: Unknown --amount-unit "${amountUnit}". Supported values: token, base, usd, percent`, 'INVALID_INPUT');
|
|
1004
1003
|
}
|
|
1005
1004
|
|
|
1006
1005
|
// --amount-unit percent is only valid for exactIn (sell-side)
|
|
1007
1006
|
if (amountUnit === 'percent' && swapMode === 'exactOut') {
|
|
1008
|
-
|
|
1009
|
-
exit(1);
|
|
1010
|
-
return;
|
|
1007
|
+
throw new CommandError('Error: --amount-unit percent is not supported with --swap-mode exactOut. Percentage is relative to your sell-token balance.', 'INVALID_INPUT');
|
|
1011
1008
|
}
|
|
1012
1009
|
|
|
1013
1010
|
// Static input validation — catches common agent errors (wrong addresses,
|
|
@@ -1015,9 +1012,7 @@ EXAMPLES:
|
|
|
1015
1012
|
try {
|
|
1016
1013
|
validateQuoteInput({ chain, toChain: toChainRaw || null, from, to, amount });
|
|
1017
1014
|
} catch (validationErr) {
|
|
1018
|
-
|
|
1019
|
-
exit(1);
|
|
1020
|
-
return;
|
|
1015
|
+
throw new CommandError(`Error: ${validationErr.message}`, 'INVALID_INPUT');
|
|
1021
1016
|
}
|
|
1022
1017
|
|
|
1023
1018
|
// When --amount-unit token is used, resolve decimals and convert to base units.
|
|
@@ -1036,9 +1031,7 @@ EXAMPLES:
|
|
|
1036
1031
|
usdTokenAmount = tokenAmount.toFixed(resolvedDecimals);
|
|
1037
1032
|
resolvedAmount = convertToBaseUnits(usdTokenAmount, resolvedDecimals);
|
|
1038
1033
|
} catch (err) {
|
|
1039
|
-
|
|
1040
|
-
exit(1);
|
|
1041
|
-
return;
|
|
1034
|
+
throw new CommandError(`Error converting USD amount: ${err.message}`, 'INVALID_INPUT');
|
|
1042
1035
|
}
|
|
1043
1036
|
} else if (amountUnit === 'token') {
|
|
1044
1037
|
try {
|
|
@@ -1046,18 +1039,14 @@ EXAMPLES:
|
|
|
1046
1039
|
resolvedDecimals = await resolveTokenDecimals(tokenForDecimals, chain);
|
|
1047
1040
|
resolvedAmount = convertToBaseUnits(amount, resolvedDecimals);
|
|
1048
1041
|
} catch (err) {
|
|
1049
|
-
|
|
1050
|
-
exit(1);
|
|
1051
|
-
return;
|
|
1042
|
+
throw new CommandError(`Error resolving token decimals: ${err.message}`, 'INVALID_INPUT');
|
|
1052
1043
|
}
|
|
1053
1044
|
} else if (amountUnit === 'percent') {
|
|
1054
1045
|
// Resolved after wallet address is available — see percent resolution block below.
|
|
1055
1046
|
} else {
|
|
1056
1047
|
const amountError = validateBaseUnitAmount(amount);
|
|
1057
1048
|
if (amountError) {
|
|
1058
|
-
|
|
1059
|
-
exit(1);
|
|
1060
|
-
return;
|
|
1049
|
+
throw new CommandError(`Error: ${amountError}`, 'INVALID_INPUT');
|
|
1061
1050
|
}
|
|
1062
1051
|
}
|
|
1063
1052
|
|
|
@@ -1073,9 +1062,7 @@ EXAMPLES:
|
|
|
1073
1062
|
if (isWalletConnect) {
|
|
1074
1063
|
walletAddress = await getWalletConnectAddress(chainType);
|
|
1075
1064
|
if (!walletAddress) {
|
|
1076
|
-
|
|
1077
|
-
exit(1);
|
|
1078
|
-
return;
|
|
1065
|
+
throw new CommandError('No WalletConnect session active. Run: walletconnect connect', 'NO_WALLET');
|
|
1079
1066
|
}
|
|
1080
1067
|
} else if (walletName) {
|
|
1081
1068
|
const wallet = showWallet(walletName);
|
|
@@ -1101,9 +1088,7 @@ EXAMPLES:
|
|
|
1101
1088
|
}
|
|
1102
1089
|
|
|
1103
1090
|
if (!walletAddress) {
|
|
1104
|
-
|
|
1105
|
-
exit(1);
|
|
1106
|
-
return;
|
|
1091
|
+
throw new CommandError('No wallet found. A wallet address is required for quotes because the trading API builds a transaction specific to the sender.\nCreate one with: nansen wallet create', 'NO_WALLET');
|
|
1107
1092
|
}
|
|
1108
1093
|
|
|
1109
1094
|
// --amount-unit percent: fetch balance, calculate percentage, convert to base units.
|
|
@@ -1120,9 +1105,7 @@ EXAMPLES:
|
|
|
1120
1105
|
});
|
|
1121
1106
|
resolvedAmount = convertToBaseUnits(tokenAmount, resolvedDecimals);
|
|
1122
1107
|
} catch (err) {
|
|
1123
|
-
|
|
1124
|
-
exit(1);
|
|
1125
|
-
return;
|
|
1108
|
+
throw new CommandError(`Error: ${err.message}`, 'INVALID_INPUT');
|
|
1126
1109
|
}
|
|
1127
1110
|
}
|
|
1128
1111
|
|
|
@@ -1148,9 +1131,7 @@ EXAMPLES:
|
|
|
1148
1131
|
resolvedAmount = convertToBaseUnits(balanceAdjusted, resolvedDecimals);
|
|
1149
1132
|
}
|
|
1150
1133
|
} catch (balanceErr) {
|
|
1151
|
-
|
|
1152
|
-
exit(1);
|
|
1153
|
-
return;
|
|
1134
|
+
throw new CommandError(`Error: ${balanceErr.message}`, 'INSUFFICIENT_BALANCE');
|
|
1154
1135
|
}
|
|
1155
1136
|
}
|
|
1156
1137
|
|
|
@@ -1197,17 +1178,23 @@ EXAMPLES:
|
|
|
1197
1178
|
const response = await getQuote(params);
|
|
1198
1179
|
|
|
1199
1180
|
if (!response.success || !response.quotes?.length) {
|
|
1200
|
-
|
|
1181
|
+
let msg = 'No quotes available';
|
|
1201
1182
|
if (response.warnings?.length) {
|
|
1202
|
-
response.warnings.
|
|
1183
|
+
msg += '\n' + response.warnings.map(w => ` Warning: ${w}`).join('\n');
|
|
1203
1184
|
}
|
|
1204
|
-
|
|
1205
|
-
return;
|
|
1185
|
+
throw new CommandError(msg, 'NO_QUOTES');
|
|
1206
1186
|
}
|
|
1207
1187
|
|
|
1208
1188
|
log('');
|
|
1209
1189
|
response.quotes.forEach((q, i) => log(formatQuote(q, i)));
|
|
1210
1190
|
|
|
1191
|
+
// Gas balance validation — check that the wallet has enough native token for gas.
|
|
1192
|
+
try {
|
|
1193
|
+
await validateGasBalance({ chain, walletAddress });
|
|
1194
|
+
} catch (gasErr) {
|
|
1195
|
+
throw new CommandError(`Error: ${gasErr.message}`, 'INSUFFICIENT_GAS');
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1211
1198
|
const signerType = isWalletConnect ? 'walletconnect' : walletProvider;
|
|
1212
1199
|
const quoteId = saveQuote(response, chain, signerType, privyWalletIds, isCrossChain ? toChainRaw : null);
|
|
1213
1200
|
log(`\n Quote ID: ${quoteId}`);
|
|
@@ -1225,13 +1212,14 @@ EXAMPLES:
|
|
|
1225
1212
|
return undefined; // Output already printed above
|
|
1226
1213
|
|
|
1227
1214
|
} catch (err) {
|
|
1215
|
+
if (err instanceof CommandError) throw err;
|
|
1228
1216
|
let message = err.message;
|
|
1229
1217
|
if (err.code === 'INVALID_AMOUNT' || /amount/i.test(err.message)) {
|
|
1230
1218
|
message += '. Amounts must be in base units (e.g., 1000000000 lamports for 1 SOL, 1000000000000000000 wei for 1 ETH)';
|
|
1231
1219
|
}
|
|
1232
|
-
|
|
1233
|
-
if (err.details)
|
|
1234
|
-
|
|
1220
|
+
let msg = `Error: ${message}`;
|
|
1221
|
+
if (err.details) msg += `\n Details: ${JSON.stringify(err.details)}`;
|
|
1222
|
+
throw new CommandError(msg, err.code || 'QUOTE_ERROR');
|
|
1235
1223
|
}
|
|
1236
1224
|
},
|
|
1237
1225
|
|
|
@@ -1241,8 +1229,7 @@ EXAMPLES:
|
|
|
1241
1229
|
const noSimulate = flags['no-simulate'];
|
|
1242
1230
|
|
|
1243
1231
|
if (!quoteId) {
|
|
1244
|
-
|
|
1245
|
-
Usage: nansen trade execute --quote <quoteId> [options]
|
|
1232
|
+
throw new CommandError(`Usage: nansen trade execute --quote <quoteId> [options]
|
|
1246
1233
|
|
|
1247
1234
|
OPTIONS:
|
|
1248
1235
|
--quote <id> Quote ID from 'nansen quote'
|
|
@@ -1250,10 +1237,7 @@ OPTIONS:
|
|
|
1250
1237
|
--no-simulate Skip pre-broadcast simulation
|
|
1251
1238
|
|
|
1252
1239
|
EXAMPLES:
|
|
1253
|
-
nansen trade execute --quote 1708900000000-abc123
|
|
1254
|
-
`);
|
|
1255
|
-
exit(1);
|
|
1256
|
-
return;
|
|
1240
|
+
nansen trade execute --quote 1708900000000-abc123`, 'MISSING_ARGS');
|
|
1257
1241
|
}
|
|
1258
1242
|
|
|
1259
1243
|
try {
|
|
@@ -1264,9 +1248,7 @@ EXAMPLES:
|
|
|
1264
1248
|
|
|
1265
1249
|
const allQuotes = quoteData.response.quotes || [];
|
|
1266
1250
|
if (!allQuotes.length) {
|
|
1267
|
-
|
|
1268
|
-
exit(1);
|
|
1269
|
-
return;
|
|
1251
|
+
throw new CommandError('❌ No quote data found', 'NO_QUOTES');
|
|
1270
1252
|
}
|
|
1271
1253
|
|
|
1272
1254
|
// --quote-index pins a specific quote (no fallback)
|
|
@@ -1277,10 +1259,7 @@ EXAMPLES:
|
|
|
1277
1259
|
// Check if any quote in range has transaction data before prompting for password
|
|
1278
1260
|
const hasAnyTransaction = allQuotes.slice(startIndex, endIndex).some(q => q?.transaction);
|
|
1279
1261
|
if (!hasAnyTransaction) {
|
|
1280
|
-
|
|
1281
|
-
log(' Ensure userWalletAddress was provided when fetching the quote.');
|
|
1282
|
-
exit(1);
|
|
1283
|
-
return;
|
|
1262
|
+
throw new CommandError('❌ No quotes contain transaction data.\n Ensure userWalletAddress was provided when fetching the quote.', 'NO_TRANSACTION');
|
|
1284
1263
|
}
|
|
1285
1264
|
|
|
1286
1265
|
// Determine if this is a WalletConnect or Privy-signed quote
|
|
@@ -1301,16 +1280,14 @@ EXAMPLES:
|
|
|
1301
1280
|
if (walletConfig.passwordHash) {
|
|
1302
1281
|
password = resolveTradePassword();
|
|
1303
1282
|
if (!password) {
|
|
1304
|
-
|
|
1283
|
+
throw new CommandError('Wallet is encrypted and no password was found.', 'PASSWORD_REQUIRED', {
|
|
1305
1284
|
error: 'PASSWORD_REQUIRED',
|
|
1306
1285
|
message: 'Wallet is encrypted and no password was found.',
|
|
1307
1286
|
resolution: [
|
|
1308
1287
|
'Set NANSEN_WALLET_PASSWORD environment variable',
|
|
1309
1288
|
'Or run: nansen wallet create (password is saved to OS keychain automatically)',
|
|
1310
1289
|
],
|
|
1311
|
-
})
|
|
1312
|
-
exit(1);
|
|
1313
|
-
return;
|
|
1290
|
+
});
|
|
1314
1291
|
}
|
|
1315
1292
|
}
|
|
1316
1293
|
|
|
@@ -1320,9 +1297,7 @@ EXAMPLES:
|
|
|
1320
1297
|
effectiveWalletName = list.defaultWallet;
|
|
1321
1298
|
}
|
|
1322
1299
|
if (!effectiveWalletName) {
|
|
1323
|
-
|
|
1324
|
-
exit(1);
|
|
1325
|
-
return;
|
|
1300
|
+
throw new CommandError('No wallet found. Create one with: nansen wallet create', 'NO_WALLET');
|
|
1326
1301
|
}
|
|
1327
1302
|
|
|
1328
1303
|
exported = exportWallet(effectiveWalletName, password);
|
|
@@ -1330,9 +1305,7 @@ EXAMPLES:
|
|
|
1330
1305
|
// Verify WalletConnect session is still active and address matches quote
|
|
1331
1306
|
const wcAddress = await getWalletConnectAddress(chainType);
|
|
1332
1307
|
if (!wcAddress) {
|
|
1333
|
-
|
|
1334
|
-
exit(1);
|
|
1335
|
-
return;
|
|
1308
|
+
throw new CommandError('No WalletConnect session active. Run: walletconnect connect', 'NO_WALLET');
|
|
1336
1309
|
}
|
|
1337
1310
|
// Check address matches the one used during quoting
|
|
1338
1311
|
const quoteWallet = quoteData.response?.quotes?.[0]?.transaction?.from
|
|
@@ -1340,9 +1313,7 @@ EXAMPLES:
|
|
|
1340
1313
|
if (quoteWallet && (chainType === 'solana'
|
|
1341
1314
|
? wcAddress.trim() !== quoteWallet.trim()
|
|
1342
1315
|
: wcAddress.toLowerCase().trim() !== quoteWallet.toLowerCase().trim())) {
|
|
1343
|
-
|
|
1344
|
-
exit(1);
|
|
1345
|
-
return;
|
|
1316
|
+
throw new CommandError(`Connected wallet (${wcAddress}) doesn't match quote. Get a new quote with --wallet walletconnect`, 'WALLET_MISMATCH');
|
|
1346
1317
|
}
|
|
1347
1318
|
}
|
|
1348
1319
|
|
|
@@ -1705,8 +1676,7 @@ EXAMPLES:
|
|
|
1705
1676
|
lastQuoteError = `${quoteName} reverted on-chain`;
|
|
1706
1677
|
continue;
|
|
1707
1678
|
}
|
|
1708
|
-
|
|
1709
|
-
return;
|
|
1679
|
+
throw new CommandError(`\n ⚠ Transaction was broadcast but REVERTED on-chain!\n Tx Hash: ${wcResult.txHash}\n Explorer: ${chainConfig.explorer}${wcResult.txHash}\n Error: ${receiptErr.message}`, 'TX_REVERTED');
|
|
1710
1680
|
}
|
|
1711
1681
|
|
|
1712
1682
|
log(`\n ✓ Transaction successful!`);
|
|
@@ -1896,10 +1866,7 @@ EXAMPLES:
|
|
|
1896
1866
|
lastQuoteError = `${quoteName} reverted on-chain`;
|
|
1897
1867
|
continue;
|
|
1898
1868
|
}
|
|
1899
|
-
|
|
1900
|
-
log(` This can happen due to: stale quotes, insufficient gas, or liquidity changes.`);
|
|
1901
|
-
exit(1);
|
|
1902
|
-
return;
|
|
1869
|
+
throw new CommandError(`\n ⚠ Transaction was broadcast but REVERTED on-chain!\n Tx Hash: ${result.txHash}\n Explorer: ${explorerUrl}\n Error: ${receiptErr.message}\n\n The trading API reported success, but the contract execution failed.\n This can happen due to: stale quotes, insufficient gas, or liquidity changes.`, 'TX_REVERTED');
|
|
1903
1870
|
}
|
|
1904
1871
|
}
|
|
1905
1872
|
|
|
@@ -1954,15 +1921,13 @@ EXAMPLES:
|
|
|
1954
1921
|
}
|
|
1955
1922
|
|
|
1956
1923
|
// All quotes exhausted
|
|
1957
|
-
|
|
1958
|
-
log('');
|
|
1959
|
-
exit(1);
|
|
1960
|
-
return undefined;
|
|
1924
|
+
throw new CommandError(`\n❌ All quotes failed. Last error: ${lastQuoteError || 'unknown'}\n`, 'ALL_QUOTES_FAILED');
|
|
1961
1925
|
|
|
1962
1926
|
} catch (err) {
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1927
|
+
if (err instanceof CommandError) throw err;
|
|
1928
|
+
let msg = `Error: ${err.message}`;
|
|
1929
|
+
if (err.details) msg += `\n Details: ${JSON.stringify(err.details)}`;
|
|
1930
|
+
throw new CommandError(msg, err.code || 'EXECUTE_ERROR');
|
|
1966
1931
|
}
|
|
1967
1932
|
},
|
|
1968
1933
|
|
|
@@ -1972,8 +1937,7 @@ EXAMPLES:
|
|
|
1972
1937
|
const toChain = options['to-chain'] || args[2];
|
|
1973
1938
|
|
|
1974
1939
|
if (!txHash || !fromChain || !toChain) {
|
|
1975
|
-
|
|
1976
|
-
Usage: nansen trade bridge-status --tx-hash <hash> --from-chain <chain> --to-chain <chain>
|
|
1940
|
+
throw new CommandError(`Usage: nansen trade bridge-status --tx-hash <hash> --from-chain <chain> --to-chain <chain>
|
|
1977
1941
|
|
|
1978
1942
|
Check the status of a cross-chain bridge transaction.
|
|
1979
1943
|
|
|
@@ -1983,10 +1947,7 @@ OPTIONS:
|
|
|
1983
1947
|
--to-chain <chain> Destination chain (solana or base)
|
|
1984
1948
|
|
|
1985
1949
|
EXAMPLES:
|
|
1986
|
-
nansen trade bridge-status --tx-hash 0xabc... --from-chain base --to-chain solana
|
|
1987
|
-
`);
|
|
1988
|
-
exit(1);
|
|
1989
|
-
return;
|
|
1950
|
+
nansen trade bridge-status --tx-hash 0xabc... --from-chain base --to-chain solana`, 'MISSING_ARGS');
|
|
1990
1951
|
}
|
|
1991
1952
|
|
|
1992
1953
|
try {
|
|
@@ -2010,9 +1971,10 @@ EXAMPLES:
|
|
|
2010
1971
|
if (status.lifiExplorerLink) log(` Li.Fi: ${status.lifiExplorerLink}`);
|
|
2011
1972
|
log('');
|
|
2012
1973
|
} catch (err) {
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
1974
|
+
if (err instanceof CommandError) throw err;
|
|
1975
|
+
let msg = `Error: ${err.message}`;
|
|
1976
|
+
if (err.details) msg += `\n Details: ${JSON.stringify(err.details)}`;
|
|
1977
|
+
throw new CommandError(msg, err.code || 'BRIDGE_STATUS_ERROR');
|
|
2016
1978
|
}
|
|
2017
1979
|
},
|
|
2018
1980
|
};
|
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
|
|
|
@@ -335,8 +336,8 @@ function resolveWalletPassword() {
|
|
|
335
336
|
*
|
|
336
337
|
* @param {object} config - wallet config (needs config.passwordHash)
|
|
337
338
|
* @param {object} flags - CLI flags
|
|
338
|
-
* @param {object} deps - { promptFn, log
|
|
339
|
-
* @returns {{ password: string|null, error:
|
|
339
|
+
* @param {object} deps - { promptFn, log }
|
|
340
|
+
* @returns {{ password: string|null, error: object|null }}
|
|
340
341
|
*/
|
|
341
342
|
async function resolvePasswordForCommand(config, flags, deps) {
|
|
342
343
|
if (!config.passwordHash) {
|
|
@@ -353,14 +354,14 @@ async function resolvePasswordForCommand(config, flags, deps) {
|
|
|
353
354
|
|
|
354
355
|
return {
|
|
355
356
|
password: null,
|
|
356
|
-
error:
|
|
357
|
+
error: {
|
|
357
358
|
error: 'PASSWORD_REQUIRED',
|
|
358
359
|
message: 'Wallet is encrypted and no password was found.',
|
|
359
360
|
resolution: [
|
|
360
361
|
'Set NANSEN_WALLET_PASSWORD environment variable',
|
|
361
362
|
'Or re-run wallet create with the password (it will be persisted for future use)',
|
|
362
363
|
],
|
|
363
|
-
}
|
|
364
|
+
},
|
|
364
365
|
};
|
|
365
366
|
}
|
|
366
367
|
|
|
@@ -580,7 +581,7 @@ export async function deleteWallet(name, password) {
|
|
|
580
581
|
* Build wallet command handlers for integration into CLI.
|
|
581
582
|
*/
|
|
582
583
|
export function buildWalletCommands(deps = {}) {
|
|
583
|
-
const { log = console.log
|
|
584
|
+
const { log = console.log } = deps;
|
|
584
585
|
|
|
585
586
|
return {
|
|
586
587
|
'wallet': async (args, apiInstance, flags, options) => {
|
|
@@ -599,9 +600,7 @@ export function buildWalletCommands(deps = {}) {
|
|
|
599
600
|
log('');
|
|
600
601
|
return;
|
|
601
602
|
} catch (err) {
|
|
602
|
-
|
|
603
|
-
exit(1);
|
|
604
|
-
return;
|
|
603
|
+
throw new CommandError(`❌ ${err.message}`, 'PRIVY_ERROR');
|
|
605
604
|
}
|
|
606
605
|
}
|
|
607
606
|
// All other subcommands fall through to unified handlers below
|
|
@@ -612,8 +611,8 @@ export function buildWalletCommands(deps = {}) {
|
|
|
612
611
|
const handlers = {
|
|
613
612
|
'create': async () => {
|
|
614
613
|
// Privy wallets are handled above — if we reach here with provider=privy,
|
|
615
|
-
// the privy path already
|
|
616
|
-
// where
|
|
614
|
+
// the privy path already threw CommandError. Guard against environments
|
|
615
|
+
// where throw does not propagate (agent frameworks, test harnesses).
|
|
617
616
|
if (isPrivy) return;
|
|
618
617
|
|
|
619
618
|
const name = options.name || args[1] || 'default';
|
|
@@ -628,28 +627,22 @@ export function buildWalletCommands(deps = {}) {
|
|
|
628
627
|
|
|
629
628
|
// Step 2: If --human flag, allow interactive prompt (requires TTY)
|
|
630
629
|
if (!password && flags.human && !process.stdin.isTTY && !deps.promptFn) {
|
|
631
|
-
|
|
630
|
+
throw new CommandError('--human requires an interactive terminal. Set NANSEN_WALLET_PASSWORD env var instead.', 'NOT_A_TTY', {
|
|
632
631
|
error: 'NOT_A_TTY',
|
|
633
632
|
message: '--human requires an interactive terminal. Set NANSEN_WALLET_PASSWORD env var instead.',
|
|
634
|
-
})
|
|
635
|
-
exit(1);
|
|
636
|
-
return;
|
|
633
|
+
});
|
|
637
634
|
}
|
|
638
635
|
if (!password && flags.human && (process.stdin.isTTY || deps.promptFn)) {
|
|
639
636
|
password = await promptPassword('Enter wallet password: ', deps);
|
|
640
637
|
if (password && password.length < 12) {
|
|
641
|
-
|
|
642
|
-
exit(1);
|
|
643
|
-
return;
|
|
638
|
+
throw new CommandError('❌ Password must be at least 12 characters', 'INVALID_INPUT');
|
|
644
639
|
}
|
|
645
640
|
if (password) {
|
|
646
641
|
const config = getWalletConfig();
|
|
647
642
|
if (!config.passwordHash) {
|
|
648
643
|
const confirm = await promptPassword('Confirm password: ', deps);
|
|
649
644
|
if (password !== confirm) {
|
|
650
|
-
|
|
651
|
-
exit(1);
|
|
652
|
-
return;
|
|
645
|
+
throw new CommandError('❌ Passwords do not match', 'INVALID_INPUT');
|
|
653
646
|
}
|
|
654
647
|
}
|
|
655
648
|
}
|
|
@@ -657,20 +650,16 @@ export function buildWalletCommands(deps = {}) {
|
|
|
657
650
|
|
|
658
651
|
// Step 3: No password available — return structured error for agents
|
|
659
652
|
if (!password) {
|
|
660
|
-
|
|
653
|
+
throw new CommandError('A wallet password is required. Ask the user to provide one.', 'PASSWORD_REQUIRED', {
|
|
661
654
|
error: 'PASSWORD_REQUIRED',
|
|
662
655
|
message: 'A wallet password is required. Ask the user to provide one.',
|
|
663
656
|
instructions: 'Re-run with: NANSEN_WALLET_PASSWORD=<password> nansen wallet create',
|
|
664
657
|
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;
|
|
658
|
+
});
|
|
668
659
|
}
|
|
669
660
|
|
|
670
661
|
if (password.length < 12) {
|
|
671
|
-
|
|
672
|
-
exit(1);
|
|
673
|
-
return;
|
|
662
|
+
throw new CommandError('❌ Password must be at least 12 characters', 'INVALID_INPUT');
|
|
674
663
|
}
|
|
675
664
|
}
|
|
676
665
|
|
|
@@ -678,9 +667,7 @@ export function buildWalletCommands(deps = {}) {
|
|
|
678
667
|
if (password !== null) {
|
|
679
668
|
const config = getWalletConfig();
|
|
680
669
|
if (config.passwordHash && !verifyPassword(password, config)) {
|
|
681
|
-
|
|
682
|
-
exit(1);
|
|
683
|
-
return;
|
|
670
|
+
throw new CommandError('❌ Incorrect password — does not match existing wallets.', 'INCORRECT_PASSWORD');
|
|
684
671
|
}
|
|
685
672
|
}
|
|
686
673
|
|
|
@@ -725,8 +712,7 @@ export function buildWalletCommands(deps = {}) {
|
|
|
725
712
|
log('');
|
|
726
713
|
return;
|
|
727
714
|
} catch (err) {
|
|
728
|
-
|
|
729
|
-
exit(1);
|
|
715
|
+
throw new CommandError(`❌ ${err.message}`, 'CREATE_FAILED');
|
|
730
716
|
}
|
|
731
717
|
},
|
|
732
718
|
|
|
@@ -750,9 +736,7 @@ export function buildWalletCommands(deps = {}) {
|
|
|
750
736
|
'show': async () => {
|
|
751
737
|
const name = options.name || args[1];
|
|
752
738
|
if (!name) {
|
|
753
|
-
|
|
754
|
-
exit(1);
|
|
755
|
-
return;
|
|
739
|
+
throw new CommandError('Usage: nansen wallet show <name>', 'MISSING_ARGS');
|
|
756
740
|
}
|
|
757
741
|
try {
|
|
758
742
|
const result = showWallet(name);
|
|
@@ -764,25 +748,20 @@ export function buildWalletCommands(deps = {}) {
|
|
|
764
748
|
log(` Created: ${result.createdAt}\n`);
|
|
765
749
|
return;
|
|
766
750
|
} catch (err) {
|
|
767
|
-
|
|
768
|
-
exit(1);
|
|
751
|
+
throw new CommandError(`❌ ${err.message}`, 'SHOW_FAILED');
|
|
769
752
|
}
|
|
770
753
|
},
|
|
771
754
|
|
|
772
755
|
'export': async () => {
|
|
773
756
|
const name = options.name || args[1];
|
|
774
757
|
if (!name) {
|
|
775
|
-
|
|
776
|
-
exit(1);
|
|
777
|
-
return;
|
|
758
|
+
throw new CommandError('Usage: nansen wallet export <name>', 'MISSING_ARGS');
|
|
778
759
|
}
|
|
779
760
|
|
|
780
761
|
const config = getWalletConfig();
|
|
781
762
|
const { password, error } = await resolvePasswordForCommand(config, flags, deps);
|
|
782
763
|
if (error) {
|
|
783
|
-
|
|
784
|
-
exit(1);
|
|
785
|
-
return;
|
|
764
|
+
throw new CommandError(error.message, 'PASSWORD_REQUIRED', error);
|
|
786
765
|
}
|
|
787
766
|
try {
|
|
788
767
|
const result = exportWallet(name, password);
|
|
@@ -796,34 +775,28 @@ export function buildWalletCommands(deps = {}) {
|
|
|
796
775
|
log('');
|
|
797
776
|
return;
|
|
798
777
|
} catch (err) {
|
|
799
|
-
|
|
800
|
-
exit(1);
|
|
778
|
+
throw new CommandError(`❌ ${err.message}`, 'EXPORT_FAILED');
|
|
801
779
|
}
|
|
802
780
|
},
|
|
803
781
|
|
|
804
782
|
'default': async () => {
|
|
805
783
|
const name = options.name || args[1];
|
|
806
784
|
if (!name) {
|
|
807
|
-
|
|
808
|
-
exit(1);
|
|
809
|
-
return;
|
|
785
|
+
throw new CommandError('Usage: nansen wallet default <name>', 'MISSING_ARGS');
|
|
810
786
|
}
|
|
811
787
|
try {
|
|
812
788
|
const result = setDefaultWallet(name);
|
|
813
789
|
log(`✓ Default wallet set to "${result.defaultWallet}"`);
|
|
814
790
|
return;
|
|
815
791
|
} catch (err) {
|
|
816
|
-
|
|
817
|
-
exit(1);
|
|
792
|
+
throw new CommandError(`❌ ${err.message}`, 'DEFAULT_FAILED');
|
|
818
793
|
}
|
|
819
794
|
},
|
|
820
795
|
|
|
821
796
|
'delete': async () => {
|
|
822
797
|
const name = options.name || args[1];
|
|
823
798
|
if (!name) {
|
|
824
|
-
|
|
825
|
-
exit(1);
|
|
826
|
-
return;
|
|
799
|
+
throw new CommandError('Usage: nansen wallet delete <name>', 'MISSING_ARGS');
|
|
827
800
|
}
|
|
828
801
|
|
|
829
802
|
// Check if this is a Privy wallet (no password needed)
|
|
@@ -839,9 +812,7 @@ export function buildWalletCommands(deps = {}) {
|
|
|
839
812
|
const config = getWalletConfig();
|
|
840
813
|
const resolved = await resolvePasswordForCommand(config, flags, deps);
|
|
841
814
|
if (resolved.error) {
|
|
842
|
-
|
|
843
|
-
exit(1);
|
|
844
|
-
return;
|
|
815
|
+
throw new CommandError(resolved.error.message, 'PASSWORD_REQUIRED', resolved.error);
|
|
845
816
|
}
|
|
846
817
|
password = resolved.password;
|
|
847
818
|
}
|
|
@@ -857,8 +828,7 @@ export function buildWalletCommands(deps = {}) {
|
|
|
857
828
|
}
|
|
858
829
|
return;
|
|
859
830
|
} catch (err) {
|
|
860
|
-
|
|
861
|
-
exit(1);
|
|
831
|
+
throw new CommandError(`❌ ${err.message}`, 'DELETE_FAILED');
|
|
862
832
|
}
|
|
863
833
|
},
|
|
864
834
|
|
|
@@ -866,28 +836,20 @@ export function buildWalletCommands(deps = {}) {
|
|
|
866
836
|
const { sendTokens } = await import('./transfer.js');
|
|
867
837
|
|
|
868
838
|
if (!options.to) {
|
|
869
|
-
|
|
870
|
-
exit(1);
|
|
871
|
-
return;
|
|
839
|
+
throw new CommandError('--to <address> is required', 'MISSING_ARGS');
|
|
872
840
|
}
|
|
873
841
|
|
|
874
842
|
const isMax = flags.max || options.amount === 'max';
|
|
875
843
|
if (!options.amount && !isMax) {
|
|
876
|
-
|
|
877
|
-
exit(1);
|
|
878
|
-
return;
|
|
844
|
+
throw new CommandError('--amount <number> or --max is required', 'MISSING_ARGS');
|
|
879
845
|
}
|
|
880
846
|
|
|
881
847
|
if (!options.chain) {
|
|
882
|
-
|
|
883
|
-
exit(1);
|
|
884
|
-
return;
|
|
848
|
+
throw new CommandError('--chain <evm|solana> is required', 'MISSING_ARGS');
|
|
885
849
|
}
|
|
886
850
|
|
|
887
851
|
if (!['evm', 'solana', 'ethereum', 'base'].includes(options.chain)) {
|
|
888
|
-
|
|
889
|
-
exit(1);
|
|
890
|
-
return;
|
|
852
|
+
throw new CommandError('--chain must be one of: evm, solana, ethereum, base', 'INVALID_INPUT');
|
|
891
853
|
}
|
|
892
854
|
|
|
893
855
|
const isWalletConnect = options.wallet === 'walletconnect' || options.wallet === 'wc';
|
|
@@ -912,9 +874,7 @@ export function buildWalletCommands(deps = {}) {
|
|
|
912
874
|
const sendConfig = getWalletConfig();
|
|
913
875
|
const resolved = await resolvePasswordForCommand(sendConfig, flags, deps);
|
|
914
876
|
if (resolved.error) {
|
|
915
|
-
|
|
916
|
-
exit(1);
|
|
917
|
-
return;
|
|
877
|
+
throw new CommandError(resolved.error.message, 'PASSWORD_REQUIRED', resolved.error);
|
|
918
878
|
}
|
|
919
879
|
password = resolved.password;
|
|
920
880
|
}
|
|
@@ -960,8 +920,7 @@ export function buildWalletCommands(deps = {}) {
|
|
|
960
920
|
log('');
|
|
961
921
|
return;
|
|
962
922
|
} catch (err) {
|
|
963
|
-
|
|
964
|
-
exit(1);
|
|
923
|
+
throw new CommandError(err.message, 'SEND_FAILED');
|
|
965
924
|
}
|
|
966
925
|
},
|
|
967
926
|
|
|
@@ -979,16 +938,14 @@ export function buildWalletCommands(deps = {}) {
|
|
|
979
938
|
'secure': async () => {
|
|
980
939
|
const { password, source } = retrievePassword();
|
|
981
940
|
if (!password) {
|
|
982
|
-
|
|
941
|
+
throw new CommandError('No wallet password found in any store.', 'NO_PASSWORD_FOUND', {
|
|
983
942
|
error: 'NO_PASSWORD_FOUND',
|
|
984
943
|
message: 'No wallet password found in any store.',
|
|
985
944
|
resolution: [
|
|
986
945
|
'Set NANSEN_WALLET_PASSWORD and run: nansen wallet secure',
|
|
987
946
|
'This will store it in the OS keychain.',
|
|
988
947
|
],
|
|
989
|
-
})
|
|
990
|
-
exit(1);
|
|
991
|
-
return;
|
|
948
|
+
});
|
|
992
949
|
}
|
|
993
950
|
|
|
994
951
|
if (source === 'keychain') {
|
|
@@ -999,20 +956,19 @@ export function buildWalletCommands(deps = {}) {
|
|
|
999
956
|
// Verify password actually decrypts wallets before overwriting keychain
|
|
1000
957
|
const walletConfig = getWalletConfig();
|
|
1001
958
|
if (walletConfig.passwordHash && !verifyPassword(password, walletConfig)) {
|
|
1002
|
-
|
|
959
|
+
const resolution = source === 'file'
|
|
960
|
+
? [
|
|
961
|
+
'The password in ~/.nansen/wallets/.credentials is incorrect.',
|
|
962
|
+
'Run: nansen wallet forget-password then re-run with the correct password: NANSEN_WALLET_PASSWORD=<pw> nansen wallet secure',
|
|
963
|
+
]
|
|
964
|
+
: [
|
|
965
|
+
'Unset NANSEN_WALLET_PASSWORD if it is stale, then re-run: nansen wallet secure',
|
|
966
|
+
];
|
|
967
|
+
throw new CommandError(`Password from '${source}' does not match the wallet's stored hash.`, 'INCORRECT_PASSWORD', {
|
|
1003
968
|
error: 'INCORRECT_PASSWORD',
|
|
1004
969
|
message: `Password from '${source}' does not match the wallet's stored hash.`,
|
|
1005
|
-
resolution
|
|
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;
|
|
970
|
+
resolution,
|
|
971
|
+
});
|
|
1016
972
|
}
|
|
1017
973
|
|
|
1018
974
|
// Try to migrate to keychain
|
|
@@ -1027,18 +983,17 @@ export function buildWalletCommands(deps = {}) {
|
|
|
1027
983
|
log(' Removed ~/.nansen/wallets/.credentials.');
|
|
1028
984
|
}
|
|
1029
985
|
} else {
|
|
1030
|
-
|
|
986
|
+
const msg = source === 'file'
|
|
987
|
+
? 'OS keychain is not available. Password remains in ~/.nansen/wallets/.credentials (insecure).'
|
|
988
|
+
: 'OS keychain is not available. Password is only in the NANSEN_WALLET_PASSWORD env var (not persisted).';
|
|
989
|
+
throw new CommandError(msg, 'KEYCHAIN_UNAVAILABLE', {
|
|
1031
990
|
error: 'KEYCHAIN_UNAVAILABLE',
|
|
1032
|
-
message:
|
|
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).',
|
|
991
|
+
message: msg,
|
|
1035
992
|
resolution: [
|
|
1036
993
|
'Set NANSEN_WALLET_PASSWORD in a secrets manager or system keyring',
|
|
1037
994
|
'Use a containerized secrets agent (e.g. Vault, 1Password CLI)',
|
|
1038
995
|
],
|
|
1039
|
-
})
|
|
1040
|
-
exit(1);
|
|
1041
|
-
return;
|
|
996
|
+
});
|
|
1042
997
|
}
|
|
1043
998
|
},
|
|
1044
999
|
|
|
@@ -1104,9 +1059,7 @@ EXAMPLES:
|
|
|
1104
1059
|
};
|
|
1105
1060
|
|
|
1106
1061
|
if (!handlers[subcommand]) {
|
|
1107
|
-
|
|
1108
|
-
exit(1);
|
|
1109
|
-
return;
|
|
1062
|
+
throw new CommandError(`Unknown subcommand: ${subcommand}. Available: ${Object.keys(handlers).join(', ')}`, 'UNKNOWN_COMMAND');
|
|
1110
1063
|
}
|
|
1111
1064
|
|
|
1112
1065
|
// --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,
|
|
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
|
-
*
|
|
25
|
+
* Returns base58-encoded PDA. Delegates algorithm to transfer.js.
|
|
27
26
|
*/
|
|
28
27
|
export function deriveATA(ownerBase58, mintBase58, tokenProgramBase58 = TOKEN_PROGRAM) {
|
|
29
|
-
|
|
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 =============
|