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/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Extracted from index.js for coverage
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { NansenAPI, NansenError, ErrorCode, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir, validateAddress, normalizeAddress, sleep } from './api.js';
|
|
6
|
+
import { NansenAPI, NansenError, CommandError, ErrorCode, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir, validateAddress, normalizeAddress, sleep } from './api.js';
|
|
7
7
|
import { buildWalletCommands } from './wallet.js';
|
|
8
8
|
import { buildTradingCommands } from './trading.js';
|
|
9
9
|
import { formatAlertsTable, buildAlertsCommands } from './commands/alerts.js';
|
|
@@ -793,7 +793,6 @@ export function buildCommands(deps = {}) {
|
|
|
793
793
|
saveConfigFn = saveConfig,
|
|
794
794
|
deleteConfigFn = deleteConfig,
|
|
795
795
|
getConfigFileFn = getConfigFile,
|
|
796
|
-
exit = process.exit,
|
|
797
796
|
isTTY = process.stdin.isTTY
|
|
798
797
|
} = deps;
|
|
799
798
|
|
|
@@ -895,12 +894,10 @@ export function buildCommands(deps = {}) {
|
|
|
895
894
|
|
|
896
895
|
if (!apiKey && flags.human) {
|
|
897
896
|
if (!isTTY) {
|
|
898
|
-
|
|
897
|
+
throw new CommandError('--human requires an interactive terminal. Use --api-key or NANSEN_API_KEY env var instead.', 'NOT_A_TTY', {
|
|
899
898
|
error: 'NOT_A_TTY',
|
|
900
899
|
message: '--human requires an interactive terminal. Use --api-key or NANSEN_API_KEY env var instead.',
|
|
901
|
-
})
|
|
902
|
-
exit(1);
|
|
903
|
-
return;
|
|
900
|
+
});
|
|
904
901
|
}
|
|
905
902
|
log('Nansen CLI Login\n');
|
|
906
903
|
log('Get your API key at: https://app.nansen.ai/auth/agent-setup\n');
|
|
@@ -908,7 +905,7 @@ export function buildCommands(deps = {}) {
|
|
|
908
905
|
}
|
|
909
906
|
|
|
910
907
|
if (!apiKey || apiKey.trim().length === 0) {
|
|
911
|
-
|
|
908
|
+
throw new CommandError('No API key provided.', 'API_KEY_REQUIRED', {
|
|
912
909
|
error: 'API_KEY_REQUIRED',
|
|
913
910
|
message: 'No API key provided.',
|
|
914
911
|
resolution: [
|
|
@@ -916,9 +913,7 @@ export function buildCommands(deps = {}) {
|
|
|
916
913
|
'Or set NANSEN_API_KEY environment variable',
|
|
917
914
|
'Get your API key at: https://app.nansen.ai/auth/agent-setup',
|
|
918
915
|
],
|
|
919
|
-
})
|
|
920
|
-
exit(1);
|
|
921
|
-
return;
|
|
916
|
+
});
|
|
922
917
|
}
|
|
923
918
|
|
|
924
919
|
// Verify API key before saving
|
|
@@ -933,20 +928,17 @@ export function buildCommands(deps = {}) {
|
|
|
933
928
|
accountInfo = await testApi.getAccount();
|
|
934
929
|
} catch (error) {
|
|
935
930
|
if (error.code === ErrorCode.UNAUTHORIZED) {
|
|
936
|
-
|
|
931
|
+
throw new CommandError('The API key is not valid.', 'INVALID_API_KEY', {
|
|
937
932
|
error: 'INVALID_API_KEY',
|
|
938
933
|
message: 'The API key is not valid.',
|
|
939
|
-
resolution: ['Check your key at https://app.nansen.ai/auth/agent-setup']
|
|
940
|
-
})
|
|
941
|
-
} else {
|
|
942
|
-
log(JSON.stringify({
|
|
943
|
-
error: 'VERIFICATION_FAILED',
|
|
944
|
-
message: `Could not verify API key: ${error.message}`,
|
|
945
|
-
resolution: ['Check your internet connection', 'Try again']
|
|
946
|
-
}));
|
|
934
|
+
resolution: ['Check your key at https://app.nansen.ai/auth/agent-setup'],
|
|
935
|
+
});
|
|
947
936
|
}
|
|
948
|
-
|
|
949
|
-
|
|
937
|
+
throw new CommandError(`Could not verify API key: ${error.message}`, 'VERIFICATION_FAILED', {
|
|
938
|
+
error: 'VERIFICATION_FAILED',
|
|
939
|
+
message: `Could not verify API key: ${error.message}`,
|
|
940
|
+
resolution: ['Check your internet connection', 'Try again'],
|
|
941
|
+
});
|
|
950
942
|
}
|
|
951
943
|
|
|
952
944
|
// Key is valid - now save
|
|
@@ -1415,23 +1407,40 @@ export function buildCommands(deps = {}) {
|
|
|
1415
1407
|
const sortBy = options['sort-by'];
|
|
1416
1408
|
const query = options.query;
|
|
1417
1409
|
const status = options.status;
|
|
1418
|
-
const
|
|
1410
|
+
const orderBy = parseSort(options.sort, options['order-by']);
|
|
1419
1411
|
const pagination = buildPagination(options);
|
|
1420
1412
|
|
|
1413
|
+
// Screener-specific filter options
|
|
1414
|
+
const tags = options.tags ? options.tags.split(',').map(t => t.trim()) : undefined;
|
|
1415
|
+
const minLiquidity = options['min-liquidity'] != null ? Number(options['min-liquidity']) : undefined;
|
|
1416
|
+
const maxLiquidity = options['max-liquidity'] != null ? Number(options['max-liquidity']) : undefined;
|
|
1417
|
+
const minUniqueTraders24h = options['min-unique-traders-24h'] != null ? Number(options['min-unique-traders-24h']) : undefined;
|
|
1418
|
+
const maxUniqueTraders24h = options['max-unique-traders-24h'] != null ? Number(options['max-unique-traders-24h']) : undefined;
|
|
1419
|
+
const minVolume24hr = options['min-volume-24hr'] != null ? Number(options['min-volume-24hr']) : undefined;
|
|
1420
|
+
const maxVolume24hr = options['max-volume-24hr'] != null ? Number(options['max-volume-24hr']) : undefined;
|
|
1421
|
+
const negRisk = options['neg-risk'] != null ? options['neg-risk'] === 'true' : undefined;
|
|
1422
|
+
const minOpenInterest = options['min-open-interest'] != null ? Number(options['min-open-interest']) : undefined;
|
|
1423
|
+
const maxOpenInterest = options['max-open-interest'] != null ? Number(options['max-open-interest']) : undefined;
|
|
1424
|
+
const endDateBefore = options['end-date-before'];
|
|
1425
|
+
const endDateAfter = options['end-date-after'];
|
|
1426
|
+
const minPrice = options['min-price'] != null ? Number(options['min-price']) : undefined;
|
|
1427
|
+
const maxPrice = options['max-price'] != null ? Number(options['max-price']) : undefined;
|
|
1428
|
+
|
|
1421
1429
|
const handlers = {
|
|
1422
|
-
'ohlcv': () => apiInstance.pmOhlcv({ marketId,
|
|
1430
|
+
'ohlcv': () => apiInstance.pmOhlcv({ marketId, orderBy, pagination }),
|
|
1423
1431
|
'orderbook': () => apiInstance.pmOrderbook({ marketId, pagination }),
|
|
1424
|
-
'top-holders': () => apiInstance.pmTopHolders({ marketId,
|
|
1425
|
-
'trades-by-market': () => apiInstance.pmTradesByMarket({ marketId, pagination }),
|
|
1426
|
-
'trades-by-address': () => apiInstance.pmTradesByAddress({ address, pagination }),
|
|
1427
|
-
'market-screener': () => apiInstance.pmMarketScreener({ sortBy, query, status, pagination }),
|
|
1428
|
-
'event-screener': () => apiInstance.pmEventScreener({ sortBy, query, status, pagination }),
|
|
1429
|
-
'pnl-by-market': () => apiInstance.pmPnlByMarket({ marketId, pagination }),
|
|
1430
|
-
'pnl-by-address': () => apiInstance.pmPnlByAddress({ address, pagination }),
|
|
1432
|
+
'top-holders': () => apiInstance.pmTopHolders({ marketId, orderBy, pagination }),
|
|
1433
|
+
'trades-by-market': () => apiInstance.pmTradesByMarket({ marketId, orderBy, pagination }),
|
|
1434
|
+
'trades-by-address': () => apiInstance.pmTradesByAddress({ address, orderBy, pagination }),
|
|
1435
|
+
'market-screener': () => apiInstance.pmMarketScreener({ orderBy, sortBy, query, status, tags, minLiquidity, maxLiquidity, minUniqueTraders24h, maxUniqueTraders24h, minVolume24hr, maxVolume24hr, negRisk, minOpenInterest, maxOpenInterest, endDateBefore, endDateAfter, minPrice, maxPrice, pagination }),
|
|
1436
|
+
'event-screener': () => apiInstance.pmEventScreener({ orderBy, sortBy, query, status, tags, minLiquidity, maxLiquidity, minUniqueTraders24h, maxUniqueTraders24h, minVolume24hr, maxVolume24hr, negRisk, minOpenInterest, maxOpenInterest, endDateBefore, endDateAfter, pagination }),
|
|
1437
|
+
'pnl-by-market': () => apiInstance.pmPnlByMarket({ marketId, orderBy, pagination }),
|
|
1438
|
+
'pnl-by-address': () => apiInstance.pmPnlByAddress({ address, orderBy, pagination }),
|
|
1431
1439
|
'position-detail': () => apiInstance.pmPositionDetail({ marketId, pagination }),
|
|
1432
1440
|
'categories': () => apiInstance.pmCategories({ pagination }),
|
|
1441
|
+
'address-summary': () => apiInstance.pmAddressSummary({ address, pagination }),
|
|
1433
1442
|
'help': () => ({
|
|
1434
|
-
commands: ['ohlcv', 'orderbook', 'top-holders', 'trades-by-market', 'trades-by-address', 'market-screener', 'event-screener', 'pnl-by-market', 'pnl-by-address', 'position-detail', 'categories'],
|
|
1443
|
+
commands: ['ohlcv', 'orderbook', 'top-holders', 'trades-by-market', 'trades-by-address', 'market-screener', 'event-screener', 'pnl-by-market', 'pnl-by-address', 'position-detail', 'categories', 'address-summary'],
|
|
1435
1444
|
description: 'Polymarket prediction market analytics',
|
|
1436
1445
|
example: 'nansen research pm market-screener --sort-by volume_24hr --limit 20'
|
|
1437
1446
|
})
|
|
@@ -1732,7 +1741,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1732
1741
|
};
|
|
1733
1742
|
const formatted = formatOutput(errorData, { pretty, table });
|
|
1734
1743
|
output(formatted.text);
|
|
1735
|
-
trackCommandFailed({ command: fullCommand, duration_ms: Date.now() - startTime, error_code: 'UNKNOWN_COMMAND', flags: usedFlags, chain });
|
|
1744
|
+
await trackCommandFailed({ command: fullCommand, duration_ms: Date.now() - startTime, error_code: 'UNKNOWN_COMMAND', flags: usedFlags, chain });
|
|
1736
1745
|
exit(1);
|
|
1737
1746
|
return { type: 'error', data: errorData };
|
|
1738
1747
|
}
|
|
@@ -1759,7 +1768,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1759
1768
|
|
|
1760
1769
|
// Commands that handle their own output return undefined
|
|
1761
1770
|
if (result === undefined) {
|
|
1762
|
-
trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
1771
|
+
await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
1763
1772
|
return { type: 'no-output', command };
|
|
1764
1773
|
}
|
|
1765
1774
|
|
|
@@ -1767,7 +1776,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1767
1776
|
if (command === 'schema') {
|
|
1768
1777
|
const formatted = formatOutput(result, { pretty, table: false });
|
|
1769
1778
|
output(formatted.text);
|
|
1770
|
-
trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
1779
|
+
await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
1771
1780
|
return { type: 'schema', data: result };
|
|
1772
1781
|
}
|
|
1773
1782
|
|
|
@@ -1780,6 +1789,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1780
1789
|
// Alerts list with --table uses custom table format
|
|
1781
1790
|
if (command === 'alerts' && subcommand === 'list' && table) {
|
|
1782
1791
|
output(formatAlertsTable(result));
|
|
1792
|
+
await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
1783
1793
|
return { type: 'success', data: result };
|
|
1784
1794
|
}
|
|
1785
1795
|
|
|
@@ -1790,20 +1800,26 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1790
1800
|
if (streamOutput) {
|
|
1791
1801
|
output(streamOutput);
|
|
1792
1802
|
}
|
|
1793
|
-
trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
1803
|
+
await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
1794
1804
|
return { type: 'stream', data: result };
|
|
1795
1805
|
}
|
|
1796
1806
|
|
|
1797
1807
|
const successData = { success: true, data: result };
|
|
1798
1808
|
const formatted = formatOutput(successData, { pretty, table, csv });
|
|
1799
1809
|
output(formatted.text);
|
|
1800
|
-
trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
1810
|
+
await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
1801
1811
|
return { type: csv ? 'csv' : 'success', data: result };
|
|
1802
1812
|
} catch (error) {
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1813
|
+
let errorData;
|
|
1814
|
+
if (error instanceof CommandError) {
|
|
1815
|
+
output(error.data ? JSON.stringify(error.data) : error.message);
|
|
1816
|
+
errorData = { error: error.message, code: error.code };
|
|
1817
|
+
} else {
|
|
1818
|
+
errorData = formatError(error);
|
|
1819
|
+
const formatted = formatOutput(errorData, { pretty, table, csv });
|
|
1820
|
+
output(formatted.text);
|
|
1821
|
+
}
|
|
1822
|
+
await trackCommandFailed({
|
|
1807
1823
|
command: fullCommand,
|
|
1808
1824
|
duration_ms: Date.now() - startTime,
|
|
1809
1825
|
error_code: error.code || 'UNKNOWN',
|
package/src/index.js
CHANGED
package/src/schema.json
CHANGED
|
@@ -588,18 +588,42 @@
|
|
|
588
588
|
"endpoint": "/api/v1/prediction-market/market-screener",
|
|
589
589
|
"description": "Get Prediction Market Screener",
|
|
590
590
|
"options": {
|
|
591
|
-
"query": {
|
|
592
|
-
|
|
593
|
-
}
|
|
591
|
+
"query": { "default": "" },
|
|
592
|
+
"sort-by": { "description": "Deprecated: use --sort field:dir instead" },
|
|
593
|
+
"tags": {},
|
|
594
|
+
"min-liquidity": {},
|
|
595
|
+
"max-liquidity": {},
|
|
596
|
+
"min-unique-traders-24h": {},
|
|
597
|
+
"max-unique-traders-24h": {},
|
|
598
|
+
"min-volume-24hr": {},
|
|
599
|
+
"max-volume-24hr": {},
|
|
600
|
+
"neg-risk": {},
|
|
601
|
+
"min-open-interest": {},
|
|
602
|
+
"max-open-interest": {},
|
|
603
|
+
"end-date-before": {},
|
|
604
|
+
"end-date-after": {},
|
|
605
|
+
"min-price": {},
|
|
606
|
+
"max-price": {}
|
|
594
607
|
}
|
|
595
608
|
},
|
|
596
609
|
"event-screener": {
|
|
597
610
|
"endpoint": "/api/v1/prediction-market/event-screener",
|
|
598
611
|
"description": "Get Prediction Market Event Screener",
|
|
599
612
|
"options": {
|
|
600
|
-
"query": {
|
|
601
|
-
|
|
602
|
-
}
|
|
613
|
+
"query": { "default": "" },
|
|
614
|
+
"sort-by": { "description": "Deprecated: use --sort field:dir instead" },
|
|
615
|
+
"tags": {},
|
|
616
|
+
"min-liquidity": {},
|
|
617
|
+
"max-liquidity": {},
|
|
618
|
+
"min-unique-traders-24h": {},
|
|
619
|
+
"max-unique-traders-24h": {},
|
|
620
|
+
"min-volume-24hr": {},
|
|
621
|
+
"max-volume-24hr": {},
|
|
622
|
+
"neg-risk": {},
|
|
623
|
+
"min-open-interest": {},
|
|
624
|
+
"max-open-interest": {},
|
|
625
|
+
"end-date-before": {},
|
|
626
|
+
"end-date-after": {}
|
|
603
627
|
}
|
|
604
628
|
},
|
|
605
629
|
"pnl-by-market": {
|
|
@@ -641,6 +665,15 @@
|
|
|
641
665
|
"categories": {
|
|
642
666
|
"endpoint": "/api/v1/prediction-market/categories",
|
|
643
667
|
"description": "Get Prediction Market Categories"
|
|
668
|
+
},
|
|
669
|
+
"address-summary": {
|
|
670
|
+
"endpoint": "/api/v1/prediction-market/address-summary",
|
|
671
|
+
"description": "Get wallet-level PnL summary for a Polymarket address",
|
|
672
|
+
"options": {
|
|
673
|
+
"address": {
|
|
674
|
+
"required": true
|
|
675
|
+
}
|
|
676
|
+
}
|
|
644
677
|
}
|
|
645
678
|
},
|
|
646
679
|
"description": "Polymarket prediction market analytics"
|
package/src/telemetry.js
CHANGED
|
@@ -22,7 +22,7 @@ const { version: cliVersion } = JSON.parse(
|
|
|
22
22
|
const TELEMETRY_URL =
|
|
23
23
|
'https://bi-data-sources.nansen.ai/events-service-68ifmnpsx2uq7cgab8dw/v2/event';
|
|
24
24
|
|
|
25
|
-
const TIMEOUT_MS =
|
|
25
|
+
const TIMEOUT_MS = 1000;
|
|
26
26
|
|
|
27
27
|
// ─── opt-out ──────────────────────────────────────────────
|
|
28
28
|
|
|
@@ -122,15 +122,17 @@ export function getSessionId() {
|
|
|
122
122
|
// ─── send ──────────────────────────────────────────────────
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Send a telemetry event.
|
|
125
|
+
* Send a telemetry event. Returns a promise that resolves when the request
|
|
126
|
+
* completes (or fails/times out). Never rejects — errors are swallowed.
|
|
127
|
+
* Callers that need to ensure delivery before process.exit can await this.
|
|
126
128
|
*/
|
|
127
129
|
function sendEvent(event) {
|
|
128
|
-
if (TELEMETRY_DISABLED) return;
|
|
130
|
+
if (TELEMETRY_DISABLED) return Promise.resolve();
|
|
129
131
|
const controller = new AbortController();
|
|
130
132
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
131
133
|
timer.unref();
|
|
132
134
|
|
|
133
|
-
fetch(TELEMETRY_URL, {
|
|
135
|
+
return fetch(TELEMETRY_URL, {
|
|
134
136
|
method: 'POST',
|
|
135
137
|
headers: { 'Content-Type': 'application/json' },
|
|
136
138
|
body: JSON.stringify(event),
|
|
@@ -178,7 +180,7 @@ export function trackCommandSucceeded({
|
|
|
178
180
|
flags = [],
|
|
179
181
|
chain = null,
|
|
180
182
|
}) {
|
|
181
|
-
sendEvent({
|
|
183
|
+
return sendEvent({
|
|
182
184
|
event: 'cli_command_succeeded',
|
|
183
185
|
event_source: getEventSource(),
|
|
184
186
|
event_id: crypto.randomUUID(),
|
|
@@ -216,7 +218,7 @@ export function trackCommandFailed({
|
|
|
216
218
|
flags = [],
|
|
217
219
|
chain = null,
|
|
218
220
|
}) {
|
|
219
|
-
sendEvent({
|
|
221
|
+
return sendEvent({
|
|
220
222
|
event: 'cli_command_failed',
|
|
221
223
|
event_source: getEventSource(),
|
|
222
224
|
event_id: crypto.randomUUID(),
|
package/src/trade-validation.js
CHANGED
|
@@ -61,6 +61,16 @@ export function validateQuoteInput({ chain, toChain, from, to, amount }) {
|
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
// 5. At least one side must be USDC or the native token
|
|
66
|
+
const fromIsAnchor = isUsdcOrNative(from, normalizedChain);
|
|
67
|
+
const toIsAnchor = isUsdcOrNative(to, normalizedToChain);
|
|
68
|
+
if (!fromIsAnchor && !toIsAnchor) {
|
|
69
|
+
const anchorDesc = normalizedChain === normalizedToChain
|
|
70
|
+
? `USDC or the native token (${NATIVE_SYMBOLS[normalizedChain] ?? normalizedChain})`
|
|
71
|
+
: `USDC or the native token on either side (${NATIVE_SYMBOLS[normalizedChain] ?? normalizedChain} on ${normalizedChain}, ${NATIVE_SYMBOLS[normalizedToChain] ?? normalizedToChain} on ${normalizedToChain})`;
|
|
72
|
+
throw new Error(`Invalid swap: at least one token must be ${anchorDesc}. Got: ${from} → ${to}.`);
|
|
73
|
+
}
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
// Native token decimals per chain (for converting balance from base units)
|
|
@@ -112,13 +122,35 @@ const NATIVE_TOKEN_ADDRESSES = {
|
|
|
112
122
|
base: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
|
|
113
123
|
};
|
|
114
124
|
|
|
125
|
+
// USDC contract addresses per chain.
|
|
126
|
+
const USDC_ADDRESSES = {
|
|
127
|
+
solana: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
|
128
|
+
base: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
|
|
129
|
+
};
|
|
130
|
+
|
|
115
131
|
// Native token symbols for error messages.
|
|
116
132
|
const NATIVE_SYMBOLS = { solana: 'SOL', base: 'ETH' };
|
|
117
133
|
|
|
134
|
+
const MIN_GAS_AMOUNTS = { solana: 0.01, base: 0.000024 };
|
|
118
135
|
const FEE_BUFFER = { solana: 0.005, base: 0.00004 };
|
|
119
136
|
const HIGH_PERCENTAGE_THRESHOLD = 95;
|
|
120
137
|
const AUTO_ADJUST_THRESHOLD_PERCENT = 2;
|
|
121
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Check if an address is USDC or the native token for a chain (case-insensitive for EVM).
|
|
141
|
+
*/
|
|
142
|
+
function isUsdcOrNative(address, chain) {
|
|
143
|
+
const usdc = USDC_ADDRESSES[chain];
|
|
144
|
+
const native = NATIVE_TOKEN_ADDRESSES[chain];
|
|
145
|
+
if (!usdc && !native) return false;
|
|
146
|
+
if (chain === 'solana') {
|
|
147
|
+
return address === usdc || address === native;
|
|
148
|
+
}
|
|
149
|
+
// EVM: case-insensitive
|
|
150
|
+
const lower = address.toLowerCase();
|
|
151
|
+
return (usdc && lower === usdc.toLowerCase()) || (native && lower === native.toLowerCase());
|
|
152
|
+
}
|
|
153
|
+
|
|
122
154
|
/**
|
|
123
155
|
* Check if an address is the native token for a chain (case-insensitive for EVM).
|
|
124
156
|
*/
|
|
@@ -269,6 +301,32 @@ export async function resolvePercentAmount({ chain, from, walletAddress, percent
|
|
|
269
301
|
return String(parseFloat(tokenAmount.toFixed(decimals)));
|
|
270
302
|
}
|
|
271
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Validate that the wallet has enough native token for gas fees.
|
|
306
|
+
*
|
|
307
|
+
* Returns { hasSufficientNative } or throws on validation failure.
|
|
308
|
+
* Best-effort: if RPC fails, returns passing result.
|
|
309
|
+
*/
|
|
310
|
+
export async function validateGasBalance({ chain, walletAddress }) {
|
|
311
|
+
const normalizedChain = chain.toLowerCase();
|
|
312
|
+
const minGas = MIN_GAS_AMOUNTS[normalizedChain];
|
|
313
|
+
if (minGas === undefined) return { hasSufficientNative: true };
|
|
314
|
+
|
|
315
|
+
const balance = await fetchNativeBalance(normalizedChain, walletAddress);
|
|
316
|
+
|
|
317
|
+
// RPC failure — proceed without validation.
|
|
318
|
+
if (balance === null) return { hasSufficientNative: true };
|
|
319
|
+
|
|
320
|
+
if (balance >= minGas) {
|
|
321
|
+
return { hasSufficientNative: true };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const symbol = NATIVE_SYMBOLS[normalizedChain] || 'native token';
|
|
325
|
+
throw new Error(
|
|
326
|
+
`Insufficient ${symbol} for gas fees. Wallet has ${balance} ${symbol} but needs at least ${minGas} ${symbol}. Fund the wallet before trading.`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
272
330
|
/**
|
|
273
331
|
* Fetch an ERC-20 or SPL token balance for a wallet.
|
|
274
332
|
* Returns balance in human-readable token units, or null on RPC failure.
|