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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.15.0",
3
+ "version": "1.16.1",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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 || 0), 0) },
658
- { address: addr2, total_usd: tokens2.reduce((sum, t) => sum + (t.balance_usd || 0), 0) },
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' // 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 (EVM only).
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
- if (chainType !== 'evm') {
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
- if (chainType !== 'evm') {
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 && wcAddress.toLowerCase() !== quoteWallet.toLowerCase()) {
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
- log(' Signing Solana transaction...');
1180
- signedTransaction = signSolanaTransaction(txBase64, exported.solana.privateKey);
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 is only supported for EVM chains');
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 only Solana via WalletConnect is not supported.
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
- return data.accounts?.[0]?.address || null;
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
+ }