nansen-cli 1.27.1 → 1.28.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 +8 -0
- package/package.json +1 -1
- package/skills/nansen-token-screener/SKILL.md +54 -0
- package/src/api.js +7 -0
- package/src/cli.js +38 -4
- package/src/keychain.js +1 -0
- package/src/limit-order.js +873 -0
- package/src/privy.js +9 -0
- package/src/schema.json +145 -0
- package/src/telemetry.js +5 -1
- package/src/transfer.js +1 -1
- package/src/walletconnect-trading.js +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.28.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#417](https://github.com/nansen-ai/nansen-cli/pull/417) [`ae6079f`](https://github.com/nansen-ai/nansen-cli/commit/ae6079f12d06d11fe357237b84387fcaffcfd387) Thanks [@0xlaveen](https://github.com/0xlaveen)! - Add `trade limit-order` commands (create, list, cancel, update) for Jupiter Limit Order V2 on Solana. Supports local, Privy, and WalletConnect wallets.
|
|
8
|
+
|
|
9
|
+
- [#413](https://github.com/nansen-ai/nansen-cli/pull/413) [`94bd349`](https://github.com/nansen-ai/nansen-cli/commit/94bd349bdf9a0a3f3389975144281306eea0e4ca) Thanks [@jake-kennis](https://github.com/jake-kennis)! - Add `top-tokens` subcommand to discover top-scoring tokens by Nansen Score. Calls the public endpoint (`/api/v1/nansen-score/top-tokens`) with optional `--market-cap` filter.
|
|
10
|
+
|
|
3
11
|
## 1.27.1
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -45,10 +45,64 @@ nansen research token indicators --token $TOKEN --chain $CHAIN
|
|
|
45
45
|
# Flow intelligence — only use for promising tokens from screener/indicators above
|
|
46
46
|
nansen research token flow-intelligence --token $TOKEN --chain $CHAIN
|
|
47
47
|
# → net_flow_usd per label: smart_trader, whale, exchange, fresh_wallets, public_figure
|
|
48
|
+
|
|
49
|
+
# Nansen Score Top Tokens — "what should I buy?" (public endpoint, any authenticated API key)
|
|
50
|
+
# Use this FIRST for discovery, then drill into individual tokens with `indicators` above
|
|
51
|
+
nansen research token top-tokens --limit 25
|
|
52
|
+
nansen research token top-tokens --market-cap largecap --limit 10
|
|
53
|
+
# → chain, token_address, token_symbol, performance_score, risk_score,
|
|
54
|
+
# per-indicator contributions, market_cap_group, latest_date, last_trigger_on
|
|
48
55
|
```
|
|
49
56
|
|
|
50
57
|
Screener timeframes: `5m`, `10m`, `1h`, `6h`, `24h`, `7d`, `30d`
|
|
51
58
|
|
|
52
59
|
Indicators: score is "bullish"/"bearish"/"neutral". signal_percentile > 70 = historically significant. Some tokens return empty indicators — not an error.
|
|
53
60
|
|
|
61
|
+
## Top tokens — Nansen Score field reference
|
|
62
|
+
|
|
63
|
+
Results are pre-filtered to `performance_score >= 15` server-side and returned sorted by:
|
|
64
|
+
1. `performance_score` DESC
|
|
65
|
+
2. `market_cap_group` priority (largecap → midcap → lowcap)
|
|
66
|
+
3. `risk_score` DESC
|
|
67
|
+
4. 24h volume DESC
|
|
68
|
+
|
|
69
|
+
So row 0 is always the strongest candidate for the filter you applied — no client-side ranking needed.
|
|
70
|
+
|
|
71
|
+
Market cap buckets (used in both the sort priority and the `--market-cap` filter):
|
|
72
|
+
- `lowcap`: market cap < $100M
|
|
73
|
+
- `midcap`: market cap $100M – $1B
|
|
74
|
+
- `largecap`: market cap > $1B
|
|
75
|
+
|
|
76
|
+
Every contribution is **ternary** — exactly one of `{negative, 0, positive}` per field. No partial values. Zero means "indicator didn't apply to this token" (out of scope), not "indicator was neutral".
|
|
77
|
+
|
|
78
|
+
**Performance Score (Alpha — "likely to outperform BTC over 7–30d")**
|
|
79
|
+
Range: `-60 to +75` (arithmetic bounds; live max is closer to `+45` since no single token hits every positive indicator simultaneously). Buy threshold: `>= 15`. Sum of the five `*_performance` fields below.
|
|
80
|
+
| Field | Contribution | Trigger | What the underlying indicator measures |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `price_momentum_performance` | +30 / 0 | upstream score `bullish` → +30 | Price momentum, scored against separate thresholds for large-cap vs. low/mid-cap tokens. |
|
|
83
|
+
| `chain_fees_performance` | +30 / 0 | `bullish` (30-day fee growth > +1%) → +30 | 30-day spending momentum on network fees (geometric mean of daily returns). **Only tracked for a handful of L1 native tokens (e.g. ETH, TRX, AVAX, RON); always 0 for every other token.** |
|
|
84
|
+
| `trading_range_performance` | +15 / 0 | `bullish` (price breaks above resistance in an uptrend) → +15 | 14-day price trend combined with position vs. nearest support/resistance. In practice fires mostly on established tokens that have well-defined levels — can fire at any market cap, but is rare for new / low-liquidity tokens. |
|
|
85
|
+
| `chain_tvl_performance` | 0 / -35 | `bearish` (composite TVL growth < 0) → -35 | TVL momentum composite signal. Only non-zero for chains / L2s whose TVL is tracked. No positive path exists — the field only deducts. |
|
|
86
|
+
| `protocol_fees_performance` | 0 / -25 | `bearish` (14-day fee growth < -3%) → -25 | 14-day protocol fee momentum. Only non-zero for tokens backed by protocols with measurable fee revenue. No positive path — deduction only. |
|
|
87
|
+
|
|
88
|
+
**Risk Score (Safety — "filters falling knives / dangerous setups")**
|
|
89
|
+
Range: `-60 to +80` (arithmetic bounds). Safety threshold: `> 0` (positive = safer, negative = riskier). Sum of the four `*_risk` fields below. For every risk field: upstream score `low` → positive contribution, `high` → negative contribution, `medium`/missing → 0.
|
|
90
|
+
| Field | Contribution | What the underlying indicator measures |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `btc_reflexivity_risk` | +40 / -20 | Rolling 5-event median ratio of token drop to BTC drop on days BTC falls >3%. Ratio ≤ 1 → `low` → **+40** (token holds up as well as or better than BTC on drawdowns). Ratio > 1 → `high` → **-20** (token drops harder than BTC). Skipped for stablecoins and tokens with <$1M 24h volume. |
|
|
93
|
+
| `liquidity_risk` | +20 / -20 | Ratio of on-chain liquidity to market cap (`total_liquidity_usd / market_cap_usd`). Higher ratio → `low` → **+20** (deep books relative to cap). Very thin ratio → `high` → **-20**. |
|
|
94
|
+
| `concentration_risk` | +10 / -10 | Top-10 holder concentration as a fraction of supply. `< 0.12` → `low` → **+10** (well-distributed). `> 0.55` → `high` → **-10** (whale-concentrated). |
|
|
95
|
+
| `inflation_risk` | +10 / -10 | EMA of daily token supply inflation rate. Negative / near-zero → `low` → **+10** (stable or deflationary supply). Strongly positive → `high` → **-10** (high dilution). Only evaluated for tokens >= $100M market cap. |
|
|
96
|
+
|
|
97
|
+
Other response fields:
|
|
98
|
+
- `market_cap_group`: `lowcap` / `midcap` / `largecap` — see thresholds above.
|
|
99
|
+
- `latest_date`: ISO datetime of the most recent indicator refresh for this token.
|
|
100
|
+
- `last_trigger_on`: ISO datetime of the most recent trigger across contributing indicators (MAX aggregate — individual indicators may be days-to-months stale even when this looks fresh). Use `indicators` on a specific token to audit per-indicator ages.
|
|
101
|
+
|
|
102
|
+
**Stablecoins rank high but aren't picks.** USDC, USDT, DAI, FDUSD and similar score well on chain_fees + liquidity indicators but aren't what "what should I buy" means. Filter them out of the shortlist using the canonical whitelist at `nansen-dbt-ch-tokens/seeds/stablecoins_for_indicator.csv` before drilling into `indicators`.
|
|
103
|
+
|
|
104
|
+
Typical workflow: start with `top-tokens` for a shortlist → drop stablecoins → run `indicators` on the top 3–5 to inspect individual signals and their signal_percentile → `flow-intelligence` only on the finalists to confirm SM conviction.
|
|
105
|
+
|
|
106
|
+
Field meanings and contribution mappings above are sourced from `nansen-dbt-ch-tokens/models/indicators/api_nansen_score_indicators_all_tokens_latest.sql` and per-indicator model yml files. Sign conventions and live value ranges were validated against production ClickHouse data.
|
|
107
|
+
|
|
54
108
|
Flow intelligence is credit-heavy. Use it to confirm SM conviction on tokens that already look promising from screener + indicators, not as a first pass on every token.
|
package/src/api.js
CHANGED
|
@@ -1203,6 +1203,13 @@ export class NansenAPI {
|
|
|
1203
1203
|
});
|
|
1204
1204
|
}
|
|
1205
1205
|
|
|
1206
|
+
async topTokens(params = {}) {
|
|
1207
|
+
const { marketCapGroup, limit = 25 } = params;
|
|
1208
|
+
const body = { limit };
|
|
1209
|
+
if (marketCapGroup) body.market_cap_group = marketCapGroup;
|
|
1210
|
+
return this.request('/api/v1/nansen-score/top-tokens', body);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1206
1213
|
// ============= Perp Endpoints =============
|
|
1207
1214
|
|
|
1208
1215
|
async perpScreener(params = {}) {
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
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
|
+
import { buildLimitOrderCommands } from './limit-order.js';
|
|
9
10
|
import { formatAlertsTable, buildAlertsCommands } from './commands/alerts.js';
|
|
10
11
|
import { buildAgentCommands } from './commands/agent.js';
|
|
11
12
|
import { resolveAddress, isEnsName } from './ens.js';
|
|
@@ -1276,8 +1277,13 @@ export function buildCommands(deps = {}) {
|
|
|
1276
1277
|
const withLabels = resolveBooleanOption(options, flags, 'premium-labels');
|
|
1277
1278
|
return apiInstance.tokenPerpPnlLeaderboard({ tokenSymbol, filters, orderBy, pagination, days, withLabels });
|
|
1278
1279
|
},
|
|
1280
|
+
'top-tokens': () => {
|
|
1281
|
+
const marketCapGroup = options['market-cap'] || options['market-cap-group'];
|
|
1282
|
+
const limit = options.limit ? parseInt(options.limit) : undefined;
|
|
1283
|
+
return apiInstance.topTokens({ marketCapGroup, limit });
|
|
1284
|
+
},
|
|
1279
1285
|
'help': () => ({
|
|
1280
|
-
commands: ['info', 'ohlcv', 'screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
|
|
1286
|
+
commands: ['info', 'ohlcv', 'screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard', 'top-tokens'],
|
|
1281
1287
|
description: 'Token God Mode endpoints',
|
|
1282
1288
|
example: 'nansen token screener --chain solana --timeframe 24h --smart-money --include-stablecoins false'
|
|
1283
1289
|
})
|
|
@@ -1474,8 +1480,9 @@ export function buildCommands(deps = {}) {
|
|
|
1474
1480
|
return cmds[category](args.slice(1), apiInstance, flags, options);
|
|
1475
1481
|
};
|
|
1476
1482
|
|
|
1477
|
-
// 'trade' delegates to quote/execute from buildTradingCommands
|
|
1483
|
+
// 'trade' delegates to quote/execute from buildTradingCommands and limit-order from buildLimitOrderCommands
|
|
1478
1484
|
const tradingCmds = buildTradingCommands(deps);
|
|
1485
|
+
const limitOrderCmds = buildLimitOrderCommands(deps);
|
|
1479
1486
|
cmds['trade'] = async (args, apiInstance, flags, options) => {
|
|
1480
1487
|
const sub = args[0];
|
|
1481
1488
|
if (!sub || sub === 'help') {
|
|
@@ -1485,12 +1492,14 @@ SUBCOMMANDS:
|
|
|
1485
1492
|
quote Get a swap quote (price, route, fees)
|
|
1486
1493
|
execute Sign and broadcast a quoted swap
|
|
1487
1494
|
bridge-status Check cross-chain bridge transaction status
|
|
1495
|
+
limit-order Limit order management (Solana only)
|
|
1488
1496
|
|
|
1489
1497
|
USAGE:
|
|
1490
1498
|
nansen trade quote --chain <chain> --from <token> --to <token> --amount <units> [--wallet <name>]
|
|
1491
1499
|
nansen trade quote --chain <chain> --to-chain <chain> --from <token> --to <token> --amount <units>
|
|
1492
1500
|
nansen trade execute --quote <quoteId> [--wallet <name>]
|
|
1493
1501
|
nansen trade bridge-status --tx-hash <hash> --from-chain <chain> --to-chain <chain>
|
|
1502
|
+
nansen trade limit-order <create|list|cancel|update> [options]
|
|
1494
1503
|
|
|
1495
1504
|
EXAMPLES:
|
|
1496
1505
|
nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
@@ -1498,9 +1507,11 @@ EXAMPLES:
|
|
|
1498
1507
|
nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
|
|
1499
1508
|
nansen trade execute --quote 1708900000000-abc123
|
|
1500
1509
|
nansen trade bridge-status --tx-hash 0xabc... --from-chain base --to-chain solana
|
|
1510
|
+
nansen trade limit-order create --from SOL --to USDC --amount 1000000000 --trigger-mint SOL --trigger-condition below --trigger-price 80
|
|
1511
|
+
nansen trade limit-order list
|
|
1501
1512
|
|
|
1502
1513
|
WALLET:
|
|
1503
|
-
--wallet <name> Use a named wallet, or "walletconnect" / "wc" for WalletConnect
|
|
1514
|
+
--wallet <name> Use a named wallet, or "walletconnect" / "wc" for WalletConnect.
|
|
1504
1515
|
Defaults to the default local wallet if omitted.
|
|
1505
1516
|
|
|
1506
1517
|
SYMBOLS:
|
|
@@ -1518,8 +1529,31 @@ CROSS-CHAIN NOTES (when using --to-chain):
|
|
|
1518
1529
|
Typical bridge time: 1-5 minutes`);
|
|
1519
1530
|
return;
|
|
1520
1531
|
}
|
|
1532
|
+
if (sub === 'limit-order') {
|
|
1533
|
+
const loSub = args[1];
|
|
1534
|
+
if (!loSub || loSub === 'help') {
|
|
1535
|
+
log(`nansen trade limit-order — Limit order commands (Solana only)
|
|
1536
|
+
|
|
1537
|
+
SUBCOMMANDS:
|
|
1538
|
+
create Place a new limit order
|
|
1539
|
+
list List your limit orders
|
|
1540
|
+
cancel Cancel an open order
|
|
1541
|
+
update Update trigger price or slippage
|
|
1542
|
+
|
|
1543
|
+
USAGE:
|
|
1544
|
+
nansen trade limit-order create --from <token> --to <token> --amount <units> --trigger-mint <token> --trigger-condition <above|below> --trigger-price <usd>
|
|
1545
|
+
nansen trade limit-order list [--state <active|past>]
|
|
1546
|
+
nansen trade limit-order cancel --order <orderId>
|
|
1547
|
+
nansen trade limit-order update --order <orderId> --trigger-price <usd>`);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
if (!limitOrderCmds[loSub]) {
|
|
1551
|
+
throw new NansenError(`Unknown limit-order subcommand: ${loSub}. Available: create, list, cancel, update`, ErrorCode.UNKNOWN);
|
|
1552
|
+
}
|
|
1553
|
+
return limitOrderCmds[loSub](args.slice(2), apiInstance, flags, options);
|
|
1554
|
+
}
|
|
1521
1555
|
if (!tradingCmds[sub]) {
|
|
1522
|
-
throw new NansenError(`Unknown trade subcommand: ${sub}. Available: quote, execute, bridge-status`, ErrorCode.UNKNOWN);
|
|
1556
|
+
throw new NansenError(`Unknown trade subcommand: ${sub}. Available: quote, execute, bridge-status, limit-order`, ErrorCode.UNKNOWN);
|
|
1523
1557
|
}
|
|
1524
1558
|
return tradingCmds[sub](args.slice(1), apiInstance, flags, options);
|
|
1525
1559
|
};
|
package/src/keychain.js
CHANGED
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nansen CLI - Limit Order Commands (Jupiter Trigger V2)
|
|
3
|
+
*
|
|
4
|
+
* Supports create, list, cancel, and update of limit orders on Solana.
|
|
5
|
+
* Uses challenge-response JWT auth with disk caching.
|
|
6
|
+
* Zero external dependencies — uses Node.js built-in crypto only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { base58Encode, exportWallet, getWalletConfig, showWallet } from './wallet.js';
|
|
12
|
+
import { signEd25519, base58Decode, parseAmount, getTokenInfo } from './transfer.js';
|
|
13
|
+
import { signSolanaTransaction, resolveTokenAddress } from './trading.js';
|
|
14
|
+
import { validateTokenAddress } from './api.js';
|
|
15
|
+
import { getWalletConnectAddress, sendSolanaTransactionViaWalletConnect, signSolanaMessageViaWalletConnect } from './walletconnect-trading.js';
|
|
16
|
+
import { retrievePassword } from './keychain.js';
|
|
17
|
+
import { CHAIN_RPCS } from './rpc-urls.js';
|
|
18
|
+
|
|
19
|
+
// ============= Constants =============
|
|
20
|
+
|
|
21
|
+
const TRADING_API_URL = process.env.NANSEN_TRADING_API_URL || 'https://trading-api.nansen.ai';
|
|
22
|
+
const LO_PREFIX = '/limit-order/v2';
|
|
23
|
+
const SOLSCAN_TX_URL = 'https://solscan.io/tx/';
|
|
24
|
+
|
|
25
|
+
// ============= JWT Auth & Caching (Local File) =============
|
|
26
|
+
|
|
27
|
+
function getAuthFilePath() {
|
|
28
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
29
|
+
return path.join(home, '.nansen', 'limit-order-auth.json');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Save a JWT token to ~/.nansen/limit-order-auth.json.
|
|
34
|
+
* Keyed by wallet pubkey so switching wallets invalidates correctly.
|
|
35
|
+
*/
|
|
36
|
+
export function saveCachedToken(walletPubkey, token) {
|
|
37
|
+
try {
|
|
38
|
+
const filePath = getAuthFilePath();
|
|
39
|
+
const dir = path.dirname(filePath);
|
|
40
|
+
if (!fs.existsSync(dir)) {
|
|
41
|
+
fs.mkdirSync(dir, { mode: 0o700, recursive: true });
|
|
42
|
+
}
|
|
43
|
+
const data = JSON.stringify({
|
|
44
|
+
walletPubkey,
|
|
45
|
+
token,
|
|
46
|
+
// 23-hour TTL provides 1-hour safety margin against server's 24-hour JWT
|
|
47
|
+
expiresAt: Date.now() + 23 * 3600 * 1000,
|
|
48
|
+
});
|
|
49
|
+
fs.writeFileSync(filePath, data, { mode: 0o600 });
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load a cached JWT token from ~/.nansen/limit-order-auth.json.
|
|
58
|
+
* Returns the token string if valid and not expired, null otherwise.
|
|
59
|
+
*/
|
|
60
|
+
export function loadCachedToken(walletPubkey) {
|
|
61
|
+
try {
|
|
62
|
+
const filePath = getAuthFilePath();
|
|
63
|
+
if (!fs.existsSync(filePath)) return null;
|
|
64
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
65
|
+
const data = JSON.parse(raw);
|
|
66
|
+
if (data.walletPubkey !== walletPubkey) return null;
|
|
67
|
+
// 5-minute buffer before expiry to avoid mid-request failures
|
|
68
|
+
if (data.expiresAt <= Date.now() + 300_000) return null;
|
|
69
|
+
return data.token;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============= API Client =============
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Make an authenticated request to the limit order V2 API.
|
|
79
|
+
*/
|
|
80
|
+
async function loFetch(method, endpoint, { token, body, query } = {}) {
|
|
81
|
+
const url = new URL(`${LO_PREFIX}${endpoint}`, TRADING_API_URL);
|
|
82
|
+
if (query) {
|
|
83
|
+
for (const [key, value] of Object.entries(query)) {
|
|
84
|
+
if (value !== undefined && value !== null) {
|
|
85
|
+
url.searchParams.set(key, String(value));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const headers = {
|
|
91
|
+
'Accept': 'application/json',
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
};
|
|
94
|
+
if (token) {
|
|
95
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
96
|
+
}
|
|
97
|
+
if (process.env.NANSEN_API_KEY) {
|
|
98
|
+
headers['X-API-Key'] = process.env.NANSEN_API_KEY;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const opts = { method, headers };
|
|
102
|
+
if (body !== undefined) {
|
|
103
|
+
opts.body = JSON.stringify(body);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const res = await fetch(url.toString(), opts);
|
|
107
|
+
const text = await res.text();
|
|
108
|
+
|
|
109
|
+
let parsed;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(text);
|
|
112
|
+
} catch {
|
|
113
|
+
throw Object.assign(
|
|
114
|
+
new Error(`Limit order API returned non-JSON response (status ${res.status})`),
|
|
115
|
+
{ code: 'NON_JSON_RESPONSE', status: res.status, details: text.slice(0, 200) }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
const code = parsed.code || 'LIMIT_ORDER_ERROR';
|
|
121
|
+
const msg = parsed.message || `Limit order request failed with status ${res.status}`;
|
|
122
|
+
throw Object.assign(new Error(msg), { code, status: res.status, details: parsed.details });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- Auth endpoints (no JWT required) ---
|
|
129
|
+
|
|
130
|
+
export async function getChallenge(walletPubkey) {
|
|
131
|
+
return loFetch('POST', '/auth/challenge', { body: { walletPubkey } });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function verifyChallenge(walletPubkey, signatureBase58) {
|
|
135
|
+
return loFetch('POST', '/auth/verify', { body: { walletPubkey, signature: signatureBase58 } });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Vault endpoints ---
|
|
139
|
+
|
|
140
|
+
export async function getVault(token, userPubkey) {
|
|
141
|
+
return loFetch('GET', '/vault', { token, query: { userPubkey } });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function registerVault(token) {
|
|
145
|
+
return loFetch('POST', '/vault/register', { token, body: {} });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Order lifecycle endpoints ---
|
|
149
|
+
|
|
150
|
+
export async function craftDeposit(token, { inputMint, outputMint, userAddress, amount }) {
|
|
151
|
+
return loFetch('POST', '/deposit/craft', {
|
|
152
|
+
token,
|
|
153
|
+
body: { inputMint, outputMint, userAddress, amount },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function createOrder(token, params) {
|
|
158
|
+
return loFetch('POST', '/create', { token, body: params });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function listOrders(token, userPubkey, filters = {}) {
|
|
162
|
+
return loFetch('GET', '/orders', {
|
|
163
|
+
token,
|
|
164
|
+
query: { userPubkey, ...filters },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function updateOrder(token, orderId, params) {
|
|
169
|
+
return loFetch('PATCH', `/orders/${orderId}`, { token, body: params });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function cancelOrderRequest(token, orderId) {
|
|
173
|
+
return loFetch('POST', `/cancel/${orderId}`, { token, body: {} });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function confirmCancelOrder(token, orderId, { signedTransaction, cancelRequestId }) {
|
|
177
|
+
return loFetch('POST', `/cancel/${orderId}/confirm`, {
|
|
178
|
+
token,
|
|
179
|
+
body: { signedTransaction, cancelRequestId },
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============= Message Signing =============
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Sign a message with a Solana wallet.
|
|
187
|
+
* Returns raw signature bytes as a Buffer.
|
|
188
|
+
*
|
|
189
|
+
* @param {Buffer} message - Raw message bytes
|
|
190
|
+
* @param {'local'|'privy'|'walletconnect'} walletType
|
|
191
|
+
* @param {object} walletInfo - Type-specific signing info
|
|
192
|
+
* @returns {Promise<Buffer>} Raw Ed25519 signature (64 bytes)
|
|
193
|
+
*/
|
|
194
|
+
export async function signSolanaMessage(message, walletType, walletInfo) {
|
|
195
|
+
if (walletType === 'local') {
|
|
196
|
+
// Extract seed (first 32 bytes of the 64-byte keypair hex)
|
|
197
|
+
const seed = Buffer.from(walletInfo.privateKeyHex.slice(0, 64), 'hex');
|
|
198
|
+
return signEd25519(message, seed);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (walletType === 'privy') {
|
|
202
|
+
const result = await walletInfo.privyClient.signSolanaMessage(
|
|
203
|
+
walletInfo.walletId,
|
|
204
|
+
message,
|
|
205
|
+
);
|
|
206
|
+
const sigBase64 = result.data?.signature || result.signature;
|
|
207
|
+
return Buffer.from(sigBase64, 'base64');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (walletType === 'walletconnect') {
|
|
211
|
+
const result = await signSolanaMessageViaWalletConnect(message);
|
|
212
|
+
// WC returns base58-encoded signature
|
|
213
|
+
return Buffer.from(base58Decode(result.signature));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
throw new Error(`Unsupported wallet type: ${walletType}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ============= Authentication Flow =============
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Authenticate with the limit order API and return a JWT.
|
|
223
|
+
* Uses disk cache to avoid re-signing for every CLI invocation.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} walletPubkey - Solana wallet address
|
|
226
|
+
* @param {'local'|'privy'|'walletconnect'} walletType
|
|
227
|
+
* @param {object} walletInfo - Signing info
|
|
228
|
+
* @param {function} log - Logger
|
|
229
|
+
* @returns {Promise<string>} JWT token
|
|
230
|
+
*/
|
|
231
|
+
export async function authenticate(walletPubkey, walletType, walletInfo, log = () => {}) {
|
|
232
|
+
const cached = loadCachedToken(walletPubkey);
|
|
233
|
+
if (cached) {
|
|
234
|
+
return cached;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
log(' Authenticating with limit order API...');
|
|
238
|
+
const { challenge } = await getChallenge(walletPubkey);
|
|
239
|
+
const messageBuffer = Buffer.from(challenge, 'utf8');
|
|
240
|
+
|
|
241
|
+
log(' Signing challenge...');
|
|
242
|
+
const signatureBytes = await signSolanaMessage(messageBuffer, walletType, walletInfo);
|
|
243
|
+
const signatureBase58 = base58Encode(signatureBytes);
|
|
244
|
+
|
|
245
|
+
const { token } = await verifyChallenge(walletPubkey, signatureBase58);
|
|
246
|
+
saveCachedToken(walletPubkey, token);
|
|
247
|
+
|
|
248
|
+
return token;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ============= Wallet Resolution =============
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Resolve a Solana wallet for limit orders.
|
|
255
|
+
* Follows the same 3-way dispatch as trading.js: WalletConnect / named / default.
|
|
256
|
+
*
|
|
257
|
+
* @returns {{ pubkey, walletType, walletInfo, privyWalletIds }}
|
|
258
|
+
*/
|
|
259
|
+
export async function resolveSolanaWallet(walletName, deps = {}) {
|
|
260
|
+
const { log = console.log, exit = process.exit } = deps;
|
|
261
|
+
|
|
262
|
+
const isWalletConnect = walletName === 'walletconnect' || walletName === 'wc';
|
|
263
|
+
|
|
264
|
+
if (isWalletConnect) {
|
|
265
|
+
const address = await getWalletConnectAddress('solana');
|
|
266
|
+
if (!address) {
|
|
267
|
+
log('No WalletConnect session active. Run: walletconnect connect');
|
|
268
|
+
exit(1);
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return { pubkey: address, walletType: 'walletconnect', walletInfo: {}, privyWalletIds: null };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let wallet;
|
|
275
|
+
if (walletName) {
|
|
276
|
+
wallet = showWallet(walletName);
|
|
277
|
+
} else {
|
|
278
|
+
try {
|
|
279
|
+
const config = getWalletConfig();
|
|
280
|
+
if (config.defaultWallet) {
|
|
281
|
+
wallet = showWallet(config.defaultWallet);
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
// No wallet configured
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!wallet || !wallet.solana) {
|
|
289
|
+
log('No Solana wallet found. Create one with: nansen wallet create');
|
|
290
|
+
exit(1);
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (wallet.provider === 'privy') {
|
|
295
|
+
const { PrivyClient } = await import('./privy.js');
|
|
296
|
+
const privyClient = new PrivyClient(process.env.PRIVY_APP_ID, process.env.PRIVY_APP_SECRET);
|
|
297
|
+
return {
|
|
298
|
+
pubkey: wallet.solana,
|
|
299
|
+
walletType: 'privy',
|
|
300
|
+
walletInfo: { privyClient, walletId: wallet.privyWalletIds?.solana },
|
|
301
|
+
privyWalletIds: wallet.privyWalletIds,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Local wallet — need password for signing
|
|
306
|
+
return {
|
|
307
|
+
pubkey: wallet.solana,
|
|
308
|
+
walletType: 'local',
|
|
309
|
+
walletInfo: {}, // privateKeyHex populated lazily when signing is needed
|
|
310
|
+
walletName: wallet.name,
|
|
311
|
+
privyWalletIds: null,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get the private key hex for a local wallet, prompting for password if needed.
|
|
317
|
+
*/
|
|
318
|
+
function getLocalWalletPrivateKey(walletName) {
|
|
319
|
+
const config = getWalletConfig();
|
|
320
|
+
let password = null;
|
|
321
|
+
if (config.passwordHash) {
|
|
322
|
+
const result = retrievePassword();
|
|
323
|
+
password = result.password;
|
|
324
|
+
if (!password) {
|
|
325
|
+
throw new Error('Wallet is encrypted and no password was found. Set NANSEN_WALLET_PASSWORD env var.');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const effectiveName = walletName || config.defaultWallet;
|
|
329
|
+
const exported = exportWallet(effectiveName, password);
|
|
330
|
+
return exported.solana.privateKey;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ============= Transaction Signing =============
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Sign a Solana transaction (base64) using the appropriate wallet type.
|
|
337
|
+
* Returns base64-encoded signed transaction.
|
|
338
|
+
*/
|
|
339
|
+
export async function signTransaction(txBase64, walletType, walletInfo) {
|
|
340
|
+
if (walletType === 'local') {
|
|
341
|
+
return signSolanaTransaction(txBase64, walletInfo.privateKeyHex);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (walletType === 'privy') {
|
|
345
|
+
const result = await walletInfo.privyClient.signSolanaTransaction(
|
|
346
|
+
walletInfo.walletId,
|
|
347
|
+
txBase64,
|
|
348
|
+
);
|
|
349
|
+
return result.data?.signed_transaction || result.signed_transaction;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (walletType === 'walletconnect') {
|
|
353
|
+
// WC expects base58 for Solana transactions
|
|
354
|
+
const txBytes = Buffer.from(txBase64, 'base64');
|
|
355
|
+
const txBase58 = base58Encode(txBytes);
|
|
356
|
+
const result = await sendSolanaTransactionViaWalletConnect(txBase58);
|
|
357
|
+
if (result.signedTransaction) {
|
|
358
|
+
// WC returns base58; convert to base64
|
|
359
|
+
const signedBytes = base58Decode(result.signedTransaction);
|
|
360
|
+
return Buffer.from(signedBytes).toString('base64');
|
|
361
|
+
}
|
|
362
|
+
throw new Error('WalletConnect did not return a signed transaction');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
throw new Error(`Unsupported wallet type: ${walletType}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ============= Expiry Parsing =============
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Parse an expiry duration string to epoch milliseconds.
|
|
372
|
+
* Accepts: "24h", "7d", "30d", or raw epoch ms string.
|
|
373
|
+
* Returns null for no expiry.
|
|
374
|
+
*/
|
|
375
|
+
export function parseExpiry(expiryStr) {
|
|
376
|
+
if (!expiryStr || expiryStr === 'never') return null;
|
|
377
|
+
|
|
378
|
+
const match = expiryStr.match(/^(\d+)(h|d)$/i);
|
|
379
|
+
if (match) {
|
|
380
|
+
const value = parseInt(match[1], 10);
|
|
381
|
+
const unit = match[2].toLowerCase();
|
|
382
|
+
const ms = unit === 'h' ? value * 3600 * 1000 : value * 24 * 3600 * 1000;
|
|
383
|
+
return Date.now() + ms;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Try as raw epoch ms
|
|
387
|
+
const num = Number(expiryStr);
|
|
388
|
+
if (!isNaN(num) && num > Date.now() - 86400000) {
|
|
389
|
+
return num;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
throw new Error(`Invalid expiry format: "${expiryStr}". Use "24h", "7d", "30d", or epoch ms.`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ============= Order Formatting =============
|
|
396
|
+
|
|
397
|
+
function formatOrderStatus(status) {
|
|
398
|
+
const map = {
|
|
399
|
+
pending: 'Pending',
|
|
400
|
+
open: 'Open',
|
|
401
|
+
executing: 'Executing',
|
|
402
|
+
filled: 'Filled',
|
|
403
|
+
pending_withdraw: 'Withdrawing',
|
|
404
|
+
cancelled: 'Cancelled',
|
|
405
|
+
expired: 'Expired',
|
|
406
|
+
failed: 'Failed',
|
|
407
|
+
};
|
|
408
|
+
return map[status] || status;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Reverse lookup: address → { symbol, decimals } for known Solana tokens
|
|
412
|
+
const KNOWN_SOLANA_TOKENS = {
|
|
413
|
+
'So11111111111111111111111111111111111111112': { symbol: 'SOL', decimals: 9 },
|
|
414
|
+
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': { symbol: 'USDC', decimals: 6 },
|
|
415
|
+
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': { symbol: 'USDT', decimals: 6 },
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
function tokenLabel(address) {
|
|
419
|
+
if (!address) return '?';
|
|
420
|
+
const info = KNOWN_SOLANA_TOKENS[address];
|
|
421
|
+
return info ? `${info.symbol} (${address})` : address;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Format a base-unit amount to human-readable (e.g. 116000000 SOL → "0.116 SOL").
|
|
426
|
+
* Falls back to raw amount for unknown tokens.
|
|
427
|
+
*/
|
|
428
|
+
function formatAmount(amount, mintAddress) {
|
|
429
|
+
if (!amount) return '?';
|
|
430
|
+
const info = KNOWN_SOLANA_TOKENS[mintAddress];
|
|
431
|
+
if (!info) return `${amount} ${mintAddress || '?'}`;
|
|
432
|
+
const raw = BigInt(amount);
|
|
433
|
+
const divisor = BigInt(10 ** info.decimals);
|
|
434
|
+
const whole = raw / divisor;
|
|
435
|
+
const frac = raw % divisor;
|
|
436
|
+
const fracStr = frac.toString().padStart(info.decimals, '0').replace(/0+$/, '');
|
|
437
|
+
const humanAmount = fracStr ? `${whole}.${fracStr}` : `${whole}`;
|
|
438
|
+
return `${humanAmount} ${info.symbol} (${amount} base units)`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function formatTimestamp(ts) {
|
|
442
|
+
if (!ts) return '?';
|
|
443
|
+
const num = Number(ts);
|
|
444
|
+
if (isNaN(num)) return ts;
|
|
445
|
+
const date = new Date(num);
|
|
446
|
+
return `${date.toLocaleString()} (${date.toISOString()})`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function formatOrder(order, index) {
|
|
450
|
+
const lines = [];
|
|
451
|
+
const label = index !== undefined ? ` Order #${index + 1}` : ' Order';
|
|
452
|
+
lines.push(`${label} (${order.id})`);
|
|
453
|
+
lines.push(` Status: ${formatOrderStatus(order.status)}`);
|
|
454
|
+
lines.push(` Sell: ${formatAmount(order.inputAmount, order.inputMint)}`);
|
|
455
|
+
lines.push(` Buy: ${tokenLabel(order.outputMint)}`);
|
|
456
|
+
lines.push(` Trigger: ${order.triggerCondition} $${order.triggerPriceUsd} on ${tokenLabel(order.triggerMint)}`);
|
|
457
|
+
lines.push(` Slippage: ${order.slippageBps != null ? `${order.slippageBps} bps` : 'auto'}`);
|
|
458
|
+
lines.push(` Created: ${formatTimestamp(order.createdAt)}`);
|
|
459
|
+
if (order.expiresAt) lines.push(` Expires: ${formatTimestamp(order.expiresAt)}`);
|
|
460
|
+
if (order.fills?.length > 0) {
|
|
461
|
+
lines.push(` Fills: ${order.fills.length}`);
|
|
462
|
+
for (const fill of order.fills) {
|
|
463
|
+
lines.push(` ${fill.inputAmount} → ${fill.outputAmount} (${fill.txSignature?.slice(0, 12)}...)`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return lines.join('\n');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ============= CLI Command Builder =============
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Build limit order command handlers for CLI integration.
|
|
473
|
+
*/
|
|
474
|
+
export function buildLimitOrderCommands(deps = {}) {
|
|
475
|
+
const { log = console.log, exit = process.exit } = deps;
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
'create': async (args, apiInstance, flags, options) => {
|
|
479
|
+
const fromRaw = options.from || options['from-token'] || args[0];
|
|
480
|
+
const toRaw = options.to || options['to-token'] || args[1];
|
|
481
|
+
const from = resolveTokenAddress(fromRaw, 'solana');
|
|
482
|
+
const to = resolveTokenAddress(toRaw, 'solana');
|
|
483
|
+
const amount = options.amount || args[2];
|
|
484
|
+
const triggerPrice = options['trigger-price'];
|
|
485
|
+
const triggerCondition = options['trigger-condition'];
|
|
486
|
+
const triggerMintRaw = options['trigger-mint'];
|
|
487
|
+
const slippageBps = options['slippage-bps'] != null ? Number(options['slippage-bps']) : undefined;
|
|
488
|
+
const expiresStr = options.expires || '30d';
|
|
489
|
+
const walletName = options.wallet;
|
|
490
|
+
|
|
491
|
+
if (!from || !to || !amount || triggerPrice == null || !triggerMintRaw || !triggerCondition) {
|
|
492
|
+
log(`
|
|
493
|
+
Usage: nansen trade limit-order create --from <token> --to <token> --amount <amount> --trigger-mint <token> --trigger-condition <above|below> --trigger-price <usd>
|
|
494
|
+
|
|
495
|
+
OPTIONS:
|
|
496
|
+
--from <symbol|address> Token to sell (symbol like SOL, USDC or address)
|
|
497
|
+
--to <symbol|address> Token to buy (symbol like USDC, SOL or address)
|
|
498
|
+
--amount <amount> Amount to sell in token units (e.g. 1.5 for 1.5 SOL, 80 for 80 USDC)
|
|
499
|
+
--trigger-mint <symbol|addr> Token whose price triggers the order (e.g. SOL)
|
|
500
|
+
--trigger-condition <cond> "above" or "below"
|
|
501
|
+
--trigger-price <usd> Trigger price in USD (must be a positive number)
|
|
502
|
+
--slippage-bps <bps> Slippage in basis points (100 = 1%), omit for auto
|
|
503
|
+
--expires <duration> Expiry duration: "24h", "7d", "30d" (default: 30d)
|
|
504
|
+
--wallet <name> Wallet name (or "walletconnect"/"wc")
|
|
505
|
+
|
|
506
|
+
EXAMPLES:
|
|
507
|
+
# Sell 1 SOL for USDC when SOL drops below $80
|
|
508
|
+
nansen trade limit-order create --from SOL --to USDC --amount 1 --trigger-mint SOL --trigger-condition below --trigger-price 80
|
|
509
|
+
# Buy SOL with 80 USDC when SOL goes above $100
|
|
510
|
+
nansen trade limit-order create --from USDC --to SOL --amount 80 --trigger-mint SOL --trigger-condition above --trigger-price 100`);
|
|
511
|
+
exit(1);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Validate token addresses are valid Solana addresses (catches EVM addresses, typos, etc.)
|
|
516
|
+
const fromValidation = validateTokenAddress(from, 'solana');
|
|
517
|
+
if (!fromValidation.valid) {
|
|
518
|
+
log(`Error: Invalid --from token address: ${fromValidation.error}`);
|
|
519
|
+
exit(1);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const toValidation = validateTokenAddress(to, 'solana');
|
|
523
|
+
if (!toValidation.valid) {
|
|
524
|
+
log(`Error: Invalid --to token address: ${toValidation.error}`);
|
|
525
|
+
exit(1);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Amount is always in human-readable token units (e.g. 1.5 = 1.5 SOL)
|
|
530
|
+
// Converted to base units (lamports) internally
|
|
531
|
+
let amountBaseUnits;
|
|
532
|
+
try {
|
|
533
|
+
const num = Number(amount);
|
|
534
|
+
if (isNaN(num) || num <= 0) {
|
|
535
|
+
log('Error: --amount must be a positive number in token units (e.g. 1.5 for 1.5 SOL, 80 for 80 USDC).');
|
|
536
|
+
exit(1);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const fromInfo = KNOWN_SOLANA_TOKENS[from];
|
|
540
|
+
let decimals;
|
|
541
|
+
if (fromInfo) {
|
|
542
|
+
decimals = fromInfo.decimals;
|
|
543
|
+
} else {
|
|
544
|
+
const tokenInfo = await getTokenInfo(CHAIN_RPCS.solana, from);
|
|
545
|
+
decimals = tokenInfo.decimals;
|
|
546
|
+
}
|
|
547
|
+
amountBaseUnits = String(parseAmount(String(amount), decimals));
|
|
548
|
+
} catch (err) {
|
|
549
|
+
log(`Error: Could not resolve decimals for ${from}: ${err.message}`);
|
|
550
|
+
exit(1);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const price = Number(triggerPrice);
|
|
555
|
+
if (isNaN(price) || price <= 0) {
|
|
556
|
+
log('Error: --trigger-price must be a positive number (USD price).');
|
|
557
|
+
exit(1);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (triggerCondition !== 'above' && triggerCondition !== 'below') {
|
|
562
|
+
log('Error: --trigger-condition must be "above" or "below".');
|
|
563
|
+
exit(1);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
let expiresAt;
|
|
568
|
+
try {
|
|
569
|
+
expiresAt = parseExpiry(expiresStr);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
log(`Error: ${err.message}`);
|
|
572
|
+
exit(1);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const triggerMint = resolveTokenAddress(triggerMintRaw, 'solana');
|
|
577
|
+
|
|
578
|
+
const tmValidation = validateTokenAddress(triggerMint, 'solana');
|
|
579
|
+
if (!tmValidation.valid) {
|
|
580
|
+
log(`Error: Invalid --trigger-mint address: ${tmValidation.error}`);
|
|
581
|
+
exit(1);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
// 1. Resolve wallet
|
|
587
|
+
const resolved = await resolveSolanaWallet(walletName, deps);
|
|
588
|
+
if (!resolved) return;
|
|
589
|
+
|
|
590
|
+
let { pubkey, walletType, walletInfo } = resolved;
|
|
591
|
+
|
|
592
|
+
// For local wallets, load private key now
|
|
593
|
+
if (walletType === 'local') {
|
|
594
|
+
const privateKeyHex = getLocalWalletPrivateKey(resolved.walletName);
|
|
595
|
+
walletInfo = { privateKeyHex };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
log(`\nCreating limit order on Solana...`);
|
|
599
|
+
log(` Wallet: ${pubkey}`);
|
|
600
|
+
log(` Sell: ${formatAmount(amountBaseUnits, from)}`);
|
|
601
|
+
log(` Buy: ${to}`);
|
|
602
|
+
log(` Trigger: $${price} (${triggerCondition})`);
|
|
603
|
+
|
|
604
|
+
// 2. Authenticate
|
|
605
|
+
const token = await authenticate(pubkey, walletType, walletInfo, log);
|
|
606
|
+
|
|
607
|
+
// 3. Check vault, auto-register if needed
|
|
608
|
+
// Backend returns { vaultPubkey: "..." } when vault exists, or throws/returns empty when not
|
|
609
|
+
let hasVault = false;
|
|
610
|
+
try {
|
|
611
|
+
const vaultInfo = await getVault(token, pubkey);
|
|
612
|
+
hasVault = !!(vaultInfo?.vaultPubkey || vaultInfo?.vaultAddress);
|
|
613
|
+
} catch {
|
|
614
|
+
// No vault found
|
|
615
|
+
}
|
|
616
|
+
if (!hasVault) {
|
|
617
|
+
log(' Registering vault for first-time use...');
|
|
618
|
+
try {
|
|
619
|
+
await registerVault(token);
|
|
620
|
+
} catch (regErr) {
|
|
621
|
+
// Vault may already exist — ignore "already registered" errors
|
|
622
|
+
if (!/already registered/i.test(regErr.message)) throw regErr;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// 4. Craft deposit transaction
|
|
627
|
+
log(' Crafting deposit transaction...');
|
|
628
|
+
const deposit = await craftDeposit(token, {
|
|
629
|
+
inputMint: from,
|
|
630
|
+
outputMint: to,
|
|
631
|
+
userAddress: pubkey,
|
|
632
|
+
amount: amountBaseUnits,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// 5. Sign deposit transaction
|
|
636
|
+
log(' Signing deposit transaction...');
|
|
637
|
+
const signedDepositTx = await signTransaction(deposit.transaction, walletType, walletInfo);
|
|
638
|
+
|
|
639
|
+
// 6. Create order
|
|
640
|
+
log(' Submitting order...');
|
|
641
|
+
const orderParams = {
|
|
642
|
+
orderType: 'single',
|
|
643
|
+
depositRequestId: deposit.requestId,
|
|
644
|
+
depositSignedTx: signedDepositTx,
|
|
645
|
+
userPubkey: pubkey,
|
|
646
|
+
inputMint: from,
|
|
647
|
+
inputAmount: amountBaseUnits,
|
|
648
|
+
outputMint: to,
|
|
649
|
+
triggerMint,
|
|
650
|
+
triggerCondition,
|
|
651
|
+
triggerPriceUsd: price, // Must be Number, not string
|
|
652
|
+
...(slippageBps != null ? { slippageBps } : {}),
|
|
653
|
+
...(expiresAt != null ? { expiresAt } : {}),
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const result = await createOrder(token, orderParams);
|
|
657
|
+
|
|
658
|
+
log(`\n ✓ Limit order created`);
|
|
659
|
+
log(` Order ID: ${result.id}`);
|
|
660
|
+
log(` Tx: ${result.txSignature}`);
|
|
661
|
+
log(` Explorer: ${SOLSCAN_TX_URL}${result.txSignature}`);
|
|
662
|
+
log('');
|
|
663
|
+
|
|
664
|
+
} catch (err) {
|
|
665
|
+
log(`Error: ${err.message}`);
|
|
666
|
+
if (err.details) log(` Details: ${JSON.stringify(err.details)}`);
|
|
667
|
+
if (err.cause) log(` Cause: ${err.cause.message || err.cause}`);
|
|
668
|
+
exit(1);
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
|
|
672
|
+
'list': async (args, apiInstance, flags, options) => {
|
|
673
|
+
const walletName = options.wallet;
|
|
674
|
+
const state = options.state;
|
|
675
|
+
const mint = options.mint ? resolveTokenAddress(options.mint, 'solana') : undefined;
|
|
676
|
+
const limit = options.limit || 20;
|
|
677
|
+
const offset = options.offset || 0;
|
|
678
|
+
const sort = options.sort;
|
|
679
|
+
const dir = options.dir || 'desc';
|
|
680
|
+
|
|
681
|
+
if (mint) {
|
|
682
|
+
const mintValidation = validateTokenAddress(mint, 'solana');
|
|
683
|
+
if (!mintValidation.valid) {
|
|
684
|
+
log(`Error: Invalid --mint address: ${mintValidation.error}`);
|
|
685
|
+
exit(1);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
const resolved = await resolveSolanaWallet(walletName, deps);
|
|
692
|
+
if (!resolved) return;
|
|
693
|
+
|
|
694
|
+
let { pubkey, walletType, walletInfo } = resolved;
|
|
695
|
+
|
|
696
|
+
// For local wallets, load private key for auth
|
|
697
|
+
if (walletType === 'local') {
|
|
698
|
+
const privateKeyHex = getLocalWalletPrivateKey(resolved.walletName);
|
|
699
|
+
walletInfo = { privateKeyHex };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const token = await authenticate(pubkey, walletType, walletInfo, log);
|
|
703
|
+
|
|
704
|
+
const result = await listOrders(token, pubkey, { state, mint, limit, offset, sort, dir });
|
|
705
|
+
const orders = result.orders || [];
|
|
706
|
+
|
|
707
|
+
if (orders.length === 0) {
|
|
708
|
+
log('\nNo limit orders found.');
|
|
709
|
+
if (state) log(` (filtered by state: ${state})`);
|
|
710
|
+
log('');
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
log(`\nLimit Orders (${result.pagination?.total || orders.length} total):\n`);
|
|
715
|
+
orders.forEach((order, i) => {
|
|
716
|
+
if (i > 0) log('');
|
|
717
|
+
log(formatOrder(order, i));
|
|
718
|
+
});
|
|
719
|
+
if (result.pagination && result.pagination.total > offset + orders.length) {
|
|
720
|
+
log(`\n Showing ${offset + 1}-${offset + orders.length} of ${result.pagination.total}. Use --offset ${offset + orders.length} to see more.`);
|
|
721
|
+
}
|
|
722
|
+
log('');
|
|
723
|
+
|
|
724
|
+
} catch (err) {
|
|
725
|
+
log(`Error: ${err.message}`);
|
|
726
|
+
if (err.details) log(` Details: ${JSON.stringify(err.details)}`);
|
|
727
|
+
exit(1);
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
|
|
731
|
+
'cancel': async (args, apiInstance, flags, options) => {
|
|
732
|
+
const orderId = options.order || options['order-id'] || args[0];
|
|
733
|
+
const walletName = options.wallet;
|
|
734
|
+
|
|
735
|
+
if (!orderId) {
|
|
736
|
+
log(`
|
|
737
|
+
Usage: nansen trade limit-order cancel --order <orderId>
|
|
738
|
+
|
|
739
|
+
OPTIONS:
|
|
740
|
+
--order <id> Order ID to cancel
|
|
741
|
+
--wallet <name> Wallet name (or "walletconnect"/"wc")
|
|
742
|
+
|
|
743
|
+
EXAMPLES:
|
|
744
|
+
nansen trade limit-order cancel --order abc123`);
|
|
745
|
+
exit(1);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
const resolved = await resolveSolanaWallet(walletName, deps);
|
|
751
|
+
if (!resolved) return;
|
|
752
|
+
|
|
753
|
+
let { pubkey, walletType, walletInfo } = resolved;
|
|
754
|
+
|
|
755
|
+
if (walletType === 'local') {
|
|
756
|
+
const privateKeyHex = getLocalWalletPrivateKey(resolved.walletName);
|
|
757
|
+
walletInfo = { privateKeyHex };
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
log(`\nCancelling order ${orderId}...`);
|
|
761
|
+
|
|
762
|
+
// 1. Authenticate
|
|
763
|
+
const token = await authenticate(pubkey, walletType, walletInfo, log);
|
|
764
|
+
|
|
765
|
+
// 2. Request cancellation — get unsigned withdrawal tx
|
|
766
|
+
log(' Requesting cancellation...');
|
|
767
|
+
const cancelResult = await cancelOrderRequest(token, orderId);
|
|
768
|
+
|
|
769
|
+
// 3. Sign the withdrawal transaction
|
|
770
|
+
log(' Signing withdrawal transaction...');
|
|
771
|
+
const signedTx = await signTransaction(cancelResult.transaction, walletType, walletInfo);
|
|
772
|
+
|
|
773
|
+
// 4. Confirm cancellation
|
|
774
|
+
log(' Confirming cancellation...');
|
|
775
|
+
const confirmed = await confirmCancelOrder(token, orderId, {
|
|
776
|
+
signedTransaction: signedTx,
|
|
777
|
+
cancelRequestId: cancelResult.requestId,
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
log(`\n ✓ Order cancelled`);
|
|
781
|
+
log(` Order ID: ${confirmed.id}`);
|
|
782
|
+
log(` Tx: ${confirmed.txSignature}`);
|
|
783
|
+
log(` Explorer: ${SOLSCAN_TX_URL}${confirmed.txSignature}`);
|
|
784
|
+
log('');
|
|
785
|
+
|
|
786
|
+
} catch (err) {
|
|
787
|
+
log(`Error: ${err.message}`);
|
|
788
|
+
if (err.details) log(` Details: ${JSON.stringify(err.details)}`);
|
|
789
|
+
exit(1);
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
|
|
793
|
+
'update': async (args, apiInstance, flags, options) => {
|
|
794
|
+
const orderId = options.order || options['order-id'] || args[0];
|
|
795
|
+
const triggerPrice = options['trigger-price'];
|
|
796
|
+
const slippageBps = options['slippage-bps'];
|
|
797
|
+
const walletName = options.wallet;
|
|
798
|
+
|
|
799
|
+
if (!orderId) {
|
|
800
|
+
log(`
|
|
801
|
+
Usage: nansen trade limit-order update --order <orderId> [--trigger-price <usd>] [--slippage-bps <bps>]
|
|
802
|
+
|
|
803
|
+
OPTIONS:
|
|
804
|
+
--order <id> Order ID to update
|
|
805
|
+
--trigger-price <usd> New trigger price in USD
|
|
806
|
+
--slippage-bps <bps> Slippage in basis points (100 = 1%)
|
|
807
|
+
--wallet <name> Wallet name (or "walletconnect"/"wc")
|
|
808
|
+
|
|
809
|
+
NOTE: Only provided fields are updated. Auto slippage can only be set at creation time
|
|
810
|
+
(by omitting --slippage-bps from the create command).
|
|
811
|
+
|
|
812
|
+
EXAMPLES:
|
|
813
|
+
nansen trade limit-order update --order abc123 --trigger-price 85
|
|
814
|
+
nansen trade limit-order update --order abc123 --slippage-bps 100`);
|
|
815
|
+
exit(1);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (triggerPrice == null && slippageBps == null) {
|
|
820
|
+
log('Error: Provide at least one of --trigger-price or --slippage-bps to update.');
|
|
821
|
+
exit(1);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const updateBody = { orderType: 'single' };
|
|
826
|
+
if (triggerPrice != null) {
|
|
827
|
+
const price = Number(triggerPrice);
|
|
828
|
+
if (isNaN(price) || price <= 0) {
|
|
829
|
+
log('Error: --trigger-price must be a positive number.');
|
|
830
|
+
exit(1);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
updateBody.triggerPriceUsd = price;
|
|
834
|
+
}
|
|
835
|
+
if (slippageBps != null) {
|
|
836
|
+
const bps = Number(slippageBps);
|
|
837
|
+
if (isNaN(bps) || bps < 0 || bps > 10000) {
|
|
838
|
+
log('Error: --slippage-bps must be between 0 and 10000 basis points.');
|
|
839
|
+
exit(1);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
updateBody.slippageBps = bps;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
try {
|
|
846
|
+
const resolved = await resolveSolanaWallet(walletName, deps);
|
|
847
|
+
if (!resolved) return;
|
|
848
|
+
|
|
849
|
+
let { pubkey, walletType, walletInfo } = resolved;
|
|
850
|
+
|
|
851
|
+
if (walletType === 'local') {
|
|
852
|
+
const privateKeyHex = getLocalWalletPrivateKey(resolved.walletName);
|
|
853
|
+
walletInfo = { privateKeyHex };
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
log(`\nUpdating order ${orderId}...`);
|
|
857
|
+
|
|
858
|
+
const token = await authenticate(pubkey, walletType, walletInfo, log);
|
|
859
|
+
await updateOrder(token, orderId, updateBody);
|
|
860
|
+
|
|
861
|
+
log(`\n ✓ Order updated`);
|
|
862
|
+
if (updateBody.triggerPriceUsd != null) log(` Trigger price: $${updateBody.triggerPriceUsd}`);
|
|
863
|
+
if (updateBody.slippageBps != null) log(` Slippage: ${updateBody.slippageBps} bps`);
|
|
864
|
+
log('');
|
|
865
|
+
|
|
866
|
+
} catch (err) {
|
|
867
|
+
log(`Error: ${err.message}`);
|
|
868
|
+
if (err.details) log(` Details: ${JSON.stringify(err.details)}`);
|
|
869
|
+
exit(1);
|
|
870
|
+
}
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
}
|
package/src/privy.js
CHANGED
|
@@ -125,6 +125,15 @@ export class PrivyClient {
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
async signSolanaMessage(walletId, messageBuffer) {
|
|
129
|
+
const messageBase64 = Buffer.from(messageBuffer).toString('base64');
|
|
130
|
+
return this._request("POST", `/wallets/${walletId}/rpc`, {
|
|
131
|
+
method: "signMessage",
|
|
132
|
+
chain_type: "solana",
|
|
133
|
+
params: { message: messageBase64, encoding: "base64" },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
// ============= Helpers =============
|
package/src/schema.json
CHANGED
|
@@ -500,6 +500,19 @@
|
|
|
500
500
|
"default": true
|
|
501
501
|
}
|
|
502
502
|
}
|
|
503
|
+
},
|
|
504
|
+
"top-tokens": {
|
|
505
|
+
"endpoint": "/api/v1/nansen-score/top-tokens",
|
|
506
|
+
"description": "Top tokens ranked by Nansen Score (public endpoint, any authenticated API key)",
|
|
507
|
+
"options": {
|
|
508
|
+
"market-cap": {
|
|
509
|
+
"description": "Filter by market cap group",
|
|
510
|
+
"enum": ["lowcap", "midcap", "largecap"]
|
|
511
|
+
},
|
|
512
|
+
"limit": {
|
|
513
|
+
"default": 25
|
|
514
|
+
}
|
|
515
|
+
}
|
|
503
516
|
}
|
|
504
517
|
},
|
|
505
518
|
"description": "Token God Mode - deep analytics for any token"
|
|
@@ -906,6 +919,138 @@
|
|
|
906
919
|
"description": "Destination chain (solana or base)"
|
|
907
920
|
}
|
|
908
921
|
}
|
|
922
|
+
},
|
|
923
|
+
"limit-order": {
|
|
924
|
+
"description": "Limit order management (Solana only)",
|
|
925
|
+
"subcommands": {
|
|
926
|
+
"create": {
|
|
927
|
+
"description": "Place a new limit order",
|
|
928
|
+
"options": {
|
|
929
|
+
"from": {
|
|
930
|
+
"type": "string",
|
|
931
|
+
"required": true,
|
|
932
|
+
"description": "Token to sell (address or symbol)"
|
|
933
|
+
},
|
|
934
|
+
"to": {
|
|
935
|
+
"type": "string",
|
|
936
|
+
"required": true,
|
|
937
|
+
"description": "Token to buy (address or symbol)"
|
|
938
|
+
},
|
|
939
|
+
"amount": {
|
|
940
|
+
"type": "string",
|
|
941
|
+
"required": true,
|
|
942
|
+
"description": "Amount in base units (e.g. lamports)"
|
|
943
|
+
},
|
|
944
|
+
"trigger-price": {
|
|
945
|
+
"type": "number",
|
|
946
|
+
"required": true,
|
|
947
|
+
"description": "Trigger price in USD"
|
|
948
|
+
},
|
|
949
|
+
"trigger-condition": {
|
|
950
|
+
"type": "string",
|
|
951
|
+
"required": true,
|
|
952
|
+
"description": "Trigger when price is 'above' or 'below' target"
|
|
953
|
+
},
|
|
954
|
+
"trigger-mint": {
|
|
955
|
+
"type": "string",
|
|
956
|
+
"required": true,
|
|
957
|
+
"description": "Token whose price triggers the order (e.g. SOL)"
|
|
958
|
+
},
|
|
959
|
+
"slippage-bps": {
|
|
960
|
+
"type": "number",
|
|
961
|
+
"description": "Slippage tolerance in basis points (e.g. 50 = 0.5%)"
|
|
962
|
+
},
|
|
963
|
+
"expires": {
|
|
964
|
+
"type": "string",
|
|
965
|
+
"default": "30d",
|
|
966
|
+
"description": "Expiry duration (e.g. 24h, 7d, 30d) or epoch ms"
|
|
967
|
+
},
|
|
968
|
+
"wallet": {
|
|
969
|
+
"type": "string",
|
|
970
|
+
"description": "Wallet name (or \"walletconnect\"/\"wc\")"
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
"chains": ["solana"],
|
|
974
|
+
"prerequisites": [
|
|
975
|
+
"A Solana wallet must be configured. Run: nansen wallet create"
|
|
976
|
+
]
|
|
977
|
+
},
|
|
978
|
+
"list": {
|
|
979
|
+
"description": "List your limit orders",
|
|
980
|
+
"options": {
|
|
981
|
+
"state": {
|
|
982
|
+
"type": "string",
|
|
983
|
+
"description": "Filter by state (active, past)"
|
|
984
|
+
},
|
|
985
|
+
"mint": {
|
|
986
|
+
"type": "string",
|
|
987
|
+
"description": "Filter by token mint address"
|
|
988
|
+
},
|
|
989
|
+
"limit": {
|
|
990
|
+
"type": "number",
|
|
991
|
+
"default": 20,
|
|
992
|
+
"description": "Max results per page"
|
|
993
|
+
},
|
|
994
|
+
"offset": {
|
|
995
|
+
"type": "number",
|
|
996
|
+
"default": 0,
|
|
997
|
+
"description": "Pagination offset"
|
|
998
|
+
},
|
|
999
|
+
"sort": {
|
|
1000
|
+
"type": "string",
|
|
1001
|
+
"description": "Sort field"
|
|
1002
|
+
},
|
|
1003
|
+
"dir": {
|
|
1004
|
+
"type": "string",
|
|
1005
|
+
"default": "desc",
|
|
1006
|
+
"description": "Sort direction (asc or desc)"
|
|
1007
|
+
},
|
|
1008
|
+
"wallet": {
|
|
1009
|
+
"type": "string",
|
|
1010
|
+
"description": "Wallet name (or \"walletconnect\"/\"wc\")"
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
"chains": ["solana"]
|
|
1014
|
+
},
|
|
1015
|
+
"cancel": {
|
|
1016
|
+
"description": "Cancel an open limit order",
|
|
1017
|
+
"options": {
|
|
1018
|
+
"order": {
|
|
1019
|
+
"type": "string",
|
|
1020
|
+
"required": true,
|
|
1021
|
+
"description": "Order ID to cancel"
|
|
1022
|
+
},
|
|
1023
|
+
"wallet": {
|
|
1024
|
+
"type": "string",
|
|
1025
|
+
"description": "Wallet name (or \"walletconnect\"/\"wc\")"
|
|
1026
|
+
}
|
|
1027
|
+
},
|
|
1028
|
+
"chains": ["solana"]
|
|
1029
|
+
},
|
|
1030
|
+
"update": {
|
|
1031
|
+
"description": "Update trigger price or slippage on an existing order",
|
|
1032
|
+
"options": {
|
|
1033
|
+
"order": {
|
|
1034
|
+
"type": "string",
|
|
1035
|
+
"required": true,
|
|
1036
|
+
"description": "Order ID to update"
|
|
1037
|
+
},
|
|
1038
|
+
"trigger-price": {
|
|
1039
|
+
"type": "number",
|
|
1040
|
+
"description": "New trigger price in USD"
|
|
1041
|
+
},
|
|
1042
|
+
"slippage-bps": {
|
|
1043
|
+
"type": "number",
|
|
1044
|
+
"description": "New slippage in basis points (0-10000)"
|
|
1045
|
+
},
|
|
1046
|
+
"wallet": {
|
|
1047
|
+
"type": "string",
|
|
1048
|
+
"description": "Wallet name (or \"walletconnect\"/\"wc\")"
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
"chains": ["solana"]
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
909
1054
|
}
|
|
910
1055
|
}
|
|
911
1056
|
},
|
package/src/telemetry.js
CHANGED
|
@@ -158,9 +158,13 @@ function buildContext() {
|
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
160
|
* Convert a command string like "smart-money netflow" to a path like "/smart-money/netflow".
|
|
161
|
+
* Agent ("agent …") always maps to "/agent" so prompts are not exploded into path segments.
|
|
161
162
|
*/
|
|
162
163
|
function commandToPath(command) {
|
|
163
|
-
|
|
164
|
+
if (typeof command === 'string' && /^agent(?:\s+|$)/.test(command.trimStart())) {
|
|
165
|
+
return '/agent';
|
|
166
|
+
}
|
|
167
|
+
return '/' + String(command).replace(/\s+/g, '/');
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
/**
|
package/src/transfer.js
CHANGED
|
@@ -302,7 +302,7 @@ function modPowBig(base, exp, mod) {
|
|
|
302
302
|
return result;
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
async function getTokenInfo(rpcUrl, mint) {
|
|
305
|
+
export async function getTokenInfo(rpcUrl, mint) {
|
|
306
306
|
// Get mint account to determine token program and decimals
|
|
307
307
|
const info = await rpcCall(rpcUrl, 'getAccountInfo', [mint, { encoding: 'jsonParsed' }]);
|
|
308
308
|
if (!info || !info.value) throw new Error(`Token mint ${mint} not found`);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { wcExec } from './walletconnect-exec.js';
|
|
12
|
+
import { base58Encode } from './wallet.js';
|
|
12
13
|
|
|
13
14
|
const SOLANA_MAINNET_CHAIN = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
|
|
14
15
|
|
|
@@ -153,3 +154,27 @@ export async function sendSolanaTransactionViaWalletConnect(txBase58, timeoutMs
|
|
|
153
154
|
|
|
154
155
|
throw new Error('Unexpected response from walletconnect Solana sign');
|
|
155
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Sign a Solana message via WalletConnect.
|
|
160
|
+
*
|
|
161
|
+
* Used for challenge-response authentication (e.g., Jupiter Limit Order V2).
|
|
162
|
+
* Returns the raw Ed25519 signature as base58.
|
|
163
|
+
*
|
|
164
|
+
* @param {Buffer} messageBuffer - Raw message bytes to sign
|
|
165
|
+
* @param {number} [timeoutMs=120000] - Timeout for user approval
|
|
166
|
+
* @returns {{ signature: string }} Base58-encoded signature
|
|
167
|
+
*/
|
|
168
|
+
export async function signSolanaMessageViaWalletConnect(messageBuffer, timeoutMs = 120000) {
|
|
169
|
+
const payload = {
|
|
170
|
+
message: base58Encode(messageBuffer),
|
|
171
|
+
chainId: SOLANA_MAINNET_CHAIN,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const output = await wcExec('walletconnect', ['sign-message', JSON.stringify(payload)], timeoutMs);
|
|
175
|
+
const result = parseWcJson(output);
|
|
176
|
+
|
|
177
|
+
if (result.signature) return { signature: result.signature };
|
|
178
|
+
|
|
179
|
+
throw new Error('Unexpected response from walletconnect sign-message');
|
|
180
|
+
}
|