nansen-cli 1.5.1 → 1.7.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/AGENTS.md +222 -0
- package/README.md +36 -2
- package/SKILL.md +25 -21
- package/TODO.md +17 -0
- package/package.json +1 -1
- package/src/api.js +101 -29
- package/src/cli.js +65 -18
- package/src/crypto.js +215 -0
- package/src/trading.js +1081 -0
- package/src/transfer.js +723 -0
- package/src/wallet.js +764 -0
- package/src/x402-evm.js +207 -0
- package/src/x402-svm.js +474 -0
- package/src/x402.js +205 -0
package/src/cli.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { NansenAPI, NansenError, ErrorCode, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir, validateAddress, sleep } from './api.js';
|
|
7
|
+
import { buildWalletCommands } from './wallet.js';
|
|
8
|
+
import { buildTradingCommands } from './trading.js';
|
|
7
9
|
import fs from 'fs';
|
|
8
10
|
import { getUpdateNotification, scheduleUpdateCheck } from './update-check.js';
|
|
9
11
|
import { createRequire } from 'module';
|
|
@@ -165,12 +167,20 @@ export const SCHEMA = {
|
|
|
165
167
|
'token': {
|
|
166
168
|
description: 'Token God Mode - deep analytics for any token',
|
|
167
169
|
subcommands: {
|
|
170
|
+
'indicators': {
|
|
171
|
+
description: 'Risk and reward indicators for a token (Nansen Score)',
|
|
172
|
+
options: {
|
|
173
|
+
token: { type: 'string', required: true, description: 'Token address' },
|
|
174
|
+
chain: { type: 'string', default: 'ethereum' }
|
|
175
|
+
},
|
|
176
|
+
returns: ['token_info[market_cap_usd, market_cap_group, is_stablecoin]', 'risk_indicators[indicator_type, score, signal, signal_percentile, last_trigger_on]', 'reward_indicators[indicator_type, score, signal, signal_percentile, last_trigger_on]']
|
|
177
|
+
},
|
|
168
178
|
'info': {
|
|
169
179
|
description: 'Get detailed information for a specific token',
|
|
170
180
|
options: {
|
|
171
181
|
token: { type: 'string', required: true, description: 'Token address' },
|
|
172
182
|
chain: { type: 'string', default: 'solana' },
|
|
173
|
-
timeframe: { type: 'string', default: '
|
|
183
|
+
timeframe: { type: 'string', default: '1d', enum: ['5m', '1h', '6h', '12h', '1d', '7d'] }
|
|
174
184
|
},
|
|
175
185
|
returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'liquidity_usd']
|
|
176
186
|
},
|
|
@@ -279,6 +289,16 @@ export const SCHEMA = {
|
|
|
279
289
|
}
|
|
280
290
|
}
|
|
281
291
|
},
|
|
292
|
+
'search': {
|
|
293
|
+
description: 'Search for tokens and entities across Nansen',
|
|
294
|
+
options: {
|
|
295
|
+
query: { type: 'string', required: true, description: 'Search query (token name, symbol, address, or entity)' },
|
|
296
|
+
type: { type: 'string', default: 'any', enum: ['token', 'entity', 'any'], description: 'Result type filter' },
|
|
297
|
+
chain: { type: 'string', description: 'Filter by chain (e.g., ethereum, solana)' },
|
|
298
|
+
limit: { type: 'number', default: 25, description: 'Max results (1-50)' }
|
|
299
|
+
},
|
|
300
|
+
returns: ['tokens[name, symbol, chain, address, price, volume_24h, market_cap, rank]', 'entities[name, tags, rank]', 'total_results']
|
|
301
|
+
},
|
|
282
302
|
'points': {
|
|
283
303
|
description: 'Nansen Points analytics',
|
|
284
304
|
subcommands: {
|
|
@@ -299,7 +319,8 @@ export const SCHEMA = {
|
|
|
299
319
|
fields: { type: 'string', description: 'Comma-separated list of fields to include in output' },
|
|
300
320
|
'no-retry': { type: 'boolean', description: 'Disable automatic retry on rate limits/errors' },
|
|
301
321
|
retries: { type: 'number', default: 3, description: 'Max retry attempts' },
|
|
302
|
-
format: { type: 'string', enum: ['json', 'csv'], description: 'Output format (default: json)' }
|
|
322
|
+
format: { type: 'string', enum: ['json', 'csv'], description: 'Output format (default: json)' },
|
|
323
|
+
'x402-payment-signature': { type: 'string', description: 'Pre-signed x402 payment signature header' }
|
|
303
324
|
},
|
|
304
325
|
chains: ['ethereum', 'solana', 'base', 'bnb', 'arbitrum', 'polygon', 'optimism', 'avalanche', 'linea', 'scroll', 'mantle', 'ronin', 'sei', 'plasma', 'sonic', 'monad', 'hyperevm', 'iotaevm'],
|
|
305
326
|
smartMoneyLabels: ['Fund', 'Smart Trader', '30D Smart Trader', '90D Smart Trader', '180D Smart Trader', 'Smart HL Perps Trader']
|
|
@@ -820,14 +841,17 @@ COMMANDS:
|
|
|
820
841
|
login Save your API key (interactive)
|
|
821
842
|
logout Remove saved API key
|
|
822
843
|
schema Output JSON schema for all commands (for agent introspection)
|
|
844
|
+
wallet Local wallet management (create, list, show, export, default, delete)
|
|
845
|
+
quote Get a DEX swap quote (chain, tokens, amount)
|
|
846
|
+
execute Sign and broadcast a quoted trade
|
|
823
847
|
cache Cache management (clear)
|
|
824
848
|
smart-money Smart Money analytics (netflow, dex-trades, perp-trades, holdings, dcas, historical-holdings)
|
|
825
849
|
profiler Wallet profiling (balance, labels, transactions, pnl, pnl-summary, search,
|
|
826
850
|
historical-balances, related-wallets, counterparties, perp-positions, perp-trades,
|
|
827
851
|
batch, trace, compare)
|
|
828
|
-
token Token God Mode (info, screener, holders, flows, dex-trades, pnl,
|
|
829
|
-
flow-intelligence, transfers, jup-dca, perp-trades,
|
|
830
|
-
perp-pnl-leaderboard)
|
|
852
|
+
token Token God Mode (info, indicators, screener, holders, flows, dex-trades, pnl,
|
|
853
|
+
who-bought-sold, flow-intelligence, transfers, jup-dca, perp-trades,
|
|
854
|
+
perp-positions, perp-pnl-leaderboard)
|
|
831
855
|
portfolio Portfolio analytics (defi)
|
|
832
856
|
perp Perpetual futures analytics (screener, leaderboard)
|
|
833
857
|
points Nansen Points analytics (leaderboard)
|
|
@@ -847,6 +871,7 @@ GLOBAL OPTIONS:
|
|
|
847
871
|
--symbol Token symbol (for perp endpoints)
|
|
848
872
|
--no-retry Disable automatic retry on rate limits/errors
|
|
849
873
|
--retries <n> Max retry attempts (default: 3)
|
|
874
|
+
--x402-payment-signature <sig> Pre-signed x402 payment signature header
|
|
850
875
|
--cache Enable response caching (default: off)
|
|
851
876
|
--no-cache Disable cache for this request
|
|
852
877
|
--cache-ttl <s> Cache TTL in seconds (default: 300)
|
|
@@ -948,10 +973,16 @@ export function buildCommands(deps = {}) {
|
|
|
948
973
|
|
|
949
974
|
return {
|
|
950
975
|
'login': async (args, apiInstance, flags, options) => {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
976
|
+
// Support non-interactive: nansen login --api-key <key>
|
|
977
|
+
let apiKey = options['api-key'] || options.apiKey;
|
|
978
|
+
|
|
979
|
+
if (!apiKey) {
|
|
980
|
+
log('Nansen CLI Login\n');
|
|
981
|
+
log('Get your API key at: https://app.nansen.ai/api\n');
|
|
982
|
+
log('Tip: For non-interactive use: nansen login --api-key <key>\n');
|
|
983
|
+
|
|
984
|
+
apiKey = await promptFn('Enter your API key: ', true);
|
|
985
|
+
}
|
|
955
986
|
|
|
956
987
|
if (!apiKey || apiKey.trim().length === 0) {
|
|
957
988
|
log('\n❌ No API key provided');
|
|
@@ -1179,7 +1210,8 @@ export function buildCommands(deps = {}) {
|
|
|
1179
1210
|
}
|
|
1180
1211
|
|
|
1181
1212
|
const handlers = {
|
|
1182
|
-
'
|
|
1213
|
+
'indicators': () => apiInstance.tokenIndicators({ tokenAddress, chain }),
|
|
1214
|
+
'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe: options.timeframe }),
|
|
1183
1215
|
'screener': async () => {
|
|
1184
1216
|
const search = options.search;
|
|
1185
1217
|
// When searching, fetch more results to filter from (API has no server-side search)
|
|
@@ -1292,6 +1324,15 @@ export function buildCommands(deps = {}) {
|
|
|
1292
1324
|
return handlers[subcommand]();
|
|
1293
1325
|
},
|
|
1294
1326
|
|
|
1327
|
+
'search': async (args, apiInstance, flags, options) => {
|
|
1328
|
+
return apiInstance.generalSearch({
|
|
1329
|
+
query: args[0] || options.query,
|
|
1330
|
+
resultType: options.type,
|
|
1331
|
+
chain: options.chain,
|
|
1332
|
+
limit: options.limit
|
|
1333
|
+
});
|
|
1334
|
+
},
|
|
1335
|
+
|
|
1295
1336
|
'points': async (args, apiInstance, flags, options) => {
|
|
1296
1337
|
const subcommand = args[0] || 'help';
|
|
1297
1338
|
const tier = options.tier;
|
|
@@ -1316,7 +1357,7 @@ export function buildCommands(deps = {}) {
|
|
|
1316
1357
|
}
|
|
1317
1358
|
|
|
1318
1359
|
// Commands that don't require API authentication
|
|
1319
|
-
export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache'];
|
|
1360
|
+
export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache', 'wallet', 'quote', 'execute'];
|
|
1320
1361
|
|
|
1321
1362
|
// Command aliases for convenience
|
|
1322
1363
|
export const COMMAND_ALIASES = {
|
|
@@ -1459,7 +1500,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1459
1500
|
scheduleUpdateCheck();
|
|
1460
1501
|
const notify = () => { if (updateNotification) errorOutput(updateNotification); };
|
|
1461
1502
|
|
|
1462
|
-
const commands = { ...buildCommands(deps), ...commandOverrides };
|
|
1503
|
+
const commands = { ...buildCommands(deps), ...buildWalletCommands(deps), ...buildTradingCommands(deps), ...commandOverrides };
|
|
1463
1504
|
|
|
1464
1505
|
if (flags.version || flags.v) {
|
|
1465
1506
|
output(VERSION);
|
|
@@ -1492,10 +1533,12 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1492
1533
|
return { type: 'command-help', command };
|
|
1493
1534
|
}
|
|
1494
1535
|
}
|
|
1495
|
-
//
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1536
|
+
// Commands with handlers (e.g. quote, execute) show their own usage
|
|
1537
|
+
if (command === 'help' || !commands[command]) {
|
|
1538
|
+
output(BANNER + HELP);
|
|
1539
|
+
notify();
|
|
1540
|
+
return { type: 'help' };
|
|
1541
|
+
}
|
|
1499
1542
|
}
|
|
1500
1543
|
|
|
1501
1544
|
if (!commands[command]) {
|
|
@@ -1538,7 +1581,11 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1538
1581
|
ttl: options['cache-ttl'] !== undefined ? options['cache-ttl'] : 300
|
|
1539
1582
|
};
|
|
1540
1583
|
|
|
1541
|
-
const
|
|
1584
|
+
const defaultHeaders = {};
|
|
1585
|
+
if (options['x402-payment-signature']) {
|
|
1586
|
+
defaultHeaders['Payment-Signature'] = options['x402-payment-signature'];
|
|
1587
|
+
}
|
|
1588
|
+
const api = new NansenAPIClass(undefined, undefined, { retry: retryOptions, cache: cacheOptions, defaultHeaders });
|
|
1542
1589
|
let result = await commands[command](subArgs, api, flags, options);
|
|
1543
1590
|
|
|
1544
1591
|
// Apply field filtering if --fields is specified
|
|
@@ -1566,7 +1613,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1566
1613
|
} catch (error) {
|
|
1567
1614
|
const errorData = formatError(error);
|
|
1568
1615
|
const formatted = formatOutput(errorData, { pretty, table, csv });
|
|
1569
|
-
|
|
1616
|
+
output(formatted.text);
|
|
1570
1617
|
notify();
|
|
1571
1618
|
exit(1);
|
|
1572
1619
|
return { type: 'error', data: errorData };
|
package/src/crypto.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared cryptographic primitives for EVM transaction signing.
|
|
3
|
+
* Exports keccak256, secp256k1 ECDSA signing, RLP encoding.
|
|
4
|
+
* Zero external dependencies — uses Node.js built-in crypto only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
|
|
9
|
+
// ============= Keccak-256 =============
|
|
10
|
+
|
|
11
|
+
// Keccak-256 (NOT SHA3-256; Ethereum uses original Keccak with 0x01 padding).
|
|
12
|
+
// Uses a flat 25-element state array (lanes indexed as state[x + 5*y])
|
|
13
|
+
// with BigInt64 arithmetic.
|
|
14
|
+
|
|
15
|
+
const RC = [
|
|
16
|
+
0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
|
|
17
|
+
0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
|
|
18
|
+
0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
|
|
19
|
+
0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
|
|
20
|
+
0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
|
|
21
|
+
0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const ROT = [
|
|
25
|
+
0, 1, 62, 28, 27,
|
|
26
|
+
36, 44, 6, 55, 20,
|
|
27
|
+
3, 10, 43, 25, 39,
|
|
28
|
+
41, 45, 15, 21, 8,
|
|
29
|
+
18, 2, 61, 56, 14,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const M = 0xffffffffffffffffn;
|
|
33
|
+
|
|
34
|
+
function rot64(v, r) {
|
|
35
|
+
return r === 0 ? v : ((v << BigInt(r)) | (v >> BigInt(64 - r))) & M;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function keccakF(s) {
|
|
39
|
+
for (let round = 0; round < 24; round++) {
|
|
40
|
+
const c0 = s[0] ^ s[5] ^ s[10] ^ s[15] ^ s[20];
|
|
41
|
+
const c1 = s[1] ^ s[6] ^ s[11] ^ s[16] ^ s[21];
|
|
42
|
+
const c2 = s[2] ^ s[7] ^ s[12] ^ s[17] ^ s[22];
|
|
43
|
+
const c3 = s[3] ^ s[8] ^ s[13] ^ s[18] ^ s[23];
|
|
44
|
+
const c4 = s[4] ^ s[9] ^ s[14] ^ s[19] ^ s[24];
|
|
45
|
+
const d0 = (c4 ^ rot64(c1, 1)) & M;
|
|
46
|
+
const d1 = (c0 ^ rot64(c2, 1)) & M;
|
|
47
|
+
const d2 = (c1 ^ rot64(c3, 1)) & M;
|
|
48
|
+
const d3 = (c2 ^ rot64(c4, 1)) & M;
|
|
49
|
+
const d4 = (c3 ^ rot64(c0, 1)) & M;
|
|
50
|
+
for (let y = 0; y < 25; y += 5) {
|
|
51
|
+
s[y] = (s[y] ^ d0) & M;
|
|
52
|
+
s[y + 1] = (s[y + 1] ^ d1) & M;
|
|
53
|
+
s[y + 2] = (s[y + 2] ^ d2) & M;
|
|
54
|
+
s[y + 3] = (s[y + 3] ^ d3) & M;
|
|
55
|
+
s[y + 4] = (s[y + 4] ^ d4) & M;
|
|
56
|
+
}
|
|
57
|
+
const t = new Array(25);
|
|
58
|
+
for (let x = 0; x < 5; x++) {
|
|
59
|
+
for (let y = 0; y < 5; y++) {
|
|
60
|
+
const src = x + 5 * y;
|
|
61
|
+
const dst = y + 5 * ((2 * x + 3 * y) % 5);
|
|
62
|
+
t[dst] = rot64(s[src], ROT[src]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (let y = 0; y < 25; y += 5) {
|
|
66
|
+
const t0 = t[y], t1 = t[y+1], t2 = t[y+2], t3 = t[y+3], t4 = t[y+4];
|
|
67
|
+
s[y] = (t0 ^ ((~t1 & M) & t2)) & M;
|
|
68
|
+
s[y+1] = (t1 ^ ((~t2 & M) & t3)) & M;
|
|
69
|
+
s[y+2] = (t2 ^ ((~t3 & M) & t4)) & M;
|
|
70
|
+
s[y+3] = (t3 ^ ((~t4 & M) & t0)) & M;
|
|
71
|
+
s[y+4] = (t4 ^ ((~t0 & M) & t1)) & M;
|
|
72
|
+
}
|
|
73
|
+
s[0] = (s[0] ^ RC[round]) & M;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function keccak256(input) {
|
|
78
|
+
const rate = 136;
|
|
79
|
+
const s = new Array(25).fill(0n);
|
|
80
|
+
const blocks = Math.max(1, Math.ceil((input.length + 1) / rate));
|
|
81
|
+
const padded = Buffer.alloc(blocks * rate);
|
|
82
|
+
input.copy(padded);
|
|
83
|
+
padded[input.length] ^= 0x01;
|
|
84
|
+
padded[padded.length - 1] ^= 0x80;
|
|
85
|
+
for (let off = 0; off < padded.length; off += rate) {
|
|
86
|
+
for (let i = 0; i < 17; i++) {
|
|
87
|
+
s[i] ^= padded.readBigUInt64LE(off + i * 8);
|
|
88
|
+
}
|
|
89
|
+
keccakF(s);
|
|
90
|
+
}
|
|
91
|
+
const out = Buffer.alloc(32);
|
|
92
|
+
for (let i = 0; i < 4; i++) {
|
|
93
|
+
out.writeBigUInt64LE(s[i] & M, i * 8);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============= secp256k1 ECDSA =============
|
|
99
|
+
|
|
100
|
+
const P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2Fn;
|
|
101
|
+
const N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n;
|
|
102
|
+
const Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798n;
|
|
103
|
+
const Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8n;
|
|
104
|
+
|
|
105
|
+
function modInv(a, m) {
|
|
106
|
+
let [old_r, r] = [((a % m) + m) % m, m];
|
|
107
|
+
let [old_s, s] = [1n, 0n];
|
|
108
|
+
while (r !== 0n) {
|
|
109
|
+
const q = old_r / r;
|
|
110
|
+
[old_r, r] = [r, old_r - q * r];
|
|
111
|
+
[old_s, s] = [s, old_s - q * s];
|
|
112
|
+
}
|
|
113
|
+
return ((old_s % m) + m) % m;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ptAdd(x1, y1, x2, y2) {
|
|
117
|
+
if (x1 === null) return [x2, y2];
|
|
118
|
+
if (x2 === null) return [x1, y1];
|
|
119
|
+
if (x1 === x2 && y1 === y2) {
|
|
120
|
+
const lam = (3n * x1 * x1 * modInv(2n * y1, P)) % P;
|
|
121
|
+
const x3 = ((lam * lam - 2n * x1) % P + P) % P;
|
|
122
|
+
return [x3, ((lam * (x1 - x3) - y1) % P + P) % P];
|
|
123
|
+
}
|
|
124
|
+
if (x1 === x2) return [null, null];
|
|
125
|
+
const lam = (((y2 - y1) % P + P) * modInv(((x2 - x1) % P + P) % P, P)) % P;
|
|
126
|
+
const x3 = ((lam * lam - x1 - x2) % P + P) % P;
|
|
127
|
+
return [x3, ((lam * (x1 - x3) - y1) % P + P) % P];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ptMul(k, x, y) {
|
|
131
|
+
let [rx, ry] = [null, null];
|
|
132
|
+
let [qx, qy] = [x, y];
|
|
133
|
+
while (k > 0n) {
|
|
134
|
+
if (k & 1n) [rx, ry] = ptAdd(rx, ry, qx, qy);
|
|
135
|
+
[qx, qy] = ptAdd(qx, qy, qx, qy);
|
|
136
|
+
k >>= 1n;
|
|
137
|
+
}
|
|
138
|
+
return [rx, ry];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function rfc6979k(privBuf, hash) {
|
|
142
|
+
let v = Buffer.alloc(32, 0x01);
|
|
143
|
+
let k = Buffer.alloc(32, 0x00);
|
|
144
|
+
k = crypto.createHmac('sha256', k).update(Buffer.concat([v, Buffer.from([0x00]), privBuf, hash])).digest();
|
|
145
|
+
v = crypto.createHmac('sha256', k).update(v).digest();
|
|
146
|
+
k = crypto.createHmac('sha256', k).update(Buffer.concat([v, Buffer.from([0x01]), privBuf, hash])).digest();
|
|
147
|
+
v = crypto.createHmac('sha256', k).update(v).digest();
|
|
148
|
+
while (true) {
|
|
149
|
+
v = crypto.createHmac('sha256', k).update(v).digest();
|
|
150
|
+
const candidate = BigInt('0x' + v.toString('hex'));
|
|
151
|
+
if (candidate >= 1n && candidate < N) return candidate;
|
|
152
|
+
k = crypto.createHmac('sha256', k).update(Buffer.concat([v, Buffer.from([0x00])])).digest();
|
|
153
|
+
v = crypto.createHmac('sha256', k).update(v).digest();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Sign a 32-byte hash with secp256k1 ECDSA.
|
|
159
|
+
* Uses RFC 6979 deterministic k and low-S normalization (EIP-2).
|
|
160
|
+
* Returns { r, s, v } where v is the recovery ID (0 or 1).
|
|
161
|
+
*/
|
|
162
|
+
export function signSecp256k1(hash, privateKey) {
|
|
163
|
+
const z = BigInt('0x' + hash.toString('hex'));
|
|
164
|
+
const d = BigInt('0x' + privateKey.toString('hex'));
|
|
165
|
+
const k = rfc6979k(privateKey, hash);
|
|
166
|
+
const [rx, ry] = ptMul(k, Gx, Gy);
|
|
167
|
+
const r = rx % N;
|
|
168
|
+
if (r === 0n) throw new Error('Invalid signature: r=0');
|
|
169
|
+
let s = (modInv(k, N) * ((z + r * d) % N)) % N;
|
|
170
|
+
if (s === 0n) throw new Error('Invalid signature: s=0');
|
|
171
|
+
let v = (ry % 2n === 0n) ? 0 : 1;
|
|
172
|
+
if (s > N >> 1n) { s = N - s; v ^= 1; }
|
|
173
|
+
return {
|
|
174
|
+
r: Buffer.from(r.toString(16).padStart(64, '0'), 'hex'),
|
|
175
|
+
s: Buffer.from(s.toString(16).padStart(64, '0'), 'hex'),
|
|
176
|
+
v,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============= RLP Encoding =============
|
|
181
|
+
|
|
182
|
+
export function bigIntToMinBuf(n) {
|
|
183
|
+
if (n === 0n) return Buffer.alloc(0);
|
|
184
|
+
const hex = n.toString(16);
|
|
185
|
+
return Buffer.from(hex.length % 2 ? '0' + hex : hex, 'hex');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function rlpEncode(input) {
|
|
189
|
+
if (Array.isArray(input)) {
|
|
190
|
+
const encoded = input.map(rlpEncode);
|
|
191
|
+
const payload = Buffer.concat(encoded);
|
|
192
|
+
if (payload.length < 56) {
|
|
193
|
+
return Buffer.concat([Buffer.from([0xc0 + payload.length]), payload]);
|
|
194
|
+
}
|
|
195
|
+
const lenBytes = bigIntToMinBuf(BigInt(payload.length));
|
|
196
|
+
return Buffer.concat([Buffer.from([0xf7 + lenBytes.length]), lenBytes, payload]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let data;
|
|
200
|
+
if (Buffer.isBuffer(input)) {
|
|
201
|
+
data = input;
|
|
202
|
+
} else {
|
|
203
|
+
let hex = (typeof input === 'string' ? input : '').replace(/^0x/, '');
|
|
204
|
+
hex = hex.replace(/^0+/, '');
|
|
205
|
+
if (hex === '' || hex.length === 0) return Buffer.from([0x80]);
|
|
206
|
+
if (hex.length % 2) hex = '0' + hex;
|
|
207
|
+
data = Buffer.from(hex, 'hex');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (data.length === 0) return Buffer.from([0x80]);
|
|
211
|
+
if (data.length === 1 && data[0] < 0x80) return data;
|
|
212
|
+
if (data.length < 56) return Buffer.concat([Buffer.from([0x80 + data.length]), data]);
|
|
213
|
+
const lenBytes = bigIntToMinBuf(BigInt(data.length));
|
|
214
|
+
return Buffer.concat([Buffer.from([0xb7 + lenBytes.length]), lenBytes, data]);
|
|
215
|
+
}
|