perp-cli 0.12.15 → 0.14.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 +278 -31
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +286 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +74 -84
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +286 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +939 -245
- package/dist/commands/history.js +72 -45
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +316 -337
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +219 -6
- package/dist/exchanges/aster.js +1023 -153
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +166 -0
- package/dist/exchanges/ondo/adapter.js +835 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +122 -0
- package/dist/exchanges/ondo/funds.js +290 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +288 -4
- package/dist/exchanges/pacifica.js +1013 -76
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1383 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +34 -0
- package/dist/exchanges/venues/ondo.js +140 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +28 -0
- package/dist/exchanges/venues/qfex.js +132 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +157 -682
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +14 -0
- package/dist/landing.js +36 -0
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +820 -402
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +338 -134
- package/skills/perp-cli/references/agent-operations.md +181 -92
- package/skills/perp-cli/references/commands.md +475 -132
- package/skills/perp-cli/references/strategies.md +44 -38
- package/skills/perp-cli/scripts/arb-monitor.sh +22 -17
- package/skills/perp-cli/scripts/funding-analysis.sh +20 -10
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +33 -16
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
package/dist/commands/funds.js
CHANGED
|
@@ -1,13 +1,46 @@
|
|
|
1
|
+
import { isDryRun as dryRunFlag, supportsDryRun } from "../cli-flags.js";
|
|
1
2
|
import chalk from "chalk";
|
|
2
3
|
import { printJson, formatUsd, jsonOk, jsonError } from "../utils.js";
|
|
3
4
|
import { logExecution } from "../execution-log.js";
|
|
4
5
|
import { hasPacificaSdk, hasEvmAddress, isWithdrawCapable, isUsdTransferCapable, hasLighterAccount } from "../exchanges/capabilities.js";
|
|
5
6
|
import { registerFundsBridgeCommands } from "./bridge.js";
|
|
6
7
|
import { registerFundsRebalanceCommands } from "./rebalance.js";
|
|
8
|
+
import { PerpError, isConnectFailure, isDefiniteOrderFailure, isTransportFailure, orderOutcomeUnknown, parsePositiveDecimal } from "../errors.js";
|
|
9
|
+
import { cliCredentialOptsFrom, requireMasterWallet } from "../config.js";
|
|
10
|
+
import { getVenue, requireCapabilities, READ_ONLY_CREDENTIALS } from "../exchanges/venue.js";
|
|
11
|
+
import { unsupported } from "../exchanges/trading-v2.js";
|
|
12
|
+
import { registerOnchainFundsCommands } from "./funds-onchain.js";
|
|
13
|
+
import { ONDO_WITHDRAW_NETWORK } from "../exchanges/venues/ondo.js";
|
|
14
|
+
import { QFEX_BANKER_NETWORK, QFEX_WITHDRAW_NETWORK } from "../exchanges/venues/qfex.js";
|
|
15
|
+
import { loadSettings } from "../settings.js";
|
|
16
|
+
import { assertWithdrawAllowed, forgetWithdrawAttempt, recordWithdrawAttempt } from "../withdraw-gate.js";
|
|
17
|
+
import "../exchanges/venues/index.js";
|
|
18
|
+
/**
|
|
19
|
+
* Execution-log status of a failed funds action: an unknown outcome (ORDER_PENDING — the request
|
|
20
|
+
* was sent and may have executed) is logged "unverified", never "failed".
|
|
21
|
+
*/
|
|
22
|
+
export function fundsFailStatus(err) {
|
|
23
|
+
return err instanceof PerpError && err.structured.code === "ORDER_PENDING" ? "unverified" : "failed";
|
|
24
|
+
}
|
|
25
|
+
/** `{ txHash }` of a broadcast transaction whose outcome is unknown (ORDER_PENDING details), for the execution log. */
|
|
26
|
+
function pendingTxMeta(err) {
|
|
27
|
+
const h = err instanceof PerpError ? err.structured.details?.txHash : undefined;
|
|
28
|
+
return typeof h === "string" ? { txHash: h } : {};
|
|
29
|
+
}
|
|
7
30
|
const DEFAULT_RELAYER = "http://localhost:3100";
|
|
8
31
|
function getRelayerUrl() {
|
|
9
32
|
return process.env.PERP_RELAYER_URL || DEFAULT_RELAYER;
|
|
10
33
|
}
|
|
34
|
+
function relayerUnavailable() {
|
|
35
|
+
return new PerpError("EXCHANGE_UNREACHABLE", `The relayer at ${getRelayerUrl()} is not reachable; nothing was sent`, {
|
|
36
|
+
remediation: "Start the relayer / set PERP_RELAYER_URL, or drop --relay for a direct deposit (you pay gas)",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** A broadcast deposit whose confirmation could not be read: unknown outcome, carrying the signature. */
|
|
40
|
+
export function depositOutcomeUnknown(signature, e) {
|
|
41
|
+
const pending = orderOutcomeUnknown("pacifica", `deposit transaction ${signature}`, e, `Do NOT deposit again yet — the transaction was broadcast and may still confirm. Check signature ${signature} on a Solana explorer, then: perp -e pacifica account balance`);
|
|
42
|
+
return new PerpError("ORDER_PENDING", pending.message, { exchange: "pacifica", signature, remediation: pending.structured.remediation });
|
|
43
|
+
}
|
|
11
44
|
async function relayerAvailable() {
|
|
12
45
|
try {
|
|
13
46
|
const res = await fetch(`${getRelayerUrl()}/health`, { signal: AbortSignal.timeout(2000) });
|
|
@@ -17,30 +50,159 @@ async function relayerAvailable() {
|
|
|
17
50
|
return false;
|
|
18
51
|
}
|
|
19
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Local withdraw-gate verdict for a Hyperliquid destination that is not (known to be) the account
|
|
55
|
+
* owner: settings.withdraw.hyperliquid allowlist + maxPerTx / maxPerDay (the QFEX gate mechanism,
|
|
56
|
+
* src/withdraw-gate.ts). "passes" or "would refuse: …"; never throws a POLICY_DENIED (dry runs).
|
|
57
|
+
*/
|
|
58
|
+
export function hyperliquidGateVerdict(network, amount, destination, nowMs) {
|
|
59
|
+
const gate = loadSettings().withdraw.hyperliquid;
|
|
60
|
+
try {
|
|
61
|
+
assertWithdrawAllowed("hyperliquid", network, amount, destination, gate === undefined ? null : gate, nowMs);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (e instanceof PerpError && e.structured.code === "POLICY_DENIED")
|
|
65
|
+
return `would refuse: ${e.message}`;
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
return "passes (destination allowlisted in settings withdraw.hyperliquid, within maxPerTx / maxPerDay)";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Enforce settings.withdraw.hyperliquid for a master-signed withdraw3 / usdSend to `destination`
|
|
72
|
+
* and record the attempt in the withdraw ledger BEFORE it is sent. Returns the ledger id (forget it
|
|
73
|
+
* only on a definite refusal), or null when the destination is the owner (no gate, no ledger).
|
|
74
|
+
*/
|
|
75
|
+
export function hyperliquidGateEnforce(network, amount, destination, owner, nowMs) {
|
|
76
|
+
if (owner !== null && destination.toLowerCase() === owner.toLowerCase())
|
|
77
|
+
return null;
|
|
78
|
+
const gate = loadSettings().withdraw.hyperliquid;
|
|
79
|
+
assertWithdrawAllowed("hyperliquid", network, amount, destination, gate === undefined ? null : gate, nowMs);
|
|
80
|
+
return recordWithdrawAttempt("hyperliquid", network, amount, destination, nowMs);
|
|
81
|
+
}
|
|
82
|
+
/** Display names of `VenueNetworkConfig.settlementChain` / withdraw-network ids. */
|
|
83
|
+
const CHAIN_LABELS = {
|
|
84
|
+
ethereum: "Ethereum",
|
|
85
|
+
robinhood: "Robinhood Chain",
|
|
86
|
+
arbitrum: "Arbitrum",
|
|
87
|
+
solana: "Solana",
|
|
88
|
+
bnb: "BNB Chain",
|
|
89
|
+
hyperliquid: "Hyperliquid",
|
|
90
|
+
};
|
|
91
|
+
/** Label for a chain id. An unmapped id is a programming error (covered by a unit test over every venue). */
|
|
92
|
+
export function chainLabel(chain) {
|
|
93
|
+
const label = CHAIN_LABELS[chain];
|
|
94
|
+
if (label === undefined)
|
|
95
|
+
throw new Error(`no display label for chain "${chain}" (add it to CHAIN_LABELS)`);
|
|
96
|
+
return label;
|
|
97
|
+
}
|
|
98
|
+
/** Withdraw asset declared by the venue definition (capabilities.withdrawAssets[0]). */
|
|
99
|
+
function withdrawAssetOf(def) {
|
|
100
|
+
const assets = requireCapabilities(def).withdrawAssets;
|
|
101
|
+
if (assets === null || assets.length === 0) {
|
|
102
|
+
throw new Error(`${def.id} declares no withdrawAssets but has a withdraw command`);
|
|
103
|
+
}
|
|
104
|
+
return assets[0];
|
|
105
|
+
}
|
|
106
|
+
/** Withdrawal chain of venues that settle off-chain and pay out on a public chain. */
|
|
107
|
+
const OFFCHAIN_WITHDRAW_CHAIN = {
|
|
108
|
+
ondo: ONDO_WITHDRAW_NETWORK,
|
|
109
|
+
qfex: QFEX_WITHDRAW_NETWORK,
|
|
110
|
+
};
|
|
111
|
+
/** Chain a venue's withdrawals arrive on (Ondo / QFEX settle off-chain and pay out on Ethereum / Arbitrum). */
|
|
112
|
+
function withdrawChainOf(def, net) {
|
|
113
|
+
const payout = OFFCHAIN_WITHDRAW_CHAIN[def.id];
|
|
114
|
+
if (payout !== undefined)
|
|
115
|
+
return payout;
|
|
116
|
+
const cfg = def.networks[net];
|
|
117
|
+
if (cfg === null) {
|
|
118
|
+
throw new PerpError("NOT_IMPLEMENTED", `${def.displayName} has no ${net} environment`, {
|
|
119
|
+
remediation: `Run without --network testnet for ${def.id}`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return cfg.settlementChain;
|
|
123
|
+
}
|
|
124
|
+
/** "-e lighter: USDC → Ethereum; -e lighter-rh: USDG → Robinhood Chain" — from the venue definitions. */
|
|
125
|
+
export function lighterFamilyWithdrawSummary() {
|
|
126
|
+
return ["lighter", "lighter-rh"].map(id => {
|
|
127
|
+
const def = getVenue(id);
|
|
128
|
+
return `-e ${id}: ${withdrawAssetOf(def)} → ${chainLabel(def.networks.mainnet.settlementChain)}`;
|
|
129
|
+
}).join("; ");
|
|
130
|
+
}
|
|
20
131
|
export function registerFundsCommands(program, getAdapter, isJson, getNetwork, getAdapterForExchange) {
|
|
21
132
|
const funds = program.command("funds").description("Fund movement — deposit, withdraw, transfer, bridge, rebalance");
|
|
133
|
+
/** Global --dry-run (root flag; never re-declared on a subcommand — the root would swallow it). */
|
|
134
|
+
const isDryRun = () => dryRunFlag(program);
|
|
135
|
+
/**
|
|
136
|
+
* Master-only fund movement: NO_SIGNER_AVAILABLE (do it on the website / import the master)
|
|
137
|
+
* when no master wallet is on this machine — runs after the dry-run return, before any key or
|
|
138
|
+
* adapter is loaded. An imported agent / API key never signs these.
|
|
139
|
+
*/
|
|
140
|
+
const requireMaster = (venue, action) => requireMasterWallet(venue, cliCredentialOptsFrom(program.opts()), action);
|
|
141
|
+
/**
|
|
142
|
+
* Dry-run exit for a fund movement: log + print the plan (`sent: false`) and send nothing.
|
|
143
|
+
* Every caller returns this BEFORE loading a key, building an adapter, or contacting a relayer.
|
|
144
|
+
*/
|
|
145
|
+
const printFundsPlan = (plan) => {
|
|
146
|
+
logExecution({
|
|
147
|
+
type: "bridge", exchange: plan.venue, symbol: plan.asset, side: plan.action,
|
|
148
|
+
size: plan.amount, status: "simulated", dryRun: true, meta: { ...plan },
|
|
149
|
+
});
|
|
150
|
+
if (isJson()) {
|
|
151
|
+
printJson(jsonOk({ dryRun: true, sent: false, ...plan }));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
console.log(chalk.yellow(`\n [dry-run] Would ${plan.action} ${plan.amount} ${plan.asset} (${plan.venue}, ${plan.network})`));
|
|
155
|
+
for (const [k, v] of Object.entries(plan)) {
|
|
156
|
+
if (["action", "venue", "network", "asset", "amount"].includes(k))
|
|
157
|
+
continue;
|
|
158
|
+
console.log(` ${k}: ${String(v)}`);
|
|
159
|
+
}
|
|
160
|
+
console.log(chalk.gray(" Nothing was sent.\n"));
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Deposit routes whose chain RPC / bridge contract / token addresses are
|
|
164
|
+
* mainnet deployments (hardcoded below) have no testnet equivalent: a -n
|
|
165
|
+
* testnet run must fail before any key is loaded, never sign on mainnet.
|
|
166
|
+
*/
|
|
167
|
+
const requireMainnetDepositRoute = (route) => {
|
|
168
|
+
const network = getNetwork();
|
|
169
|
+
if (network === "mainnet")
|
|
170
|
+
return;
|
|
171
|
+
throw new PerpError("NO_BRIDGE_ROUTE", `${route} has no ${network} route — its chain RPC and contracts are mainnet deployments`, {
|
|
172
|
+
remediation: "Run without -n testnet to deposit on mainnet, or fund the testnet account from the venue's testnet faucet",
|
|
173
|
+
});
|
|
174
|
+
};
|
|
22
175
|
// ═══════════════════════════════════════════════════════
|
|
23
176
|
// DEPOSIT
|
|
24
177
|
// ═══════════════════════════════════════════════════════
|
|
25
178
|
const deposit = funds.command("deposit").description("Deposit funds into exchange accounts");
|
|
26
179
|
// ── Pacifica (Solana) ──
|
|
27
|
-
deposit
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.option("--no-relay", "
|
|
31
|
-
.action(async (amount,
|
|
32
|
-
const
|
|
180
|
+
supportsDryRun(deposit.command("pacifica <amount>"))
|
|
181
|
+
.description("Deposit USDC into Pacifica (supports --dry-run)")
|
|
182
|
+
.option("--relay", "Opt in to the gas-sponsoring relayer (PERP_RELAYER_URL); default: direct deposit, you pay gas")
|
|
183
|
+
.option("--no-relay", "Direct deposit, pay gas yourself (the default)")
|
|
184
|
+
.action(async (amount, rawOpts) => {
|
|
185
|
+
const opts = { relay: rawOpts.relay === true };
|
|
186
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
33
187
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
34
188
|
throw new Error("Invalid amount");
|
|
35
189
|
if (amountNum < 10)
|
|
36
190
|
throw new Error("Minimum deposit is 10 USDC");
|
|
37
|
-
|
|
38
|
-
|
|
191
|
+
if (isDryRun()) {
|
|
192
|
+
return printFundsPlan({ action: "deposit", venue: "pacifica", network: getNetwork(), asset: "USDC", amount: String(amountNum), chain: "solana", method: opts.relay ? "relayer" : "direct" });
|
|
193
|
+
}
|
|
194
|
+
await requireMaster("pacifica", "Pacifica deposit");
|
|
195
|
+
// Relayer (gas-sponsored) only when asked for with --relay; an unreachable relayer is an
|
|
196
|
+
// error, never a silent switch to a direct deposit.
|
|
197
|
+
if (opts.relay && !(await relayerAvailable()))
|
|
198
|
+
throw relayerUnavailable();
|
|
199
|
+
if (opts.relay) {
|
|
39
200
|
if (!isJson())
|
|
40
201
|
console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} via relayer (gasless)...\n`));
|
|
41
|
-
|
|
202
|
+
// The subcommand names the venue: fetch it directly, independent of -e.
|
|
203
|
+
const adapter = await getAdapterForExchange("pacifica");
|
|
42
204
|
if (adapter.name !== "pacifica")
|
|
43
|
-
throw new Error("
|
|
205
|
+
throw new Error("pacifica venue did not produce a Pacifica adapter");
|
|
44
206
|
const { PacificaAdapter } = await import("../exchanges/pacifica.js");
|
|
45
207
|
const pacAdapter = adapter;
|
|
46
208
|
try {
|
|
@@ -48,7 +210,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
48
210
|
method: "POST",
|
|
49
211
|
headers: { "Content-Type": "application/json" },
|
|
50
212
|
body: JSON.stringify({
|
|
51
|
-
userPubkey: pacAdapter.
|
|
213
|
+
userPubkey: pacAdapter.masterSolanaSigner().getPublicKeyBase58(),
|
|
52
214
|
amount: amountNum,
|
|
53
215
|
testnet: getNetwork() === "testnet",
|
|
54
216
|
}),
|
|
@@ -57,14 +219,31 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
57
219
|
const err = await buildRes.json();
|
|
58
220
|
throw new Error(err.error || "Relayer build failed");
|
|
59
221
|
}
|
|
60
|
-
const
|
|
222
|
+
const built = (await buildRes.json());
|
|
223
|
+
if (typeof built.transaction !== "string" || typeof built.fee !== "number" || typeof built.netAmount !== "number") {
|
|
224
|
+
throw new PerpError("POLICY_DENIED", "The relayer's build response is missing transaction / fee / netAmount; nothing was signed", {
|
|
225
|
+
remediation: "Drop --relay for a direct deposit (you pay gas)",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const { transaction, fee, netAmount } = built;
|
|
61
229
|
if (!isJson()) {
|
|
62
230
|
console.log(` Fee: $${formatUsd(fee)} (gas sponsored)`);
|
|
63
231
|
console.log(` Net: $${formatUsd(netAmount)} to Pacifica\n`);
|
|
64
232
|
}
|
|
65
233
|
const { Transaction } = await import("@solana/web3.js");
|
|
234
|
+
const { signSolanaTransaction } = await import("../pacifica/sign-tx.js");
|
|
66
235
|
const tx = Transaction.from(Buffer.from(transaction, "base64"));
|
|
67
|
-
|
|
236
|
+
// Never blind-sign a relayer-built transaction with the master key: it must be exactly the
|
|
237
|
+
// pinned Pacifica deposit (+ compute budget, + the declared, capped fee transfer) — else refuse.
|
|
238
|
+
const { assertRelayedDepositTransaction } = await import("../pacifica/deposit.js");
|
|
239
|
+
const { PublicKey: SolPublicKey } = await import("@solana/web3.js");
|
|
240
|
+
await assertRelayedDepositTransaction(tx, {
|
|
241
|
+
user: new SolPublicKey(pacAdapter.masterSolanaSigner().getPublicKeyBase58()),
|
|
242
|
+
requestedAmount: amount.trim(), network: getNetwork(),
|
|
243
|
+
declared: { fee, netAmount, feeRecipient: typeof built.feeRecipient === "string" ? built.feeRecipient : null },
|
|
244
|
+
});
|
|
245
|
+
// The relayer pays the fee and adds its signature after ours.
|
|
246
|
+
signSolanaTransaction(pacAdapter.masterSolanaSigner(), tx, true);
|
|
68
247
|
const signed = tx.serialize().toString("base64");
|
|
69
248
|
const submitRes = await fetch(`${getRelayerUrl()}/deposit/pacifica/submit`, {
|
|
70
249
|
method: "POST",
|
|
@@ -94,7 +273,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
94
273
|
catch (err) {
|
|
95
274
|
logExecution({
|
|
96
275
|
type: "bridge", exchange: "pacifica", symbol: "USDC", side: "deposit",
|
|
97
|
-
size: String(amountNum), status:
|
|
276
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
98
277
|
error: err instanceof Error ? err.message : String(err),
|
|
99
278
|
meta: { action: "deposit", method: "relay" },
|
|
100
279
|
});
|
|
@@ -106,9 +285,10 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
106
285
|
console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} USDC into Pacifica...\n`));
|
|
107
286
|
console.log(chalk.gray(" Requires: SOL for gas (~0.005 SOL) + USDC on Solana\n"));
|
|
108
287
|
}
|
|
109
|
-
|
|
288
|
+
// The subcommand names the venue: fetch it directly, independent of -e.
|
|
289
|
+
const adapter = await getAdapterForExchange("pacifica");
|
|
110
290
|
if (adapter.name !== "pacifica")
|
|
111
|
-
throw new Error("
|
|
291
|
+
throw new Error("pacifica venue did not produce a Pacifica adapter");
|
|
112
292
|
const { PacificaAdapter: PacAdapterCls } = await import("../exchanges/pacifica.js");
|
|
113
293
|
const pacDirect = adapter;
|
|
114
294
|
try {
|
|
@@ -117,7 +297,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
117
297
|
const network = getNetwork();
|
|
118
298
|
const rpcUrl = network === "testnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com";
|
|
119
299
|
const connection = new Connection(rpcUrl, "confirmed");
|
|
120
|
-
const
|
|
300
|
+
const { PublicKey } = await import("@solana/web3.js");
|
|
301
|
+
const { getNetworkConfig } = await import("../pacifica/constants.js");
|
|
302
|
+
const { signSolanaTransaction } = await import("../pacifica/sign-tx.js");
|
|
303
|
+
const masterSigner = pacDirect.masterSolanaSigner();
|
|
304
|
+
const owner = new PublicKey(masterSigner.getPublicKeyBase58());
|
|
305
|
+
const solBalance = await connection.getBalance(owner);
|
|
121
306
|
if (solBalance < 5_000_000) {
|
|
122
307
|
if (isJson()) {
|
|
123
308
|
printJson(jsonError("INSUFFICIENT_BALANCE", "Insufficient SOL for gas. Need ~0.005 SOL."));
|
|
@@ -128,9 +313,9 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
128
313
|
}
|
|
129
314
|
process.exit(1);
|
|
130
315
|
}
|
|
131
|
-
|
|
132
|
-
const USDC_MINT = new PublicKey(
|
|
133
|
-
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(
|
|
316
|
+
// Testnet collateral is the devnet USDP mint, mainnet is USDC (pacifica/constants.ts).
|
|
317
|
+
const USDC_MINT = new PublicKey(getNetworkConfig(network).usdcMint);
|
|
318
|
+
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(owner, { mint: USDC_MINT });
|
|
134
319
|
const usdcBalance = tokenAccounts.value.length > 0
|
|
135
320
|
? tokenAccounts.value[0].account.data.parsed.info.tokenAmount.uiAmount ?? 0
|
|
136
321
|
: 0;
|
|
@@ -143,13 +328,38 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
143
328
|
}
|
|
144
329
|
process.exit(1);
|
|
145
330
|
}
|
|
146
|
-
const ix = await buildDepositInstruction(
|
|
331
|
+
const ix = await buildDepositInstruction(owner, amount.trim(), network);
|
|
147
332
|
const tx = new Transaction().add(ix);
|
|
148
|
-
tx.feePayer =
|
|
333
|
+
tx.feePayer = owner;
|
|
149
334
|
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
150
|
-
tx
|
|
151
|
-
|
|
152
|
-
|
|
335
|
+
signSolanaTransaction(masterSigner, tx, false);
|
|
336
|
+
// The signature is known before the send: a transport failure after the request left
|
|
337
|
+
// (reset, timeout — not a refused connection) is an unknown outcome carrying it.
|
|
338
|
+
const signedSig = tx.signature;
|
|
339
|
+
let sig;
|
|
340
|
+
try {
|
|
341
|
+
sig = await connection.sendRawTransaction(tx.serialize());
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
if (signedSig !== null && isTransportFailure(e) && !isConnectFailure(e)) {
|
|
345
|
+
throw depositOutcomeUnknown((await import("bs58")).default.encode(signedSig), e);
|
|
346
|
+
}
|
|
347
|
+
throw e;
|
|
348
|
+
}
|
|
349
|
+
// Broadcast: from here a confirmation timeout / RPC error is an UNKNOWN outcome (the
|
|
350
|
+
// transaction may still land) — reported with the signature, never as "failed".
|
|
351
|
+
let confirmation;
|
|
352
|
+
try {
|
|
353
|
+
confirmation = await connection.confirmTransaction(sig, "confirmed");
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
throw depositOutcomeUnknown(sig, e);
|
|
357
|
+
}
|
|
358
|
+
if (confirmation?.value?.err !== undefined && confirmation.value.err !== null) {
|
|
359
|
+
throw new PerpError("ACTION_REJECTED", `Pacifica deposit transaction ${sig} failed on-chain: ${JSON.stringify(confirmation.value.err)} — nothing was deposited`, {
|
|
360
|
+
exchange: "pacifica", signature: sig,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
153
363
|
logExecution({
|
|
154
364
|
type: "bridge", exchange: "pacifica", symbol: "USDC", side: "deposit",
|
|
155
365
|
size: String(amountNum), status: "success", dryRun: false,
|
|
@@ -162,45 +372,52 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
162
372
|
console.log(` Signature: ${sig}\n`);
|
|
163
373
|
}
|
|
164
374
|
catch (err) {
|
|
375
|
+
const pendingSig = err instanceof PerpError ? err.structured.details?.signature : undefined;
|
|
165
376
|
logExecution({
|
|
166
377
|
type: "bridge", exchange: "pacifica", symbol: "USDC", side: "deposit",
|
|
167
|
-
size: String(amountNum), status:
|
|
378
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
168
379
|
error: err instanceof Error ? err.message : String(err),
|
|
169
|
-
meta: { action: "deposit", method: "direct" },
|
|
380
|
+
meta: { action: "deposit", method: "direct", ...(typeof pendingSig === "string" ? { signature: pendingSig } : {}) },
|
|
170
381
|
});
|
|
171
382
|
throw err;
|
|
172
383
|
}
|
|
173
384
|
});
|
|
174
385
|
// ── Hyperliquid (Arbitrum — USDC transfer to Bridge2) ──
|
|
175
|
-
deposit
|
|
176
|
-
.
|
|
177
|
-
.
|
|
178
|
-
.option("--no-relay", "
|
|
179
|
-
.action(async (amount,
|
|
180
|
-
const
|
|
386
|
+
supportsDryRun(deposit.command("hyperliquid <amount>"))
|
|
387
|
+
.description("Deposit USDC into Hyperliquid (Arbitrum; supports --dry-run)")
|
|
388
|
+
.option("--relay", "Opt in to the gas-sponsoring relayer (PERP_RELAYER_URL); default: direct deposit, you pay gas")
|
|
389
|
+
.option("--no-relay", "Direct deposit, pay gas yourself (the default)")
|
|
390
|
+
.action(async (amount, rawOpts) => {
|
|
391
|
+
const opts = { relay: rawOpts.relay === true };
|
|
392
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
181
393
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
182
394
|
throw new Error("Invalid amount");
|
|
183
395
|
if (amountNum < 5)
|
|
184
396
|
throw new Error("Minimum deposit is $5 for Hyperliquid");
|
|
185
|
-
//
|
|
186
|
-
|
|
397
|
+
// Relayer and direct Bridge2 transfer both target Arbitrum One + the mainnet HL bridge.
|
|
398
|
+
requireMainnetDepositRoute("Hyperliquid deposit (Arbitrum Bridge2 / relayer)");
|
|
399
|
+
if (isDryRun()) {
|
|
400
|
+
return printFundsPlan({ action: "deposit", venue: "hyperliquid", network: getNetwork(), asset: "USDC", amount: String(amountNum), chain: "arbitrum", method: opts.relay ? "relayer" : "USDC transfer to Bridge2" });
|
|
401
|
+
}
|
|
402
|
+
await requireMaster("hyperliquid", "Hyperliquid deposit");
|
|
403
|
+
// Relayer only when asked for with --relay; an unreachable relayer is an error.
|
|
404
|
+
if (opts.relay && !(await relayerAvailable()))
|
|
405
|
+
throw relayerUnavailable();
|
|
406
|
+
// The depositing wallet is the OWS master (no raw key): its address for the relayer, its
|
|
407
|
+
// transaction signer for the direct Bridge2 transfer.
|
|
408
|
+
const evm = await import("../evm-master-tx.js");
|
|
409
|
+
const signer = await evm.loadMasterEvmSigner("hyperliquid", cliCredentialOptsFrom(program.opts()), "Hyperliquid deposit");
|
|
410
|
+
const depositor = signer.getAddress();
|
|
411
|
+
if (opts.relay) {
|
|
187
412
|
if (!isJson())
|
|
188
413
|
console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} via relayer (gasless)...\n`));
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
let pk;
|
|
192
|
-
try {
|
|
193
|
-
pk = await loadPrivateKey("hyperliquid");
|
|
194
|
-
}
|
|
195
|
-
catch (err) {
|
|
196
|
-
throw new Error(`Private key not configured for Hyperliquid. Run: perp setup`);
|
|
197
|
-
}
|
|
198
|
-
const wallet = new ethers.Wallet(pk);
|
|
414
|
+
// The relayer is given the account address and the amount only. Nothing it returns is
|
|
415
|
+
// ever signed with the master key on this path.
|
|
199
416
|
try {
|
|
200
417
|
const res = await fetch(`${getRelayerUrl()}/deposit/hyperliquid`, {
|
|
201
418
|
method: "POST",
|
|
202
419
|
headers: { "Content-Type": "application/json" },
|
|
203
|
-
body: JSON.stringify({ userAddress:
|
|
420
|
+
body: JSON.stringify({ userAddress: depositor, amount: amountNum }),
|
|
204
421
|
});
|
|
205
422
|
const result = (await res.json());
|
|
206
423
|
if (!res.ok)
|
|
@@ -222,7 +439,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
222
439
|
catch (err) {
|
|
223
440
|
logExecution({
|
|
224
441
|
type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "deposit",
|
|
225
|
-
size: String(amountNum), status:
|
|
442
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
226
443
|
error: err instanceof Error ? err.message : String(err),
|
|
227
444
|
meta: { action: "deposit", method: "relay" },
|
|
228
445
|
});
|
|
@@ -234,150 +451,140 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
234
451
|
console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} USDC into Hyperliquid...\n`));
|
|
235
452
|
console.log(chalk.gray(" Requires: ETH on Arbitrum for gas (~0.0001 ETH) + USDC on Arbitrum\n"));
|
|
236
453
|
}
|
|
237
|
-
const { ethers } = await import("ethers");
|
|
238
|
-
const { loadPrivateKey } = await import("../config.js");
|
|
239
|
-
const pk = await loadPrivateKey("hyperliquid");
|
|
240
|
-
const provider = new ethers.JsonRpcProvider("https://arb1.arbitrum.io/rpc");
|
|
241
|
-
const wallet = new ethers.Wallet(pk, provider);
|
|
242
454
|
const HL_BRIDGE = "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7";
|
|
243
455
|
const USDC_ARB = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
console.error(chalk.red(" Insufficient ETH on Arbitrum for gas."));
|
|
251
|
-
}
|
|
252
|
-
process.exit(1);
|
|
253
|
-
}
|
|
254
|
-
const usdc = new ethers.Contract(USDC_ARB, [
|
|
255
|
-
"function transfer(address to, uint256 amount) returns (bool)",
|
|
256
|
-
"function balanceOf(address) view returns (uint256)",
|
|
257
|
-
], wallet);
|
|
258
|
-
const amountRaw = ethers.parseUnits(amount, 6);
|
|
259
|
-
const usdcBal = await usdc.balanceOf(wallet.address);
|
|
456
|
+
const ARB_RPC = "https://arb1.arbitrum.io/rpc";
|
|
457
|
+
const { parseUnits, formatUnits } = await import("ethers");
|
|
458
|
+
const rpc = evm.evmProvider(ARB_RPC, 42161);
|
|
459
|
+
await evm.assertRpcChainId(rpc, ARB_RPC, 42161, "hyperliquid");
|
|
460
|
+
const amountRaw = parseUnits(amount, await evm.erc20Decimals(rpc, USDC_ARB));
|
|
461
|
+
const usdcBal = await evm.erc20Balance(rpc, USDC_ARB, depositor);
|
|
260
462
|
if (usdcBal < amountRaw) {
|
|
261
|
-
|
|
262
|
-
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
console.error(chalk.red(` Insufficient USDC. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
266
|
-
}
|
|
267
|
-
process.exit(1);
|
|
463
|
+
throw new PerpError("INSUFFICIENT_BALANCE", `Insufficient USDC on Arbitrum. Have: $${formatUsd(Number(formatUnits(usdcBal, 6)))}`, { exchange: "hyperliquid" });
|
|
268
464
|
}
|
|
269
465
|
try {
|
|
270
466
|
if (!isJson())
|
|
271
467
|
console.log(chalk.gray(" Sending USDC to Hyperliquid Bridge2..."));
|
|
272
|
-
|
|
273
|
-
|
|
468
|
+
// Simulated, signed by the OWS master, broadcast; a lost response / receipt timeout after
|
|
469
|
+
// the broadcast is ORDER_PENDING carrying the tx hash (never a retryable error).
|
|
470
|
+
const { txHash } = await evm.sendMasterTx(rpc, {
|
|
471
|
+
venue: "hyperliquid", chainLabel: "Arbitrum", chainId: 42161, from: depositor, to: USDC_ARB, value: 0n,
|
|
472
|
+
data: evm.ERC20.encodeFunctionData("transfer", [HL_BRIDGE, amountRaw]), label: "USDC transfer to Hyperliquid Bridge2",
|
|
473
|
+
}, tx => signer.signTransaction(tx));
|
|
274
474
|
logExecution({
|
|
275
475
|
type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "deposit",
|
|
276
476
|
size: String(amountNum), status: "success", dryRun: false,
|
|
277
|
-
meta: { action: "deposit", method: "direct", txHash
|
|
477
|
+
meta: { action: "deposit", method: "direct", txHash },
|
|
278
478
|
});
|
|
279
479
|
if (isJson())
|
|
280
|
-
return printJson(jsonOk({ txHash
|
|
480
|
+
return printJson(jsonOk({ txHash, amount: amountNum, method: "direct" }));
|
|
281
481
|
console.log(chalk.green(` Deposit confirmed!`));
|
|
282
482
|
console.log(` Amount: $${formatUsd(amountNum)} USDC`);
|
|
283
|
-
console.log(` TX Hash: ${
|
|
483
|
+
console.log(` TX Hash: ${txHash}`);
|
|
284
484
|
console.log(chalk.gray(`\n Funds appear in ~1-3 minutes.\n`));
|
|
285
485
|
}
|
|
286
486
|
catch (err) {
|
|
287
487
|
logExecution({
|
|
288
488
|
type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "deposit",
|
|
289
|
-
size: String(amountNum), status:
|
|
489
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
290
490
|
error: err instanceof Error ? err.message : String(err),
|
|
291
|
-
meta: { action: "deposit", method: "direct" },
|
|
491
|
+
meta: { action: "deposit", method: "direct", ...pendingTxMeta(err) },
|
|
292
492
|
});
|
|
293
493
|
throw err;
|
|
294
494
|
}
|
|
295
495
|
});
|
|
296
496
|
// ── Lighter ──
|
|
297
497
|
const lighterDeposit = deposit.command("lighter").description("Deposit USDC into Lighter");
|
|
298
|
-
lighterDeposit
|
|
299
|
-
.
|
|
300
|
-
|
|
301
|
-
|
|
498
|
+
supportsDryRun(lighterDeposit.command("ethereum <amount>"))
|
|
499
|
+
.description("Deposit USDC via Ethereum L1 (min 1 USDC, gas: $3-10+; supports --dry-run)")
|
|
500
|
+
// Asset index comes from GET /api/v1/assetDetails, which the deposit docs
|
|
501
|
+
// name as the source for the contract's `_assetIndex`. Live registry:
|
|
502
|
+
// 3 = USDC (l1_decimals 6), 2 = LIT (l1_decimals 18), 1 = ETH. The old
|
|
503
|
+
// "2 = USDC" default contradicted this function's own USDC ERC20 transfer
|
|
504
|
+
// and its parseUnits(amount, 6). Same defect as the withdraw side, which
|
|
505
|
+
// was corrected to 3 in 966ffc0; the deposit side was missed then.
|
|
506
|
+
.option("--asset-id <id>", "Asset index (default: 3 = USDC, per /api/v1/assetDetails)", "3")
|
|
302
507
|
.option("--route <type>", "Route: 0=perps, 1=spot (default: 0)", "0")
|
|
303
508
|
.action(async (amount, opts) => {
|
|
304
|
-
const amountNum =
|
|
509
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
305
510
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
306
511
|
throw new Error("Invalid amount");
|
|
307
512
|
if (amountNum < 1)
|
|
308
513
|
throw new Error("Minimum deposit via Ethereum is 1 USDC");
|
|
514
|
+
// deposit(address _to, uint16 _assetIndex, uint8 _routeType, uint256 _amount) — Lighter docs
|
|
515
|
+
// (deposits-transfers-and-withdrawals): "`_routeType`: whether you want to deposit the asset
|
|
516
|
+
// to your perps (0), or spot account (1)". Strict: plain integers only ("3abc", "1e1", "" refused).
|
|
517
|
+
if (!/^\d+$/.test(opts.assetId) || Number(opts.assetId) > 65_535) {
|
|
518
|
+
throw new PerpError("INVALID_PARAMS", `--asset-id must be an integer asset index 0-65535 (got "${opts.assetId}")`, {
|
|
519
|
+
exchange: "lighter", remediation: "Use the asset index from GET /api/v1/assetDetails (3 = USDC)",
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
if (opts.route !== "0" && opts.route !== "1") {
|
|
523
|
+
throw new PerpError("INVALID_PARAMS", `--route must be 0 (perps) or 1 (spot) (got "${opts.route}")`, { exchange: "lighter", remediation: "Pass --route 0 or --route 1" });
|
|
524
|
+
}
|
|
525
|
+
const assetIndex = Number(opts.assetId);
|
|
526
|
+
const routeType = Number(opts.route);
|
|
527
|
+
requireMainnetDepositRoute("Lighter deposit via Ethereum L1");
|
|
528
|
+
if (isDryRun()) {
|
|
529
|
+
return printFundsPlan({ action: "deposit", venue: "lighter", network: getNetwork(), asset: "USDC", amount: String(amountNum), chain: "ethereum", contract: "0x3B4D794a66304f130a4db8f2551b0070dfcf5ca7", assetIndex: opts.assetId, routeType: opts.route, steps: "approve USDC (if allowance short) → deposit()" });
|
|
530
|
+
}
|
|
531
|
+
await requireMaster("lighter", "Lighter deposit");
|
|
309
532
|
if (!isJson()) {
|
|
310
533
|
console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} USDC into Lighter via Ethereum L1...\n`));
|
|
311
534
|
console.log(chalk.yellow(" Warning: Ethereum L1 gas fees may be high ($3-10+)\n"));
|
|
312
535
|
}
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
const pk = await loadPrivateKey("lighter");
|
|
316
|
-
const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
|
|
317
|
-
const wallet = new ethers.Wallet(pk, provider);
|
|
318
|
-
const LIGHTER_CONTRACT = "0x3B4D794a66304f130a4db8f2551b0070dfcf5ca7";
|
|
536
|
+
// EIP-55 checksummed (the previous mixed-case literal failed ethers' checksum validation).
|
|
537
|
+
const LIGHTER_CONTRACT = "0x3B4D794a66304F130a4Db8F2551B0070dfCf5ca7";
|
|
319
538
|
const USDC_ETH = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
if (isJson()) {
|
|
332
|
-
printJson(jsonError("INSUFFICIENT_BALANCE", "Insufficient ETH on Ethereum for gas. Need ~0.003 ETH ($3+)."));
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
console.error(chalk.red(" Insufficient ETH on Ethereum for gas. Need ~0.003 ETH ($3+)."));
|
|
336
|
-
}
|
|
337
|
-
process.exit(1);
|
|
338
|
-
}
|
|
339
|
-
const usdcBal = await usdc.balanceOf(wallet.address);
|
|
539
|
+
const ETH_RPC = "https://eth.llamarpc.com";
|
|
540
|
+
const { Interface, parseUnits, formatUnits } = await import("ethers");
|
|
541
|
+
const evm = await import("../evm-master-tx.js");
|
|
542
|
+
// OWS master wallet signs both L1 transactions (no raw key).
|
|
543
|
+
const signer = await evm.loadMasterEvmSigner("lighter", cliCredentialOptsFrom(program.opts()), "Lighter deposit");
|
|
544
|
+
const depositor = signer.getAddress();
|
|
545
|
+
const zkLighter = new Interface(["function deposit(address _to, uint16 _assetIndex, uint8 _routeType, uint256 _amount) payable"]);
|
|
546
|
+
const rpc = evm.evmProvider(ETH_RPC, 1);
|
|
547
|
+
await evm.assertRpcChainId(rpc, ETH_RPC, 1, "lighter");
|
|
548
|
+
const amountRaw = parseUnits(amount, await evm.erc20Decimals(rpc, USDC_ETH));
|
|
549
|
+
const usdcBal = await evm.erc20Balance(rpc, USDC_ETH, depositor);
|
|
340
550
|
if (usdcBal < amountRaw) {
|
|
341
|
-
|
|
342
|
-
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC on Ethereum. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
343
|
-
}
|
|
344
|
-
else {
|
|
345
|
-
console.error(chalk.red(` Insufficient USDC on Ethereum. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
346
|
-
}
|
|
347
|
-
process.exit(1);
|
|
551
|
+
throw new PerpError("INSUFFICIENT_BALANCE", `Insufficient USDC on Ethereum. Have: $${formatUsd(Number(formatUnits(usdcBal, 6)))}`, { exchange: "lighter" });
|
|
348
552
|
}
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
if (!isJson())
|
|
352
|
-
console.log(chalk.gray(" Approving USDC for Lighter..."));
|
|
353
|
-
const approveTx = await usdc.approve(LIGHTER_CONTRACT, amountRaw);
|
|
354
|
-
await approveTx.wait();
|
|
355
|
-
if (!isJson())
|
|
356
|
-
console.log(chalk.gray(" Approved."));
|
|
357
|
-
}
|
|
358
|
-
if (!isJson())
|
|
359
|
-
console.log(chalk.gray(" Calling deposit()..."));
|
|
553
|
+
const base = { venue: "lighter", chainLabel: "Ethereum", chainId: 1, from: depositor, value: 0n };
|
|
554
|
+
const sign = (tx) => signer.signTransaction(tx);
|
|
360
555
|
try {
|
|
361
|
-
const
|
|
362
|
-
|
|
556
|
+
const allowance = await evm.erc20Allowance(rpc, USDC_ETH, depositor, LIGHTER_CONTRACT);
|
|
557
|
+
if (allowance < amountRaw) {
|
|
558
|
+
if (!isJson())
|
|
559
|
+
console.log(chalk.gray(" Approving USDC for Lighter..."));
|
|
560
|
+
await evm.sendMasterTx(rpc, { ...base, to: USDC_ETH, data: evm.ERC20.encodeFunctionData("approve", [LIGHTER_CONTRACT, amountRaw]), label: "USDC approve" }, sign);
|
|
561
|
+
if (!isJson())
|
|
562
|
+
console.log(chalk.gray(" Approved."));
|
|
563
|
+
}
|
|
564
|
+
if (!isJson())
|
|
565
|
+
console.log(chalk.gray(" Calling deposit()..."));
|
|
566
|
+
const { txHash } = await evm.sendMasterTx(rpc, {
|
|
567
|
+
...base, to: LIGHTER_CONTRACT, label: "Lighter L1 deposit",
|
|
568
|
+
data: zkLighter.encodeFunctionData("deposit", [depositor, assetIndex, routeType, amountRaw]),
|
|
569
|
+
}, sign);
|
|
363
570
|
logExecution({
|
|
364
571
|
type: "bridge", exchange: "lighter", symbol: "USDC", side: "deposit",
|
|
365
572
|
size: String(amountNum), status: "success", dryRun: false,
|
|
366
|
-
meta: { action: "deposit", method: "ethereum", txHash
|
|
573
|
+
meta: { action: "deposit", method: "ethereum", txHash },
|
|
367
574
|
});
|
|
368
575
|
if (isJson())
|
|
369
|
-
return printJson(jsonOk({ txHash
|
|
576
|
+
return printJson(jsonOk({ txHash, amount: amountNum, chain: "ethereum", method: "direct" }));
|
|
370
577
|
console.log(chalk.green(`\n Deposit confirmed!`));
|
|
371
578
|
console.log(` Amount: $${formatUsd(amountNum)} USDC`);
|
|
372
|
-
console.log(` TX Hash: ${
|
|
579
|
+
console.log(` TX Hash: ${txHash}`);
|
|
373
580
|
console.log(chalk.gray(`\n Funds appear in your Lighter perps account shortly.\n`));
|
|
374
581
|
}
|
|
375
582
|
catch (err) {
|
|
376
583
|
logExecution({
|
|
377
584
|
type: "bridge", exchange: "lighter", symbol: "USDC", side: "deposit",
|
|
378
|
-
size: String(amountNum), status:
|
|
585
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
379
586
|
error: err instanceof Error ? err.message : String(err),
|
|
380
|
-
meta: { action: "deposit", method: "ethereum" },
|
|
587
|
+
meta: { action: "deposit", method: "ethereum", ...pendingTxMeta(err) },
|
|
381
588
|
});
|
|
382
589
|
throw err;
|
|
383
590
|
}
|
|
@@ -387,11 +594,10 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
387
594
|
base: { chainId: 8453, rpc: "https://mainnet.base.org", usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", name: "Base" },
|
|
388
595
|
avalanche: { chainId: 43114, rpc: "https://api.avax.network/ext/bc/C/rpc", usdc: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", name: "Avalanche" },
|
|
389
596
|
};
|
|
390
|
-
lighterDeposit
|
|
391
|
-
.
|
|
392
|
-
.description("Deposit USDC via CCTP (arbitrum, base, avalanche — min 5 USDC)")
|
|
597
|
+
supportsDryRun(lighterDeposit.command("cctp <chain> <amount>"))
|
|
598
|
+
.description("Deposit USDC via CCTP (arbitrum, base, avalanche — min 5 USDC; supports --dry-run)")
|
|
393
599
|
.action(async (chain, amount) => {
|
|
394
|
-
const amountNum =
|
|
600
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
395
601
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
396
602
|
throw new Error("Invalid amount");
|
|
397
603
|
if (amountNum < 5)
|
|
@@ -400,84 +606,88 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
400
606
|
const chainInfo = CCTP_CHAINS[chainKey];
|
|
401
607
|
if (!chainInfo)
|
|
402
608
|
throw new Error(`Unsupported chain: ${chain}. Use: arbitrum, base, avalanche`);
|
|
609
|
+
// The intent address comes from the -n adapter, but the USDC transfer is on a mainnet chain.
|
|
610
|
+
requireMainnetDepositRoute(`Lighter CCTP deposit (${chainInfo.name})`);
|
|
611
|
+
if (isDryRun()) {
|
|
612
|
+
return printFundsPlan({ action: "deposit", venue: "lighter", network: getNetwork(), asset: "USDC", amount: String(amountNum), chain: chainKey, method: "CCTP: create intent address → USDC transfer to it" });
|
|
613
|
+
}
|
|
614
|
+
await requireMaster("lighter", "Lighter CCTP deposit");
|
|
403
615
|
if (!isJson())
|
|
404
616
|
console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} USDC into Lighter via CCTP (${chainInfo.name})...\n`));
|
|
405
|
-
const {
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
const
|
|
617
|
+
const { parseUnits, formatUnits } = await import("ethers");
|
|
618
|
+
const evm = await import("../evm-master-tx.js");
|
|
619
|
+
// OWS master wallet signs the USDC transfer (no raw key).
|
|
620
|
+
const signer = await evm.loadMasterEvmSigner("lighter", cliCredentialOptsFrom(program.opts()), "Lighter CCTP deposit");
|
|
621
|
+
const depositor = signer.getAddress();
|
|
410
622
|
if (!isJson())
|
|
411
623
|
console.log(chalk.gray(" Creating intent address..."));
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
})
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const gasBal = await provider.getBalance(wallet.address);
|
|
433
|
-
if (gasBal < ethers.parseEther("0.00005")) {
|
|
434
|
-
if (isJson()) {
|
|
435
|
-
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient gas token on ${chainInfo.name}. Need native token for gas (~$0.01).`));
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
console.error(chalk.red(` Insufficient gas token on ${chainInfo.name}. Need native token for gas (~$0.01).`));
|
|
624
|
+
// Network-aware: the adapter was built for the selected -n network (venue REST URL).
|
|
625
|
+
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
|
626
|
+
const lt = await getAdapterForExchange("lighter");
|
|
627
|
+
if (!(lt instanceof LighterAdapter))
|
|
628
|
+
throw new Error("lighter venue did not produce a LighterAdapter");
|
|
629
|
+
if (lt.address.toLowerCase() !== depositor.toLowerCase()) {
|
|
630
|
+
throw new PerpError("INVALID_PARAMS", `Lighter adapter address ${lt.address || "(none)"} differs from the depositing wallet ${depositor}`, {
|
|
631
|
+
remediation: "Run the deposit with the master wallet that owns the Lighter account (--ows <wallet>)",
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
// The USDC goes to this address: only the documented `intent_address` field is accepted
|
|
635
|
+
// (apidocs.lighter.xyz/reference/createintentaddress → CreateIntentAddressResp, required:
|
|
636
|
+
// "code", "intent_address"). The endpoint is unauthenticated and the docs offer no
|
|
637
|
+
// authenticated read of an account's intent address to cross-check against, so the address
|
|
638
|
+
// is requested twice and both answers must name the same address.
|
|
639
|
+
const readIntentAddress = async () => {
|
|
640
|
+
const intentData = (await lt.createIntentAddress(chainInfo.chainId));
|
|
641
|
+
const addr = intentData?.intent_address;
|
|
642
|
+
if (typeof addr !== "string" || !evm.EVM_ADDRESS_RE.test(addr) || /^0x0{40}$/.test(addr)) {
|
|
643
|
+
throw new PerpError("EXCHANGE_ERROR", `Lighter returned no intent_address for chain ${chainInfo.chainId} (got ${JSON.stringify(intentData).slice(0, 160)}) — nothing was sent`, { exchange: "lighter" });
|
|
439
644
|
}
|
|
440
|
-
|
|
645
|
+
return addr;
|
|
646
|
+
};
|
|
647
|
+
const intentAddress = await readIntentAddress();
|
|
648
|
+
const intentAddressAgain = await readIntentAddress();
|
|
649
|
+
if (intentAddress.toLowerCase() !== intentAddressAgain.toLowerCase()) {
|
|
650
|
+
throw new PerpError("EXCHANGE_ERROR", `Lighter returned two different intent addresses for chain ${chainInfo.chainId} (${intentAddress} then ${intentAddressAgain}) — nothing was sent`, {
|
|
651
|
+
exchange: "lighter",
|
|
652
|
+
remediation: "Do not send funds to either address. Retry later, or deposit from the Lighter web app",
|
|
653
|
+
});
|
|
441
654
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const amountRaw =
|
|
447
|
-
const usdcBal = await
|
|
655
|
+
if (!isJson())
|
|
656
|
+
console.log(` Intent address: ${intentAddress}`);
|
|
657
|
+
const rpc = evm.evmProvider(chainInfo.rpc, chainInfo.chainId);
|
|
658
|
+
await evm.assertRpcChainId(rpc, chainInfo.rpc, chainInfo.chainId, "lighter");
|
|
659
|
+
const amountRaw = parseUnits(amount, await evm.erc20Decimals(rpc, chainInfo.usdc));
|
|
660
|
+
const usdcBal = await evm.erc20Balance(rpc, chainInfo.usdc, depositor);
|
|
448
661
|
if (usdcBal < amountRaw) {
|
|
449
|
-
|
|
450
|
-
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC on ${chainInfo.name}. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
console.error(chalk.red(` Insufficient USDC on ${chainInfo.name}. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
454
|
-
}
|
|
455
|
-
process.exit(1);
|
|
662
|
+
throw new PerpError("INSUFFICIENT_BALANCE", `Insufficient USDC on ${chainInfo.name}. Have: $${formatUsd(Number(formatUnits(usdcBal, 6)))}`, { exchange: "lighter" });
|
|
456
663
|
}
|
|
457
664
|
if (!isJson())
|
|
458
665
|
console.log(chalk.gray(` Sending USDC to intent address on ${chainInfo.name}...`));
|
|
459
666
|
try {
|
|
460
|
-
const
|
|
461
|
-
|
|
667
|
+
const { txHash } = await evm.sendMasterTx(rpc, {
|
|
668
|
+
venue: "lighter", chainLabel: chainInfo.name, chainId: chainInfo.chainId, from: depositor, to: chainInfo.usdc, value: 0n,
|
|
669
|
+
data: evm.ERC20.encodeFunctionData("transfer", [intentAddress, amountRaw]), label: "USDC transfer to the Lighter CCTP intent address",
|
|
670
|
+
}, tx => signer.signTransaction(tx));
|
|
462
671
|
logExecution({
|
|
463
672
|
type: "bridge", exchange: "lighter", symbol: "USDC", side: "deposit",
|
|
464
673
|
size: String(amountNum), status: "success", dryRun: false,
|
|
465
|
-
meta: { action: "deposit", method: "cctp", chain: chainKey, txHash
|
|
674
|
+
meta: { action: "deposit", method: "cctp", chain: chainKey, txHash },
|
|
466
675
|
});
|
|
467
676
|
if (isJson())
|
|
468
|
-
return printJson(jsonOk({ txHash
|
|
677
|
+
return printJson(jsonOk({ txHash, amount: amountNum, chain: chainKey, intentAddress, method: "cctp" }));
|
|
469
678
|
console.log(chalk.green(`\n USDC sent to intent address!`));
|
|
470
679
|
console.log(` Amount: $${formatUsd(amountNum)} USDC`);
|
|
471
|
-
console.log(` TX Hash: ${
|
|
680
|
+
console.log(` TX Hash: ${txHash}`);
|
|
472
681
|
console.log(` Chain: ${chainInfo.name}`);
|
|
473
|
-
|
|
682
|
+
// apidocs.lighter.xyz/docs/create-accounts-programmatically: "Legacy CCTP route: around 15–20 minutes."
|
|
683
|
+
console.log(chalk.gray(`\n CCTP deposits take around 15-20 minutes to credit. Funds will appear in Lighter automatically.\n`));
|
|
474
684
|
}
|
|
475
685
|
catch (err) {
|
|
476
686
|
logExecution({
|
|
477
687
|
type: "bridge", exchange: "lighter", symbol: "USDC", side: "deposit",
|
|
478
|
-
size: String(amountNum), status:
|
|
688
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
479
689
|
error: err instanceof Error ? err.message : String(err),
|
|
480
|
-
meta: { action: "deposit", method: "cctp", chain: chainKey },
|
|
690
|
+
meta: { action: "deposit", method: "cctp", chain: chainKey, ...pendingTxMeta(err) },
|
|
481
691
|
});
|
|
482
692
|
throw err;
|
|
483
693
|
}
|
|
@@ -512,23 +722,157 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
512
722
|
console.log(` Command: ${chalk.green("perp funds deposit lighter cctp avalanche <amount>")}`);
|
|
513
723
|
console.log();
|
|
514
724
|
});
|
|
725
|
+
// ── Lighter RH (Robinhood Chain contract deposit) ──
|
|
726
|
+
{
|
|
727
|
+
const rhDef = getVenue("lighter-rh");
|
|
728
|
+
const rhAsset = rhDef.networks.mainnet.collateral[0];
|
|
729
|
+
const rhChain = chainLabel(rhDef.networks.mainnet.settlementChain);
|
|
730
|
+
supportsDryRun(deposit.command("lighter-rh <amount>"))
|
|
731
|
+
.description(`Deposit ${rhAsset} into ${rhDef.displayName} via the ${rhChain} contract (approve + deposit; min 1 ${rhAsset}; supports --dry-run)`)
|
|
732
|
+
.option("--route <type>", "Route: 0=perps, 1=spot (default: 0)", "0")
|
|
733
|
+
.action(async (amount, opts) => {
|
|
734
|
+
if (opts.route !== "0" && opts.route !== "1") {
|
|
735
|
+
throw new PerpError("INVALID_PARAMS", `--route must be 0 (perps) or 1 (spot), got "${opts.route}"`);
|
|
736
|
+
}
|
|
737
|
+
const net = getNetwork();
|
|
738
|
+
const { planLighterRhDeposit, executeLighterRhDeposit, requireLighterRhDepositNetwork } = await import("../exchanges/lighter-rh-deposit.js");
|
|
739
|
+
// Uniform CAPABILITY_UNSUPPORTED on testnet, before any credential is needed.
|
|
740
|
+
requireLighterRhDepositNetwork(net);
|
|
741
|
+
// The deposit is an L1 contract call from the master wallet; the plan needs its address too.
|
|
742
|
+
if (!isDryRun())
|
|
743
|
+
await requireMaster("lighter-rh", `${rhDef.displayName} deposit`);
|
|
744
|
+
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
|
745
|
+
const lt = await getAdapterForExchange("lighter-rh");
|
|
746
|
+
if (!(lt instanceof LighterAdapter))
|
|
747
|
+
throw new Error("lighter-rh venue did not produce a LighterAdapter");
|
|
748
|
+
const plan = await planLighterRhDeposit({ network: net, amount, depositor: lt.address, routeType: opts.route === "1" ? 1 : 0 });
|
|
749
|
+
const view = {
|
|
750
|
+
venue: rhDef.id, network: net, asset: plan.asset, amount: plan.amount, depositor: plan.depositor,
|
|
751
|
+
contract: plan.contract, token: plan.token, chainId: plan.chainId, routeType: plan.routeType,
|
|
752
|
+
needsApprove: plan.needsApprove,
|
|
753
|
+
};
|
|
754
|
+
if (isDryRun()) {
|
|
755
|
+
logExecution({
|
|
756
|
+
type: "bridge", exchange: rhDef.id, symbol: plan.asset, side: "deposit",
|
|
757
|
+
size: plan.amount, status: "simulated", dryRun: true,
|
|
758
|
+
meta: { action: "deposit", method: "contract", contract: plan.contract },
|
|
759
|
+
});
|
|
760
|
+
if (isJson())
|
|
761
|
+
return printJson(jsonOk({ dryRun: true, sent: false, ...view }));
|
|
762
|
+
console.log(chalk.yellow(`\n [dry-run] Would deposit ${plan.amount} ${plan.asset} into ${rhDef.displayName}`));
|
|
763
|
+
console.log(` From: ${plan.depositor} (${rhChain}, chainId ${plan.chainId})`);
|
|
764
|
+
console.log(` Contract: ${plan.contract}`);
|
|
765
|
+
console.log(` Steps: ${plan.needsApprove ? `approve ${plan.asset} → ` : ""}deposit(route ${plan.routeType})`);
|
|
766
|
+
console.log(chalk.gray(" Nothing was sent.\n"));
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (!isJson()) {
|
|
770
|
+
console.log(chalk.cyan(`\n Depositing ${plan.amount} ${plan.asset} into ${rhDef.displayName} on ${rhChain}...\n`));
|
|
771
|
+
if (plan.needsApprove)
|
|
772
|
+
console.log(chalk.gray(` Approving ${plan.asset} for ${plan.contract}...`));
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
const res = await executeLighterRhDeposit(plan, tx => lt.signL1Transaction(tx));
|
|
776
|
+
logExecution({
|
|
777
|
+
type: "bridge", exchange: rhDef.id, symbol: plan.asset, side: "deposit",
|
|
778
|
+
size: plan.amount, status: "success", dryRun: false,
|
|
779
|
+
meta: { action: "deposit", method: "contract", txHash: res.depositTxHash, approveTxHash: res.approveTxHash },
|
|
780
|
+
});
|
|
781
|
+
if (isJson())
|
|
782
|
+
return printJson(jsonOk({ ...view, ...res, method: "contract" }));
|
|
783
|
+
console.log(chalk.green(`\n Deposit confirmed!`));
|
|
784
|
+
console.log(` Amount: ${plan.amount} ${plan.asset}`);
|
|
785
|
+
if (res.approveTxHash !== null)
|
|
786
|
+
console.log(` Approve: ${res.approveTxHash}`);
|
|
787
|
+
console.log(` TX Hash: ${res.depositTxHash}`);
|
|
788
|
+
console.log(chalk.gray(`\n Funds appear in your ${rhDef.displayName} account shortly.\n`));
|
|
789
|
+
}
|
|
790
|
+
catch (err) {
|
|
791
|
+
logExecution({
|
|
792
|
+
type: "bridge", exchange: rhDef.id, symbol: plan.asset, side: "deposit",
|
|
793
|
+
size: plan.amount, status: fundsFailStatus(err), dryRun: false,
|
|
794
|
+
error: err instanceof Error ? err.message : String(err),
|
|
795
|
+
// An unknown-outcome (ORDER_PENDING) failure carries the broadcast tx hash: keep it in the log.
|
|
796
|
+
meta: { action: "deposit", method: "contract", ...pendingTxMeta(err) },
|
|
797
|
+
});
|
|
798
|
+
throw err;
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
// ═══════════════════════════════════════════════════════
|
|
803
|
+
// FAUCET (testnet tooling)
|
|
804
|
+
// ═══════════════════════════════════════════════════════
|
|
805
|
+
const faucet = funds.command("faucet").description("[testnet tooling] Mint venue test collateral (refused on mainnet)");
|
|
806
|
+
supportsDryRun(faucet.command("pacifica <amount>"))
|
|
807
|
+
.description("[testnet tooling] Mint test USDP on Pacifica testnet (Solana devnet mint_test_usdc; master wallet; simulated before send; supports --dry-run)")
|
|
808
|
+
.action(async (amount) => {
|
|
809
|
+
const network = getNetwork();
|
|
810
|
+
if (network !== "testnet") {
|
|
811
|
+
throw unsupported("Pacifica", `the test-USDP faucet on ${network}`, "mint_test_usdc exists only on the devnet program", "Run on testnet: perp -n testnet funds faucet pacifica <amount>");
|
|
812
|
+
}
|
|
813
|
+
const { testUsdcBaseUnits, mintTestUsdc, PACIFICA_DEVNET_RPC } = await import("../pacifica/faucet.js");
|
|
814
|
+
const { getNetworkConfig } = await import("../pacifica/constants.js");
|
|
815
|
+
const baseUnits = testUsdcBaseUnits(amount);
|
|
816
|
+
const cfg = getNetworkConfig("testnet");
|
|
817
|
+
if (isDryRun()) {
|
|
818
|
+
return printFundsPlan({
|
|
819
|
+
action: "deposit", venue: "pacifica", network, asset: "USDP (test)", amount,
|
|
820
|
+
method: "faucet mint_test_usdc (simulated, then sent)", baseUnits: baseUnits.toString(),
|
|
821
|
+
programId: cfg.programId, mint: cfg.usdcMint, rpc: PACIFICA_DEVNET_RPC,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
await requireMaster("pacifica", "Pacifica testnet faucet");
|
|
825
|
+
// The subcommand names the venue: fetch it directly, independent of -e.
|
|
826
|
+
const adapter = await getAdapterForExchange("pacifica");
|
|
827
|
+
if (adapter.name !== "pacifica")
|
|
828
|
+
throw new Error("pacifica venue did not produce a Pacifica adapter");
|
|
829
|
+
const { PacificaAdapter } = await import("../exchanges/pacifica.js");
|
|
830
|
+
const pac = adapter;
|
|
831
|
+
const { Connection } = await import("@solana/web3.js");
|
|
832
|
+
try {
|
|
833
|
+
const { signature, plan } = await mintTestUsdc(new Connection(PACIFICA_DEVNET_RPC, "confirmed"), pac.masterSolanaSigner(), amount);
|
|
834
|
+
logExecution({
|
|
835
|
+
type: "bridge", exchange: "pacifica", symbol: "USDP", side: "deposit",
|
|
836
|
+
size: amount, status: "success", dryRun: false,
|
|
837
|
+
meta: { action: "faucet", network, signature, userAta: plan.userAta },
|
|
838
|
+
});
|
|
839
|
+
if (isJson())
|
|
840
|
+
return printJson(jsonOk({ signature, amount, network, method: "faucet", wallet: plan.user, userAta: plan.userAta }));
|
|
841
|
+
console.log(chalk.green(`\n Minted ${amount} test USDP on Pacifica testnet`));
|
|
842
|
+
console.log(` Wallet: ${plan.user}`);
|
|
843
|
+
console.log(` Signature: ${signature}\n`);
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
logExecution({
|
|
847
|
+
type: "bridge", exchange: "pacifica", symbol: "USDP", side: "deposit",
|
|
848
|
+
size: amount, status: fundsFailStatus(err), dryRun: false,
|
|
849
|
+
error: err instanceof Error ? err.message : String(err),
|
|
850
|
+
meta: { action: "faucet", network },
|
|
851
|
+
});
|
|
852
|
+
throw err;
|
|
853
|
+
}
|
|
854
|
+
});
|
|
515
855
|
// ═══════════════════════════════════════════════════════
|
|
516
856
|
// WITHDRAW
|
|
517
857
|
// ═══════════════════════════════════════════════════════
|
|
518
858
|
const withdraw = funds.command("withdraw").description("Withdraw funds from exchange accounts");
|
|
519
859
|
// ── Pacifica ──
|
|
520
|
-
withdraw
|
|
521
|
-
.
|
|
522
|
-
.
|
|
523
|
-
|
|
524
|
-
.action(async (amount, opts) => {
|
|
525
|
-
const amountNum = parseFloat(amount);
|
|
860
|
+
supportsDryRun(withdraw.command("pacifica <amount>"))
|
|
861
|
+
.description("Withdraw USDC from Pacifica to your own Solana wallet (Pacifica has no destination field; supports --dry-run)")
|
|
862
|
+
.action(async (amount) => {
|
|
863
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
526
864
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
527
865
|
throw new Error("Invalid amount");
|
|
528
|
-
|
|
866
|
+
if (isDryRun()) {
|
|
867
|
+
return printFundsPlan({ action: "withdraw", venue: "pacifica", network: getNetwork(), asset: "USDC", amount: String(amountNum), destination: "account owner's Solana wallet" });
|
|
868
|
+
}
|
|
869
|
+
await requireMaster("pacifica", "Pacifica withdraw");
|
|
870
|
+
// The subcommand names the venue: fetch it directly, independent of -e.
|
|
871
|
+
const adapter = await getAdapterForExchange("pacifica");
|
|
529
872
|
if (!hasPacificaSdk(adapter) || !isWithdrawCapable(adapter))
|
|
530
|
-
throw new Error("
|
|
531
|
-
|
|
873
|
+
throw new Error("pacifica venue did not produce a withdraw-capable Pacifica adapter");
|
|
874
|
+
// Pacifica withdrawals always go to the account owner's own wallet.
|
|
875
|
+
const dest = adapter.publicKey;
|
|
532
876
|
if (!isJson()) {
|
|
533
877
|
console.log(chalk.cyan(`\n Withdrawing $${formatUsd(amountNum)} USDC from Pacifica...\n`));
|
|
534
878
|
console.log(` Destination: ${dest}`);
|
|
@@ -549,7 +893,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
549
893
|
catch (err) {
|
|
550
894
|
logExecution({
|
|
551
895
|
type: "bridge", exchange: "pacifica", symbol: "USDC", side: "withdraw",
|
|
552
|
-
size: String(amountNum), status:
|
|
896
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
553
897
|
error: err instanceof Error ? err.message : String(err),
|
|
554
898
|
meta: { action: "withdraw", destination: dest },
|
|
555
899
|
});
|
|
@@ -557,21 +901,52 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
557
901
|
}
|
|
558
902
|
});
|
|
559
903
|
// ── Hyperliquid ──
|
|
560
|
-
withdraw
|
|
561
|
-
.
|
|
562
|
-
.
|
|
563
|
-
.option("--to <address>", "Destination EVM address (default: your wallet)")
|
|
904
|
+
supportsDryRun(withdraw.command("hyperliquid <amount>"))
|
|
905
|
+
.description("Withdraw USDC from Hyperliquid over CCTP (native USDC) to your wallet on Arbitrum (default), Ethereum or Base — another address only when allowlisted in settings withdraw.hyperliquid; supports --dry-run")
|
|
906
|
+
.option("--chain <chain>", "Destination chain: arbitrum | ethereum | base", "arbitrum")
|
|
907
|
+
.option("--to <address>", "Destination EVM address (default: your wallet; any other address must be on the settings withdraw.hyperliquid allowlist)")
|
|
564
908
|
.action(async (amount, opts) => {
|
|
565
|
-
const
|
|
909
|
+
const { HL_CCTP_ROUTES } = await import("../exchanges/hyperliquid.js");
|
|
910
|
+
const chain = opts.chain.toLowerCase();
|
|
911
|
+
if (!(chain in HL_CCTP_ROUTES)) {
|
|
912
|
+
throw new PerpError("INVALID_PARAMS", `--chain must be one of ${Object.keys(HL_CCTP_ROUTES).join(", ")} (got "${opts.chain}")`);
|
|
913
|
+
}
|
|
914
|
+
const route = HL_CCTP_ROUTES[chain];
|
|
915
|
+
const feeNote = route.forwardingFeeUsdc !== null
|
|
916
|
+
? `CCTP forwarding fee ${route.forwardingFeeUsdc} USDC (deducted on the way) + a small Hyperliquid send fee`
|
|
917
|
+
: "CCTP forwarding fee for this chain is not published — it is deducted on the way";
|
|
918
|
+
const amountStr = parsePositiveDecimal(amount, "amount");
|
|
919
|
+
const amountNum = Number(amountStr);
|
|
566
920
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
567
921
|
throw new Error("Invalid amount");
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
922
|
+
if (opts.to !== undefined && !/^0x[0-9a-fA-F]{40}$/.test(opts.to)) {
|
|
923
|
+
throw new PerpError("INVALID_PARAMS", `--to must be an EVM address (got "${opts.to}")`);
|
|
924
|
+
}
|
|
925
|
+
if (isDryRun()) {
|
|
926
|
+
// The owner address is not resolved in a dry run (no key is loaded): an explicit --to is
|
|
927
|
+
// judged as a non-owner destination; it needs no gate if it is the owner's own address.
|
|
928
|
+
const withdrawGate = opts.to === undefined
|
|
929
|
+
? "not needed — destination is the account owner's own address"
|
|
930
|
+
: `${hyperliquidGateVerdict(getNetwork(), amountStr, opts.to, Date.now())} (not needed if --to is the account owner's own address; checked again when sent)`;
|
|
931
|
+
return printFundsPlan({ action: "withdraw", venue: "hyperliquid", network: getNetwork(), asset: "USDC", amount: String(amountNum), destination: opts.to !== undefined ? opts.to : "account owner's address", chain, method: "CCTP (sendToEvmWithData)", cctpDomain: route.cctpDomain, fee: feeNote, withdrawGate });
|
|
932
|
+
}
|
|
933
|
+
await requireMaster("hyperliquid", "Hyperliquid withdraw");
|
|
934
|
+
// The subcommand names the venue: fetch it directly, independent of -e (like ondo / arcus).
|
|
935
|
+
const adapter = await getAdapterForExchange("hyperliquid");
|
|
936
|
+
if (!hasEvmAddress(adapter) || !isWithdrawCapable(adapter))
|
|
937
|
+
throw new Error("hyperliquid venue did not produce a withdraw-capable EVM adapter");
|
|
938
|
+
// HyperliquidAdapter.withdraw takes the CCTP destination chain.
|
|
939
|
+
const hlWithdraw = adapter.withdraw.bind(adapter);
|
|
940
|
+
const dest = opts.to === undefined ? adapter.address : opts.to;
|
|
941
|
+
// Owner-only unless the destination passes settings.withdraw.hyperliquid (checked before any read / send).
|
|
942
|
+
const toOwner = dest.toLowerCase() === adapter.address.toLowerCase();
|
|
943
|
+
if (!toOwner) {
|
|
944
|
+
const gate = loadSettings().withdraw.hyperliquid;
|
|
945
|
+
assertWithdrawAllowed("hyperliquid", getNetwork(), amountStr, dest, gate === undefined ? null : gate, Date.now());
|
|
946
|
+
}
|
|
572
947
|
if (!isJson()) {
|
|
573
948
|
console.log(chalk.cyan(`\n Withdrawing $${formatUsd(amountNum)} USDC from Hyperliquid...\n`));
|
|
574
|
-
console.log(` Destination: ${dest}`);
|
|
949
|
+
console.log(` Destination: ${dest}${toOwner ? "" : " (allowlisted in settings withdraw.hyperliquid)"}`);
|
|
575
950
|
}
|
|
576
951
|
const balance = await adapter.getBalance();
|
|
577
952
|
const available = Number(balance.available);
|
|
@@ -584,23 +959,27 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
584
959
|
}
|
|
585
960
|
process.exit(1);
|
|
586
961
|
}
|
|
962
|
+
const attempt = hyperliquidGateEnforce(getNetwork(), amountStr, dest, adapter.address, Date.now());
|
|
587
963
|
try {
|
|
588
|
-
const result = await
|
|
964
|
+
const result = await hlWithdraw(amountStr, dest, { chain: chain });
|
|
589
965
|
logExecution({
|
|
590
966
|
type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "withdraw",
|
|
591
967
|
size: String(amountNum), status: "success", dryRun: false,
|
|
592
|
-
meta: { action: "withdraw", destination: dest },
|
|
968
|
+
meta: { action: "withdraw", destination: dest, chain, method: "cctp" },
|
|
593
969
|
});
|
|
594
970
|
if (isJson())
|
|
595
|
-
return printJson(jsonOk(result));
|
|
596
|
-
console.log(chalk.green(`\n Withdrawal submitted!`));
|
|
597
|
-
console.log(` Amount: $${formatUsd(amountNum)} USDC`);
|
|
971
|
+
return printJson(jsonOk({ result, chain, method: "cctp", fee: feeNote }));
|
|
972
|
+
console.log(chalk.green(`\n Withdrawal submitted (CCTP → ${chain})!`));
|
|
973
|
+
console.log(` Amount: $${formatUsd(amountNum)} USDC — ${feeNote}`);
|
|
598
974
|
console.log(chalk.gray(`\n Processing may take a few minutes.\n`));
|
|
599
975
|
}
|
|
600
976
|
catch (err) {
|
|
977
|
+
// Only a definite refusal proves nothing left the account; an unknown outcome keeps counting.
|
|
978
|
+
if (attempt !== null && isDefiniteOrderFailure(err))
|
|
979
|
+
forgetWithdrawAttempt(attempt);
|
|
601
980
|
logExecution({
|
|
602
981
|
type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "withdraw",
|
|
603
|
-
size: String(amountNum), status:
|
|
982
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
604
983
|
error: err instanceof Error ? err.message : String(err),
|
|
605
984
|
meta: { action: "withdraw", destination: dest },
|
|
606
985
|
});
|
|
@@ -608,62 +987,349 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
608
987
|
}
|
|
609
988
|
});
|
|
610
989
|
// ── Lighter ──
|
|
611
|
-
withdraw
|
|
612
|
-
|
|
613
|
-
.description(
|
|
614
|
-
.option("--asset-id <id>", "Asset
|
|
990
|
+
supportsDryRun(withdraw.command("lighter <amount>"))
|
|
991
|
+
// Serves both Lighter-family venues (-e lighter / -e lighter-rh); text derived from their definitions.
|
|
992
|
+
.description(`Withdraw collateral from a Lighter-family venue to your own L1 wallet (${lighterFamilyWithdrawSummary()}; supports --dry-run)`)
|
|
993
|
+
.option("--asset-id <id>", "Asset index (default: 3 = the venue collateral, per /api/v1/assetDetails)", "3")
|
|
615
994
|
.option("--route <type>", "Route type: 0=perp, 1=spot (default: 0)", "0")
|
|
616
995
|
.action(async (amount, opts) => {
|
|
617
|
-
const amountNum =
|
|
996
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
618
997
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
619
998
|
throw new Error("Invalid amount");
|
|
999
|
+
if (isDryRun()) {
|
|
1000
|
+
const venue = getVenue(program.opts().exchange);
|
|
1001
|
+
if (venue.family !== "lighter")
|
|
1002
|
+
throw new Error("Requires --exchange lighter or lighter-rh");
|
|
1003
|
+
return printFundsPlan({ action: "withdraw", venue: venue.id, network: getNetwork(), asset: withdrawAssetOf(venue), amount: String(amountNum), destination: "account owner's L1 wallet", chain: withdrawChainOf(venue, getNetwork()), assetIndex: opts.assetId, routeType: opts.route });
|
|
1004
|
+
}
|
|
1005
|
+
{
|
|
1006
|
+
const venue = getVenue(program.opts().exchange);
|
|
1007
|
+
if (venue.family === "lighter")
|
|
1008
|
+
await requireMaster(venue.id, `${venue.displayName} withdraw`);
|
|
1009
|
+
}
|
|
620
1010
|
const adapter = await getAdapter();
|
|
621
1011
|
if (!hasLighterAccount(adapter) || !isWithdrawCapable(adapter))
|
|
622
|
-
throw new Error("Requires --exchange lighter");
|
|
1012
|
+
throw new Error("Requires --exchange lighter or lighter-rh");
|
|
1013
|
+
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
|
1014
|
+
if (!(adapter instanceof LighterAdapter))
|
|
1015
|
+
throw new Error("Requires --exchange lighter or lighter-rh");
|
|
1016
|
+
const venueName = getVenue(adapter.venueId).displayName;
|
|
1017
|
+
const asset = adapter.collateralSymbol;
|
|
1018
|
+
const chain = chainLabel(adapter.network.settlementChain);
|
|
623
1019
|
if (!isJson()) {
|
|
624
|
-
console.log(chalk.cyan(`\n Withdrawing
|
|
1020
|
+
console.log(chalk.cyan(`\n Withdrawing ${formatUsd(amountNum)} ${asset} from ${venueName} to ${chain}...\n`));
|
|
625
1021
|
console.log(` Account Index: ${adapter.accountIndex}`);
|
|
626
1022
|
console.log(` Address: ${adapter.address}`);
|
|
627
1023
|
}
|
|
628
1024
|
try {
|
|
629
1025
|
const result = await adapter.withdraw(String(amountNum), "", { assetId: parseInt(opts.assetId), routeType: parseInt(opts.route) });
|
|
630
1026
|
logExecution({
|
|
631
|
-
type: "bridge", exchange:
|
|
1027
|
+
type: "bridge", exchange: adapter.venueId, symbol: asset, side: "withdraw",
|
|
632
1028
|
size: String(amountNum), status: "success", dryRun: false,
|
|
633
1029
|
meta: { action: "withdraw" },
|
|
634
1030
|
});
|
|
635
1031
|
if (isJson())
|
|
636
1032
|
return printJson(jsonOk(result));
|
|
637
1033
|
console.log(chalk.green(`\n Withdrawal submitted!`));
|
|
638
|
-
console.log(` Amount:
|
|
639
|
-
console.log(chalk.gray(`\n Standard withdrawal
|
|
1034
|
+
console.log(` Amount: ${formatUsd(amountNum)} ${asset}`);
|
|
1035
|
+
console.log(chalk.gray(`\n Standard (secure) withdrawal is processed by the ${chain} contract and can take hours. Use the ${venueName} web app for a fast withdrawal.\n`));
|
|
640
1036
|
}
|
|
641
1037
|
catch (err) {
|
|
642
1038
|
logExecution({
|
|
643
|
-
type: "bridge", exchange:
|
|
644
|
-
size: String(amountNum), status:
|
|
1039
|
+
type: "bridge", exchange: adapter.venueId, symbol: asset, side: "withdraw",
|
|
1040
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
645
1041
|
error: err instanceof Error ? err.message : String(err),
|
|
646
1042
|
meta: { action: "withdraw" },
|
|
647
1043
|
});
|
|
648
1044
|
throw err;
|
|
649
1045
|
}
|
|
650
1046
|
});
|
|
1047
|
+
// ── Ondo Perps / Arcus — uniform surface (design §13.1): same <amount> + --to,
|
|
1048
|
+
// same jsonOk(result), global --dry-run plans without sending. ──
|
|
1049
|
+
for (const venueId of ["ondo", "arcus"]) {
|
|
1050
|
+
const def = getVenue(venueId);
|
|
1051
|
+
const asset = withdrawAssetOf(def);
|
|
1052
|
+
const toHelp = venueId === "ondo"
|
|
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)";
|
|
1055
|
+
supportsDryRun(withdraw.command(`${venueId} <amount>`))
|
|
1056
|
+
.description(`Withdraw ${asset} from ${def.displayName} to your ${chainLabel(withdrawChainOf(def, "mainnet"))} wallet (supports --dry-run)`)
|
|
1057
|
+
.option("--to <address>", toHelp)
|
|
1058
|
+
.action(async (amount, opts) => {
|
|
1059
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
1060
|
+
if (!/^\d+(\.\d+)?$/.test(amount) || isNaN(amountNum) || amountNum <= 0) {
|
|
1061
|
+
throw new PerpError("INVALID_PARAMS", `Withdraw amount must be a positive decimal (got "${amount}")`);
|
|
1062
|
+
}
|
|
1063
|
+
const net = getNetwork();
|
|
1064
|
+
const chain = chainLabel(withdrawChainOf(def, net));
|
|
1065
|
+
const adapter = await getAdapterForExchange(venueId);
|
|
1066
|
+
if (!isWithdrawCapable(adapter))
|
|
1067
|
+
throw new Error(`${def.id} adapter has no withdraw()`);
|
|
1068
|
+
let destination;
|
|
1069
|
+
if (venueId === "ondo") {
|
|
1070
|
+
if (opts.to === undefined) {
|
|
1071
|
+
throw new PerpError("INVALID_PARAMS", "Ondo Perps withdraw needs a destination address (the API key has no owner address)", {
|
|
1072
|
+
remediation: `perp funds withdraw ondo ${amount} --to <0x address on your Ondo address book>`,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
destination = opts.to;
|
|
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;
|
|
1088
|
+
}
|
|
1089
|
+
const plan = { venue: def.id, network: net, asset, amount: String(amountNum), destination, chain };
|
|
1090
|
+
if (isDryRun()) {
|
|
1091
|
+
logExecution({
|
|
1092
|
+
type: "bridge", exchange: def.id, symbol: asset, side: "withdraw",
|
|
1093
|
+
size: String(amountNum), status: "simulated", dryRun: true,
|
|
1094
|
+
meta: { action: "withdraw", destination },
|
|
1095
|
+
});
|
|
1096
|
+
if (isJson())
|
|
1097
|
+
return printJson(jsonOk({ dryRun: true, sent: false, ...plan }));
|
|
1098
|
+
console.log(chalk.yellow(`\n [dry-run] Would withdraw ${formatUsd(amountNum)} ${asset} from ${def.displayName} (${net})`));
|
|
1099
|
+
console.log(` Destination: ${destination} (${chain})`);
|
|
1100
|
+
console.log(chalk.gray(" Nothing was sent.\n"));
|
|
1101
|
+
return;
|
|
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
|
+
if (!isJson()) {
|
|
1114
|
+
console.log(chalk.cyan(`\n Withdrawing ${formatUsd(amountNum)} ${asset} from ${def.displayName}...\n`));
|
|
1115
|
+
console.log(` Destination: ${destination} (${chain})`);
|
|
1116
|
+
}
|
|
1117
|
+
try {
|
|
1118
|
+
const result = await adapter.withdraw(String(amountNum), destination);
|
|
1119
|
+
logExecution({
|
|
1120
|
+
type: "bridge", exchange: def.id, symbol: asset, side: "withdraw",
|
|
1121
|
+
size: String(amountNum), status: "success", dryRun: false,
|
|
1122
|
+
meta: { action: "withdraw", destination },
|
|
1123
|
+
});
|
|
1124
|
+
if (isJson())
|
|
1125
|
+
return printJson(jsonOk(result));
|
|
1126
|
+
console.log(chalk.green(`\n Withdrawal submitted!`));
|
|
1127
|
+
console.log(` Amount: ${formatUsd(amountNum)} ${asset}`);
|
|
1128
|
+
console.log(chalk.gray(`\n Track it in the ${def.displayName} app until it arrives on ${chain}.\n`));
|
|
1129
|
+
}
|
|
1130
|
+
catch (err) {
|
|
1131
|
+
logExecution({
|
|
1132
|
+
type: "bridge", exchange: def.id, symbol: asset, side: "withdraw",
|
|
1133
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
1134
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1135
|
+
meta: { action: "withdraw", destination },
|
|
1136
|
+
});
|
|
1137
|
+
throw err;
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
// ── QFEX (design §14.6, §14.12 #8) — custodial; deposit = address display, withdraw = banker POST
|
|
1142
|
+
// gated by settings.withdraw.qfex inside QfexAdapter.withdraw(). ──
|
|
1143
|
+
/** QFEX adapter for the active network — the default imported key, or the one named by --agent. */
|
|
1144
|
+
const qfexAdapterFor = async (agentName) => {
|
|
1145
|
+
const { QfexAdapter: Cls } = await import("../exchanges/qfex/adapter.js");
|
|
1146
|
+
let adapter;
|
|
1147
|
+
if (agentName === undefined) {
|
|
1148
|
+
adapter = await getAdapterForExchange("qfex");
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
const { getVenueAgent } = await import("../agent-wallet/store.js");
|
|
1152
|
+
const meta = getVenueAgent("qfex", agentName);
|
|
1153
|
+
if (meta === null) {
|
|
1154
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `No QFEX API key named "${agentName}"`, { remediation: "List keys: perp wallet agent list qfex" });
|
|
1155
|
+
}
|
|
1156
|
+
const def = getVenue("qfex");
|
|
1157
|
+
if (def.create === null)
|
|
1158
|
+
throw new Error("qfex venue has no adapter factory");
|
|
1159
|
+
adapter = await def.create({
|
|
1160
|
+
network: getNetwork(), credentials: { ...READ_ONLY_CREDENTIALS, agent: meta }, noAgent: false, hlDex: null, readOnly: false, noBuilder: true,
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
if (!(adapter instanceof Cls))
|
|
1164
|
+
throw new Error("qfex venue did not produce a QfexAdapter");
|
|
1165
|
+
return adapter;
|
|
1166
|
+
};
|
|
1167
|
+
supportsDryRun(withdraw.command("qfex <amount>"))
|
|
1168
|
+
.description("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)")
|
|
1169
|
+
.option("--to <address>", "Destination EVM address on Arbitrum One (required; must be on the settings withdraw.qfex allowlist)")
|
|
1170
|
+
.option("--agent <name>", "Imported QFEX key to use (a key with the \"Deposit and withdraw\" permission, bound to the primary account)")
|
|
1171
|
+
.action(async (amount, opts) => {
|
|
1172
|
+
const def = getVenue("qfex");
|
|
1173
|
+
const net = getNetwork();
|
|
1174
|
+
if (!/^\d+(\.\d+)?$/.test(amount) || !(Number(amount) > 0)) {
|
|
1175
|
+
throw new PerpError("INVALID_PARAMS", `Withdraw amount must be a positive decimal (got "${amount}")`);
|
|
1176
|
+
}
|
|
1177
|
+
if (opts.to === undefined) {
|
|
1178
|
+
throw new PerpError("INVALID_PARAMS", "QFEX withdraw needs a destination address (the API key has no owner address)", {
|
|
1179
|
+
remediation: `perp funds withdraw qfex ${amount} --to <0x address on the settings withdraw.qfex allowlist>`,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
const destination = opts.to;
|
|
1183
|
+
const chain = chainLabel(withdrawChainOf(def, net));
|
|
1184
|
+
if (isDryRun()) {
|
|
1185
|
+
// The local gate needs no key and no network: a dry run shows whether it would refuse.
|
|
1186
|
+
const gate = loadSettings().withdraw.qfex;
|
|
1187
|
+
assertWithdrawAllowed("qfex", net, amount, destination, gate === undefined ? null : gate, Date.now());
|
|
1188
|
+
return printFundsPlan({
|
|
1189
|
+
action: "withdraw", venue: "qfex", network: net, asset: "USDC", amount, destination, chain, bankerNetwork: QFEX_BANKER_NETWORK,
|
|
1190
|
+
fee: "taken from the amount by QFEX (not published)", // docs.qfex.com/api-reference/programmatic-funding: "`POST /withdraw` is not idempotent" and 403 =
|
|
1191
|
+
// "API key lacks **Deposit and withdraw** permission, or the account cannot use funding".
|
|
1192
|
+
// 2FA is documented for creating the API key (api-reference/api-keys), not for the withdrawal call.
|
|
1193
|
+
note: "POST /withdraw is not idempotent; QFEX answers 403 when the key lacks the \"Deposit and withdraw\" permission or the account cannot use funding",
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
const adapter = await qfexAdapterFor(opts.agent);
|
|
1197
|
+
if (!isJson()) {
|
|
1198
|
+
console.log(chalk.cyan(`\n Withdrawing ${amount} USDC from QFEX...\n`));
|
|
1199
|
+
console.log(` Destination: ${destination} (${chain})`);
|
|
1200
|
+
}
|
|
1201
|
+
try {
|
|
1202
|
+
const result = await adapter.withdraw(amount, destination);
|
|
1203
|
+
logExecution({ type: "bridge", exchange: "qfex", symbol: "USDC", side: "withdraw", size: amount, status: "success", dryRun: false, meta: { action: "withdraw", destination } });
|
|
1204
|
+
if (isJson())
|
|
1205
|
+
return printJson(jsonOk(result));
|
|
1206
|
+
console.log(chalk.green(`\n Withdrawal accepted by QFEX.`));
|
|
1207
|
+
console.log(` Amount: ${amount} USDC (fee taken from it; not published)`);
|
|
1208
|
+
console.log(chalk.gray(`\n ${result.note}\n`));
|
|
1209
|
+
}
|
|
1210
|
+
catch (err) {
|
|
1211
|
+
logExecution({ type: "bridge", exchange: "qfex", symbol: "USDC", side: "withdraw", size: amount, status: fundsFailStatus(err), dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { action: "withdraw", destination } });
|
|
1212
|
+
throw err;
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
supportsDryRun(deposit.command("qfex"))
|
|
1216
|
+
.description("Show the QFEX deposit address for USDC on Arbitrum One (perp-cli sends nothing — transfer from your own wallet; supports --dry-run)")
|
|
1217
|
+
// --chain, not --network: the root -n/--network (mainnet | testnet) would swallow a subcommand --network.
|
|
1218
|
+
.option("--chain <chain>", "Deposit chain (arbitrum only; TRON USDT is not supported)", "arbitrum")
|
|
1219
|
+
.option("--agent <name>", "Imported QFEX key to use (needs the \"Deposit and withdraw\" permission)")
|
|
1220
|
+
.action(async (opts) => {
|
|
1221
|
+
const requested = opts.chain.toLowerCase();
|
|
1222
|
+
if (requested === "tron") {
|
|
1223
|
+
throw unsupported("qfex", "depositTron", "only USDC on Arbitrum One is supported; how a TRON USDT deposit is credited as USDC margin is not documented", "Deposit USDC on Arbitrum One, or use the QFEX web app for other rails");
|
|
1224
|
+
}
|
|
1225
|
+
if (requested !== QFEX_WITHDRAW_NETWORK) {
|
|
1226
|
+
throw new PerpError("INVALID_PARAMS", `Unsupported QFEX deposit chain "${opts.chain}"`, { remediation: "Use --chain arbitrum" });
|
|
1227
|
+
}
|
|
1228
|
+
const net = getNetwork();
|
|
1229
|
+
if (isDryRun()) {
|
|
1230
|
+
return printFundsPlan({
|
|
1231
|
+
action: "deposit", venue: "qfex", network: net, asset: "USDC", amount: "(you choose)", chain: chainLabel(QFEX_WITHDRAW_NETWORK),
|
|
1232
|
+
method: `GET ${QFEX_BANKER_NETWORK} deposit address from the QFEX banker, then YOU send USDC to it from your wallet`,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
const adapter = await qfexAdapterFor(opts.agent);
|
|
1236
|
+
const addr = await adapter.depositAddress();
|
|
1237
|
+
const info = {
|
|
1238
|
+
venue: "qfex", network: net, address: addr.address, depositNetwork: addr.network, asset: addr.asset,
|
|
1239
|
+
minimum: null, confirmations: null,
|
|
1240
|
+
warning: "Send ONLY USDC on Arbitrum One to this address. Any other asset or network is lost. Minimum amount and confirmations: see the QFEX funding screen. Deposits credit the primary account.",
|
|
1241
|
+
};
|
|
1242
|
+
if (isJson())
|
|
1243
|
+
return printJson(jsonOk(info));
|
|
1244
|
+
console.log(chalk.cyan.bold(`\n QFEX deposit address (${net})\n`));
|
|
1245
|
+
console.log(` Address: ${chalk.green(addr.address)}`);
|
|
1246
|
+
console.log(` Network: ${addr.network} (Arbitrum One) Asset: ${addr.asset}`);
|
|
1247
|
+
console.log(chalk.yellow(`\n ${info.warning}`));
|
|
1248
|
+
console.log(chalk.gray(" perp-cli did not send anything — transfer from your own wallet or exchange.\n"));
|
|
1249
|
+
});
|
|
1250
|
+
// ── Aster deposit / withdraw, Ondo deposit, Ondo address book (OWS-master on-chain legs) ──
|
|
1251
|
+
registerOnchainFundsCommands({
|
|
1252
|
+
program, funds, deposit, withdraw, isJson, isDryRun, getNetwork, getAdapterForExchange,
|
|
1253
|
+
requireMaster, printFundsPlan, failStatus: fundsFailStatus,
|
|
1254
|
+
});
|
|
651
1255
|
// ═══════════════════════════════════════════════════════
|
|
652
1256
|
// TRANSFER (HL internal)
|
|
653
1257
|
// ═══════════════════════════════════════════════════════
|
|
654
|
-
funds
|
|
655
|
-
.
|
|
656
|
-
.
|
|
657
|
-
.
|
|
658
|
-
|
|
1258
|
+
supportsDryRun(funds.command("transfer <amount> <destination>"))
|
|
1259
|
+
.description("Transfer USDC to another Hyperliquid account (internal, instant; the destination must be on the settings withdraw.hyperliquid allowlist), or — with -e qfex — between your QFEX accounts (destination: primary | <account uuid>; supports --dry-run)")
|
|
1260
|
+
.option("--from <account>", "QFEX only: source account (primary | <account uuid>; default: the account the key is bound to)")
|
|
1261
|
+
.option("--agent <name>", "QFEX only: imported key to use")
|
|
1262
|
+
.action(async (amount, destination, opts) => {
|
|
1263
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
659
1264
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
660
1265
|
throw new Error("Invalid amount");
|
|
661
|
-
const
|
|
1266
|
+
const exchangeOpt = program.opts().exchange;
|
|
1267
|
+
if (exchangeOpt !== undefined && getVenue(exchangeOpt).id === "qfex") {
|
|
1268
|
+
if (isDryRun()) {
|
|
1269
|
+
return printFundsPlan({ action: "transfer", venue: "qfex", network: getNetwork(), asset: "USDC", amount, from: opts.from ?? "the key's account", destination });
|
|
1270
|
+
}
|
|
1271
|
+
const adapter = await qfexAdapterFor(opts.agent);
|
|
1272
|
+
// "primary" is resolved from the venue's own account list (is_master) — never assumed.
|
|
1273
|
+
const accounts = await adapter.listAccounts();
|
|
1274
|
+
const resolve = (name) => {
|
|
1275
|
+
if (name.toLowerCase() !== "primary")
|
|
1276
|
+
return name.toLowerCase();
|
|
1277
|
+
const primary = accounts.filter(a => a.isPrimary);
|
|
1278
|
+
if (primary.length !== 1)
|
|
1279
|
+
throw new PerpError("EXCHANGE_ERROR", `QFEX lists ${primary.length} primary accounts`, { exchange: "qfex" });
|
|
1280
|
+
return primary[0].accountId;
|
|
1281
|
+
};
|
|
1282
|
+
const from = resolve(opts.from === undefined ? adapter.accountId : opts.from);
|
|
1283
|
+
const to = resolve(destination);
|
|
1284
|
+
for (const id of [from, to]) {
|
|
1285
|
+
if (!accounts.some(a => a.accountId === id)) {
|
|
1286
|
+
throw new PerpError("INVALID_PARAMS", `QFEX account ${id} is not one of your accounts`, { remediation: `Your accounts: ${accounts.map(a => `${a.accountId}${a.isPrimary ? " (primary)" : ""}`).join(", ")}` });
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
if (!isJson())
|
|
1290
|
+
console.log(chalk.cyan(`\n Transferring ${amount} USDC on QFEX: ${from} → ${to}...\n`));
|
|
1291
|
+
const result = await adapter.transferBetweenAccounts(amount, from, to);
|
|
1292
|
+
logExecution({ type: "bridge", exchange: "qfex", symbol: "USDC", side: "transfer", size: amount, status: "success", dryRun: false, meta: { action: "transfer", from, to } });
|
|
1293
|
+
if (isJson())
|
|
1294
|
+
return printJson(jsonOk(result));
|
|
1295
|
+
console.log(chalk.green(" Transfer accepted by QFEX."));
|
|
1296
|
+
console.log(chalk.gray(" Verify both balances: perp account balance -e qfex\n"));
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
if (opts.from !== undefined || opts.agent !== undefined) {
|
|
1300
|
+
throw new PerpError("INVALID_PARAMS", "--from / --agent apply to QFEX transfers only", { remediation: "Add -e qfex, or drop the flags for a Hyperliquid transfer" });
|
|
1301
|
+
}
|
|
1302
|
+
// usdSend pays another account: the destination must pass settings.withdraw.hyperliquid
|
|
1303
|
+
// (allowlist + maxPerTx / maxPerDay, shared with withdrawals to non-owner addresses).
|
|
1304
|
+
const amountStr = parsePositiveDecimal(amount, "amount");
|
|
1305
|
+
const gateOf = () => {
|
|
1306
|
+
const g = loadSettings().withdraw.hyperliquid;
|
|
1307
|
+
return g === undefined ? null : g;
|
|
1308
|
+
};
|
|
1309
|
+
if (isDryRun()) {
|
|
1310
|
+
// The gate needs no key and no network: a dry run refuses exactly as the live send would.
|
|
1311
|
+
assertWithdrawAllowed("hyperliquid", getNetwork(), amountStr, destination, gateOf(), Date.now());
|
|
1312
|
+
return printFundsPlan({ action: "transfer", venue: "hyperliquid", network: getNetwork(), asset: "USDC", amount: String(amountNum), destination, withdrawGate: "passes (destination allowlisted in settings withdraw.hyperliquid, within maxPerTx / maxPerDay)" });
|
|
1313
|
+
}
|
|
1314
|
+
await requireMaster("hyperliquid", "Hyperliquid transfer");
|
|
1315
|
+
assertWithdrawAllowed("hyperliquid", getNetwork(), amountStr, destination, gateOf(), Date.now());
|
|
1316
|
+
// Not -e qfex → the Hyperliquid transfer (as the dry-run plan says): fetch it directly, independent of -e.
|
|
1317
|
+
const adapter = await getAdapterForExchange("hyperliquid");
|
|
662
1318
|
if (!isUsdTransferCapable(adapter))
|
|
663
|
-
throw new Error("
|
|
1319
|
+
throw new Error("hyperliquid venue did not produce a USD-transfer-capable adapter");
|
|
664
1320
|
if (!isJson())
|
|
665
1321
|
console.log(chalk.cyan(`\n Transferring $${formatUsd(amountNum)} USDC to ${destination}...\n`));
|
|
666
|
-
|
|
1322
|
+
// owner = null: every usdSend destination is gated (sending to another account is the point).
|
|
1323
|
+
const attempt = hyperliquidGateEnforce(getNetwork(), amountStr, destination, null, Date.now());
|
|
1324
|
+
let result;
|
|
1325
|
+
try {
|
|
1326
|
+
result = await adapter.usdTransfer(amountNum, destination);
|
|
1327
|
+
}
|
|
1328
|
+
catch (err) {
|
|
1329
|
+
if (attempt !== null && isDefiniteOrderFailure(err))
|
|
1330
|
+
forgetWithdrawAttempt(attempt);
|
|
1331
|
+
throw err;
|
|
1332
|
+
}
|
|
667
1333
|
if (isJson())
|
|
668
1334
|
return printJson(jsonOk(result));
|
|
669
1335
|
console.log(chalk.green(` Transfer complete!`));
|
|
@@ -685,11 +1351,20 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
685
1351
|
pacifica: { chain: "Solana", token: "USDC", min: 10, gas: "SOL ~0.005", method: "on-chain program" },
|
|
686
1352
|
hyperliquid: { chain: "Arbitrum", token: "USDC", min: 5, gas: "ETH ~0.0001", method: "USDC transfer to Bridge2" },
|
|
687
1353
|
lighter: { chain: "Ethereum L1 / Arbitrum / Base / Avalanche", token: "USDC", min: "1 (L1) or 5 (CCTP)", gas: "$3-10 (L1), ~$0.01 (CCTP)", method: "deposit() or CCTP intent" },
|
|
1354
|
+
"lighter-rh": { chain: "Robinhood Chain", token: "USDG", min: 1, gas: "Robinhood Chain ETH", method: "approve + deposit() (mainnet only)" },
|
|
1355
|
+
qfex: { chain: "Arbitrum", token: "USDC", min: null, method: "address display only: perp funds deposit qfex, then send USDC from your wallet" },
|
|
1356
|
+
aster: { chain: "BNB Chain (default) / Arbitrum", token: "USDT (default) / USDC", min: null, gas: "BNB / Arbitrum ETH", method: "approve + vault depositFor(token, forAddress, amount, broker 1) (mainnet only)", command: "perp funds deposit aster <amount> [--chain bsc|arbitrum] [--asset USDT|USDC]" },
|
|
1357
|
+
ondo: { chain: "Arbitrum (default) / Ethereum", token: "USDC", min: null, gas: "ETH", method: "USDC transfer to the account's funding address (mainnet only)", command: "perp funds deposit ondo <amount> [--chain arbitrum|ethereum] [--probe <amount>]" },
|
|
688
1358
|
},
|
|
689
1359
|
withdraw: {
|
|
690
1360
|
pacifica: { chain: "Solana", token: "USDC", speed: "~10s", fee: "none" },
|
|
691
1361
|
hyperliquid: { chain: "Arbitrum", token: "USDC", speed: "minutes", fee: "$1" },
|
|
692
1362
|
lighter: { chain: "Ethereum L1", token: "USDC", speed: "~12 hours (standard), minutes (fast)", fee: "varies" },
|
|
1363
|
+
"lighter-rh": { chain: "Robinhood Chain", token: "USDG", command: "perp -e lighter-rh funds withdraw lighter <amount>" },
|
|
1364
|
+
ondo: { chain: "Ethereum", token: "USDC", command: "perp funds withdraw ondo <amount> --to <address>", addressBook: "destination must be listed: perp funds address-book add ondo <master address>" },
|
|
1365
|
+
qfex: { chain: "Arbitrum", 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 (All accounts access)" },
|
|
1366
|
+
arcus: { chain: "Robinhood Chain", token: "USDG", command: "perp funds withdraw arcus <amount>" },
|
|
1367
|
+
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" },
|
|
693
1368
|
},
|
|
694
1369
|
}));
|
|
695
1370
|
}
|
|
@@ -714,6 +1389,15 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
714
1389
|
console.log(` L1: ${chalk.green("perp funds deposit lighter ethereum <amount>")} — gas $3-10+`);
|
|
715
1390
|
console.log(` CCTP: ${chalk.green("perp funds deposit lighter cctp <chain> <amount>")} — gas ~$0.01`);
|
|
716
1391
|
console.log(` Chains: arbitrum, base, avalanche`);
|
|
1392
|
+
console.log(chalk.white.bold("\n Lighter RH") + chalk.gray(" (Robinhood Chain)"));
|
|
1393
|
+
console.log(` Token: USDG | Min: 1 | Gas: Robinhood Chain ETH (mainnet only)`);
|
|
1394
|
+
console.log(` Command: ${chalk.green("perp funds deposit lighter-rh <amount>")}`);
|
|
1395
|
+
console.log(chalk.white.bold("\n Aster") + chalk.gray(" (BNB Chain / Arbitrum vault contract)"));
|
|
1396
|
+
console.log(` Token: USDT (default) or USDC | Gas: BNB / Arbitrum ETH (mainnet only)`);
|
|
1397
|
+
console.log(` Command: ${chalk.green("perp funds deposit aster <amount> [--chain bsc|arbitrum] [--asset USDT|USDC]")}`);
|
|
1398
|
+
console.log(chalk.white.bold("\n Ondo Perps") + chalk.gray(" (Arbitrum / Ethereum — USDC transfer to your funding address)"));
|
|
1399
|
+
console.log(` Token: USDC | Gas: ETH (mainnet only)`);
|
|
1400
|
+
console.log(` Command: ${chalk.green("perp funds deposit ondo <amount> [--chain arbitrum|ethereum] [--probe <amount>]")}`);
|
|
717
1401
|
// Withdraw info
|
|
718
1402
|
console.log(chalk.white.bold("\n ── Withdraw ──\n"));
|
|
719
1403
|
console.log(chalk.white.bold(" Pacifica") + chalk.gray(" → Solana"));
|
|
@@ -725,6 +1409,16 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
725
1409
|
console.log(chalk.white.bold("\n Lighter") + chalk.gray(" → Ethereum L1"));
|
|
726
1410
|
console.log(` Speed: ~12h (standard) | Fee: varies`);
|
|
727
1411
|
console.log(` Command: ${chalk.green("perp -e lt funds withdraw lighter <amount>")}`);
|
|
1412
|
+
console.log(chalk.white.bold("\n Lighter RH") + chalk.gray(" → Robinhood Chain (USDG)"));
|
|
1413
|
+
console.log(` Command: ${chalk.green("perp -e lighter-rh funds withdraw lighter <amount>")}`);
|
|
1414
|
+
console.log(chalk.white.bold("\n Ondo Perps") + chalk.gray(" → Ethereum (USDC)"));
|
|
1415
|
+
console.log(` Command: ${chalk.green("perp funds withdraw ondo <amount> --to <address>")}`);
|
|
1416
|
+
console.log(` Address book: ${chalk.green("perp funds address-book add ondo <your master address>")}`);
|
|
1417
|
+
console.log(chalk.white.bold("\n Arcus") + chalk.gray(" → Robinhood Chain (USDG, to the account owner)"));
|
|
1418
|
+
console.log(` Command: ${chalk.green("perp funds withdraw arcus <amount>")}`);
|
|
1419
|
+
console.log(chalk.white.bold("\n Aster") + chalk.gray(" → BNB Chain / Arbitrum (USDT, to the account's own address; mainnet only)"));
|
|
1420
|
+
console.log(` Needs an API wallet approved with --can-withdraw --ip-whitelist; fee from user-withdraw-info`);
|
|
1421
|
+
console.log(` Command: ${chalk.green("perp funds withdraw aster <amount> [--chain bsc|arbitrum] [--asset USDT|USDC]")}`);
|
|
728
1422
|
// Transfer + Bridge
|
|
729
1423
|
console.log(chalk.white.bold("\n ── Transfer & Bridge ──\n"));
|
|
730
1424
|
console.log(chalk.white.bold(" Internal Transfer") + chalk.gray(" (Hyperliquid → Hyperliquid)"));
|
|
@@ -739,11 +1433,11 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
739
1433
|
console.log(` Check: ${chalk.green("perp funds rebalance check")}`);
|
|
740
1434
|
console.log(` Plan: ${chalk.green("perp funds rebalance plan")}`);
|
|
741
1435
|
console.log(` Execute: ${chalk.green("perp funds rebalance execute --auto-bridge")}`);
|
|
742
|
-
console.log(chalk.gray("\n
|
|
1436
|
+
console.log(chalk.gray("\n Deposits are direct by default (you pay gas). Add --relay to use the gas-sponsoring relayer.\n"));
|
|
743
1437
|
});
|
|
744
1438
|
// ═══════════════════════════════════════════════════════
|
|
745
1439
|
// NESTED SUBTREES — bridge (multi-provider) + rebalance
|
|
746
1440
|
// ═══════════════════════════════════════════════════════
|
|
747
|
-
registerFundsBridgeCommands(funds, isJson);
|
|
748
|
-
registerFundsRebalanceCommands(funds, getAdapterForExchange, isJson);
|
|
1441
|
+
registerFundsBridgeCommands(funds, isJson, getNetwork);
|
|
1442
|
+
registerFundsRebalanceCommands(funds, getAdapterForExchange, isJson, getNetwork);
|
|
749
1443
|
}
|