nansen-cli 1.15.0 → 1.16.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 +14 -0
- package/package.json +1 -1
- package/src/cli.js +7 -3
- package/src/trading.js +53 -19
- package/src/transfer.js +1 -1
- package/src/walletconnect-trading.js +73 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#249](https://github.com/nansen-ai/nansen-cli/pull/249) [`0c17437`](https://github.com/nansen-ai/nansen-cli/commit/0c17437cf65d8b4f0516ede386adefec57d8ab3d) Thanks [@0xlaveen](https://github.com/0xlaveen)! - Add `pm` to top-level COMMAND_ALIASES so `nansen pm <subcommand>` works (previously only `nansen research pm <subcommand>` resolved the alias)
|
|
8
|
+
|
|
9
|
+
- [#244](https://github.com/nansen-ai/nansen-cli/pull/244) [`6427a9f`](https://github.com/nansen-ai/nansen-cli/commit/6427a9fdb7295dee94d7aed10cdb0164c46c7d73) Thanks [@Nicolai1205](https://github.com/Nicolai1205)! - Add 7 new agent skills: nansen-token-search, nansen-sm-trend, nansen-wallet-cluster, nansen-wallet-compare, nansen-token-indicators, nansen-cross-chain-flow, nansen-batch-wallet. All validated against live API.
|
|
10
|
+
|
|
11
|
+
## 1.16.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- [#196](https://github.com/nansen-ai/nansen-cli/pull/196) [`0c286c2`](https://github.com/nansen-ai/nansen-cli/commit/0c286c2d75f977894da8ff18a105aaf21f55f9f2) Thanks [@arein](https://github.com/arein)! - Add Solana WalletConnect support for trading (quote and execute). Solana wallets like Phantom and Solflare can now sign DEX swap transactions via WalletConnect v2.
|
|
16
|
+
|
|
3
17
|
## 1.15.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -654,8 +654,8 @@ export async function compareWallets(api, params = {}) {
|
|
|
654
654
|
shared_counterparties: sharedCpAddrs,
|
|
655
655
|
shared_tokens: sharedTokens,
|
|
656
656
|
balances: [
|
|
657
|
-
{ address: addr1, total_usd: tokens1.reduce((sum, t) => sum + (t.balance_usd
|
|
658
|
-
{ address: addr2, total_usd: tokens2.reduce((sum, t) => sum + (t.balance_usd
|
|
657
|
+
{ address: addr1, total_usd: tokens1.reduce((sum, t) => sum + (t.value_usd ?? t.balance_usd ?? 0), 0) },
|
|
658
|
+
{ address: addr2, total_usd: tokens2.reduce((sum, t) => sum + (t.value_usd ?? t.balance_usd ?? 0), 0) },
|
|
659
659
|
],
|
|
660
660
|
};
|
|
661
661
|
}
|
|
@@ -1240,6 +1240,9 @@ export function buildCommands(deps = {}) {
|
|
|
1240
1240
|
},
|
|
1241
1241
|
|
|
1242
1242
|
'prediction-market': async (args, apiInstance, flags, options) => {
|
|
1243
|
+
if (Date.now() < new Date('2026-03-16T00:00:00Z').getTime()) {
|
|
1244
|
+
process.stderr.write('⚠️ PnL data for prediction markets is temporarily unavailable while we improve accuracy. We\'ll update once resolved.\n');
|
|
1245
|
+
}
|
|
1243
1246
|
const subcommand = args[0] || 'help';
|
|
1244
1247
|
const marketId = options['market-id'];
|
|
1245
1248
|
const address = options.address;
|
|
@@ -1345,7 +1348,8 @@ export const COMMAND_ALIASES = {
|
|
|
1345
1348
|
'tgm': 'token', // Token God Mode
|
|
1346
1349
|
'sm': 'smart-money', // Smart Money
|
|
1347
1350
|
'prof': 'profiler', // Profiler
|
|
1348
|
-
'port': 'portfolio'
|
|
1351
|
+
'port': 'portfolio', // Portfolio
|
|
1352
|
+
'pm': 'prediction-market' // Prediction Market
|
|
1349
1353
|
};
|
|
1350
1354
|
|
|
1351
1355
|
// Aliases used inside the 'research' namespace
|
package/src/trading.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
import crypto from 'crypto';
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import path from 'path';
|
|
11
|
-
import { exportWallet, getWalletConfig, showWallet, listWallets } from './wallet.js';
|
|
11
|
+
import { base58Encode, exportWallet, getWalletConfig, showWallet, listWallets } from './wallet.js';
|
|
12
12
|
import { base58Decode } from './transfer.js';
|
|
13
13
|
import { keccak256, signSecp256k1, rlpEncode } from './crypto.js';
|
|
14
|
-
import { getWalletConnectAddress, sendTransactionViaWalletConnect, sendApprovalViaWalletConnect } from './walletconnect-trading.js';
|
|
14
|
+
import { getWalletConnectAddress, sendTransactionViaWalletConnect, sendSolanaTransactionViaWalletConnect, sendApprovalViaWalletConnect } from './walletconnect-trading.js';
|
|
15
15
|
import { retrievePassword } from './keychain.js';
|
|
16
16
|
|
|
17
17
|
// ============= Constants =============
|
|
@@ -746,7 +746,7 @@ OPTIONS:
|
|
|
746
746
|
--from <symbol|address> Input token (symbol like SOL, USDC or address)
|
|
747
747
|
--to <symbol|address> Output token (symbol like USDC, ETH or address)
|
|
748
748
|
--amount <units> Amount in BASE UNITS (e.g. lamports, wei)
|
|
749
|
-
--wallet <name> Wallet name (default: default wallet). Use "walletconnect" or "wc" for WalletConnect
|
|
749
|
+
--wallet <name> Wallet name (default: default wallet). Use "walletconnect" or "wc" for WalletConnect.
|
|
750
750
|
--slippage <pct> Slippage as decimal (e.g. 0.03 for 3%). Default: 0.03
|
|
751
751
|
--auto-slippage Enable auto slippage calculation
|
|
752
752
|
--max-auto-slippage <pct> Max auto slippage when auto-slippage enabled
|
|
@@ -778,12 +778,7 @@ EXAMPLES:
|
|
|
778
778
|
let walletProvider = 'local';
|
|
779
779
|
let privyWalletIds = null;
|
|
780
780
|
if (isWalletConnect) {
|
|
781
|
-
|
|
782
|
-
log('WalletConnect is only supported for EVM chains');
|
|
783
|
-
exit(1);
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
walletAddress = await getWalletConnectAddress();
|
|
781
|
+
walletAddress = await getWalletConnectAddress(chainType);
|
|
787
782
|
if (!walletAddress) {
|
|
788
783
|
log('No WalletConnect session active. Run: walletconnect connect');
|
|
789
784
|
exit(1);
|
|
@@ -970,12 +965,7 @@ EXAMPLES:
|
|
|
970
965
|
exported = exportWallet(effectiveWalletName, password);
|
|
971
966
|
} else {
|
|
972
967
|
// Verify WalletConnect session is still active and address matches quote
|
|
973
|
-
|
|
974
|
-
log('WalletConnect is only supported for EVM chains');
|
|
975
|
-
exit(1);
|
|
976
|
-
return;
|
|
977
|
-
}
|
|
978
|
-
const wcAddress = await getWalletConnectAddress();
|
|
968
|
+
const wcAddress = await getWalletConnectAddress(chainType);
|
|
979
969
|
if (!wcAddress) {
|
|
980
970
|
log('No WalletConnect session active. Run: walletconnect connect');
|
|
981
971
|
exit(1);
|
|
@@ -984,7 +974,9 @@ EXAMPLES:
|
|
|
984
974
|
// Check address matches the one used during quoting
|
|
985
975
|
const quoteWallet = quoteData.response?.quotes?.[0]?.transaction?.from
|
|
986
976
|
|| quoteData.response?.metadata?.userWalletAddress;
|
|
987
|
-
if (quoteWallet &&
|
|
977
|
+
if (quoteWallet && (chainType === 'solana'
|
|
978
|
+
? wcAddress.trim() !== quoteWallet.trim()
|
|
979
|
+
: wcAddress.toLowerCase().trim() !== quoteWallet.toLowerCase().trim())) {
|
|
988
980
|
log(`Connected wallet (${wcAddress}) doesn't match quote. Get a new quote with --wallet walletconnect`);
|
|
989
981
|
exit(1);
|
|
990
982
|
return;
|
|
@@ -1176,13 +1168,55 @@ EXAMPLES:
|
|
|
1176
1168
|
if (typeof txBase64 === 'object' && txBase64.data) {
|
|
1177
1169
|
txBase64 = base58Decode(txBase64.data).toString('base64');
|
|
1178
1170
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1171
|
+
|
|
1172
|
+
if (isWalletConnect) {
|
|
1173
|
+
// Solana via WalletConnect: convert base64 → base58 for WC protocol
|
|
1174
|
+
log(' Signing Solana transaction via WalletConnect...');
|
|
1175
|
+
let txBase58;
|
|
1176
|
+
try {
|
|
1177
|
+
txBase58 = base58Encode(Buffer.from(txBase64, 'base64'));
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
throw new Error(`Failed to encode transaction for WalletConnect: ${err.message}`, { cause: err });
|
|
1180
|
+
}
|
|
1181
|
+
const wcResult = await sendSolanaTransactionViaWalletConnect(txBase58);
|
|
1182
|
+
|
|
1183
|
+
if (wcResult.signedTransaction) {
|
|
1184
|
+
signedTransaction = base58Decode(wcResult.signedTransaction).toString('base64');
|
|
1185
|
+
} else if (wcResult.signature) {
|
|
1186
|
+
// Wallet returned raw Ed25519 sig → inject into unsigned tx
|
|
1187
|
+
let sigBytes;
|
|
1188
|
+
try {
|
|
1189
|
+
sigBytes = base58Decode(wcResult.signature);
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
throw new Error(`Invalid base58 signature from WalletConnect: ${err.message}`, { cause: err });
|
|
1192
|
+
}
|
|
1193
|
+
if (sigBytes.length !== 64) {
|
|
1194
|
+
throw new Error(`Invalid Ed25519 signature length: expected 64 bytes, got ${sigBytes.length}`);
|
|
1195
|
+
}
|
|
1196
|
+
// Buffer.from() creates a new buffer — safe to mutate in-place
|
|
1197
|
+
const txBytes = Buffer.from(txBase64, 'base64');
|
|
1198
|
+
const { value: sigCount, size: sigCountSize } = readCompactU16(txBytes, 0);
|
|
1199
|
+
if (sigCount < 1) {
|
|
1200
|
+
throw new Error('Transaction has no signature slots');
|
|
1201
|
+
}
|
|
1202
|
+
if (txBytes.length < sigCountSize + 64) {
|
|
1203
|
+
throw new Error(`Transaction buffer too small for signature: need ${sigCountSize + 64}, got ${txBytes.length}`);
|
|
1204
|
+
}
|
|
1205
|
+
// Inject into the first signature slot (feePayer)
|
|
1206
|
+
sigBytes.copy(txBytes, sigCountSize);
|
|
1207
|
+
signedTransaction = txBytes.toString('base64');
|
|
1208
|
+
} else {
|
|
1209
|
+
throw new Error('WalletConnect returned neither signedTransaction nor signature');
|
|
1210
|
+
}
|
|
1211
|
+
} else {
|
|
1212
|
+
log(' Signing Solana transaction...');
|
|
1213
|
+
signedTransaction = signSolanaTransaction(txBase64, exported.solana.privateKey);
|
|
1214
|
+
}
|
|
1181
1215
|
requestId = currentQuote.metadata?.requestId;
|
|
1182
1216
|
|
|
1183
1217
|
} else if (isWalletConnect) {
|
|
1184
1218
|
// EVM via WalletConnect: wallet signs and may broadcast
|
|
1185
|
-
const wcAddress = await getWalletConnectAddress();
|
|
1219
|
+
const wcAddress = await getWalletConnectAddress(chainType);
|
|
1186
1220
|
const isNative = isNativeToken(currentQuote.inputMint);
|
|
1187
1221
|
|
|
1188
1222
|
// Validate transaction.value (same checks as local wallet)
|
package/src/transfer.js
CHANGED
|
@@ -703,7 +703,7 @@ export async function sendTokens({ to, amount, chain, token = null, wallet = nul
|
|
|
703
703
|
|
|
704
704
|
if (walletconnect) {
|
|
705
705
|
if (chain === 'solana') {
|
|
706
|
-
throw new Error('WalletConnect
|
|
706
|
+
throw new Error('WalletConnect Solana transfers are not yet supported. Use a local wallet for Solana transfers.');
|
|
707
707
|
}
|
|
708
708
|
return sendTokensViaWalletConnect({ to, amount, chain, token, max, dryRun });
|
|
709
709
|
}
|
|
@@ -5,21 +5,62 @@
|
|
|
5
5
|
* (hardware wallets, mobile wallets) instead of local key storage.
|
|
6
6
|
* Uses the walletconnect CLI binary (subprocess-based, same as x402).
|
|
7
7
|
*
|
|
8
|
-
* EVM
|
|
8
|
+
* Supports EVM chains and Solana (trading only).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { wcExec } from './walletconnect-exec.js';
|
|
12
12
|
|
|
13
|
+
const SOLANA_MAINNET_CHAIN = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract the first JSON line from walletconnect CLI output.
|
|
17
|
+
* The CLI may print status messages before the JSON result.
|
|
18
|
+
*/
|
|
19
|
+
function parseWcJson(output) {
|
|
20
|
+
const lines = output.split('\n');
|
|
21
|
+
const startIdx = lines.findIndex(l => l.trimStart().startsWith('{'));
|
|
22
|
+
if (startIdx === -1) throw new Error('No JSON output from walletconnect');
|
|
23
|
+
|
|
24
|
+
// Handle multi-line JSON: collect lines until braces balance
|
|
25
|
+
let braces = 0;
|
|
26
|
+
const jsonLines = [];
|
|
27
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
28
|
+
jsonLines.push(lines[i]);
|
|
29
|
+
for (const ch of lines[i]) {
|
|
30
|
+
if (ch === '{') braces++;
|
|
31
|
+
else if (ch === '}') braces--;
|
|
32
|
+
}
|
|
33
|
+
if (braces === 0) break;
|
|
34
|
+
}
|
|
35
|
+
return JSON.parse(jsonLines.join('\n'));
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
/**
|
|
14
39
|
* Get the address of the connected WalletConnect wallet.
|
|
15
40
|
* Returns the first account address, or null if not connected / binary missing.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} [chainType] - Optional: 'evm' or 'solana'. Filters accounts by chain prefix.
|
|
43
|
+
* No arg = first account (backward compat).
|
|
16
44
|
*/
|
|
17
|
-
export async function getWalletConnectAddress() {
|
|
45
|
+
export async function getWalletConnectAddress(chainType) {
|
|
18
46
|
try {
|
|
19
47
|
const output = await wcExec('walletconnect', ['whoami', '--json'], 3000);
|
|
20
48
|
const data = JSON.parse(output);
|
|
21
49
|
if (data.connected === false) return null;
|
|
22
|
-
|
|
50
|
+
const accounts = data.accounts || [];
|
|
51
|
+
if (!accounts.length) return null;
|
|
52
|
+
|
|
53
|
+
if (chainType === 'solana') {
|
|
54
|
+
// Match Solana mainnet only — reject devnet/testnet to prevent wrong-network trades
|
|
55
|
+
const solAccount = accounts.find(a => a.chain === SOLANA_MAINNET_CHAIN);
|
|
56
|
+
return solAccount?.address || null;
|
|
57
|
+
}
|
|
58
|
+
if (chainType === 'evm') {
|
|
59
|
+
const evmAccount = accounts.find(a => a.chain?.startsWith('eip155:'));
|
|
60
|
+
return evmAccount?.address || null;
|
|
61
|
+
}
|
|
62
|
+
// No filter — return first account address (backward compat)
|
|
63
|
+
return accounts[0]?.address || null;
|
|
23
64
|
} catch {
|
|
24
65
|
return null;
|
|
25
66
|
}
|
|
@@ -50,13 +91,8 @@ export async function sendTransactionViaWalletConnect(txData, timeoutMs = 120000
|
|
|
50
91
|
};
|
|
51
92
|
|
|
52
93
|
const output = await wcExec('walletconnect', ['send-transaction', JSON.stringify(payload)], timeoutMs);
|
|
94
|
+
const result = parseWcJson(output);
|
|
53
95
|
|
|
54
|
-
// walletconnect may print status messages before the JSON line — extract JSON only
|
|
55
|
-
const jsonLine = output.split('\n').find(line => line.startsWith('{'));
|
|
56
|
-
if (!jsonLine) throw new Error('No JSON output from walletconnect send-transaction');
|
|
57
|
-
const result = JSON.parse(jsonLine);
|
|
58
|
-
|
|
59
|
-
// The CLI returns { transactionHash: "0x..." }
|
|
60
96
|
if (result.transactionHash) return { txHash: result.transactionHash };
|
|
61
97
|
if (result.txHash) return { txHash: result.txHash };
|
|
62
98
|
if (result.signedTransaction) return { signedTransaction: result.signedTransaction };
|
|
@@ -89,3 +125,31 @@ export async function sendApprovalViaWalletConnect(tokenAddress, spenderAddress,
|
|
|
89
125
|
chainId,
|
|
90
126
|
});
|
|
91
127
|
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Sign a Solana transaction via WalletConnect.
|
|
131
|
+
*
|
|
132
|
+
* The wallet signs the transaction and returns either:
|
|
133
|
+
* - { signedTransaction: "<base58>" } — full signed transaction
|
|
134
|
+
* - { signature: "<base58>" } — raw Ed25519 signature only
|
|
135
|
+
*
|
|
136
|
+
* @param {string} txBase58 - Base58-encoded Solana transaction
|
|
137
|
+
* @param {number} [timeoutMs=120000] - Timeout for user approval
|
|
138
|
+
* @returns {{ signedTransaction?: string, signature?: string }}
|
|
139
|
+
*/
|
|
140
|
+
export async function sendSolanaTransactionViaWalletConnect(txBase58, timeoutMs = 120000) {
|
|
141
|
+
const payload = {
|
|
142
|
+
transaction: txBase58,
|
|
143
|
+
chainId: SOLANA_MAINNET_CHAIN,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const output = await wcExec('walletconnect', ['send-transaction', JSON.stringify(payload)], timeoutMs);
|
|
147
|
+
const result = parseWcJson(output);
|
|
148
|
+
|
|
149
|
+
if (result.signedTransaction) return { signedTransaction: result.signedTransaction };
|
|
150
|
+
if (result.signature) return { signature: result.signature };
|
|
151
|
+
// Some wallets (e.g. Phantom) return 'transaction' instead of 'signedTransaction'
|
|
152
|
+
if (result.transaction) return { signedTransaction: result.transaction };
|
|
153
|
+
|
|
154
|
+
throw new Error('Unexpected response from walletconnect Solana sign');
|
|
155
|
+
}
|