@raintree-technology/perps 0.1.0 → 0.1.2

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/CHANGELOG.md CHANGED
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.2] - 2026-02-26
11
+
12
+ ### Added
13
+ - Added `perps setup decibel-key` for guided Decibel API key onboarding with:
14
+ - Geomi key-capture guidance in interactive mode
15
+ - live token validation against Decibel REST
16
+ - optional account-overview validation
17
+ - encrypted local vault persistence by default
18
+ - optional env-file export for CI/shell workflows
19
+ - Added encrypted vault helpers for single-secret lifecycle management:
20
+ - `saveSecretToCredentialVault`
21
+ - `getLatestCredentialVaultSecret`
22
+ - Added release-ready Decibel onboarding docs covering mainnet startup, automation boundaries, and CI-safe non-interactive flows.
23
+
24
+ ### Changed
25
+ - Updated Decibel default endpoints and package addresses for current Aptos mainnet/testnet deployment values.
26
+ - `loadConfig` now falls back to encrypted-vault Decibel bearer token when `DECIBEL_API_BEARER_TOKEN` is not set in environment.
27
+ - `setup wizard` now consumes vault-stored Decibel bearer token fallback during auth bootstrap.
28
+ - `asset book` now passes resolved exchange credentials and transport URLs into adapter connect flow for cleaner Decibel startup behavior.
29
+
30
+ ### Fixed
31
+ - Updated Decibel on-chain function routing to `dex_accounts_entry::*` entrypoints for order placement/cancel/config calls.
32
+ - Hardened Decibel WebSocket shutdown path to avoid close-time crashes when sockets are still in `CONNECTING` state.
33
+ - Clarified Decibel auth/trading error messages to point users directly to Geomi API key source and required env/vault values.
34
+
35
+ ### Tests
36
+ - Expanded Decibel coverage for:
37
+ - vault secret persistence/read-back
38
+ - `setup decibel-key` interactive and non-interactive flows
39
+ - config vault-fallback credential resolution
40
+ - Decibel websocket feed and simple orderbook command behavior.
41
+
42
+ ## [0.1.1] - 2026-02-25
43
+
44
+ ### Fixed
45
+ - Restored Node 18 CI compatibility by deferring `@inquirer/prompts` loading to runtime and using shared prompt wrappers in arb commands
46
+ - Added a Node 18-safe `globalThis.crypto` fallback for onboarding key generation paths
47
+ - Updated README command-surface tests to strip inline shell comments before execution
48
+
49
+ ### Added
50
+ - Exchange onboarding guides under `docs/exchanges/*` and README links to those guides
51
+
10
52
  ### Security
11
53
  - Replaced `elliptic` and `keccak256` with `@noble/curves` and `@noble/hashes` in Orderly adapter (CVE-2025-14505, CVE-2026-2739)
12
54
  - Removed `@types/elliptic` dev dependency
@@ -29,5 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
29
71
  - Risk management: limits, drawdown, position sizing
30
72
  - Execution journal and safety checks
31
73
 
32
- [Unreleased]: https://github.com/raintree-technology/perps/compare/v0.1.0...HEAD
74
+ [Unreleased]: https://github.com/raintree-technology/perps/compare/v0.1.2...HEAD
75
+ [0.1.2]: https://github.com/raintree-technology/perps/releases/tag/v0.1.2
76
+ [0.1.1]: https://github.com/raintree-technology/perps/releases/tag/v0.1.1
33
77
  [0.1.0]: https://github.com/raintree-technology/perps/releases/tag/v0.1.0
package/README.md CHANGED
@@ -57,7 +57,7 @@ perps data ccxt binanceusdm fetchTicker --args '["BTC/USDT:USDT"]' --json
57
57
  Credentials are handled with care:
58
58
 
59
59
  - **Testnet by default** — mainnet requires explicit `--mainnet` flag or `*_NETWORK=mainnet` env var
60
- - **Encrypted at rest** — `perps setup wizard` stores keys in an AES-256-GCM encrypted local vault (`~/.perp/`)
60
+ - **Encrypted at rest** — setup flows (including `perps setup wizard` and `perps setup decibel-key`) store secrets in an AES-256-GCM local vault (`~/.perp/`)
61
61
  - **Owner-only permissions** — all credential files are `chmod 600`, directories `chmod 700`
62
62
  - **Env vars supported** — standard `HYPERLIQUID_PRIVATE_KEY`, `AEVO_SIGNING_KEY`, etc. for CI/automation
63
63
  - **No telemetry, no phoning home** — your keys and trades stay on your machine
@@ -87,6 +87,10 @@ All five adapters implement the same `PerpDEXAdapter` interface — swap `Hyperl
87
87
  # Recommended: interactive wizard
88
88
  perps setup wizard
89
89
 
90
+ # Decibel key helper: guide + validate + store in encrypted vault (env export optional)
91
+ perps setup decibel-key
92
+ perps setup decibel-key --env-file .env.local
93
+
90
94
  # Or set env vars directly
91
95
  export HYPERLIQUID_PRIVATE_KEY="0x..."
92
96
  ```
@@ -98,7 +102,7 @@ export HYPERLIQUID_PRIVATE_KEY="0x..."
98
102
  |----------|-------|---------|
99
103
  | Hyperliquid | `HYPERLIQUID_WALLET_ADDRESS` | `HYPERLIQUID_PRIVATE_KEY` |
100
104
  | Aevo | `AEVO_API_KEY` + `AEVO_API_SECRET` | + `AEVO_SIGNING_KEY` |
101
- | Decibel | `DECIBEL_API_WALLET_ADDRESS` + `DECIBEL_API_BEARER_TOKEN` | + `DECIBEL_API_WALLET_PRIVATE_KEY` |
105
+ | Decibel | Market data: `DECIBEL_API_BEARER_TOKEN`; account reads: + `DECIBEL_API_WALLET_ADDRESS` | + `DECIBEL_API_WALLET_PRIVATE_KEY` |
102
106
  | Orderly | `ORDERLY_ACCOUNT_ID` + `ORDERLY_KEY` + `ORDERLY_SECRET` | + `ORDERLY_TRADING_SECRET` |
103
107
  | Paradex | `PARADEX_ACCOUNT_ADDRESS` + `PARADEX_PRIVATE_KEY` | same |
104
108
 
@@ -125,6 +129,7 @@ perps signal --help # Trade signals
125
129
  perps replay --help # Execution replay
126
130
  perps data --help # Raw exchange data (ccxt, pmxt)
127
131
  perps setup --help # Onboarding wizard
132
+ perps setup decibel-key --help # Decibel bearer key bootstrap + vault storage
128
133
  perps config --help # Settings (show, get, set)
129
134
  perps doctor # Health check
