perp-cli 0.12.17 → 0.14.1
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/README.md +247 -27
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +303 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +70 -92
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +248 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +925 -245
- package/dist/commands/history.js +102 -54
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +357 -459
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +240 -6
- package/dist/exchanges/aster.js +1139 -195
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +152 -0
- package/dist/exchanges/ondo/adapter.js +811 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +95 -0
- package/dist/exchanges/ondo/funds.js +198 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +293 -4
- package/dist/exchanges/pacifica.js +1044 -90
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1386 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +29 -0
- package/dist/exchanges/venues/ondo.js +136 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +39 -0
- package/dist/exchanges/venues/qfex.js +140 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +156 -684
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +7 -2
- package/dist/landing.js +19 -3
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +821 -403
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +159 -55
- package/skills/perp-cli/references/agent-operations.md +112 -58
- package/skills/perp-cli/references/commands.md +269 -73
- package/skills/perp-cli/references/strategies.md +34 -31
- package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
- package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +31 -15
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Robinhood Chain reads/txs for Arcus spot (design §12.4 #7), over
|
|
3
|
+
* `VenueNetworkConfig.rpcUrl` with ethers.
|
|
4
|
+
*
|
|
5
|
+
* Permit2 allowance logic ported from @arcus-xyz/arcus-spot-sdk@1.0.2
|
|
6
|
+
* (src/permits.ts) — Copyright Arcus, Apache License 2.0
|
|
7
|
+
* (http://www.apache.org/licenses/LICENSE-2.0). Changes: viem → ethers; the
|
|
8
|
+
* SDK's last-resort EIP-2612 domain guess (`version() ?? "1"` without a
|
|
9
|
+
* DOMAIN_SEPARATOR match) is replaced by an error (SSOT Rule #2).
|
|
10
|
+
*/
|
|
11
|
+
import { AbiCoder, Interface, JsonRpcProvider, Network, Signature, Transaction, keccak256, toUtf8Bytes } from "ethers";
|
|
12
|
+
import { PerpError, orderOutcomeUnknown, sendFailure } from "../../errors.js";
|
|
13
|
+
import { MULTICALL3_ADDRESS, PERMIT2_ADDRESS } from "./spot-router.js";
|
|
14
|
+
/** Approve receipt wait. */
|
|
15
|
+
export const ARCUS_APPROVE_RECEIPT_TIMEOUT_MS = 120_000;
|
|
16
|
+
/** Gas-limit headroom over eth_estimateGas for the approve tx (also used for the ETH balance check). */
|
|
17
|
+
const APPROVE_GAS_HEADROOM_PCT = 20n;
|
|
18
|
+
/** Multicall3 aggregate3 batch size. */
|
|
19
|
+
const MULTICALL_BATCH = 150;
|
|
20
|
+
export const MAX_UINT256 = (1n << 256n) - 1n;
|
|
21
|
+
const erc20 = new Interface([
|
|
22
|
+
"function balanceOf(address) view returns (uint256)",
|
|
23
|
+
"function allowance(address owner, address spender) view returns (uint256)",
|
|
24
|
+
"function approve(address spender, uint256 amount) returns (bool)",
|
|
25
|
+
"function name() view returns (string)",
|
|
26
|
+
"function version() view returns (string)",
|
|
27
|
+
"function nonces(address owner) view returns (uint256)",
|
|
28
|
+
"function DOMAIN_SEPARATOR() view returns (bytes32)",
|
|
29
|
+
"function eip712Domain() view returns (bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions)",
|
|
30
|
+
"event Transfer(address indexed from, address indexed to, uint256 value)",
|
|
31
|
+
]);
|
|
32
|
+
const multicall3 = new Interface([
|
|
33
|
+
"function aggregate3((address target, bool allowFailure, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[] returnData)",
|
|
34
|
+
]);
|
|
35
|
+
const TRANSFER_TOPIC = erc20.getEvent("Transfer").topicHash;
|
|
36
|
+
const EIP712_DOMAIN_TYPEHASH = keccak256(toUtf8Bytes("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"));
|
|
37
|
+
/** ethers error shapes that mean "the contract reverted / returned nothing" (vs. transport failure). */
|
|
38
|
+
function isContractRevert(e) {
|
|
39
|
+
const code = e?.code;
|
|
40
|
+
return code === "CALL_EXCEPTION" || code === "BAD_DATA";
|
|
41
|
+
}
|
|
42
|
+
export class ArcusSpotChain {
|
|
43
|
+
rpcUrl;
|
|
44
|
+
chainId;
|
|
45
|
+
provider;
|
|
46
|
+
chainChecked = false;
|
|
47
|
+
constructor(rpcUrl, chainId) {
|
|
48
|
+
this.rpcUrl = rpcUrl;
|
|
49
|
+
this.chainId = chainId;
|
|
50
|
+
const net = Network.from(chainId);
|
|
51
|
+
this.provider = new JsonRpcProvider(rpcUrl, net, { staticNetwork: net });
|
|
52
|
+
}
|
|
53
|
+
/** Fails if the RPC serves a different chain than the venue's signing chain. */
|
|
54
|
+
async ready() {
|
|
55
|
+
if (this.chainChecked)
|
|
56
|
+
return;
|
|
57
|
+
const id = BigInt(await this.provider.send("eth_chainId", []));
|
|
58
|
+
if (id !== BigInt(this.chainId)) {
|
|
59
|
+
throw new PerpError("EXCHANGE_ERROR", `RPC ${this.rpcUrl} serves chainId ${id}, expected ${this.chainId}`, { exchange: "arcus" });
|
|
60
|
+
}
|
|
61
|
+
this.chainChecked = true;
|
|
62
|
+
}
|
|
63
|
+
async call(to, data) {
|
|
64
|
+
await this.ready();
|
|
65
|
+
return this.provider.call({ to, data });
|
|
66
|
+
}
|
|
67
|
+
async nativeBalance(owner) {
|
|
68
|
+
await this.ready();
|
|
69
|
+
return this.provider.getBalance(owner);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* balanceOf(owner) for each token via Multicall3. Returns null for a target
|
|
73
|
+
* that answered with no data (no contract deployed — e.g. a wrapped token the
|
|
74
|
+
* factory has not created yet); a reverted call throws.
|
|
75
|
+
*/
|
|
76
|
+
async erc20Balances(owner, tokens) {
|
|
77
|
+
await this.ready();
|
|
78
|
+
const out = [];
|
|
79
|
+
const data = erc20.encodeFunctionData("balanceOf", [owner]);
|
|
80
|
+
for (let i = 0; i < tokens.length; i += MULTICALL_BATCH) {
|
|
81
|
+
const batch = tokens.slice(i, i + MULTICALL_BATCH);
|
|
82
|
+
const raw = await this.provider.call({
|
|
83
|
+
to: MULTICALL3_ADDRESS,
|
|
84
|
+
data: multicall3.encodeFunctionData("aggregate3", [batch.map(t => ({ target: t, allowFailure: true, callData: data }))]),
|
|
85
|
+
});
|
|
86
|
+
const [results] = multicall3.decodeFunctionResult("aggregate3", raw);
|
|
87
|
+
results.forEach((r, j) => {
|
|
88
|
+
if (!r.success) {
|
|
89
|
+
throw new PerpError("EXCHANGE_ERROR", `balanceOf reverted on token ${batch[j]}`, { exchange: "arcus" });
|
|
90
|
+
}
|
|
91
|
+
out.push(r.returnData === "0x" ? null : erc20.decodeFunctionResult("balanceOf", r.returnData)[0]);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
async permit2Allowance(token, owner) {
|
|
97
|
+
const raw = await this.call(token, erc20.encodeFunctionData("allowance", [owner, PERMIT2_ADDRESS]));
|
|
98
|
+
return erc20.decodeFunctionResult("allowance", raw)[0];
|
|
99
|
+
}
|
|
100
|
+
/** EIP-2612 nonce, or null when the token has no `nonces()` (not EIP-2612 → approve tx needed). */
|
|
101
|
+
async eip2612Nonce(token, owner) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = await this.call(token, erc20.encodeFunctionData("nonces", [owner]));
|
|
104
|
+
if (raw === "0x")
|
|
105
|
+
return null;
|
|
106
|
+
return erc20.decodeFunctionResult("nonces", raw)[0];
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
if (isContractRevert(e))
|
|
110
|
+
return null;
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* EIP-712 domain the token verifies permits with: EIP-5267 `eip712Domain()`,
|
|
116
|
+
* else a `version()` / "1" / "2" candidate that reproduces DOMAIN_SEPARATOR().
|
|
117
|
+
* Nothing verifiable → throws (no guessing).
|
|
118
|
+
*/
|
|
119
|
+
async eip2612Domain(token) {
|
|
120
|
+
try {
|
|
121
|
+
const raw = await this.call(token, erc20.encodeFunctionData("eip712Domain", []));
|
|
122
|
+
if (raw !== "0x") {
|
|
123
|
+
const d = erc20.decodeFunctionResult("eip712Domain", raw);
|
|
124
|
+
const name = d[1];
|
|
125
|
+
const version = d[2];
|
|
126
|
+
if (name && version)
|
|
127
|
+
return { name, version };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
if (!isContractRevert(e))
|
|
132
|
+
throw e;
|
|
133
|
+
}
|
|
134
|
+
const name = erc20.decodeFunctionResult("name", await this.call(token, erc20.encodeFunctionData("name", [])))[0];
|
|
135
|
+
let versionFn = null;
|
|
136
|
+
try {
|
|
137
|
+
const raw = await this.call(token, erc20.encodeFunctionData("version", []));
|
|
138
|
+
if (raw !== "0x")
|
|
139
|
+
versionFn = erc20.decodeFunctionResult("version", raw)[0];
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
if (!isContractRevert(e))
|
|
143
|
+
throw e;
|
|
144
|
+
}
|
|
145
|
+
const sepRaw = await this.call(token, erc20.encodeFunctionData("DOMAIN_SEPARATOR", []));
|
|
146
|
+
const onchain = erc20.decodeFunctionResult("DOMAIN_SEPARATOR", sepRaw)[0].toLowerCase();
|
|
147
|
+
const candidates = [...new Set([versionFn, "1", "2"].filter((v) => typeof v === "string" && v.length > 0))];
|
|
148
|
+
const coder = AbiCoder.defaultAbiCoder();
|
|
149
|
+
for (const version of candidates) {
|
|
150
|
+
const sep = keccak256(coder.encode(["bytes32", "bytes32", "bytes32", "uint256", "address"], [EIP712_DOMAIN_TYPEHASH, keccak256(toUtf8Bytes(name)), keccak256(toUtf8Bytes(version)), BigInt(this.chainId), token]));
|
|
151
|
+
if (sep.toLowerCase() === onchain)
|
|
152
|
+
return { name, version };
|
|
153
|
+
}
|
|
154
|
+
throw new PerpError("NOT_IMPLEMENTED", `Cannot determine the EIP-2612 domain of token ${token} (no eip712Domain(), no DOMAIN_SEPARATOR match)`, {
|
|
155
|
+
exchange: "arcus",
|
|
156
|
+
remediation: `Approve Permit2 (${PERMIT2_ADDRESS}) for this token from your wallet, then retry`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Send `approve(Permit2, MAX_UINT256)` from `owner`, signed via `signTx`
|
|
161
|
+
* (master key), after checking the native ETH balance covers
|
|
162
|
+
* gasLimit × maxFeePerGas. Waits for a successful receipt.
|
|
163
|
+
*/
|
|
164
|
+
async approvePermit2(token, owner, signTx) {
|
|
165
|
+
await this.ready();
|
|
166
|
+
const data = erc20.encodeFunctionData("approve", [PERMIT2_ADDRESS, MAX_UINT256]);
|
|
167
|
+
const estimate = await this.provider.estimateGas({ from: owner, to: token, data });
|
|
168
|
+
const gasLimit = estimate + (estimate * APPROVE_GAS_HEADROOM_PCT) / 100n;
|
|
169
|
+
const fee = await this.provider.getFeeData();
|
|
170
|
+
if (fee.maxFeePerGas === null || fee.maxPriorityFeePerGas === null) {
|
|
171
|
+
throw new PerpError("EXCHANGE_ERROR", `RPC ${this.rpcUrl} returned no EIP-1559 fee data`, { exchange: "arcus" });
|
|
172
|
+
}
|
|
173
|
+
const cost = gasLimit * fee.maxFeePerGas;
|
|
174
|
+
const eth = await this.provider.getBalance(owner);
|
|
175
|
+
if (eth <= cost) {
|
|
176
|
+
throw new PerpError("INSUFFICIENT_BALANCE", `Permit2 approve needs up to ${cost} wei of Robinhood Chain ETH for gas; wallet ${owner} has ${eth}`, {
|
|
177
|
+
exchange: "arcus",
|
|
178
|
+
remediation: `Fund ${owner} with ETH on Robinhood Chain (chainId ${this.chainId}), then retry`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const nonce = await this.provider.getTransactionCount(owner, "pending");
|
|
182
|
+
const tx = Transaction.from({
|
|
183
|
+
type: 2, chainId: BigInt(this.chainId), nonce, to: token, data, value: 0n, gasLimit,
|
|
184
|
+
maxFeePerGas: fee.maxFeePerGas, maxPriorityFeePerGas: fee.maxPriorityFeePerGas,
|
|
185
|
+
});
|
|
186
|
+
tx.signature = Signature.from(await signTx(tx));
|
|
187
|
+
if (tx.from?.toLowerCase() !== owner.toLowerCase()) {
|
|
188
|
+
throw new PerpError("SIGNATURE_FAILED", `Approve tx recovered signer ${tx.from}, expected ${owner}`, { exchange: "arcus" });
|
|
189
|
+
}
|
|
190
|
+
// Once broadcast, the approve's outcome is unknown until mined: a broadcast transport failure,
|
|
191
|
+
// a receipt read failure or a receipt timeout → ORDER_PENDING (never the retryable TIMEOUT,
|
|
192
|
+
// which callers re-type as "not sent"). No connection at all → definitely not sent.
|
|
193
|
+
const signedHash = tx.hash;
|
|
194
|
+
const pendingRemediation = (hash) => `Do NOT send another approve; wait for ${hash ?? "the approve tx"} to confirm (check the wallet's Robinhood Chain transactions), then retry the swap`;
|
|
195
|
+
let sent;
|
|
196
|
+
try {
|
|
197
|
+
sent = await this.provider.broadcastTransaction(tx.serialized);
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
throw sendFailure("arcus", `Permit2 approve tx ${signedHash ?? ""}`.trim(), e, pendingRemediation(signedHash));
|
|
201
|
+
}
|
|
202
|
+
let receipt;
|
|
203
|
+
try {
|
|
204
|
+
receipt = await this.provider.waitForTransaction(sent.hash, 1, ARCUS_APPROVE_RECEIPT_TIMEOUT_MS);
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
if (e.code !== "TIMEOUT") {
|
|
208
|
+
throw orderOutcomeUnknown("arcus", `Permit2 approve tx ${sent.hash}`, e, pendingRemediation(sent.hash));
|
|
209
|
+
}
|
|
210
|
+
receipt = null;
|
|
211
|
+
}
|
|
212
|
+
if (receipt === null) {
|
|
213
|
+
throw new PerpError("ORDER_PENDING", `Permit2 approve tx ${sent.hash} not mined within ${ARCUS_APPROVE_RECEIPT_TIMEOUT_MS / 1000}s`, {
|
|
214
|
+
exchange: "arcus",
|
|
215
|
+
txHash: sent.hash,
|
|
216
|
+
remediation: pendingRemediation(sent.hash),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (receipt.status !== 1) {
|
|
220
|
+
throw new PerpError("EXCHANGE_ERROR", `Permit2 approve tx ${sent.hash} reverted`, { exchange: "arcus" });
|
|
221
|
+
}
|
|
222
|
+
return sent.hash;
|
|
223
|
+
}
|
|
224
|
+
/** ERC-20 Transfer logs in a mined tx (null when the RPC has no receipt for it). */
|
|
225
|
+
async transfersIn(txHash) {
|
|
226
|
+
await this.ready();
|
|
227
|
+
const receipt = await this.provider.getTransactionReceipt(txHash);
|
|
228
|
+
if (receipt === null)
|
|
229
|
+
return null;
|
|
230
|
+
const transfers = [];
|
|
231
|
+
for (const log of receipt.logs) {
|
|
232
|
+
if (log.topics[0] !== TRANSFER_TOPIC || log.topics.length !== 3)
|
|
233
|
+
continue;
|
|
234
|
+
const ev = erc20.parseLog({ topics: [...log.topics], data: log.data });
|
|
235
|
+
if (ev === null)
|
|
236
|
+
continue;
|
|
237
|
+
transfers.push({ token: log.address, from: ev.args[0], to: ev.args[1], amount: ev.args[2] });
|
|
238
|
+
}
|
|
239
|
+
return { status: receipt.status, transfers };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arcus Spot Router HTTP client + wire types (design §12, §12.4 #7).
|
|
3
|
+
*
|
|
4
|
+
* Ported from @arcus-xyz/arcus-spot-sdk@1.0.2 (src/client.ts, src/types.ts,
|
|
5
|
+
* src/chains.ts) — Copyright Arcus, licensed under the Apache License 2.0
|
|
6
|
+
* (http://www.apache.org/licenses/LICENSE-2.0). Changes: viem removed, only
|
|
7
|
+
* the "arcus" venue shapes kept, errors mapped to PerpError.
|
|
8
|
+
*
|
|
9
|
+
* Endpoints (router.spot.arcus.xyz): GET /v1/tokens, /v1/price, /v1/quote,
|
|
10
|
+
* /v1/status; POST /v1/submit. No API key is sent — the router served
|
|
11
|
+
* /tokens, /price and /quote without one (verified 2026-10-01). Whether
|
|
12
|
+
* /submit needs one is unverified; a 401/403 on submit is reported as
|
|
13
|
+
* NOT_IMPLEMENTED (no key storage exists, SSOT Rule #3).
|
|
14
|
+
*/
|
|
15
|
+
import { PerpError } from "../../errors.js";
|
|
16
|
+
import type { Network } from "../venue-types.js";
|
|
17
|
+
/** Canonical Permit2 (same address on every chain). */
|
|
18
|
+
export declare const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
19
|
+
/** Multicall3 — `eth_getCode` non-empty on RH mainnet and testnet (verified 2026-10-01). */
|
|
20
|
+
export declare const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
21
|
+
/** `/tokens` sentinel for the chain's native asset. */
|
|
22
|
+
export declare const NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
|
23
|
+
export interface ArcusSpotDeployment {
|
|
24
|
+
routerUrl: string;
|
|
25
|
+
chainId: number;
|
|
26
|
+
/** Permit2 spender bound in every arcus-venue intent (SDK chains.ts). */
|
|
27
|
+
arcusSettlement: string;
|
|
28
|
+
}
|
|
29
|
+
export declare const ARCUS_SPOT_DEPLOYMENTS: Record<Network, ArcusSpotDeployment>;
|
|
30
|
+
/** Per-request HTTP timeout (SDK default). */
|
|
31
|
+
export declare const ARCUS_ROUTER_TIMEOUT_MS = 15000;
|
|
32
|
+
export interface RouterToken {
|
|
33
|
+
chainId: number;
|
|
34
|
+
address: string;
|
|
35
|
+
symbol: string;
|
|
36
|
+
name: string;
|
|
37
|
+
decimals: number;
|
|
38
|
+
source: string;
|
|
39
|
+
category: string;
|
|
40
|
+
verified: boolean;
|
|
41
|
+
/** Canonical wrapped (placeholder) token for RFQ fallback fills; null/absent = none. */
|
|
42
|
+
wrappedTokenAddress?: string | null;
|
|
43
|
+
}
|
|
44
|
+
export interface RouteFee {
|
|
45
|
+
amount: string;
|
|
46
|
+
token: string;
|
|
47
|
+
type: string;
|
|
48
|
+
bps?: number;
|
|
49
|
+
amountUsd?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface PriceEntry {
|
|
52
|
+
venue: string;
|
|
53
|
+
/** Net of fees, buy-token base units. */
|
|
54
|
+
buyAmount: string;
|
|
55
|
+
sellAmount: string;
|
|
56
|
+
fees: RouteFee[];
|
|
57
|
+
}
|
|
58
|
+
export interface PriceResponse {
|
|
59
|
+
recommended: string;
|
|
60
|
+
venue: string;
|
|
61
|
+
referencePrice: string | null;
|
|
62
|
+
all: PriceEntry[];
|
|
63
|
+
}
|
|
64
|
+
export interface Eip712TypedData {
|
|
65
|
+
domain: Record<string, unknown>;
|
|
66
|
+
types: Record<string, Array<{
|
|
67
|
+
name: string;
|
|
68
|
+
type: string;
|
|
69
|
+
}>>;
|
|
70
|
+
primaryType: string;
|
|
71
|
+
message: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
export interface TakerIntent {
|
|
74
|
+
taker: string;
|
|
75
|
+
takerSellToken: string;
|
|
76
|
+
takerBuyToken: string;
|
|
77
|
+
sellAmount: string;
|
|
78
|
+
minBuyAmount: string;
|
|
79
|
+
allowWrapped: boolean;
|
|
80
|
+
nonce: string;
|
|
81
|
+
deadline: string;
|
|
82
|
+
}
|
|
83
|
+
export interface TakerIntentTypedData extends Eip712TypedData {
|
|
84
|
+
message: {
|
|
85
|
+
permitted: {
|
|
86
|
+
token: string;
|
|
87
|
+
amount: string;
|
|
88
|
+
};
|
|
89
|
+
spender: string;
|
|
90
|
+
nonce: string;
|
|
91
|
+
deadline: string;
|
|
92
|
+
witness: TakerIntent;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export interface ArcusQuoteEntry extends PriceEntry {
|
|
96
|
+
venue: "arcus";
|
|
97
|
+
expiry: number;
|
|
98
|
+
toSign: TakerIntentTypedData;
|
|
99
|
+
arcus: {
|
|
100
|
+
minAmountOut: string;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export interface QuoteResponse {
|
|
104
|
+
recommended: string;
|
|
105
|
+
venue: string;
|
|
106
|
+
all: Array<PriceEntry & {
|
|
107
|
+
toSign?: unknown;
|
|
108
|
+
}>;
|
|
109
|
+
}
|
|
110
|
+
/** EIP-2612 permit folded into SwapShell.permits[] (SDK types.ts `Permit`). */
|
|
111
|
+
export interface Permit {
|
|
112
|
+
token: string;
|
|
113
|
+
value: string;
|
|
114
|
+
deadline: string;
|
|
115
|
+
v: number;
|
|
116
|
+
r: string;
|
|
117
|
+
s: string;
|
|
118
|
+
}
|
|
119
|
+
export interface ArcusSignedQuote {
|
|
120
|
+
venue: "arcus";
|
|
121
|
+
chainId: number;
|
|
122
|
+
taker: string;
|
|
123
|
+
typedData: TakerIntentTypedData;
|
|
124
|
+
signature: string;
|
|
125
|
+
permits?: Permit[];
|
|
126
|
+
}
|
|
127
|
+
export interface ArcusSubmitResponse {
|
|
128
|
+
venue: string;
|
|
129
|
+
txHash: string;
|
|
130
|
+
status: string;
|
|
131
|
+
maker?: string;
|
|
132
|
+
wrapped?: boolean;
|
|
133
|
+
settledToken?: string;
|
|
134
|
+
orderId?: string;
|
|
135
|
+
}
|
|
136
|
+
export type RouterStatus = "pending" | "submitted" | "confirmed" | "failed" | "unknown";
|
|
137
|
+
export interface StatusResponse {
|
|
138
|
+
venue: string;
|
|
139
|
+
status: RouterStatus;
|
|
140
|
+
txHash?: string | null;
|
|
141
|
+
reason?: string | null;
|
|
142
|
+
errorCode?: string | null;
|
|
143
|
+
swap?: unknown;
|
|
144
|
+
}
|
|
145
|
+
/** HTTP-level router failure (non-2xx). `routerCode` is the router's error code when it sent one. */
|
|
146
|
+
export declare class RouterHttpError extends PerpError {
|
|
147
|
+
readonly status: number;
|
|
148
|
+
readonly routerCode: string | null;
|
|
149
|
+
readonly body: unknown;
|
|
150
|
+
readonly retryAfter: string | null;
|
|
151
|
+
constructor(message: string, status: number, routerCode: string | null, body: unknown, retryAfter: string | null);
|
|
152
|
+
}
|
|
153
|
+
export declare class ArcusSpotRouter {
|
|
154
|
+
readonly deployment: ArcusSpotDeployment;
|
|
155
|
+
constructor(deployment: ArcusSpotDeployment);
|
|
156
|
+
private url;
|
|
157
|
+
private request;
|
|
158
|
+
/**
|
|
159
|
+
* Idempotent GET. A 429 is waited out per the server's Retry-After (logged to
|
|
160
|
+
* stderr, at most MAX_RATE_LIMIT_WAITS times, each ≤ MAX_RETRY_AFTER_S);
|
|
161
|
+
* beyond that → RATE_LIMITED. Never used for POST /submit.
|
|
162
|
+
*/
|
|
163
|
+
private get;
|
|
164
|
+
tokens(): Promise<RouterToken[]>;
|
|
165
|
+
price(sellToken: string, buyToken: string, sellAmount: bigint): Promise<PriceResponse>;
|
|
166
|
+
quote(p: {
|
|
167
|
+
sellToken: string;
|
|
168
|
+
buyToken: string;
|
|
169
|
+
sellAmount: bigint;
|
|
170
|
+
taker: string;
|
|
171
|
+
slippageBps: number;
|
|
172
|
+
}): Promise<QuoteResponse>;
|
|
173
|
+
/** Single attempt — never retried (a resend could double-execute). Throws RouterHttpError / PerpError. */
|
|
174
|
+
submit(body: ArcusSignedQuote): Promise<ArcusSubmitResponse>;
|
|
175
|
+
status(id: string): Promise<StatusResponse>;
|
|
176
|
+
statusUrl(id: string): string;
|
|
177
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arcus Spot Router HTTP client + wire types (design §12, §12.4 #7).
|
|
3
|
+
*
|
|
4
|
+
* Ported from @arcus-xyz/arcus-spot-sdk@1.0.2 (src/client.ts, src/types.ts,
|
|
5
|
+
* src/chains.ts) — Copyright Arcus, licensed under the Apache License 2.0
|
|
6
|
+
* (http://www.apache.org/licenses/LICENSE-2.0). Changes: viem removed, only
|
|
7
|
+
* the "arcus" venue shapes kept, errors mapped to PerpError.
|
|
8
|
+
*
|
|
9
|
+
* Endpoints (router.spot.arcus.xyz): GET /v1/tokens, /v1/price, /v1/quote,
|
|
10
|
+
* /v1/status; POST /v1/submit. No API key is sent — the router served
|
|
11
|
+
* /tokens, /price and /quote without one (verified 2026-10-01). Whether
|
|
12
|
+
* /submit needs one is unverified; a 401/403 on submit is reported as
|
|
13
|
+
* NOT_IMPLEMENTED (no key storage exists, SSOT Rule #3).
|
|
14
|
+
*/
|
|
15
|
+
import { PerpError } from "../../errors.js";
|
|
16
|
+
/** Canonical Permit2 (same address on every chain). */
|
|
17
|
+
export const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
18
|
+
/** Multicall3 — `eth_getCode` non-empty on RH mainnet and testnet (verified 2026-10-01). */
|
|
19
|
+
export const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
20
|
+
/** `/tokens` sentinel for the chain's native asset. */
|
|
21
|
+
export const NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
|
22
|
+
export const ARCUS_SPOT_DEPLOYMENTS = {
|
|
23
|
+
mainnet: {
|
|
24
|
+
routerUrl: "https://router.spot.arcus.xyz/v1",
|
|
25
|
+
chainId: 4663,
|
|
26
|
+
arcusSettlement: "0x006102b16A04c20306A28b652745D3973D7D24fa",
|
|
27
|
+
},
|
|
28
|
+
testnet: {
|
|
29
|
+
routerUrl: "https://router.spot.testnet.arcus.xyz/v1",
|
|
30
|
+
chainId: 46630,
|
|
31
|
+
arcusSettlement: "0xE8D8b187754D8a5Ca4Ea4E77Cd01506f9332A773",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
/** Per-request HTTP timeout (SDK default). */
|
|
35
|
+
export const ARCUS_ROUTER_TIMEOUT_MS = 15_000;
|
|
36
|
+
/** Longest Retry-After (seconds) a GET waits out before failing with RATE_LIMITED. */
|
|
37
|
+
const MAX_RETRY_AFTER_S = 60;
|
|
38
|
+
/** Rate-limit waits per GET before failing with RATE_LIMITED. */
|
|
39
|
+
const MAX_RATE_LIMIT_WAITS = 2;
|
|
40
|
+
/** Router codes meaning the trade is below the router's minimum size/fee. */
|
|
41
|
+
const BELOW_MINIMUM_CODES = new Set(["TRADE_NOTIONAL_BELOW_MINIMUM", "TRADE_BELOW_MINIMUM_FEE"]);
|
|
42
|
+
/** HTTP-level router failure (non-2xx). `routerCode` is the router's error code when it sent one. */
|
|
43
|
+
export class RouterHttpError extends PerpError {
|
|
44
|
+
status;
|
|
45
|
+
routerCode;
|
|
46
|
+
body;
|
|
47
|
+
retryAfter;
|
|
48
|
+
constructor(message, status, routerCode, body, retryAfter) {
|
|
49
|
+
super(status === 429 ? "RATE_LIMITED" : routerCode !== null && BELOW_MINIMUM_CODES.has(routerCode) ? "SIZE_TOO_SMALL" : "EXCHANGE_ERROR", message, { exchange: "arcus", httpStatus: status, routerCode });
|
|
50
|
+
this.status = status;
|
|
51
|
+
this.routerCode = routerCode;
|
|
52
|
+
this.body = body;
|
|
53
|
+
this.retryAfter = retryAfter;
|
|
54
|
+
this.name = "RouterHttpError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function sleep(ms) {
|
|
58
|
+
return new Promise(r => setTimeout(r, ms));
|
|
59
|
+
}
|
|
60
|
+
function routerErrorMessage(body) {
|
|
61
|
+
if (!body || typeof body !== "object")
|
|
62
|
+
return { code: null, message: typeof body === "string" && body ? body : null };
|
|
63
|
+
const b = body;
|
|
64
|
+
const code = typeof b.code === "string" ? b.code : null;
|
|
65
|
+
const error = typeof b.error === "string" ? b.error : null;
|
|
66
|
+
const details = Array.isArray(b.details)
|
|
67
|
+
? b.details
|
|
68
|
+
.map(d => {
|
|
69
|
+
const item = d;
|
|
70
|
+
const v = typeof item.venue === "string" ? item.venue : "upstream";
|
|
71
|
+
const c = typeof item.error?.code === "string" ? item.error.code : null;
|
|
72
|
+
const m = typeof item.error?.message === "string" ? item.error.message : null;
|
|
73
|
+
return `${v}: ${[c, m].filter(Boolean).join(" ")}`;
|
|
74
|
+
})
|
|
75
|
+
.join("; ")
|
|
76
|
+
: null;
|
|
77
|
+
return { code, message: [error ?? code, details].filter(Boolean).join(" — ") || null };
|
|
78
|
+
}
|
|
79
|
+
export class ArcusSpotRouter {
|
|
80
|
+
deployment;
|
|
81
|
+
constructor(deployment) {
|
|
82
|
+
this.deployment = deployment;
|
|
83
|
+
}
|
|
84
|
+
url(path, params) {
|
|
85
|
+
const u = new URL(`${this.deployment.routerUrl}/${path}`);
|
|
86
|
+
for (const [k, v] of Object.entries(params))
|
|
87
|
+
u.searchParams.set(k, v);
|
|
88
|
+
return u.toString();
|
|
89
|
+
}
|
|
90
|
+
async request(method, url, body) {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timer = setTimeout(() => controller.abort(), ARCUS_ROUTER_TIMEOUT_MS);
|
|
93
|
+
let res;
|
|
94
|
+
try {
|
|
95
|
+
res = await fetch(url, {
|
|
96
|
+
method,
|
|
97
|
+
headers: body === undefined ? undefined : { "Content-Type": "application/json" },
|
|
98
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
99
|
+
signal: controller.signal,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
const aborted = controller.signal.aborted;
|
|
104
|
+
throw new PerpError(aborted ? "TIMEOUT" : "EXCHANGE_UNREACHABLE", `Arcus router ${method} ${url} ${aborted ? `timed out after ${ARCUS_ROUTER_TIMEOUT_MS}ms` : `failed: ${e instanceof Error ? e.message : String(e)}`}`, { exchange: "arcus" });
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
}
|
|
109
|
+
const text = await res.text();
|
|
110
|
+
let parsed = null;
|
|
111
|
+
if (text) {
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(text);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
parsed = text;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
const { code, message } = routerErrorMessage(parsed);
|
|
121
|
+
throw new RouterHttpError(`Arcus router ${method} ${new URL(url).pathname} → HTTP ${res.status}${message ? `: ${message}` : ""}`, res.status, code, parsed, res.headers.get("retry-after"));
|
|
122
|
+
}
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Idempotent GET. A 429 is waited out per the server's Retry-After (logged to
|
|
127
|
+
* stderr, at most MAX_RATE_LIMIT_WAITS times, each ≤ MAX_RETRY_AFTER_S);
|
|
128
|
+
* beyond that → RATE_LIMITED. Never used for POST /submit.
|
|
129
|
+
*/
|
|
130
|
+
async get(path, params) {
|
|
131
|
+
const url = this.url(path, params);
|
|
132
|
+
for (let waits = 0;; waits++) {
|
|
133
|
+
try {
|
|
134
|
+
return await this.request("GET", url, undefined);
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
if (!(e instanceof RouterHttpError) || e.status !== 429)
|
|
138
|
+
throw e;
|
|
139
|
+
const ra = e.retryAfter === null ? Number.NaN : Number(e.retryAfter);
|
|
140
|
+
if (!Number.isFinite(ra) || ra < 0 || ra > MAX_RETRY_AFTER_S || waits >= MAX_RATE_LIMIT_WAITS) {
|
|
141
|
+
throw new PerpError("RATE_LIMITED", `Arcus router rate limit on /${path} (Retry-After: ${Number.isFinite(ra) ? `${ra}s` : "none"})`, {
|
|
142
|
+
exchange: "arcus",
|
|
143
|
+
remediation: "Wait a minute and retry",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
console.error(`[arcus-spot] router rate-limited on /${path}; waiting ${ra}s (Retry-After)`);
|
|
147
|
+
await sleep(ra * 1000);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async tokens() {
|
|
152
|
+
const rows = await this.get("tokens", { chainId: String(this.deployment.chainId) });
|
|
153
|
+
if (!Array.isArray(rows))
|
|
154
|
+
throw new PerpError("EXCHANGE_ERROR", "Arcus router /tokens did not return a list", { exchange: "arcus" });
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
157
|
+
async price(sellToken, buyToken, sellAmount) {
|
|
158
|
+
return this.get("price", {
|
|
159
|
+
chainId: String(this.deployment.chainId), sellToken, buyToken, sellAmount: sellAmount.toString(),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async quote(p) {
|
|
163
|
+
return this.get("quote", {
|
|
164
|
+
chainId: String(this.deployment.chainId),
|
|
165
|
+
sellToken: p.sellToken,
|
|
166
|
+
buyToken: p.buyToken,
|
|
167
|
+
sellAmount: p.sellAmount.toString(),
|
|
168
|
+
taker: p.taker,
|
|
169
|
+
slippageBps: String(p.slippageBps),
|
|
170
|
+
allowWrapped: "false", // §12.4 #7 — never accept a wrapped placeholder
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Single attempt — never retried (a resend could double-execute). Throws RouterHttpError / PerpError. */
|
|
174
|
+
async submit(body) {
|
|
175
|
+
return this.request("POST", `${this.deployment.routerUrl}/submit`, body);
|
|
176
|
+
}
|
|
177
|
+
async status(id) {
|
|
178
|
+
return this.get("status", { venue: "arcus", id, chainId: String(this.deployment.chainId) });
|
|
179
|
+
}
|
|
180
|
+
statusUrl(id) {
|
|
181
|
+
return this.url("status", { venue: "arcus", id, chainId: String(this.deployment.chainId) });
|
|
182
|
+
}
|
|
183
|
+
}
|