perp-cli 0.14.3 → 0.15.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 +152 -13
- package/dist/agent-wallet/store.d.ts +3 -2
- package/dist/agent-wallet/store.js +2 -2
- package/dist/api/public/hyperliquid.js +4 -0
- package/dist/api/public/pacifica.js +83 -13
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- package/dist/bot/apex/pulse.js +3 -0
- package/dist/bot/apex/radar.js +2 -1
- package/dist/bot/engine.d.ts +10 -1
- package/dist/bot/engine.js +14 -6
- package/dist/bot/indicators.d.ts +7 -0
- package/dist/bot/indicators.js +16 -0
- package/dist/bot/perp-order.d.ts +4 -4
- package/dist/bot/perp-order.js +5 -5
- package/dist/bot/quarantine.d.ts +1 -1
- package/dist/bot/quarantine.js +1 -1
- package/dist/bot/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +2 -2
- package/dist/bot/strategies/funding-arb-utils.d.ts +1 -1
- package/dist/bot/strategies/funding-arb-utils.js +5 -5
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
- package/dist/bot/strategies/liquidation-mm.js +8 -2
- package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
- package/dist/bot/strategies/momentum-breakout.js +11 -3
- package/dist/bot/strategies/spot-perp-arb-strategy.js +2 -2
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +1 -1
- package/dist/bot/strategy-types.d.ts +2 -1
- package/dist/bot/tui/BotDashboard.d.ts +1 -1
- package/dist/bot/tui/BotDashboard.js +1 -1
- package/dist/cache.js +12 -6
- package/dist/cli-program.d.ts +15 -0
- package/dist/cli-program.js +125 -5
- package/dist/commands/agent-import.d.ts +7 -0
- package/dist/commands/agent-import.js +30 -1
- package/dist/commands/agent.d.ts +6 -3
- package/dist/commands/agent.js +160 -12
- package/dist/commands/arb/index.js +4 -4
- package/dist/commands/arb-auto.js +21 -17
- package/dist/commands/arb.d.ts +12 -0
- package/dist/commands/arb.js +61 -143
- package/dist/commands/bot.js +1 -1
- package/dist/commands/builder.js +1 -1
- package/dist/commands/funds.js +113 -3
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +27 -7
- package/dist/commands/manage.js +23 -21
- package/dist/commands/market.js +167 -35
- package/dist/commands/options.d.ts +9 -0
- package/dist/commands/options.js +134 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +468 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.js +32 -47
- package/dist/commands/rebalance.d.ts +1 -0
- package/dist/commands/rebalance.js +15 -0
- package/dist/commands/risk.d.ts +11 -2
- package/dist/commands/risk.js +39 -44
- package/dist/commands/trade.d.ts +1 -0
- package/dist/commands/trade.js +63 -58
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +15 -4
- package/dist/constants.js +6 -0
- package/dist/dashboard/server.js +1 -1
- package/dist/errors.d.ts +24 -1
- package/dist/errors.js +25 -3
- package/dist/exchanges/arcus/adapter.d.ts +7 -7
- package/dist/exchanges/arcus/adapter.js +12 -12
- package/dist/exchanges/arcus/agent.d.ts +1 -1
- package/dist/exchanges/arcus/agent.js +1 -1
- package/dist/exchanges/arcus/client.js +1 -1
- package/dist/exchanges/arcus/signing.d.ts +2 -3
- package/dist/exchanges/arcus/signing.js +1 -1
- package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
- package/dist/exchanges/arcus/spot-chain.js +1 -1
- package/dist/exchanges/arcus/spot-router.d.ts +1 -1
- package/dist/exchanges/arcus/spot-router.js +2 -2
- package/dist/exchanges/arcus/spot.d.ts +2 -3
- package/dist/exchanges/arcus/spot.js +2 -3
- package/dist/exchanges/aster-spot.d.ts +3 -3
- package/dist/exchanges/aster-spot.js +6 -9
- package/dist/exchanges/aster-typed-data.d.ts +1 -1
- package/dist/exchanges/aster-typed-data.js +1 -1
- package/dist/exchanges/aster.d.ts +4 -4
- package/dist/exchanges/aster.js +31 -13
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +1 -2
- package/dist/exchanges/canonical-symbols.js +26 -2
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +68 -0
- package/dist/exchanges/derive/options.js +360 -0
- package/dist/exchanges/extended/adapter.d.ts +82 -0
- package/dist/exchanges/extended/adapter.js +179 -0
- package/dist/exchanges/extended/client.d.ts +38 -0
- package/dist/exchanges/extended/client.js +88 -0
- package/dist/exchanges/extended/public.d.ts +130 -0
- package/dist/exchanges/extended/public.js +429 -0
- package/dist/exchanges/grvt/adapter.d.ts +86 -0
- package/dist/exchanges/grvt/adapter.js +206 -0
- package/dist/exchanges/grvt/client.d.ts +25 -0
- package/dist/exchanges/grvt/client.js +86 -0
- package/dist/exchanges/grvt/public.d.ts +151 -0
- package/dist/exchanges/grvt/public.js +438 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
- package/dist/exchanges/hyperliquid-spot.js +5 -5
- package/dist/exchanges/hyperliquid.d.ts +22 -12
- package/dist/exchanges/hyperliquid.js +80 -34
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +94 -6
- package/dist/exchanges/interface.js +16 -0
- package/dist/exchanges/ioc-outcome.d.ts +1 -1
- package/dist/exchanges/ioc-outcome.js +1 -1
- package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
- package/dist/exchanges/legacy-trading-v2.js +7 -2
- package/dist/exchanges/lighter-public.d.ts +2 -2
- package/dist/exchanges/lighter-public.js +2 -2
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +3 -4
- package/dist/exchanges/lighter-spot.js +6 -7
- package/dist/exchanges/lighter.d.ts +11 -11
- package/dist/exchanges/lighter.js +28 -14
- package/dist/exchanges/ondo/adapter.d.ts +5 -6
- package/dist/exchanges/ondo/adapter.js +16 -14
- package/dist/exchanges/ondo/agent.d.ts +1 -1
- package/dist/exchanges/ondo/agent.js +1 -1
- package/dist/exchanges/ondo/client.d.ts +1 -1
- package/dist/exchanges/ondo/client.js +2 -2
- package/dist/exchanges/ondo/public.d.ts +8 -1
- package/dist/exchanges/ondo/public.js +13 -2
- package/dist/exchanges/ondo/spot.d.ts +2 -2
- package/dist/exchanges/ondo/spot.js +3 -3
- package/dist/exchanges/options-interface.d.ts +158 -0
- package/dist/exchanges/options-interface.js +17 -0
- package/dist/exchanges/options-venues.d.ts +12 -0
- package/dist/exchanges/options-venues.js +35 -0
- package/dist/exchanges/pacifica-atomic.d.ts +211 -0
- package/dist/exchanges/pacifica-atomic.js +440 -0
- package/dist/exchanges/pacifica-decimal.d.ts +10 -0
- package/dist/exchanges/pacifica-decimal.js +51 -0
- package/dist/exchanges/pacifica-presigned.d.ts +53 -0
- package/dist/exchanges/pacifica-presigned.js +141 -0
- package/dist/exchanges/pacifica-print.d.ts +200 -0
- package/dist/exchanges/pacifica-print.js +417 -0
- package/dist/exchanges/pacifica-spot.d.ts +2 -2
- package/dist/exchanges/pacifica-spot.js +3 -3
- package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
- package/dist/exchanges/pacifica-typed-data.js +1 -1
- package/dist/exchanges/pacifica-vaults.d.ts +121 -0
- package/dist/exchanges/pacifica-vaults.js +264 -0
- package/dist/exchanges/pacifica.d.ts +32 -5
- package/dist/exchanges/pacifica.js +114 -48
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/qfex/adapter.d.ts +18 -5
- package/dist/exchanges/qfex/adapter.js +32 -12
- package/dist/exchanges/qfex/agent.d.ts +1 -1
- package/dist/exchanges/qfex/agent.js +1 -1
- package/dist/exchanges/qfex/client.d.ts +1 -1
- package/dist/exchanges/qfex/client.js +1 -1
- package/dist/exchanges/qfex/errors.d.ts +1 -1
- package/dist/exchanges/qfex/errors.js +2 -2
- package/dist/exchanges/qfex/public.d.ts +3 -3
- package/dist/exchanges/qfex/public.js +3 -3
- package/dist/exchanges/qfex/signing.d.ts +2 -2
- package/dist/exchanges/qfex/signing.js +4 -5
- package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
- package/dist/exchanges/qfex/ws-transport.js +3 -3
- package/dist/exchanges/rate-limiter.d.ts +1 -1
- package/dist/exchanges/rate-limiter.js +5 -2
- package/dist/exchanges/registry.d.ts +1 -1
- package/dist/exchanges/registry.js +1 -1
- package/dist/exchanges/risex/adapter.d.ts +148 -0
- package/dist/exchanges/risex/adapter.js +796 -0
- package/dist/exchanges/risex/agent.d.ts +154 -0
- package/dist/exchanges/risex/agent.js +384 -0
- package/dist/exchanges/risex/client.d.ts +47 -0
- package/dist/exchanges/risex/client.js +174 -0
- package/dist/exchanges/risex/nonce.d.ts +43 -0
- package/dist/exchanges/risex/nonce.js +212 -0
- package/dist/exchanges/risex/public.d.ts +64 -0
- package/dist/exchanges/risex/public.js +150 -0
- package/dist/exchanges/risex/session.d.ts +71 -0
- package/dist/exchanges/risex/session.js +171 -0
- package/dist/exchanges/risex/typed-data.d.ts +173 -0
- package/dist/exchanges/risex/typed-data.js +240 -0
- package/dist/exchanges/risex/units.d.ts +29 -0
- package/dist/exchanges/risex/units.js +104 -0
- package/dist/exchanges/slippage.d.ts +1 -1
- package/dist/exchanges/slippage.js +1 -1
- package/dist/exchanges/spot-factory.d.ts +3 -3
- package/dist/exchanges/spot-factory.js +7 -7
- package/dist/exchanges/spot-interface.d.ts +5 -5
- package/dist/exchanges/spot-interface.js +2 -2
- package/dist/exchanges/spot-result.d.ts +3 -3
- package/dist/exchanges/spot-result.js +3 -3
- package/dist/exchanges/trading-v2.d.ts +3 -3
- package/dist/exchanges/trading-v2.js +9 -6
- package/dist/exchanges/txflow/adapter.d.ts +54 -0
- package/dist/exchanges/txflow/adapter.js +118 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +153 -0
- package/dist/exchanges/txflow/public.d.ts +76 -0
- package/dist/exchanges/txflow/public.js +284 -0
- package/dist/exchanges/unknown-symbol.d.ts +29 -0
- package/dist/exchanges/unknown-symbol.js +51 -0
- package/dist/exchanges/variational/adapter.d.ts +71 -0
- package/dist/exchanges/variational/adapter.js +137 -0
- package/dist/exchanges/variational/client.d.ts +21 -0
- package/dist/exchanges/variational/client.js +66 -0
- package/dist/exchanges/variational/public.d.ts +126 -0
- package/dist/exchanges/variational/public.js +430 -0
- package/dist/exchanges/venue-types.d.ts +6 -7
- package/dist/exchanges/venue-types.js +2 -2
- package/dist/exchanges/venue.d.ts +51 -8
- package/dist/exchanges/venue.js +54 -4
- package/dist/exchanges/venues/arcus.d.ts +1 -2
- package/dist/exchanges/venues/arcus.js +5 -4
- package/dist/exchanges/venues/aster.d.ts +2 -2
- package/dist/exchanges/venues/aster.js +6 -4
- package/dist/exchanges/venues/create-helpers.d.ts +1 -1
- package/dist/exchanges/venues/create-helpers.js +3 -3
- package/dist/exchanges/venues/extended.d.ts +21 -0
- package/dist/exchanges/venues/extended.js +96 -0
- package/dist/exchanges/venues/grvt.d.ts +30 -0
- package/dist/exchanges/venues/grvt.js +98 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
- package/dist/exchanges/venues/hyperliquid.js +10 -8
- package/dist/exchanges/venues/index.d.ts +7 -2
- package/dist/exchanges/venues/index.js +8 -2
- package/dist/exchanges/venues/lighter-family.d.ts +7 -9
- package/dist/exchanges/venues/lighter-family.js +9 -11
- package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
- package/dist/exchanges/venues/lighter-rh.js +7 -5
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +5 -3
- package/dist/exchanges/venues/ondo.d.ts +1 -1
- package/dist/exchanges/venues/ondo.js +5 -3
- package/dist/exchanges/venues/pacifica.d.ts +2 -2
- package/dist/exchanges/venues/pacifica.js +6 -4
- package/dist/exchanges/venues/qfex.d.ts +1 -1
- package/dist/exchanges/venues/qfex.js +4 -2
- package/dist/exchanges/venues/risex-release.d.ts +14 -0
- package/dist/exchanges/venues/risex-release.js +14 -0
- package/dist/exchanges/venues/risex.d.ts +31 -0
- package/dist/exchanges/venues/risex.js +116 -0
- package/dist/exchanges/venues/txflow.d.ts +18 -0
- package/dist/exchanges/venues/txflow.js +77 -0
- package/dist/exchanges/venues/variational.d.ts +21 -0
- package/dist/exchanges/venues/variational.js +85 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/funding/legacy-public.d.ts +1 -1
- package/dist/funding/legacy-public.js +9 -4
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +44 -4
- package/dist/funding/rates.js +56 -12
- package/dist/index.js +7 -6
- package/dist/jobs.d.ts +1 -0
- package/dist/jobs.js +25 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +1 -1
- package/dist/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +58 -0
- package/dist/mcp-server.js +113 -59
- package/dist/pacifica/client.d.ts +67 -4
- package/dist/pacifica/client.js +170 -8
- package/dist/pacifica/rate-limit.d.ts +144 -0
- package/dist/pacifica/rate-limit.js +267 -0
- package/dist/pacifica/types/order.d.ts +2 -1
- package/dist/plan-executor.js +7 -2
- package/dist/rebalance.js +1 -1
- package/dist/risk.d.ts +2 -1
- package/dist/risk.js +2 -4
- package/dist/settings.d.ts +32 -3
- package/dist/signer/ows-evm.d.ts +1 -1
- package/dist/signer/ows-evm.js +1 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/smart-order.js +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +1 -1
- package/dist/trade-validator.js +2 -2
- package/dist/utils.d.ts +12 -3
- package/dist/utils.js +11 -3
- package/dist/venue-health.d.ts +40 -0
- package/dist/venue-health.js +75 -0
- package/dist/withdraw-gate.d.ts +1 -1
- package/dist/withdraw-gate.js +1 -1
- package/dist/ws/index.d.ts +959 -0
- package/dist/ws/index.js +4295 -0
- package/dist/ws/node.d.ts +975 -0
- package/dist/ws/node.js +4366 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +58 -10
- package/skills/perp-cli/SKILL.md +62 -9
- package/skills/perp-cli/references/agent-operations.md +7 -0
- package/skills/perp-cli/references/commands.md +76 -5
- package/skills/perp-cli/references/ws-library.md +492 -0
package/dist/cli-program.js
CHANGED
|
@@ -6,13 +6,15 @@ import { Command } from "commander";
|
|
|
6
6
|
import { cliCredentialOptsFrom, hasCredentials, resolveCredentials } from "./config.js";
|
|
7
7
|
import { PacificaAdapter } from "./exchanges/pacifica.js";
|
|
8
8
|
import { HyperliquidAdapter } from "./exchanges/hyperliquid.js";
|
|
9
|
-
import { getVenue, listVenues, resolveVenueId, READ_ONLY_CREDENTIALS } from "./exchanges/venue.js";
|
|
9
|
+
import { getVenue, listVenues, resolveVenueId, roleUnsupported, tryResolveVenueId, READ_ONLY_CREDENTIALS } from "./exchanges/venue.js";
|
|
10
10
|
import { registerAdapterNetwork } from "./exchanges/adapter-network.js";
|
|
11
11
|
import "./exchanges/venues/index.js";
|
|
12
12
|
import { PerpError } from "./errors.js";
|
|
13
13
|
import { registerMarketCommands } from "./commands/market.js";
|
|
14
14
|
import { registerAccountCommands } from "./commands/account.js";
|
|
15
15
|
import { registerOutcomeCommands } from "./commands/outcome.js";
|
|
16
|
+
import { registerOptionsCommands } from "./commands/options.js";
|
|
17
|
+
import { registerPacificaEarnCommands } from "./commands/pacifica-earn.js";
|
|
16
18
|
import { registerTradeCommands } from "./commands/trade.js";
|
|
17
19
|
// manage commands now register under `wallet manage ...` via registerWalletCommands.
|
|
18
20
|
// stream commands removed — WS feeds still used by dashboard/event-stream internally
|
|
@@ -86,6 +88,11 @@ export function landingData(program, version) {
|
|
|
86
88
|
return {
|
|
87
89
|
name: program.name(),
|
|
88
90
|
version,
|
|
91
|
+
// Registry facts only (no reads): an "unofficial" venue is read through an undocumented API.
|
|
92
|
+
venues: listVenues().map(v => ({
|
|
93
|
+
id: v.id, displayName: v.displayName, roles: [...v.roles], apiStatus: v.apiStatus.kind,
|
|
94
|
+
note: v.apiStatus.kind === "unofficial" ? v.apiStatus.note : null,
|
|
95
|
+
})),
|
|
89
96
|
usage: `${program.name()} [options] <command> [args] (add --json for one JSON document on stdout)`,
|
|
90
97
|
help: `${program.name()} <command> --help`,
|
|
91
98
|
commands: program.commands
|
|
@@ -131,9 +138,9 @@ export function buildProgram(opts) {
|
|
|
131
138
|
});
|
|
132
139
|
let _adapter = null;
|
|
133
140
|
/**
|
|
134
|
-
* One adapter per `${venueId}:${network}:${wallet}:${hlDex}
|
|
141
|
+
* One adapter per `${venueId}:${network}:${wallet}:${hlDex}`.
|
|
135
142
|
* Every adapter is built by the venue definition's create(ctx) — the single
|
|
136
|
-
* creation path shared with the MCP server
|
|
143
|
+
* creation path shared with the MCP server.
|
|
137
144
|
*/
|
|
138
145
|
const _adapters = new Map();
|
|
139
146
|
function cliCredentialOpts() {
|
|
@@ -147,7 +154,7 @@ export function buildProgram(opts) {
|
|
|
147
154
|
const wallet = opts.ows ? `ows:${opts.ows}` : (opts.wallet ?? "default");
|
|
148
155
|
return `${venue}:${network}:${wallet}:${hlDex ?? "-"}`;
|
|
149
156
|
}
|
|
150
|
-
/** restrictedRegions notice: stderr, once per venue per process (
|
|
157
|
+
/** restrictedRegions notice: stderr, once per venue per process (warn, never block). */
|
|
151
158
|
const _restrictedWarned = new Set();
|
|
152
159
|
function warnRestrictedRegions(def) {
|
|
153
160
|
if (def.restrictedRegions.length === 0 || _restrictedWarned.has(def.id))
|
|
@@ -167,6 +174,10 @@ export function buildProgram(opts) {
|
|
|
167
174
|
remediation: `${def.id} is market-data only — use the funding/market public commands (e.g. perp arb scan)`,
|
|
168
175
|
});
|
|
169
176
|
}
|
|
177
|
+
// A market-data-only venue (TxFlow: unofficial read-only API) never gets a credentialed adapter:
|
|
178
|
+
// every account / trading / funds / bot path refuses here, before any credential or request.
|
|
179
|
+
if (!publicOnly && !def.roles.includes("account"))
|
|
180
|
+
throw roleUnsupported(def, "account");
|
|
170
181
|
const network = currentNetwork();
|
|
171
182
|
const hlDex = def.id === "hyperliquid" ? dex : null;
|
|
172
183
|
const key = publicOnly ? `${def.id}:${network}:public:${hlDex ?? "-"}` : adapterCacheKey(def.id, network, hlDex);
|
|
@@ -246,6 +257,8 @@ export function buildProgram(opts) {
|
|
|
246
257
|
registerMarketCommands(program, getPublicAdapter, isJson, getPublicAdapterForExchange, getAdapter);
|
|
247
258
|
registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
|
|
248
259
|
registerOutcomeCommands(program, getAdapterForExchange, isJson);
|
|
260
|
+
registerOptionsCommands(program, isJson);
|
|
261
|
+
registerPacificaEarnCommands(program, getAdapterForExchange, isJson);
|
|
249
262
|
registerTradeCommands(program, getAdapter, isJson, isDryRunFlag, getAdapterForExchange);
|
|
250
263
|
// manage tree wired inside registerWalletCommands below.
|
|
251
264
|
// stream commands removed
|
|
@@ -273,7 +286,15 @@ export function buildProgram(opts) {
|
|
|
273
286
|
// registerFundsCommands above.
|
|
274
287
|
// Background processes & strategies
|
|
275
288
|
registerBackgroundCommands(program, isJson, getAdapterForExchange);
|
|
276
|
-
|
|
289
|
+
// Every strategy command (bots, plans, TWAP / DCA / grid, `strategy run`, `--exchanges` legs,
|
|
290
|
+
// config-file venues) builds its adapters through these: a venue without the trading role
|
|
291
|
+
// (TxFlow, release-gated RISEx) is refused here, before any key is opened — also under --dry-run.
|
|
292
|
+
const strategyVenueGate = (venue) => {
|
|
293
|
+
const def = getVenue(venue);
|
|
294
|
+
if (!def.roles.includes("trading"))
|
|
295
|
+
throw roleUnsupported(def, "automation");
|
|
296
|
+
};
|
|
297
|
+
registerStrategyCommands(program, async () => { strategyVenueGate(getExchange()); return getAdapter(); }, async (exchange) => { strategyVenueGate(exchange); return getAdapterForExchange(exchange); }, isJson);
|
|
277
298
|
// Agent-friendly commands
|
|
278
299
|
registerRiskCommands(program, getAdapterForExchange, isJson);
|
|
279
300
|
registerHistoryCommands(program, isJson, getAdapterForExchange);
|
|
@@ -286,7 +307,12 @@ export function buildProgram(opts) {
|
|
|
286
307
|
registerPortfolioCommand(program, getAdapterForExchange, isJson, version, getHLAdapterForDex);
|
|
287
308
|
registerInitCommand(program);
|
|
288
309
|
registerAlertCommands(program, isJson);
|
|
310
|
+
installNestedHelp(program);
|
|
311
|
+
installMissingSubcommandError(program);
|
|
289
312
|
// Switch shared API URLs if --network testnet is used
|
|
313
|
+
// A venue without the trading role (market-data-only TxFlow, release-gated RISEx) refuses every
|
|
314
|
+
// trading command before anything else — also under --dry-run, whose plans skip the adapter.
|
|
315
|
+
program.hook("preAction", (_root, actionCommand) => assertCommandVenueRole(actionCommand, program.opts().exchange));
|
|
290
316
|
// Validates -n and enforces default-deny --dry-run before any action runs.
|
|
291
317
|
installDryRunGuard(program);
|
|
292
318
|
// Secrets on argv (review L10): -k is rejected outright; --passphrase still works but warns loudly.
|
|
@@ -301,3 +327,97 @@ export function buildProgram(opts) {
|
|
|
301
327
|
program.hook("preAction", () => setQuarantineNetwork(networkOf(program)));
|
|
302
328
|
return { program, getAdapterForExchange, isJson };
|
|
303
329
|
}
|
|
330
|
+
/** `perp trade <leaf>` commands that only read (order status, fills, P&L tracking, spot reads). */
|
|
331
|
+
const TRADE_READ_COMMANDS = new Set(["status", "fills", "pnl-track", "spot-orders", "spot-balance"]);
|
|
332
|
+
/**
|
|
333
|
+
* The trading-role gate at command resolution: `perp -e <venue> trade <write>`, every
|
|
334
|
+
* `perp trade multi <venue>:…` leg and `perp wallet agent approve|import|revoke|rotate <venue>`
|
|
335
|
+
* (revoke --force / --local-only excepted: local cleanup only) and every `strategy … --exchanges` leg refuse with roleUnsupported() (CAPABILITY_UNSUPPORTED) when the venue has no trading role. Runs
|
|
336
|
+
* before the dry-run guard and before any action, so a dry-run plan can never present a gated
|
|
337
|
+
* venue as tradable. Unknown venue names are left to the command's own UNKNOWN_VENUE handling.
|
|
338
|
+
*/
|
|
339
|
+
export function assertCommandVenueRole(actionCommand, exchangeOpt) {
|
|
340
|
+
const path = commandPath(actionCommand).split(" ");
|
|
341
|
+
const leaf = path[path.length - 1];
|
|
342
|
+
let venueArgs;
|
|
343
|
+
let feature;
|
|
344
|
+
if (leaf === "multi" && (path[0] === "trade" || path.length === 1)) {
|
|
345
|
+
venueArgs = actionCommand.args.map(leg => leg.split(":")[0]);
|
|
346
|
+
feature = "trading";
|
|
347
|
+
}
|
|
348
|
+
else if (path[0] === "strategy" && typeof actionCommand.opts().exchanges === "string") {
|
|
349
|
+
// `--exchanges` legs (strategy run / funding-arb / quick-arb / delta-neutral, incl. their defaults):
|
|
350
|
+
// refused before the first leg's adapter is built. The -e venue is gated by the strategy adapter factory.
|
|
351
|
+
venueArgs = String(actionCommand.opts().exchanges).split(",").map(v => v.trim()).filter(v => v !== "");
|
|
352
|
+
feature = "automation";
|
|
353
|
+
}
|
|
354
|
+
else if (path[0] === "trade" && path.length === 2 && !TRADE_READ_COMMANDS.has(leaf)) {
|
|
355
|
+
venueArgs = [exchangeOpt];
|
|
356
|
+
feature = "trading";
|
|
357
|
+
}
|
|
358
|
+
else if (path[0] === "wallet" && path[1] === "agent" && ["approve", "import", "revoke", "rotate"].includes(path[2] ?? "")) {
|
|
359
|
+
// `revoke --force` / `--local-only` only clears local records (nothing signed or sent): allowed.
|
|
360
|
+
const o = actionCommand.opts();
|
|
361
|
+
if (path[2] === "revoke" && (o.force === true || o.localOnly === true))
|
|
362
|
+
return;
|
|
363
|
+
venueArgs = actionCommand.args.slice(0, 1);
|
|
364
|
+
feature = path[2] === "import" ? "agentImport" : `agent ${path[2]}`;
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
for (const v of venueArgs) {
|
|
370
|
+
const id = tryResolveVenueId(v);
|
|
371
|
+
if (id !== null && !getVenue(id).roles.includes("trading"))
|
|
372
|
+
throw roleUnsupported(getVenue(id), feature);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/** `perp a b c` path of a command (root name excluded). */
|
|
376
|
+
function commandPath(cmd) {
|
|
377
|
+
const names = [];
|
|
378
|
+
for (let c = cmd; c !== null && c.parent !== null; c = c.parent)
|
|
379
|
+
names.unshift(c.name());
|
|
380
|
+
return names.join(" ");
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* `perp help <group> <leaf> ...` shows the help of the deepest named command. Commander's built-in
|
|
384
|
+
* root help command resolves only its first operand, so `perp help market list` printed the
|
|
385
|
+
* `market` group help. Groups keep their own built-in `help` subcommand (`perp market help list`).
|
|
386
|
+
*/
|
|
387
|
+
function installNestedHelp(program) {
|
|
388
|
+
program.helpCommand(false);
|
|
389
|
+
program
|
|
390
|
+
.command("help [command...]")
|
|
391
|
+
.description("display help for command (any depth: perp help trade limit)")
|
|
392
|
+
.action((names) => {
|
|
393
|
+
let cmd = program;
|
|
394
|
+
for (const name of names) {
|
|
395
|
+
const sub = cmd.commands.find(c => c.name() === name || c.aliases().includes(name));
|
|
396
|
+
if (sub === undefined) {
|
|
397
|
+
const where = cmd === program ? "perp" : `perp ${commandPath(cmd)}`;
|
|
398
|
+
throw new PerpError("INVALID_PARAMS", `Unknown command '${name}' under '${where}'`, {
|
|
399
|
+
remediation: `Available: ${cmd.commands.map(c => c.name()).join(", ")} — see: ${where} --help`,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
cmd = sub;
|
|
403
|
+
}
|
|
404
|
+
cmd.help();
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* A command group run without a subcommand (`perp history`): Commander prints the group's help as
|
|
409
|
+
* an error. In --json mode that reached stdout as a CLI_ERROR whose message was the usage text;
|
|
410
|
+
* it is now one INVALID_PARAMS document naming the subcommands, with remediation (exit 1).
|
|
411
|
+
* Human mode keeps the usage listing on stderr.
|
|
412
|
+
*/
|
|
413
|
+
function installMissingSubcommandError(program) {
|
|
414
|
+
program.on("beforeAllHelp", (ctx) => {
|
|
415
|
+
if (!ctx.error || program.opts().json !== true)
|
|
416
|
+
return;
|
|
417
|
+
const path = `perp ${commandPath(ctx.command)}`.trim();
|
|
418
|
+
const subs = ctx.command.commands.map(c => c.name()).filter(n => n !== "help");
|
|
419
|
+
throw new PerpError("INVALID_PARAMS", `'${path}' needs a subcommand: ${subs.join(", ")}`, {
|
|
420
|
+
remediation: `Run one of them, e.g. ${path} ${subs[0]} — list them with: ${path} --help`,
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
}
|
|
@@ -90,6 +90,13 @@ export declare function importSecretLineCount(venue: VenueId): number;
|
|
|
90
90
|
export declare function importSecretFromLines(venue: VenueId, lines: string[]): ImportSecret;
|
|
91
91
|
/** Read the secret: hidden TTY prompt, else piped stdin (ondo: 2 lines key id + secret; others: 1 line). */
|
|
92
92
|
export declare function readImportSecret(venue: VenueId): Promise<ImportSecret>;
|
|
93
|
+
/**
|
|
94
|
+
* Venues whose keys cannot be imported. TxFlow: market-data-only (unofficial read-only API).
|
|
95
|
+
* RISEx: perp-cli trades only with a signer it registered itself and then restricted ON-CHAIN to
|
|
96
|
+
* the trading permission; a signer created in the RISEx app keeps every permission (including
|
|
97
|
+
* deposit / withdraw), so it is never stored here.
|
|
98
|
+
*/
|
|
99
|
+
export declare function assertImportSupported(venue: VenueId, network: Network): void;
|
|
93
100
|
/** Venues whose import cannot be checked against the venue (stored as "unverified"). */
|
|
94
101
|
export declare const UNVERIFIED_IMPORT_VENUES: readonly VenueId[];
|
|
95
102
|
/**
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
import chalk from "chalk";
|
|
25
25
|
import { PerpError, classifyError } from "../errors.js";
|
|
26
26
|
import { acquireLock, releaseLock, getAgent, setAgent, deleteAgent, listAgents, agentNetworkOf, agentSourceOf, getVenueAgent, setVenueAgent, deleteVenueAgent, } from "../agent-wallet/store.js";
|
|
27
|
-
import { getVenue, networkConfig } from "../exchanges/venue.js";
|
|
27
|
+
import { getVenue, networkConfig, roleUnsupported } from "../exchanges/venue.js";
|
|
28
|
+
import { unsupported } from "../exchanges/trading-v2.js";
|
|
28
29
|
import { LIGHTER_RH_RESERVED_KEY_INDEXES } from "../exchanges/venues/lighter-family.js";
|
|
29
30
|
import { loadOws } from "../signer/ows-loader.js";
|
|
30
31
|
import { defaultAgentName, isLighterFamilyVenue, parseExpiresIn, promptHidden, qfexWithdrawKeyWarning } from "./agent.js";
|
|
@@ -67,6 +68,11 @@ export function importSecretLabel(venue) {
|
|
|
67
68
|
case "arcus": return "Arcus API key private key (Ed25519)";
|
|
68
69
|
case "ondo": return "Ondo API secret";
|
|
69
70
|
case "qfex": return "QFEX API secret";
|
|
71
|
+
case "risex": return "RISEx signer private key";
|
|
72
|
+
case "txflow": return "TxFlow key (not supported)";
|
|
73
|
+
case "variational": return "Variational key (not supported)";
|
|
74
|
+
case "grvt": return "GRVT key (not supported)";
|
|
75
|
+
case "extended": return "Extended key (not supported)";
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
/**
|
|
@@ -154,6 +160,20 @@ function redacted(err, secrets, venue) {
|
|
|
154
160
|
details.remediation = scrub(e.remediation, forms);
|
|
155
161
|
return new PerpError(e.code, scrub(e.message, forms), details);
|
|
156
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Venues whose keys cannot be imported. TxFlow: market-data-only (unofficial read-only API).
|
|
165
|
+
* RISEx: perp-cli trades only with a signer it registered itself and then restricted ON-CHAIN to
|
|
166
|
+
* the trading permission; a signer created in the RISEx app keeps every permission (including
|
|
167
|
+
* deposit / withdraw), so it is never stored here.
|
|
168
|
+
*/
|
|
169
|
+
export function assertImportSupported(venue, network) {
|
|
170
|
+
const def = getVenue(venue);
|
|
171
|
+
if (!def.roles.includes("trading"))
|
|
172
|
+
throw roleUnsupported(def, "agentImport");
|
|
173
|
+
if (venue === "risex") {
|
|
174
|
+
throw unsupported("risex", "agentImport", "perp-cli only uses RISEx signers it registered and restricted on-chain to trading-only permissions; an app-created signer keeps deposit / withdraw permissions", `Register a restricted signer with your master wallet: perp${network === "testnet" ? " -n testnet" : ""} wallet agent approve risex --master <ows-wallet>`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
157
177
|
/** Venues whose import cannot be checked against the venue (stored as "unverified"). */
|
|
158
178
|
export const UNVERIFIED_IMPORT_VENUES = ["pacifica"];
|
|
159
179
|
/**
|
|
@@ -722,6 +742,8 @@ async function importQfex(p, agentName, s, replacing) {
|
|
|
722
742
|
* appears in a thrown error.
|
|
723
743
|
*/
|
|
724
744
|
export async function importVenueAgent(p) {
|
|
745
|
+
// Refused before the secret is even inspected (nothing read, stored or sent).
|
|
746
|
+
assertImportSupported(p.venue, p.network);
|
|
725
747
|
const secrets = secretsOf(p.secret);
|
|
726
748
|
if (secrets.some(x => x.trim().length === 0))
|
|
727
749
|
throw new PerpError("INVALID_PARAMS", "Empty key — nothing was imported");
|
|
@@ -751,6 +773,13 @@ export async function importVenueAgent(p) {
|
|
|
751
773
|
case "lighter":
|
|
752
774
|
case "lighter-rh":
|
|
753
775
|
return await importLighter(p, p.venue, agentName, key, replacing);
|
|
776
|
+
case "risex":
|
|
777
|
+
case "txflow":
|
|
778
|
+
case "variational":
|
|
779
|
+
case "grvt":
|
|
780
|
+
case "extended":
|
|
781
|
+
// Unreachable: assertImportSupported() refused these before the lock.
|
|
782
|
+
throw new PerpError("CAPABILITY_UNSUPPORTED", `${p.venue} keys cannot be imported`);
|
|
754
783
|
}
|
|
755
784
|
}
|
|
756
785
|
catch (err) {
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Option } from "commander";
|
|
2
|
-
import { loadSettings, type OndoAgentMeta, type ArcusAgentMeta, type QfexAgentMeta } from "../settings.js";
|
|
2
|
+
import { loadSettings, type OndoAgentMeta, type ArcusAgentMeta, type QfexAgentMeta, type RisexAgentMeta } from "../settings.js";
|
|
3
3
|
import "../exchanges/venues/index.js";
|
|
4
4
|
import type { Network, VenueId } from "../exchanges/venue-types.js";
|
|
5
5
|
import { type LighterVenueId } from "../exchanges/venues/lighter-family.js";
|
|
@@ -27,7 +27,7 @@ export interface VerifyResult {
|
|
|
27
27
|
/** Extract the agent list from a Pacifica /agent/list envelope; throws on an unrecognized shape. */
|
|
28
28
|
export declare function parsePacAgentList(json: unknown): unknown[];
|
|
29
29
|
/**
|
|
30
|
-
* Confirmation of the account a QFEX key was resolved to
|
|
30
|
+
* Confirmation of the account a QFEX key was resolved to. In a terminal: a
|
|
31
31
|
* y/N prompt showing the UUID. Without one (piped stdin, --json): only an explicit --account-id
|
|
32
32
|
* that equals the resolved account counts — the resolved UUID is printed on stderr either way.
|
|
33
33
|
*/
|
|
@@ -94,6 +94,9 @@ export type ApproveAgentOutcome = {
|
|
|
94
94
|
} | {
|
|
95
95
|
venue: "arcus";
|
|
96
96
|
meta: ArcusAgentMeta;
|
|
97
|
+
} | {
|
|
98
|
+
venue: "risex";
|
|
99
|
+
meta: RisexAgentMeta;
|
|
97
100
|
} | {
|
|
98
101
|
venue: LighterVenueId;
|
|
99
102
|
agentName: string;
|
|
@@ -131,7 +134,7 @@ export declare function qfexWithdrawKeyWarning(meta: Pick<QfexAgentMeta, "agentN
|
|
|
131
134
|
/** Venues `wallet agent rotate` (and `approve --rotate`) can rotate. */
|
|
132
135
|
type RotatableVenue = "aster" | "hyperliquid" | "pacifica" | LighterVenueId;
|
|
133
136
|
type RotatedAgentOutcome = Exclude<ApproveAgentOutcome, {
|
|
134
|
-
venue: "ondo" | "arcus";
|
|
137
|
+
venue: "ondo" | "arcus" | "risex";
|
|
135
138
|
}>;
|
|
136
139
|
export interface RotateAgentParams {
|
|
137
140
|
exchange: RotatableVenue;
|
package/dist/commands/agent.js
CHANGED
|
@@ -9,7 +9,8 @@ import chalk from "chalk";
|
|
|
9
9
|
import bs58 from "bs58";
|
|
10
10
|
import { loadSettings } from "../settings.js";
|
|
11
11
|
import { getAgent, setAgent, deleteAgent, listAgents, acquireLock, releaseLock, getVenueAgent, getVenueAgentForNetwork, setVenueAgent, deleteVenueAgent, listVenueAgents, agentNetworkOf, recordedAgentNetworkOf, assertAgentNetwork, getAgentForNetwork, agentSourceOf, } from "../agent-wallet/store.js";
|
|
12
|
-
import { getVenue, networkConfig, resolveVenueId } from "../exchanges/venue.js";
|
|
12
|
+
import { getVenue, networkConfig, resolveVenueId, roleUnsupported } from "../exchanges/venue.js";
|
|
13
|
+
import { RISEX_APP_URL } from "../exchanges/venues/risex.js";
|
|
13
14
|
import { listConfiguredAccountVenues } from "../config.js";
|
|
14
15
|
import "../exchanges/venues/index.js";
|
|
15
16
|
import { LIGHTER_RH_RESERVED_KEY_INDEXES } from "../exchanges/venues/lighter-family.js";
|
|
@@ -114,7 +115,7 @@ function readPipedStdin() {
|
|
|
114
115
|
});
|
|
115
116
|
}
|
|
116
117
|
/**
|
|
117
|
-
* Ondo API key id + secret, from stdin only (
|
|
118
|
+
* Ondo API key id + secret, from stdin only (never flags).
|
|
118
119
|
* TTY: key id prompt, then hidden secret prompt. Piped: line 1 = key id, line 2 = secret.
|
|
119
120
|
*/
|
|
120
121
|
/** Lighter ChangePubKey settlement window when confirming a slot (docs example: 30 s, 1 s apart; kept under the 60 req/min budget). */
|
|
@@ -445,7 +446,7 @@ async function verifyQfex(network, opts) {
|
|
|
445
446
|
};
|
|
446
447
|
}
|
|
447
448
|
/**
|
|
448
|
-
* Confirmation of the account a QFEX key was resolved to
|
|
449
|
+
* Confirmation of the account a QFEX key was resolved to. In a terminal: a
|
|
449
450
|
* y/N prompt showing the UUID. Without one (piped stdin, --json): only an explicit --account-id
|
|
450
451
|
* that equals the resolved account counts — the resolved UUID is printed on stderr either way.
|
|
451
452
|
*/
|
|
@@ -471,6 +472,40 @@ export function confirmImportAccount(explicitAccountId, json) {
|
|
|
471
472
|
}
|
|
472
473
|
};
|
|
473
474
|
}
|
|
475
|
+
/** RISEx: session-key status on the venue and the on-chain permission bitmap (read-only). */
|
|
476
|
+
async function verifyRisex(network, opts) {
|
|
477
|
+
const meta = opts.agentName !== undefined ? getVenueAgent("risex", opts.agentName) : getVenueAgentForNetwork("risex", network);
|
|
478
|
+
// Read-only release (trading role gated): approve / revoke are refused, so point to the RISEx app.
|
|
479
|
+
const tradingGated = !getVenue("risex").roles.includes("trading");
|
|
480
|
+
if (!meta) {
|
|
481
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `No RISEx signer registered for ${network}`, {
|
|
482
|
+
remediation: tradingGated
|
|
483
|
+
? `RISEx is read-only in this perp-cli release (trading in a later release): manage signers in the RISEx app (${RISEX_APP_URL})`
|
|
484
|
+
: `perp${network === "testnet" ? " -n testnet" : ""} wallet agent approve risex --master <ows-wallet>`,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const { verifyRisexAgent } = await import("../exchanges/risex/agent.js");
|
|
488
|
+
const v = await verifyRisexAgent(meta);
|
|
489
|
+
const warnings = [];
|
|
490
|
+
if (!v.active)
|
|
491
|
+
warnings.push(`RISEx signer ${meta.signerAddress} status is ${String(v.status)} (1 = Active)`);
|
|
492
|
+
if (v.expired)
|
|
493
|
+
warnings.push(`RISEx signer ${meta.signerAddress} expired at ${meta.expiresAt}`);
|
|
494
|
+
if (v.active && (!v.tradingOnly || meta.permissions.tradingOnly !== true)) {
|
|
495
|
+
// An active signer that can do more than trade is a failure, never ok:true.
|
|
496
|
+
throw new PerpError("POLICY_DENIED", `RISEx signer ${meta.signerAddress} ("${meta.agentName}") is NOT restricted to trading-only (on-chain ${JSON.stringify(v.permissions)}, bitmap ${String(v.bitmap)}; recorded tradingOnly=${String(meta.permissions.tradingOnly)})${v.reason !== null ? ` — ${v.reason}` : ""}`, {
|
|
497
|
+
exchange: "risex", signer: meta.signerAddress, permissions: v.permissions, bitmap: v.bitmap,
|
|
498
|
+
remediation: tradingGated
|
|
499
|
+
? `Revoke it in the RISEx app (${RISEX_APP_URL}); then remove the local record: perp${network === "testnet" ? " -n testnet" : ""} wallet agent revoke risex ${meta.agentName} --force`
|
|
500
|
+
: `Revoke it: perp${network === "testnet" ? " -n testnet" : ""} wallet agent revoke risex ${meta.agentName}, then approve a new one`,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
registered: v.active, count: 1,
|
|
505
|
+
items: [{ agentName: meta.agentName, signer: meta.signerAddress, account: meta.accountAddress, network: meta.network, status: v.status, permissions: v.permissions, tradingOnly: v.tradingOnly, expiresAt: meta.expiresAt }],
|
|
506
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
474
509
|
/** Dispatch a verify to the venue-specific implementation. */
|
|
475
510
|
async function verifyVenue(ex, network, opts) {
|
|
476
511
|
if (ex === "aster")
|
|
@@ -487,8 +522,10 @@ async function verifyVenue(ex, network, opts) {
|
|
|
487
522
|
return verifyArcus(network, opts);
|
|
488
523
|
if (ex === "qfex")
|
|
489
524
|
return verifyQfex(network, opts);
|
|
525
|
+
if (ex === "risex")
|
|
526
|
+
return verifyRisex(network, opts);
|
|
490
527
|
throw new PerpError("NOT_IMPLEMENTED", `${ex} has no agents to verify`, {
|
|
491
|
-
remediation: "perp wallet agent verify <aster|hyperliquid|pacifica|lighter|lighter-rh|ondo|arcus|qfex> [agentName]",
|
|
528
|
+
remediation: "perp wallet agent verify <aster|hyperliquid|pacifica|lighter|lighter-rh|ondo|arcus|qfex|risex> [agentName]",
|
|
492
529
|
});
|
|
493
530
|
}
|
|
494
531
|
/**
|
|
@@ -509,6 +546,9 @@ export function agentApproveUnsupportedReason(venue, network, op) {
|
|
|
509
546
|
}
|
|
510
547
|
/** Throws the uniform CAPABILITY_UNSUPPORTED error when agentApproveUnsupportedReason() is non-null. */
|
|
511
548
|
export function assertAgentApproveSupported(venue, network, op) {
|
|
549
|
+
const def = getVenue(venue);
|
|
550
|
+
if (!def.roles.includes("trading"))
|
|
551
|
+
throw roleUnsupported(def, `agent ${op}`);
|
|
512
552
|
if (venue === "qfex") {
|
|
513
553
|
// Programmatic key creation needs a website session (SIWE + MFA) — not a CLI path.
|
|
514
554
|
throw unsupported("qfex", "agentApprove", "API keys are created (and revoked) on the QFEX website, behind 2FA", `Create the key in QFEX Developer Settings, then import it: perp${network === "testnet" ? " -n testnet" : ""} wallet agent import qfex`);
|
|
@@ -540,6 +580,11 @@ export function defaultAgentName(venue, network) {
|
|
|
540
580
|
case "ondo": return `perp-cli-ondo${suffix}`;
|
|
541
581
|
case "arcus": return `perp-cli-arcus${suffix}`;
|
|
542
582
|
case "qfex": return `perp-cli-qfex${suffix}`;
|
|
583
|
+
case "risex": return `perp-cli-risex${suffix}`;
|
|
584
|
+
case "txflow": return `perp-cli-txflow${suffix}`;
|
|
585
|
+
case "variational": return `perp-cli-variational${suffix}`;
|
|
586
|
+
case "grvt": return `perp-cli-grvt${suffix}`;
|
|
587
|
+
case "extended": return `perp-cli-extended${suffix}`;
|
|
543
588
|
}
|
|
544
589
|
}
|
|
545
590
|
/**
|
|
@@ -622,7 +667,7 @@ function requirePassphrase(passphrase) {
|
|
|
622
667
|
export async function approveVenueAgent(p) {
|
|
623
668
|
assertAgentApproveSupported(p.venue, p.network, "approve");
|
|
624
669
|
const agentName = p.agentName ?? defaultAgentName(p.venue, p.network);
|
|
625
|
-
// Ondo: HMAC key entered on stdin only
|
|
670
|
+
// Ondo: HMAC key entered on stdin only
|
|
626
671
|
if (p.venue === "ondo") {
|
|
627
672
|
assertVenueAgentNameFree("ondo", agentName, p.network);
|
|
628
673
|
if (getVenueAgent("ondo", agentName) && !p.rotate) {
|
|
@@ -636,7 +681,7 @@ export async function approveVenueAgent(p) {
|
|
|
636
681
|
setVenueAgent("ondo", meta);
|
|
637
682
|
return { venue: "ondo", meta };
|
|
638
683
|
}
|
|
639
|
-
// Arcus: OWS Ed25519 agent + EIP-712 createApiKey
|
|
684
|
+
// Arcus: OWS Ed25519 agent + EIP-712 createApiKey
|
|
640
685
|
if (p.venue === "arcus") {
|
|
641
686
|
const masterName = requireMaster(p.master);
|
|
642
687
|
assertVenueAgentNameFree("arcus", agentName, p.network);
|
|
@@ -660,6 +705,53 @@ export async function approveVenueAgent(p) {
|
|
|
660
705
|
releaseLock("arcus");
|
|
661
706
|
}
|
|
662
707
|
}
|
|
708
|
+
// RISEx: master-signed RegisterSigner, then an on-chain restriction to trading-only.
|
|
709
|
+
if (p.venue === "risex") {
|
|
710
|
+
const masterName = requireMaster(p.master);
|
|
711
|
+
assertVenueAgentNameFree("risex", agentName, p.network);
|
|
712
|
+
if (p.canWithdraw || p.canSpot) {
|
|
713
|
+
throw new PerpError("INVALID_PARAMS", "RISEx signers registered by perp-cli are always restricted to trading only (no fund movement, no spot)", {
|
|
714
|
+
remediation: "Drop --can-withdraw / --can-spot",
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
const expiresAt = parseExpiresInParam(p.expiresIn);
|
|
718
|
+
const { approveRisexAgent, revokeRisexAgent, risexAgentWalletName, risexRotatePreflight, risexSignerStatus } = await import("../exchanges/risex/agent.js");
|
|
719
|
+
acquireLock("risex");
|
|
720
|
+
try {
|
|
721
|
+
const existing = getVenueAgent("risex", agentName);
|
|
722
|
+
if (existing !== null) {
|
|
723
|
+
if (!p.rotate) {
|
|
724
|
+
throw new PerpError("INVALID_PARAMS", `RISEx signer "${agentName}" already exists`, {
|
|
725
|
+
remediation: `Replace it (revoke the old signer, register a fresh one): perp${p.network === "testnet" ? " -n testnet" : ""} wallet agent approve risex --rotate — or choose another --agent-name`,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
// Rotate: the new signer's checks (pins, maintenance, master gas) pass first, then a
|
|
729
|
+
// venue-confirmed revoke of the old signer, then drop its key, then approve.
|
|
730
|
+
await risexRotatePreflight(masterName, p.passphrase, p.network);
|
|
731
|
+
// A kept record whose signer never registered (or is already revoked) has nothing to
|
|
732
|
+
// revoke on the venue: it is only forgotten locally. An Active one is revoked first.
|
|
733
|
+
const status = await risexSignerStatus(existing);
|
|
734
|
+
if (status === 1)
|
|
735
|
+
await revokeRisexAgent({ meta: existing, masterOwsWallet: masterName, passphrase: p.passphrase });
|
|
736
|
+
try {
|
|
737
|
+
loadOws().deleteWallet(existing.agentWalletName);
|
|
738
|
+
}
|
|
739
|
+
catch { /* already gone */ }
|
|
740
|
+
deleteVenueAgent("risex", agentName);
|
|
741
|
+
}
|
|
742
|
+
else if (p.rotate && loadOws().listWallets().some((w) => w.name === risexAgentWalletName(masterName, p.network))) {
|
|
743
|
+
throw new PerpError("INVALID_PARAMS", `OWS wallet "${risexAgentWalletName(masterName, p.network)}" exists but no RISEx signer record uses it`, {
|
|
744
|
+
remediation: `If no RISEx signer uses its key, delete it (ows wallet delete ${risexAgentWalletName(masterName, p.network)}) and re-run the approve`,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
const meta = await approveRisexAgent({ agentName, masterOwsWallet: masterName, passphrase: p.passphrase, network: p.network, expiresAt });
|
|
748
|
+
setVenueAgent("risex", meta);
|
|
749
|
+
return { venue: "risex", meta };
|
|
750
|
+
}
|
|
751
|
+
finally {
|
|
752
|
+
releaseLock("risex");
|
|
753
|
+
}
|
|
754
|
+
}
|
|
663
755
|
const expiresAt = parseExpiresInParam(p.expiresIn);
|
|
664
756
|
const masterName = requireMaster(p.master);
|
|
665
757
|
const expiresAtIso = expiresAt.toISOString();
|
|
@@ -993,6 +1085,16 @@ export function printApproveOutcome(outcome) {
|
|
|
993
1085
|
console.log(` Valid Until: ${chalk.cyan(outcome.meta.validUntil)}`);
|
|
994
1086
|
console.log();
|
|
995
1087
|
return;
|
|
1088
|
+
case "risex":
|
|
1089
|
+
console.log(chalk.green.bold("\n RISEx signer registered and restricted to trading-only!\n"));
|
|
1090
|
+
console.log(` Name: ${chalk.cyan(outcome.meta.agentName)}`);
|
|
1091
|
+
console.log(` Signer: ${chalk.cyan(outcome.meta.signerAddress)}`);
|
|
1092
|
+
console.log(` Account: ${chalk.cyan(outcome.meta.accountAddress)}`);
|
|
1093
|
+
console.log(` Expires: ${chalk.cyan(outcome.meta.expiresAt)}`);
|
|
1094
|
+
console.log(` Permissions: ${chalk.cyan("trading only")} ${chalk.gray(`(on-chain; disabled ${outcome.meta.permissions.disabled.join(", ")}; verified ${outcome.meta.permissions.verifiedAt})`)}`);
|
|
1095
|
+
console.log(chalk.gray(" Deposits / withdrawals: RISEx app only (perp-cli never moves RISEx funds)."));
|
|
1096
|
+
console.log();
|
|
1097
|
+
return;
|
|
996
1098
|
case "lighter":
|
|
997
1099
|
case "lighter-rh":
|
|
998
1100
|
console.log(chalk.green.bold(`\n ${getVenue(outcome.venue).displayName} Agent approved successfully!\n`));
|
|
@@ -1043,7 +1145,7 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
1043
1145
|
// ── agent approve <exchange> ── register an agent wallet ──────────────────
|
|
1044
1146
|
agent
|
|
1045
1147
|
.command("approve <exchange>")
|
|
1046
|
-
.description("Register an agent wallet / API key for an exchange (aster, hyperliquid, pacifica, lighter, lighter-rh, ondo, arcus)")
|
|
1148
|
+
.description("Register an agent wallet / API key for an exchange (aster, hyperliquid, pacifica, lighter, lighter-rh, ondo, arcus, risex — risex: a trading-only session signer, restricted on-chain)")
|
|
1047
1149
|
.option("--master <name>", "Master OWS wallet name")
|
|
1048
1150
|
.option("--agent-name <name>", "Agent name (default: perp-cli-aster)")
|
|
1049
1151
|
.option("--expires-in <duration>", "Expiry: 30d, 90d, 180d, 1y, or ISO-8601 datetime (default: 90d)")
|
|
@@ -1087,7 +1189,8 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
1087
1189
|
assertAgentApproveSupported(venue, network, "approve");
|
|
1088
1190
|
let master = opts.master ?? null;
|
|
1089
1191
|
let agentName = opts.agentName ?? null;
|
|
1090
|
-
|
|
1192
|
+
// RISEx signers default to 30 days (owner decision; the venue maximum is not documented).
|
|
1193
|
+
let expiresIn = opts.expiresIn ?? (venue === "risex" ? "30d" : "90d");
|
|
1091
1194
|
let canPerp = opts.canPerp ?? true;
|
|
1092
1195
|
let canSpot = opts.canSpot ?? false;
|
|
1093
1196
|
let canWithdraw = opts.canWithdraw ?? false;
|
|
@@ -1218,6 +1321,9 @@ Examples:
|
|
|
1218
1321
|
}
|
|
1219
1322
|
const network = networkOf(command);
|
|
1220
1323
|
const venue = resolveVenueId(exchange);
|
|
1324
|
+
// TxFlow (market data only) / RISEx (perp-cli-registered, restricted signers only): refused before any stdin read.
|
|
1325
|
+
const { assertImportSupported } = await import("./agent-import.js");
|
|
1326
|
+
assertImportSupported(venue, network);
|
|
1221
1327
|
const int = (flag, raw) => {
|
|
1222
1328
|
if (raw === undefined)
|
|
1223
1329
|
return null;
|
|
@@ -1296,14 +1402,14 @@ Examples:
|
|
|
1296
1402
|
for (const { exchange: ex, meta } of listVenueAgents()) {
|
|
1297
1403
|
if (venueFilter !== null && venueFilter !== ex)
|
|
1298
1404
|
continue;
|
|
1299
|
-
const expiresAt = meta.kind === "arcus" ? meta.validUntil : null;
|
|
1405
|
+
const expiresAt = meta.kind === "arcus" ? meta.validUntil : meta.kind === "risex" ? meta.expiresAt : null;
|
|
1300
1406
|
const status = expiresAt !== null && expiresAt !== "" && new Date(expiresAt).getTime() < now ? "expired" : "active";
|
|
1301
1407
|
rows.push({
|
|
1302
1408
|
name: meta.agentName,
|
|
1303
1409
|
exchange: ex,
|
|
1304
1410
|
network: meta.network,
|
|
1305
1411
|
source: agentSourceOf(ex, meta),
|
|
1306
|
-
evmAddress: meta.kind === "arcus" ? meta.ownerAddress : "",
|
|
1412
|
+
evmAddress: meta.kind === "arcus" ? meta.ownerAddress : meta.kind === "risex" ? meta.signerAddress : "",
|
|
1307
1413
|
// Ondo keys carry no expiry; shown as "—".
|
|
1308
1414
|
expiresAt: expiresAt ?? "",
|
|
1309
1415
|
status,
|
|
@@ -1373,9 +1479,11 @@ Examples:
|
|
|
1373
1479
|
network = networkOf(command);
|
|
1374
1480
|
// An imported key is only ever removed locally, so it needs no venue support for revoke
|
|
1375
1481
|
// (e.g. an imported Aster testnet key).
|
|
1376
|
-
const rec = exchange === "ondo" || exchange === "arcus" || exchange === "qfex" ? getVenueAgent(exchange, agentName) : getAgent(exchange, agentName);
|
|
1482
|
+
const rec = exchange === "ondo" || exchange === "arcus" || exchange === "qfex" || exchange === "risex" ? getVenueAgent(exchange, agentName) : getAgent(exchange, agentName);
|
|
1377
1483
|
// QFEX keys only exist as imports: a missing record is "already removed", never an approve-capability error.
|
|
1378
|
-
|
|
1484
|
+
// A release-gated venue (RISEx in 0.15.0) still lets --force / --local-only clear local state: nothing is signed or sent.
|
|
1485
|
+
const localCleanupOfGated = localOnly && getVenue(exchange).gatedRoles !== null;
|
|
1486
|
+
if (exchange !== "qfex" && !localCleanupOfGated && (rec === null || agentSourceOf(exchange, rec) !== "imported"))
|
|
1379
1487
|
assertAgentApproveSupported(exchange, network, "revoke");
|
|
1380
1488
|
}
|
|
1381
1489
|
catch (e) {
|
|
@@ -1425,6 +1533,46 @@ Examples:
|
|
|
1425
1533
|
console.log(chalk.green(`Agent "${agentName}" on qfex removed locally.`) + chalk.yellow(`\n ${note}`));
|
|
1426
1534
|
return;
|
|
1427
1535
|
}
|
|
1536
|
+
// ── RISEx: RevokeSigner signed by the master wallet (gasless REST); then the key is deleted ──
|
|
1537
|
+
if (exchange === "risex") {
|
|
1538
|
+
const risexMeta = getVenueAgent("risex", agentName);
|
|
1539
|
+
if (!risexMeta) {
|
|
1540
|
+
if (useJson)
|
|
1541
|
+
printJson({ ok: true, data: { alreadyRevoked: true } });
|
|
1542
|
+
else
|
|
1543
|
+
console.log(`Agent "${agentName}" on risex not found locally — already revoked.`);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
if (risexMeta.network !== network) {
|
|
1547
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `RISEx signer "${agentName}" is registered on ${risexMeta.network}, not ${network}`, {
|
|
1548
|
+
remediation: risexMeta.network === "mainnet" ? "Run without -n testnet to revoke it" : "Run with -n testnet to revoke it",
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
let txHash = null;
|
|
1552
|
+
if (!localOnly) {
|
|
1553
|
+
const masterName = opts.master ?? loadSettings().owsActiveWallet;
|
|
1554
|
+
if (!masterName) {
|
|
1555
|
+
throw new PerpError("INVALID_PARAMS", "Master wallet name is required to revoke a RISEx signer", {
|
|
1556
|
+
remediation: "Use --master <name>, or --force to clear local state only (then revoke it in the RISEx app)",
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
const passphrase = await resolvePassphrase({ flag: passphraseFlag });
|
|
1560
|
+
const { revokeRisexAgent } = await import("../exchanges/risex/agent.js");
|
|
1561
|
+
txHash = (await revokeRisexAgent({ meta: risexMeta, masterOwsWallet: masterName, passphrase })).txHash;
|
|
1562
|
+
}
|
|
1563
|
+
try {
|
|
1564
|
+
loadOws().deleteWallet(risexMeta.agentWalletName);
|
|
1565
|
+
}
|
|
1566
|
+
catch { /* already gone */ }
|
|
1567
|
+
deleteVenueAgent("risex", agentName);
|
|
1568
|
+
if (useJson)
|
|
1569
|
+
printJson(jsonOk({ revoked: !localOnly, local: localOnly, txHash }));
|
|
1570
|
+
else
|
|
1571
|
+
console.log(localOnly
|
|
1572
|
+
? chalk.yellow(`Agent "${agentName}" on risex removed locally only — revoke signer ${risexMeta.signerAddress} in the RISEx app.`)
|
|
1573
|
+
: chalk.green(`Agent "${agentName}" on risex revoked successfully.`));
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1428
1576
|
// ── Arcus: EIP-712 RevokeApiKey signed by the master wallet ──
|
|
1429
1577
|
if (exchange === "arcus") {
|
|
1430
1578
|
const arcusMeta = getVenueAgent("arcus", agentName);
|
|
@@ -420,7 +420,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
420
420
|
};
|
|
421
421
|
// Collect spot balances per exchange
|
|
422
422
|
const spotBalancesByExchange = new Map();
|
|
423
|
-
// Bot/arb spot scope
|
|
423
|
+
// Bot/arb spot scope: configured venues with a separate spot balance + transfer.
|
|
424
424
|
const configuredVenues = new Set(await listConfiguredAccountVenues());
|
|
425
425
|
for (const def of listBotSpotVenues()) {
|
|
426
426
|
if (!configuredVenues.has(def.id))
|
|
@@ -802,7 +802,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
802
802
|
await dropStaleStateEntry(sym, opts.yes === true, dryRun);
|
|
803
803
|
return;
|
|
804
804
|
}
|
|
805
|
-
// Global quarantine
|
|
805
|
+
// Global quarantine: an unknown / unhedged outcome was recorded for
|
|
806
806
|
// this symbol. A human may still close it, but only knowingly (--force).
|
|
807
807
|
const quarantined = quarantineEntryFor(sym, (m) => console.error(chalk.red(m)));
|
|
808
808
|
if (quarantined !== null) {
|
|
@@ -846,7 +846,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
846
846
|
console.log(chalk.cyan(` [DRY-RUN] Would close spot-perp ${sym}: sell spot on ${spotExch} + buy back the perp short on ${perpExch}`));
|
|
847
847
|
return;
|
|
848
848
|
}
|
|
849
|
-
// One implementation for spot-perp close (fill-checked legs
|
|
849
|
+
// One implementation for spot-perp close (fill-checked legs): arb-auto's handler.
|
|
850
850
|
if (handleSpotClose === undefined)
|
|
851
851
|
throw new Error("spot-perp close handler is not registered (registerArbAutoCommands not called)");
|
|
852
852
|
await handleSpotClose(sym, { spotExch, perpExch }, getAdapterForExchange, isJson);
|
|
@@ -982,7 +982,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
982
982
|
// Close the legs sequentially (long, then short), reduce-only and verified, each
|
|
983
983
|
// sized from the venue's own position size. No retries: a definite failure of the
|
|
984
984
|
// long close changes nothing; an unknown outcome or a leg left open after the
|
|
985
|
-
// other closed quarantines the symbol
|
|
985
|
+
// other closed quarantines the symbol.
|
|
986
986
|
const results = [];
|
|
987
987
|
const closeRes = await closePerpPairVerified({
|
|
988
988
|
source: "arb-close", sym,
|