perp-cli 0.14.0 → 0.14.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/dist/arb/sizing.js +29 -7
- package/dist/bot/conditions.d.ts +5 -1
- package/dist/bot/conditions.js +16 -11
- package/dist/bot/engine.d.ts +12 -0
- package/dist/bot/engine.js +55 -44
- package/dist/bot/strategies/claude-agent.js +2 -2
- package/dist/bot/strategies/funding-arb-strategy.js +16 -20
- package/dist/bot/strategies/funding-arb-utils.d.ts +19 -0
- package/dist/bot/strategies/funding-arb-utils.js +44 -0
- package/dist/bot/strategies/funding-auto.js +61 -37
- package/dist/bot/strategies/grid-strategy.d.ts +2 -0
- package/dist/bot/strategies/grid-strategy.js +21 -2
- package/dist/bot/strategies/spot-perp-arb-strategy.js +28 -20
- package/dist/bot/tui/BotDashboard.d.ts +13 -2
- package/dist/bot/tui/BotDashboard.js +8 -3
- package/dist/bridge-engine.d.ts +2 -0
- package/dist/bridge-engine.js +68 -31
- package/dist/cli-program.js +19 -2
- package/dist/commands/account.js +2 -2
- package/dist/commands/arb/index.d.ts +20 -0
- package/dist/commands/arb/index.js +121 -36
- package/dist/commands/arb-auto.d.ts +36 -0
- package/dist/commands/arb-auto.js +202 -76
- package/dist/commands/arb.js +51 -35
- package/dist/commands/bot.d.ts +6 -0
- package/dist/commands/bot.js +55 -45
- package/dist/commands/funds-onchain.d.ts +1 -1
- package/dist/commands/funds-onchain.js +12 -49
- package/dist/commands/funds.js +51 -52
- package/dist/commands/history.js +165 -62
- package/dist/commands/portfolio.d.ts +5 -0
- package/dist/commands/portfolio.js +58 -20
- package/dist/commands/rebalance.js +46 -10
- package/dist/commands/settings.js +2 -1
- package/dist/commands/trade.js +8 -4
- package/dist/commands/wallet.js +74 -155
- package/dist/cross-chain-margin.d.ts +29 -2
- package/dist/cross-chain-margin.js +30 -4
- package/dist/dashboard/server.d.ts +45 -8
- package/dist/dashboard/server.js +113 -42
- package/dist/dashboard/ui.js +46 -27
- package/dist/dashboard/ws-feeds.d.ts +25 -3
- package/dist/dashboard/ws-feeds.js +219 -82
- package/dist/dex-asset-map.d.ts +23 -4
- package/dist/dex-asset-map.js +39 -13
- package/dist/exchanges/arcus/adapter.js +33 -6
- package/dist/exchanges/arcus/public.d.ts +6 -0
- package/dist/exchanges/arcus/public.js +12 -4
- package/dist/exchanges/aster.d.ts +33 -0
- package/dist/exchanges/aster.js +162 -55
- package/dist/exchanges/hyperliquid.d.ts +30 -0
- package/dist/exchanges/hyperliquid.js +159 -34
- package/dist/exchanges/lighter.d.ts +1 -1
- package/dist/exchanges/lighter.js +11 -9
- package/dist/exchanges/ondo/adapter.d.ts +4 -18
- package/dist/exchanges/ondo/adapter.js +5 -29
- package/dist/exchanges/ondo/funds.d.ts +1 -29
- package/dist/exchanges/ondo/funds.js +2 -94
- package/dist/exchanges/pacifica.d.ts +12 -0
- package/dist/exchanges/pacifica.js +78 -35
- package/dist/exchanges/qfex/adapter.js +4 -1
- package/dist/exchanges/venues/ondo.d.ts +0 -5
- package/dist/exchanges/venues/ondo.js +2 -6
- package/dist/exchanges/venues/qfex.d.ts +11 -0
- package/dist/exchanges/venues/qfex.js +10 -2
- package/dist/mcp-server.js +2 -2
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -0
- package/dist/rebalance.d.ts +20 -1
- package/dist/rebalance.js +31 -5
- package/dist/strategies/funding-arb.js +38 -7
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +20 -3
- package/skills/perp-cli/references/agent-operations.md +2 -2
- package/skills/perp-cli/references/commands.md +12 -9
package/dist/commands/bot.js
CHANGED
|
@@ -162,17 +162,10 @@ export function registerStrategyCommands(program, getAdapter, getAdapterFor, isJ
|
|
|
162
162
|
.option("--background", "Run in background (tmux)")
|
|
163
163
|
.option("--headless", "Run without TUI dashboard")
|
|
164
164
|
.option("--job-id <id>", "Job ID")
|
|
165
|
-
.action(async (opts) => {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
adapters.set(name, await getAdapterFor(name));
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
// skip unavailable
|
|
174
|
-
}
|
|
175
|
-
}
|
|
165
|
+
.action(async (opts, cmd) => {
|
|
166
|
+
const adapters = await loadBotVenueAdapters(opts.exchanges.split(",").map(e => e.trim()), cmd.getOptionValueSource("exchanges") === "cli", getAdapterFor);
|
|
167
|
+
// The strategy scans only the venues that loaded.
|
|
168
|
+
const exchangeNames = [...adapters.keys()];
|
|
176
169
|
if (adapters.size < 2) {
|
|
177
170
|
console.error(chalk.red("\n Need at least 2 exchanges for arbitrage.\n"));
|
|
178
171
|
return;
|
|
@@ -207,7 +200,8 @@ export function registerStrategyCommands(program, getAdapter, getAdapterFor, isJ
|
|
|
207
200
|
`strategy`, `quick-arb`,
|
|
208
201
|
`--min-spread`, opts.minSpread, `--close-spread`, opts.closeSpread,
|
|
209
202
|
`--size`, opts.size, `--max-positions`, opts.maxPositions,
|
|
210
|
-
|
|
203
|
+
// The venues the parent loaded (a skipped default venue is not re-tried as "explicit").
|
|
204
|
+
`--exchanges`, exchangeNames.join(","), `--interval`, opts.interval,
|
|
211
205
|
`--max-drawdown`, opts.maxDrawdown,
|
|
212
206
|
];
|
|
213
207
|
const job = startJob({
|
|
@@ -241,17 +235,10 @@ export function registerStrategyCommands(program, getAdapter, getAdapterFor, isJ
|
|
|
241
235
|
.option("--background", "Run in background (tmux)")
|
|
242
236
|
.option("--headless", "Run without TUI dashboard")
|
|
243
237
|
.option("--job-id <id>", "Job ID")
|
|
244
|
-
.action(async (opts) => {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
adapters.set(name, await getAdapterFor(name));
|
|
250
|
-
}
|
|
251
|
-
catch {
|
|
252
|
-
// skip unavailable
|
|
253
|
-
}
|
|
254
|
-
}
|
|
238
|
+
.action(async (opts, cmd) => {
|
|
239
|
+
const adapters = await loadBotVenueAdapters(opts.exchanges.split(",").map(e => e.trim()), cmd.getOptionValueSource("exchanges") === "cli", getAdapterFor);
|
|
240
|
+
// The strategy trades only the venues that loaded.
|
|
241
|
+
const exchangeNames = [...adapters.keys()];
|
|
255
242
|
if (adapters.size < 1) {
|
|
256
243
|
console.error(chalk.red("\n Need at least 1 exchange for delta-neutral.\n"));
|
|
257
244
|
return;
|
|
@@ -287,7 +274,7 @@ export function registerStrategyCommands(program, getAdapter, getAdapterFor, isJ
|
|
|
287
274
|
`strategy`, `delta-neutral`,
|
|
288
275
|
`--min-rate`, opts.minRate, `--close-rate`, opts.closeRate,
|
|
289
276
|
`--size`, opts.size, `--max-positions`, opts.maxPositions,
|
|
290
|
-
`--leverage`, opts.leverage, `--exchanges`,
|
|
277
|
+
`--leverage`, opts.leverage, `--exchanges`, exchangeNames.join(","),
|
|
291
278
|
`--interval`, opts.interval, `--max-drawdown`, opts.maxDrawdown,
|
|
292
279
|
];
|
|
293
280
|
const job = startJob({
|
|
@@ -355,15 +342,9 @@ export function registerStrategyCommands(program, getAdapter, getAdapterFor, isJ
|
|
|
355
342
|
const built = preset.buildConfig("multi", sym);
|
|
356
343
|
const config = opts.size !== undefined ? withPresetSize(built, opts.size) : built;
|
|
357
344
|
const arbStrategy = config.strategy;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
adapters.set(exName, await getAdapterFor(exName));
|
|
362
|
-
}
|
|
363
|
-
catch {
|
|
364
|
-
// skip unavailable
|
|
365
|
-
}
|
|
366
|
-
}
|
|
345
|
+
// The preset's venue list is a default (not user-passed): a failing venue is skipped with a warning.
|
|
346
|
+
const adapters = await loadBotVenueAdapters(arbStrategy.exchanges, false, getAdapterFor);
|
|
347
|
+
arbStrategy.exchanges = [...adapters.keys()];
|
|
367
348
|
if (adapters.size < 2) {
|
|
368
349
|
console.error(chalk.red("\n Need at least 2 exchanges for arbitrage.\n"));
|
|
369
350
|
return;
|
|
@@ -756,6 +737,44 @@ export function registerStrategyCommands(program, getAdapter, getAdapterFor, isJ
|
|
|
756
737
|
// Nested scripted plans: `perp strategy plan {validate|execute|example}`
|
|
757
738
|
registerStrategyPlanCommands(bot, getAdapter, isJson);
|
|
758
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* Adapters for a multi-venue bot. An explicitly passed list (--exchanges on the command line)
|
|
742
|
+
* must load in full: a venue that fails refuses the start with a PerpError naming it. A default
|
|
743
|
+
* list skips a failing venue with a stderr warning naming it and the cause.
|
|
744
|
+
*/
|
|
745
|
+
export async function loadBotVenueAdapters(names, explicit, getAdapterFor) {
|
|
746
|
+
const adapters = new Map();
|
|
747
|
+
for (const name of names) {
|
|
748
|
+
try {
|
|
749
|
+
const a = await getAdapterFor(name);
|
|
750
|
+
// A venue without credentials gets a read-only adapter: it can neither trade nor report
|
|
751
|
+
// the bot's balances, so it is "not configured" — refused when listed explicitly.
|
|
752
|
+
if (a.isReadOnly === true) {
|
|
753
|
+
if (explicit) {
|
|
754
|
+
throw new PerpError("NO_SIGNER_AVAILABLE", `--exchanges venue ${name} has no credentials configured`, {
|
|
755
|
+
exchange: name, remediation: `Set it up (perp setup) or remove it from --exchanges`,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
process.stderr.write(`[info] ${name} not configured (no credentials) — the bot runs without it\n`);
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
adapters.set(name, a);
|
|
762
|
+
}
|
|
763
|
+
catch (e) {
|
|
764
|
+
if (e instanceof PerpError && e.structured.code === "NO_SIGNER_AVAILABLE" && explicit)
|
|
765
|
+
throw e;
|
|
766
|
+
const cause = e instanceof Error ? e.message : String(e);
|
|
767
|
+
if (explicit) {
|
|
768
|
+
throw new PerpError(e instanceof PerpError ? e.structured.code : "EXCHANGE_ERROR", `--exchanges venue ${name} could not be initialised: ${cause}`, {
|
|
769
|
+
exchange: name,
|
|
770
|
+
remediation: `Fix ${name}'s setup (credentials / network: perp -e ${name} portfolio), or remove it from --exchanges`,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
process.stderr.write(`[warn] ${name} unavailable (${cause}) — the bot runs without it\n`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return adapters;
|
|
777
|
+
}
|
|
759
778
|
function makeLog() {
|
|
760
779
|
return (msg) => {
|
|
761
780
|
const ts = new Date().toLocaleTimeString();
|
|
@@ -898,19 +917,10 @@ function registerRunSubcommands(parent, getAdapter, getAdapterFor, isJson) {
|
|
|
898
917
|
.option("--rebalance-threshold <usd>", "Rebalance when available < this ($)", "100")
|
|
899
918
|
.option("--max-drawdown <usd>", "Close all positions if total uPnL exceeds this loss ($)")
|
|
900
919
|
.option("--job-id <id>", "Job ID (set automatically for background jobs)")
|
|
901
|
-
.action(async (opts) => {
|
|
920
|
+
.action(async (opts, cmd) => {
|
|
902
921
|
const exchangeNames = opts.exchanges.split(",").map((e) => e.trim());
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
try {
|
|
906
|
-
const a = await getAdapterFor(name);
|
|
907
|
-
adapters.set(name, a);
|
|
908
|
-
}
|
|
909
|
-
catch {
|
|
910
|
-
if (!isJson())
|
|
911
|
-
console.log(chalk.yellow(` Skipping ${name} (no credentials)`));
|
|
912
|
-
}
|
|
913
|
-
}
|
|
922
|
+
// An explicitly listed venue that fails to load refuses the start; a default one is skipped with a warning.
|
|
923
|
+
const adapters = await loadBotVenueAdapters(exchangeNames, cmd.getOptionValueSource("exchanges") === "cli", getAdapterFor);
|
|
914
924
|
if (adapters.size < 2) {
|
|
915
925
|
console.error(chalk.red("Need at least 2 exchanges for arbitrage. Set credentials for 2+ exchanges."));
|
|
916
926
|
process.exit(1);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `funds deposit aster|ondo`, `funds withdraw aster
|
|
2
|
+
* `funds deposit aster|ondo`, `funds withdraw aster` — fund movements
|
|
3
3
|
* whose on-chain leg is signed by the OWS master wallet (src/evm-master-tx.ts).
|
|
4
4
|
*
|
|
5
5
|
* Every command: `--dry-run` prints the plan and returns before any key, adapter or network is
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `funds deposit aster|ondo`, `funds withdraw aster
|
|
2
|
+
* `funds deposit aster|ondo`, `funds withdraw aster` — fund movements
|
|
3
3
|
* whose on-chain leg is signed by the OWS master wallet (src/evm-master-tx.ts).
|
|
4
4
|
*
|
|
5
5
|
* Every command: `--dry-run` prints the plan and returns before any key, adapter or network is
|
|
@@ -12,7 +12,7 @@ import { cliCredentialOptsFrom } from "../config.js";
|
|
|
12
12
|
import { PerpError, parsePositiveDecimal } from "../errors.js";
|
|
13
13
|
import { logExecution } from "../execution-log.js";
|
|
14
14
|
import { unsupported } from "../exchanges/trading-v2.js";
|
|
15
|
-
import { getVenue
|
|
15
|
+
import { getVenue } from "../exchanges/venue.js";
|
|
16
16
|
import { ASTER_FUNDS_CHAINS } from "../exchanges/venues/aster.js";
|
|
17
17
|
import { ONDO_DEPOSIT_CHAINS } from "../exchanges/venues/ondo.js";
|
|
18
18
|
import { jsonOk, printJson } from "../utils.js";
|
|
@@ -235,52 +235,15 @@ export function registerOnchainFundsCommands(ctx) {
|
|
|
235
235
|
}
|
|
236
236
|
});
|
|
237
237
|
// ═══ Address book (Ondo withdrawal destinations) ═══
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!/^0x[0-9a-fA-F]{40}$/.test(address))
|
|
249
|
-
throw new PerpError("INVALID_PARAMS", `"${address}" is not an EVM address`);
|
|
250
|
-
const plan = {
|
|
251
|
-
venue: "ondo", network: net, address, label: opts.label === undefined ? null : opts.label,
|
|
252
|
-
steps: "POST /v1/auth/erc-4361/address_book/get_challenge → personal_sign with the OWS master wallet → POST …/complete_challenge",
|
|
253
|
-
};
|
|
254
|
-
if (isDryRun()) {
|
|
255
|
-
if (isJson())
|
|
256
|
-
return printJson(jsonOk({ dryRun: true, sent: false, action: "address-book add", ...plan }));
|
|
257
|
-
console.log(chalk.yellow(`\n [dry-run] Would add ${address} to the Ondo Perps address book (${net})`));
|
|
258
|
-
console.log(` steps: ${plan.steps}`);
|
|
259
|
-
console.log(chalk.gray(" Nothing was signed or sent.\n"));
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
const { loadMasterEvmSigner } = await import("../evm-master-tx.js");
|
|
263
|
-
const signer = await loadMasterEvmSigner("ondo", creds(), "Adding an Ondo address book entry");
|
|
264
|
-
if (signer.getAddress().toLowerCase() !== address.toLowerCase()) {
|
|
265
|
-
throw new PerpError("INVALID_PARAMS", `The CLI only adds the master wallet's own address (${signer.getAddress()}) to the Ondo address book; ${address} is a different address`, {
|
|
266
|
-
remediation: "Add other addresses in the Ondo Perps web app (Withdraw → address book)",
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
const { OndoAdapter } = await import("../exchanges/ondo/adapter.js");
|
|
270
|
-
const adapter = await getAdapterForExchange("ondo");
|
|
271
|
-
if (!(adapter instanceof OndoAdapter))
|
|
272
|
-
throw new Error("ondo venue did not produce an OndoAdapter");
|
|
273
|
-
const { addOndoAddressBookEntry } = await import("../exchanges/ondo/funds.js");
|
|
274
|
-
const res = await addOndoAddressBookEntry(adapter.restClient, {
|
|
275
|
-
wallet: signer.getAddress(), restUrl: networkConfig(def, net).restUrl, label: plan.label, signMessage: m => signer.signMessage(m),
|
|
276
|
-
});
|
|
277
|
-
if (isJson())
|
|
278
|
-
return printJson(jsonOk({ venue: "ondo", network: net, ...res }));
|
|
279
|
-
if (res.alreadyListed)
|
|
280
|
-
console.log(chalk.green(`\n ${res.address} is already on the Ondo Perps address book.\n`));
|
|
281
|
-
else if (res.listed)
|
|
282
|
-
console.log(chalk.green(`\n Added ${res.address} to the Ondo Perps address book.\n`));
|
|
283
|
-
else
|
|
284
|
-
console.log(chalk.yellow(`\n Ondo accepted the challenge, but ${res.address} is not listed yet (a cooldown may apply). Check the Ondo Perps app before withdrawing.\n`));
|
|
238
|
+
// Ondo Perps withdrawals are web-app only (the API does not allow them), so an address-book
|
|
239
|
+
// entry made here would serve nothing. The command stays so the refusal is explicit.
|
|
240
|
+
const addressBook = funds.command("address-book").description("Not available: the Ondo Perps withdrawal address book is managed in the web app");
|
|
241
|
+
// Optional arguments: a bare `address-book add` gets the same explicit refusal, not a usage error.
|
|
242
|
+
supportsDryRun(addressBook.command("add [venue] [address]"))
|
|
243
|
+
.description("Not available: no venue offers a withdrawal address book over the CLI (Ondo Perps: web app only; refused, also with --dry-run)")
|
|
244
|
+
.option("--label <text>", "Ignored")
|
|
245
|
+
// Venue-neutral: no venue has a CLI address book, so the arguments are not even resolved.
|
|
246
|
+
.action(async () => {
|
|
247
|
+
throw unsupported("perp-cli", "address-book", "perp-cli has no withdrawal address book for any venue (Ondo Perps withdrawals are web-app only)", "Manage withdrawals and address books in the venue's web app");
|
|
285
248
|
});
|
|
286
249
|
}
|
package/dist/commands/funds.js
CHANGED
|
@@ -10,8 +10,7 @@ import { cliCredentialOptsFrom, requireMasterWallet } from "../config.js";
|
|
|
10
10
|
import { getVenue, requireCapabilities, READ_ONLY_CREDENTIALS } from "../exchanges/venue.js";
|
|
11
11
|
import { unsupported } from "../exchanges/trading-v2.js";
|
|
12
12
|
import { registerOnchainFundsCommands } from "./funds-onchain.js";
|
|
13
|
-
import {
|
|
14
|
-
import { QFEX_BANKER_NETWORK, QFEX_WITHDRAW_NETWORK } from "../exchanges/venues/qfex.js";
|
|
13
|
+
import { QFEX_BANKER_NETWORK, QFEX_WITHDRAW_DISABLED, QFEX_WITHDRAW_ENABLED, QFEX_WITHDRAW_NETWORK } from "../exchanges/venues/qfex.js";
|
|
15
14
|
import { loadSettings } from "../settings.js";
|
|
16
15
|
import { assertWithdrawAllowed, forgetWithdrawAttempt, recordWithdrawAttempt } from "../withdraw-gate.js";
|
|
17
16
|
import "../exchanges/venues/index.js";
|
|
@@ -95,6 +94,13 @@ export function chainLabel(chain) {
|
|
|
95
94
|
throw new Error(`no display label for chain "${chain}" (add it to CHAIN_LABELS)`);
|
|
96
95
|
return label;
|
|
97
96
|
}
|
|
97
|
+
/** Circle forwarding fee per HL CCTP destination, from HL_CCTP_ROUTES (null = not published). */
|
|
98
|
+
async function hlCctpFeeText() {
|
|
99
|
+
const { HL_CCTP_ROUTES } = await import("../exchanges/hyperliquid.js");
|
|
100
|
+
return Object.entries(HL_CCTP_ROUTES)
|
|
101
|
+
.map(([chain, r]) => `${chain}: ${r.forwardingFeeUsdc === null ? "fee not published" : `${r.forwardingFeeUsdc} USDC`}`)
|
|
102
|
+
.join(", ");
|
|
103
|
+
}
|
|
98
104
|
/** Withdraw asset declared by the venue definition (capabilities.withdrawAssets[0]). */
|
|
99
105
|
function withdrawAssetOf(def) {
|
|
100
106
|
const assets = requireCapabilities(def).withdrawAssets;
|
|
@@ -105,10 +111,9 @@ function withdrawAssetOf(def) {
|
|
|
105
111
|
}
|
|
106
112
|
/** Withdrawal chain of venues that settle off-chain and pay out on a public chain. */
|
|
107
113
|
const OFFCHAIN_WITHDRAW_CHAIN = {
|
|
108
|
-
ondo: ONDO_WITHDRAW_NETWORK,
|
|
109
114
|
qfex: QFEX_WITHDRAW_NETWORK,
|
|
110
115
|
};
|
|
111
|
-
/** Chain a venue's withdrawals arrive on (
|
|
116
|
+
/** Chain a venue's withdrawals arrive on (QFEX settles off-chain and pays out on Arbitrum). */
|
|
112
117
|
function withdrawChainOf(def, net) {
|
|
113
118
|
const payout = OFFCHAIN_WITHDRAW_CHAIN[def.id];
|
|
114
119
|
if (payout !== undefined)
|
|
@@ -1044,14 +1049,20 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
1044
1049
|
throw err;
|
|
1045
1050
|
}
|
|
1046
1051
|
});
|
|
1047
|
-
// ──
|
|
1052
|
+
// ── Arcus — uniform surface (design §13.1): same <amount> + --to,
|
|
1048
1053
|
// same jsonOk(result), global --dry-run plans without sending. ──
|
|
1049
|
-
|
|
1054
|
+
// Ondo Perps: the API does not allow withdrawals (address-book destinations only; adding one is
|
|
1055
|
+
// refused for API keys). The command stays so the refusal is explicit — also under --dry-run.
|
|
1056
|
+
supportsDryRun(withdraw.command("ondo [amount]"))
|
|
1057
|
+
.description("Not available: Ondo Perps withdrawals are web-app only (refused, also with --dry-run)")
|
|
1058
|
+
.option("--to <address>", "Ignored — the API cannot withdraw")
|
|
1059
|
+
.action(async () => {
|
|
1060
|
+
throw unsupported("Ondo Perps", "withdraw", "the Ondo Perps API does not allow withdrawals", "Withdraw from the Ondo Perps web app");
|
|
1061
|
+
});
|
|
1062
|
+
for (const venueId of ["arcus"]) {
|
|
1050
1063
|
const def = getVenue(venueId);
|
|
1051
1064
|
const asset = withdrawAssetOf(def);
|
|
1052
|
-
const toHelp =
|
|
1053
|
-
? "Destination EVM address (required; must be on the Ondo address book — see: perp funds address-book add ondo <address>)"
|
|
1054
|
-
: "Destination EVM address (must equal the Arcus account owner — the default)";
|
|
1065
|
+
const toHelp = "Destination EVM address (must equal the Arcus account owner — the default)";
|
|
1055
1066
|
supportsDryRun(withdraw.command(`${venueId} <amount>`))
|
|
1056
1067
|
.description(`Withdraw ${asset} from ${def.displayName} to your ${chainLabel(withdrawChainOf(def, "mainnet"))} wallet (supports --dry-run)`)
|
|
1057
1068
|
.option("--to <address>", toHelp)
|
|
@@ -1065,27 +1076,16 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
1065
1076
|
const adapter = await getAdapterForExchange(venueId);
|
|
1066
1077
|
if (!isWithdrawCapable(adapter))
|
|
1067
1078
|
throw new Error(`${def.id} adapter has no withdraw()`);
|
|
1068
|
-
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
}
|
|
1077
|
-
else {
|
|
1078
|
-
const { ArcusAdapter } = await import("../exchanges/arcus/adapter.js");
|
|
1079
|
-
if (!(adapter instanceof ArcusAdapter))
|
|
1080
|
-
throw new Error("arcus venue did not produce an ArcusAdapter");
|
|
1081
|
-
const owner = adapter.withdrawDestination;
|
|
1082
|
-
if (opts.to !== undefined && opts.to.toLowerCase() !== owner.toLowerCase()) {
|
|
1083
|
-
throw new PerpError("INVALID_PARAMS", `Arcus only withdraws to the account owner ${owner}`, {
|
|
1084
|
-
remediation: `Omit --to (defaults to ${owner})`,
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
destination = owner;
|
|
1079
|
+
const { ArcusAdapter } = await import("../exchanges/arcus/adapter.js");
|
|
1080
|
+
if (!(adapter instanceof ArcusAdapter))
|
|
1081
|
+
throw new Error("arcus venue did not produce an ArcusAdapter");
|
|
1082
|
+
const owner = adapter.withdrawDestination;
|
|
1083
|
+
if (opts.to !== undefined && opts.to.toLowerCase() !== owner.toLowerCase()) {
|
|
1084
|
+
throw new PerpError("INVALID_PARAMS", `Arcus only withdraws to the account owner ${owner}`, {
|
|
1085
|
+
remediation: `Omit --to (defaults to ${owner})`,
|
|
1086
|
+
});
|
|
1088
1087
|
}
|
|
1088
|
+
const destination = owner;
|
|
1089
1089
|
const plan = { venue: def.id, network: net, asset, amount: String(amountNum), destination, chain };
|
|
1090
1090
|
if (isDryRun()) {
|
|
1091
1091
|
logExecution({
|
|
@@ -1100,16 +1100,6 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
1100
1100
|
console.log(chalk.gray(" Nothing was sent.\n"));
|
|
1101
1101
|
return;
|
|
1102
1102
|
}
|
|
1103
|
-
if (venueId === "ondo") {
|
|
1104
|
-
// Address-book preflight (read-only): Ondo refuses destinations that are not listed.
|
|
1105
|
-
const { OndoAdapter } = await import("../exchanges/ondo/adapter.js");
|
|
1106
|
-
if (!(adapter instanceof OndoAdapter))
|
|
1107
|
-
throw new Error("ondo venue did not produce an OndoAdapter");
|
|
1108
|
-
const book = await adapter.addressBook();
|
|
1109
|
-
if (!book.some(a => a.toLowerCase() === destination.toLowerCase())) {
|
|
1110
|
-
throw (await import("../exchanges/ondo/funds.js")).ondoAddressNotOnBook(destination, book);
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
1103
|
if (!isJson()) {
|
|
1114
1104
|
console.log(chalk.cyan(`\n Withdrawing ${formatUsd(amountNum)} ${asset} from ${def.displayName}...\n`));
|
|
1115
1105
|
console.log(` Destination: ${destination} (${chain})`);
|
|
@@ -1139,7 +1129,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
1139
1129
|
});
|
|
1140
1130
|
}
|
|
1141
1131
|
// ── QFEX (design §14.6, §14.12 #8) — custodial; deposit = address display, withdraw = banker POST
|
|
1142
|
-
// gated by settings.withdraw.qfex inside QfexAdapter.withdraw(). ──
|
|
1132
|
+
// gated by settings.withdraw.qfex inside QfexAdapter.withdraw(); switched off (QFEX_WITHDRAW_ENABLED). ──
|
|
1143
1133
|
/** QFEX adapter for the active network — the default imported key, or the one named by --agent. */
|
|
1144
1134
|
const qfexAdapterFor = async (agentName) => {
|
|
1145
1135
|
const { QfexAdapter: Cls } = await import("../exchanges/qfex/adapter.js");
|
|
@@ -1164,11 +1154,15 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
1164
1154
|
throw new Error("qfex venue did not produce a QfexAdapter");
|
|
1165
1155
|
return adapter;
|
|
1166
1156
|
};
|
|
1167
|
-
supportsDryRun(withdraw.command("qfex <amount>"))
|
|
1168
|
-
.description(
|
|
1157
|
+
supportsDryRun(withdraw.command(QFEX_WITHDRAW_ENABLED ? "qfex <amount>" : "qfex [amount]"))
|
|
1158
|
+
.description(QFEX_WITHDRAW_ENABLED
|
|
1159
|
+
? "Withdraw USDC from QFEX to an allowlisted Arbitrum One address (needs settings withdraw.qfex and a key with the \"Deposit and withdraw\" permission; supports --dry-run)"
|
|
1160
|
+
: "Not available: QFEX withdrawals are disabled in perp-cli for now — use the QFEX web app (refused, also with --dry-run)")
|
|
1169
1161
|
.option("--to <address>", "Destination EVM address on Arbitrum One (required; must be on the settings withdraw.qfex allowlist)")
|
|
1170
1162
|
.option("--agent <name>", "Imported QFEX key to use (a key with the \"Deposit and withdraw\" permission, bound to the primary account)")
|
|
1171
1163
|
.action(async (amount, opts) => {
|
|
1164
|
+
if (!QFEX_WITHDRAW_ENABLED)
|
|
1165
|
+
throw unsupported("QFEX", "withdraw", QFEX_WITHDRAW_DISABLED.reason, QFEX_WITHDRAW_DISABLED.remediation);
|
|
1172
1166
|
const def = getVenue("qfex");
|
|
1173
1167
|
const net = getNetwork();
|
|
1174
1168
|
if (!/^\d+(\.\d+)?$/.test(amount) || !(Number(amount) > 0)) {
|
|
@@ -1247,7 +1241,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
1247
1241
|
console.log(chalk.yellow(`\n ${info.warning}`));
|
|
1248
1242
|
console.log(chalk.gray(" perp-cli did not send anything — transfer from your own wallet or exchange.\n"));
|
|
1249
1243
|
});
|
|
1250
|
-
// ── Aster deposit / withdraw, Ondo deposit
|
|
1244
|
+
// ── Aster deposit / withdraw, Ondo deposit (OWS-master on-chain legs; address book refused) ──
|
|
1251
1245
|
registerOnchainFundsCommands({
|
|
1252
1246
|
program, funds, deposit, withdraw, isJson, isDryRun, getNetwork, getAdapterForExchange,
|
|
1253
1247
|
requireMaster, printFundsPlan, failStatus: fundsFailStatus,
|
|
@@ -1358,11 +1352,13 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
1358
1352
|
},
|
|
1359
1353
|
withdraw: {
|
|
1360
1354
|
pacifica: { chain: "Solana", token: "USDC", speed: "~10s", fee: "none" },
|
|
1361
|
-
hyperliquid: { chain: "Arbitrum", token: "USDC", speed: "minutes", fee: "
|
|
1355
|
+
hyperliquid: { chain: "Arbitrum (default) / Ethereum / Base", token: "USDC", speed: "minutes (CCTP attestation)", method: "CCTP (sendToEvmWithData)", fee: await hlCctpFeeText(), command: "perp funds withdraw hyperliquid <amount> [--chain arbitrum|ethereum|base]" },
|
|
1362
1356
|
lighter: { chain: "Ethereum L1", token: "USDC", speed: "~12 hours (standard), minutes (fast)", fee: "varies" },
|
|
1363
1357
|
"lighter-rh": { chain: "Robinhood Chain", token: "USDG", command: "perp -e lighter-rh funds withdraw lighter <amount>" },
|
|
1364
|
-
ondo: {
|
|
1365
|
-
qfex:
|
|
1358
|
+
ondo: { supported: false, note: "the Ondo Perps API does not allow withdrawals — withdraw in the Ondo Perps web app" },
|
|
1359
|
+
qfex: QFEX_WITHDRAW_ENABLED
|
|
1360
|
+
? { chain: chainLabel(QFEX_WITHDRAW_NETWORK), token: "USDC", command: "perp funds withdraw qfex <amount> --to <allowlisted address>", gate: "settings withdraw.qfex (allowlist, maxPerTx, maxPerDay)", note: "needs a key with the \"Deposit and withdraw\" permission" }
|
|
1361
|
+
: { supported: false, note: `${QFEX_WITHDRAW_DISABLED.reason} — ${QFEX_WITHDRAW_DISABLED.remediation}` },
|
|
1366
1362
|
arcus: { chain: "Robinhood Chain", token: "USDG", command: "perp funds withdraw arcus <amount>" },
|
|
1367
1363
|
aster: { chain: "BNB Chain (default) / Arbitrum", token: "USDT (default) / USDC", command: "perp funds withdraw aster <amount> [--chain bsc|arbitrum] [--asset USDT|USDC]", fee: "POST /fapi/v3/aster/user-withdraw-info (taken from the amount)", requires: "mainnet; API wallet approved with --can-withdraw --ip-whitelist; destination = the account's own address" },
|
|
1368
1364
|
},
|
|
@@ -1403,19 +1399,22 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
1403
1399
|
console.log(chalk.white.bold(" Pacifica") + chalk.gray(" → Solana"));
|
|
1404
1400
|
console.log(` Speed: ~10s | Fee: none`);
|
|
1405
1401
|
console.log(` Command: ${chalk.green("perp funds withdraw pacifica <amount>")}`);
|
|
1406
|
-
console.log(chalk.white.bold("\n Hyperliquid") + chalk.gray(" → Arbitrum"));
|
|
1407
|
-
console.log(`
|
|
1408
|
-
console.log(` Command: ${chalk.green("perp
|
|
1402
|
+
console.log(chalk.white.bold("\n Hyperliquid") + chalk.gray(" → Arbitrum (default) / Ethereum / Base over CCTP"));
|
|
1403
|
+
console.log(` Fee: ${await hlCctpFeeText()}`);
|
|
1404
|
+
console.log(` Command: ${chalk.green("perp funds withdraw hyperliquid <amount> [--chain arbitrum|ethereum|base]")}`);
|
|
1409
1405
|
console.log(chalk.white.bold("\n Lighter") + chalk.gray(" → Ethereum L1"));
|
|
1410
1406
|
console.log(` Speed: ~12h (standard) | Fee: varies`);
|
|
1411
1407
|
console.log(` Command: ${chalk.green("perp -e lt funds withdraw lighter <amount>")}`);
|
|
1412
1408
|
console.log(chalk.white.bold("\n Lighter RH") + chalk.gray(" → Robinhood Chain (USDG)"));
|
|
1413
1409
|
console.log(` Command: ${chalk.green("perp -e lighter-rh funds withdraw lighter <amount>")}`);
|
|
1414
|
-
console.log(chalk.white.bold("\n Ondo Perps") + chalk.gray("
|
|
1415
|
-
console.log(
|
|
1416
|
-
console.log(` Address book: ${chalk.green("perp funds address-book add ondo <your master address>")}`);
|
|
1410
|
+
console.log(chalk.white.bold("\n Ondo Perps") + chalk.gray(" — not available over the API"));
|
|
1411
|
+
console.log(chalk.gray(" Withdraw in the Ondo Perps web app."));
|
|
1417
1412
|
console.log(chalk.white.bold("\n Arcus") + chalk.gray(" → Robinhood Chain (USDG, to the account owner)"));
|
|
1418
1413
|
console.log(` Command: ${chalk.green("perp funds withdraw arcus <amount>")}`);
|
|
1414
|
+
console.log(chalk.white.bold("\n QFEX") + chalk.gray(QFEX_WITHDRAW_ENABLED ? ` → ${chainLabel(QFEX_WITHDRAW_NETWORK)} (USDC, allowlisted address)` : " — disabled in perp-cli for now"));
|
|
1415
|
+
console.log(QFEX_WITHDRAW_ENABLED
|
|
1416
|
+
? ` Command: ${chalk.green("perp funds withdraw qfex <amount> --to <allowlisted address>")}`
|
|
1417
|
+
: chalk.gray(` ${QFEX_WITHDRAW_DISABLED.remediation}.`));
|
|
1419
1418
|
console.log(chalk.white.bold("\n Aster") + chalk.gray(" → BNB Chain / Arbitrum (USDT, to the account's own address; mainnet only)"));
|
|
1420
1419
|
console.log(` Needs an API wallet approved with --can-withdraw --ip-whitelist; fee from user-withdraw-info`);
|
|
1421
1420
|
console.log(` Command: ${chalk.green("perp funds withdraw aster <amount> [--chain bsc|arbitrum] [--asset USDT|USDC]")}`);
|