nansen-cli 1.0.2 → 1.1.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/CLAUDE.md +185 -0
- package/README.md +52 -10
- package/TODO.md +47 -0
- package/package.json +1 -1
- package/src/api.js +259 -42
- package/src/cli.js +843 -0
- package/src/index.js +4 -499
package/src/api.js
CHANGED
|
@@ -9,6 +9,102 @@ import { fileURLToPath } from 'url';
|
|
|
9
9
|
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
|
|
12
|
+
// ============= Error Codes =============
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Structured error codes for programmatic handling by AI agents
|
|
16
|
+
*/
|
|
17
|
+
export const ErrorCode = {
|
|
18
|
+
// Authentication & Authorization
|
|
19
|
+
UNAUTHORIZED: 'UNAUTHORIZED', // 401 - Invalid or missing API key
|
|
20
|
+
FORBIDDEN: 'FORBIDDEN', // 403 - Valid key but insufficient permissions
|
|
21
|
+
|
|
22
|
+
// Rate Limiting
|
|
23
|
+
RATE_LIMITED: 'RATE_LIMITED', // 429 - Too many requests
|
|
24
|
+
|
|
25
|
+
// Validation Errors
|
|
26
|
+
INVALID_ADDRESS: 'INVALID_ADDRESS', // Address format validation failed
|
|
27
|
+
INVALID_TOKEN: 'INVALID_TOKEN', // Token address validation failed
|
|
28
|
+
INVALID_CHAIN: 'INVALID_CHAIN', // Unsupported or invalid chain
|
|
29
|
+
INVALID_PARAMS: 'INVALID_PARAMS', // Generic parameter validation error
|
|
30
|
+
MISSING_PARAM: 'MISSING_PARAM', // Required parameter not provided
|
|
31
|
+
|
|
32
|
+
// Resource Errors
|
|
33
|
+
NOT_FOUND: 'NOT_FOUND', // 404 - Resource not found
|
|
34
|
+
TOKEN_NOT_FOUND: 'TOKEN_NOT_FOUND', // Token doesn't exist
|
|
35
|
+
ADDRESS_NOT_FOUND: 'ADDRESS_NOT_FOUND', // Address has no data
|
|
36
|
+
|
|
37
|
+
// Server Errors
|
|
38
|
+
SERVER_ERROR: 'SERVER_ERROR', // 500+ - Nansen API internal error
|
|
39
|
+
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE', // 503 - API temporarily down
|
|
40
|
+
|
|
41
|
+
// Client Errors
|
|
42
|
+
NETWORK_ERROR: 'NETWORK_ERROR', // Connection failed
|
|
43
|
+
TIMEOUT: 'TIMEOUT', // Request timed out
|
|
44
|
+
|
|
45
|
+
// Generic
|
|
46
|
+
UNKNOWN: 'UNKNOWN', // Unclassified error
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Custom error class with structured error codes
|
|
51
|
+
*/
|
|
52
|
+
export class NansenError extends Error {
|
|
53
|
+
constructor(message, code = ErrorCode.UNKNOWN, status = null, data = null) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = 'NansenError';
|
|
56
|
+
this.code = code;
|
|
57
|
+
this.status = status;
|
|
58
|
+
this.data = data;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
toJSON() {
|
|
62
|
+
return {
|
|
63
|
+
error: this.message,
|
|
64
|
+
code: this.code,
|
|
65
|
+
status: this.status,
|
|
66
|
+
details: this.data,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Map HTTP status codes to error codes
|
|
73
|
+
*/
|
|
74
|
+
function statusToErrorCode(status, data = {}) {
|
|
75
|
+
const message = data?.message || data?.error || '';
|
|
76
|
+
const messageLower = message.toLowerCase();
|
|
77
|
+
|
|
78
|
+
switch (status) {
|
|
79
|
+
case 400:
|
|
80
|
+
if (messageLower.includes('address')) return ErrorCode.INVALID_ADDRESS;
|
|
81
|
+
if (messageLower.includes('token')) return ErrorCode.INVALID_TOKEN;
|
|
82
|
+
if (messageLower.includes('chain')) return ErrorCode.INVALID_CHAIN;
|
|
83
|
+
return ErrorCode.INVALID_PARAMS;
|
|
84
|
+
case 401:
|
|
85
|
+
return ErrorCode.UNAUTHORIZED;
|
|
86
|
+
case 403:
|
|
87
|
+
return ErrorCode.FORBIDDEN;
|
|
88
|
+
case 404:
|
|
89
|
+
if (messageLower.includes('token')) return ErrorCode.TOKEN_NOT_FOUND;
|
|
90
|
+
if (messageLower.includes('address') || messageLower.includes('wallet')) return ErrorCode.ADDRESS_NOT_FOUND;
|
|
91
|
+
return ErrorCode.NOT_FOUND;
|
|
92
|
+
case 429:
|
|
93
|
+
return ErrorCode.RATE_LIMITED;
|
|
94
|
+
case 500:
|
|
95
|
+
case 502:
|
|
96
|
+
return ErrorCode.SERVER_ERROR;
|
|
97
|
+
case 503:
|
|
98
|
+
return ErrorCode.SERVICE_UNAVAILABLE;
|
|
99
|
+
case 504:
|
|
100
|
+
return ErrorCode.TIMEOUT;
|
|
101
|
+
default:
|
|
102
|
+
if (status >= 500) return ErrorCode.SERVER_ERROR;
|
|
103
|
+
if (status >= 400) return ErrorCode.INVALID_PARAMS;
|
|
104
|
+
return ErrorCode.UNKNOWN;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
12
108
|
// ============= Config Paths =============
|
|
13
109
|
|
|
14
110
|
const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.nansen');
|
|
@@ -70,26 +166,26 @@ const EVM_CHAINS = [
|
|
|
70
166
|
* Validate address format for a given chain
|
|
71
167
|
* @param {string} address - The address to validate
|
|
72
168
|
* @param {string} chain - The blockchain (ethereum, solana, etc.)
|
|
73
|
-
* @returns {{valid: boolean, error?: string}}
|
|
169
|
+
* @returns {{valid: boolean, error?: string, code?: string}}
|
|
74
170
|
*/
|
|
75
171
|
export function validateAddress(address, chain = 'ethereum') {
|
|
76
172
|
if (!address || typeof address !== 'string') {
|
|
77
|
-
return { valid: false, error: 'Address is required' };
|
|
173
|
+
return { valid: false, error: 'Address is required', code: ErrorCode.MISSING_PARAM };
|
|
78
174
|
}
|
|
79
175
|
|
|
80
176
|
const trimmed = address.trim();
|
|
81
177
|
|
|
82
178
|
if (EVM_CHAINS.includes(chain)) {
|
|
83
179
|
if (!ADDRESS_PATTERNS.evm.test(trimmed)) {
|
|
84
|
-
return { valid: false, error: `Invalid EVM address format. Expected 0x followed by 40 hex characters
|
|
180
|
+
return { valid: false, error: `Invalid EVM address format. Expected 0x followed by 40 hex characters.`, code: ErrorCode.INVALID_ADDRESS };
|
|
85
181
|
}
|
|
86
182
|
} else if (chain === 'solana') {
|
|
87
183
|
if (!ADDRESS_PATTERNS.solana.test(trimmed)) {
|
|
88
|
-
return { valid: false, error: `Invalid Solana address format. Expected Base58 string (32-44 chars)
|
|
184
|
+
return { valid: false, error: `Invalid Solana address format. Expected Base58 string (32-44 chars).`, code: ErrorCode.INVALID_ADDRESS };
|
|
89
185
|
}
|
|
90
186
|
} else if (chain === 'bitcoin') {
|
|
91
187
|
if (!ADDRESS_PATTERNS.bitcoin.test(trimmed)) {
|
|
92
|
-
return { valid: false, error: `Invalid Bitcoin address format
|
|
188
|
+
return { valid: false, error: `Invalid Bitcoin address format.`, code: ErrorCode.INVALID_ADDRESS };
|
|
93
189
|
}
|
|
94
190
|
}
|
|
95
191
|
// For unknown chains, allow any non-empty string (API will validate)
|
|
@@ -139,38 +235,159 @@ function loadConfig() {
|
|
|
139
235
|
|
|
140
236
|
const config = loadConfig();
|
|
141
237
|
|
|
238
|
+
// ============= Retry Configuration =============
|
|
239
|
+
|
|
240
|
+
const DEFAULT_RETRY_OPTIONS = {
|
|
241
|
+
maxRetries: 3,
|
|
242
|
+
baseDelayMs: 1000,
|
|
243
|
+
maxDelayMs: 30000,
|
|
244
|
+
retryOnStatus: [429, 500, 502, 503, 504],
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Sleep for a given number of milliseconds
|
|
249
|
+
*/
|
|
250
|
+
function sleep(ms) {
|
|
251
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Calculate delay with exponential backoff and jitter
|
|
256
|
+
*/
|
|
257
|
+
function calculateBackoff(attempt, baseDelayMs, maxDelayMs, retryAfterMs = null) {
|
|
258
|
+
// If server specifies retry-after, use it (with some jitter)
|
|
259
|
+
if (retryAfterMs) {
|
|
260
|
+
const jitter = Math.random() * 1000;
|
|
261
|
+
return Math.min(retryAfterMs + jitter, maxDelayMs);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Exponential backoff: base * 2^attempt + random jitter
|
|
265
|
+
const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
|
|
266
|
+
const jitter = Math.random() * baseDelayMs;
|
|
267
|
+
return Math.min(exponentialDelay + jitter, maxDelayMs);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Parse retry-after header (supports seconds or HTTP date)
|
|
272
|
+
*/
|
|
273
|
+
function parseRetryAfter(headerValue) {
|
|
274
|
+
if (!headerValue) return null;
|
|
275
|
+
|
|
276
|
+
// Try parsing as seconds
|
|
277
|
+
const seconds = parseInt(headerValue, 10);
|
|
278
|
+
if (!isNaN(seconds)) {
|
|
279
|
+
return seconds * 1000;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Try parsing as HTTP date
|
|
283
|
+
const date = new Date(headerValue);
|
|
284
|
+
if (!isNaN(date.getTime())) {
|
|
285
|
+
return Math.max(0, date.getTime() - Date.now());
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
142
291
|
export class NansenAPI {
|
|
143
|
-
constructor(apiKey = config.apiKey, baseUrl = config.baseUrl) {
|
|
292
|
+
constructor(apiKey = config.apiKey, baseUrl = config.baseUrl, options = {}) {
|
|
144
293
|
if (!apiKey) {
|
|
145
|
-
throw new
|
|
294
|
+
throw new NansenError(
|
|
295
|
+
'API key required. Run `nansen login` or set NANSEN_API_KEY environment variable.',
|
|
296
|
+
ErrorCode.UNAUTHORIZED
|
|
297
|
+
);
|
|
146
298
|
}
|
|
147
299
|
this.apiKey = apiKey;
|
|
148
300
|
this.baseUrl = baseUrl;
|
|
301
|
+
this.retryOptions = { ...DEFAULT_RETRY_OPTIONS, ...options.retry };
|
|
149
302
|
}
|
|
150
303
|
|
|
151
304
|
async request(endpoint, body = {}, options = {}) {
|
|
152
305
|
const url = `${this.baseUrl}${endpoint}`;
|
|
306
|
+
const { maxRetries, baseDelayMs, maxDelayMs, retryOnStatus } = this.retryOptions;
|
|
307
|
+
const shouldRetry = options.retry !== false; // Allow disabling retry per-request
|
|
153
308
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
309
|
+
let lastError;
|
|
310
|
+
|
|
311
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
312
|
+
let response;
|
|
313
|
+
try {
|
|
314
|
+
response = await fetch(url, {
|
|
315
|
+
method: 'POST',
|
|
316
|
+
headers: {
|
|
317
|
+
'Content-Type': 'application/json',
|
|
318
|
+
'apikey': this.apiKey,
|
|
319
|
+
...options.headers
|
|
320
|
+
},
|
|
321
|
+
body: JSON.stringify(body)
|
|
322
|
+
});
|
|
323
|
+
} catch (err) {
|
|
324
|
+
// Network-level errors - retry these too
|
|
325
|
+
lastError = new NansenError(
|
|
326
|
+
`Network error: ${err.message}`,
|
|
327
|
+
ErrorCode.NETWORK_ERROR,
|
|
328
|
+
null,
|
|
329
|
+
{ originalError: err.message, attempt: attempt + 1 }
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
if (shouldRetry && attempt < maxRetries) {
|
|
333
|
+
const delayMs = calculateBackoff(attempt, baseDelayMs, maxDelayMs);
|
|
334
|
+
await sleep(delayMs);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
throw lastError;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let data;
|
|
341
|
+
try {
|
|
342
|
+
data = await response.json();
|
|
343
|
+
} catch (err) {
|
|
344
|
+
// Non-JSON response (rare, usually server errors)
|
|
345
|
+
const error = new NansenError(
|
|
346
|
+
`Invalid response from API (status ${response.status})`,
|
|
347
|
+
response.status >= 500 ? ErrorCode.SERVER_ERROR : ErrorCode.UNKNOWN,
|
|
348
|
+
response.status,
|
|
349
|
+
{ body: await response.text().catch(() => null), attempt: attempt + 1 }
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
if (shouldRetry && attempt < maxRetries && response.status >= 500) {
|
|
353
|
+
const delayMs = calculateBackoff(attempt, baseDelayMs, maxDelayMs);
|
|
354
|
+
await sleep(delayMs);
|
|
355
|
+
lastError = error;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
const message = data.message || data.error || `API error: ${response.status}`;
|
|
363
|
+
const code = statusToErrorCode(response.status, data);
|
|
364
|
+
const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));
|
|
365
|
+
|
|
366
|
+
lastError = new NansenError(message, code, response.status, {
|
|
367
|
+
...data,
|
|
368
|
+
attempt: attempt + 1,
|
|
369
|
+
retryAfterMs
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Retry on specific status codes
|
|
373
|
+
if (shouldRetry && attempt < maxRetries && retryOnStatus.includes(response.status)) {
|
|
374
|
+
const delayMs = calculateBackoff(attempt, baseDelayMs, maxDelayMs, retryAfterMs);
|
|
375
|
+
await sleep(delayMs);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
throw lastError;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Success - add retry metadata if we retried
|
|
383
|
+
if (attempt > 0) {
|
|
384
|
+
data._meta = { ...(data._meta || {}), retriedAttempts: attempt };
|
|
385
|
+
}
|
|
386
|
+
return data;
|
|
171
387
|
}
|
|
172
|
-
|
|
173
|
-
|
|
388
|
+
|
|
389
|
+
// Should not reach here, but just in case
|
|
390
|
+
throw lastError;
|
|
174
391
|
}
|
|
175
392
|
|
|
176
393
|
// ============= Smart Money Endpoints =============
|
|
@@ -242,7 +459,7 @@ export class NansenAPI {
|
|
|
242
459
|
const { address, entityName, chain = 'ethereum', hideSpamToken = true, filters = {}, orderBy } = params;
|
|
243
460
|
if (address) {
|
|
244
461
|
const validation = validateAddress(address, chain);
|
|
245
|
-
if (!validation.valid) throw new
|
|
462
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
246
463
|
}
|
|
247
464
|
return this.request('/api/v1/profiler/address/current-balance', {
|
|
248
465
|
address,
|
|
@@ -258,7 +475,7 @@ export class NansenAPI {
|
|
|
258
475
|
const { address, chain = 'ethereum', pagination = { page: 1, recordsPerPage: 100 } } = params;
|
|
259
476
|
if (address) {
|
|
260
477
|
const validation = validateAddress(address, chain);
|
|
261
|
-
if (!validation.valid) throw new
|
|
478
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
262
479
|
}
|
|
263
480
|
return this.request('/api/beta/profiler/address/labels', {
|
|
264
481
|
parameters: { address, chain },
|
|
@@ -270,7 +487,7 @@ export class NansenAPI {
|
|
|
270
487
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination } = params;
|
|
271
488
|
if (address) {
|
|
272
489
|
const validation = validateAddress(address, chain);
|
|
273
|
-
if (!validation.valid) throw new
|
|
490
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
274
491
|
}
|
|
275
492
|
return this.request('/api/v1/profiler/address/transactions', {
|
|
276
493
|
address,
|
|
@@ -285,7 +502,7 @@ export class NansenAPI {
|
|
|
285
502
|
const { address, chain = 'ethereum' } = params;
|
|
286
503
|
if (address) {
|
|
287
504
|
const validation = validateAddress(address, chain);
|
|
288
|
-
if (!validation.valid) throw new
|
|
505
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
289
506
|
}
|
|
290
507
|
return this.request('/api/v1/profiler/address/pnl-and-trade-performance', {
|
|
291
508
|
address,
|
|
@@ -305,7 +522,7 @@ export class NansenAPI {
|
|
|
305
522
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
306
523
|
if (address) {
|
|
307
524
|
const validation = validateAddress(address, chain);
|
|
308
|
-
if (!validation.valid) throw new
|
|
525
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
309
526
|
}
|
|
310
527
|
const to = new Date().toISOString().split('T')[0];
|
|
311
528
|
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
@@ -323,7 +540,7 @@ export class NansenAPI {
|
|
|
323
540
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination } = params;
|
|
324
541
|
if (address) {
|
|
325
542
|
const validation = validateAddress(address, chain);
|
|
326
|
-
if (!validation.valid) throw new
|
|
543
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
327
544
|
}
|
|
328
545
|
return this.request('/api/v1/profiler/address/related-wallets', {
|
|
329
546
|
address,
|
|
@@ -338,7 +555,7 @@ export class NansenAPI {
|
|
|
338
555
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
339
556
|
if (address) {
|
|
340
557
|
const validation = validateAddress(address, chain);
|
|
341
|
-
if (!validation.valid) throw new
|
|
558
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
342
559
|
}
|
|
343
560
|
const to = new Date().toISOString().split('T')[0];
|
|
344
561
|
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
@@ -356,7 +573,7 @@ export class NansenAPI {
|
|
|
356
573
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
357
574
|
if (address) {
|
|
358
575
|
const validation = validateAddress(address, chain);
|
|
359
|
-
if (!validation.valid) throw new
|
|
576
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
360
577
|
}
|
|
361
578
|
const to = new Date().toISOString().split('T')[0];
|
|
362
579
|
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
@@ -411,7 +628,7 @@ export class NansenAPI {
|
|
|
411
628
|
const { tokenAddress, chain = 'solana', labelType = 'all_holders', filters = {}, orderBy, pagination } = params;
|
|
412
629
|
if (tokenAddress) {
|
|
413
630
|
const validation = validateTokenAddress(tokenAddress, chain);
|
|
414
|
-
if (!validation.valid) throw new
|
|
631
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
415
632
|
}
|
|
416
633
|
return this.request('/api/v1/tgm/holders', {
|
|
417
634
|
token_address: tokenAddress,
|
|
@@ -427,7 +644,7 @@ export class NansenAPI {
|
|
|
427
644
|
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination } = params;
|
|
428
645
|
if (tokenAddress) {
|
|
429
646
|
const validation = validateTokenAddress(tokenAddress, chain);
|
|
430
|
-
if (!validation.valid) throw new
|
|
647
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
431
648
|
}
|
|
432
649
|
return this.request('/api/v1/tgm/flows', {
|
|
433
650
|
token_address: tokenAddress,
|
|
@@ -442,7 +659,7 @@ export class NansenAPI {
|
|
|
442
659
|
const { tokenAddress, chain = 'solana', onlySmartMoney = false, filters = {}, orderBy, pagination, days = 7 } = params;
|
|
443
660
|
if (tokenAddress) {
|
|
444
661
|
const validation = validateTokenAddress(tokenAddress, chain);
|
|
445
|
-
if (!validation.valid) throw new
|
|
662
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
446
663
|
}
|
|
447
664
|
const to = new Date().toISOString().split('T')[0];
|
|
448
665
|
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
@@ -467,7 +684,7 @@ export class NansenAPI {
|
|
|
467
684
|
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
468
685
|
if (tokenAddress) {
|
|
469
686
|
const validation = validateTokenAddress(tokenAddress, chain);
|
|
470
|
-
if (!validation.valid) throw new
|
|
687
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
471
688
|
}
|
|
472
689
|
const to = new Date().toISOString().split('T')[0];
|
|
473
690
|
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
@@ -485,7 +702,7 @@ export class NansenAPI {
|
|
|
485
702
|
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination } = params;
|
|
486
703
|
if (tokenAddress) {
|
|
487
704
|
const validation = validateTokenAddress(tokenAddress, chain);
|
|
488
|
-
if (!validation.valid) throw new
|
|
705
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
489
706
|
}
|
|
490
707
|
return this.request('/api/v1/tgm/who-bought-sold', {
|
|
491
708
|
token_address: tokenAddress,
|
|
@@ -500,7 +717,7 @@ export class NansenAPI {
|
|
|
500
717
|
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination } = params;
|
|
501
718
|
if (tokenAddress) {
|
|
502
719
|
const validation = validateTokenAddress(tokenAddress, chain);
|
|
503
|
-
if (!validation.valid) throw new
|
|
720
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
504
721
|
}
|
|
505
722
|
return this.request('/api/v1/tgm/flow-intelligence', {
|
|
506
723
|
token_address: tokenAddress,
|
|
@@ -515,7 +732,7 @@ export class NansenAPI {
|
|
|
515
732
|
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 7 } = params;
|
|
516
733
|
if (tokenAddress) {
|
|
517
734
|
const validation = validateTokenAddress(tokenAddress, chain);
|
|
518
|
-
if (!validation.valid) throw new
|
|
735
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
519
736
|
}
|
|
520
737
|
const to = new Date().toISOString().split('T')[0];
|
|
521
738
|
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
@@ -534,7 +751,7 @@ export class NansenAPI {
|
|
|
534
751
|
// JUP DCA is Solana-only
|
|
535
752
|
if (tokenAddress) {
|
|
536
753
|
const validation = validateTokenAddress(tokenAddress, 'solana');
|
|
537
|
-
if (!validation.valid) throw new
|
|
754
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
538
755
|
}
|
|
539
756
|
return this.request('/api/v1/tgm/jup-dca', {
|
|
540
757
|
token_address: tokenAddress,
|