nansen-cli 1.43.0 → 1.44.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 +80 -0
- package/README.md +34 -0
- package/package.json +3 -3
- package/src/api.js +184 -23
- package/src/bridge.js +103 -5
- package/src/cli.js +47 -53
- package/src/commands/completion.js +652 -0
- package/src/commands/mcp.js +19 -3
- package/src/commands/research.js +199 -25
- package/src/hl-client.js +20 -8
- package/src/limit-order.js +31 -20
- package/src/mcp-verify.js +66 -1
- package/src/perp.js +72 -24
- package/src/query-options.js +32 -0
- package/src/schema.json +631 -131
- package/src/semver.js +26 -0
- package/src/telemetry.js +118 -19
- package/src/trading.js +196 -22
- package/src/transfer.js +4 -1
- package/src/update-check.js +13 -6
- package/src/wallet.js +10 -6
- package/src/x402.js +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.44.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#563](https://github.com/nansen-ai/nansen-cli/pull/563) [`3b3e11b`](https://github.com/nansen-ai/nansen-cli/commit/3b3e11b5d346662bf352cbe2e3f66e54b22213b0) Thanks [@hulk-linus](https://github.com/apps/hulk-linus)! - Emit privacy-preserving per-leg Hyperliquid attribution through the canonical trade-perps outcome events.
|
|
8
|
+
|
|
9
|
+
- [#580](https://github.com/nansen-ai/nansen-cli/pull/580) [`c5085c5`](https://github.com/nansen-ai/nansen-cli/commit/c5085c5d062d848604402949a3e03c87d8b013bd) Thanks [@gulshngill](https://github.com/gulshngill)! - Add `nansen completion <bash|zsh|fish>`, which prints a shell completion script generated from the CLI's own command schema. Completions cover nested subcommands, per-command flags, global flags, and the enum values a flag accepts.
|
|
10
|
+
|
|
11
|
+
- [#597](https://github.com/nansen-ai/nansen-cli/pull/597) [`8453355`](https://github.com/nansen-ai/nansen-cli/commit/845335501837956db2f7a899bf251048bbd706d9) Thanks [@kome12](https://github.com/kome12)! - Security: `nansen mcp verify` no longer sends a saved API key (from `nansen login` / `NANSEN_API_KEY` / config) to a custom `--url` without explicit consent. Forwarding a saved key to a non-default URL now requires `--send-api-key`, and no key is ever sent over plain HTTP to a non-loopback host. An inline `--api-key` is unaffected.
|
|
12
|
+
|
|
13
|
+
Note: verifying a custom `--url` with a saved key now errors unless `--send-api-key` is passed (previously it warned and proceeded).
|
|
14
|
+
|
|
15
|
+
- [#557](https://github.com/nansen-ai/nansen-cli/pull/557) [`5a73b43`](https://github.com/nansen-ai/nansen-cli/commit/5a73b43e4f2fc753c435babcea648b156f7a99a9) Thanks [@gulshngill](https://github.com/gulshngill)! - Add the `nansen research address-premium-labels` command.
|
|
16
|
+
|
|
17
|
+
- [#555](https://github.com/nansen-ai/nansen-cli/pull/555) [`4285d7f`](https://github.com/nansen-ai/nansen-cli/commit/4285d7fc22fde5bacb5b67eb3bcb6e94bb6a680e) Thanks [@gulshngill](https://github.com/gulshngill)! - Add the `nansen research chain-rank` command.
|
|
18
|
+
|
|
19
|
+
- [#561](https://github.com/nansen-ai/nansen-cli/pull/561) [`dd8035c`](https://github.com/nansen-ai/nansen-cli/commit/dd8035c09f5c99c08247686c621c5c14c30607cc) Thanks [@gulshngill](https://github.com/gulshngill)! - Add the `nansen research historical-token-ohlcv` command.
|
|
20
|
+
|
|
21
|
+
- [#560](https://github.com/nansen-ai/nansen-cli/pull/560) [`52f2f09`](https://github.com/nansen-ai/nansen-cli/commit/52f2f095baeaa48c43d5b0f3b1a3b036166266b5) Thanks [@gulshngill](https://github.com/gulshngill)! - Add the `nansen research perp-pnl-summary` command.
|
|
22
|
+
|
|
23
|
+
- [#559](https://github.com/nansen-ai/nansen-cli/pull/559) [`ed2d7a5`](https://github.com/nansen-ai/nansen-cli/commit/ed2d7a5ac00b51240f5a2d0fb7828131dc5074c8) Thanks [@gulshngill](https://github.com/gulshngill)! - Add the `nansen research position-intelligence` command.
|
|
24
|
+
|
|
25
|
+
- [#558](https://github.com/nansen-ai/nansen-cli/pull/558) [`e273152`](https://github.com/nansen-ai/nansen-cli/commit/e273152358792203435191a0f30dd307699721aa) Thanks [@gulshngill](https://github.com/gulshngill)! - Add the `nansen research smart-money-pnl-leaderboard` command.
|
|
26
|
+
|
|
27
|
+
- [#556](https://github.com/nansen-ai/nansen-cli/pull/556) [`3c14359`](https://github.com/nansen-ai/nansen-cli/commit/3c143596b6d7e624338f8d9b1a0972bcbe4d7ad7) Thanks [@gulshngill](https://github.com/gulshngill)! - Add the `nansen research token-sectors` command.
|
|
28
|
+
|
|
29
|
+
- [#562](https://github.com/nansen-ai/nansen-cli/pull/562) [`0d0f5e6`](https://github.com/nansen-ai/nansen-cli/commit/0d0f5e6b83ae91f0215eac3ad2e9216897e6666c) Thanks [@gulshngill](https://github.com/gulshngill)! - Add the `nansen research transaction-with-token-transfer-lookup` command.
|
|
30
|
+
|
|
31
|
+
- [#581](https://github.com/nansen-ai/nansen-cli/pull/581) [`8726a23`](https://github.com/nansen-ai/nansen-cli/commit/8726a23c0e6028c98f941ee9a638dffa50e01ab1) Thanks [@gulshngill](https://github.com/gulshngill)! - Every command option in `nansen schema` now carries a type and a description (140 research and `wallet send` options had neither), and the `research points` group is described. `research token ohlcv --timeframe` documents its real default (`1d`), and `wallet send --chain`, `research search --type`, and the prediction-market `--neg-risk` filters declare the values they accept, so `--help` and shell completion offer them. Fixed `--neg-risk true` on the prediction-market screeners, which was sent to the API as `neg_risk: false` because the parsed boolean was compared against the string `'true'`. `nansen mcp install` and `nansen mcp uninstall` declare their positional client in the schema, and `nansen completion` scripts now complete it (`claude-code`, `claude-desktop`, `cursor`) in bash, zsh, and fish.
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- [#575](https://github.com/nansen-ai/nansen-cli/pull/575) [`927ffea`](https://github.com/nansen-ai/nansen-cli/commit/927ffea1762e7de78160a209aa3cc72298b42c24) Thanks [@kome12](https://github.com/kome12)! - Fail closed on an ambiguous broadcast failure in swap and bridge execute. When
|
|
36
|
+
`/execute` (swap) returns any 5xx (regardless of body shape — a structured 504
|
|
37
|
+
`UPSTREAM_TIMEOUT` is treated the same as a bare 502) or any other
|
|
38
|
+
uninterpretable response — or the POST, or reading its body, throws a network
|
|
39
|
+
error — after the signed tx was sent, or a bridge broadcast fails ambiguously,
|
|
40
|
+
the tx may already be live. The quote is now marked spent and the
|
|
41
|
+
run aborts — rather than trying the next candidate quote or leaving the quote
|
|
42
|
+
reusable — so a re-execute (or agent auto-retry) can't double-broadcast. Gasless
|
|
43
|
+
(Relay solver-paid) swaps additionally do not retry the `/execute` POST: the
|
|
44
|
+
signed authorization is broadcast by Relay's own wrapping tx, so a re-POST can't
|
|
45
|
+
be node-deduped and could trigger a second solve — a single attempt fails closed
|
|
46
|
+
instead. A bridge send is kept reusable only when the node's error proves the tx never
|
|
47
|
+
entered the mempool (a pre-broadcast validation rejection such as insufficient
|
|
48
|
+
funds); in-flight txpool states like "already known" or "nonce too low" fail
|
|
49
|
+
closed.
|
|
50
|
+
|
|
51
|
+
- [#572](https://github.com/nansen-ai/nansen-cli/pull/572) [`2ba5c15`](https://github.com/nansen-ai/nansen-cli/commit/2ba5c15a68f6cc2045dcfb928d65eecdb4083292) Thanks [@teyrebaz33](https://github.com/teyrebaz33)! - Fix `nansen changelog --since <version>` silently returning "No changelog entries found" for a version missing its patch number (e.g. `--since 1.43` instead of `--since 1.43.0`), even when matching entries exist. The comparison compared the missing component against `undefined`, and `>` is always `false` against `undefined` in both directions, so a version that matched on major.minor always came out "less than" the since-value. A missing component is now treated as `0`, and a `--since` value that isn't a valid version (e.g. `--since abc`) now prints a clear error instead of silently matching nothing.
|
|
52
|
+
|
|
53
|
+
The version-comparison logic is now shared (`src/semver.js`) between `nansen changelog --since` and the update-notifier's `isNewer` check, which had the identical bug in its own separate parser. `isNewer` couldn't misfire in practice (both versions it compares are always fully-qualified x.y.z today), but it's the same defect class, so it's fixed the same way rather than left in place.
|
|
54
|
+
|
|
55
|
+
- [#569](https://github.com/nansen-ai/nansen-cli/pull/569) [`04932c7`](https://github.com/nansen-ai/nansen-cli/commit/04932c7fb26fb2c4c4f9b14eddcd43a8e3060aa4) Thanks [@memosr](https://github.com/memosr)! - Refuse to re-execute a swap quote that has already been broadcast, mirroring the single-use guard `nansen bridge execute` already had. `nansen trade execute` now marks the quote as spent (`executedAt`) the instant a transaction is broadcast — before waiting for its receipt — so a `RECEIPT_TIMEOUT` (the tx is on-chain but the command exits non-zero) no longer leaves the quote replayable. Retrying the same `--quote <id>` after such a failure previously re-signed and re-broadcast the swap under a fresh nonce instead of being refused.
|
|
56
|
+
|
|
57
|
+
- [#592](https://github.com/nansen-ai/nansen-cli/pull/592) [`60bd2ef`](https://github.com/nansen-ai/nansen-cli/commit/60bd2ef64f5eebe89112bfa3366c013981f365aa) Thanks [@ygd58](https://github.com/ygd58)! - Fix the x402 auto-payment fallback in `src/api.js` generating multiple payment authorizations for the same request after an ambiguous outcome (issue #583). After a signed `Payment-Signature` was transmitted, `_x402Retry` previously collapsed every non-ok response — a clean rejection, a 5xx, an unreadable body — and every transport failure into a single `null`, and callers treated any `null` as "safe to try the next payment option/provider". That meant a 5xx, a timeout, or an unparseable response (any of which could mean the server already received and settled the payment) triggered signing and transmitting a _second_ independent payment for the same logical request. Separately, a genuine successful response whose JSON body happened to be `null` was indistinguishable from a rejection, risking a second payment for an already-settled call.
|
|
58
|
+
|
|
59
|
+
`_x402Retry` now returns a dedicated `X402_PAYMENT_REJECTED` sentinel only for a provably clean rejection (a non-5xx status with a readable body), and throws `NansenError` with the new `PAYMENT_AMBIGUOUS` code for anything else — a transport failure, a 5xx, or an unreadable body on either a rejection or a success. All three fallback call sites (Privy, local wallet, WalletConnect) now check against the sentinel instead of `null`, and re-throw a `PAYMENT_AMBIGUOUS` error immediately instead of silently moving on to the next provider.
|
|
60
|
+
|
|
61
|
+
- [#524](https://github.com/nansen-ai/nansen-cli/pull/524) [`3e8dcc2`](https://github.com/nansen-ai/nansen-cli/commit/3e8dcc233598383556004da7aaa0a64275beee3f) Thanks [@dolmaciabdullah-byte](https://github.com/dolmaciabdullah-byte)! - Use BigInt for EVM balance in `checkX402Balance` to avoid precision loss on 18-decimal tokens (BSC stablecoins): `parseInt(hex, 16)` loses integer precision once the raw wei value exceeds `Number.MAX_SAFE_INTEGER` (~9.0e15 wei, i.e. ~0.009 tokens at 18 decimals), skewing the low-balance warning.
|
|
62
|
+
|
|
63
|
+
- [#568](https://github.com/nansen-ai/nansen-cli/pull/568) [`9a45fc4`](https://github.com/nansen-ai/nansen-cli/commit/9a45fc494b3bcc0ebeb37f867902aecc075ee92f) Thanks [@Kewe63](https://github.com/Kewe63)! - Reject non-finite limit-order trigger prices and expiry values before wallet or API activity.
|
|
64
|
+
|
|
65
|
+
- [#567](https://github.com/nansen-ai/nansen-cli/pull/567) [`c0fe57b`](https://github.com/nansen-ai/nansen-cli/commit/c0fe57b6f881aa7338a16f6aa2fd33d5e6d42757) Thanks [@Kewe63](https://github.com/Kewe63)! - Keep `research perp` analytics-only instead of routing trading subcommands through the top-level perp dispatcher.
|
|
66
|
+
|
|
67
|
+
- [#558](https://github.com/nansen-ai/nansen-cli/pull/558) [`002921e`](https://github.com/nansen-ai/nansen-cli/commit/002921e5d72e7e54de26f241cb4b409e408b8bf9) Thanks [@gulshngill](https://github.com/gulshngill)! - Reject non-integer or non-positive `--limit` values with an actionable error instead of forwarding them to the API.
|
|
68
|
+
|
|
69
|
+
## 1.43.1
|
|
70
|
+
|
|
71
|
+
### Patch Changes
|
|
72
|
+
|
|
73
|
+
- [#526](https://github.com/nansen-ai/nansen-cli/pull/526) [`a7ab05c`](https://github.com/nansen-ai/nansen-cli/commit/a7ab05c592bddd1e0ca33c08586ef325bcc19277) Thanks [@dolmaciabdullah-byte](https://github.com/dolmaciabdullah-byte)! - Fix `formatValue` displaying `1000.00K` instead of `1.00M` when a value like 999999.995 rounds up at the K/M boundary.
|
|
74
|
+
|
|
75
|
+
- [#548](https://github.com/nansen-ai/nansen-cli/pull/548) [`000d01a`](https://github.com/nansen-ai/nansen-cli/commit/000d01a8530de7e5bb6af8a637ba1b4fe406b732) Thanks [@Sertug17](https://github.com/Sertug17)! - Fix `parseAmount` silently producing wrong values for negative decimal inputs. Negative amounts are now rejected with a clear error.
|
|
76
|
+
|
|
77
|
+
- [#551](https://github.com/nansen-ai/nansen-cli/pull/551) [`092535a`](https://github.com/nansen-ai/nansen-cli/commit/092535aa32bedb8c11a6ee624936bffca247d2d5) Thanks [@kome12](https://github.com/kome12)! - Harden `parseAmount` input validation: reject negative amounts wrapped in whitespace (previously silently miscalculated), and reject non-numeric or malformed inputs (e.g. `abc`, empty string, `1.`, `.5`) with a clear error instead of throwing a raw error or silently returning `0`.
|
|
78
|
+
|
|
79
|
+
- [#433](https://github.com/nansen-ai/nansen-cli/pull/433) [`977fbc5`](https://github.com/nansen-ai/nansen-cli/commit/977fbc51a75ef4a0764db161b4bbee26d1ebc3ae) Thanks [@aikido-autofix](https://github.com/apps/aikido-autofix)! - Validate the wallet name before the Privy pre-read in `wallet delete` and `wallet send`, routing both reads through `getWalletFile()` so the path stays confined to the wallets directory.
|
|
80
|
+
|
|
81
|
+
- [#545](https://github.com/nansen-ai/nansen-cli/pull/545) [`642eb20`](https://github.com/nansen-ai/nansen-cli/commit/642eb20a48fe1ff5a9ba17fcbeac4604c7172698) Thanks [@kome12](https://github.com/kome12)! - Update Noble crypto dependencies to the patched 2.4.x releases.
|
|
82
|
+
|
|
3
83
|
## 1.43.0
|
|
4
84
|
|
|
5
85
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -51,11 +51,23 @@ nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
|
51
51
|
nansen trade execute --quote <quoteId>
|
|
52
52
|
nansen wallet <subcommand> [options]
|
|
53
53
|
nansen mcp install <client> # add the Nansen MCP server to Claude Code/Desktop or Cursor
|
|
54
|
+
nansen completion <bash|zsh|fish> # shell completions (no API key needed)
|
|
54
55
|
nansen schema [command] [--pretty] # full command reference (no API key needed)
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
**Research categories:** `smart-money` (`sm`), `token` (`tgm`), `profiler` (`prof`), `portfolio` (`port`), `prediction-market` (`pm`), `search`, `perp`, `points`
|
|
58
59
|
|
|
60
|
+
**Direct research subcommands:**
|
|
61
|
+
- `nansen research chain-rank [--timeframe-days 7|30|365] [--chain-type all|evm]` — rank chains by growth metrics
|
|
62
|
+
- `nansen research token-sectors` — list token sectors available for filtering
|
|
63
|
+
- `nansen research address-premium-labels --address <addr> [--chain <chain>] [--page <n>] [--limit <n>]` — get all labels for an address, including premium labels
|
|
64
|
+
- `nansen research smart-money-pnl-leaderboard [--chains c1,c2] [--timeframe-days 1|7|30|90|180] [--filters '<json>'] [--sort <field[:asc|desc]>] [--page <n>] [--limit <n>]` — rank smart money wallets by PnL
|
|
65
|
+
- `nansen research position-intelligence --symbol <symbol>` — aggregate Hyperliquid positions by trader cohort
|
|
66
|
+
- `nansen research perp-pnl-summary --address <addr> --from-date <date> --to-date <date>` — summarize realized Hyperliquid PnL for an address
|
|
67
|
+
- `nansen research transaction-with-token-transfer-lookup --transaction-hash <hash> [--chain <chain>] [--block-timestamp "YYYY-MM-DD HH:MM:SS"]` — look up a transaction and its token/NFT transfers
|
|
68
|
+
|
|
69
|
+
Plus the `historical-*` point-in-time commands — run `nansen research help` for the full list.
|
|
70
|
+
|
|
59
71
|
**Trade:** `quote`, `execute`, `bridge-status`, `limit-order` — DEX swaps on Solana and Base, cross-chain bridges, and Solana limit orders.
|
|
60
72
|
|
|
61
73
|
**Wallet:** `create`, `list`, `show`, `export`, `default`, `delete`, `send` — local or Privy server-side wallets (EVM + Solana).
|
|
@@ -253,6 +265,28 @@ tempo request POST https://api.nansen.ai/api/v1/smart-money/netflow \
|
|
|
253
265
|
|
|
254
266
|
> Note: MPP is server-side opt-in (`MPP_ENABLED=true` on the API). It's available on dev today and rolling out to prod — if `tempo request` returns a non-MPP 402, fall back to x402 or an API key.
|
|
255
267
|
|
|
268
|
+
## Shell Completions
|
|
269
|
+
|
|
270
|
+
Tab-completion for commands, subcommands, flags, and the values a flag accepts:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# bash
|
|
274
|
+
echo 'eval "$(nansen completion bash)"' >> ~/.bashrc
|
|
275
|
+
# or system-wide: nansen completion bash > /etc/bash_completion.d/nansen
|
|
276
|
+
|
|
277
|
+
# zsh
|
|
278
|
+
nansen completion zsh > "${fpath[1]}/_nansen" && compinit
|
|
279
|
+
# or, in ~/.zshrc after the compinit line: eval "$(nansen completion zsh)"
|
|
280
|
+
|
|
281
|
+
# fish
|
|
282
|
+
nansen completion fish > ~/.config/fish/completions/nansen.fish
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The scripts are generated from the same schema as `nansen schema`, so they cover
|
|
286
|
+
every nested subcommand and stay in step with the CLI. Nothing is written to
|
|
287
|
+
disk and no network call is made — the script goes to stdout. Re-run the command
|
|
288
|
+
after upgrading the CLI to pick up new commands.
|
|
289
|
+
|
|
256
290
|
## Key Options
|
|
257
291
|
|
|
258
292
|
| Option | Description |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nansen-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.44.0",
|
|
4
4
|
"description": "AI-agent CLI for Nansen API analytics, DEX swaps, and cross-chain trading",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"@ethereumjs/rlp": "^10.1.1",
|
|
76
|
-
"@noble/curves": "^2.0
|
|
77
|
-
"@noble/hashes": "^2.0
|
|
76
|
+
"@noble/curves": "^2.4.0",
|
|
77
|
+
"@noble/hashes": "^2.4.0",
|
|
78
78
|
"@scure/base": "^2.0.0"
|
|
79
79
|
}
|
|
80
80
|
}
|
package/src/api.js
CHANGED
|
@@ -19,6 +19,16 @@ import { readResponseMeta } from './response-meta.js';
|
|
|
19
19
|
*/
|
|
20
20
|
export const RESPONSE_META = Symbol('nansenResponseMeta');
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Sentinel returned by _x402Retry to mean "this payment option was cleanly
|
|
24
|
+
* rejected without settlement, safe to try the next option" — distinct from
|
|
25
|
+
* a genuine successful response whose JSON body happens to be `null`.
|
|
26
|
+
* Using `null` for both (the previous behavior) made a legitimate null-body
|
|
27
|
+
* success indistinguishable from a clean rejection, so the caller would sign
|
|
28
|
+
* and transmit ANOTHER payment for a request that had already succeeded.
|
|
29
|
+
*/
|
|
30
|
+
export const X402_PAYMENT_REJECTED = Symbol('x402PaymentRejected');
|
|
31
|
+
|
|
22
32
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
33
|
|
|
24
34
|
export function telemetryHeaders() {
|
|
@@ -65,6 +75,7 @@ export const ErrorCode = {
|
|
|
65
75
|
// Client Errors
|
|
66
76
|
NETWORK_ERROR: 'NETWORK_ERROR', // Connection failed
|
|
67
77
|
TIMEOUT: 'TIMEOUT', // Request timed out
|
|
78
|
+
PAYMENT_AMBIGUOUS: 'PAYMENT_AMBIGUOUS', // x402 payment outcome unknown after transmission — do not retry with another payment
|
|
68
79
|
|
|
69
80
|
// Generic
|
|
70
81
|
UNKNOWN: 'UNKNOWN', // Unclassified error
|
|
@@ -530,7 +541,14 @@ export class NansenAPI {
|
|
|
530
541
|
|
|
531
542
|
/**
|
|
532
543
|
* Retry a POST request with a payment signature.
|
|
533
|
-
* Returns parsed JSON if the paid request succeeds, or
|
|
544
|
+
* Returns parsed JSON if the paid request succeeds, or the X402_PAYMENT_REJECTED
|
|
545
|
+
* sentinel if the server cleanly, legibly rejected it without settling.
|
|
546
|
+
* Throws a NansenError(PAYMENT_AMBIGUOUS) — instead of returning the sentinel —
|
|
547
|
+
* for any outcome that doesn't prove the payment was rejected: a transport
|
|
548
|
+
* failure after transmission, an HTTP 5xx, or a response body that can't be
|
|
549
|
+
* parsed. In all of those cases the server may already have received and
|
|
550
|
+
* settled the payment, so the caller must not treat it as safe to retry with
|
|
551
|
+
* a different option — that would risk paying twice for the same request.
|
|
534
552
|
* Logs the payment and warns about low balance when walletLabel and network are given.
|
|
535
553
|
*
|
|
536
554
|
* @param {string} signature - Payment-Signature header value
|
|
@@ -539,7 +557,7 @@ export class NansenAPI {
|
|
|
539
557
|
* @param {string} url - Request URL
|
|
540
558
|
* @param {object} body - Request body (will be cleaned)
|
|
541
559
|
* @param {object} [options={}] - Request options (may include .method, .headers)
|
|
542
|
-
* @returns {Promise<object|
|
|
560
|
+
* @returns {Promise<object|typeof X402_PAYMENT_REJECTED>}
|
|
543
561
|
*
|
|
544
562
|
* TODO: full fix — extract the entire x402 provider dispatch from request() into
|
|
545
563
|
* an attemptX402Payment() method so adding a new payment provider only requires
|
|
@@ -550,21 +568,52 @@ export class NansenAPI {
|
|
|
550
568
|
// POST burned a payment signature then hit the wrong route for GET/DELETE/PATCH.
|
|
551
569
|
const method = options.method || 'POST';
|
|
552
570
|
const isGet = method === 'GET';
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
571
|
+
let paidResponse;
|
|
572
|
+
try {
|
|
573
|
+
paidResponse = await fetch(url, {
|
|
574
|
+
method,
|
|
575
|
+
redirect: 'error',
|
|
576
|
+
headers: {
|
|
577
|
+
...(!isGet && { 'Content-Type': 'application/json' }),
|
|
578
|
+
'X-Client-Type': 'nansen-cli',
|
|
579
|
+
'X-Client-Version': packageVersion,
|
|
580
|
+
...telemetryHeaders(),
|
|
581
|
+
'Payment-Signature': signature,
|
|
582
|
+
...this.defaultHeaders,
|
|
583
|
+
...options.headers,
|
|
584
|
+
},
|
|
585
|
+
...(!isGet && method !== 'DELETE' && { body: JSON.stringify(NansenAPI.cleanBody(body)) }),
|
|
586
|
+
});
|
|
587
|
+
} catch (err) {
|
|
588
|
+
// The signature was already on the wire when the connection failed —
|
|
589
|
+
// the server may have received and settled it before we lost the
|
|
590
|
+
// response. Fail closed rather than let the caller sign and send a
|
|
591
|
+
// second payment for the same logical request.
|
|
592
|
+
throw new NansenError(
|
|
593
|
+
`x402 payment outcome unknown: request failed after the signed payment was transmitted (${err.message}). Not attempting another payment for the same request.`,
|
|
594
|
+
ErrorCode.PAYMENT_AMBIGUOUS,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
if (!paidResponse.ok) {
|
|
598
|
+
// A 5xx doesn't prove the payment was rejected — the server could have
|
|
599
|
+
// processed it before failing to respond. Only a readable non-5xx
|
|
600
|
+
// rejection body is safe to treat as "try the next option".
|
|
601
|
+
if (paidResponse.status >= 500) {
|
|
602
|
+
throw new NansenError(
|
|
603
|
+
`x402 payment outcome unknown: server returned ${paidResponse.status} after the signed payment was transmitted. Not attempting another payment for the same request.`,
|
|
604
|
+
ErrorCode.PAYMENT_AMBIGUOUS,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
await paidResponse.json();
|
|
609
|
+
} catch (err) {
|
|
610
|
+
throw new NansenError(
|
|
611
|
+
`x402 payment outcome unknown: rejection response body was unreadable (${err.message}). Not attempting another payment for the same request.`,
|
|
612
|
+
ErrorCode.PAYMENT_AMBIGUOUS,
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
return X402_PAYMENT_REJECTED;
|
|
616
|
+
}
|
|
568
617
|
if (walletLabel) {
|
|
569
618
|
console.error(`[x402] Paid via ${walletLabel}${network ? ` (${network})` : ''}`);
|
|
570
619
|
}
|
|
@@ -577,7 +626,18 @@ export class NansenAPI {
|
|
|
577
626
|
}
|
|
578
627
|
} catch { /* balance check is best-effort */ }
|
|
579
628
|
}
|
|
580
|
-
|
|
629
|
+
let data;
|
|
630
|
+
try {
|
|
631
|
+
data = await paidResponse.json();
|
|
632
|
+
} catch (err) {
|
|
633
|
+
// The payment was accepted (2xx) — it settled. We just can't read the
|
|
634
|
+
// response body, so surface that plainly rather than silently treating
|
|
635
|
+
// it as a rejection and paying again.
|
|
636
|
+
throw new NansenError(
|
|
637
|
+
`x402 payment succeeded but its response body was unreadable (${err.message}). The payment was not repeated.`,
|
|
638
|
+
ErrorCode.PAYMENT_AMBIGUOUS,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
581
641
|
const meta = readResponseMeta(paidResponse);
|
|
582
642
|
this.lastResponseMeta = meta;
|
|
583
643
|
if (meta && data !== null && typeof data === 'object') data[RESPONSE_META] = meta;
|
|
@@ -712,9 +772,14 @@ export class NansenAPI {
|
|
|
712
772
|
const { createPrivyPaymentSignatures } = await import('./privy.js');
|
|
713
773
|
for await (const { signature, network } of createPrivyPaymentSignatures(response, url)) {
|
|
714
774
|
const result = await this._x402Retry(signature, `Privy wallet ${defaultWalletName}`, network, url, body, options);
|
|
715
|
-
if (result !==
|
|
775
|
+
if (result !== X402_PAYMENT_REJECTED) return result;
|
|
716
776
|
}
|
|
717
777
|
} catch (privyErr) {
|
|
778
|
+
// An ambiguous outcome (transport failure, 5xx, unreadable body)
|
|
779
|
+
// after a signed payment was already transmitted must not be
|
|
780
|
+
// treated as an ordinary payment failure — there is no other
|
|
781
|
+
// provider to fall back to here, and retrying could double-pay.
|
|
782
|
+
if (privyErr instanceof NansenError && privyErr.code === ErrorCode.PAYMENT_AMBIGUOUS) throw privyErr;
|
|
718
783
|
message = `x402 Privy payment failed: ${privyErr.message}`;
|
|
719
784
|
}
|
|
720
785
|
} else {
|
|
@@ -724,10 +789,17 @@ export class NansenAPI {
|
|
|
724
789
|
const { createPaymentSignatures } = await import('./x402.js');
|
|
725
790
|
for await (const { signature, network, asset } of createPaymentSignatures(response, url)) {
|
|
726
791
|
const result = await this._x402Retry(signature, `local wallet ${defaultWalletName}`, network, url, body, options, asset);
|
|
727
|
-
if (result !==
|
|
728
|
-
// This payment option was rejected, try next
|
|
792
|
+
if (result !== X402_PAYMENT_REJECTED) return result;
|
|
793
|
+
// This payment option was cleanly rejected without settling, try next
|
|
729
794
|
}
|
|
730
|
-
} catch {
|
|
795
|
+
} catch (localErr) {
|
|
796
|
+
// An ambiguous outcome here means a signed payment may already
|
|
797
|
+
// be in flight or settled server-side. Do NOT fall through to
|
|
798
|
+
// WalletConnect below — that would sign and transmit a second,
|
|
799
|
+
// independent payment authorization for the same request.
|
|
800
|
+
if (localErr instanceof NansenError && localErr.code === ErrorCode.PAYMENT_AMBIGUOUS) throw localErr;
|
|
801
|
+
/* local wallet unavailable for any other reason, try WalletConnect */
|
|
802
|
+
}
|
|
731
803
|
|
|
732
804
|
// 2. Fall back to WalletConnect (walletconnect-x402.js)
|
|
733
805
|
{
|
|
@@ -749,8 +821,13 @@ export class NansenAPI {
|
|
|
749
821
|
const { handleX402Payment } = await import('./walletconnect-x402.js');
|
|
750
822
|
const paymentSignature = await handleX402Payment(paymentRequirements);
|
|
751
823
|
const result = await this._x402Retry(paymentSignature, 'WalletConnect', null, url, body, options);
|
|
752
|
-
if (result !==
|
|
824
|
+
if (result !== X402_PAYMENT_REJECTED) return result;
|
|
753
825
|
} catch (x402Err) {
|
|
826
|
+
// WalletConnect is the last resort in this chain — an
|
|
827
|
+
// ambiguous outcome here still must not be reported as an
|
|
828
|
+
// ordinary "payment failed" that invites the caller to
|
|
829
|
+
// retry the whole request (and sign yet another payment).
|
|
830
|
+
if (x402Err instanceof NansenError && x402Err.code === ErrorCode.PAYMENT_AMBIGUOUS) throw x402Err;
|
|
754
831
|
if (!this.apiKey) {
|
|
755
832
|
message = 'No API key configured. Three ways to authenticate:\n' +
|
|
756
833
|
' 1. API key: run `nansen login --human` or set NANSEN_API_KEY (get key at https://app.nansen.ai/auth/agent-setup)\n' +
|
|
@@ -834,6 +911,16 @@ export class NansenAPI {
|
|
|
834
911
|
return this.request('/api/v1/account', {}, { method: 'GET', cache: false });
|
|
835
912
|
}
|
|
836
913
|
|
|
914
|
+
// ============= Chain Endpoints =============
|
|
915
|
+
|
|
916
|
+
async chainRank(params = {}) {
|
|
917
|
+
const { timeFrame = 7, chainType = 'all' } = params;
|
|
918
|
+
return this.request('/api/v1/chains/chain-rank', {
|
|
919
|
+
time_frame: timeFrame,
|
|
920
|
+
chain_type: chainType
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
837
924
|
// ============= Smart Money Endpoints =============
|
|
838
925
|
|
|
839
926
|
async smartMoneyNetflow(params = {}) {
|
|
@@ -896,6 +983,17 @@ export class NansenAPI {
|
|
|
896
983
|
});
|
|
897
984
|
}
|
|
898
985
|
|
|
986
|
+
async smartMoneyPnlLeaderboard(params = {}) {
|
|
987
|
+
const { chains = ['solana'], timeframe = 7, filters = {}, orderBy, pagination } = params;
|
|
988
|
+
return this.request('/api/v1/smart-money/pnl-leaderboard', {
|
|
989
|
+
chains,
|
|
990
|
+
timeframe,
|
|
991
|
+
filters,
|
|
992
|
+
order_by: orderBy,
|
|
993
|
+
pagination
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
899
997
|
// ============= Profiler Endpoints =============
|
|
900
998
|
|
|
901
999
|
async addressBalance(params = {}) {
|
|
@@ -921,6 +1019,16 @@ export class NansenAPI {
|
|
|
921
1019
|
});
|
|
922
1020
|
}
|
|
923
1021
|
|
|
1022
|
+
async addressPremiumLabels(params = {}) {
|
|
1023
|
+
const { address, chain = 'all', pagination = { page: 1, per_page: 100 } } = params;
|
|
1024
|
+
if (address) requireValidAddress(address, chain);
|
|
1025
|
+
return this.request('/api/v1/profiler/address/premium-labels', {
|
|
1026
|
+
address,
|
|
1027
|
+
chain,
|
|
1028
|
+
pagination
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
924
1032
|
async addressTransactions(params = {}) {
|
|
925
1033
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30, date } = params;
|
|
926
1034
|
if (address) requireValidAddress(address, chain);
|
|
@@ -970,6 +1078,10 @@ export class NansenAPI {
|
|
|
970
1078
|
return this.request('/api/v1/search/general', body);
|
|
971
1079
|
}
|
|
972
1080
|
|
|
1081
|
+
async tokenSectors() {
|
|
1082
|
+
return this.request('/api/v1/search/token-sectors', {}, { method: 'GET' });
|
|
1083
|
+
}
|
|
1084
|
+
|
|
973
1085
|
async webSearch(params = {}) {
|
|
974
1086
|
const { queries, numResults = 10 } = params;
|
|
975
1087
|
if (!queries || queries.length === 0) {
|
|
@@ -1092,6 +1204,25 @@ export class NansenAPI {
|
|
|
1092
1204
|
});
|
|
1093
1205
|
}
|
|
1094
1206
|
|
|
1207
|
+
async addressPerpPnlSummary(params = {}) {
|
|
1208
|
+
const { address, fromDate, toDate } = params;
|
|
1209
|
+
// HL addresses are EVM-format, so ethereum validation accepts every valid HL address
|
|
1210
|
+
if (address) requireValidAddress(address, 'ethereum');
|
|
1211
|
+
return this.request('/api/v1/profiler/perp-pnl-summary', {
|
|
1212
|
+
address,
|
|
1213
|
+
date: { from: fromDate, to: toDate }
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
async transactionWithTokenTransferLookup(params = {}) {
|
|
1218
|
+
const { chain = 'ethereum', transactionHash, blockTimestamp } = params;
|
|
1219
|
+
return this.request('/api/v1/transaction-with-token-transfer-lookup', {
|
|
1220
|
+
chain,
|
|
1221
|
+
transaction_hash: transactionHash,
|
|
1222
|
+
block_timestamp: blockTimestamp
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1095
1226
|
// ============= Token God Mode Endpoints =============
|
|
1096
1227
|
|
|
1097
1228
|
async tokenScreener(params = {}) {
|
|
@@ -1240,6 +1371,13 @@ export class NansenAPI {
|
|
|
1240
1371
|
});
|
|
1241
1372
|
}
|
|
1242
1373
|
|
|
1374
|
+
async tokenPositionIntelligence(params = {}) {
|
|
1375
|
+
const { tokenAddress } = params;
|
|
1376
|
+
return this.request('/api/v1/tgm/position-intelligence', {
|
|
1377
|
+
token_address: tokenAddress
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1243
1381
|
async tokenPerpPnlLeaderboard(params = {}) {
|
|
1244
1382
|
const { tokenSymbol, filters = {}, orderBy, pagination, days = 30, withLabels } = params;
|
|
1245
1383
|
const body = {
|
|
@@ -1632,6 +1770,29 @@ export class NansenAPI {
|
|
|
1632
1770
|
});
|
|
1633
1771
|
}
|
|
1634
1772
|
|
|
1773
|
+
async researchHistoricalTokenOhlcv(params = {}) {
|
|
1774
|
+
const { chain = 'solana', tokenAddress, fromDate, asOfDate, asOfTs, timeframe, applyBlacklistFilter } = params;
|
|
1775
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1776
|
+
if (!fromDate) {
|
|
1777
|
+
throw new NansenError('fromDate is required', ErrorCode.MISSING_PARAM);
|
|
1778
|
+
}
|
|
1779
|
+
if (asOfDate && asOfTs) {
|
|
1780
|
+
throw new NansenError('asOfDate and asOfTs are mutually exclusive', ErrorCode.INVALID_PARAMS);
|
|
1781
|
+
}
|
|
1782
|
+
if (!asOfDate && !asOfTs) {
|
|
1783
|
+
throw new NansenError('One of asOfDate or asOfTs is required', ErrorCode.MISSING_PARAM);
|
|
1784
|
+
}
|
|
1785
|
+
return this.request('/api/v1beta1/tgm/historical-token-ohlcv', {
|
|
1786
|
+
chain,
|
|
1787
|
+
token_address: tokenAddress,
|
|
1788
|
+
date_from: fromDate,
|
|
1789
|
+
as_of_date: asOfDate,
|
|
1790
|
+
as_of_ts: asOfTs,
|
|
1791
|
+
timeframe,
|
|
1792
|
+
apply_blacklist_filter: applyBlacklistFilter
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1635
1796
|
// ============= Smart Alert Endpoints =============
|
|
1636
1797
|
|
|
1637
1798
|
async alertsList(params = {}) {
|
package/src/bridge.js
CHANGED
|
@@ -1077,6 +1077,56 @@ export async function resolveEvmStepFees(chain, txData, overrides = {}) {
|
|
|
1077
1077
|
return { gasPrice: await evmRpcCall(chain, 'eth_gasPrice') };
|
|
1078
1078
|
}
|
|
1079
1079
|
|
|
1080
|
+
// eth_sendRawTransaction JSON-RPC error messages that PROVE the tx never
|
|
1081
|
+
// entered the mempool: the node rejected it during validation, so nothing is in
|
|
1082
|
+
// flight and the quote stays reusable. Matched case-insensitively as substrings
|
|
1083
|
+
// of the node's error text. Every entry here must be UNAMBIGUOUSLY pre-broadcast
|
|
1084
|
+
// across EVM node implementations. Everything NOT listed fails closed — crucially
|
|
1085
|
+
// the txpool states ("already known", "known transaction", "already imported",
|
|
1086
|
+
// "nonce too low", "replacement transaction underpriced") that mean a tx is
|
|
1087
|
+
// ALREADY in flight; treating those as safe rejections would let a re-execute
|
|
1088
|
+
// sign a fresh nonce and broadcast a second bridge tx.
|
|
1089
|
+
//
|
|
1090
|
+
// Deliberately EXCLUDED: the bare "transaction underpriced". On go-ethereum /
|
|
1091
|
+
// op-geth (which is what Base — today's only EVM bridge chain — runs) it means a
|
|
1092
|
+
// fresh too-low-fee tx that was never accepted, i.e. safe. But some other nodes
|
|
1093
|
+
// (certain Besu / Nethermind versions) reuse that same bare message for a failed
|
|
1094
|
+
// REPLACEMENT (a nonce collision with an in-flight tx). Since the allowlist is
|
|
1095
|
+
// not chain-scoped and processEvmStep runs for whatever CHAIN_RPCS resolves, we
|
|
1096
|
+
// fail closed on it rather than risk a future non-geth chain misclassifying an
|
|
1097
|
+
// in-flight tx as safe. Cost is at most a needless re-quote.
|
|
1098
|
+
const PRE_BROADCAST_SEND_ERRORS = [
|
|
1099
|
+
'insufficient funds',
|
|
1100
|
+
'intrinsic gas too low',
|
|
1101
|
+
'gas too low',
|
|
1102
|
+
'exceeds block gas limit',
|
|
1103
|
+
'exceeds the block gas limit',
|
|
1104
|
+
'max fee per gas less than block base fee',
|
|
1105
|
+
'fee cap less than block base fee',
|
|
1106
|
+
'max priority fee per gas higher than max fee per gas',
|
|
1107
|
+
'invalid sender',
|
|
1108
|
+
'invalid signature',
|
|
1109
|
+
'could not decode',
|
|
1110
|
+
'rlp: ', // geth's RLP decode errors ("rlp: input string too long", …); the
|
|
1111
|
+
// trailing space avoids matching "rlp" embedded in an unrelated message
|
|
1112
|
+
'negative value',
|
|
1113
|
+
];
|
|
1114
|
+
|
|
1115
|
+
// True when we are CONFIDENT a failed send never put the tx into the mempool, so
|
|
1116
|
+
// the bridge quote may be retried. This is deliberately NARROW: a definitive
|
|
1117
|
+
// answer requires a JSON-RPC rejection (RPC_JSON_ERROR) whose message is on the
|
|
1118
|
+
// pre-broadcast allowlist, or a missing-RPC config error (the request never
|
|
1119
|
+
// left this process). A transport/HTTP failure, or ANY other JSON-RPC error
|
|
1120
|
+
// (including in-flight txpool states and the cross-node-ambiguous bare
|
|
1121
|
+
// "transaction underpriced"), is treated as unsafe and must fail closed — see
|
|
1122
|
+
// evmRpcCall (trading.js) for the error codes.
|
|
1123
|
+
function isPreBroadcastSendRejection(err) {
|
|
1124
|
+
if (err?.code === 'RPC_UNCONFIGURED') return true;
|
|
1125
|
+
if (err?.code !== 'RPC_JSON_ERROR') return false;
|
|
1126
|
+
const msg = (err.message || '').toLowerCase();
|
|
1127
|
+
return PRE_BROADCAST_SEND_ERRORS.some(pattern => msg.includes(pattern));
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1080
1130
|
async function processEvmStep(step, { chain, privateKeyHex, signerAddress, log, onBroadcast, feeOverrides, nonceSequence, intent }) {
|
|
1081
1131
|
for (const item of step.items || []) {
|
|
1082
1132
|
if (item.status === 'complete') continue;
|
|
@@ -1114,7 +1164,20 @@ async function processEvmStep(step, { chain, privateKeyHex, signerAddress, log,
|
|
|
1114
1164
|
);
|
|
1115
1165
|
|
|
1116
1166
|
log(` Broadcasting ${step.id} on ${chain}...`);
|
|
1117
|
-
|
|
1167
|
+
let txHash;
|
|
1168
|
+
try {
|
|
1169
|
+
txHash = await evmRpcCall(chain, 'eth_sendRawTransaction', [signedTx]);
|
|
1170
|
+
} catch (sendErr) {
|
|
1171
|
+
// Fail closed UNLESS we're confident the tx never entered the mempool (a
|
|
1172
|
+
// pre-broadcast validation rejection, or no RPC configured). Everything
|
|
1173
|
+
// else — a lost ack, a gateway error, OR an in-flight txpool state like
|
|
1174
|
+
// "already known" / "nonce too low" that a node reports as a JSON-RPC
|
|
1175
|
+
// error — may already be broadcasting, so mark the quote spent before
|
|
1176
|
+
// rethrowing to abort, so a later re-execute can't re-sign this step at a
|
|
1177
|
+
// fresh nonce and double-broadcast.
|
|
1178
|
+
if (!isPreBroadcastSendRejection(sendErr)) onBroadcast?.(step.id, null);
|
|
1179
|
+
throw sendErr;
|
|
1180
|
+
}
|
|
1118
1181
|
// In flight now: record it before waiting on the receipt, because a receipt
|
|
1119
1182
|
// timeout must not leave the quote reusable.
|
|
1120
1183
|
onBroadcast?.(step.id, txHash);
|
|
@@ -1158,7 +1221,16 @@ async function processSignatureStepLocal(step, { privateKeyHex, log, apiInstance
|
|
|
1158
1221
|
}
|
|
1159
1222
|
|
|
1160
1223
|
log(` Signing ${step.id} (EIP-712)...`);
|
|
1161
|
-
|
|
1224
|
+
// Fail closed on ANY submit failure: the proxy may have forwarded the
|
|
1225
|
+
// authorization before the error surfaced, and we can't tell that from a
|
|
1226
|
+
// clean pre-forward rejection. Consume the quote before rethrowing so a
|
|
1227
|
+
// re-execute can't resubmit this leg.
|
|
1228
|
+
try {
|
|
1229
|
+
await postBridgeExecute(apiInstance, targetUrl, postBody);
|
|
1230
|
+
} catch (sendErr) {
|
|
1231
|
+
onBroadcast?.(step.id, null);
|
|
1232
|
+
throw sendErr;
|
|
1233
|
+
}
|
|
1162
1234
|
onBroadcast?.(step.id, null);
|
|
1163
1235
|
log(` Submitted to ${new URL(targetUrl).hostname}`);
|
|
1164
1236
|
} else if (signData.action) {
|
|
@@ -1196,7 +1268,17 @@ async function processSignatureStepLocal(step, { privateKeyHex, log, apiInstance
|
|
|
1196
1268
|
};
|
|
1197
1269
|
|
|
1198
1270
|
log(` Signing ${step.id} (Hyperliquid deposit)...`);
|
|
1199
|
-
|
|
1271
|
+
// Fail closed if the SUBMIT itself throws (transport/proxy error — HL may
|
|
1272
|
+
// still have received the action). A clean HL rejection instead returns
|
|
1273
|
+
// 200 and is caught by assertHyperliquidStepAccepted below, OUTSIDE this
|
|
1274
|
+
// try: that is a definitive no-op, so it must NOT consume the quote.
|
|
1275
|
+
let result;
|
|
1276
|
+
try {
|
|
1277
|
+
result = await postBridgeExecute(apiInstance, 'https://api.hyperliquid.xyz/exchange', hlBody);
|
|
1278
|
+
} catch (sendErr) {
|
|
1279
|
+
onBroadcast?.(step.id, null);
|
|
1280
|
+
throw sendErr;
|
|
1281
|
+
}
|
|
1200
1282
|
assertHyperliquidStepAccepted(result, step.id);
|
|
1201
1283
|
onBroadcast?.(step.id, null);
|
|
1202
1284
|
log(` Submitted to api.hyperliquid.xyz`);
|
|
@@ -1270,7 +1352,14 @@ async function processSignatureStepPrivy(step, { privyClient, walletId, log, api
|
|
|
1270
1352
|
} else {
|
|
1271
1353
|
postBody.signature = signature;
|
|
1272
1354
|
}
|
|
1273
|
-
|
|
1355
|
+
// Fail closed on any submit failure — see the local path for the rationale
|
|
1356
|
+
// (an ambiguous forward must not leave the quote reusable).
|
|
1357
|
+
try {
|
|
1358
|
+
await postBridgeExecute(apiInstance, targetUrl, postBody);
|
|
1359
|
+
} catch (sendErr) {
|
|
1360
|
+
onBroadcast?.(step.id, null);
|
|
1361
|
+
throw sendErr;
|
|
1362
|
+
}
|
|
1274
1363
|
onBroadcast?.(step.id, null);
|
|
1275
1364
|
} else {
|
|
1276
1365
|
const [rHex, sHex, vHex] = [signature.slice(2, 66), signature.slice(66, 130), signature.slice(130, 132)];
|
|
@@ -1279,7 +1368,16 @@ async function processSignatureStepPrivy(step, { privyClient, walletId, log, api
|
|
|
1279
1368
|
nonce: signData.nonce,
|
|
1280
1369
|
signature: { r: '0x' + rHex, s: '0x' + sHex, v: parseInt(vHex, 16) },
|
|
1281
1370
|
};
|
|
1282
|
-
|
|
1371
|
+
// Submit-throw = ambiguous → fail closed; a clean HL rejection returns 200
|
|
1372
|
+
// and is caught by assertHyperliquidStepAccepted (outside the try) as a
|
|
1373
|
+
// definitive no-op that must NOT consume the quote.
|
|
1374
|
+
let result;
|
|
1375
|
+
try {
|
|
1376
|
+
result = await postBridgeExecute(apiInstance, 'https://api.hyperliquid.xyz/exchange', hlBody);
|
|
1377
|
+
} catch (sendErr) {
|
|
1378
|
+
onBroadcast?.(step.id, null);
|
|
1379
|
+
throw sendErr;
|
|
1380
|
+
}
|
|
1283
1381
|
assertHyperliquidStepAccepted(result, step.id);
|
|
1284
1382
|
onBroadcast?.(step.id, null);
|
|
1285
1383
|
}
|