130
135
  ```
@@ -27,7 +27,7 @@ export class DecibelOrderManager {
27
27
  const chainSize = formatSize(sizeUnits, market);
28
28
  const isBuy = side === "buy";
29
29
  const payload = {
30
- function: `${this.packageAddress}::dex_accounts::place_order_to_subaccount`,
30
+ function: `${this.packageAddress}::dex_accounts_entry::place_order_to_subaccount`,
31
31
  typeArguments: [],
32
32
  functionArguments: [
33
33
  this.subaccountAddress,
@@ -68,7 +68,7 @@ export class DecibelOrderManager {
68
68
  }
69
69
  async cancelOrder(orderId, marketAddr) {
70
70
  const payload = {
71
- function: `${this.packageAddress}::dex_accounts::cancel_order_to_subaccount`,
71
+ function: `${this.packageAddress}::dex_accounts_entry::cancel_order_to_subaccount`,
72
72
  typeArguments: [],
73
73
  functionArguments: [this.subaccountAddress, orderId, marketAddr],
74
74
  };
@@ -85,7 +85,7 @@ export class DecibelOrderManager {
85
85
  }
86
86
  async cancelAllOrders(marketAddr) {
87
87
  const payload = {
88
- function: `${this.packageAddress}::dex_accounts::cancel_bulk_order_to_subaccount`,
88
+ function: `${this.packageAddress}::dex_accounts_entry::cancel_bulk_order_to_subaccount`,
89
89
  typeArguments: [],
90
90
  functionArguments: [this.subaccountAddress, marketAddr],
91
91
  };
@@ -103,7 +103,7 @@ export class DecibelOrderManager {
103
103
  async configureUserSettingsForMarket(marketAddr, isCross, userLeverageBps) {
104
104
  const leverageBps = Math.max(1, Math.round(userLeverageBps));
105
105
  const payload = {
106
- function: `${this.packageAddress}::dex_accounts::configure_user_settings_for_market`,
106
+ function: `${this.packageAddress}::dex_accounts_entry::configure_user_settings_for_market`,
107
107
  typeArguments: [],
108
108
  functionArguments: [this.subaccountAddress, marketAddr, isCross, leverageBps],
109
109
  };
@@ -112,7 +112,7 @@ export class DecibelRestClient {
112
112
  };
113
113
  if (requiresAuth) {
114
114
  if (!this.bearerToken) {
115
- throw new Error(`Decibel endpoint '${path}' requires DECIBEL_API_BEARER_TOKEN`);
115
+ throw new Error(`Decibel endpoint '${path}' requires DECIBEL_API_BEARER_TOKEN (Client API key secret from app.decibel.trade -> Geomi -> API Keys).`);
116
116
  }
117
117
  headers.Authorization = `Bearer ${this.bearerToken}`;
118
118
  }
@@ -41,9 +41,21 @@ export class DecibelWsFeed extends EventEmitter {
41
41
  this.shouldReconnect = false;
42
42
  this.clearReconnectTimer();
43
43
  this.stopPing();
44
- this.ws?.removeAllListeners();
45
- this.ws?.close();
44
+ const ws = this.ws;
46
45
  this.ws = null;
46
+ if (!ws) {
47
+ return;
48
+ }
49
+ // Closing a CONNECTING socket can emit an error from ws; keep a no-op handler
50
+ // so shutdown does not crash the process.
51
+ if (ws.readyState === WebSocket.CONNECTING) {
52
+ ws.once("error", () => { });
53
+ ws.terminate();
54
+ return;
55
+ }
56
+ if (ws.readyState === WebSocket.OPEN) {
57
+ ws.close();
58
+ }
47
59
  }
48
60
  isConnected() {
49
61
  return this.ws?.readyState === WebSocket.OPEN;
@@ -14,13 +14,13 @@ const DECIBEL_DEFAULTS = {
14
14
  fullnodeUrl: "https://api.testnet.aptoslabs.com/v1",
15
15
  restUrl: "https://api.testnet.aptoslabs.com/decibel",
16
16
  wsUrl: "wss://api.testnet.aptoslabs.com/decibel/ws",
17
- packageAddress: "0x95254fcb0816b9f3ec71aa4de5f5f7f8f3efeef9239f0d705a4cd3fe2f452de3",
17
+ packageAddress: "0x952535c3049e52f195f26798c2f1340d7dd5100edbe0f464e520a974d16fbe9f",
18
18
  },
19
19
  mainnet: {
20
20
  fullnodeUrl: "https://api.mainnet.aptoslabs.com/v1",
21
21
  restUrl: "https://api.mainnet.aptoslabs.com/decibel",
22
22
  wsUrl: "wss://api.mainnet.aptoslabs.com/decibel/ws",
23
- packageAddress: "0xb8a5788314451ce4d2fbbad32e1bad88d4184b73943b7fe5166eab93cf1a5a95",
23
+ packageAddress: "0x50ead22afd6ffd9769e3b3d6e0e64a2a350d68e8b102c4e72e33d0b8cfdfdb06",
24
24
  },
25
25
  };
26
26
  export class DecibelAdapter {
@@ -809,15 +809,15 @@ export class DecibelAdapter {
809
809
  }
810
810
  ensureAccountReadAuth() {
811
811
  if (!this.accountAddress) {
812
- throw new Error("Decibel account reads require account address (set DECIBEL_API_WALLET_ADDRESS or subaccount).");
812
+ throw new Error("Decibel account reads require account address (set DECIBEL_API_WALLET_ADDRESS or DECIBEL_SUBACCOUNT_ADDRESS).");
813
813
  }
814
814
  if (!this.config?.credentials?.apiBearerToken) {
815
- throw new Error("Decibel account reads require DECIBEL_API_BEARER_TOKEN.");
815
+ throw new Error("Decibel account reads require DECIBEL_API_BEARER_TOKEN (Client API key secret from app.decibel.trade -> Geomi -> API Keys).");
816
816
  }
817
817
  }
818
818
  ensureOrderManager() {
819
819
  if (!this.orderManager) {
820
- throw new Error("Decibel trading requires private key + package settings + subaccount (DECIBEL_API_WALLET_PRIVATE_KEY, DECIBEL_API_WALLET_ADDRESS, DECIBEL_API_BEARER_TOKEN).");
820
+ throw new Error("Decibel trading requires DECIBEL_API_WALLET_PRIVATE_KEY, DECIBEL_API_WALLET_ADDRESS, DECIBEL_API_BEARER_TOKEN, and package/network settings.");
821
821
  }
822
822
  }
823
823
  requireMarket(symbol) {
@@ -3,9 +3,9 @@
3
3
  * Execute basis trades (cash-and-carry arbitrage) on a single exchange
4
4
  */
5
5
  import { createHash } from "node:crypto";
6
- import { confirm } from "@inquirer/prompts";
7
6
  import { getContext, getOutputOptions, getSelectedExchange } from "../../cli/program.js";
8
7
  import { output, outputError } from "../../cli/output.js";
8
+ import { confirm } from "../../lib/prompts.js";
9
9
  import { getExchangeAdapterById } from "../../lib/exchange.js";
10
10
  import { DEFAULT_ARB_SIZE_USD } from "../../lib/constants.js";
11
11
  import { validateAsset, validateSize } from "../../lib/validate.js";
@@ -178,10 +178,7 @@ export function registerArbBasisExecuteCommand(arb) {
178
178
  return;
179
179
  }
180
180
  if (!opts.yes) {
181
- const confirmed = await confirm({
182
- message: "Execute this basis trade?",
183
- default: false,
184
- });
181
+ const confirmed = await confirm("Execute this basis trade?", false);
185
182
  if (!confirmed) {
186
183
  if (isJson) {
187
184
  emitJson({
@@ -2,9 +2,9 @@
2
2
  * Arb Execute Command
3
3
  * Execute delta-neutral funding arbitrage across exchanges
4
4
  */
5
- import { confirm } from "@inquirer/prompts";
6
5
  import { getContext, getOutputOptions } from "../../cli/program.js";
7
6
  import { output, outputError } from "../../cli/output.js";
7
+ import { confirm } from "../../lib/prompts.js";
8
8
  import { getExchangeAdapterById } from "../../lib/exchange.js";
9
9
  import { getLatestFundingRates } from "../../lib/db/funding-history.js";
10
10
  import { getExchangeIdByName, DEFAULT_ARB_SIZE_USD } from "../../lib/constants.js";
@@ -196,10 +196,7 @@ export function registerArbExecuteCommand(arb) {
196
196
  return;
197
197
  }
198
198
  if (!opts.yes) {
199
- const confirmed = await confirm({
200
- message: "Execute this arbitrage trade?",
201
- default: false,
202
- });
199
+ const confirmed = await confirm("Execute this arbitrage trade?", false);
203
200
  if (!confirmed) {
204
201
  if (isJson) {
205
202
  emitJson({
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Simple order book command using the adapter interface
3
3
  */
4
- import { getContext, getOutputOptions } from "../../cli/program.js";
4
+ import { getContext, getOutputOptions, getSelectedExchange } from "../../cli/program.js";
5
5
  import { output, outputError } from "../../cli/output.js";
6
6
  import { getExchangeAdapter } from "../../lib/exchange.js";
7
+ import { getExchangeCredentials } from "../../lib/config.js";
7
8
  export function registerBookSimpleCommand(asset) {
8
9
  asset
9
10
  .command("book <symbol>")
@@ -12,12 +13,19 @@ export function registerBookSimpleCommand(asset) {
12
13
  .action(async function (symbol) {
13
14
  const ctx = getContext(this);
14
15
  const outputOpts = getOutputOptions(this);
16
+ const exchangeId = getSelectedExchange(this);
15
17
  const opts = this.opts();
16
18
  const depth = parseInt(opts.depth, 10);
17
19
  const adapter = getExchangeAdapter();
18
20
  let connected = false;
19
21
  try {
20
- await adapter.connect({ testnet: ctx.config.testnet });
22
+ const credentials = getExchangeCredentials(ctx.config, exchangeId);
23
+ await adapter.connect({
24
+ testnet: ctx.config.testnet,
25
+ rpcUrl: credentials.fullnodeUrl,
26
+ wsUrl: credentials.wsUrl,
27
+ credentials,
28
+ });
21
29
  connected = true;
22
30
  // Normalize symbol (accept BTC, BTC-PERP, btc, etc.)
23
31
  let market = symbol.toUpperCase();
@@ -6,6 +6,7 @@
6
6
  import { output, outputError } from "../../cli/output.js";
7
7
  import { getContext, getOutputOptions, getVerbose } from "../../cli/program.js";
8
8
  import { getAvailableExchanges, getExchangeAdapter, getExchangeAdapterById, } from "../../lib/exchange.js";
9
+ import { getExchangeCredentials } from "../../lib/config.js";
9
10
  import { pLimit } from "../../lib/rate-limit.js";
10
11
  export function registerMarketsLsSimpleCommand(markets) {
11
12
  markets
@@ -38,7 +39,8 @@ async function handleSingleExchange(ctx, outputOpts, opts, limit = 0) {
38
39
  const adapter = getExchangeAdapter();
39
40
  let connected = false;
40
41
  try {
41
- await adapter.connect({ testnet: ctx.config.testnet });
42
+ const credentials = getExchangeCredentials(ctx.config, adapter.info.id);
43
+ await adapter.connect({ testnet: ctx.config.testnet, credentials });
42
44
  connected = true;
43
45
  const markets = await adapter.getMarkets();
44
46
  let filtered = markets;
@@ -88,7 +90,8 @@ async function fetchAllExchangeMarkets(ctx) {
88
90
  const connectedAdapters = [];
89
91
  const tasks = exchangeIds.map((id) => async () => {
90
92
  const adapter = getExchangeAdapterById(id);
91
- await adapter.connect({ testnet: ctx.config.testnet });
93
+ const credentials = getExchangeCredentials(ctx.config, id);
94
+ await adapter.connect({ testnet: ctx.config.testnet, credentials });
92
95
  connectedAdapters.push(adapter);
93
96
  const markets = await adapter.getMarkets();
94
97
  return { id, name: adapter.info.name, markets };
@@ -1,14 +1,81 @@
1
- import { existsSync, writeFileSync } from "node:fs";
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
3
  import { resolve } from "node:path";
3
4
  import { getOutputOptions } from "../../cli/program.js";
4
5
  import { output, outputError, outputSuccess } from "../../cli/output.js";
5
6
  import { withJsonContract } from "../../lib/contracts.js";
6
7
  import { createAccount, getAccountByAliasForExchange, } from "../../lib/db/index.js";
7
8
  import { privateKeyToAccount } from "viem/accounts";
8
- import { getCredentialVaultSummary, saveOnboardingProfileToVault, } from "../../lib/credential-vault.js";
9
+ import { getCredentialVaultSummary, getLatestCredentialVaultSecret, saveSecretToCredentialVault, saveOnboardingProfileToVault, } from "../../lib/credential-vault.js";
9
10
  import { ALL_SETUP_CHAINS, ALL_SETUP_EXCHANGES, buildOnboardingEnvFile, parseSetupChains, parseSetupExchanges, parseSetupMode, resolveExchangesFromChains, runOnboardingStateMachine, } from "../../lib/onboarding.js";
10
- import { confirm, multiSelect, select } from "../../lib/prompts.js";
11
+ import { confirm, multiSelect, pressEnterOrEsc, prompt, select } from "../../lib/prompts.js";
11
12
  import { hardenPrivateFile, PRIVATE_FILE_MODE } from "../../lib/fs-security.js";
13
+ import { FetchError, fetchWithTimeout } from "../../lib/fetch.js";
14
+ const DECIBEL_REST_DEFAULTS = {
15
+ mainnet: "https://api.mainnet.aptoslabs.com/decibel",
16
+ testnet: "https://api.testnet.aptoslabs.com/decibel",
17
+ };
18
+ function normalizeText(value) {
19
+ if (!value)
20
+ return undefined;
21
+ const trimmed = value.trim();
22
+ return trimmed.length > 0 ? trimmed : undefined;
23
+ }
24
+ function normalizeNetwork(value) {
25
+ const normalized = normalizeText(value)?.toLowerCase();
26
+ if (normalized === "testnet")
27
+ return "testnet";
28
+ return "mainnet";
29
+ }
30
+ function resolveDecibelNetwork(opts, command) {
31
+ if (opts.network) {
32
+ return normalizeNetwork(opts.network);
33
+ }
34
+ const globalOpts = command.optsWithGlobals();
35
+ if (globalOpts.mainnet)
36
+ return "mainnet";
37
+ if (globalOpts.testnet)
38
+ return "testnet";
39
+ return normalizeNetwork(process.env.DECIBEL_NETWORK);
40
+ }
41
+ function resolveDecibelRestUrl(opts, command) {
42
+ const explicit = normalizeText(opts.restUrl) ?? normalizeText(process.env.DECIBEL_REST_URL);
43
+ if (explicit) {
44
+ return explicit.replace(/\/+$/, "");
45
+ }
46
+ return DECIBEL_REST_DEFAULTS[resolveDecibelNetwork(opts, command)];
47
+ }
48
+ function openExternalUrl(url) {
49
+ const platform = process.platform;
50
+ if (platform === "darwin") {
51
+ execFileSync("open", [url], { stdio: "ignore" });
52
+ return;
53
+ }
54
+ if (platform === "win32") {
55
+ execFileSync("cmd", ["/c", "start", "", url], { stdio: "ignore" });
56
+ return;
57
+ }
58
+ execFileSync("xdg-open", [url], { stdio: "ignore" });
59
+ }
60
+ function upsertEnvVar(content, key, value) {
61
+ const lines = content.split(/\r?\n/);
62
+ const matcher = new RegExp(`^\\s*#?\\s*${key}=`);
63
+ let replaced = false;
64
+ const next = lines.map((line) => {
65
+ if (matcher.test(line)) {
66
+ replaced = true;
67
+ return `${key}=${value}`;
68
+ }
69
+ return line;
70
+ });
71
+ if (!replaced) {
72
+ if (next.length > 0 && next[next.length - 1].trim() !== "") {
73
+ next.push("");
74
+ }
75
+ next.push(`${key}=${value}`);
76
+ }
77
+ return `${next.join("\n").replace(/\n+$/, "")}\n`;
78
+ }
12
79
  const MODE_DESCRIPTIONS = {
13
80
  "mainnet-data": "No credentials. Use real mainnet data immediately.",
14
81
  "testnet-execution": "Force selected exchanges to testnet for all commands.",
@@ -37,8 +104,11 @@ function normalizeAuthOptions(opts) {
37
104
  const orderlyChainId = opts.orderlyChainId ??
38
105
  process.env.ORDERLY_CHAIN_ID ??
39
106
  process.env.ORDERLY_TESTNET_FAUCET_CHAIN_ID;
107
+ const vaultDecibelBearer = getLatestCredentialVaultSecret("DECIBEL_API_BEARER_TOKEN");
40
108
  return {
41
- decibelBearerToken: opts.decibelBearerToken ?? process.env.DECIBEL_API_BEARER_TOKEN,
109
+ decibelBearerToken: opts.decibelBearerToken ??
110
+ process.env.DECIBEL_API_BEARER_TOKEN ??
111
+ vaultDecibelBearer,
42
112
  aevoApiKey: opts.aevoApiKey ?? process.env.AEVO_API_KEY,
43
113
  aevoApiSecret: opts.aevoApiSecret ?? process.env.AEVO_API_SECRET,
44
114
  aevoAccountPrivateKey: (opts.aevoAccountPrivateKey ??
@@ -302,6 +372,224 @@ function printWizardSummary(args) {
302
372
  }
303
373
  export function registerSetupCommands(program) {
304
374
  const setup = program.command("setup").description("Onboarding and environment setup");
375
+ setup
376
+ .command("decibel-key")
377
+ .description("Bootstrap a Decibel Client API key flow, validate it, and store it in the encrypted vault")
378
+ .option("--token <token>", "Decibel Client API key secret (overrides DECIBEL_API_BEARER_TOKEN)")
379
+ .option("--wallet-address <address>", "Account address to check with account_overview")
380
+ .option("--subaccount-address <address>", "Subaccount address to check with account_overview")
381
+ .option("--network <network>", "Decibel network (mainnet or testnet)")
382
+ .option("--rest-url <url>", "Decibel REST URL override")
383
+ .option("--env-file <path>", "Write/update DECIBEL_API_BEARER_TOKEN in this env file")
384
+ .option("--no-vault", "Skip encrypted vault persistence for DECIBEL_API_BEARER_TOKEN")
385
+ .option("--no-open-browser", "Do not open Geomi/API keys pages automatically")
386
+ .option("--require-account", "Fail if account_overview check fails")
387
+ .option("--yes", "Skip interactive confirmations")
388
+ .action(async function () {
389
+ const outputOpts = getOutputOptions(this);
390
+ const opts = this.opts();
391
+ const hasInteractiveTty = process.stdin.isTTY && process.stdout.isTTY;
392
+ const assumeDefaults = Boolean(opts.yes) || !hasInteractiveTty;
393
+ const network = resolveDecibelNetwork(opts, this);
394
+ const restUrl = resolveDecibelRestUrl(opts, this);
395
+ const persistToVault = opts.vault ?? true;
396
+ try {
397
+ const vaultToken = normalizeText(getLatestCredentialVaultSecret("DECIBEL_API_BEARER_TOKEN"));
398
+ let tokenSource = "interactive";
399
+ let token = normalizeText(opts.token);
400
+ if (token) {
401
+ tokenSource = "cli";
402
+ }
403
+ else {
404
+ token = normalizeText(process.env.DECIBEL_API_BEARER_TOKEN);
405
+ if (token) {
406
+ tokenSource = "env";
407
+ }
408
+ else if (vaultToken) {
409
+ token = vaultToken;
410
+ tokenSource = "vault";
411
+ }
412
+ }
413
+ const usedInteractiveCapture = !token;
414
+ if (!token) {
415
+ if (!hasInteractiveTty) {
416
+ throw new Error("No Decibel API key found. Pass --token, set DECIBEL_API_BEARER_TOKEN, or run once interactively to store it in the encrypted vault.");
417
+ }
418
+ console.log("Decibel key generation is currently handled in Geomi (web UI).");
419
+ if (opts.openBrowser ?? true) {
420
+ let shouldOpen = true;
421
+ if (!assumeDefaults) {
422
+ shouldOpen = await pressEnterOrEsc("Press Enter to open Decibel + Geomi API keys pages (or Esc to skip)");
423
+ }
424
+ if (shouldOpen) {
425
+ try {
426
+ openExternalUrl("https://app.decibel.trade");
427
+ openExternalUrl("https://app.decibel.trade/geomi");
428
+ }
429
+ catch {
430
+ // Fall through with manual links.
431
+ }
432
+ }
433
+ }
434
+ console.log("");
435
+ console.log("Create/copy your Key Secret from Geomi -> API Keys, then paste it below.");
436
+ token = normalizeText(await prompt("Paste DECIBEL_API_BEARER_TOKEN (Key Secret):"));
437
+ if (!token) {
438
+ throw new Error("No Decibel API key provided.");
439
+ }
440
+ tokenSource = "interactive";
441
+ }
442
+ const headers = {
443
+ Authorization: `Bearer ${token}`,
444
+ Origin: "https://app.decibel.trade",
445
+ };
446
+ const marketsUrl = new URL("api/v1/markets", `${restUrl.replace(/\/+$/, "")}/`).toString();
447
+ const markets = await fetchWithTimeout(marketsUrl, {
448
+ headers,
449
+ retries: 0,
450
+ });
451
+ const marketCount = Array.isArray(markets) ? markets.length : 0;
452
+ if (marketCount === 0) {
453
+ throw new Error("Decibel token validated but returned no markets.");
454
+ }
455
+ const accountAddress = normalizeText(opts.subaccountAddress) ??
456
+ normalizeText(opts.walletAddress) ??
457
+ normalizeText(process.env.DECIBEL_SUBACCOUNT_ADDRESS) ??
458
+ normalizeText(process.env.DECIBEL_API_WALLET_ADDRESS);
459
+ let accountCheck;
460
+ if (accountAddress) {
461
+ const accountUrl = new URL("api/v1/account_overview", `${restUrl.replace(/\/+$/, "")}/`);
462
+ accountUrl.searchParams.set("account", accountAddress);
463
+ try {
464
+ await fetchWithTimeout(accountUrl.toString(), {
465
+ headers,
466
+ retries: 0,
467
+ });
468
+ accountCheck = {
469
+ status: "ok",
470
+ detail: `account_overview succeeded for ${accountAddress}`,
471
+ };
472
+ }
473
+ catch (err) {
474
+ const detail = err instanceof Error ? err.message : String(err);
475
+ accountCheck = {
476
+ status: "failed",
477
+ detail: `account_overview failed for ${accountAddress}: ${detail}`,
478
+ };
479
+ if (opts.requireAccount) {
480
+ throw new Error(accountCheck.detail);
481
+ }
482
+ }
483
+ }
484
+ else {
485
+ accountCheck = {
486
+ status: "skipped",
487
+ detail: "No DECIBEL_API_WALLET_ADDRESS/DECIBEL_SUBACCOUNT_ADDRESS provided.",
488
+ };
489
+ }
490
+ let vaultRef;
491
+ let vaultStatus = "disabled";
492
+ let vaultPath;
493
+ if (persistToVault) {
494
+ vaultPath = getCredentialVaultSummary().path;
495
+ if (vaultToken === token) {
496
+ vaultStatus = "unchanged";
497
+ }
498
+ else {
499
+ vaultRef = saveSecretToCredentialVault("DECIBEL_API_BEARER_TOKEN", token, {
500
+ mode: "setup.decibel-key",
501
+ exchange: "decibel",
502
+ });
503
+ vaultStatus = "stored";
504
+ vaultPath = vaultRef.path;
505
+ }
506
+ }
507
+ let envFilePath;
508
+ if (opts.envFile) {
509
+ envFilePath = resolve(opts.envFile.trim());
510
+ const existing = existsSync(envFilePath) ? readFileSync(envFilePath, "utf8") : "";
511
+ const updated = upsertEnvVar(existing, "DECIBEL_API_BEARER_TOKEN", token);
512
+ writeFileSync(envFilePath, updated, { mode: PRIVATE_FILE_MODE });
513
+ hardenPrivateFile(envFilePath);
514
+ }
515
+ const nextSteps = envFilePath
516
+ ? [
517
+ `source ${envFilePath}`,
518
+ "perps markets ls -e decibel",
519
+ "perps asset book BTC-PERP -e decibel",
520
+ ]
521
+ : persistToVault
522
+ ? [
523
+ "perps markets ls -e decibel",
524
+ "perps asset book BTC-PERP -e decibel",
525
+ ]
526
+ : [
527
+ "Export DECIBEL_API_BEARER_TOKEN in your shell (or re-run without --no-vault).",
528
+ "perps markets ls -e decibel",
529
+ "perps asset book BTC-PERP -e decibel",
530
+ ];
531
+ const payload = withJsonContract("setup.decibel_key.result", {
532
+ network,
533
+ restUrl,
534
+ tokenSource,
535
+ tokenCapturedInteractively: usedInteractiveCapture,
536
+ tokenValidated: true,
537
+ marketCount,
538
+ accountCheck,
539
+ vault: {
540
+ enabled: persistToVault,
541
+ status: vaultStatus,
542
+ id: vaultRef?.id,
543
+ path: vaultPath,
544
+ },
545
+ envFile: envFilePath,
546
+ envVar: "DECIBEL_API_BEARER_TOKEN",
547
+ nextSteps,
548
+ });
549
+ if (outputOpts.json) {
550
+ output(payload, outputOpts);
551
+ return;
552
+ }
553
+ outputSuccess("Decibel API key validated successfully.");
554
+ console.log("");
555
+ console.log(`Network: ${network}`);
556
+ console.log(`REST URL: ${restUrl}`);
557
+ console.log(`Markets checked: ${marketCount}`);
558
+ console.log(`Token source: ${tokenSource}`);
559
+ if (accountCheck) {
560
+ console.log(`Account check: [${accountCheck.status}] ${accountCheck.detail}`);
561
+ }
562
+ if (persistToVault && vaultPath) {
563
+ if (vaultStatus === "stored" && vaultRef) {
564
+ console.log(`Vault: stored ${vaultRef.id} (${vaultPath})`);
565
+ }
566
+ else {
567
+ console.log(`Vault: unchanged (${vaultPath})`);
568
+ }
569
+ }
570
+ else {
571
+ console.log("Vault: skipped (--no-vault)");
572
+ }
573
+ if (envFilePath) {
574
+ console.log(`Env file: ${envFilePath}`);
575
+ }
576
+ console.log("");
577
+ console.log("Next steps:");
578
+ for (const [index, step] of nextSteps.entries()) {
579
+ console.log(`${index + 1}) ${step}`);
580
+ }
581
+ console.log("");
582
+ }
583
+ catch (err) {
584
+ const message = err instanceof FetchError
585
+ ? `Decibel key bootstrap failed: ${err.message}`
586
+ : err instanceof Error
587
+ ? err.message
588
+ : String(err);
589
+ outputError(message);
590
+ process.exit(1);
591
+ }
592
+ });
305
593
  setup
306
594
  .command("wizard")
307
595
  .description("Interactive setup for mainnet data defaults and testnet onboarding")
package/dist/index.js CHANGED
File without changes
@@ -1,5 +1,6 @@
1
1
  import { privateKeyToAccount } from "viem/accounts";
2
2
  import { getDefaultAccountForExchange } from "./db/index.js";
3
+ import { getLatestCredentialVaultSecret } from "./credential-vault.js";
3
4
  import { parseEnv } from "./schema.js";
4
5
  import { recordTelemetryMetric } from "./telemetry.js";
5
6
  const DECIBEL_NETWORK_DEFAULTS = {
@@ -7,13 +8,13 @@ const DECIBEL_NETWORK_DEFAULTS = {
7
8
  fullnodeUrl: "https://api.testnet.aptoslabs.com/v1",
8
9
  restUrl: "https://api.testnet.aptoslabs.com/decibel",
9
10
  wsUrl: "wss://api.testnet.aptoslabs.com/decibel/ws",
10
- packageAddress: "0x95254fcb0816b9f3ec71aa4de5f5f7f8f3efeef9239f0d705a4cd3fe2f452de3",
11
+ packageAddress: "0x952535c3049e52f195f26798c2f1340d7dd5100edbe0f464e520a974d16fbe9f",
11
12
  },
12
13
  mainnet: {
13
14
  fullnodeUrl: "https://api.mainnet.aptoslabs.com/v1",
14
15
  restUrl: "https://api.mainnet.aptoslabs.com/decibel",
15
16
  wsUrl: "wss://api.mainnet.aptoslabs.com/decibel/ws",
16
- packageAddress: "0xb8a5788314451ce4d2fbbad32e1bad88d4184b73943b7fe5166eab93cf1a5a95",
17
+ packageAddress: "0x50ead22afd6ffd9769e3b3d6e0e64a2a350d68e8b102c4e72e33d0b8cfdfdb06",
17
18
  },
18
19
  };
19
20
  const ORDERLY_DEFAULTS = {
@@ -84,10 +85,14 @@ function resolveHyperliquidSettings(testnet, env) {
84
85
  function resolveDecibelSettings(testnet, env) {
85
86
  const network = resolveExchangeNetwork(testnet, env.DECIBEL_NETWORK);
86
87
  const defaults = DECIBEL_NETWORK_DEFAULTS[network];
88
+ const envBearerToken = env.DECIBEL_API_BEARER_TOKEN;
89
+ const vaultBearerToken = envBearerToken
90
+ ? undefined
91
+ : getLatestCredentialVaultSecret("DECIBEL_API_BEARER_TOKEN");
87
92
  const credentials = {
88
93
  apiWalletPrivateKey: env.DECIBEL_API_WALLET_PRIVATE_KEY,
89
94
  apiWalletAddress: env.DECIBEL_API_WALLET_ADDRESS,
90
- apiBearerToken: env.DECIBEL_API_BEARER_TOKEN,
95
+ apiBearerToken: envBearerToken ?? vaultBearerToken,
91
96
  subaccountAddress: env.DECIBEL_SUBACCOUNT_ADDRESS,
92
97
  };
93
98
  const hasAnyCredentials = !!credentials.apiWalletPrivateKey ||
@@ -291,7 +296,7 @@ export function getExchangeCredentials(config, exchangeId, requirement = {}) {
291
296
  if (requirement.requireReadAccess &&
292
297
  (!credentials.apiBearerToken || !credentials.accountAddress)) {
293
298
  recordAuth("read", "failed");
294
- throw new Error("Decibel authenticated reads require DECIBEL_API_WALLET_ADDRESS + DECIBEL_API_BEARER_TOKEN.");
299
+ throw new Error("Decibel authenticated reads require DECIBEL_API_WALLET_ADDRESS + DECIBEL_API_BEARER_TOKEN (Client API key secret from app.decibel.trade -> Geomi -> API Keys).");
295
300
  }
296
301
  if (requirement.requireReadAccess) {
297
302
  recordAuth("read", "success");
@@ -302,7 +307,7 @@ export function getExchangeCredentials(config, exchangeId, requirement = {}) {
302
307
  !credentials.apiBearerToken ||
303
308
  !credentials.packageAddress)) {
304
309
  recordAuth("trade", "failed");
305
- throw new Error("Decibel trading requires DECIBEL_API_WALLET_PRIVATE_KEY, DECIBEL_API_WALLET_ADDRESS, DECIBEL_API_BEARER_TOKEN, and package/network settings.");
310
+ throw new Error("Decibel trading requires DECIBEL_API_WALLET_PRIVATE_KEY, DECIBEL_API_WALLET_ADDRESS, and DECIBEL_API_BEARER_TOKEN (Client API key secret from app.decibel.trade -> Geomi -> API Keys), plus package/network settings.");
306
311
  }
307
312
  if (requirement.requireTrading) {
308
313
  recordAuth("trade", "success");
@@ -20,3 +20,11 @@ export declare function getCredentialVaultSummary(): {
20
20
  entries: number;
21
21
  updatedAt?: string;
22
22
  };
23
+ export declare function getLatestCredentialVaultSecret(secretKey: string): string | undefined;
24
+ export declare function saveSecretToCredentialVault(secretKey: string, secretValue: string, options?: {
25
+ mode?: string;
26
+ exchange?: string;
27
+ }): {
28
+ id: string;
29
+ path: string;
30
+ };
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { CREDENTIAL_VAULT_PATH } from "./paths.js";
4
- import { encryptSecret } from "./secrets.js";
4
+ import { decryptSecret, encryptSecret } from "./secrets.js";
5
5
  import { ensurePrivateDir, hardenPrivateFile, PRIVATE_FILE_MODE } from "./fs-security.js";
6
6
  const VAULT_SCHEMA_VERSION = 1;
7
7
  function createVault() {
@@ -78,6 +78,25 @@ function buildId(profile) {
78
78
  const suffix = Math.random().toString(36).slice(2, 8);
79
79
  return `onb-${day}-${suffix}`;
80
80
  }
81
+ function buildSecretId(now) {
82
+ const day = now.toISOString().slice(0, 10).replace(/-/g, "");
83
+ const suffix = Math.random().toString(36).slice(2, 8);
84
+ return `sec-${day}-${suffix}`;
85
+ }
86
+ function normalizeSecretKey(secretKey) {
87
+ const normalized = secretKey.trim().toUpperCase();
88
+ if (!/^[A-Z0-9_]+$/.test(normalized)) {
89
+ throw new Error(`Invalid vault secret key '${secretKey}'`);
90
+ }
91
+ return normalized;
92
+ }
93
+ function normalizeSecretValue(secretValue) {
94
+ const normalized = secretValue.trim();
95
+ if (!normalized) {
96
+ throw new Error("Vault secret value cannot be empty");
97
+ }
98
+ return normalized;
99
+ }
81
100
  export function saveOnboardingProfileToVault(profile) {
82
101
  const vault = readVault();
83
102
  const id = buildId(profile);
@@ -107,3 +126,44 @@ export function getCredentialVaultSummary() {
107
126
  updatedAt: vault.updatedAt,
108
127
  };
109
128
  }
129
+ export function getLatestCredentialVaultSecret(secretKey) {
130
+ const normalizedKey = normalizeSecretKey(secretKey);
131
+ const vault = readVault();
132
+ for (const entry of vault.entries) {
133
+ const encryptedValue = entry?.encrypted?.[normalizedKey];
134
+ if (typeof encryptedValue !== "string" || encryptedValue.trim().length === 0) {
135
+ continue;
136
+ }
137
+ try {
138
+ const plaintext = decryptSecret(encryptedValue).trim();
139
+ if (plaintext.length > 0) {
140
+ return plaintext;
141
+ }
142
+ }
143
+ catch {
144
+ // Skip malformed entries and continue searching older entries.
145
+ }
146
+ }
147
+ return undefined;
148
+ }
149
+ export function saveSecretToCredentialVault(secretKey, secretValue, options = {}) {
150
+ const key = normalizeSecretKey(secretKey);
151
+ const value = normalizeSecretValue(secretValue);
152
+ const now = new Date();
153
+ const vault = readVault();
154
+ const id = buildSecretId(now);
155
+ const entry = {
156
+ id,
157
+ createdAt: now.toISOString(),
158
+ mode: options.mode ?? "manual_secret",
159
+ exchanges: options.exchange ? [options.exchange] : [],
160
+ wallets: {},
161
+ encrypted: {
162
+ [key]: encryptSecret(value),
163
+ },
164
+ };
165
+ vault.entries.unshift(entry);
166
+ vault.updatedAt = now.toISOString();
167
+ writeVault(vault);
168
+ return { id, path: CREDENTIAL_VAULT_PATH };
169
+ }
@@ -1,4 +1,4 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { randomBytes, webcrypto } from "node:crypto";
2
2
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { Ed25519Account, Ed25519PrivateKey } from "@aptos-labs/ts-sdk";
@@ -40,6 +40,10 @@ const AEVO_TESTNET_CHAIN_ID = 11155111;
40
40
  const PARADEX_TESTNET_API = "https://api.testnet.paradex.trade/v1";
41
41
  const SECONDS_PER_DAY = 86_400;
42
42
  const MILLISECONDS_PER_DAY = 86_400_000;
43
+ // Node 18 may not expose globalThis.crypto in every runtime context.
44
+ if (!globalThis.crypto) {
45
+ globalThis.crypto = webcrypto;
46
+ }
43
47
  function dedupe(values, order) {
44
48
  const set = new Set(values);
45
49
  return order.filter((item) => set.has(item));
@@ -608,7 +612,7 @@ export async function bootstrapOnboardingAuth(profile, options = {}) {
608
612
  exchange: "decibel",
609
613
  service: "decibel auth",
610
614
  status: "ok",
611
- detail: "using DECIBEL_API_BEARER_TOKEN",
615
+ detail: "using DECIBEL_API_BEARER_TOKEN (Client API key secret)",
612
616
  });
613
617
  }
614
618
  else {
@@ -616,7 +620,7 @@ export async function bootstrapOnboardingAuth(profile, options = {}) {
616
620
  exchange: "decibel",
617
621
  service: "decibel auth",
618
622
  status: "manual",
619
- detail: "still requires DECIBEL_API_BEARER_TOKEN",
623
+ detail: "still requires DECIBEL_API_BEARER_TOKEN (Client API key secret from app.decibel.trade -> Geomi -> API Keys)",
620
624
  });
621
625
  }
622
626
  }
@@ -833,7 +837,7 @@ const ONBOARDING_PROVIDERS = {
833
837
  manualAuthFollowUp: {
834
838
  exchange: "decibel",
835
839
  env: ["DECIBEL_API_BEARER_TOKEN"],
836
- detail: "Set a Decibel bearer token for authenticated reads/trading after wallet generation.",
840
+ detail: "Create a key in app.decibel.trade -> Geomi -> API Keys, copy the Key Secret, and set DECIBEL_API_BEARER_TOKEN.",
837
841
  },
838
842
  fundingTask: ({ profile, options }) => requestAptosFaucet({
839
843
  aptosAddress: profile.wallets.aptosAddress,
@@ -1,9 +1,16 @@
1
- import { input, select as inquirerSelect, confirm as inquirerConfirm, checkbox, } from "@inquirer/prompts";
2
1
  import { inquirerTheme, highlighter } from "./ui-tokens.js";
2
+ let promptsModule = null;
3
+ async function loadPrompts() {
4
+ if (promptsModule)
5
+ return promptsModule;
6
+ promptsModule = await import("@inquirer/prompts");
7
+ return promptsModule;
8
+ }
3
9
  /**
4
10
  * Prompt for text input
5
11
  */
6
12
  export async function prompt(question) {
13
+ const { input } = await loadPrompts();
7
14
  const answer = await input({
8
15
  message: question,
9
16
  theme: inquirerTheme,
@@ -14,6 +21,7 @@ export async function prompt(question) {
14
21
  * Prompt for selection from a list of options with arrow key navigation
15
22
  */
16
23
  export async function select(question, options) {
24
+ const { select: inquirerSelect } = await loadPrompts();
17
25
  const result = await inquirerSelect({
18
26
  message: question,
19
27
  choices: options.map((opt) => ({
@@ -29,6 +37,7 @@ export async function select(question, options) {
29
37
  * Prompt for multiple selections with checkboxes
30
38
  */
31
39
  export async function multiSelect(question, options) {
40
+ const { checkbox } = await loadPrompts();
32
41
  const results = await checkbox({
33
42
  message: question,
34
43
  choices: options.map((opt) => ({
@@ -44,6 +53,7 @@ export async function multiSelect(question, options) {
44
53
  * Prompt for yes/no confirmation
45
54
  */
46
55
  export async function confirm(question, defaultValue = false) {
56
+ const { confirm: inquirerConfirm } = await loadPrompts();
47
57
  return inquirerConfirm({
48
58
  message: question,
49
59
  default: defaultValue,
@@ -54,6 +64,7 @@ export async function confirm(question, defaultValue = false) {
54
64
  * Wait for user to press Enter
55
65
  */
56
66
  export async function waitForEnter(message = "Press Enter to continue...") {
67
+ const { input } = await loadPrompts();
57
68
  await input({
58
69
  message,
59
70
  theme: {
@@ -60,80 +60,80 @@ export declare const envSchema: z.ZodEffects<z.ZodObject<{
60
60
  EXECUTION_TAKE_PROFIT_PCT: number;
61
61
  EXECUTION_SPREAD_OFFSET_PCT: number;
62
62
  OPERATOR_HEARTBEAT_INTERVAL_MS: number;
63
- DECIBEL_REST_URL?: string | undefined;
64
- DECIBEL_WS_URL?: string | undefined;
65
- DECIBEL_FULLNODE_URL?: string | undefined;
66
- AEVO_REST_URL?: string | undefined;
67
63
  ORDERLY_REST_URL?: string | undefined;
68
- PARADEX_REST_URL?: string | undefined;
69
- HYPERLIQUID_PRIVATE_KEY?: string | undefined;
70
- HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
71
- HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
72
- DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
73
- DECIBEL_API_WALLET_ADDRESS?: string | undefined;
64
+ AEVO_REST_URL?: string | undefined;
65
+ PARADEX_ACCOUNT_ADDRESS?: string | undefined;
66
+ PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
74
67
  DECIBEL_API_BEARER_TOKEN?: string | undefined;
75
- DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
76
- DECIBEL_PACKAGE_ADDRESS?: string | undefined;
77
- DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
78
68
  AEVO_API_KEY?: string | undefined;
79
69
  AEVO_API_SECRET?: string | undefined;
80
- AEVO_SIGNING_KEY?: string | undefined;
81
70
  AEVO_ACCOUNT_PRIVATE_KEY?: string | undefined;
82
- AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
83
- ORDERLY_BROKER_ID?: string | undefined;
84
- ORDERLY_CHAIN_ID?: string | undefined;
85
71
  ORDERLY_ACCOUNT_ID?: string | undefined;
86
72
  ORDERLY_KEY?: string | undefined;
87
73
  ORDERLY_SECRET?: string | undefined;
88
- ORDERLY_TRADING_KEY?: string | undefined;
89
- ORDERLY_TRADING_SECRET?: string | undefined;
90
- ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
74
+ ORDERLY_BROKER_ID?: string | undefined;
91
75
  PARADEX_API_BEARER_TOKEN?: string | undefined;
92
- PARADEX_ACCOUNT_ADDRESS?: string | undefined;
76
+ HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
77
+ DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
78
+ AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
79
+ ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
80
+ PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
81
+ PARADEX_REST_URL?: string | undefined;
82
+ HYPERLIQUID_PRIVATE_KEY?: string | undefined;
83
+ DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
84
+ AEVO_SIGNING_KEY?: string | undefined;
85
+ ORDERLY_TRADING_SECRET?: string | undefined;
93
86
  PARADEX_PRIVATE_KEY?: string | undefined;
94
- PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
87
+ DECIBEL_REST_URL?: string | undefined;
88
+ DECIBEL_WS_URL?: string | undefined;
89
+ DECIBEL_FULLNODE_URL?: string | undefined;
90
+ HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
91
+ DECIBEL_API_WALLET_ADDRESS?: string | undefined;
92
+ DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
93
+ DECIBEL_PACKAGE_ADDRESS?: string | undefined;
94
+ ORDERLY_CHAIN_ID?: string | undefined;
95
+ ORDERLY_TRADING_KEY?: string | undefined;
95
96
  PARADEX_CHAIN_ID?: string | undefined;
96
- PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
97
97
  PERPS_ALLOW_UNTRUSTED_ENDPOINTS?: "0" | "1" | "true" | "false" | undefined;
98
98
  PERPS_BLOCKED_EXCHANGES?: string | undefined;
99
99
  PERPS_BLOCKED_MARKETS?: string | undefined;
100
100
  PERPS_BLOCKED_MARKET_PATTERNS?: string | undefined;
101
101
  PERPS_REPUTATION_BLOCKLIST_FILE?: string | undefined;
102
102
  }, {
103
- DECIBEL_REST_URL?: string | undefined;
104
- DECIBEL_WS_URL?: string | undefined;
105
- DECIBEL_FULLNODE_URL?: string | undefined;
106
- AEVO_REST_URL?: string | undefined;
107
103
  ORDERLY_REST_URL?: string | undefined;
108
- PARADEX_REST_URL?: string | undefined;
109
- HYPERLIQUID_PRIVATE_KEY?: string | undefined;
110
- HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
111
- HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
112
- DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
113
- DECIBEL_API_WALLET_ADDRESS?: string | undefined;
104
+ AEVO_REST_URL?: string | undefined;
105
+ PARADEX_ACCOUNT_ADDRESS?: string | undefined;
106
+ PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
114
107
  DECIBEL_API_BEARER_TOKEN?: string | undefined;
115
- DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
116
- DECIBEL_PACKAGE_ADDRESS?: string | undefined;
117
- DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
118
108
  AEVO_API_KEY?: string | undefined;
119
109
  AEVO_API_SECRET?: string | undefined;
120
- AEVO_SIGNING_KEY?: string | undefined;
121
110
  AEVO_ACCOUNT_PRIVATE_KEY?: string | undefined;
122
- AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
123
- ORDERLY_BROKER_ID?: string | undefined;
124
- ORDERLY_CHAIN_ID?: string | undefined;
125
111
  ORDERLY_ACCOUNT_ID?: string | undefined;
126
112
  ORDERLY_KEY?: string | undefined;
127
113
  ORDERLY_SECRET?: string | undefined;
128
- ORDERLY_TRADING_KEY?: string | undefined;
129
- ORDERLY_TRADING_SECRET?: string | undefined;
130
- ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
114
+ ORDERLY_BROKER_ID?: string | undefined;
131
115
  PARADEX_API_BEARER_TOKEN?: string | undefined;
132
- PARADEX_ACCOUNT_ADDRESS?: string | undefined;
116
+ HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
117
+ DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
118
+ AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
119
+ ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
120
+ PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
121
+ PARADEX_REST_URL?: string | undefined;
122
+ HYPERLIQUID_PRIVATE_KEY?: string | undefined;
123
+ DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
124
+ AEVO_SIGNING_KEY?: string | undefined;
125
+ ORDERLY_TRADING_SECRET?: string | undefined;
133
126
  PARADEX_PRIVATE_KEY?: string | undefined;
134
- PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
127
+ DECIBEL_REST_URL?: string | undefined;
128
+ DECIBEL_WS_URL?: string | undefined;
129
+ DECIBEL_FULLNODE_URL?: string | undefined;
130
+ HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
131
+ DECIBEL_API_WALLET_ADDRESS?: string | undefined;
132
+ DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
133
+ DECIBEL_PACKAGE_ADDRESS?: string | undefined;
134
+ ORDERLY_CHAIN_ID?: string | undefined;
135
+ ORDERLY_TRADING_KEY?: string | undefined;
135
136
  PARADEX_CHAIN_ID?: string | undefined;
136
- PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
137
137
  RISK_MAX_POSITION_SIZE_USD?: number | undefined;
138
138
  RISK_MAX_TOTAL_EXPOSURE_USD?: number | undefined;
139
139
  RISK_MAX_LEVERAGE?: number | undefined;
@@ -160,80 +160,80 @@ export declare const envSchema: z.ZodEffects<z.ZodObject<{
160
160
  EXECUTION_TAKE_PROFIT_PCT: number;
161
161
  EXECUTION_SPREAD_OFFSET_PCT: number;
162
162
  OPERATOR_HEARTBEAT_INTERVAL_MS: number;
163
- DECIBEL_REST_URL?: string | undefined;
164
- DECIBEL_WS_URL?: string | undefined;
165
- DECIBEL_FULLNODE_URL?: string | undefined;
166
- AEVO_REST_URL?: string | undefined;
167
163
  ORDERLY_REST_URL?: string | undefined;
168
- PARADEX_REST_URL?: string | undefined;
169
- HYPERLIQUID_PRIVATE_KEY?: string | undefined;
170
- HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
171
- HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
172
- DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
173
- DECIBEL_API_WALLET_ADDRESS?: string | undefined;
164
+ AEVO_REST_URL?: string | undefined;
165
+ PARADEX_ACCOUNT_ADDRESS?: string | undefined;
166
+ PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
174
167
  DECIBEL_API_BEARER_TOKEN?: string | undefined;
175
- DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
176
- DECIBEL_PACKAGE_ADDRESS?: string | undefined;
177
- DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
178
168
  AEVO_API_KEY?: string | undefined;
179
169
  AEVO_API_SECRET?: string | undefined;
180
- AEVO_SIGNING_KEY?: string | undefined;
181
170
  AEVO_ACCOUNT_PRIVATE_KEY?: string | undefined;
182
- AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
183
- ORDERLY_BROKER_ID?: string | undefined;
184
- ORDERLY_CHAIN_ID?: string | undefined;
185
171
  ORDERLY_ACCOUNT_ID?: string | undefined;
186
172
  ORDERLY_KEY?: string | undefined;
187
173
  ORDERLY_SECRET?: string | undefined;
188
- ORDERLY_TRADING_KEY?: string | undefined;
189
- ORDERLY_TRADING_SECRET?: string | undefined;
190
- ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
174
+ ORDERLY_BROKER_ID?: string | undefined;
191
175
  PARADEX_API_BEARER_TOKEN?: string | undefined;
192
- PARADEX_ACCOUNT_ADDRESS?: string | undefined;
176
+ HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
177
+ DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
178
+ AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
179
+ ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
180
+ PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
181
+ PARADEX_REST_URL?: string | undefined;
182
+ HYPERLIQUID_PRIVATE_KEY?: string | undefined;
183
+ DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
184
+ AEVO_SIGNING_KEY?: string | undefined;
185
+ ORDERLY_TRADING_SECRET?: string | undefined;
193
186
  PARADEX_PRIVATE_KEY?: string | undefined;
194
- PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
187
+ DECIBEL_REST_URL?: string | undefined;
188
+ DECIBEL_WS_URL?: string | undefined;
189
+ DECIBEL_FULLNODE_URL?: string | undefined;
190
+ HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
191
+ DECIBEL_API_WALLET_ADDRESS?: string | undefined;
192
+ DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
193
+ DECIBEL_PACKAGE_ADDRESS?: string | undefined;
194
+ ORDERLY_CHAIN_ID?: string | undefined;
195
+ ORDERLY_TRADING_KEY?: string | undefined;
195
196
  PARADEX_CHAIN_ID?: string | undefined;
196
- PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
197
197
  PERPS_ALLOW_UNTRUSTED_ENDPOINTS?: "0" | "1" | "true" | "false" | undefined;
198
198
  PERPS_BLOCKED_EXCHANGES?: string | undefined;
199
199
  PERPS_BLOCKED_MARKETS?: string | undefined;
200
200
  PERPS_BLOCKED_MARKET_PATTERNS?: string | undefined;
201
201
  PERPS_REPUTATION_BLOCKLIST_FILE?: string | undefined;
202
202
  }, {
203
- DECIBEL_REST_URL?: string | undefined;
204
- DECIBEL_WS_URL?: string | undefined;
205
- DECIBEL_FULLNODE_URL?: string | undefined;
206
- AEVO_REST_URL?: string | undefined;
207
203
  ORDERLY_REST_URL?: string | undefined;
208
- PARADEX_REST_URL?: string | undefined;
209
- HYPERLIQUID_PRIVATE_KEY?: string | undefined;
210
- HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
211
- HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
212
- DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
213
- DECIBEL_API_WALLET_ADDRESS?: string | undefined;
204
+ AEVO_REST_URL?: string | undefined;
205
+ PARADEX_ACCOUNT_ADDRESS?: string | undefined;
206
+ PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
214
207
  DECIBEL_API_BEARER_TOKEN?: string | undefined;
215
- DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
216
- DECIBEL_PACKAGE_ADDRESS?: string | undefined;
217
- DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
218
208
  AEVO_API_KEY?: string | undefined;
219
209
  AEVO_API_SECRET?: string | undefined;
220
- AEVO_SIGNING_KEY?: string | undefined;
221
210
  AEVO_ACCOUNT_PRIVATE_KEY?: string | undefined;
222
- AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
223
- ORDERLY_BROKER_ID?: string | undefined;
224
- ORDERLY_CHAIN_ID?: string | undefined;
225
211
  ORDERLY_ACCOUNT_ID?: string | undefined;
226
212
  ORDERLY_KEY?: string | undefined;
227
213
  ORDERLY_SECRET?: string | undefined;
228
- ORDERLY_TRADING_KEY?: string | undefined;
229
- ORDERLY_TRADING_SECRET?: string | undefined;
230
- ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
214
+ ORDERLY_BROKER_ID?: string | undefined;
231
215
  PARADEX_API_BEARER_TOKEN?: string | undefined;
232
- PARADEX_ACCOUNT_ADDRESS?: string | undefined;
216
+ HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
217
+ DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
218
+ AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
219
+ ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
220
+ PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
221
+ PARADEX_REST_URL?: string | undefined;
222
+ HYPERLIQUID_PRIVATE_KEY?: string | undefined;
223
+ DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
224
+ AEVO_SIGNING_KEY?: string | undefined;
225
+ ORDERLY_TRADING_SECRET?: string | undefined;
233
226
  PARADEX_PRIVATE_KEY?: string | undefined;
234
- PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
227
+ DECIBEL_REST_URL?: string | undefined;
228
+ DECIBEL_WS_URL?: string | undefined;
229
+ DECIBEL_FULLNODE_URL?: string | undefined;
230
+ HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
231
+ DECIBEL_API_WALLET_ADDRESS?: string | undefined;
232
+ DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
233
+ DECIBEL_PACKAGE_ADDRESS?: string | undefined;
234
+ ORDERLY_CHAIN_ID?: string | undefined;
235
+ ORDERLY_TRADING_KEY?: string | undefined;
235
236
  PARADEX_CHAIN_ID?: string | undefined;
236
- PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
237
237
  RISK_MAX_POSITION_SIZE_USD?: number | undefined;
238
238
  RISK_MAX_TOTAL_EXPOSURE_USD?: number | undefined;
239
239
  RISK_MAX_LEVERAGE?: number | undefined;
@@ -34,9 +34,23 @@ const secureWsUrl = z
34
34
  message: "must use wss:// (ws:// allowed only for localhost)",
35
35
  });
36
36
  const EXCHANGE_URL_HOST_ALLOWLIST = {
37
- DECIBEL_REST_URL: ["api.testnet.aptoslabs.com", "api.mainnet.aptoslabs.com"],
38
- DECIBEL_WS_URL: ["api.testnet.aptoslabs.com", "api.mainnet.aptoslabs.com"],
39
- DECIBEL_FULLNODE_URL: ["api.testnet.aptoslabs.com", "api.mainnet.aptoslabs.com"],
37
+ DECIBEL_REST_URL: [
38
+ "api.decibel.trade",
39
+ "api.testnet.aptoslabs.com",
40
+ "api.netna.aptoslabs.com",
41
+ "api.mainnet.aptoslabs.com",
42
+ ],
43
+ DECIBEL_WS_URL: [
44
+ "api.decibel.trade",
45
+ "api.testnet.aptoslabs.com",
46
+ "api.netna.aptoslabs.com",
47
+ "api.mainnet.aptoslabs.com",
48
+ ],
49
+ DECIBEL_FULLNODE_URL: [
50
+ "api.testnet.aptoslabs.com",
51
+ "api.netna.aptoslabs.com",
52
+ "api.mainnet.aptoslabs.com",
53
+ ],
40
54
  AEVO_REST_URL: ["api-testnet.aevo.xyz", "api.aevo.xyz"],
41
55
  ORDERLY_REST_URL: ["testnet-api.orderly.org", "api-evm.orderly.org"],
42
56
  PARADEX_REST_URL: ["api.testnet.paradex.trade", "api.prod.paradex.trade"],
@@ -118,13 +132,6 @@ export const envSchema = z
118
132
  message: "required when DECIBEL_API_WALLET_PRIVATE_KEY is set",
119
133
  });
120
134
  }
121
- if (env.DECIBEL_API_BEARER_TOKEN && !env.DECIBEL_API_WALLET_ADDRESS) {
122
- ctx.addIssue({
123
- code: z.ZodIssueCode.custom,
124
- path: ["DECIBEL_API_WALLET_ADDRESS"],
125
- message: "required when DECIBEL_API_BEARER_TOKEN is set",
126
- });
127
- }
128
135
  const aevoCredentialValues = [env.AEVO_API_KEY, env.AEVO_API_SECRET];
129
136
  const aevoCredentialCount = aevoCredentialValues.filter(Boolean).length;
130
137
  if (aevoCredentialCount === 1) {
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raintree-technology/perps",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Universal CLI for perpetual DEXes",
5
5
  "author": "Raintree Technology (https://raintree.technology)",
6
6
  "contributors": [