feat: add Jupiter Limit Order V2 support (Solana)#328
feat: add Jupiter Limit Order V2 support (Solana)#328
Conversation
a1def47 to
fbb0817
Compare
TimNooren
left a comment
There was a problem hiding this comment.
@Pepewitch Thanks🙌 Some small comments. My main concern is the interface, since it's going to be difficult to change once released. I think the current nansen trade quote/execute interface is not ideal, but I'd say having at least a consistent interface for now is more important, so let's stick with it.
Let's also add a changeset file🙏
src/cli.js
Outdated
| update Update trigger price or slippage | ||
|
|
||
| USAGE: | ||
| nansen trade limit-order create --from <token> --to <token> --amount <units> --trigger-mint <token> --trigger-condition <above|below> --trigger-price <usd> |
There was a problem hiding this comment.
IIUC the unit here is the human-readable amount. In nansen trade quote/execute we use raw amounts (i.e. not adjusting for decimals). Maybe we should be consistent at least? We can add a --amount-unit, similar to #324.
Also, I saw that this needs to be >$10. Might also be good to document.
src/schema.json
Outdated
| }, | ||
| "slippage-bps": { | ||
| "type": "number", | ||
| "description": "Slippage tolerance in basis points (e.g. 50 = 0.5%)" |
There was a problem hiding this comment.
In nansen trade quote/execute we use slippage and it's a human-readable percentage. Would be good to be consistent.
src/schema.json
Outdated
| "amount": { | ||
| "type": "string", | ||
| "required": true, | ||
| "description": "Amount in base units (e.g. lamports)" |
There was a problem hiding this comment.
This is currently not correct right?
src/limit-order.js
Outdated
|
|
||
| /** | ||
| * Resolve a Solana wallet for limit orders. | ||
| * Follows the same 3-way dispatch as trading.js: WalletConnect / named / default. |
There was a problem hiding this comment.
Can we extract this code then instead of duplicating?
src/limit-order.js
Outdated
|
|
||
| if (!orderId) { | ||
| log(` | ||
| Usage: nansen trade limit-order update --order <orderId> [--trigger-price <usd>] [--slippage-bps <bps>] |
src/limit-order.js
Outdated
| headers['Authorization'] = `Bearer ${token}`; | ||
| } | ||
| if (process.env.NANSEN_API_KEY) { | ||
| headers['X-API-Key'] = process.env.NANSEN_API_KEY; |
There was a problem hiding this comment.
This should probably not be included.
0xlaveen
left a comment
There was a problem hiding this comment.
Smoke test results (Privy wallet, Solana mainnet)
Ran 42 end-to-end tests against a Privy server wallet (Hm6a7ZkpPqTHZVqueaPNQRD4t7mg9DKPECpGo1QcFXjR) on mainnet. Full lifecycle verified across SOL→USDC, SOL→JUP, and SOL→WBTC (Wormhole) pairs. All 4 commands work. All 1180 unit tests pass.
Bug fixed during testing: --state query param should be --status — the API rejects state with "Request validation failed". Fix is in a follow-up commit on the branch.
Two issues flagged below in inline comments.
— Ari
src/limit-order.js
Outdated
| const triggerPrice = options['trigger-price']; | ||
| const triggerCondition = options['trigger-condition']; | ||
| const triggerMintRaw = options['trigger-mint']; | ||
| const slippageBps = options['slippage-bps'] != null ? Number(options['slippage-bps']) : undefined; |
There was a problem hiding this comment.
create doesn't validate slippageBps range client-side, but update does (line 831). If a user passes --slippage-bps 20000, it passes through to the API which rejects it with: Too big: expected number to be <=10000. The error message is decent, but for consistency with update we should add the same client-side check here:
if (slippageBps != null && (slippageBps < 0 || slippageBps > 10000)) {
log('Error: --slippage-bps must be between 0 and 10000 basis points.');
exit(1);
return;
}
src/limit-order.js
Outdated
| ...(expiresAt != null ? { expiresAt } : {}), | ||
| }; | ||
|
|
||
| const result = await createOrder(token, orderParams); |
There was a problem hiding this comment.
This is the most impactful UX issue I found during testing. When createOrder returns a 500 Gateway Timeout (which happened intermittently from Jupiter), the CLI reports an error — but the order was actually created on the backend. The user thinks it failed, retries, and creates duplicates. During testing I accumulated 7 ghost orders this way.
Suggestion: catch timeout/5xx errors specifically and follow up with a listOrders check:
} catch (err) {
if (err.status >= 500) {
log(`Warning: Order submission returned an error, but may have succeeded.`);
log(` Checking order status...`);
try {
const check = await listOrders(token, pubkey, { limit: 1 });
const recent = check.orders?.[0];
if (recent && recent.inputMint === from && recent.outputMint === to) {
log(` Found recent order ${recent.id} (status: ${recent.status}). It may be yours.`);
log(` Run: nansen trade limit-order list`);
return;
}
} catch { /* list check failed, fall through to error */ }
}
log(`Error: ${err.message}`);
// ...
}This saved me a lot of confusion during testing — orders were silently created despite the error output.
There was a problem hiding this comment.
Should trading-api protect against this instead of the user? And we should maybe rely on depositRequestId instead of ecent.inputMint === from && recent.outputMint === to? @Pepewitch
1ca2600 to
e91fc90
Compare
Add limit order commands under `nansen trade limit-order` powered by the Jupiter Trigger API V2 backend. Supports create, list, cancel, and update operations with all three wallet types (local, Privy, WalletConnect). Key additions: - src/limit-order.js: Core module with JWT auth/caching, API client, wallet resolution, and command handlers - src/privy.js: Add signSolanaMessage() to PrivyClient - src/walletconnect-trading.js: Add signSolanaMessageViaWalletConnect() - src/cli.js: Wire limit-order sub-namespace into trade command - src/schema.json: Add limit-order command definitions - src/__tests__/limit-order.test.js: 50 comprehensive tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move limit order JWT caching from ~/.nansen/limit-order-auth.json to macOS Keychain / Linux secret-tool, matching the existing pattern in keychain.js for wallet password storage. Falls back gracefully if keychain is unavailable (token just won't be cached). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add keychainStoreValue/keychainRetrieveValue to keychain.js as generic exports, then use them from limit-order.js. This follows the existing pattern of centralizing all OS keychain access in keychain.js. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keychain fallback (re-auth every command) causes excessive API hits.
Simple file at ~/.nansen/limit-order-auth.json with chmod 600 is
sufficient for a 24h JWT. Also fixes vault check to match actual
API response shape ({ vault: null } vs { vault: { ... } }).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove leftover account parameter from keychainStore/keychainRetrieve that was introduced for the keychain-based JWT approach. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reject EVM addresses and other invalid formats early with a clear error message instead of letting them reach the Jupiter API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…create Both flags are now required to prevent ambiguous orders (e.g. triggering on a stablecoin price). Users must explicitly specify which token to monitor and the direction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ult check, better formatting - Require --trigger-mint and --trigger-condition (no defaults) - Fix vault existence check to match actual API response shape - Show human-readable timestamps with ISO string in brackets - Show full token addresses with symbol labels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Amount is now always in token units (e.g. 1.5 = 1.5 SOL), converted to base units internally. Uses parseAmount/getTokenInfo from transfer.js for known tokens and RPC-based decimal lookup for arbitrary tokens. Also adds 16 amount parsing tests covering precision, IEEE 754 edge cases, truncation, and different token decimals. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…st output - Rename flag to --slippage-bps for clarity (100 = 1%) - Always display slippage in order list (shows "auto" when not set) - Document that auto slippage cannot be restored via update (backend limitation) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
List now defaults to --state open. Use --state all to see everything. Also improves error output with cause details for network errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This reverts commit 37966ab.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove X-API-Key header from limit order requests (JWT auth only) - Fix schema.json amount description to reflect human-readable units - Rename subcommand from `limit-order` to `limit` for brevity - Update help text to use `--amount <token-units>` - Add TODO for extracting shared wallet resolution with trading.js Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Validate --slippage-bps range (0-10000) in create, matching update - On 5xx errors during order creation, check recent orders to warn about potential duplicates before the user retries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
e91fc90 to
e44aa31
Compare
- Amount defaults to base units (lamports), add --amount-unit token/usd - Replace --slippage-bps (basis points) with --slippage (decimal pct, matching swap) - Use shared resolveTokenDecimals/convertToBaseUnits from trading.js - Remove duplicate KNOWN_SOLANA_TOKENS/parseAmount/getTokenInfo usage - Update schema, help text, and tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The API returns { vaultAddress: "..." } but the code checked for
vaultPubkey, causing it to always attempt re-registration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…cies - Remove --amount-unit usd (misleading for limit orders: converts at current price, not trigger price) - Fix --state schema docs: "active, past" not "open, filled, cancelled, expired" - Fix vault comment to match actual API field (vaultAddress) - Fix update command slippage display to show percentage like formatOrder Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 5xx error recovery: ghost order detection, no match, list failure, 4xx skip - Vault: detects vaultAddress field (not just vaultPubkey) - Create slippage: bps passthrough, auto omit, negative/overflow rejection - Amount edge cases: zero, Infinity, negative - Order ID resolution: options['order-id'] and args[0] for cancel/update - Update combined: trigger-price + slippage together - List filtering: --mint resolution, --sort/--dir, pagination, invalid mint - signTransaction: local, privy, unsupported type - WalletConnect: branch dispatch verification - Rename slippageRaw → slippage to match trading.js convention Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- cli.js help: --state <open|filled|cancelled|expired> → <active|past> - Tests: state 'open' → 'active', 'filled' → 'past' (matches real API) - Tests: mock vault responses use vaultAddress (matches real API response) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add orderSubmitted flag to skip ghost check on pre-submission errors (e.g. craftDeposit failures like "transfer hook extension") - Store submittedAt timestamp before createOrder call - Match ghost orders by inputMint, outputMint, inputAmount, triggerPriceUsd, AND createdAt >= submittedAt (not a hardcoded 60s window) - Fetch 5 recent orders instead of 1 for more reliable matching - Add test for pre-submission 5xx skipping ghost check Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deduplicates wallet resolution logic between trading.js and limit-order.js by extracting a shared resolveWalletForChain() function in trading.js. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Add
trade limitcommands for Jupiter Limit Order V2 on Solana — create, list, cancel, and update trigger-based orders.Commands
trade limit create— place a limit order (deposit + order in one flow)trade limit list— list orders with--state active|pastfiltering, pagination, mint filtertrade limit cancel— cancel an order (withdrawal + confirmation)trade limit update— update trigger price and/or slippage on an existing orderDesign decisions
--amount-unit tokenfor human-readable (e.g.--amount 1.5 --amount-unit token)--slippage 0.03(decimal, 3%) — converted internally to bps for the APIresolveTokenDecimals(),convertToBaseUnits(),validateBaseUnitAmount(),resolveTokenAddress()— no duplicate infrastructure~/.nansen/limit-order-auth.jsoncreateOrderreturns 5xx, checks recent orders (matching token pair, amount, trigger price, and timestamp) to warn about duplicates before the user retriesFiles
src/limit-order.jssrc/cli.jstrade limit <sub>to limit-order handlerssrc/schema.jsonsrc/privy.jssignSolanaMessage()for Privy walletssrc/walletconnect-trading.jssignSolanaMessageViaWalletConnect()for WC walletssrc/__tests__/limit-order.test.jsTest plan
npm test— 1378 tests pass (96 new for limit-order)npm run lint— clean--state active, verified formattingcreateOrdercall, not on pre-submission errorsactive/past)Generated with Claude Code