perp-cli 0.9.8 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/bot/engine.js +23 -4
- package/dist/bot/presets.js +3 -3
- package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
- package/dist/bot/strategies/funding-arb-strategy.js +41 -176
- package/dist/bot/strategies/funding-auto.js +2 -2
- 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 +20 -16
- 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 +1122 -176
- package/dist/config.d.ts +9 -0
- package/dist/config.js +55 -9
- 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 +88 -34
- package/dist/exchanges/lighter-spot.js +19 -5
- package/dist/exchanges/lighter.d.ts +66 -0
- package/dist/exchanges/lighter.js +213 -31
- 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/contracts.d.ts +14 -0
- package/dist/guardrail/contracts.js +72 -0
- package/dist/guardrail/perp-guardrail.d.ts +21 -0
- package/dist/guardrail/perp-guardrail.js +145 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +307 -362
- package/dist/jobs.js +3 -1
- package/dist/mcp-server.js +92 -88
- package/dist/position-history.js +47 -4
- package/dist/settings.d.ts +69 -3
- package/dist/settings.js +3 -2
- package/dist/signer/index.d.ts +2 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/ows-evm.d.ts +32 -0
- package/dist/signer/ows-evm.js +86 -0
- package/dist/signer/ows-loader.d.ts +2 -0
- package/dist/signer/ows-loader.js +6 -0
- package/dist/signer/ows-solana.d.ts +25 -0
- package/dist/signer/ows-solana.js +56 -0
- 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 +4 -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/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
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
import { LocalEvmSigner } from "../signer/index.js";
|
|
2
|
+
import { isExpired } from "../agent-wallet/expiry.js";
|
|
3
|
+
import { PerpError } from "../errors.js";
|
|
4
|
+
/** Lighter trigger orderType enum (matches WASM signer). */
|
|
5
|
+
export const LIGHTER_ORDER_TYPE = {
|
|
6
|
+
STOP_LOSS: 2,
|
|
7
|
+
STOP_LOSS_LIMIT: 3,
|
|
8
|
+
TAKE_PROFIT: 4,
|
|
9
|
+
TAKE_PROFIT_LIMIT: 5,
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Pick Lighter trigger orderType from side, trigger price, and current mark.
|
|
13
|
+
*
|
|
14
|
+
* STOP_LOSS protects against adverse moves; TAKE_PROFIT locks in favorable moves.
|
|
15
|
+
* buy + trigger >= mark → STOP_LOSS (close short on rise / breakout entry)
|
|
16
|
+
* buy + trigger < mark → TAKE_PROFIT (close short on fall)
|
|
17
|
+
* sell + trigger <= mark → STOP_LOSS (close long on fall)
|
|
18
|
+
* sell + trigger > mark → TAKE_PROFIT (close long on rise)
|
|
19
|
+
*
|
|
20
|
+
* Tied trigger==mark is classified as STOP_LOSS — the protective default.
|
|
21
|
+
*/
|
|
22
|
+
export function classifyTriggerOrderType(side, triggerPrice, markPrice, isMarket) {
|
|
23
|
+
const isStopLoss = side === "buy" ? triggerPrice >= markPrice : triggerPrice <= markPrice;
|
|
24
|
+
if (isStopLoss)
|
|
25
|
+
return isMarket ? LIGHTER_ORDER_TYPE.STOP_LOSS : LIGHTER_ORDER_TYPE.STOP_LOSS_LIMIT;
|
|
26
|
+
return isMarket ? LIGHTER_ORDER_TYPE.TAKE_PROFIT : LIGHTER_ORDER_TYPE.TAKE_PROFIT_LIMIT;
|
|
27
|
+
}
|
|
2
28
|
export class LighterAdapter {
|
|
3
29
|
name = "lighter";
|
|
4
30
|
chain = "ethereum";
|
|
@@ -21,6 +47,16 @@ export class LighterAdapter {
|
|
|
21
47
|
_marketsCache = null;
|
|
22
48
|
_marketsCacheTime = 0;
|
|
23
49
|
static CACHE_TTL = 30_000;
|
|
50
|
+
// ── 3-tier signer state (Phase 2d) ──
|
|
51
|
+
/** Tier 1 metadata (Phase 2d). When set + not expired + !_useNoAgent, agent
|
|
52
|
+
* apiKeyIndex/publicKey/accountIndex take priority over master/PK env. */
|
|
53
|
+
_agentMeta;
|
|
54
|
+
/** Tier 1 secp256k1 hex private key (40 bytes) for the agent slot. Lighter
|
|
55
|
+
* has no OWS curve binding for raw L2 keys; this is held in-memory only when
|
|
56
|
+
* passed by the caller (e.g. immediately after `setupApiKey()` or via env). */
|
|
57
|
+
_agentApiKey;
|
|
58
|
+
/** --no-agent bypass flag. */
|
|
59
|
+
_useNoAgent = false;
|
|
24
60
|
/**
|
|
25
61
|
* @param evmKey EVM private key (0x-prefixed, 32 bytes) — for deposits & key registration
|
|
26
62
|
* @param testnet Use testnet (chain ID 300) instead of mainnet (chain ID 304)
|
|
@@ -28,9 +64,12 @@ export class LighterAdapter {
|
|
|
28
64
|
*/
|
|
29
65
|
constructor(evmKey, testnet = false, opts) {
|
|
30
66
|
this._evmKey = evmKey;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
67
|
+
// SSOT Rule #3: L2 API key is no longer read from env. Caller must inject
|
|
68
|
+
// via opts.apiKey (Tier 1 agent path uses setAgentSigner; auto-setup at
|
|
69
|
+
// init() runs ChangePubKey when apiKey is empty AND we have an EVM key).
|
|
70
|
+
this._apiKey = opts?.apiKey ?? "";
|
|
71
|
+
this._accountIndexInit = opts?.accountIndex ?? -1;
|
|
72
|
+
this._apiKeyIndex = opts?.apiKeyIndex ?? 4;
|
|
34
73
|
this._address = "";
|
|
35
74
|
this._testnet = testnet;
|
|
36
75
|
this._readOnly = !this._apiKey;
|
|
@@ -64,19 +103,120 @@ export class LighterAdapter {
|
|
|
64
103
|
/** Inject an external EVM signer. Call before init() to skip LocalEvmSigner creation. */
|
|
65
104
|
setSigner(signer) {
|
|
66
105
|
this._evmSigner = signer;
|
|
106
|
+
this._address = signer.getAddress();
|
|
107
|
+
// Reset any caller-supplied accountIndex hint: when the agent flow injects
|
|
108
|
+
// an OWS signer that differs from the constructor-time master, force a
|
|
109
|
+
// fresh /api/v1/account lookup keyed on the injected signer. Without this
|
|
110
|
+
// reset, a stale account index would route ChangePubKey to the wrong
|
|
111
|
+
// account (Lighter rejects the L1 sig with code=21504 "fail to l1 signature").
|
|
112
|
+
this._accountIndexInit = -1;
|
|
113
|
+
this._accountIndex = -1;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Tier 1: inject a Lighter agent's slot+L2 key (Phase 2d).
|
|
117
|
+
*
|
|
118
|
+
* Caller supplies the AgentMeta (settings.agents.lighter[name]) plus the
|
|
119
|
+
* 40-byte L2 private key (hex). Adapter swaps `_apiKey`/`_apiKeyIndex` to
|
|
120
|
+
* the agent's slot/key so all subsequent `signCreateOrder`/`signCancelOrder`
|
|
121
|
+
* etc. flow through the agent identity.
|
|
122
|
+
*
|
|
123
|
+
* NOTE: when the agent path is active, `_address` (master EVM) is left
|
|
124
|
+
* unchanged because Lighter REST queries (account, balance, positions) use
|
|
125
|
+
* `account_index` not address. The L1 EVM identity is irrelevant for the
|
|
126
|
+
* hot-path; only the L2 secp256k1 key is consulted.
|
|
127
|
+
*/
|
|
128
|
+
setAgentSigner(meta, agentApiKey) {
|
|
129
|
+
this._agentMeta = meta;
|
|
130
|
+
this._agentApiKey = agentApiKey;
|
|
131
|
+
// Reset any prior accountIndex hint: the agent may belong to a different
|
|
132
|
+
// Lighter account than the master signer, so any stale value must not
|
|
133
|
+
// override the AgentMeta-derived accountIndex applied in init().
|
|
134
|
+
this._accountIndexInit = -1;
|
|
135
|
+
this._accountIndex = -1;
|
|
136
|
+
// Active slot+key swap happens lazily in `_resolveSigner()`/init flows.
|
|
137
|
+
}
|
|
138
|
+
/** Bypass Tier 1 (agent) — used for `--no-agent` flag. */
|
|
139
|
+
setNoAgent(noAgent) {
|
|
140
|
+
this._useNoAgent = noAgent;
|
|
141
|
+
}
|
|
142
|
+
/** Which signer tier is currently active (for debug/diagnostics). */
|
|
143
|
+
get activeSignerTier() {
|
|
144
|
+
try {
|
|
145
|
+
return this._resolveSigner().tier;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Resolve the active signer tier across three tiers (Phase 2d).
|
|
153
|
+
*
|
|
154
|
+
* Tier 1 (agent): active when AgentMeta + agent L2 key set, not expired,
|
|
155
|
+
* and `--no-agent` NOT set. The actual swap of `_apiKeyIndex`/`_apiKey`
|
|
156
|
+
* happens in `init()` (or via `_activateAgentTier()` post-init).
|
|
157
|
+
* Tier 2 (master): active when an OWS master EVM signer was injected via
|
|
158
|
+
* `setSigner()`. Used for the L1 ChangePubKey path (auto-setup at slot 4).
|
|
159
|
+
* Tier 3 (pk): active when constructor was supplied an evmKey. Same role
|
|
160
|
+
* as Tier 2 — signs L1 ChangePubKey for auto-setup.
|
|
161
|
+
*
|
|
162
|
+
* Lighter is unique among the 4 DEXs in that the actual order-signing is
|
|
163
|
+
* always done by the L2 secp256k1 key (`_apiKey`), regardless of tier. The
|
|
164
|
+
* tiers differ in WHICH L2 key is loaded: Tier 1 uses the agent's slot/key
|
|
165
|
+
* from AgentMeta; Tier 2/3 use whatever the env/auto-setup produced.
|
|
166
|
+
*/
|
|
167
|
+
_resolveSigner() {
|
|
168
|
+
if (!this._useNoAgent && this._agentMeta && this._agentApiKey) {
|
|
169
|
+
if (isExpired(this._agentMeta)) {
|
|
170
|
+
// Skip Tier 1 only if Tier 2/3 available; otherwise throw AGENT_EXPIRED
|
|
171
|
+
if (!this._evmSigner && !this._evmKey) {
|
|
172
|
+
throw new PerpError("AGENT_EXPIRED", "Agent wallet has expired", {
|
|
173
|
+
remediation: "perp wallet agent approve lighter --rotate",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// fall through
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
return { tier: "agent" };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (this._evmSigner)
|
|
183
|
+
return { tier: "master" };
|
|
184
|
+
if (this._evmKey)
|
|
185
|
+
return { tier: "pk" };
|
|
186
|
+
throw new PerpError("NO_SIGNER_AVAILABLE", "No signing path configured for Lighter", {
|
|
187
|
+
remediation: "Run one of: (a) perp wallet agent approve lighter --master <wallet>; (b) perp wallet generate && perp -e lighter --ows <wallet>; (c) export LIGHTER_PRIVATE_KEY=0x...",
|
|
188
|
+
});
|
|
67
189
|
}
|
|
68
190
|
async init() {
|
|
191
|
+
// Phase 2d: when an agent is registered (Tier 1), swap _apiKey/_apiKeyIndex
|
|
192
|
+
// to the agent's slot/key BEFORE any of the existing init logic runs. This
|
|
193
|
+
// ensures the WASM client below is created with the agent's credentials.
|
|
194
|
+
// The agent L2 key is loaded from the encrypted keystore by the caller and
|
|
195
|
+
// injected via setAgentSigner() — never read from env (SSOT Rule #3).
|
|
196
|
+
if (!this._useNoAgent && this._agentMeta && this._agentApiKey && !isExpired(this._agentMeta)) {
|
|
197
|
+
this._apiKey = this._agentApiKey;
|
|
198
|
+
if (typeof this._agentMeta.apiKeyIndex === "number") {
|
|
199
|
+
this._apiKeyIndex = this._agentMeta.apiKeyIndex;
|
|
200
|
+
}
|
|
201
|
+
if (typeof this._agentMeta.accountIndex === "number") {
|
|
202
|
+
this._accountIndex = this._agentMeta.accountIndex;
|
|
203
|
+
this._accountIndexInit = this._agentMeta.accountIndex;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
69
206
|
// Initialize EVM signer if not externally injected (skip if no key — read-only mode)
|
|
70
207
|
if (!this._evmSigner && this._evmKey) {
|
|
71
208
|
this._evmSigner = await LocalEvmSigner.create(this._evmKey);
|
|
72
209
|
this._address = this._evmSigner.getAddress();
|
|
73
210
|
}
|
|
211
|
+
else if (this._evmSigner && !this._address) {
|
|
212
|
+
this._address = this._evmSigner.getAddress();
|
|
213
|
+
}
|
|
74
214
|
// Fetch account index from REST API (skip if no address — read-only mode)
|
|
75
215
|
if (!this._address) {
|
|
76
216
|
await this._refreshMarketMap();
|
|
77
217
|
return;
|
|
78
218
|
}
|
|
79
|
-
// If accountIndex is already known (from env/opts), skip API lookup entirely
|
|
219
|
+
// If accountIndex is already known (from env/opts/agent meta), skip API lookup entirely
|
|
80
220
|
if (this._accountIndexInit >= 0) {
|
|
81
221
|
this._accountIndex = this._accountIndexInit;
|
|
82
222
|
}
|
|
@@ -114,19 +254,18 @@ export class LighterAdapter {
|
|
|
114
254
|
// setupApiKey already configured the static WASM client — reuse it
|
|
115
255
|
this._signer = LighterAdapter._wasmClient;
|
|
116
256
|
signerReady = true;
|
|
117
|
-
//
|
|
257
|
+
// SSOT Rule #3: persist L2 key to encrypted keystore so subsequent
|
|
258
|
+
// process invocations can reload it without re-running ChangePubKey.
|
|
118
259
|
try {
|
|
119
|
-
const {
|
|
120
|
-
|
|
121
|
-
setEnvVar("LIGHTER_ACCOUNT_INDEX", String(this._accountIndex));
|
|
122
|
-
setEnvVar("LIGHTER_API_KEY_INDEX", String(autoKeyIndex));
|
|
260
|
+
const { saveLighterKey } = await import("../agent-wallet/lighter-keystore.js");
|
|
261
|
+
saveLighterKey(this._accountIndex, autoKeyIndex, apiKey, "");
|
|
123
262
|
}
|
|
124
|
-
catch { /* non-critical —
|
|
263
|
+
catch { /* non-critical — keystore save may fail in some contexts */ }
|
|
125
264
|
}
|
|
126
265
|
catch (e) {
|
|
127
266
|
// Auto-setup failed — log the error and continue in read-only mode
|
|
128
267
|
const msg = e instanceof Error ? e.message : String(e);
|
|
129
|
-
console.error(`[lighter] API key auto-setup failed: ${msg}. Trading will be read-only. Run 'perp
|
|
268
|
+
console.error(`[lighter] API key auto-setup failed: ${msg}. Trading will be read-only. Run 'perp wallet agent approve lighter' to register an agent.`);
|
|
130
269
|
}
|
|
131
270
|
}
|
|
132
271
|
// Initialize signer for trading if we have an API key (reuse singleton WASM client)
|
|
@@ -136,7 +275,7 @@ export class LighterAdapter {
|
|
|
136
275
|
if (cleanKey.startsWith("0x"))
|
|
137
276
|
cleanKey = cleanKey.slice(2);
|
|
138
277
|
if (cleanKey.length > 0 && !/^[0-9a-fA-F]+$/.test(cleanKey)) {
|
|
139
|
-
console.error(`[lighter] API key contains invalid chars (expected hex).
|
|
278
|
+
console.error(`[lighter] API key contains invalid chars (expected hex). Run 'perp wallet agent approve lighter' to register an agent.`);
|
|
140
279
|
this._apiKey = "";
|
|
141
280
|
}
|
|
142
281
|
else if (cleanKey.length === 0) {
|
|
@@ -144,7 +283,7 @@ export class LighterAdapter {
|
|
|
144
283
|
}
|
|
145
284
|
else if (this._accountIndex < 0) {
|
|
146
285
|
// Account lookup failed or returned no accounts — cannot initialize WASM signer
|
|
147
|
-
console.error(`[lighter] Account index not available (got ${this._accountIndex}). Trading will be read-only. Ensure the wallet has a Lighter account or
|
|
286
|
+
console.error(`[lighter] Account index not available (got ${this._accountIndex}). Trading will be read-only. Ensure the wallet has a Lighter account, or run 'perp wallet agent approve lighter' to register an agent.`);
|
|
148
287
|
this._apiKey = "";
|
|
149
288
|
}
|
|
150
289
|
else {
|
|
@@ -280,9 +419,10 @@ export class LighterAdapter {
|
|
|
280
419
|
const maxLev = imf > 0 ? Math.floor(10000 / imf) : 50;
|
|
281
420
|
const fr = fundingRates.get(d.symbol);
|
|
282
421
|
const dec = this._marketDecimals.get(d.symbol.toUpperCase());
|
|
422
|
+
const fundingMark = fr && Number(fr.markPrice) > 0 ? fr.markPrice : null;
|
|
283
423
|
markets.push({
|
|
284
424
|
symbol: d.symbol,
|
|
285
|
-
markPrice:
|
|
425
|
+
markPrice: fundingMark ?? String(d.last_trade_price ?? 0),
|
|
286
426
|
indexPrice: String(d.last_trade_price ?? 0),
|
|
287
427
|
fundingRate: fr?.rate ?? null,
|
|
288
428
|
volume24h: String(d.daily_quote_token_volume ?? 0),
|
|
@@ -616,28 +756,29 @@ export class LighterAdapter {
|
|
|
616
756
|
const marketIndex = this.getMarketIndex(symbol);
|
|
617
757
|
const { baseAmount } = this.toTicks(symbol, parseFloat(size), 0);
|
|
618
758
|
const { priceTicks: triggerTicks } = this.toTicks(symbol, 0, parseFloat(triggerPrice));
|
|
619
|
-
|
|
759
|
+
const triggerNum = parseFloat(triggerPrice);
|
|
760
|
+
const markPrice = await this.getMarkPrice(symbol);
|
|
620
761
|
const isMarket = !opts?.limitPrice;
|
|
621
762
|
let priceTicks;
|
|
622
763
|
if (isMarket) {
|
|
623
|
-
const markPrice = await this.getMarkPrice(symbol);
|
|
624
764
|
const slippagePrice = side === "buy" ? markPrice * 2 : markPrice * 0.5;
|
|
625
765
|
priceTicks = this.toTicks(symbol, 0, slippagePrice).priceTicks;
|
|
626
766
|
}
|
|
627
767
|
else {
|
|
628
768
|
priceTicks = this.toTicks(symbol, 0, parseFloat(opts.limitPrice)).priceTicks;
|
|
629
769
|
}
|
|
770
|
+
const orderType = classifyTriggerOrderType(side, triggerNum, markPrice, isMarket);
|
|
630
771
|
const signed = await this.signOrder({
|
|
631
772
|
marketIndex,
|
|
632
773
|
clientOrderIndex: this.nextClientOrderIndex(),
|
|
633
774
|
baseAmount,
|
|
634
775
|
price: Math.max(priceTicks, 1),
|
|
635
776
|
isAsk: side === "sell" ? 1 : 0,
|
|
636
|
-
orderType
|
|
777
|
+
orderType,
|
|
637
778
|
timeInForce: isMarket ? 0 : 1, // IOC for market, GTT for limit
|
|
638
779
|
reduceOnly: opts?.reduceOnly ? 1 : 0,
|
|
639
780
|
triggerPrice: triggerTicks,
|
|
640
|
-
orderExpiry:
|
|
781
|
+
orderExpiry: -1, // matches Lighter SDK DEFAULT_28_DAY_ORDER_EXPIRY for trigger orders
|
|
641
782
|
nonce,
|
|
642
783
|
});
|
|
643
784
|
return this.sendTx(signed);
|
|
@@ -961,8 +1102,11 @@ export class LighterAdapter {
|
|
|
961
1102
|
is_external_deposit: "true",
|
|
962
1103
|
}),
|
|
963
1104
|
});
|
|
964
|
-
if (!res.ok)
|
|
965
|
-
|
|
1105
|
+
if (!res.ok) {
|
|
1106
|
+
const text = await res.text();
|
|
1107
|
+
const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
1108
|
+
throw new Error(`createIntentAddress failed: ${clean}`);
|
|
1109
|
+
}
|
|
966
1110
|
return res.json();
|
|
967
1111
|
}
|
|
968
1112
|
/**
|
|
@@ -989,7 +1133,15 @@ export class LighterAdapter {
|
|
|
989
1133
|
account_index: String(this._accountIndex),
|
|
990
1134
|
api_key_index: String(this._apiKeyIndex),
|
|
991
1135
|
});
|
|
992
|
-
|
|
1136
|
+
const nonce = res.nonce ?? res.next_nonce;
|
|
1137
|
+
// SSOT rule #2: refuse to silently default to nonce 0. A stale or
|
|
1138
|
+
// missing nonce will cause Lighter to reject the tx with cryptic errors
|
|
1139
|
+
// and the user has no way to diagnose the upstream failure.
|
|
1140
|
+
if (nonce === undefined || !Number.isFinite(nonce) || nonce < 0) {
|
|
1141
|
+
throw new Error(`Lighter /nextNonce returned no valid nonce for account_index=${this._accountIndex} ` +
|
|
1142
|
+
`api_key_index=${this._apiKeyIndex}; raw response=${JSON.stringify(res)}`);
|
|
1143
|
+
}
|
|
1144
|
+
return nonce;
|
|
993
1145
|
}
|
|
994
1146
|
async signOrder(params) {
|
|
995
1147
|
try {
|
|
@@ -1017,11 +1169,18 @@ export class LighterAdapter {
|
|
|
1017
1169
|
throw new Error(`Signer error: ${signed.error}`);
|
|
1018
1170
|
if (!signed.txInfo)
|
|
1019
1171
|
throw new Error("Signer returned empty txInfo");
|
|
1172
|
+
// SSOT rule #2: refuse to silently default txType to 0 — different tx
|
|
1173
|
+
// types (createOrder=14, cancelOrder=15, updateLeverage=20, etc.) need
|
|
1174
|
+
// different routing on Lighter's side, and a 0 either rejects with a
|
|
1175
|
+
// generic error or silently misroutes.
|
|
1176
|
+
if (signed.txType === undefined || !Number.isFinite(signed.txType)) {
|
|
1177
|
+
throw new Error(`Lighter signer returned no txType (received ${signed.txType}); refusing to send with default 0.`);
|
|
1178
|
+
}
|
|
1020
1179
|
const res = await fetch(`${this._baseUrl}/api/v1/sendTx`, {
|
|
1021
1180
|
method: "POST",
|
|
1022
1181
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1023
1182
|
body: new URLSearchParams({
|
|
1024
|
-
tx_type: String(signed.txType
|
|
1183
|
+
tx_type: String(signed.txType),
|
|
1025
1184
|
tx_info: signed.txInfo,
|
|
1026
1185
|
}),
|
|
1027
1186
|
});
|
|
@@ -1035,8 +1194,11 @@ export class LighterAdapter {
|
|
|
1035
1194
|
for (const [k, v] of Object.entries(params))
|
|
1036
1195
|
url.searchParams.set(k, v);
|
|
1037
1196
|
const res = await fetch(url.toString());
|
|
1038
|
-
if (!res.ok)
|
|
1039
|
-
|
|
1197
|
+
if (!res.ok) {
|
|
1198
|
+
const text = await res.text();
|
|
1199
|
+
const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
1200
|
+
throw new Error(`GET ${path} failed (${res.status}): ${clean}`);
|
|
1201
|
+
}
|
|
1040
1202
|
return res.json();
|
|
1041
1203
|
}
|
|
1042
1204
|
/** Generate a unique client order index (uint48). */
|
|
@@ -1046,8 +1208,7 @@ export class LighterAdapter {
|
|
|
1046
1208
|
}
|
|
1047
1209
|
ensureSigner() {
|
|
1048
1210
|
if (this._readOnly || this._accountIndex < 0 || !this._signer) {
|
|
1049
|
-
throw new Error("This command requires a Lighter API key. Run `perp
|
|
1050
|
-
"then set LIGHTER_API_KEY in your .env");
|
|
1211
|
+
throw new Error("This command requires a Lighter API key. Run `perp wallet agent approve lighter` to register an agent.");
|
|
1051
1212
|
}
|
|
1052
1213
|
}
|
|
1053
1214
|
static _wasmClient = null;
|
|
@@ -1098,16 +1259,22 @@ export class LighterAdapter {
|
|
|
1098
1259
|
throw new Error("No EVM private key configured. Run: perp setup");
|
|
1099
1260
|
}
|
|
1100
1261
|
if (this._accountIndex < 0) {
|
|
1101
|
-
throw new Error("Account index not available. Call init() first
|
|
1262
|
+
throw new Error("Account index not available. Call init() first.");
|
|
1102
1263
|
}
|
|
1103
1264
|
// 1. Generate key pair
|
|
1104
1265
|
const { privateKey, publicKey } = await LighterAdapter.generateApiKey();
|
|
1105
1266
|
// 2. Get nonce from API
|
|
1267
|
+
// SSOT rule #2: refuse to silently default to nonce 0 — see getNextNonce.
|
|
1106
1268
|
const nonceRes = await this.restGet("/nextNonce", {
|
|
1107
1269
|
account_index: String(this._accountIndex),
|
|
1108
1270
|
api_key_index: String(apiKeyIndex),
|
|
1109
1271
|
});
|
|
1110
|
-
const nonce = nonceRes.nonce ?? nonceRes.next_nonce
|
|
1272
|
+
const nonce = nonceRes.nonce ?? nonceRes.next_nonce;
|
|
1273
|
+
if (nonce === undefined || !Number.isFinite(nonce) || nonce < 0) {
|
|
1274
|
+
throw new Error(`Lighter /nextNonce returned no valid nonce for ChangePubKey ` +
|
|
1275
|
+
`(account_index=${this._accountIndex} api_key_index=${apiKeyIndex}); ` +
|
|
1276
|
+
`raw response=${JSON.stringify(nonceRes)}`);
|
|
1277
|
+
}
|
|
1111
1278
|
// 3. Create signer client with new key and sign ChangePubKey
|
|
1112
1279
|
const client = await LighterAdapter.getWasmClient();
|
|
1113
1280
|
await client.createClient({
|
|
@@ -1135,11 +1302,15 @@ export class LighterAdapter {
|
|
|
1135
1302
|
const txInfo = JSON.parse(signed.txInfo);
|
|
1136
1303
|
txInfo.L1Sig = l1Sig;
|
|
1137
1304
|
// 6. Submit to Lighter API
|
|
1305
|
+
// SSOT rule #2: refuse to silently default txType to 0 — see sendTx.
|
|
1306
|
+
if (signed.txType === undefined || !Number.isFinite(signed.txType)) {
|
|
1307
|
+
throw new Error(`Lighter signChangePubKey returned no txType (received ${signed.txType}); refusing to send with default 0.`);
|
|
1308
|
+
}
|
|
1138
1309
|
const sendRes = await fetch(`${this._baseUrl}/api/v1/sendTx`, {
|
|
1139
1310
|
method: "POST",
|
|
1140
1311
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1141
1312
|
body: new URLSearchParams({
|
|
1142
|
-
tx_type: String(signed.txType
|
|
1313
|
+
tx_type: String(signed.txType),
|
|
1143
1314
|
tx_info: JSON.stringify(txInfo),
|
|
1144
1315
|
}),
|
|
1145
1316
|
});
|
|
@@ -1160,7 +1331,14 @@ export class LighterAdapter {
|
|
|
1160
1331
|
account_index: String(this._accountIndex),
|
|
1161
1332
|
api_key_index: String(apiKeyIndex),
|
|
1162
1333
|
});
|
|
1163
|
-
const
|
|
1334
|
+
const freshNonceCandidate = freshNonceRes.nonce ?? freshNonceRes.next_nonce;
|
|
1335
|
+
if (freshNonceCandidate === undefined || !Number.isFinite(freshNonceCandidate) || freshNonceCandidate < 0) {
|
|
1336
|
+
// SSOT rule #2: refuse to silently fall back to the original nonce.
|
|
1337
|
+
// Skip the retry attempt and surface the upstream nonce-fetch failure.
|
|
1338
|
+
lastError = `fresh /nextNonce returned no valid nonce (raw=${JSON.stringify(freshNonceRes)})`;
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
const freshNonce = freshNonceCandidate + attempt;
|
|
1164
1342
|
const retrySigned = await client.signChangePubKey({
|
|
1165
1343
|
pubkey: publicKey, nonce: freshNonce, apiKeyIndex, accountIndex: this._accountIndex,
|
|
1166
1344
|
});
|
|
@@ -1168,13 +1346,17 @@ export class LighterAdapter {
|
|
|
1168
1346
|
lastError = retrySigned.error ?? "incomplete response";
|
|
1169
1347
|
continue;
|
|
1170
1348
|
}
|
|
1349
|
+
if (retrySigned.txType === undefined || !Number.isFinite(retrySigned.txType)) {
|
|
1350
|
+
lastError = `signChangePubKey retry returned no txType (received ${retrySigned.txType})`;
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1171
1353
|
const retryTxInfo = JSON.parse(retrySigned.txInfo);
|
|
1172
1354
|
retryTxInfo.L1Sig = await this._evmSigner.signMessage(retrySigned.messageToSign);
|
|
1173
1355
|
const retryRes = await fetch(`${this._baseUrl}/api/v1/sendTx`, {
|
|
1174
1356
|
method: "POST",
|
|
1175
1357
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1176
1358
|
body: new URLSearchParams({
|
|
1177
|
-
tx_type: String(retrySigned.txType
|
|
1359
|
+
tx_type: String(retrySigned.txType),
|
|
1178
1360
|
tx_info: JSON.stringify(retryTxInfo),
|
|
1179
1361
|
}),
|
|
1180
1362
|
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacifica agent-wallet signing builders (Phase 2c).
|
|
3
|
+
*
|
|
4
|
+
* Pacifica auth model — off-chain signed messages over Solana Ed25519.
|
|
5
|
+
* Reference: https://pacifica.gitbook.io/docs/api-documentation/api/signing.md
|
|
6
|
+
*
|
|
7
|
+
* Canonical JSON rules (from Pacifica signing spec + production reference
|
|
8
|
+
* `src/pacifica/signing.ts:prepareMessage()`):
|
|
9
|
+
* 1. Recursively sort all object keys alphabetically.
|
|
10
|
+
* 2. Compact JSON (`JSON.stringify(value)` — no whitespace).
|
|
11
|
+
* 3. Outer wrapper has the shape `{ <header fields>, "data": <payload> }`
|
|
12
|
+
* where header is `{ type, timestamp, expiry_window }`. The `data` key
|
|
13
|
+
* is sorted lexicographically alongside the header fields.
|
|
14
|
+
*
|
|
15
|
+
* The agent bind/unbind action types are documented in HypurrQuant_FE
|
|
16
|
+
* `PacificaPerpAdapter.ts` (production reference for HypurrQuant_FE; see
|
|
17
|
+
* project memory `hypurrquant_fe_reference.md`). We mirror their `bind_agent_wallet`
|
|
18
|
+
* and `unbind_agent_wallet` operation types here.
|
|
19
|
+
*
|
|
20
|
+
* Pure functions — no side effects, no fs/fetch/env reads.
|
|
21
|
+
*/
|
|
22
|
+
/** Default expiry window for signed Pacifica REST requests (ms). */
|
|
23
|
+
export declare const DEFAULT_EXPIRY_WINDOW = 5000;
|
|
24
|
+
export interface BindAgentMessageParams {
|
|
25
|
+
/** Master Solana base58 public key. */
|
|
26
|
+
account: string;
|
|
27
|
+
/** Agent Solana base58 public key. */
|
|
28
|
+
agentWallet: string;
|
|
29
|
+
/** Optional ms-epoch timestamp; defaults to `Date.now()`. */
|
|
30
|
+
timestamp?: number;
|
|
31
|
+
/** Optional expiry window (ms); defaults to DEFAULT_EXPIRY_WINDOW. */
|
|
32
|
+
expiryWindow?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface UnbindAgentMessageParams {
|
|
35
|
+
/** Master Solana base58 public key. */
|
|
36
|
+
account: string;
|
|
37
|
+
/**
|
|
38
|
+
* Agent Solana base58 public key to unbind. Pass an empty string to
|
|
39
|
+
* unbind all agent wallets per Pacifica convention.
|
|
40
|
+
*/
|
|
41
|
+
agentWallet: string;
|
|
42
|
+
timestamp?: number;
|
|
43
|
+
expiryWindow?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface BuiltMessage {
|
|
46
|
+
/** Canonical JSON string ready for Ed25519 signing. */
|
|
47
|
+
canonicalJson: string;
|
|
48
|
+
/** Header captured for use in the REST envelope. */
|
|
49
|
+
header: {
|
|
50
|
+
type: string;
|
|
51
|
+
timestamp: number;
|
|
52
|
+
expiry_window: number;
|
|
53
|
+
};
|
|
54
|
+
/** Original payload object (caller supplies in REST envelope alongside signature). */
|
|
55
|
+
payload: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build the canonical JSON for `bind_agent_wallet`.
|
|
59
|
+
*
|
|
60
|
+
* Wire shape (after sorting):
|
|
61
|
+
* {"data":{"agent_wallet":"<base58>"},"expiry_window":5000,"timestamp":...,"type":"bind_agent_wallet"}
|
|
62
|
+
*
|
|
63
|
+
* The `account` field is supplied separately in the REST envelope; only the
|
|
64
|
+
* `agent_wallet` payload field is included inside the signed `data` block.
|
|
65
|
+
* This mirrors `buildSignedRequest()` in `src/pacifica/signing.ts`, which is
|
|
66
|
+
* the production reference for non-agent signed Pacifica requests.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildBindAgentMessage(params: BindAgentMessageParams): BuiltMessage;
|
|
69
|
+
/**
|
|
70
|
+
* Build the canonical JSON for `unbind_agent_wallet`.
|
|
71
|
+
*
|
|
72
|
+
* Pacifica convention: empty `agent_wallet` string unbinds all agents; a
|
|
73
|
+
* specific base58 address unbinds just that agent.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildUnbindAgentMessage(params: UnbindAgentMessageParams): BuiltMessage;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacifica agent-wallet signing builders (Phase 2c).
|
|
3
|
+
*
|
|
4
|
+
* Pacifica auth model — off-chain signed messages over Solana Ed25519.
|
|
5
|
+
* Reference: https://pacifica.gitbook.io/docs/api-documentation/api/signing.md
|
|
6
|
+
*
|
|
7
|
+
* Canonical JSON rules (from Pacifica signing spec + production reference
|
|
8
|
+
* `src/pacifica/signing.ts:prepareMessage()`):
|
|
9
|
+
* 1. Recursively sort all object keys alphabetically.
|
|
10
|
+
* 2. Compact JSON (`JSON.stringify(value)` — no whitespace).
|
|
11
|
+
* 3. Outer wrapper has the shape `{ <header fields>, "data": <payload> }`
|
|
12
|
+
* where header is `{ type, timestamp, expiry_window }`. The `data` key
|
|
13
|
+
* is sorted lexicographically alongside the header fields.
|
|
14
|
+
*
|
|
15
|
+
* The agent bind/unbind action types are documented in HypurrQuant_FE
|
|
16
|
+
* `PacificaPerpAdapter.ts` (production reference for HypurrQuant_FE; see
|
|
17
|
+
* project memory `hypurrquant_fe_reference.md`). We mirror their `bind_agent_wallet`
|
|
18
|
+
* and `unbind_agent_wallet` operation types here.
|
|
19
|
+
*
|
|
20
|
+
* Pure functions — no side effects, no fs/fetch/env reads.
|
|
21
|
+
*/
|
|
22
|
+
import { sortJsonKeys } from "../pacifica/signing.js";
|
|
23
|
+
/** Default expiry window for signed Pacifica REST requests (ms). */
|
|
24
|
+
export const DEFAULT_EXPIRY_WINDOW = 5_000;
|
|
25
|
+
// ── Builders ─────────────────────────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Build the canonical JSON for `bind_agent_wallet`.
|
|
28
|
+
*
|
|
29
|
+
* Wire shape (after sorting):
|
|
30
|
+
* {"data":{"agent_wallet":"<base58>"},"expiry_window":5000,"timestamp":...,"type":"bind_agent_wallet"}
|
|
31
|
+
*
|
|
32
|
+
* The `account` field is supplied separately in the REST envelope; only the
|
|
33
|
+
* `agent_wallet` payload field is included inside the signed `data` block.
|
|
34
|
+
* This mirrors `buildSignedRequest()` in `src/pacifica/signing.ts`, which is
|
|
35
|
+
* the production reference for non-agent signed Pacifica requests.
|
|
36
|
+
*/
|
|
37
|
+
export function buildBindAgentMessage(params) {
|
|
38
|
+
const timestamp = params.timestamp ?? Date.now();
|
|
39
|
+
const expiryWindow = params.expiryWindow ?? DEFAULT_EXPIRY_WINDOW;
|
|
40
|
+
const type = "bind_agent_wallet";
|
|
41
|
+
const payload = {
|
|
42
|
+
agent_wallet: params.agentWallet,
|
|
43
|
+
};
|
|
44
|
+
const header = { type, timestamp, expiry_window: expiryWindow };
|
|
45
|
+
const wrapped = { ...header, data: payload };
|
|
46
|
+
const sorted = sortJsonKeys(wrapped);
|
|
47
|
+
return {
|
|
48
|
+
canonicalJson: JSON.stringify(sorted),
|
|
49
|
+
header,
|
|
50
|
+
payload,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the canonical JSON for `unbind_agent_wallet`.
|
|
55
|
+
*
|
|
56
|
+
* Pacifica convention: empty `agent_wallet` string unbinds all agents; a
|
|
57
|
+
* specific base58 address unbinds just that agent.
|
|
58
|
+
*/
|
|
59
|
+
export function buildUnbindAgentMessage(params) {
|
|
60
|
+
const timestamp = params.timestamp ?? Date.now();
|
|
61
|
+
const expiryWindow = params.expiryWindow ?? DEFAULT_EXPIRY_WINDOW;
|
|
62
|
+
const type = "unbind_agent_wallet";
|
|
63
|
+
const payload = {
|
|
64
|
+
agent_wallet: params.agentWallet,
|
|
65
|
+
};
|
|
66
|
+
const header = { type, timestamp, expiry_window: expiryWindow };
|
|
67
|
+
const wrapped = { ...header, data: payload };
|
|
68
|
+
const sorted = sortJsonKeys(wrapped);
|
|
69
|
+
return {
|
|
70
|
+
canonicalJson: JSON.stringify(sorted),
|
|
71
|
+
header,
|
|
72
|
+
payload,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -2,22 +2,50 @@ import { Keypair } from "@solana/web3.js";
|
|
|
2
2
|
import { PacificaClient, type Network } from "../pacifica/index.js";
|
|
3
3
|
import type { ExchangeAdapter, ExchangeMarketInfo, ExchangePosition, ExchangeOrder, ExchangeBalance, ExchangeTrade, ExchangeFundingPayment, ExchangeKline } from "./interface.js";
|
|
4
4
|
import type { SolanaSigner } from "../signer/index.js";
|
|
5
|
+
import type { AgentMeta } from "../settings.js";
|
|
5
6
|
export declare class PacificaAdapter implements ExchangeAdapter {
|
|
6
7
|
readonly name = "pacifica";
|
|
7
8
|
readonly chain = "solana";
|
|
8
9
|
readonly aliases: readonly ["pac"];
|
|
9
10
|
private client;
|
|
11
|
+
/** Tier 3: PK direct (constructor-supplied keypair). */
|
|
10
12
|
private _solanaSigner;
|
|
13
|
+
/** Tier 2: OWS master signer (set via setSigner). */
|
|
14
|
+
private _masterSigner?;
|
|
15
|
+
/** Tier 1: agent OWS signer (Phase 2c). */
|
|
16
|
+
private _agentSigner?;
|
|
17
|
+
/** Tier 1 metadata. */
|
|
18
|
+
private _agentMeta?;
|
|
19
|
+
/** --no-agent bypass flag. */
|
|
20
|
+
private _useNoAgent;
|
|
21
|
+
/** True when adapter was constructed without a real PK (read-only). */
|
|
11
22
|
private _hasRealKey;
|
|
23
|
+
/** Public Solana base58 of the active signer (`_resolveSigner().address`). */
|
|
12
24
|
private account;
|
|
25
|
+
/** Bound `signMessage` for the active signer (Phase 2c routes through `_resolveSigner`). */
|
|
13
26
|
private signMessage;
|
|
14
27
|
private _marketsCache;
|
|
15
28
|
private _marketsCacheTime;
|
|
16
29
|
private static readonly CACHE_TTL;
|
|
17
30
|
constructor(keypair: Keypair, network?: Network, builderCode?: string, hasRealKey?: boolean);
|
|
18
31
|
private ensureSigner;
|
|
19
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Inject an external Solana signer (Tier 2 — OWS master).
|
|
34
|
+
* Backward-compatible: existing callers (e.g. `perp -e pacifica --ows ...`
|
|
35
|
+
* in `_initWithOws`) continue to work; the master signer is what
|
|
36
|
+
* `_resolveSigner()` falls back to when no agent is registered.
|
|
37
|
+
*/
|
|
20
38
|
setSigner(signer: SolanaSigner): void;
|
|
39
|
+
/** Tier 1: inject agent OWS Solana signer (Phase 2c). */
|
|
40
|
+
setAgentSigner(meta: AgentMeta, signer: SolanaSigner): void;
|
|
41
|
+
/** Bypass Tier 1 (agent) — used for `--no-agent` flag. */
|
|
42
|
+
setNoAgent(noAgent: boolean): void;
|
|
43
|
+
/** True only when ALL THREE tiers are unconfigured. */
|
|
44
|
+
get isReadOnly(): boolean;
|
|
45
|
+
/** Which signer tier is currently active (for debug/diagnostics). */
|
|
46
|
+
get activeSignerTier(): "agent" | "master" | "pk" | null;
|
|
47
|
+
/** Resolve the active signer across three tiers. Mirrors AsterAdapter._resolveSigner. */
|
|
48
|
+
private _resolveSigner;
|
|
21
49
|
/** Access the underlying Keypair (only available with LocalSolanaSigner). */
|
|
22
50
|
get keypair(): Keypair;
|
|
23
51
|
private _getPrices;
|