perp-cli 0.11.0 → 0.12.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 +30 -25
- package/dist/_stderr-filter.d.ts +0 -0
- package/dist/_stderr-filter.js +30 -0
- package/dist/agent-wallet/expiry.d.ts +15 -0
- package/dist/agent-wallet/expiry.js +26 -0
- package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
- package/dist/agent-wallet/lighter-keystore.js +232 -0
- package/dist/agent-wallet/passphrase.d.ts +13 -0
- package/dist/agent-wallet/passphrase.js +54 -0
- package/dist/agent-wallet/signing-strategy.d.ts +67 -0
- package/dist/agent-wallet/signing-strategy.js +51 -0
- package/dist/agent-wallet/store.d.ts +37 -0
- package/dist/agent-wallet/store.js +172 -0
- package/dist/api/public/_http.d.ts +15 -0
- package/dist/api/public/_http.js +24 -0
- package/dist/api/public/hyperliquid.js +36 -24
- package/dist/api/public/lighter.d.ts +7 -1
- package/dist/api/public/lighter.js +85 -37
- package/dist/api/public/pacifica.js +47 -18
- package/dist/bridge-engine.js +102 -49
- package/dist/cli-spec.js +1 -2
- package/dist/commands/account.js +2 -500
- package/dist/commands/agent.d.ts +8 -2
- package/dist/commands/agent.js +1903 -366
- package/dist/commands/alerts.js +3 -1
- package/dist/commands/arb/index.js +37 -7
- package/dist/commands/arb-auto.js +44 -11
- package/dist/commands/arb.js +83 -34
- package/dist/commands/bot.d.ts +1 -1
- package/dist/commands/bot.js +18 -14
- package/dist/commands/bridge.d.ts +1 -1
- package/dist/commands/bridge.js +4 -4
- package/dist/commands/funds.d.ts +1 -1
- package/dist/commands/funds.js +18 -68
- package/dist/commands/jobs.d.ts +1 -1
- package/dist/commands/jobs.js +2 -2
- package/dist/commands/manage.d.ts +1 -1
- package/dist/commands/manage.js +12 -84
- package/dist/commands/plan.d.ts +1 -1
- package/dist/commands/plan.js +2 -2
- package/dist/commands/portfolio.d.ts +19 -3
- package/dist/commands/portfolio.js +747 -163
- package/dist/commands/rebalance.d.ts +1 -1
- package/dist/commands/rebalance.js +18 -12
- package/dist/commands/risk.js +2 -6
- package/dist/commands/settings.js +30 -53
- package/dist/commands/trade.js +16 -15
- package/dist/commands/wallet.d.ts +2 -1
- package/dist/commands/wallet.js +756 -28
- package/dist/errors.d.ts +64 -0
- package/dist/errors.js +82 -2
- package/dist/event-stream.js +12 -1
- package/dist/exchanges/aster-typed-data.d.ts +156 -0
- package/dist/exchanges/aster-typed-data.js +189 -0
- package/dist/exchanges/aster.d.ts +60 -11
- package/dist/exchanges/aster.js +278 -136
- package/dist/exchanges/hyperliquid.d.ts +14 -0
- package/dist/exchanges/hyperliquid.js +84 -34
- package/dist/exchanges/lighter-spot.js +14 -3
- package/dist/exchanges/lighter.d.ts +66 -0
- package/dist/exchanges/lighter.js +199 -27
- package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
- package/dist/exchanges/pacifica-typed-data.js +74 -0
- package/dist/exchanges/pacifica.d.ts +29 -1
- package/dist/exchanges/pacifica.js +94 -6
- package/dist/funding/rates.js +59 -21
- package/dist/guardrail/perp-guardrail.js +9 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +268 -360
- package/dist/jobs.js +3 -1
- package/dist/mcp-server.js +85 -83
- package/dist/position-history.js +47 -4
- package/dist/settings.d.ts +67 -3
- package/dist/settings.js +1 -2
- package/dist/signer/ows-evm.d.ts +14 -0
- package/dist/signer/ows-evm.js +28 -9
- package/dist/smart-order.d.ts +8 -2
- package/dist/smart-order.js +46 -11
- package/dist/strategies/funding-arb.js +29 -8
- package/dist/trade-validator.js +30 -5
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +19 -0
- package/package.json +2 -2
- package/skills/perp-cli/SKILL.md +23 -14
- package/skills/perp-cli/references/agent-operations.md +6 -4
- package/skills/perp-cli/references/commands.md +55 -34
- package/skills/perp-cli/scripts/preflight.sh +4 -4
- package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
- package/dist/__tests__/alert-logic.test.d.ts +0 -1
- package/dist/__tests__/alert-logic.test.js +0 -107
- package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
- package/dist/__tests__/arb-auto-3dex.test.js +0 -397
- package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
- package/dist/__tests__/arb-history-stats.test.js +0 -176
- package/dist/__tests__/arb-logic.test.d.ts +0 -1
- package/dist/__tests__/arb-logic.test.js +0 -84
- package/dist/__tests__/arb-manage.test.d.ts +0 -1
- package/dist/__tests__/arb-manage.test.js +0 -253
- package/dist/__tests__/arb-new-features.test.d.ts +0 -1
- package/dist/__tests__/arb-new-features.test.js +0 -457
- package/dist/__tests__/arb-sizing.test.d.ts +0 -1
- package/dist/__tests__/arb-sizing.test.js +0 -48
- package/dist/__tests__/arb-state.test.d.ts +0 -1
- package/dist/__tests__/arb-state.test.js +0 -284
- package/dist/__tests__/arb-userflow.test.d.ts +0 -1
- package/dist/__tests__/arb-userflow.test.js +0 -945
- package/dist/__tests__/arb-utils.test.d.ts +0 -1
- package/dist/__tests__/arb-utils.test.js +0 -264
- package/dist/__tests__/bot-conditions.test.d.ts +0 -1
- package/dist/__tests__/bot-conditions.test.js +0 -341
- package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
- package/dist/__tests__/client-id-tracker.test.js +0 -137
- package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
- package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
- package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
- package/dist/__tests__/commands/order-intent.test.js +0 -600
- package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
- package/dist/__tests__/commands/trade-commands.test.js +0 -821
- package/dist/__tests__/config.test.d.ts +0 -1
- package/dist/__tests__/config.test.js +0 -86
- package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
- package/dist/__tests__/cross-chain-margin.test.js +0 -287
- package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
- package/dist/__tests__/dex-asset-map.test.js +0 -191
- package/dist/__tests__/errors.test.d.ts +0 -1
- package/dist/__tests__/errors.test.js +0 -110
- package/dist/__tests__/event-stream.test.d.ts +0 -1
- package/dist/__tests__/event-stream.test.js +0 -276
- package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
- package/dist/__tests__/exchanges/interface.test.js +0 -132
- package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
- package/dist/__tests__/exchanges/mock-adapter.js +0 -137
- package/dist/__tests__/execution-log.test.d.ts +0 -1
- package/dist/__tests__/execution-log.test.js +0 -106
- package/dist/__tests__/funding-calc.test.d.ts +0 -1
- package/dist/__tests__/funding-calc.test.js +0 -71
- package/dist/__tests__/funding-history.test.d.ts +0 -1
- package/dist/__tests__/funding-history.test.js +0 -343
- package/dist/__tests__/funding-rates.test.d.ts +0 -1
- package/dist/__tests__/funding-rates.test.js +0 -342
- package/dist/__tests__/funding.test.d.ts +0 -1
- package/dist/__tests__/funding.test.js +0 -173
- package/dist/__tests__/gap-logic.test.d.ts +0 -1
- package/dist/__tests__/gap-logic.test.js +0 -43
- package/dist/__tests__/hip3-dex.test.d.ts +0 -1
- package/dist/__tests__/hip3-dex.test.js +0 -234
- package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
- package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
- package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
- package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
- package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
- package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/bridge.integration.test.js +0 -309
- package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
- package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
- package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
- package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
- package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
- package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
- package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/lighter.integration.test.js +0 -53
- package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
- package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
- package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
- package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
- package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
- package/dist/__tests__/liquidity.test.d.ts +0 -1
- package/dist/__tests__/liquidity.test.js +0 -225
- package/dist/__tests__/plan-executor.test.d.ts +0 -1
- package/dist/__tests__/plan-executor.test.js +0 -314
- package/dist/__tests__/position-history.test.d.ts +0 -1
- package/dist/__tests__/position-history.test.js +0 -367
- package/dist/__tests__/retry.test.d.ts +0 -1
- package/dist/__tests__/retry.test.js +0 -310
- package/dist/__tests__/risk-assessment.test.d.ts +0 -1
- package/dist/__tests__/risk-assessment.test.js +0 -288
- package/dist/__tests__/security-adversarial.test.d.ts +0 -1
- package/dist/__tests__/security-adversarial.test.js +0 -574
- package/dist/__tests__/strategies.test.d.ts +0 -1
- package/dist/__tests__/strategies.test.js +0 -539
- package/dist/__tests__/trade-execution.test.d.ts +0 -1
- package/dist/__tests__/trade-execution.test.js +0 -129
- package/dist/__tests__/trade-validator.test.d.ts +0 -1
- package/dist/__tests__/trade-validator.test.js +0 -655
- package/dist/__tests__/utils.test.d.ts +0 -1
- package/dist/__tests__/utils.test.js +0 -76
- package/dist/arb/reconcile.d.ts +0 -28
- package/dist/arb/reconcile.js +0 -162
- package/dist/commands/alert.d.ts +0 -2
- package/dist/commands/alert.js +0 -421
- package/dist/commands/analytics.d.ts +0 -3
- package/dist/commands/analytics.js +0 -311
- package/dist/commands/arb-manage.d.ts +0 -5
- package/dist/commands/arb-manage.js +0 -5
- package/dist/commands/dashboard.d.ts +0 -3
- package/dist/commands/dashboard.js +0 -176
- package/dist/commands/deposit.d.ts +0 -4
- package/dist/commands/deposit.js +0 -573
- package/dist/commands/dex.d.ts +0 -3
- package/dist/commands/dex.js +0 -120
- package/dist/commands/env.d.ts +0 -2
- package/dist/commands/env.js +0 -136
- package/dist/commands/funding.d.ts +0 -3
- package/dist/commands/funding.js +0 -554
- package/dist/commands/gap.d.ts +0 -2
- package/dist/commands/gap.js +0 -305
- package/dist/commands/health.d.ts +0 -2
- package/dist/commands/health.js +0 -67
- package/dist/commands/multileg.d.ts +0 -16
- package/dist/commands/multileg.js +0 -174
- package/dist/commands/ows.d.ts +0 -2
- package/dist/commands/ows.js +0 -887
- package/dist/commands/pnl.d.ts +0 -12
- package/dist/commands/pnl.js +0 -243
- package/dist/commands/run.d.ts +0 -3
- package/dist/commands/run.js +0 -202
- package/dist/commands/stream.d.ts +0 -5
- package/dist/commands/stream.js +0 -200
- package/dist/commands/withdraw.d.ts +0 -3
- package/dist/commands/withdraw.js +0 -187
- package/dist/exchanges/edgex.d.ts +0 -62
- package/dist/exchanges/edgex.js +0 -552
- package/dist/exchanges/nado.d.ts +0 -52
- package/dist/exchanges/nado.js +0 -223
- package/dist/funding/index.d.ts +0 -3
- package/dist/funding/index.js +0 -3
- package/dist/retry.d.ts +0 -74
- package/dist/retry.js +0 -129
- package/dist/ws/market-feed.d.ts +0 -74
- package/dist/ws/market-feed.js +0 -227
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AgentMeta } from "../settings.js";
|
|
2
|
+
/**
|
|
3
|
+
* Attempt to acquire the lockfile for an exchange's agent-approve operation.
|
|
4
|
+
* Returns void on success. Throws LOCK_HELD if another live process holds it.
|
|
5
|
+
* Auto-clears stale locks (>5 min old OR dead PID).
|
|
6
|
+
*
|
|
7
|
+
* Exported so callers can hold the lock across multiple operations (e.g. the
|
|
8
|
+
* full approve flow) before calling setAgent.
|
|
9
|
+
*/
|
|
10
|
+
export declare function acquireLock(exchange: string): void;
|
|
11
|
+
export declare function releaseLock(exchange: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the specific named agent, or the first non-partial agent when
|
|
14
|
+
* agentName is omitted.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getAgent(exchange: string, agentName?: string): AgentMeta | null;
|
|
17
|
+
/**
|
|
18
|
+
* Persist an agent meta entry for the given exchange.
|
|
19
|
+
* Acquires a lockfile before writing to prevent concurrent races (AC-14).
|
|
20
|
+
*
|
|
21
|
+
* Re-entrant: if the calling process already holds the lock (e.g. the approve
|
|
22
|
+
* flow acquired it at the start), this function writes directly without a
|
|
23
|
+
* double-acquire or release.
|
|
24
|
+
*/
|
|
25
|
+
export declare function setAgent(exchange: string, meta: AgentMeta): void;
|
|
26
|
+
/**
|
|
27
|
+
* Delete an agent entry. Returns true if it existed and was removed, false if
|
|
28
|
+
* it was already absent (idempotent per AC-4).
|
|
29
|
+
*/
|
|
30
|
+
export declare function deleteAgent(exchange: string, agentName: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* List all registered agents, optionally filtered to a single exchange.
|
|
33
|
+
*/
|
|
34
|
+
export declare function listAgents(exchange?: string): Array<{
|
|
35
|
+
exchange: string;
|
|
36
|
+
meta: AgentMeta;
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { loadSettings, saveSettings } from "../settings.js";
|
|
4
|
+
import { PerpError } from "../errors.js";
|
|
5
|
+
const PERP_DIR = resolve(process.env["HOME"] ?? "~", ".perp");
|
|
6
|
+
const LOCKS_DIR = resolve(PERP_DIR, "locks");
|
|
7
|
+
const LOCK_STALE_MS = 5 * 60 * 1000; // 5 minutes
|
|
8
|
+
function lockPath(exchange) {
|
|
9
|
+
return resolve(LOCKS_DIR, `agent-approve-${exchange}.lock`);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Attempt to acquire the lockfile for an exchange's agent-approve operation.
|
|
13
|
+
* Returns void on success. Throws LOCK_HELD if another live process holds it.
|
|
14
|
+
* Auto-clears stale locks (>5 min old OR dead PID).
|
|
15
|
+
*
|
|
16
|
+
* Exported so callers can hold the lock across multiple operations (e.g. the
|
|
17
|
+
* full approve flow) before calling setAgent.
|
|
18
|
+
*/
|
|
19
|
+
export function acquireLock(exchange) {
|
|
20
|
+
mkdirSync(LOCKS_DIR, { recursive: true, mode: 0o700 });
|
|
21
|
+
const path = lockPath(exchange);
|
|
22
|
+
if (existsSync(path)) {
|
|
23
|
+
const contents = readFileSync(path, "utf-8");
|
|
24
|
+
const [pidStr, tsStr] = contents.trim().split("\n");
|
|
25
|
+
const pid = parseInt(pidStr ?? "", 10);
|
|
26
|
+
const ts = tsStr ? new Date(tsStr).getTime() : 0;
|
|
27
|
+
const age = Date.now() - ts;
|
|
28
|
+
const isStaleByTime = isNaN(ts) || age > LOCK_STALE_MS;
|
|
29
|
+
const isDeadPid = !isPidAlive(pid);
|
|
30
|
+
if (!isStaleByTime && !isDeadPid) {
|
|
31
|
+
throw new PerpError("LOCK_HELD", `Agent approve lock held by PID ${pid} (started ${tsStr ?? "unknown"})`, {
|
|
32
|
+
remediation: `Wait 5s and retry, or remove ~/.perp/locks/agent-approve-${exchange}.lock if stale`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Stale lock — clear it
|
|
36
|
+
unlinkSync(path);
|
|
37
|
+
}
|
|
38
|
+
writeFileSync(path, `${process.pid}\n${new Date().toISOString()}`, { mode: 0o600 });
|
|
39
|
+
}
|
|
40
|
+
export function releaseLock(exchange) {
|
|
41
|
+
const path = lockPath(exchange);
|
|
42
|
+
try {
|
|
43
|
+
if (existsSync(path)) {
|
|
44
|
+
unlinkSync(path);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Best effort — ignore failures on lock release
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns true if the current process already holds the lock for this exchange.
|
|
53
|
+
* Used by setAgent to implement re-entrant locking.
|
|
54
|
+
*/
|
|
55
|
+
function callerHoldsLock(exchange) {
|
|
56
|
+
const path = lockPath(exchange);
|
|
57
|
+
if (!existsSync(path))
|
|
58
|
+
return false;
|
|
59
|
+
try {
|
|
60
|
+
const contents = readFileSync(path, "utf-8");
|
|
61
|
+
const [pidStr] = contents.trim().split("\n");
|
|
62
|
+
return parseInt(pidStr ?? "", 10) === process.pid;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Check whether a PID is alive via signal 0.
|
|
69
|
+
*
|
|
70
|
+
* `process.kill(pid, 0)` throws:
|
|
71
|
+
* - ESRCH → no such process (dead)
|
|
72
|
+
* - EPERM → process exists but we lack permission (alive — e.g. PID 1 on macOS)
|
|
73
|
+
* Any other error is treated conservatively as "alive" to avoid clearing valid locks.
|
|
74
|
+
*/
|
|
75
|
+
function isPidAlive(pid) {
|
|
76
|
+
if (isNaN(pid) || pid <= 0)
|
|
77
|
+
return false;
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, 0);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
// ESRCH = no such process → dead
|
|
84
|
+
// EPERM = no permission to signal → process IS alive
|
|
85
|
+
const code = e.code;
|
|
86
|
+
return code !== "ESRCH";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns the specific named agent, or the first non-partial agent when
|
|
91
|
+
* agentName is omitted.
|
|
92
|
+
*/
|
|
93
|
+
export function getAgent(exchange, agentName) {
|
|
94
|
+
const settings = loadSettings();
|
|
95
|
+
const byExchange = settings.agents?.[exchange];
|
|
96
|
+
if (!byExchange)
|
|
97
|
+
return null;
|
|
98
|
+
if (agentName !== undefined) {
|
|
99
|
+
return byExchange[agentName] ?? null;
|
|
100
|
+
}
|
|
101
|
+
// Return first non-partial agent
|
|
102
|
+
for (const meta of Object.values(byExchange)) {
|
|
103
|
+
if (meta.status !== "partial") {
|
|
104
|
+
return meta;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Persist an agent meta entry for the given exchange.
|
|
111
|
+
* Acquires a lockfile before writing to prevent concurrent races (AC-14).
|
|
112
|
+
*
|
|
113
|
+
* Re-entrant: if the calling process already holds the lock (e.g. the approve
|
|
114
|
+
* flow acquired it at the start), this function writes directly without a
|
|
115
|
+
* double-acquire or release.
|
|
116
|
+
*/
|
|
117
|
+
export function setAgent(exchange, meta) {
|
|
118
|
+
const alreadyLocked = callerHoldsLock(exchange);
|
|
119
|
+
if (!alreadyLocked) {
|
|
120
|
+
acquireLock(exchange);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const settings = loadSettings();
|
|
124
|
+
if (!settings.agents) {
|
|
125
|
+
settings.agents = {};
|
|
126
|
+
}
|
|
127
|
+
const key = exchange;
|
|
128
|
+
if (!settings.agents[key]) {
|
|
129
|
+
settings.agents[exchange] = {};
|
|
130
|
+
}
|
|
131
|
+
settings.agents[exchange][meta.agentName] = meta;
|
|
132
|
+
saveSettings(settings);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
if (!alreadyLocked) {
|
|
136
|
+
releaseLock(exchange);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Delete an agent entry. Returns true if it existed and was removed, false if
|
|
142
|
+
* it was already absent (idempotent per AC-4).
|
|
143
|
+
*/
|
|
144
|
+
export function deleteAgent(exchange, agentName) {
|
|
145
|
+
const settings = loadSettings();
|
|
146
|
+
const byExchange = settings.agents?.[exchange];
|
|
147
|
+
if (!byExchange || !(agentName in byExchange)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
delete byExchange[agentName];
|
|
151
|
+
saveSettings(settings);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* List all registered agents, optionally filtered to a single exchange.
|
|
156
|
+
*/
|
|
157
|
+
export function listAgents(exchange) {
|
|
158
|
+
const settings = loadSettings();
|
|
159
|
+
if (!settings.agents)
|
|
160
|
+
return [];
|
|
161
|
+
const result = [];
|
|
162
|
+
for (const [ex, agents] of Object.entries(settings.agents)) {
|
|
163
|
+
if (exchange !== undefined && ex !== exchange)
|
|
164
|
+
continue;
|
|
165
|
+
if (agents && typeof agents === "object") {
|
|
166
|
+
for (const meta of Object.values(agents)) {
|
|
167
|
+
result.push({ exchange: ex, meta });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal HTTP helpers for the public DEX fetchers.
|
|
3
|
+
*
|
|
4
|
+
* SSOT rule #2 boilerplate consolidation: every public fetcher needed the
|
|
5
|
+
* same 5-line "throw on non-2xx with body excerpt" guard. Centralized here
|
|
6
|
+
* so each call site is one assertOk(...) line.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Throw if the Response is not OK, with HTTP status + (truncated) body
|
|
10
|
+
* excerpt for diagnostics.
|
|
11
|
+
*
|
|
12
|
+
* Use BEFORE `res.json()` so a 5xx JSON error body cannot fulfill as if it
|
|
13
|
+
* were valid data.
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertOk(res: Response, label: string): Promise<void>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal HTTP helpers for the public DEX fetchers.
|
|
3
|
+
*
|
|
4
|
+
* SSOT rule #2 boilerplate consolidation: every public fetcher needed the
|
|
5
|
+
* same 5-line "throw on non-2xx with body excerpt" guard. Centralized here
|
|
6
|
+
* so each call site is one assertOk(...) line.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Throw if the Response is not OK, with HTTP status + (truncated) body
|
|
10
|
+
* excerpt for diagnostics.
|
|
11
|
+
*
|
|
12
|
+
* Use BEFORE `res.json()` so a 5xx JSON error body cannot fulfill as if it
|
|
13
|
+
* were valid data.
|
|
14
|
+
*/
|
|
15
|
+
export async function assertOk(res, label) {
|
|
16
|
+
if (res.ok)
|
|
17
|
+
return;
|
|
18
|
+
let body = "";
|
|
19
|
+
try {
|
|
20
|
+
body = await res.text();
|
|
21
|
+
}
|
|
22
|
+
catch { /* body read failed; surface status alone */ }
|
|
23
|
+
throw new Error(`${label} returned HTTP ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`);
|
|
24
|
+
}
|
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
import { HYPERLIQUID_API_URL } from "./urls.js";
|
|
2
|
+
import { assertOk } from "./_http.js";
|
|
2
3
|
import { withCache, TTL_MARKET } from "../../cache.js";
|
|
3
4
|
// ── Internal ──
|
|
4
|
-
function hlPost(type) {
|
|
5
|
-
|
|
5
|
+
async function hlPost(type) {
|
|
6
|
+
// SSOT rule #2: a non-2xx response is a real failure, not "no rows" — see assertOk.
|
|
7
|
+
const res = await fetch(HYPERLIQUID_API_URL, {
|
|
6
8
|
method: "POST",
|
|
7
9
|
headers: { "Content-Type": "application/json" },
|
|
8
10
|
body: JSON.stringify({ type }),
|
|
9
|
-
})
|
|
11
|
+
});
|
|
12
|
+
await assertOk(res, `Hyperliquid info ${type}`);
|
|
13
|
+
return res.json();
|
|
10
14
|
}
|
|
11
15
|
// ── Fetchers ──
|
|
12
16
|
export function fetchHyperliquidMeta() {
|
|
17
|
+
// SSOT rule #2: error must propagate. Row-level skipping handles missing
|
|
18
|
+
// markPx / funding so a 0 doesn't reach downstream comparisons.
|
|
13
19
|
return withCache("pub:hl:metaAndAssetCtxs", TTL_MARKET, async () => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return [];
|
|
29
|
-
}
|
|
20
|
+
const json = await hlPost("metaAndAssetCtxs");
|
|
21
|
+
const universe = (json[0] ?? {}).universe ?? [];
|
|
22
|
+
const ctxs = (json[1] ?? []);
|
|
23
|
+
const out = [];
|
|
24
|
+
universe.forEach((asset, i) => {
|
|
25
|
+
const ctx = (ctxs[i] ?? {});
|
|
26
|
+
const symbol = String(asset.name ?? "");
|
|
27
|
+
const funding = Number(ctx.funding);
|
|
28
|
+
const markPx = Number(ctx.markPx);
|
|
29
|
+
if (!symbol || !Number.isFinite(funding) || !Number.isFinite(markPx) || markPx <= 0)
|
|
30
|
+
return;
|
|
31
|
+
out.push({ symbol, funding, markPx });
|
|
32
|
+
});
|
|
33
|
+
return out;
|
|
30
34
|
});
|
|
31
35
|
}
|
|
32
36
|
export function fetchHyperliquidMetaRaw() {
|
|
33
|
-
|
|
37
|
+
// SSOT rule #2: caller is responsible for error handling. Removed silent
|
|
38
|
+
// `.catch(() => null)` so a network failure surfaces as a rejected Promise.
|
|
39
|
+
// Callers should wrap with Promise.allSettled to keep multi-DEX comparisons
|
|
40
|
+
// running when one DEX is down.
|
|
41
|
+
return withCache("pub:hl:metaAndAssetCtxs:raw", TTL_MARKET, () => hlPost("metaAndAssetCtxs"));
|
|
34
42
|
}
|
|
35
43
|
export function parseHyperliquidMetaRaw(raw) {
|
|
36
44
|
const rates = new Map();
|
|
@@ -45,15 +53,19 @@ export function parseHyperliquidMetaRaw(raw) {
|
|
|
45
53
|
const sym = String(a.name ?? "");
|
|
46
54
|
if (!sym)
|
|
47
55
|
return;
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
56
|
+
// SSOT rule #2: skip rows missing a real funding rate; never publish 0.
|
|
57
|
+
const funding = Number(ctx.funding);
|
|
58
|
+
if (Number.isFinite(funding))
|
|
59
|
+
rates.set(sym, funding);
|
|
60
|
+
const mp = Number(ctx.markPx);
|
|
61
|
+
if (Number.isFinite(mp) && mp > 0)
|
|
51
62
|
prices.set(sym, mp);
|
|
52
63
|
});
|
|
53
64
|
return { rates, prices };
|
|
54
65
|
}
|
|
55
66
|
export function fetchHyperliquidAllMidsRaw() {
|
|
56
|
-
|
|
67
|
+
// SSOT rule #2: error must propagate; see fetchHyperliquidMetaRaw.
|
|
68
|
+
return withCache("pub:hl:allMids:raw", TTL_MARKET, () => hlPost("allMids"));
|
|
57
69
|
}
|
|
58
70
|
// ── Health check ──
|
|
59
71
|
export async function pingHyperliquid() {
|
|
@@ -7,7 +7,13 @@ export interface LighterFundingEntry {
|
|
|
7
7
|
marketId: number;
|
|
8
8
|
symbol: string;
|
|
9
9
|
rate: number;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Lighter's funding-rates endpoint sends mark_price intermittently —
|
|
12
|
+
* not every row carries it. Consumers must resolve the actual mark price
|
|
13
|
+
* by falling back to orderBookDetails.lastTradePrice when this is null.
|
|
14
|
+
* SSOT rule #2 lives at the CONSUMER (resolve or skip), not here.
|
|
15
|
+
*/
|
|
16
|
+
markPrice: number | null;
|
|
11
17
|
}
|
|
12
18
|
export declare function fetchLighterOrderBookDetails(): Promise<LighterMarketDetail[]>;
|
|
13
19
|
export declare function fetchLighterOrderBookDetailsRaw(): Promise<unknown>;
|
|
@@ -1,52 +1,85 @@
|
|
|
1
1
|
import { LIGHTER_API_URL } from "./urls.js";
|
|
2
|
+
import { assertOk } from "./_http.js";
|
|
2
3
|
import { withCache, TTL_MARKET } from "../../cache.js";
|
|
3
4
|
// ── Fetchers ──
|
|
4
5
|
export function fetchLighterOrderBookDetails() {
|
|
6
|
+
// SSOT rule #2: error must propagate; callers cannot distinguish "API
|
|
7
|
+
// returned no markets" from "API down" otherwise. Row-level skipping is
|
|
8
|
+
// applied for missing/invalid last_trade_price so we never publish a
|
|
9
|
+
// 0 price downstream.
|
|
5
10
|
return withCache("pub:lt:orderBookDetails", TTL_MARKET, async () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
const res = await fetch(`${LIGHTER_API_URL}/api/v1/orderBookDetails`);
|
|
12
|
+
await assertOk(res, "Lighter orderBookDetails");
|
|
13
|
+
const json = await res.json();
|
|
14
|
+
const details = (json.order_book_details ?? []);
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const m of details) {
|
|
17
|
+
const marketId = Number(m.market_id);
|
|
18
|
+
const symbol = String(m.symbol ?? "");
|
|
19
|
+
const lastTradePrice = Number(m.last_trade_price);
|
|
20
|
+
if (!Number.isFinite(marketId) || !symbol || !Number.isFinite(lastTradePrice) || lastTradePrice <= 0) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
out.push({ marketId, symbol, lastTradePrice });
|
|
18
24
|
}
|
|
25
|
+
return out;
|
|
19
26
|
});
|
|
20
27
|
}
|
|
21
28
|
export function fetchLighterOrderBookDetailsRaw() {
|
|
22
|
-
|
|
29
|
+
// SSOT rule #2: caller (arb / arb-auto) is responsible for error handling.
|
|
30
|
+
// Removed silent `.catch(() => null)` so a network failure surfaces as a
|
|
31
|
+
// rejected Promise instead of being indistinguishable from "API returned no
|
|
32
|
+
// markets". Non-2xx responses are also rejected so a 5xx JSON body cannot
|
|
33
|
+
// fulfill as if it were valid data.
|
|
34
|
+
return withCache("pub:lt:orderBookDetails:raw", TTL_MARKET, async () => {
|
|
35
|
+
const res = await fetch(`${LIGHTER_API_URL}/api/v1/orderBookDetails`);
|
|
36
|
+
await assertOk(res, "Lighter orderBookDetails");
|
|
37
|
+
return res.json();
|
|
38
|
+
});
|
|
23
39
|
}
|
|
24
40
|
export function fetchLighterFundingRates() {
|
|
41
|
+
// SSOT rule #2: error must propagate.
|
|
42
|
+
// Row-level: rate is required (it's the field the endpoint exists for); a
|
|
43
|
+
// missing rate means the row is malformed and we skip it. mark_price is
|
|
44
|
+
// documented as intermittent on Lighter's side — when missing, surface as
|
|
45
|
+
// null so the consumer can apply the orderBookDetails fallback (a
|
|
46
|
+
// documented price-source preference, NOT an error fallback).
|
|
25
47
|
return withCache("pub:lt:fundingRates", TTL_MARKET, async () => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
const res = await fetch(`${LIGHTER_API_URL}/api/v1/funding-rates`);
|
|
49
|
+
await assertOk(res, "Lighter funding-rates");
|
|
50
|
+
const json = await res.json();
|
|
51
|
+
const list = (json.funding_rates ?? []);
|
|
52
|
+
const entries = [];
|
|
53
|
+
for (const fr of list) {
|
|
54
|
+
if (String(fr.exchange ?? "").toLowerCase() !== "lighter")
|
|
55
|
+
continue;
|
|
56
|
+
const marketId = Number(fr.market_id);
|
|
57
|
+
// symbol is intentionally allowed to be empty — Lighter's funding-rates
|
|
58
|
+
// payload often omits symbol per row, and the consumer resolves it from
|
|
59
|
+
// orderBookDetails by marketId. Required: marketId + valid rate.
|
|
60
|
+
const symbol = String(fr.symbol ?? "");
|
|
61
|
+
const rateRaw = fr.rate ?? fr.funding_rate;
|
|
62
|
+
const rate = Number(rateRaw);
|
|
63
|
+
if (!Number.isFinite(marketId) || rateRaw === undefined || !Number.isFinite(rate)) {
|
|
64
|
+
continue;
|
|
40
65
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
66
|
+
const markPriceRaw = fr.mark_price;
|
|
67
|
+
const markPriceNumber = Number(markPriceRaw);
|
|
68
|
+
const markPrice = markPriceRaw !== undefined && Number.isFinite(markPriceNumber) && markPriceNumber > 0
|
|
69
|
+
? markPriceNumber
|
|
70
|
+
: null;
|
|
71
|
+
entries.push({ marketId, symbol, rate, markPrice });
|
|
45
72
|
}
|
|
73
|
+
return entries;
|
|
46
74
|
});
|
|
47
75
|
}
|
|
48
76
|
export function fetchLighterFundingRatesRaw() {
|
|
49
|
-
|
|
77
|
+
// SSOT rule #2: error must propagate; see fetchLighterOrderBookDetailsRaw.
|
|
78
|
+
return withCache("pub:lt:fundingRates:raw", TTL_MARKET, async () => {
|
|
79
|
+
const res = await fetch(`${LIGHTER_API_URL}/api/v1/funding-rates`);
|
|
80
|
+
await assertOk(res, "Lighter funding-rates");
|
|
81
|
+
return res.json();
|
|
82
|
+
});
|
|
50
83
|
}
|
|
51
84
|
export function parseLighterRaw(detailsRaw, fundingRaw) {
|
|
52
85
|
const rates = new Map();
|
|
@@ -57,9 +90,13 @@ export function parseLighterRaw(detailsRaw, fundingRaw) {
|
|
|
57
90
|
const details = (detailsRaw.order_book_details ?? []);
|
|
58
91
|
for (const m of details) {
|
|
59
92
|
const mid = Number(m.market_id);
|
|
93
|
+
if (!Number.isFinite(mid))
|
|
94
|
+
continue;
|
|
60
95
|
idToSym.set(mid, String(m.symbol ?? ""));
|
|
61
|
-
const p = Number(m.last_trade_price
|
|
62
|
-
|
|
96
|
+
const p = Number(m.last_trade_price);
|
|
97
|
+
// SSOT rule #2: only register prices that are real positive numbers;
|
|
98
|
+
// never silently default missing last_trade_price to 0.
|
|
99
|
+
if (Number.isFinite(p) && p > 0)
|
|
63
100
|
idToPrice.set(mid, p);
|
|
64
101
|
}
|
|
65
102
|
}
|
|
@@ -72,9 +109,20 @@ export function parseLighterRaw(detailsRaw, fundingRaw) {
|
|
|
72
109
|
const sym = String(fr.symbol ?? "") || idToSym.get(Number(fr.market_id)) || "";
|
|
73
110
|
if (!sym || rates.has(sym))
|
|
74
111
|
continue;
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
112
|
+
const rateRaw = fr.rate ?? fr.funding_rate;
|
|
113
|
+
const rate = Number(rateRaw);
|
|
114
|
+
// SSOT rule #2: skip rows whose funding rate is missing — never publish 0.
|
|
115
|
+
if (rateRaw === undefined || !Number.isFinite(rate))
|
|
116
|
+
continue;
|
|
117
|
+
rates.set(sym, rate);
|
|
118
|
+
// Prefer fr.mark_price; fall back to the orderBookDetails last-trade
|
|
119
|
+
// price as a documented price-source preference (NOT an error fallback).
|
|
120
|
+
const directMark = Number(fr.mark_price);
|
|
121
|
+
const fallbackMark = idToPrice.get(Number(fr.market_id));
|
|
122
|
+
const mp = Number.isFinite(directMark) && directMark > 0
|
|
123
|
+
? directMark
|
|
124
|
+
: (fallbackMark !== undefined && fallbackMark > 0 ? fallbackMark : undefined);
|
|
125
|
+
if (mp !== undefined)
|
|
78
126
|
prices.set(sym, mp);
|
|
79
127
|
}
|
|
80
128
|
}
|
|
@@ -1,28 +1,51 @@
|
|
|
1
1
|
import { PACIFICA_API_URL } from "./urls.js";
|
|
2
|
+
import { assertOk } from "./_http.js";
|
|
2
3
|
import { withCache, TTL_MARKET } from "../../cache.js";
|
|
3
4
|
// ── Fetchers ──
|
|
4
5
|
export function fetchPacificaPrices() {
|
|
6
|
+
// SSOT rule #2: error must propagate. Row-level skipping is applied for
|
|
7
|
+
// missing/invalid mark or funding so a 0 doesn't reach downstream paths.
|
|
5
8
|
return withCache("pub:pac:prices", TTL_MARKET, async () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return [];
|
|
12
|
-
return data.map((p) => ({
|
|
13
|
-
symbol: String(p.symbol ?? ""),
|
|
14
|
-
funding: Number(p.next_funding ?? p.funding ?? 0),
|
|
15
|
-
mark: Number(p.mark ?? 0),
|
|
16
|
-
nextFunding: p.next_funding ? Number(p.next_funding) : undefined,
|
|
17
|
-
}));
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
9
|
+
const res = await fetch(PACIFICA_API_URL);
|
|
10
|
+
await assertOk(res, "Pacifica prices");
|
|
11
|
+
const json = await res.json();
|
|
12
|
+
const data = json.data ?? json;
|
|
13
|
+
if (!Array.isArray(data))
|
|
20
14
|
return [];
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const p of data) {
|
|
17
|
+
const symbol = String(p.symbol ?? "");
|
|
18
|
+
const fundingRaw = p.next_funding ?? p.funding;
|
|
19
|
+
const funding = Number(fundingRaw);
|
|
20
|
+
const mark = Number(p.mark);
|
|
21
|
+
if (!symbol || fundingRaw === undefined || !Number.isFinite(funding) || !Number.isFinite(mark) || mark <= 0) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const nextFundingRaw = p.next_funding;
|
|
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
|
+
});
|
|
21
34
|
}
|
|
35
|
+
return out;
|
|
22
36
|
});
|
|
23
37
|
}
|
|
24
38
|
export function fetchPacificaPricesRaw() {
|
|
25
|
-
|
|
39
|
+
// SSOT rule #2: caller is responsible for error handling. Removed silent
|
|
40
|
+
// `.catch(() => null)` so a network failure surfaces as a rejected Promise.
|
|
41
|
+
// Callers should wrap with Promise.allSettled to keep multi-DEX comparisons
|
|
42
|
+
// running when one DEX is down. Non-2xx responses are also rejected so a
|
|
43
|
+
// 5xx JSON body cannot fulfill as if it were valid data.
|
|
44
|
+
return withCache("pub:pac:prices:raw", TTL_MARKET, async () => {
|
|
45
|
+
const res = await fetch(PACIFICA_API_URL);
|
|
46
|
+
await assertOk(res, "Pacifica prices");
|
|
47
|
+
return res.json();
|
|
48
|
+
});
|
|
26
49
|
}
|
|
27
50
|
export function parsePacificaRaw(raw) {
|
|
28
51
|
const rates = new Map();
|
|
@@ -34,9 +57,15 @@ export function parsePacificaRaw(raw) {
|
|
|
34
57
|
const sym = String(p.symbol ?? "");
|
|
35
58
|
if (!sym)
|
|
36
59
|
continue;
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
60
|
+
// SSOT rule #2: skip rows missing real funding/mark; never publish 0.
|
|
61
|
+
const fundingRaw = p.next_funding ?? p.funding;
|
|
62
|
+
const fundingNumber = Number(fundingRaw);
|
|
63
|
+
if (fundingRaw !== undefined && Number.isFinite(fundingNumber)) {
|
|
64
|
+
rates.set(sym, fundingNumber);
|
|
65
|
+
}
|
|
66
|
+
const markCandidates = [Number(p.mark), Number(p.price)];
|
|
67
|
+
const mark = markCandidates.find(v => Number.isFinite(v) && v > 0);
|
|
68
|
+
if (mark !== undefined)
|
|
40
69
|
prices.set(sym, mark);
|
|
41
70
|
}
|
|
42
71
|
return { rates, prices };
|