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.
Files changed (254) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bot/engine.js +23 -4
  21. package/dist/bot/presets.js +3 -3
  22. package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
  23. package/dist/bot/strategies/funding-arb-strategy.js +41 -176
  24. package/dist/bot/strategies/funding-auto.js +2 -2
  25. package/dist/bridge-engine.js +102 -49
  26. package/dist/cli-spec.js +1 -2
  27. package/dist/commands/account.js +2 -500
  28. package/dist/commands/agent.d.ts +8 -2
  29. package/dist/commands/agent.js +1903 -366
  30. package/dist/commands/alerts.js +3 -1
  31. package/dist/commands/arb/index.js +37 -7
  32. package/dist/commands/arb-auto.js +44 -11
  33. package/dist/commands/arb.js +83 -34
  34. package/dist/commands/bot.d.ts +1 -1
  35. package/dist/commands/bot.js +20 -16
  36. package/dist/commands/bridge.d.ts +1 -1
  37. package/dist/commands/bridge.js +4 -4
  38. package/dist/commands/funds.d.ts +1 -1
  39. package/dist/commands/funds.js +18 -68
  40. package/dist/commands/jobs.d.ts +1 -1
  41. package/dist/commands/jobs.js +2 -2
  42. package/dist/commands/manage.d.ts +1 -1
  43. package/dist/commands/manage.js +12 -84
  44. package/dist/commands/plan.d.ts +1 -1
  45. package/dist/commands/plan.js +2 -2
  46. package/dist/commands/portfolio.d.ts +19 -3
  47. package/dist/commands/portfolio.js +747 -163
  48. package/dist/commands/rebalance.d.ts +1 -1
  49. package/dist/commands/rebalance.js +18 -12
  50. package/dist/commands/risk.js +2 -6
  51. package/dist/commands/settings.js +30 -53
  52. package/dist/commands/trade.js +16 -15
  53. package/dist/commands/wallet.d.ts +2 -1
  54. package/dist/commands/wallet.js +1122 -176
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +55 -9
  57. package/dist/errors.d.ts +64 -0
  58. package/dist/errors.js +82 -2
  59. package/dist/event-stream.js +12 -1
  60. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  61. package/dist/exchanges/aster-typed-data.js +189 -0
  62. package/dist/exchanges/aster.d.ts +60 -11
  63. package/dist/exchanges/aster.js +278 -136
  64. package/dist/exchanges/hyperliquid.d.ts +14 -0
  65. package/dist/exchanges/hyperliquid.js +88 -34
  66. package/dist/exchanges/lighter-spot.js +19 -5
  67. package/dist/exchanges/lighter.d.ts +66 -0
  68. package/dist/exchanges/lighter.js +213 -31
  69. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  70. package/dist/exchanges/pacifica-typed-data.js +74 -0
  71. package/dist/exchanges/pacifica.d.ts +29 -1
  72. package/dist/exchanges/pacifica.js +94 -6
  73. package/dist/funding/rates.js +59 -21
  74. package/dist/guardrail/contracts.d.ts +14 -0
  75. package/dist/guardrail/contracts.js +72 -0
  76. package/dist/guardrail/perp-guardrail.d.ts +21 -0
  77. package/dist/guardrail/perp-guardrail.js +145 -0
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.js +307 -362
  80. package/dist/jobs.js +3 -1
  81. package/dist/mcp-server.js +92 -88
  82. package/dist/position-history.js +47 -4
  83. package/dist/settings.d.ts +69 -3
  84. package/dist/settings.js +3 -2
  85. package/dist/signer/index.d.ts +2 -0
  86. package/dist/signer/index.js +2 -0
  87. package/dist/signer/ows-evm.d.ts +32 -0
  88. package/dist/signer/ows-evm.js +86 -0
  89. package/dist/signer/ows-loader.d.ts +2 -0
  90. package/dist/signer/ows-loader.js +6 -0
  91. package/dist/signer/ows-solana.d.ts +25 -0
  92. package/dist/signer/ows-solana.js +56 -0
  93. package/dist/smart-order.d.ts +8 -2
  94. package/dist/smart-order.js +46 -11
  95. package/dist/strategies/funding-arb.js +29 -8
  96. package/dist/trade-validator.js +30 -5
  97. package/dist/utils.d.ts +10 -0
  98. package/dist/utils.js +19 -0
  99. package/package.json +4 -2
  100. package/skills/perp-cli/SKILL.md +23 -14
  101. package/skills/perp-cli/references/agent-operations.md +6 -4
  102. package/skills/perp-cli/references/commands.md +55 -34
  103. package/skills/perp-cli/scripts/preflight.sh +4 -4
  104. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  105. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  106. package/dist/__tests__/alert-logic.test.js +0 -107
  107. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  108. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  109. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  110. package/dist/__tests__/arb-history-stats.test.js +0 -176
  111. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  112. package/dist/__tests__/arb-logic.test.js +0 -84
  113. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  114. package/dist/__tests__/arb-manage.test.js +0 -253
  115. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  116. package/dist/__tests__/arb-new-features.test.js +0 -457
  117. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  118. package/dist/__tests__/arb-sizing.test.js +0 -48
  119. package/dist/__tests__/arb-state.test.d.ts +0 -1
  120. package/dist/__tests__/arb-state.test.js +0 -284
  121. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  122. package/dist/__tests__/arb-userflow.test.js +0 -945
  123. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  124. package/dist/__tests__/arb-utils.test.js +0 -264
  125. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  126. package/dist/__tests__/bot-conditions.test.js +0 -341
  127. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  128. package/dist/__tests__/client-id-tracker.test.js +0 -137
  129. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  130. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  131. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  132. package/dist/__tests__/commands/order-intent.test.js +0 -600
  133. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  134. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  135. package/dist/__tests__/config.test.d.ts +0 -1
  136. package/dist/__tests__/config.test.js +0 -86
  137. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  138. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  139. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  140. package/dist/__tests__/dex-asset-map.test.js +0 -191
  141. package/dist/__tests__/errors.test.d.ts +0 -1
  142. package/dist/__tests__/errors.test.js +0 -110
  143. package/dist/__tests__/event-stream.test.d.ts +0 -1
  144. package/dist/__tests__/event-stream.test.js +0 -276
  145. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  146. package/dist/__tests__/exchanges/interface.test.js +0 -132
  147. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  148. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  149. package/dist/__tests__/execution-log.test.d.ts +0 -1
  150. package/dist/__tests__/execution-log.test.js +0 -106
  151. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  152. package/dist/__tests__/funding-calc.test.js +0 -71
  153. package/dist/__tests__/funding-history.test.d.ts +0 -1
  154. package/dist/__tests__/funding-history.test.js +0 -343
  155. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  156. package/dist/__tests__/funding-rates.test.js +0 -342
  157. package/dist/__tests__/funding.test.d.ts +0 -1
  158. package/dist/__tests__/funding.test.js +0 -173
  159. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  160. package/dist/__tests__/gap-logic.test.js +0 -43
  161. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  162. package/dist/__tests__/hip3-dex.test.js +0 -234
  163. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  165. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  166. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  167. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  169. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  170. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  171. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  173. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  175. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  177. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  178. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  179. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  180. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  181. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  182. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  183. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  184. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  185. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  186. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  187. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  188. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  189. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  190. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  191. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  192. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  193. package/dist/__tests__/liquidity.test.d.ts +0 -1
  194. package/dist/__tests__/liquidity.test.js +0 -225
  195. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  196. package/dist/__tests__/plan-executor.test.js +0 -314
  197. package/dist/__tests__/position-history.test.d.ts +0 -1
  198. package/dist/__tests__/position-history.test.js +0 -367
  199. package/dist/__tests__/retry.test.d.ts +0 -1
  200. package/dist/__tests__/retry.test.js +0 -310
  201. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  202. package/dist/__tests__/risk-assessment.test.js +0 -288
  203. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  204. package/dist/__tests__/security-adversarial.test.js +0 -574
  205. package/dist/__tests__/strategies.test.d.ts +0 -1
  206. package/dist/__tests__/strategies.test.js +0 -539
  207. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  208. package/dist/__tests__/trade-execution.test.js +0 -129
  209. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  210. package/dist/__tests__/trade-validator.test.js +0 -655
  211. package/dist/__tests__/utils.test.d.ts +0 -1
  212. package/dist/__tests__/utils.test.js +0 -76
  213. package/dist/arb/reconcile.d.ts +0 -28
  214. package/dist/arb/reconcile.js +0 -162
  215. package/dist/commands/alert.d.ts +0 -2
  216. package/dist/commands/alert.js +0 -421
  217. package/dist/commands/analytics.d.ts +0 -3
  218. package/dist/commands/analytics.js +0 -311
  219. package/dist/commands/arb-manage.d.ts +0 -5
  220. package/dist/commands/arb-manage.js +0 -5
  221. package/dist/commands/dashboard.d.ts +0 -3
  222. package/dist/commands/dashboard.js +0 -176
  223. package/dist/commands/deposit.d.ts +0 -4
  224. package/dist/commands/deposit.js +0 -573
  225. package/dist/commands/dex.d.ts +0 -3
  226. package/dist/commands/dex.js +0 -120
  227. package/dist/commands/env.d.ts +0 -2
  228. package/dist/commands/env.js +0 -136
  229. package/dist/commands/funding.d.ts +0 -3
  230. package/dist/commands/funding.js +0 -554
  231. package/dist/commands/gap.d.ts +0 -2
  232. package/dist/commands/gap.js +0 -305
  233. package/dist/commands/health.d.ts +0 -2
  234. package/dist/commands/health.js +0 -67
  235. package/dist/commands/multileg.d.ts +0 -16
  236. package/dist/commands/multileg.js +0 -174
  237. package/dist/commands/pnl.d.ts +0 -12
  238. package/dist/commands/pnl.js +0 -243
  239. package/dist/commands/run.d.ts +0 -3
  240. package/dist/commands/run.js +0 -202
  241. package/dist/commands/stream.d.ts +0 -5
  242. package/dist/commands/stream.js +0 -200
  243. package/dist/commands/withdraw.d.ts +0 -3
  244. package/dist/commands/withdraw.js +0 -187
  245. package/dist/exchanges/edgex.d.ts +0 -62
  246. package/dist/exchanges/edgex.js +0 -552
  247. package/dist/exchanges/nado.d.ts +0 -52
  248. package/dist/exchanges/nado.js +0 -223
  249. package/dist/funding/index.d.ts +0 -3
  250. package/dist/funding/index.js +0 -3
  251. package/dist/retry.d.ts +0 -74
  252. package/dist/retry.js +0 -129
  253. package/dist/ws/market-feed.d.ts +0 -74
  254. package/dist/ws/market-feed.js +0 -227
