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/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/skills/nansen-alerts-webhook-listener/SKILL.md +386 -0
- package/skills/nansen-trading/SKILL.md +12 -1
- package/src/api.js +82 -14
- package/src/cli.js +56 -40
- package/src/index.js +1 -1
- package/src/schema.json +39 -6
- package/src/telemetry.js +8 -6
- package/src/trade-validation.js +58 -0
- package/src/trading.js +55 -93
- package/src/transfer.js +13 -10
- package/src/wallet.js +66 -117
- package/src/x402-svm.js +3 -23
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/transfer.js
CHANGED
|
@@ -21,6 +21,16 @@ const ATA_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
|
|
|
21
21
|
// Alias: buildEvmTransaction uses 'evm' as a generic fallback
|
|
22
22
|
const CHAIN_IDS = { ...EVM_CHAIN_IDS, evm: 1 };
|
|
23
23
|
|
|
24
|
+
// ============= Address Derivation =============
|
|
25
|
+
|
|
26
|
+
function deriveEvmAddress(privateKeyHex) {
|
|
27
|
+
const privBuf = Buffer.from(privateKeyHex, 'hex');
|
|
28
|
+
const ecdh = crypto.createECDH('secp256k1');
|
|
29
|
+
ecdh.setPrivateKey(privBuf);
|
|
30
|
+
const pubKey = ecdh.getPublicKey(null, 'uncompressed');
|
|
31
|
+
return '0x' + keccak256(pubKey.subarray(1)).subarray(12).toString('hex');
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
// ============= Address Validation =============
|
|
25
35
|
|
|
26
36
|
function validateEvmAddress(address) {
|
|
@@ -106,12 +116,9 @@ async function buildEvmTransaction({ to, amount, token, privateKey, chain, max =
|
|
|
106
116
|
const rpcUrl = CHAIN_RPCS[chain] || CHAIN_RPCS.evm;
|
|
107
117
|
const chainId = CHAIN_IDS[chain] || 1;
|
|
108
118
|
|
|
109
|
-
// Derive address
|
|
119
|
+
// Derive address and buffer for signing
|
|
110
120
|
const privBuf = Buffer.from(privateKey, 'hex');
|
|
111
|
-
const
|
|
112
|
-
ecdh.setPrivateKey(privBuf);
|
|
113
|
-
const pubKey = ecdh.getPublicKey(null, 'uncompressed');
|
|
114
|
-
const from = '0x' + keccak256(pubKey.subarray(1)).subarray(12).toString('hex');
|
|
121
|
+
const from = deriveEvmAddress(privateKey);
|
|
115
122
|
|
|
116
123
|
// Nonce
|
|
117
124
|
const nonceHex = await rpcCall(rpcUrl, 'eth_getTransactionCount', [from, 'latest']);
|
|
@@ -735,11 +742,7 @@ export async function sendTokens({ to, amount, chain, token = null, wallet = nul
|
|
|
735
742
|
|
|
736
743
|
if (max && token) {
|
|
737
744
|
// Max ERC-20: full token balance
|
|
738
|
-
const
|
|
739
|
-
const ecdh = crypto.createECDH('secp256k1');
|
|
740
|
-
ecdh.setPrivateKey(privBuf);
|
|
741
|
-
const pubKey = ecdh.getPublicKey(null, 'uncompressed');
|
|
742
|
-
const from = '0x' + keccak256(pubKey.subarray(1)).subarray(12).toString('hex');
|
|
745
|
+
const from = deriveEvmAddress(walletData.evm.privateKey);
|
|
743
746
|
const balResult = await rpcCall(rpcUrl, 'eth_call', [{
|
|
744
747
|
to: token, data: '0x70a08231' + from.slice(2).padStart(64, '0'),
|
|
745
748
|
}, 'latest']);
|