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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AgentMeta } from "../settings.js";
|
|
1
|
+
import type { AgentMeta, AgentSource, AnyAgentMeta, ArcusAgentMeta, OndoAgentMeta, QfexAgentMeta } from "../settings.js";
|
|
2
|
+
import type { Network } from "../exchanges/venue-types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Attempt to acquire the lockfile for an exchange's agent-approve operation.
|
|
4
5
|
* Returns void on success. Throws LOCK_HELD if another live process holds it.
|
|
@@ -9,11 +10,27 @@ import type { AgentMeta } from "../settings.js";
|
|
|
9
10
|
*/
|
|
10
11
|
export declare function acquireLock(exchange: string): void;
|
|
11
12
|
export declare function releaseLock(exchange: string): void;
|
|
13
|
+
export declare function getAgent(exchange: string, agentName?: string): AgentMeta | null;
|
|
12
14
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* Network a legacy-shaped agent is registered on, or null when it cannot be
|
|
16
|
+
* classified (an untagged Lighter-family record, whose approve always honored -n).
|
|
15
17
|
*/
|
|
16
|
-
export declare function
|
|
18
|
+
export declare function recordedAgentNetworkOf(exchange: string, meta: AgentMeta): Network | null;
|
|
19
|
+
/**
|
|
20
|
+
* How an agent record got here (AgentMeta.source). Records written before the field existed were
|
|
21
|
+
* all created by `wallet agent approve` (master-signed on this machine), except Ondo keys, which
|
|
22
|
+
* are always created in the Ondo Perps web app.
|
|
23
|
+
*/
|
|
24
|
+
export declare function agentSourceOf(exchange: string, meta: AnyAgentMeta): AgentSource;
|
|
25
|
+
/**
|
|
26
|
+
* Network a legacy-shaped agent is registered on. Untagged Lighter-family
|
|
27
|
+
* records (whose approve always honored -n) cannot be classified and throw.
|
|
28
|
+
*/
|
|
29
|
+
export declare function agentNetworkOf(exchange: string, meta: AgentMeta): Network;
|
|
30
|
+
/** Throws AGENT_NOT_REGISTERED when `meta` was registered on a different network than `network`. */
|
|
31
|
+
export declare function assertAgentNetwork(exchange: string, meta: AgentMeta, network: Network): void;
|
|
32
|
+
/** First non-partial agent of `exchange` registered on `network`, or null. */
|
|
33
|
+
export declare function getAgentForNetwork(exchange: string, network: Network): AgentMeta | null;
|
|
17
34
|
/**
|
|
18
35
|
* Persist an agent meta entry for the given exchange.
|
|
19
36
|
* Acquires a lockfile before writing to prevent concurrent races (AC-14).
|
|
@@ -35,3 +52,19 @@ export declare function listAgents(exchange?: string): Array<{
|
|
|
35
52
|
exchange: string;
|
|
36
53
|
meta: AgentMeta;
|
|
37
54
|
}>;
|
|
55
|
+
type VenueAgentMap = {
|
|
56
|
+
ondo: OndoAgentMeta;
|
|
57
|
+
arcus: ArcusAgentMeta;
|
|
58
|
+
qfex: QfexAgentMeta;
|
|
59
|
+
};
|
|
60
|
+
/** Named agent, or the only/first registered one when agentName is omitted. */
|
|
61
|
+
export declare function getVenueAgent<V extends keyof VenueAgentMap>(venue: V, agentName?: string): VenueAgentMap[V] | null;
|
|
62
|
+
/** First Ondo/Arcus agent registered on `network` (meta.network), or null — never another network's key. */
|
|
63
|
+
export declare function getVenueAgentForNetwork<V extends keyof VenueAgentMap>(venue: V, network: Network): VenueAgentMap[V] | null;
|
|
64
|
+
export declare function setVenueAgent<V extends keyof VenueAgentMap>(venue: V, meta: VenueAgentMap[V]): void;
|
|
65
|
+
export declare function deleteVenueAgent(venue: keyof VenueAgentMap, agentName: string): boolean;
|
|
66
|
+
export declare function listVenueAgents(): Array<{
|
|
67
|
+
exchange: keyof VenueAgentMap;
|
|
68
|
+
meta: OndoAgentMeta | ArcusAgentMeta | QfexAgentMeta;
|
|
69
|
+
}>;
|
|
70
|
+
export {};
|
|
@@ -90,7 +90,15 @@ function isPidAlive(pid) {
|
|
|
90
90
|
* Returns the specific named agent, or the first non-partial agent when
|
|
91
91
|
* agentName is omitted.
|
|
92
92
|
*/
|
|
93
|
+
/** Venues whose agents use a non-legacy meta shape (see getVenueAgent). */
|
|
94
|
+
const NON_LEGACY_AGENT_VENUES = new Set(["ondo", "arcus", "qfex"]);
|
|
95
|
+
function assertLegacyAgentVenue(exchange) {
|
|
96
|
+
if (NON_LEGACY_AGENT_VENUES.has(exchange)) {
|
|
97
|
+
throw new PerpError("INVALID_PARAMS", `${exchange} agents use a venue-specific meta; call getVenueAgent("${exchange}")`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
93
100
|
export function getAgent(exchange, agentName) {
|
|
101
|
+
assertLegacyAgentVenue(exchange);
|
|
94
102
|
const settings = loadSettings();
|
|
95
103
|
const byExchange = settings.agents?.[exchange];
|
|
96
104
|
if (!byExchange)
|
|
@@ -106,6 +114,69 @@ export function getAgent(exchange, agentName) {
|
|
|
106
114
|
}
|
|
107
115
|
return null;
|
|
108
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Venues whose agent approve/rotate was hardwired to mainnet endpoints before
|
|
119
|
+
* agent records carried `network` — an untagged record of these venues was
|
|
120
|
+
* provably registered on mainnet.
|
|
121
|
+
*/
|
|
122
|
+
const PRE_TAG_MAINNET_ONLY_VENUES = new Set(["aster", "hyperliquid", "pacifica"]);
|
|
123
|
+
/**
|
|
124
|
+
* Network a legacy-shaped agent is registered on, or null when it cannot be
|
|
125
|
+
* classified (an untagged Lighter-family record, whose approve always honored -n).
|
|
126
|
+
*/
|
|
127
|
+
export function recordedAgentNetworkOf(exchange, meta) {
|
|
128
|
+
if (meta.network !== undefined)
|
|
129
|
+
return meta.network;
|
|
130
|
+
if (PRE_TAG_MAINNET_ONLY_VENUES.has(exchange))
|
|
131
|
+
return "mainnet";
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* How an agent record got here (AgentMeta.source). Records written before the field existed were
|
|
136
|
+
* all created by `wallet agent approve` (master-signed on this machine), except Ondo keys, which
|
|
137
|
+
* are always created in the Ondo Perps web app.
|
|
138
|
+
*/
|
|
139
|
+
export function agentSourceOf(exchange, meta) {
|
|
140
|
+
if (meta.source !== undefined)
|
|
141
|
+
return meta.source;
|
|
142
|
+
return exchange === "ondo" || exchange === "qfex" ? "imported" : "approved";
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Network a legacy-shaped agent is registered on. Untagged Lighter-family
|
|
146
|
+
* records (whose approve always honored -n) cannot be classified and throw.
|
|
147
|
+
*/
|
|
148
|
+
export function agentNetworkOf(exchange, meta) {
|
|
149
|
+
const recorded = recordedAgentNetworkOf(exchange, meta);
|
|
150
|
+
if (recorded !== null)
|
|
151
|
+
return recorded;
|
|
152
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `${exchange} agent "${meta.agentName}" has no recorded network (registered before network tagging)`, {
|
|
153
|
+
remediation: `Set "network": "mainnet" or "testnet" on settings.agents["${exchange}"]["${meta.agentName}"] in ~/.perp/settings.json, or re-register it: perp [-n testnet] wallet agent rotate ${exchange} ${meta.agentName}`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/** Throws AGENT_NOT_REGISTERED when `meta` was registered on a different network than `network`. */
|
|
157
|
+
export function assertAgentNetwork(exchange, meta, network) {
|
|
158
|
+
const registered = agentNetworkOf(exchange, meta);
|
|
159
|
+
if (registered !== network) {
|
|
160
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `${exchange} agent "${meta.agentName}" is registered on ${registered}, not ${network}`, {
|
|
161
|
+
remediation: registered === "mainnet"
|
|
162
|
+
? `Run without -n testnet to use or manage it, or register a testnet agent: perp -n testnet wallet agent approve ${exchange}`
|
|
163
|
+
: `Run with -n testnet to use or manage it, or register a mainnet agent: perp wallet agent approve ${exchange}`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** First non-partial agent of `exchange` registered on `network`, or null. */
|
|
168
|
+
export function getAgentForNetwork(exchange, network) {
|
|
169
|
+
assertLegacyAgentVenue(exchange);
|
|
170
|
+
const settings = loadSettings();
|
|
171
|
+
const byExchange = settings.agents?.[exchange];
|
|
172
|
+
if (!byExchange)
|
|
173
|
+
return null;
|
|
174
|
+
for (const meta of Object.values(byExchange)) {
|
|
175
|
+
if (meta.status !== "partial" && agentNetworkOf(exchange, meta) === network)
|
|
176
|
+
return meta;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
109
180
|
/**
|
|
110
181
|
* Persist an agent meta entry for the given exchange.
|
|
111
182
|
* Acquires a lockfile before writing to prevent concurrent races (AC-14).
|
|
@@ -115,6 +186,7 @@ export function getAgent(exchange, agentName) {
|
|
|
115
186
|
* double-acquire or release.
|
|
116
187
|
*/
|
|
117
188
|
export function setAgent(exchange, meta) {
|
|
189
|
+
assertLegacyAgentVenue(exchange);
|
|
118
190
|
const alreadyLocked = callerHoldsLock(exchange);
|
|
119
191
|
if (!alreadyLocked) {
|
|
120
192
|
acquireLock(exchange);
|
|
@@ -162,6 +234,8 @@ export function listAgents(exchange) {
|
|
|
162
234
|
for (const [ex, agents] of Object.entries(settings.agents)) {
|
|
163
235
|
if (exchange !== undefined && ex !== exchange)
|
|
164
236
|
continue;
|
|
237
|
+
if (NON_LEGACY_AGENT_VENUES.has(ex))
|
|
238
|
+
continue; // listed by listVenueAgents()
|
|
165
239
|
if (agents && typeof agents === "object") {
|
|
166
240
|
for (const meta of Object.values(agents)) {
|
|
167
241
|
result.push({ exchange: ex, meta });
|
|
@@ -170,3 +244,61 @@ export function listAgents(exchange) {
|
|
|
170
244
|
}
|
|
171
245
|
return result;
|
|
172
246
|
}
|
|
247
|
+
/** Named agent, or the only/first registered one when agentName is omitted. */
|
|
248
|
+
export function getVenueAgent(venue, agentName) {
|
|
249
|
+
const settings = loadSettings();
|
|
250
|
+
const byVenue = settings.agents?.[venue];
|
|
251
|
+
if (!byVenue)
|
|
252
|
+
return null;
|
|
253
|
+
if (agentName !== undefined)
|
|
254
|
+
return byVenue[agentName] ?? null;
|
|
255
|
+
const first = Object.values(byVenue)[0];
|
|
256
|
+
return first === undefined ? null : first;
|
|
257
|
+
}
|
|
258
|
+
/** First Ondo/Arcus agent registered on `network` (meta.network), or null — never another network's key. */
|
|
259
|
+
export function getVenueAgentForNetwork(venue, network) {
|
|
260
|
+
const settings = loadSettings();
|
|
261
|
+
const byVenue = settings.agents?.[venue];
|
|
262
|
+
if (!byVenue)
|
|
263
|
+
return null;
|
|
264
|
+
const match = Object.values(byVenue).find(m => m.network === network);
|
|
265
|
+
return match === undefined ? null : match;
|
|
266
|
+
}
|
|
267
|
+
export function setVenueAgent(venue, meta) {
|
|
268
|
+
const alreadyLocked = callerHoldsLock(venue);
|
|
269
|
+
if (!alreadyLocked)
|
|
270
|
+
acquireLock(venue);
|
|
271
|
+
try {
|
|
272
|
+
const settings = loadSettings();
|
|
273
|
+
if (!settings.agents)
|
|
274
|
+
settings.agents = {};
|
|
275
|
+
const map = (settings.agents[venue] ?? {});
|
|
276
|
+
map[meta.agentName] = meta;
|
|
277
|
+
settings.agents[venue] = map;
|
|
278
|
+
saveSettings(settings);
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
if (!alreadyLocked)
|
|
282
|
+
releaseLock(venue);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
export function deleteVenueAgent(venue, agentName) {
|
|
286
|
+
const settings = loadSettings();
|
|
287
|
+
const byVenue = settings.agents?.[venue];
|
|
288
|
+
if (!byVenue || !(agentName in byVenue))
|
|
289
|
+
return false;
|
|
290
|
+
delete byVenue[agentName];
|
|
291
|
+
saveSettings(settings);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
export function listVenueAgents() {
|
|
295
|
+
const settings = loadSettings();
|
|
296
|
+
const out = [];
|
|
297
|
+
for (const v of ["ondo", "arcus", "qfex"]) {
|
|
298
|
+
const m = settings.agents?.[v];
|
|
299
|
+
if (m)
|
|
300
|
+
for (const meta of Object.values(m))
|
|
301
|
+
out.push({ exchange: v, meta });
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function venueKeystoreDir(dir: string): string;
|
|
2
|
+
export declare function venueKeystorePath(dir: string, name: string): string;
|
|
3
|
+
/** Encrypt `plaintext` and write it with mode 0600. Returns the absolute path. */
|
|
4
|
+
export declare function saveVenueSecret(dir: string, name: string, plaintext: string, passphrase: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Atomic keystore write: tmp file (0600, exclusive create) in the same directory, fsync,
|
|
7
|
+
* then rename over the target. A crash or full disk never leaves a truncated keystore
|
|
8
|
+
* where a valid one (or none) was. Throws on any failure after removing the tmp file.
|
|
9
|
+
*/
|
|
10
|
+
export declare function writeKeystoreAtomic(path: string, contents: string): void;
|
|
11
|
+
/** Read + decrypt a stored secret. `remediation` is surfaced on every failure. */
|
|
12
|
+
export declare function loadVenueSecret(dir: string, name: string, passphrase: string, remediation: string): string;
|
|
13
|
+
/** Idempotent delete. Returns true if a file was removed. */
|
|
14
|
+
export declare function deleteVenueSecret(dir: string, name: string): boolean;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic per-venue encrypted secret store (SSOT Rule #3).
|
|
3
|
+
*
|
|
4
|
+
* `~/.perp/<dir>/<name>.json`, AES-256-GCM with a scrypt-derived key — the same
|
|
5
|
+
* scheme as lighter-keystore.ts / OWS. Used for secrets that are not an
|
|
6
|
+
* OWS-native curve key: Lighter-family L2 slot keys (`lighter-rh-agents`) and
|
|
7
|
+
* Ondo HMAC API secrets (`ondo-agents`).
|
|
8
|
+
*
|
|
9
|
+
* No fallbacks (Rule #2): missing file / wrong passphrase / corrupt file throw.
|
|
10
|
+
*/
|
|
11
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync } from "fs";
|
|
12
|
+
import { resolve } from "path";
|
|
13
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
|
|
14
|
+
import { PerpError } from "../errors.js";
|
|
15
|
+
const KDF = { dklen: 32, n: 65536, p: 1, r: 8 };
|
|
16
|
+
function assertSafeSegment(s, what) {
|
|
17
|
+
if (!/^[A-Za-z0-9._-]+$/.test(s) || s === "." || s === "..") {
|
|
18
|
+
throw new PerpError("INVALID_PARAMS", `invalid keystore ${what}: "${s}"`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function venueKeystoreDir(dir) {
|
|
22
|
+
assertSafeSegment(dir, "directory");
|
|
23
|
+
const home = process.env["HOME"];
|
|
24
|
+
if (!home)
|
|
25
|
+
throw new PerpError("INVALID_PARAMS", "HOME is not set; cannot locate ~/.perp keystore");
|
|
26
|
+
return resolve(home, ".perp", dir);
|
|
27
|
+
}
|
|
28
|
+
export function venueKeystorePath(dir, name) {
|
|
29
|
+
assertSafeSegment(name, "name");
|
|
30
|
+
return resolve(venueKeystoreDir(dir), `${name}.json`);
|
|
31
|
+
}
|
|
32
|
+
/** Encrypt `plaintext` and write it with mode 0600. Returns the absolute path. */
|
|
33
|
+
export function saveVenueSecret(dir, name, plaintext, passphrase) {
|
|
34
|
+
const d = venueKeystoreDir(dir);
|
|
35
|
+
if (!existsSync(d))
|
|
36
|
+
mkdirSync(d, { recursive: true, mode: 0o700 });
|
|
37
|
+
chmodSync(d, 0o700);
|
|
38
|
+
const path = venueKeystorePath(dir, name);
|
|
39
|
+
const salt = randomBytes(32);
|
|
40
|
+
const iv = randomBytes(12);
|
|
41
|
+
const dk = scryptSync(Buffer.from(passphrase, "utf-8"), salt, KDF.dklen, { N: KDF.n, r: KDF.r, p: KDF.p, maxmem: 128 * 1024 * 1024 });
|
|
42
|
+
const cipher = createCipheriv("aes-256-gcm", dk, iv);
|
|
43
|
+
const ciphertext = Buffer.concat([cipher.update(Buffer.from(plaintext, "utf-8")), cipher.final()]);
|
|
44
|
+
const file = {
|
|
45
|
+
version: 1,
|
|
46
|
+
kind: "venue-secret",
|
|
47
|
+
dir,
|
|
48
|
+
name,
|
|
49
|
+
crypto: {
|
|
50
|
+
cipher: "aes-256-gcm",
|
|
51
|
+
cipherparams: { iv: iv.toString("hex") },
|
|
52
|
+
ciphertext: ciphertext.toString("hex"),
|
|
53
|
+
auth_tag: cipher.getAuthTag().toString("hex"),
|
|
54
|
+
kdf: "scrypt",
|
|
55
|
+
kdfparams: { dklen: KDF.dklen, n: KDF.n, p: KDF.p, r: KDF.r, salt: salt.toString("hex") },
|
|
56
|
+
},
|
|
57
|
+
createdAt: new Date().toISOString(),
|
|
58
|
+
};
|
|
59
|
+
writeKeystoreAtomic(path, JSON.stringify(file, null, 2));
|
|
60
|
+
return path;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Atomic keystore write: tmp file (0600, exclusive create) in the same directory, fsync,
|
|
64
|
+
* then rename over the target. A crash or full disk never leaves a truncated keystore
|
|
65
|
+
* where a valid one (or none) was. Throws on any failure after removing the tmp file.
|
|
66
|
+
*/
|
|
67
|
+
export function writeKeystoreAtomic(path, contents) {
|
|
68
|
+
const tmp = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
69
|
+
try {
|
|
70
|
+
const fd = openSync(tmp, "wx", 0o600);
|
|
71
|
+
try {
|
|
72
|
+
writeSync(fd, contents);
|
|
73
|
+
fsyncSync(fd);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
closeSync(fd);
|
|
77
|
+
}
|
|
78
|
+
chmodSync(tmp, 0o600);
|
|
79
|
+
renameSync(tmp, path);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
if (existsSync(tmp))
|
|
83
|
+
unlinkSync(tmp);
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Read + decrypt a stored secret. `remediation` is surfaced on every failure. */
|
|
88
|
+
export function loadVenueSecret(dir, name, passphrase, remediation) {
|
|
89
|
+
const path = venueKeystorePath(dir, name);
|
|
90
|
+
if (!existsSync(path)) {
|
|
91
|
+
throw new PerpError("KEY_NOT_FOUND", `Keystore not found: ${path}`, { remediation });
|
|
92
|
+
}
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
throw new PerpError("SIGNATURE_FAILED", `Keystore corrupt (${path}): ${e instanceof Error ? e.message : String(e)}`, { remediation });
|
|
99
|
+
}
|
|
100
|
+
if (parsed.version !== 1 || parsed.crypto?.cipher !== "aes-256-gcm" || parsed.crypto?.kdf !== "scrypt") {
|
|
101
|
+
throw new PerpError("SIGNATURE_FAILED", `Keystore has unsupported version/cipher: ${path}`, { remediation });
|
|
102
|
+
}
|
|
103
|
+
const kp = parsed.crypto.kdfparams;
|
|
104
|
+
const dk = scryptSync(Buffer.from(passphrase, "utf-8"), Buffer.from(kp.salt, "hex"), kp.dklen, { N: kp.n, r: kp.r, p: kp.p, maxmem: 128 * 1024 * 1024 });
|
|
105
|
+
try {
|
|
106
|
+
const decipher = createDecipheriv("aes-256-gcm", dk, Buffer.from(parsed.crypto.cipherparams.iv, "hex"));
|
|
107
|
+
decipher.setAuthTag(Buffer.from(parsed.crypto.auth_tag, "hex"));
|
|
108
|
+
return Buffer.concat([decipher.update(Buffer.from(parsed.crypto.ciphertext, "hex")), decipher.final()]).toString("utf-8");
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
throw new PerpError("SIGNATURE_FAILED", `Keystore decrypt failed (${path}): ${e instanceof Error ? e.message : String(e)}`, { remediation });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Idempotent delete. Returns true if a file was removed. */
|
|
115
|
+
export function deleteVenueSecret(dir, name) {
|
|
116
|
+
const path = venueKeystorePath(dir, name);
|
|
117
|
+
if (!existsSync(path))
|
|
118
|
+
return false;
|
|
119
|
+
unlinkSync(path);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HYPERLIQUID_API_URL } from "./urls.js";
|
|
1
|
+
import { HYPERLIQUID_API_URL, sharedApiNetwork } from "./urls.js";
|
|
2
2
|
import { assertOk } from "./_http.js";
|
|
3
3
|
import { withCache, TTL_MARKET } from "../../cache.js";
|
|
4
4
|
// ── Internal ──
|
|
@@ -16,7 +16,7 @@ async function hlPost(type) {
|
|
|
16
16
|
export function fetchHyperliquidMeta() {
|
|
17
17
|
// SSOT rule #2: error must propagate. Row-level skipping handles missing
|
|
18
18
|
// markPx / funding so a 0 doesn't reach downstream comparisons.
|
|
19
|
-
return withCache(
|
|
19
|
+
return withCache(`pub:hl:${sharedApiNetwork()}:metaAndAssetCtxs`, TTL_MARKET, async () => {
|
|
20
20
|
const json = await hlPost("metaAndAssetCtxs");
|
|
21
21
|
const universe = (json[0] ?? {}).universe ?? [];
|
|
22
22
|
const ctxs = (json[1] ?? []);
|
|
@@ -38,7 +38,7 @@ export function fetchHyperliquidMetaRaw() {
|
|
|
38
38
|
// `.catch(() => null)` so a network failure surfaces as a rejected Promise.
|
|
39
39
|
// Callers should wrap with Promise.allSettled to keep multi-DEX comparisons
|
|
40
40
|
// running when one DEX is down.
|
|
41
|
-
return withCache(
|
|
41
|
+
return withCache(`pub:hl:${sharedApiNetwork()}:metaAndAssetCtxs:raw`, TTL_MARKET, () => hlPost("metaAndAssetCtxs"));
|
|
42
42
|
}
|
|
43
43
|
export function parseHyperliquidMetaRaw(raw) {
|
|
44
44
|
const rates = new Map();
|
|
@@ -65,7 +65,7 @@ export function parseHyperliquidMetaRaw(raw) {
|
|
|
65
65
|
}
|
|
66
66
|
export function fetchHyperliquidAllMidsRaw() {
|
|
67
67
|
// SSOT rule #2: error must propagate; see fetchHyperliquidMetaRaw.
|
|
68
|
-
return withCache(
|
|
68
|
+
return withCache(`pub:hl:${sharedApiNetwork()}:allMids:raw`, TTL_MARKET, () => hlPost("allMids"));
|
|
69
69
|
}
|
|
70
70
|
// ── Health check ──
|
|
71
71
|
export async function pingHyperliquid() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LIGHTER_API_URL } from "./urls.js";
|
|
1
|
+
import { LIGHTER_API_URL, sharedApiNetwork } from "./urls.js";
|
|
2
2
|
import { assertOk } from "./_http.js";
|
|
3
3
|
import { withCache, TTL_MARKET } from "../../cache.js";
|
|
4
4
|
// ── Fetchers ──
|
|
@@ -7,7 +7,7 @@ export function fetchLighterOrderBookDetails() {
|
|
|
7
7
|
// returned no markets" from "API down" otherwise. Row-level skipping is
|
|
8
8
|
// applied for missing/invalid last_trade_price so we never publish a
|
|
9
9
|
// 0 price downstream.
|
|
10
|
-
return withCache(
|
|
10
|
+
return withCache(`pub:lt:${sharedApiNetwork()}:orderBookDetails`, TTL_MARKET, async () => {
|
|
11
11
|
const res = await fetch(`${LIGHTER_API_URL}/api/v1/orderBookDetails`);
|
|
12
12
|
await assertOk(res, "Lighter orderBookDetails");
|
|
13
13
|
const json = await res.json();
|
|
@@ -31,7 +31,7 @@ export function fetchLighterOrderBookDetailsRaw() {
|
|
|
31
31
|
// rejected Promise instead of being indistinguishable from "API returned no
|
|
32
32
|
// markets". Non-2xx responses are also rejected so a 5xx JSON body cannot
|
|
33
33
|
// fulfill as if it were valid data.
|
|
34
|
-
return withCache(
|
|
34
|
+
return withCache(`pub:lt:${sharedApiNetwork()}:orderBookDetails:raw`, TTL_MARKET, async () => {
|
|
35
35
|
const res = await fetch(`${LIGHTER_API_URL}/api/v1/orderBookDetails`);
|
|
36
36
|
await assertOk(res, "Lighter orderBookDetails");
|
|
37
37
|
return res.json();
|
|
@@ -44,7 +44,7 @@ export function fetchLighterFundingRates() {
|
|
|
44
44
|
// documented as intermittent on Lighter's side — when missing, surface as
|
|
45
45
|
// null so the consumer can apply the orderBookDetails fallback (a
|
|
46
46
|
// documented price-source preference, NOT an error fallback).
|
|
47
|
-
return withCache(
|
|
47
|
+
return withCache(`pub:lt:${sharedApiNetwork()}:fundingRates`, TTL_MARKET, async () => {
|
|
48
48
|
const res = await fetch(`${LIGHTER_API_URL}/api/v1/funding-rates`);
|
|
49
49
|
await assertOk(res, "Lighter funding-rates");
|
|
50
50
|
const json = await res.json();
|
|
@@ -75,7 +75,7 @@ export function fetchLighterFundingRates() {
|
|
|
75
75
|
}
|
|
76
76
|
export function fetchLighterFundingRatesRaw() {
|
|
77
77
|
// SSOT rule #2: error must propagate; see fetchLighterOrderBookDetailsRaw.
|
|
78
|
-
return withCache(
|
|
78
|
+
return withCache(`pub:lt:${sharedApiNetwork()}:fundingRates:raw`, TTL_MARKET, async () => {
|
|
79
79
|
const res = await fetch(`${LIGHTER_API_URL}/api/v1/funding-rates`);
|
|
80
80
|
await assertOk(res, "Lighter funding-rates");
|
|
81
81
|
return res.json();
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { PACIFICA_API_URL } from "./urls.js";
|
|
1
|
+
import { PACIFICA_API_URL, sharedApiNetwork } from "./urls.js";
|
|
2
2
|
import { assertOk } from "./_http.js";
|
|
3
3
|
import { withCache, TTL_MARKET } from "../../cache.js";
|
|
4
4
|
// ── Fetchers ──
|
|
5
5
|
export function fetchPacificaPrices() {
|
|
6
6
|
// SSOT rule #2: error must propagate. Row-level skipping is applied for
|
|
7
7
|
// missing/invalid mark or funding so a 0 doesn't reach downstream paths.
|
|
8
|
-
return withCache(
|
|
8
|
+
return withCache(`pub:pac:${sharedApiNetwork()}:prices`, TTL_MARKET, async () => {
|
|
9
9
|
const res = await fetch(PACIFICA_API_URL);
|
|
10
10
|
await assertOk(res, "Pacifica prices");
|
|
11
11
|
const json = await res.json();
|
|
@@ -21,16 +21,7 @@ export function fetchPacificaPrices() {
|
|
|
21
21
|
if (!symbol || fundingRaw === undefined || !Number.isFinite(funding) || !Number.isFinite(mark) || mark <= 0) {
|
|
22
22
|
continue;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
const nextFundingNumber = Number(nextFundingRaw);
|
|
26
|
-
out.push({
|
|
27
|
-
symbol,
|
|
28
|
-
funding,
|
|
29
|
-
mark,
|
|
30
|
-
nextFunding: nextFundingRaw !== undefined && Number.isFinite(nextFundingNumber)
|
|
31
|
-
? nextFundingNumber
|
|
32
|
-
: undefined,
|
|
33
|
-
});
|
|
24
|
+
out.push({ symbol, funding, mark });
|
|
34
25
|
}
|
|
35
26
|
return out;
|
|
36
27
|
});
|
|
@@ -41,7 +32,7 @@ export function fetchPacificaPricesRaw() {
|
|
|
41
32
|
// Callers should wrap with Promise.allSettled to keep multi-DEX comparisons
|
|
42
33
|
// running when one DEX is down. Non-2xx responses are also rejected so a
|
|
43
34
|
// 5xx JSON body cannot fulfill as if it were valid data.
|
|
44
|
-
return withCache(
|
|
35
|
+
return withCache(`pub:pac:${sharedApiNetwork()}:prices:raw`, TTL_MARKET, async () => {
|
|
45
36
|
const res = await fetch(PACIFICA_API_URL);
|
|
46
37
|
await assertOk(res, "Pacifica prices");
|
|
47
38
|
return res.json();
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared API URL management for all exchanges.
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* `legacyPublicBaseUrl(venue, net)` is the network-aware source for the public
|
|
5
|
+
* data path (funding/legacy-public.ts). The mutable `*_API_URL` globals remain
|
|
6
|
+
* for the older fetchers below and are switched by setSharedApiNetwork().
|
|
4
7
|
*/
|
|
8
|
+
type LegacyPublicVenue = "pacifica" | "hyperliquid" | "aster";
|
|
9
|
+
export declare function legacyPublicBaseUrl(venue: LegacyPublicVenue, net: "mainnet" | "testnet"): string;
|
|
5
10
|
export declare let PACIFICA_API_URL: string;
|
|
6
11
|
export declare let HYPERLIQUID_API_URL: string;
|
|
7
12
|
export declare let LIGHTER_API_URL: string;
|
|
13
|
+
/** Network selected by the CLI entry point (setSharedApiNetwork). */
|
|
14
|
+
export declare function sharedApiNetwork(): "mainnet" | "testnet";
|
|
8
15
|
/**
|
|
9
16
|
* Switch shared API URLs between mainnet and testnet.
|
|
10
17
|
* Call from CLI entry point after parsing --network flag.
|
|
11
18
|
*/
|
|
12
19
|
export declare function setSharedApiNetwork(network: "mainnet" | "testnet"): void;
|
|
20
|
+
export {};
|
package/dist/api/public/urls.js
CHANGED
|
@@ -1,33 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared API URL management for all exchanges.
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* `legacyPublicBaseUrl(venue, net)` is the network-aware source for the public
|
|
5
|
+
* data path (funding/legacy-public.ts). The mutable `*_API_URL` globals remain
|
|
6
|
+
* for the older fetchers below and are switched by setSharedApiNetwork().
|
|
4
7
|
*/
|
|
8
|
+
import { PerpError } from "../../errors.js";
|
|
9
|
+
import { asterVenue } from "../../exchanges/venues/aster.js";
|
|
10
|
+
import { MAINNET_REST as PAC_MAINNET_REST, TESTNET_REST as PAC_TESTNET_REST } from "../../pacifica/constants.js";
|
|
11
|
+
/** REST base per legacy venue and network. null = no public testnet API known. */
|
|
12
|
+
const LEGACY_PUBLIC_BASES = {
|
|
13
|
+
pacifica: { mainnet: PAC_MAINNET_REST, testnet: PAC_TESTNET_REST },
|
|
14
|
+
hyperliquid: { mainnet: "https://api.hyperliquid.xyz", testnet: "https://api.hyperliquid-testnet.xyz" },
|
|
15
|
+
// SSOT: the Aster venue definition (venues/aster.ts) — same hosts AsterAdapter uses.
|
|
16
|
+
aster: { mainnet: asterVenue.networks.mainnet.restUrl, testnet: asterVenue.networks.testnet === null ? null : asterVenue.networks.testnet.restUrl },
|
|
17
|
+
};
|
|
18
|
+
export function legacyPublicBaseUrl(venue, net) {
|
|
19
|
+
const url = LEGACY_PUBLIC_BASES[venue][net];
|
|
20
|
+
if (url === null) {
|
|
21
|
+
throw new PerpError("NOT_IMPLEMENTED", `${venue} has no public ${net} market-data API`, {
|
|
22
|
+
remediation: `Run without --testnet for ${venue} market data`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return url;
|
|
26
|
+
}
|
|
5
27
|
const MAINNET_URLS = {
|
|
6
|
-
pacifica:
|
|
28
|
+
pacifica: `${PAC_MAINNET_REST}/info/prices`,
|
|
7
29
|
hyperliquid: "https://api.hyperliquid.xyz/info",
|
|
8
30
|
lighter: "https://mainnet.zklighter.elliot.ai",
|
|
9
31
|
};
|
|
10
32
|
const TESTNET_URLS = {
|
|
11
|
-
pacifica:
|
|
33
|
+
pacifica: `${PAC_TESTNET_REST}/info/prices`,
|
|
12
34
|
hyperliquid: "https://api.hyperliquid-testnet.xyz/info",
|
|
13
35
|
lighter: "https://testnet.zklighter.elliot.ai",
|
|
14
36
|
};
|
|
15
37
|
export let PACIFICA_API_URL = MAINNET_URLS.pacifica;
|
|
16
38
|
export let HYPERLIQUID_API_URL = MAINNET_URLS.hyperliquid;
|
|
17
39
|
export let LIGHTER_API_URL = MAINNET_URLS.lighter;
|
|
40
|
+
let _sharedNetwork = "mainnet";
|
|
41
|
+
/** Network selected by the CLI entry point (setSharedApiNetwork). */
|
|
42
|
+
export function sharedApiNetwork() {
|
|
43
|
+
return _sharedNetwork;
|
|
44
|
+
}
|
|
18
45
|
/**
|
|
19
46
|
* Switch shared API URLs between mainnet and testnet.
|
|
20
47
|
* Call from CLI entry point after parsing --network flag.
|
|
21
48
|
*/
|
|
22
49
|
export function setSharedApiNetwork(network) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
else {
|
|
29
|
-
PACIFICA_API_URL = MAINNET_URLS.pacifica;
|
|
30
|
-
HYPERLIQUID_API_URL = MAINNET_URLS.hyperliquid;
|
|
31
|
-
LIGHTER_API_URL = MAINNET_URLS.lighter;
|
|
32
|
-
}
|
|
50
|
+
_sharedNetwork = network;
|
|
51
|
+
const urls = network === "testnet" ? TESTNET_URLS : MAINNET_URLS;
|
|
52
|
+
PACIFICA_API_URL = urls.pacifica;
|
|
53
|
+
HYPERLIQUID_API_URL = urls.hyperliquid;
|
|
54
|
+
LIGHTER_API_URL = urls.lighter;
|
|
33
55
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** How long to wait for another process's lock, and when a lock left by a dead process is stale. */
|
|
2
|
+
export declare const FILE_LOCK_WAIT_MS = 5000;
|
|
3
|
+
export declare const FILE_LOCK_STALE_MS = 30000;
|
|
4
|
+
/**
|
|
5
|
+
* Run `fn` holding the advisory lock file `<path>.lock` (O_EXCL create) — for a load → modify →
|
|
6
|
+
* save of a state file shared by several perp processes (bots, daemons, CLI), so one process
|
|
7
|
+
* never overwrites another's concurrent write. A lock older than FILE_LOCK_STALE_MS (its holder
|
|
8
|
+
* died) is removed; a live one not released within FILE_LOCK_WAIT_MS → LOCK_HELD (nothing is
|
|
9
|
+
* written on top of an unknown concurrent write).
|
|
10
|
+
*/
|
|
11
|
+
export declare function withFileLock<T>(path: string, what: string, fn: () => T): T;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, openSync, statSync, unlinkSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { PerpError } from "../errors.js";
|
|
4
|
+
/** How long to wait for another process's lock, and when a lock left by a dead process is stale. */
|
|
5
|
+
export const FILE_LOCK_WAIT_MS = 5_000;
|
|
6
|
+
export const FILE_LOCK_STALE_MS = 30_000;
|
|
7
|
+
/** Locks this process holds (path → depth): a nested load-modify-save in the same process never self-deadlocks. */
|
|
8
|
+
const held = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Run `fn` holding the advisory lock file `<path>.lock` (O_EXCL create) — for a load → modify →
|
|
11
|
+
* save of a state file shared by several perp processes (bots, daemons, CLI), so one process
|
|
12
|
+
* never overwrites another's concurrent write. A lock older than FILE_LOCK_STALE_MS (its holder
|
|
13
|
+
* died) is removed; a live one not released within FILE_LOCK_WAIT_MS → LOCK_HELD (nothing is
|
|
14
|
+
* written on top of an unknown concurrent write).
|
|
15
|
+
*/
|
|
16
|
+
export function withFileLock(path, what, fn) {
|
|
17
|
+
const lock = `${path}.lock`;
|
|
18
|
+
const depth = held.get(lock) ?? 0;
|
|
19
|
+
if (depth > 0) {
|
|
20
|
+
held.set(lock, depth + 1);
|
|
21
|
+
try {
|
|
22
|
+
return fn();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
held.set(lock, depth);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const dir = resolve(path, "..");
|
|
29
|
+
if (!existsSync(dir))
|
|
30
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
31
|
+
const deadline = Date.now() + FILE_LOCK_WAIT_MS;
|
|
32
|
+
const sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
33
|
+
for (;;) {
|
|
34
|
+
try {
|
|
35
|
+
closeSync(openSync(lock, "wx", 0o600));
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
if (e.code !== "EEXIST")
|
|
40
|
+
throw e;
|
|
41
|
+
let stale = false;
|
|
42
|
+
try {
|
|
43
|
+
stale = Date.now() - statSync(lock).mtimeMs > FILE_LOCK_STALE_MS;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue; // released between the open and the stat: try again
|
|
47
|
+
}
|
|
48
|
+
if (stale) {
|
|
49
|
+
try {
|
|
50
|
+
unlinkSync(lock);
|
|
51
|
+
}
|
|
52
|
+
catch { /* another process removed it */ }
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (Date.now() > deadline) {
|
|
56
|
+
throw new PerpError("LOCK_HELD", `${what} is locked by another perp process (${lock})`, {
|
|
57
|
+
remediation: `Retry shortly; if no other perp process is running, delete ${lock}`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
Atomics.wait(sleeper, 0, 0, 10);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
held.set(lock, 1);
|
|
64
|
+
try {
|
|
65
|
+
return fn();
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
held.delete(lock);
|
|
69
|
+
try {
|
|
70
|
+
unlinkSync(lock);
|
|
71
|
+
}
|
|
72
|
+
catch { /* already gone */ }
|
|
73
|
+
}
|
|
74
|
+
}
|