@@ -1,13 +1,27 @@
1
1
  import { PacificaClient } from "../pacifica/index.js";
2
2
  import { LocalSolanaSigner } from "../signer/index.js";
3
+ import { isExpired } from "../agent-wallet/expiry.js";
4
+ import { PerpError } from "../errors.js";
3
5
  export class PacificaAdapter {
4
6
  name = "pacifica";
5
7
  chain = "solana";
6
8
  aliases = ["pac"];
7
9
  client;
10
+ /** Tier 3: PK direct (constructor-supplied keypair). */
8
11
  _solanaSigner;
12
+ /** Tier 2: OWS master signer (set via setSigner). */
13
+ _masterSigner;
14
+ /** Tier 1: agent OWS signer (Phase 2c). */
15
+ _agentSigner;
16
+ /** Tier 1 metadata. */
17
+ _agentMeta;
18
+ /** --no-agent bypass flag. */
19
+ _useNoAgent = false;
20
+ /** True when adapter was constructed without a real PK (read-only). */
9
21
  _hasRealKey;
22
+ /** Public Solana base58 of the active signer (`_resolveSigner().address`). */
10
23
  account;
24
+ /** Bound `signMessage` for the active signer (Phase 2c routes through `_resolveSigner`). */
11
25
  signMessage;
12
26
  _marketsCache = null;
13
27
  _marketsCacheTime = 0;
@@ -17,18 +31,92 @@ export class PacificaAdapter {
17
31
  this._hasRealKey = hasRealKey;
18
32
  this.account = this._solanaSigner.getPublicKeyBase58();
19
33
  this.client = new PacificaClient({ network, builderCode });
20
- this.signMessage = (msg) => this._solanaSigner.signMessage(msg);
34
+ // Hot-path methods always go through `_resolveSigner()` so the bound
35
+ // `signMessage` reflects the highest-priority active tier.
36
+ this.signMessage = (msg) => this._resolveSigner().signer.signMessage(msg);
21
37
  }
22
38
  ensureSigner() {
23
- if (!this._hasRealKey) {
24
- throw new Error("No private key configured. Run: perp setup");
25
- }
39
+ // Resolves across all tiers; throws PerpError if none available.
40
+ this._resolveSigner();
26
41
  }
27
- /** Inject an external Solana signer. */
42
+ /**
43
+ * Inject an external Solana signer (Tier 2 — OWS master).
44
+ * Backward-compatible: existing callers (e.g. `perp -e pacifica --ows ...`
45
+ * in `_initWithOws`) continue to work; the master signer is what
46
+ * `_resolveSigner()` falls back to when no agent is registered.
47
+ */
28
48
  setSigner(signer) {
49
+ this._masterSigner = signer;
29
50
  this._solanaSigner = signer;
30
51
  this.account = signer.getPublicKeyBase58();
31
- this.signMessage = (msg) => this._solanaSigner.signMessage(msg);
52
+ this._hasRealKey = true;
53
+ this.signMessage = (msg) => this._resolveSigner().signer.signMessage(msg);
54
+ }
55
+ /** Tier 1: inject agent OWS Solana signer (Phase 2c). */
56
+ setAgentSigner(meta, signer) {
57
+ this._agentMeta = meta;
58
+ this._agentSigner = signer;
59
+ // The `account` field is the master Solana address (used in REST envelopes
60
+ // as the `account` key) — agent path puts the master in `account` and the
61
+ // agent in `agent_wallet`. Capture the master Solana address from the
62
+ // AgentMeta so REST flows can reference it.
63
+ if (meta.userSolanaAddress) {
64
+ this.account = meta.userSolanaAddress;
65
+ }
66
+ }
67
+ /** Bypass Tier 1 (agent) — used for `--no-agent` flag. */
68
+ setNoAgent(noAgent) {
69
+ this._useNoAgent = noAgent;
70
+ }
71
+ /** True only when ALL THREE tiers are unconfigured. */
72
+ get isReadOnly() {
73
+ return !this._agentSigner && !this._masterSigner && !this._hasRealKey;
74
+ }
75
+ /** Which signer tier is currently active (for debug/diagnostics). */
76
+ get activeSignerTier() {
77
+ try {
78
+ return this._resolveSigner().tier;
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ /** Resolve the active signer across three tiers. Mirrors AsterAdapter._resolveSigner. */
85
+ _resolveSigner() {
86
+ // Tier 1: agent (when registered, not expired, --no-agent NOT set)
87
+ if (!this._useNoAgent && this._agentSigner && this._agentMeta) {
88
+ if (isExpired(this._agentMeta)) {
89
+ // Skip Tier 1 only if Tier 2/3 available; otherwise throw AGENT_EXPIRED
90
+ if (!this._masterSigner && !this._hasRealKey) {
91
+ throw new PerpError("AGENT_EXPIRED", "Agent wallet has expired", {
92
+ remediation: "perp wallet agent approve pacifica --rotate",
93
+ });
94
+ }
95
+ // fall through to Tier 2/3
96
+ }
97
+ else {
98
+ return { tier: "agent", signer: this._agentSigner, address: this._agentSigner.getPublicKeyBase58() };
99
+ }
100
+ }
101
+ // Tier 2: OWS master
102
+ if (this._masterSigner) {
103
+ return { tier: "master", signer: this._masterSigner, address: this._masterSigner.getPublicKeyBase58() };
104
+ }
105
+ // Tier 3: PK direct
106
+ if (this._hasRealKey) {
107
+ // Guard: legacy test fixtures (e.g. bugfix-v042.test.ts) construct adapters
108
+ // via `Object.create(PacificaAdapter.prototype)` and only set `_hasRealKey = true`
109
+ // — `_solanaSigner` is undefined in that case. Fall back to the cached
110
+ // `account` field when available.
111
+ const address = this._solanaSigner
112
+ ? this._solanaSigner.getPublicKeyBase58()
113
+ : this.account;
114
+ return { tier: "pk", signer: this._solanaSigner, address };
115
+ }
116
+ // No signer at any tier
117
+ throw new PerpError("NO_SIGNER_AVAILABLE", "No signing path configured for Pacifica", {
118
+ remediation: "Run one of: (a) perp wallet agent approve pacifica --master <wallet>; (b) perp wallet generate && perp -e pacifica --ows <wallet>; (c) export PACIFICA_PRIVATE_KEY=...",
119
+ });
32
120
  }
33
121
  /** Access the underlying Keypair (only available with LocalSolanaSigner). */
34
122
  get keypair() {
@@ -56,6 +56,9 @@ async function fetchHyperliquidRates() {
56
56
  }
57
57
  }
58
58
  async function fetchLighterRates() {
59
+ // Outer try/catch returning [] is intentional graceful degradation; see
60
+ // fetchAsterRates for the same pattern. Inside, SSOT rule #2 applies —
61
+ // skip rows whose mark price cannot be resolved instead of 0-coercing.
59
62
  try {
60
63
  const [details, funding] = await Promise.all([
61
64
  fetchLighterOrderBookDetails(),
@@ -63,25 +66,47 @@ async function fetchLighterRates() {
63
66
  ]);
64
67
  const priceMap = new Map(details.map(d => [d.marketId, d.lastTradePrice]));
65
68
  const symMap = new Map(details.map(d => [d.marketId, d.symbol]));
66
- return funding.map(fr => {
69
+ const out = [];
70
+ for (const fr of funding) {
67
71
  const symbol = fr.symbol || symMap.get(fr.marketId) || "";
72
+ if (!symbol)
73
+ continue;
68
74
  const rate = fr.rate;
69
- const hourly = toHourlyRate(rate, "lighter");
70
- return {
75
+ // Prefer fr.markPrice; fall back to orderBook last-trade as a documented
76
+ // price-source preference (NOT an error fallback).
77
+ const directMark = fr.markPrice;
78
+ const fallbackMark = priceMap.get(fr.marketId);
79
+ let markPrice;
80
+ if (directMark !== null && Number.isFinite(directMark) && directMark > 0) {
81
+ markPrice = directMark;
82
+ }
83
+ else if (fallbackMark !== undefined && Number.isFinite(fallbackMark) && fallbackMark > 0) {
84
+ markPrice = fallbackMark;
85
+ }
86
+ if (!Number.isFinite(rate) || markPrice === undefined)
87
+ continue;
88
+ out.push({
71
89
  exchange: "lighter",
72
90
  symbol,
73
91
  fundingRate: rate,
74
- hourlyRate: hourly,
92
+ hourlyRate: toHourlyRate(rate, "lighter"),
75
93
  annualizedPct: annualizeRate(rate, "lighter"),
76
- markPrice: fr.markPrice || priceMap.get(fr.marketId) || 0,
77
- };
78
- });
94
+ markPrice,
95
+ });
96
+ }
97
+ return out;
79
98
  }
80
99
  catch {
81
100
  return [];
82
101
  }
83
102
  }
84
103
  async function fetchAsterRates() {
104
+ // Outer try/catch returning [] is intentional graceful degradation: when
105
+ // the entire endpoint is unreachable, the cross-DEX comparison should
106
+ // continue with the remaining venues.
107
+ // Inside the try block, SSOT rule #2 applies — refuse to silently 0-coerce
108
+ // missing markPrice / lastFundingRate, since those values feed downstream
109
+ // sizing and any 0 corrupts the comparison.
85
110
  try {
86
111
  const [premiums, tickers] = await Promise.all([
87
112
  fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex").then(r => r.json()),
@@ -89,25 +114,38 @@ async function fetchAsterRates() {
89
114
  ]);
90
115
  const priceMap = new Map();
91
116
  for (const t of tickers ?? []) {
92
- priceMap.set(String(t.symbol), Number(t.lastPrice ?? 0));
117
+ const last = Number(t.lastPrice);
118
+ if (Number.isFinite(last) && last > 0)
119
+ priceMap.set(String(t.symbol), last);
93
120
  }
94
- return (premiums ?? [])
95
- .filter(p => String(p.symbol ?? "").endsWith("USDT"))
96
- .map(p => {
121
+ const out = [];
122
+ for (const p of premiums ?? []) {
97
123
  const rawSymbol = String(p.symbol ?? "");
124
+ if (!rawSymbol.endsWith("USDT"))
125
+ continue;
126
+ const fundingRate = Number(p.lastFundingRate);
127
+ // Prefer premiumIndex.markPrice; fall back to ticker.lastPrice as a
128
+ // documented price-source preference (NOT an error fallback).
129
+ const markFromPremium = Number(p.markPrice);
130
+ const markPrice = Number.isFinite(markFromPremium) && markFromPremium > 0
131
+ ? markFromPremium
132
+ : priceMap.get(rawSymbol);
133
+ if (!Number.isFinite(fundingRate) || markPrice === undefined || !Number.isFinite(markPrice) || markPrice <= 0) {
134
+ continue;
135
+ }
98
136
  const symbol = rawSymbol.replace(/USDT$/, "");
99
- const rate = Number(p.lastFundingRate ?? 0);
100
- const hourly = toHourlyRate(rate, "aster");
101
- return {
137
+ const nextFundingTimeRaw = Number(p.nextFundingTime);
138
+ out.push({
102
139
  exchange: "aster",
103
140
  symbol,
104
- fundingRate: rate,
105
- hourlyRate: hourly,
106
- annualizedPct: annualizeRate(rate, "aster"),
107
- markPrice: Number(p.markPrice ?? priceMap.get(rawSymbol) ?? 0),
108
- nextFundingTime: Number(p.nextFundingTime ?? 0),
109
- };
110
- });
141
+ fundingRate,
142
+ hourlyRate: toHourlyRate(fundingRate, "aster"),
143
+ annualizedPct: annualizeRate(fundingRate, "aster"),
144
+ markPrice,
145
+ nextFundingTime: Number.isFinite(nextFundingTimeRaw) ? nextFundingTimeRaw : 0,
146
+ });
147
+ }
148
+ return out;
111
149
  }
112
150
  catch {
113
151
  return [];
@@ -0,0 +1,14 @@
1
+ /**
2
+ * DEX contract whitelist for perp-guardrail policy executable.
3
+ * Keyed by CAIP-2 chain ID → array of allowed contract/program addresses.
4
+ */
5
+ export declare const USDC_ADDRESSES: Record<string, string>;
6
+ export declare const ALLOWED_CONTRACTS: Record<string, string[]>;
7
+ export declare const ALLOWED_CHAINS: string[];
8
+ export declare const DEFAULT_LIMITS: {
9
+ max_tx_usd: number;
10
+ max_daily_usd: number;
11
+ max_withdraw_usd: number;
12
+ max_daily_withdraw_usd: number;
13
+ };
14
+ export declare const DANGEROUS_SELECTORS: string[];
@@ -0,0 +1,72 @@
1
+ /**
2
+ * DEX contract whitelist for perp-guardrail policy executable.
3
+ * Keyed by CAIP-2 chain ID → array of allowed contract/program addresses.
4
+ */
5
+ // ── Pacifica (Solana) ──
6
+ const PACIFICA_MAINNET = [
7
+ "PCFA5iYgmqK6MqPhWNKg7Yv7auX7VZ4Cx7T1eJyrAMH", // Pacifica program
8
+ "9Gdmhq4Gv1LnNMp7aiS1HSVd7pNnXNMsbuXALCQRmGjY", // Central state
9
+ "72R843XwZxqWhsJceARQQTTbYtWy6Zw9et2YV4FpRHTa", // Vault
10
+ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mint
11
+ "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", // SPL Token
12
+ "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", // Associated Token
13
+ "11111111111111111111111111111111", // System Program
14
+ "CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe", // CCTP V2 Token Messenger
15
+ "CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC", // CCTP V2 Message Transmitter
16
+ ];
17
+ const PACIFICA_TESTNET = [
18
+ "peRPsYCcB1J9jvrs29jiGdjkytxs8uHLmSPLKKP9ptm", // Testnet program
19
+ "2zPRq1Qvdq5A4Ld6WsH7usgCge4ApZRYfhhf5VAjfXxv", // Testnet central state
20
+ "5SDFdHZGTZbyRYu54CgmRkCGnPHC5pYaN27p7XGLqnBs", // Testnet vault
21
+ "USDPqRbLidFGufty2s3oizmDEKdqx7ePTqzDMbf5ZKM", // Testnet USDC
22
+ "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
23
+ "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
24
+ "11111111111111111111111111111111",
25
+ ];
26
+ // ── Hyperliquid (Arbitrum + HyperCore) ──
27
+ const HYPERLIQUID_CONTRACTS = [
28
+ "0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757", // CctpForwarder (HyperCore)
29
+ ];
30
+ // ── CCTP V2 (shared across EVM chains) ──
31
+ const CCTP_V2_EVM = [
32
+ "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", // TokenMessengerV2
33
+ "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64", // MessageTransmitterV2
34
+ ];
35
+ // ── USDC addresses ──
36
+ export const USDC_ADDRESSES = {
37
+ "eip155:1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // Ethereum
38
+ "eip155:42161": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // Arbitrum
39
+ "eip155:8453": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base
40
+ };
41
+ // ── Complete whitelist by CAIP-2 chain ──
42
+ export const ALLOWED_CONTRACTS = {
43
+ // Solana mainnet
44
+ "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": PACIFICA_MAINNET,
45
+ // Solana devnet
46
+ "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": PACIFICA_TESTNET,
47
+ // Arbitrum (Hyperliquid deposits + CCTP)
48
+ "eip155:42161": [...HYPERLIQUID_CONTRACTS, ...CCTP_V2_EVM, USDC_ADDRESSES["eip155:42161"]],
49
+ // Hyperliquid signing domain (phantom chain for typed data)
50
+ "eip155:1337": [], // no contract calls, only typed data signing
51
+ // Base (CCTP bridge + USDC)
52
+ "eip155:8453": [...CCTP_V2_EVM, USDC_ADDRESSES["eip155:8453"]],
53
+ // Lighter (ZK rollup)
54
+ "eip155:304": [], // signing-based, no direct contract interaction
55
+ "eip155:300": [], // Lighter testnet
56
+ // Ethereum mainnet (CCTP + USDC)
57
+ "eip155:1": [...CCTP_V2_EVM, USDC_ADDRESSES["eip155:1"]],
58
+ };
59
+ // ── Allowed chain IDs (all chains our DEXes operate on) ──
60
+ export const ALLOWED_CHAINS = Object.keys(ALLOWED_CONTRACTS);
61
+ // ── Default guardrail limits ──
62
+ export const DEFAULT_LIMITS = {
63
+ max_tx_usd: 1000,
64
+ max_daily_usd: 5000,
65
+ max_withdraw_usd: 500,
66
+ max_daily_withdraw_usd: 2000,
67
+ };
68
+ // ── Common blocked selectors ──
69
+ export const DANGEROUS_SELECTORS = [
70
+ "0x095ea7b3", // approve(address,uint256) — unlimited token approvals
71
+ "0x23b872dd", // transferFrom(address,address,uint256)
72
+ ];
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * perp-guardrail — OWS Policy Engine custom executable.
4
+ *
5
+ * Protocol:
6
+ * stdin → PolicyContext JSON (from OWS engine)
7
+ * stdout → PolicyResult JSON {"allow": bool, "reason"?: string}
8
+ *
9
+ * Validates:
10
+ * 1. Contract address whitelist (per chain)
11
+ * 2. Per-transaction USD amount limit
12
+ * 3. Daily cumulative spending limit
13
+ * 4. Withdrawal limits (per-tx and daily)
14
+ * 5. Blocked function selectors (e.g. approve)
15
+ * 6. Allowed trading symbols
16
+ *
17
+ * Configuration is read from PolicyContext.policy_config:
18
+ * { max_tx_usd, max_daily_usd, max_withdraw_usd, max_daily_withdraw_usd,
19
+ * allowed_symbols?, block_selectors?, allowed_contracts?, allowed_chains? }
20
+ */
21
+ export {};
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * perp-guardrail — OWS Policy Engine custom executable.
4
+ *
5
+ * Protocol:
6
+ * stdin → PolicyContext JSON (from OWS engine)
7
+ * stdout → PolicyResult JSON {"allow": bool, "reason"?: string}
8
+ *
9
+ * Validates:
10
+ * 1. Contract address whitelist (per chain)
11
+ * 2. Per-transaction USD amount limit
12
+ * 3. Daily cumulative spending limit
13
+ * 4. Withdrawal limits (per-tx and daily)
14
+ * 5. Blocked function selectors (e.g. approve)
15
+ * 6. Allowed trading symbols
16
+ *
17
+ * Configuration is read from PolicyContext.policy_config:
18
+ * { max_tx_usd, max_daily_usd, max_withdraw_usd, max_daily_withdraw_usd,
19
+ * allowed_symbols?, block_selectors?, allowed_contracts?, allowed_chains? }
20
+ */
21
+ import { ALLOWED_CONTRACTS, ALLOWED_CHAINS, DEFAULT_LIMITS } from "./contracts.js";
22
+ function deny(reason) {
23
+ return { allow: false, reason };
24
+ }
25
+ function allow() {
26
+ return { allow: true };
27
+ }
28
+ function weiToUsd(weiStr, ethPriceUsd) {
29
+ const wei = BigInt(weiStr || "0");
30
+ const ethValue = Number(wei) / 1e18;
31
+ return ethValue * ethPriceUsd;
32
+ }
33
+ // Well-known EVM function selectors
34
+ const KNOWN_SELECTORS = {
35
+ "0x095ea7b3": "approve",
36
+ "0x23b872dd": "transferFrom",
37
+ "0xa9059cbb": "transfer",
38
+ "0x3ccfd60b": "withdraw",
39
+ "0x2e1a7d4d": "withdraw(uint256)",
40
+ "0xd0e30db0": "deposit",
41
+ };
42
+ function isWithdrawTx(data) {
43
+ if (!data || data === "0x")
44
+ return false;
45
+ const selector = data.slice(0, 10).toLowerCase();
46
+ return selector === "0x3ccfd60b" || selector === "0x2e1a7d4d";
47
+ }
48
+ function evaluate(ctx) {
49
+ // SSOT rule #2: policy_config must be explicitly supplied. A missing
50
+ // policy_config previously fell through to silent defaults — that is a
51
+ // safety-critical fallback (the user might assume their custom limits are
52
+ // active when in fact only baked-in defaults are). Fail closed instead.
53
+ if (!ctx.policy_config) {
54
+ return deny("policy_config missing from PolicyContext; refusing to fall through to baked-in defaults. " +
55
+ "Caller must supply policy_config explicitly (max_tx_usd, max_daily_usd, etc.).");
56
+ }
57
+ const config = ctx.policy_config;
58
+ const maxTxUsd = config.max_tx_usd ?? DEFAULT_LIMITS.max_tx_usd;
59
+ const maxDailyUsd = config.max_daily_usd ?? DEFAULT_LIMITS.max_daily_usd;
60
+ const maxWithdrawUsd = config.max_withdraw_usd ?? maxTxUsd;
61
+ const maxDailyWithdrawUsd = config.max_daily_withdraw_usd ?? maxDailyUsd;
62
+ const contracts = config.allowed_contracts ?? ALLOWED_CONTRACTS;
63
+ const chains = config.allowed_chains ?? ALLOWED_CHAINS;
64
+ const ethPrice = config.eth_price_usd ?? 3500;
65
+ const blockSelectors = config.block_selectors ?? [];
66
+ // 1. Chain check
67
+ if (!chains.includes(ctx.chain_id)) {
68
+ return deny(`chain ${ctx.chain_id} not in allowed list`);
69
+ }
70
+ // 2. Contract whitelist (skip for signing-only chains like eip155:1337, eip155:304)
71
+ const chainContracts = contracts[ctx.chain_id];
72
+ if (chainContracts && chainContracts.length > 0 && ctx.transaction.to) {
73
+ const target = ctx.transaction.to.toLowerCase();
74
+ const allowed = chainContracts.map(a => a.toLowerCase());
75
+ if (!allowed.includes(target)) {
76
+ return deny(`contract ${ctx.transaction.to} not whitelisted for ${ctx.chain_id}`);
77
+ }
78
+ }
79
+ // 3. Blocked function selectors (e.g. approve, transferFrom)
80
+ if (blockSelectors.length > 0 && ctx.transaction.data && ctx.transaction.data !== "0x") {
81
+ const selector = ctx.transaction.data.slice(0, 10).toLowerCase();
82
+ if (blockSelectors.map(s => s.toLowerCase()).includes(selector)) {
83
+ const name = KNOWN_SELECTORS[selector] || selector;
84
+ return deny(`function ${name} is blocked by policy`);
85
+ }
86
+ }
87
+ // 4. Per-transaction amount
88
+ if (ctx.transaction.value && ctx.transaction.value !== "0") {
89
+ const txUsd = weiToUsd(ctx.transaction.value, ethPrice);
90
+ if (txUsd > maxTxUsd) {
91
+ return deny(`tx value $${txUsd.toFixed(2)} exceeds limit $${maxTxUsd}`);
92
+ }
93
+ }
94
+ // 5. Withdrawal-specific limits
95
+ if (isWithdrawTx(ctx.transaction.data)) {
96
+ if (ctx.transaction.value && ctx.transaction.value !== "0") {
97
+ const withdrawUsd = weiToUsd(ctx.transaction.value, ethPrice);
98
+ if (withdrawUsd > maxWithdrawUsd) {
99
+ return deny(`withdrawal $${withdrawUsd.toFixed(2)} exceeds per-tx limit $${maxWithdrawUsd}`);
100
+ }
101
+ }
102
+ // Daily withdrawal check uses same daily_total (OWS tracks cumulative)
103
+ if (ctx.spending.daily_total && ctx.spending.daily_total !== "0") {
104
+ const dailyUsd = weiToUsd(ctx.spending.daily_total, ethPrice);
105
+ if (dailyUsd > maxDailyWithdrawUsd) {
106
+ return deny(`daily withdrawals $${dailyUsd.toFixed(2)} exceeds limit $${maxDailyWithdrawUsd}`);
107
+ }
108
+ }
109
+ }
110
+ // 6. Daily spending limit (all operations)
111
+ if (ctx.spending.daily_total && ctx.spending.daily_total !== "0") {
112
+ const dailyUsd = weiToUsd(ctx.spending.daily_total, ethPrice);
113
+ if (dailyUsd > maxDailyUsd) {
114
+ return deny(`daily spending $${dailyUsd.toFixed(2)} exceeds limit $${maxDailyUsd}`);
115
+ }
116
+ }
117
+ return allow();
118
+ }
119
+ // ── Main: read stdin, evaluate, write stdout ──
120
+ async function main() {
121
+ const chunks = [];
122
+ for await (const chunk of process.stdin) {
123
+ chunks.push(chunk);
124
+ }
125
+ const input = Buffer.concat(chunks).toString("utf-8").trim();
126
+ if (!input) {
127
+ process.stdout.write(JSON.stringify(deny("empty stdin")));
128
+ process.exit(0);
129
+ }
130
+ let ctx;
131
+ try {
132
+ ctx = JSON.parse(input);
133
+ }
134
+ catch {
135
+ process.stdout.write(JSON.stringify(deny("invalid JSON on stdin")));
136
+ process.exit(0);
137
+ }
138
+ const result = evaluate(ctx);
139
+ process.stdout.write(JSON.stringify(result));
140
+ process.exit(0);
141
+ }
142
+ main().catch((e) => {
143
+ process.stdout.write(JSON.stringify(deny(`internal error: ${e instanceof Error ? e.message : String(e)}`)));
144
+ process.exit(0);
145
+ });
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import "./_stderr-filter.js";