perp-cli 0.12.17 → 0.14.1
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 +247 -27
- 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 +303 -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 +70 -92
- 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 +248 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +925 -245
- package/dist/commands/history.js +102 -54
- 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 +357 -459
- 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 +240 -6
- package/dist/exchanges/aster.js +1139 -195
- 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 +152 -0
- package/dist/exchanges/ondo/adapter.js +811 -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 +95 -0
- package/dist/exchanges/ondo/funds.js +198 -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 +293 -4
- package/dist/exchanges/pacifica.js +1044 -90
- 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 +1386 -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 +29 -0
- package/dist/exchanges/venues/ondo.js +136 -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 +39 -0
- package/dist/exchanges/venues/qfex.js +140 -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 +156 -684
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +7 -2
- package/dist/landing.js +19 -3
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +821 -403
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -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 +159 -55
- package/skills/perp-cli/references/agent-operations.md +112 -58
- package/skills/perp-cli/references/commands.md +269 -73
- package/skills/perp-cli/references/strategies.md +34 -31
- package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
- package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
- 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 +31 -15
- 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/bridge.js
CHANGED
|
@@ -1,8 +1,203 @@
|
|
|
1
|
+
import { parsePositiveDecimal } from "../errors.js";
|
|
2
|
+
import { isDryRun, supportsDryRun } from "../cli-flags.js";
|
|
1
3
|
import chalk from "chalk";
|
|
2
4
|
import { formatUsd, makeTable, printJson, jsonOk } from "../utils.js";
|
|
3
5
|
import { logExecution } from "../execution-log.js";
|
|
4
|
-
import { executeBestBridge, getAllQuotes, checkDebridgeStatus, checkBridgeGasBalance, CHAIN_IDS, USDC_ADDRESSES, EXCHANGE_TO_CHAIN, } from "../bridge-engine.js";
|
|
5
|
-
|
|
6
|
+
import { executeBestBridge, getAllQuotes, checkDebridgeStatus, checkBridgeGasBalance, assertBridgeNetwork, assertDistinctBridgeChains, assertCctpRouteSupported, parseUsdcAmount, CHAIN_IDS, USDC_ADDRESSES, EXCHANGE_TO_CHAIN, } from "../bridge-engine.js";
|
|
7
|
+
import { cliCredentialOptsFrom } from "../config.js";
|
|
8
|
+
import { PerpError } from "../errors.js";
|
|
9
|
+
import { CCTP_DOMAINS } from "../bridge-engine.js";
|
|
10
|
+
import { RELAY_CHAINS, relayAsset, relayChain } from "../relay-swap.js";
|
|
11
|
+
import { bridgeAddress, bridgeVenueForChain, loadBridgeSigner } from "../bridge-signers.js";
|
|
12
|
+
/** Chains only the Relay path serves (not in the CCTP / deBridge tables): robinhood, bsc. */
|
|
13
|
+
function relayOnlyChain(chain) {
|
|
14
|
+
return chain in RELAY_CHAINS && !(chain in CCTP_DOMAINS) && !(chain in CHAIN_IDS);
|
|
15
|
+
}
|
|
16
|
+
function rootCredOpts(cmd) {
|
|
17
|
+
let root = cmd;
|
|
18
|
+
while (root.parent)
|
|
19
|
+
root = root.parent;
|
|
20
|
+
return cliCredentialOptsFrom(root.opts());
|
|
21
|
+
}
|
|
22
|
+
function parseProvider(raw) {
|
|
23
|
+
if (raw === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
const p = raw.toLowerCase();
|
|
26
|
+
if (p === "cctp" || p === "relay" || p === "debridge")
|
|
27
|
+
return p;
|
|
28
|
+
throw new PerpError("INVALID_PARAMS", `Invalid provider: ${raw}`, { remediation: "Use --provider cctp, relay, or debridge (or omit it for the best quote)" });
|
|
29
|
+
}
|
|
30
|
+
/** The quote to execute: the forced provider's, else the best (quotes are sorted by amountOut). */
|
|
31
|
+
function pickQuote(quotes, provider, srcChain, dstChain) {
|
|
32
|
+
if (quotes.length === 0) {
|
|
33
|
+
throw new PerpError("NO_BRIDGE_ROUTE", `No bridge available for ${srcChain} → ${dstChain}`, {
|
|
34
|
+
remediation: "Check supported chains / routes: perp funds bridge chains",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (provider === undefined)
|
|
38
|
+
return quotes[0];
|
|
39
|
+
const match = quotes.find(q => q.provider === provider);
|
|
40
|
+
if (!match) {
|
|
41
|
+
throw new PerpError("NO_BRIDGE_ROUTE", `Provider "${provider}" is not available for ${srcChain} → ${dstChain}`, {
|
|
42
|
+
remediation: `Drop --provider to use the best available route (available: ${quotes.map(q => q.provider).join(", ")})`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return match;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* JSON view of a quote. A Relay quote's raw payload carries the verified transactions (BigInt
|
|
49
|
+
* values, not JSON-serializable): shown as step kinds, as `relaySend` does.
|
|
50
|
+
*/
|
|
51
|
+
function quoteJson(q) {
|
|
52
|
+
if (q.provider !== "relay")
|
|
53
|
+
return q;
|
|
54
|
+
const { txs, ...rest } = q.raw;
|
|
55
|
+
return { ...q, raw: { ...rest, steps: txs.map(t => t.kind) } };
|
|
56
|
+
}
|
|
57
|
+
/** Gas preflight result → INSUFFICIENT_BALANCE before anything is signed. */
|
|
58
|
+
function assertBridgeGas(gasCheck, needsDstGas) {
|
|
59
|
+
if (gasCheck.ok)
|
|
60
|
+
return;
|
|
61
|
+
throw new PerpError("INSUFFICIENT_BALANCE", `Insufficient gas for bridge — nothing was sent:\n${gasCheck.errors.map(e => ` ✗ ${e}`).join("\n")}`, {
|
|
62
|
+
gas: gasCheck.errors,
|
|
63
|
+
remediation: needsDstGas
|
|
64
|
+
? "Fund the native gas token on the chains listed, or use --fast (Circle auto-relays; no destination gas needed)"
|
|
65
|
+
: "Fund the native gas token on the chains listed, then retry",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Execute through the engine; ORDER_PENDING (sent, outcome unknown) is logged as unverified, never as failed. */
|
|
69
|
+
async function executeLogged(side, amount, fast, run) {
|
|
70
|
+
try {
|
|
71
|
+
const result = await run();
|
|
72
|
+
logExecution({ type: "bridge", exchange: "bridge", symbol: "USDC", side, size: amount, status: "success", dryRun: false, meta: { provider: result.provider, txHash: result.txHash, fast } });
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const pending = err instanceof PerpError && err.structured.code === "ORDER_PENDING";
|
|
77
|
+
const txHash = err instanceof PerpError ? err.structured.details?.txHash : undefined;
|
|
78
|
+
logExecution({
|
|
79
|
+
type: "bridge", exchange: "bridge", symbol: "USDC", side, size: amount, status: pending ? "unverified" : "failed", dryRun: false,
|
|
80
|
+
error: err instanceof Error ? err.message : String(err),
|
|
81
|
+
meta: { fast, ...(typeof txHash === "string" ? { txHash } : {}) },
|
|
82
|
+
});
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `funds bridge send` through Relay (any pinned asset pair, EVM origin, OWS master wallet).
|
|
88
|
+
* Dry run: the verified quote + the output-protection verdict, no key loaded, nothing sent.
|
|
89
|
+
* Live: sender and recipient are the master wallet's own addresses (no arbitrary recipient).
|
|
90
|
+
*/
|
|
91
|
+
async function relaySend(opts, cmd, isJson, getNetwork) {
|
|
92
|
+
const dryRun = isDryRun(cmd);
|
|
93
|
+
const src = opts.from.toLowerCase();
|
|
94
|
+
const dst = opts.to.toLowerCase();
|
|
95
|
+
const fromAsset = (opts.fromAsset === undefined ? "USDC" : opts.fromAsset).toUpperCase();
|
|
96
|
+
const toAsset = (opts.toAsset === undefined ? "USDC" : opts.toAsset).toUpperCase();
|
|
97
|
+
const amount = parsePositiveDecimal(opts.amount, "--amount");
|
|
98
|
+
const minOut = opts.minOut === undefined ? null : parsePositiveDecimal(opts.minOut, "--min-out");
|
|
99
|
+
if (opts.fast === true)
|
|
100
|
+
throw new PerpError("INVALID_PARAMS", "--fast is a CCTP option; Relay fills in seconds", { remediation: "Drop --fast" });
|
|
101
|
+
const origin = relayChain(src);
|
|
102
|
+
const dest = relayChain(dst);
|
|
103
|
+
relayAsset(src, fromAsset);
|
|
104
|
+
relayAsset(dst, toAsset);
|
|
105
|
+
if (origin.vm !== "evm") {
|
|
106
|
+
throw new PerpError("NO_BRIDGE_ROUTE", `Relay: bridging FROM ${origin.label} is not implemented (only EVM-origin deposits are verified and signed)`, {
|
|
107
|
+
remediation: "Bridge from an EVM chain, or use --provider cctp for a Solana-origin USDC bridge (deBridge from Solana is refused too)",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const credOpts = rootCredOpts(cmd);
|
|
111
|
+
/** The master wallet's own address on a chain family — public address only, nothing is decrypted. */
|
|
112
|
+
const ownAddress = async (vm) => {
|
|
113
|
+
const { resolveOwsMaster } = await import("../config.js");
|
|
114
|
+
const m = await resolveOwsMaster("hyperliquid", credOpts);
|
|
115
|
+
if (m === null) {
|
|
116
|
+
throw new PerpError("NO_SIGNER_AVAILABLE", "Relay bridging is signed by the OWS master wallet, which is not selected on this computer", {
|
|
117
|
+
remediation: dryRun
|
|
118
|
+
? "For a dry run without a wallet pass both --sender <address> and --recipient <address>; otherwise select the master wallet (--ows <name> / perp wallet use <name>)"
|
|
119
|
+
: "Import the master wallet into the OWS vault (perp wallet import --name <name> --chain evm), then perp wallet use <name> or pass --ows <name>",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (vm === "evm")
|
|
123
|
+
return (await import("../signer/ows-evm.js")).OwsEvmSigner.create(m.wallet, m.passphrase).getAddress();
|
|
124
|
+
return (await import("../signer/ows-solana.js")).OwsSolanaSigner.create(m.wallet, m.passphrase).getPublicKeyBase58();
|
|
125
|
+
};
|
|
126
|
+
const same = (vm, a, b) => (vm === "evm" ? a.toLowerCase() === b.toLowerCase() : a === b);
|
|
127
|
+
let sender;
|
|
128
|
+
let recipient;
|
|
129
|
+
if (dryRun) {
|
|
130
|
+
sender = opts.sender === undefined ? await ownAddress("evm") : opts.sender;
|
|
131
|
+
recipient = opts.recipient === undefined ? await ownAddress(dest.vm) : opts.recipient;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
sender = await ownAddress("evm");
|
|
135
|
+
recipient = await ownAddress(dest.vm);
|
|
136
|
+
if (opts.sender !== undefined && !same("evm", opts.sender, sender)) {
|
|
137
|
+
throw new PerpError("INVALID_PARAMS", `--sender ${opts.sender} is not the master wallet (${sender})`, { remediation: "Omit --sender" });
|
|
138
|
+
}
|
|
139
|
+
if (opts.recipient !== undefined && !same(dest.vm, opts.recipient, recipient)) {
|
|
140
|
+
throw new PerpError("POLICY_DENIED", `Relay bridging only pays the master wallet's own ${dest.label} address ${recipient}; --recipient ${opts.recipient} is refused`, {
|
|
141
|
+
remediation: "Omit --recipient (the master wallet's own address on the destination chain is used)",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!isJson())
|
|
146
|
+
console.log(chalk.cyan(`\n Relay bridge: ${amount} ${fromAsset} (${origin.label}) → ${toAsset} (${dest.label})\n`));
|
|
147
|
+
const { quoteRelaySwap, relayMinOutVerdict, assertRelayMinOut, executeRelaySwap } = await import("../relay-swap.js");
|
|
148
|
+
const quote = await quoteRelaySwap({ from: src, to: dst, fromAsset, toAsset, amount, sender, recipient });
|
|
149
|
+
const verdict = relayMinOutVerdict(quote, minOut);
|
|
150
|
+
const { txs, ...quoteView } = quote;
|
|
151
|
+
const view = { ...quoteView, steps: txs.map(t => t.kind) };
|
|
152
|
+
if (!isJson()) {
|
|
153
|
+
console.log(` From: ${sender} (${origin.label})`);
|
|
154
|
+
console.log(` To: ${recipient} (${dest.label})`);
|
|
155
|
+
console.log(` Amount in: ${quote.amountIn} ${fromAsset}${quote.amountInUsd !== null ? ` (~$${quote.amountInUsd})` : ""}`);
|
|
156
|
+
console.log(` Expected out: ${quote.expectedOut} ${toAsset}${quote.expectedOutUsd !== null ? ` (~$${quote.expectedOutUsd})` : ""}`);
|
|
157
|
+
console.log(` Guaranteed: ${quote.minimumOut} ${toAsset} (protocol-enforced minimum)`);
|
|
158
|
+
console.log(` Fees: relayer $${quote.fees.relayerUsd}${quote.fees.originGasUsd !== null ? `, origin gas ~$${quote.fees.originGasUsd}` : ""}${quote.totalImpactPercent !== null ? ` (total impact ${quote.totalImpactPercent}%)` : ""}`);
|
|
159
|
+
console.log(` ETA: ${quote.timeEstimateSec === null ? "unknown" : `~${quote.timeEstimateSec}s`}`);
|
|
160
|
+
console.log(` Steps: ${view.steps.join(" → ")}`);
|
|
161
|
+
console.log(` Min-out check: ${verdict.ok ? chalk.green("ok") : chalk.red("REFUSED")} — ${verdict.reason}\n`);
|
|
162
|
+
}
|
|
163
|
+
if (dryRun) {
|
|
164
|
+
logExecution({ type: "bridge", exchange: "bridge", symbol: fromAsset, side: `${src}->${dst}`, size: amount, status: "simulated", dryRun: true, meta: { provider: "relay", requestId: quote.requestId, toAsset } });
|
|
165
|
+
if (isJson())
|
|
166
|
+
return printJson(jsonOk({ dryRun: true, sent: false, provider: "relay", quote: view, minOut: verdict }));
|
|
167
|
+
console.log(chalk.yellow(" [DRY RUN] Nothing was signed or sent.\n"));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
assertRelayMinOut(quote, minOut);
|
|
171
|
+
const { loadMasterEvmSigner } = await import("../evm-master-tx.js");
|
|
172
|
+
const signer = await loadMasterEvmSigner("hyperliquid", credOpts, "Relay bridge");
|
|
173
|
+
if (!same("evm", signer.getAddress(), sender)) {
|
|
174
|
+
throw new PerpError("INVALID_PARAMS", `Relay: signer ${signer.getAddress()} is not the quoted sender ${sender} — nothing was sent`, { remediation: "Omit --sender" });
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const result = await executeRelaySwap(getNetwork(), quote, tx => signer.signTransaction(tx));
|
|
178
|
+
logExecution({ type: "bridge", exchange: "bridge", symbol: fromAsset, side: `${src}->${dst}`, size: amount, status: "success", dryRun: false, meta: { provider: "relay", requestId: result.requestId, txHash: result.depositTxHash, toAsset } });
|
|
179
|
+
if (isJson())
|
|
180
|
+
return printJson(jsonOk(result));
|
|
181
|
+
console.log(chalk.green(" Bridge filled."));
|
|
182
|
+
if (result.approveTxHash !== null)
|
|
183
|
+
console.log(` Approve: ${result.approveTxHash}`);
|
|
184
|
+
console.log(` Deposit: ${result.depositTxHash} (${origin.label})`);
|
|
185
|
+
for (const h of result.destinationTxHashes)
|
|
186
|
+
console.log(` Fill: ${h} (${dest.label})`);
|
|
187
|
+
console.log(` Request id: ${result.requestId}\n`);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
const pending = err instanceof PerpError && err.structured.code === "ORDER_PENDING";
|
|
191
|
+
const txHash = err instanceof PerpError ? err.structured.details?.txHash : undefined;
|
|
192
|
+
logExecution({
|
|
193
|
+
type: "bridge", exchange: "bridge", symbol: fromAsset, side: `${src}->${dst}`, size: amount, status: pending ? "unverified" : "failed", dryRun: false,
|
|
194
|
+
error: err instanceof Error ? err.message : String(err),
|
|
195
|
+
meta: { provider: "relay", requestId: quote.requestId, toAsset, ...(typeof txHash === "string" ? { txHash } : {}) },
|
|
196
|
+
});
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export function registerFundsBridgeCommands(parent, isJson, getNetwork) {
|
|
6
201
|
const bridge = parent.command("bridge").description("Cross-chain USDC bridge (multi-provider: cctp, relay, debridge — auto-cheapest)");
|
|
7
202
|
// ── bridge chains ──
|
|
8
203
|
bridge
|
|
@@ -10,7 +205,7 @@ export function registerFundsBridgeCommands(parent, isJson) {
|
|
|
10
205
|
.description("List supported chains and USDC addresses")
|
|
11
206
|
.action(async () => {
|
|
12
207
|
if (isJson()) {
|
|
13
|
-
return printJson(jsonOk({ chains: CHAIN_IDS, usdc: USDC_ADDRESSES, exchanges: EXCHANGE_TO_CHAIN }));
|
|
208
|
+
return printJson(jsonOk({ chains: CHAIN_IDS, usdc: USDC_ADDRESSES, exchanges: EXCHANGE_TO_CHAIN, relay: RELAY_CHAINS }));
|
|
14
209
|
}
|
|
15
210
|
console.log(chalk.cyan.bold("\n Supported Bridge Chains\n"));
|
|
16
211
|
const rows = Object.entries(CHAIN_IDS).map(([chain, id]) => [
|
|
@@ -24,7 +219,11 @@ export function registerFundsBridgeCommands(parent, isJson) {
|
|
|
24
219
|
for (const [ex, chain] of Object.entries(EXCHANGE_TO_CHAIN)) {
|
|
25
220
|
console.log(` ${chalk.white.bold(ex.padEnd(14))} → ${chain}`);
|
|
26
221
|
}
|
|
27
|
-
console.log(chalk.
|
|
222
|
+
console.log(chalk.cyan.bold("\n Relay chains / assets (--provider relay, --from-asset / --to-asset)\n"));
|
|
223
|
+
for (const [name, def] of Object.entries(RELAY_CHAINS)) {
|
|
224
|
+
console.log(` ${chalk.white.bold(name.padEnd(14))} ${String(def.chainId).padEnd(10)} ${Object.keys(def.assets).join(", ")}`);
|
|
225
|
+
}
|
|
226
|
+
console.log(chalk.gray("\n Providers: Circle CCTP, Relay, deBridge DLN\n"));
|
|
28
227
|
});
|
|
29
228
|
// ── bridge quote ──
|
|
30
229
|
bridge
|
|
@@ -33,21 +232,28 @@ export function registerFundsBridgeCommands(parent, isJson) {
|
|
|
33
232
|
.requiredOption("--from <chain>", "Source chain (solana, arbitrum, base)")
|
|
34
233
|
.requiredOption("--to <chain>", "Destination chain")
|
|
35
234
|
.requiredOption("--amount <amount>", "USDC amount")
|
|
36
|
-
.option("--sender <address>", "Source address")
|
|
37
|
-
.option("--recipient <address>", "Destination address")
|
|
38
|
-
.action(async (opts) => {
|
|
235
|
+
.option("--sender <address>", "Source address (default: the master wallet's own address on --from; public address only, nothing decrypted)")
|
|
236
|
+
.option("--recipient <address>", "Destination address (default: the master wallet's own address on --to)")
|
|
237
|
+
.action(async (opts, cmd) => {
|
|
39
238
|
const srcChain = opts.from.toLowerCase();
|
|
40
239
|
const dstChain = opts.to.toLowerCase();
|
|
41
|
-
|
|
240
|
+
assertDistinctBridgeChains(srcChain, dstChain);
|
|
241
|
+
assertCctpRouteSupported(srcChain, dstChain);
|
|
242
|
+
const amount = parseUsdcAmount(opts.amount, "--amount"); // exact decimal string (≤ 6 decimals)
|
|
243
|
+
// Omitted addresses are the master wallet's own public addresses (OWS: vault account list).
|
|
244
|
+
const credOpts = rootCredOpts(cmd);
|
|
245
|
+
const sender = opts.sender ?? await bridgeAddress(srcChain, bridgeVenueForChain(srcChain), credOpts);
|
|
246
|
+
const recipient = opts.recipient ?? await bridgeAddress(dstChain, bridgeVenueForChain(dstChain), credOpts);
|
|
42
247
|
if (!isJson())
|
|
43
248
|
console.log(chalk.cyan(`\n Fetching quotes: $${amount} USDC ${srcChain} → ${dstChain}...\n`));
|
|
44
|
-
const quotes = await getAllQuotes(srcChain, dstChain, amount,
|
|
45
|
-
if (isJson())
|
|
46
|
-
return printJson(jsonOk({ quotes }));
|
|
249
|
+
const quotes = await getAllQuotes(srcChain, dstChain, amount, sender, recipient);
|
|
47
250
|
if (quotes.length === 0) {
|
|
48
|
-
|
|
49
|
-
|
|
251
|
+
throw new PerpError("NO_BRIDGE_ROUTE", `No bridge provider quoted ${srcChain} → ${dstChain}`, {
|
|
252
|
+
remediation: "Check supported chains / routes: perp funds bridge chains",
|
|
253
|
+
});
|
|
50
254
|
}
|
|
255
|
+
if (isJson())
|
|
256
|
+
return printJson(jsonOk({ quotes: quotes.map(quoteJson) }));
|
|
51
257
|
const PROVIDER_NAMES = {
|
|
52
258
|
cctp: "Circle CCTP",
|
|
53
259
|
relay: "Relay",
|
|
@@ -56,7 +262,8 @@ export function registerFundsBridgeCommands(parent, isJson) {
|
|
|
56
262
|
const rows = quotes.map((q, i) => {
|
|
57
263
|
const tag = i === 0 ? chalk.green(" ★ BEST") : "";
|
|
58
264
|
const name = PROVIDER_NAMES[q.provider] ?? q.provider;
|
|
59
|
-
const
|
|
265
|
+
const usdcFee = q.fee <= 0 && !q.nativeFee ? chalk.green("FREE") : `$${formatUsd(q.fee)}`;
|
|
266
|
+
const feeStr = q.nativeFee ? `${usdcFee} + ${q.nativeFee.amount} ${q.nativeFee.symbol}${q.nativeFee.usd !== null ? ` (≈$${formatUsd(q.nativeFee.usd)})` : " (unpriced)"}` : usdcFee;
|
|
60
267
|
const pct = ((q.fee / q.amountIn) * 100).toFixed(2);
|
|
61
268
|
const gasTag = q.gasIncluded
|
|
62
269
|
? chalk.green("included")
|
|
@@ -69,7 +276,7 @@ export function registerFundsBridgeCommands(parent, isJson) {
|
|
|
69
276
|
gasTag,
|
|
70
277
|
];
|
|
71
278
|
});
|
|
72
|
-
console.log(chalk.cyan.bold(` Bridge Quotes: $${formatUsd(amount)} USDC ${srcChain} → ${dstChain}\n`));
|
|
279
|
+
console.log(chalk.cyan.bold(` Bridge Quotes: $${formatUsd(Number(amount))} USDC ${srcChain} → ${dstChain}\n`));
|
|
73
280
|
console.log(makeTable(["Provider", "Receive", "Fee", "ETA", "Dst Gas"], rows));
|
|
74
281
|
// Show gas notes per provider
|
|
75
282
|
for (const q of quotes) {
|
|
@@ -83,81 +290,52 @@ export function registerFundsBridgeCommands(parent, isJson) {
|
|
|
83
290
|
console.log(chalk.gray(` Pick provider: --provider cctp|relay|debridge\n`));
|
|
84
291
|
});
|
|
85
292
|
// ── bridge send ──
|
|
86
|
-
bridge
|
|
87
|
-
.
|
|
88
|
-
.description("Execute a USDC bridge transaction")
|
|
293
|
+
supportsDryRun(bridge.command("send"))
|
|
294
|
+
.description("Execute a bridge transaction — USDC by default; any pinned asset pair through Relay with --from-asset / --to-asset (supports --dry-run: quotes only, nothing sent)")
|
|
89
295
|
.requiredOption("--from <chain>", "Source chain")
|
|
90
296
|
.requiredOption("--to <chain>", "Destination chain")
|
|
91
|
-
.requiredOption("--amount <amount>", "USDC
|
|
92
|
-
.option("--
|
|
93
|
-
.option("--
|
|
297
|
+
.requiredOption("--amount <amount>", "Amount of the source asset (USDC unless --from-asset)")
|
|
298
|
+
.option("--from-asset <symbol>", "Relay: source asset (e.g. USDG, USDC, USDT, ETH; default USDC)")
|
|
299
|
+
.option("--to-asset <symbol>", "Relay: destination asset (e.g. USDC, USDT, SOL, ETH; default USDC)")
|
|
300
|
+
.option("--min-out <amount>", "Relay: minimum output accepted, in the destination asset (required for stable → native)")
|
|
301
|
+
.option("--sender <address>", "Source address (auto-detected from key; live: must be this wallet's own address)")
|
|
302
|
+
.option("--recipient <address>", "Destination address (live: must be this wallet's own address on the destination chain; any other is refused)")
|
|
94
303
|
.option("--provider <name>", "Force provider: cctp, relay, debridge (default: cheapest)")
|
|
95
304
|
.option("--fast", "Use fast finality (~1-2 min, $1-1.3 fee, auto-relay). Default: standard (~13-20 min, $0.01, manual relay)")
|
|
96
|
-
.
|
|
97
|
-
|
|
305
|
+
.action(async (opts, cmd) => {
|
|
306
|
+
const dryRun = isDryRun(cmd);
|
|
307
|
+
// Mainnet-only routes: refuse -n testnet before any key is loaded.
|
|
308
|
+
assertBridgeNetwork(getNetwork());
|
|
98
309
|
const srcChain = opts.from.toLowerCase();
|
|
99
310
|
const dstChain = opts.to.toLowerCase();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const pk = await loadPrivateKey("pacifica");
|
|
114
|
-
const keypair = parseSolanaKeypair(pk);
|
|
115
|
-
senderAddress = opts.sender ?? keypair.publicKey.toBase58();
|
|
116
|
-
signerKey = pk;
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
const exchange = "hyperliquid";
|
|
120
|
-
const pk = await loadPrivateKey(exchange);
|
|
121
|
-
const { ethers } = await import("ethers");
|
|
122
|
-
const wallet = new ethers.Wallet(pk);
|
|
123
|
-
senderAddress = opts.sender ?? wallet.address;
|
|
124
|
-
signerKey = pk;
|
|
125
|
-
}
|
|
126
|
-
// Determine recipient address and destination signer key
|
|
127
|
-
let dstSignerKey;
|
|
128
|
-
if (opts.recipient) {
|
|
129
|
-
recipientAddress = opts.recipient;
|
|
130
|
-
}
|
|
131
|
-
else if (dstChain === "solana") {
|
|
132
|
-
const pk = await loadPrivateKey("pacifica");
|
|
133
|
-
const keypair = parseSolanaKeypair(pk);
|
|
134
|
-
recipientAddress = keypair.publicKey.toBase58();
|
|
135
|
-
dstSignerKey = pk;
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
const exchange = "hyperliquid";
|
|
139
|
-
const pk = await loadPrivateKey(exchange);
|
|
140
|
-
const { ethers } = await import("ethers");
|
|
141
|
-
recipientAddress = new ethers.Wallet(pk).address;
|
|
142
|
-
dstSignerKey = pk;
|
|
311
|
+
// Before any key is loaded or quote fetched.
|
|
312
|
+
assertDistinctBridgeChains(srcChain, dstChain);
|
|
313
|
+
assertCctpRouteSupported(srcChain, dstChain);
|
|
314
|
+
// Relay path: asked for by name, by an asset / min-out flag, or implied by a Relay-only chain.
|
|
315
|
+
const relayRoute = opts.provider?.toLowerCase() === "relay" || opts.fromAsset !== undefined || opts.toAsset !== undefined
|
|
316
|
+
|| opts.minOut !== undefined || relayOnlyChain(srcChain) || relayOnlyChain(dstChain);
|
|
317
|
+
if (relayRoute) {
|
|
318
|
+
if (opts.provider !== undefined && opts.provider.toLowerCase() !== "relay") {
|
|
319
|
+
throw new PerpError("INVALID_PARAMS", `--from-asset / --to-asset / --min-out and the ${relayOnlyChain(srcChain) ? srcChain : dstChain} chain are served by Relay only, not "${opts.provider}"`, {
|
|
320
|
+
remediation: "Use --provider relay (or drop --provider)",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return relaySend(opts, cmd, isJson, getNetwork);
|
|
143
324
|
}
|
|
325
|
+
const amount = parseUsdcAmount(opts.amount, "--amount"); // exact decimal string (≤ 6 decimals)
|
|
326
|
+
const chosenProvider = parseProvider(opts.provider);
|
|
327
|
+
// Addresses are public (review L3): an address passed with --sender / --recipient needs no
|
|
328
|
+
// wallet at all, and an omitted one is the master's own public address (OWS: the vault's
|
|
329
|
+
// account list; nothing is decrypted). Signers are only loaded after the dry-run return.
|
|
330
|
+
const credOpts = rootCredOpts(cmd);
|
|
331
|
+
const addressOf = (chain) => bridgeAddress(chain, bridgeVenueForChain(chain), credOpts);
|
|
332
|
+
const senderAddress = opts.sender ?? await addressOf(srcChain);
|
|
333
|
+
const recipientAddress = opts.recipient ?? await addressOf(dstChain);
|
|
144
334
|
// Fetch all quotes
|
|
145
335
|
if (!isJson())
|
|
146
336
|
console.log(chalk.cyan(`\n Bridge: $${amount} USDC ${srcChain} → ${dstChain}\n`));
|
|
147
337
|
const quotes = await getAllQuotes(srcChain, dstChain, amount, senderAddress, recipientAddress);
|
|
148
|
-
|
|
149
|
-
throw new Error(`No bridge available for ${srcChain} → ${dstChain}`);
|
|
150
|
-
// Pick the quote to use
|
|
151
|
-
let selectedQuote;
|
|
152
|
-
if (chosenProvider) {
|
|
153
|
-
const match = quotes.find(q => q.provider === chosenProvider);
|
|
154
|
-
if (!match)
|
|
155
|
-
throw new Error(`Provider "${chosenProvider}" not available for this route`);
|
|
156
|
-
selectedQuote = match;
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
selectedQuote = quotes[0]; // best (sorted by amountOut)
|
|
160
|
-
}
|
|
338
|
+
const selectedQuote = pickQuote(quotes, chosenProvider, srcChain, dstChain);
|
|
161
339
|
const PROVIDER_NAMES = {
|
|
162
340
|
cctp: "Circle CCTP",
|
|
163
341
|
relay: "Relay",
|
|
@@ -168,7 +346,8 @@ export function registerFundsBridgeCommands(parent, isJson) {
|
|
|
168
346
|
const rows = quotes.map((q) => {
|
|
169
347
|
const name = PROVIDER_NAMES[q.provider] ?? q.provider;
|
|
170
348
|
const selected = q.provider === selectedQuote.provider;
|
|
171
|
-
const
|
|
349
|
+
const usdcFee = q.fee <= 0 && !q.nativeFee ? chalk.green("FREE") : `$${formatUsd(q.fee)}`;
|
|
350
|
+
const feeStr = q.nativeFee ? `${usdcFee} + ${q.nativeFee.amount} ${q.nativeFee.symbol}${q.nativeFee.usd !== null ? ` (≈$${formatUsd(q.nativeFee.usd)})` : " (unpriced)"}` : usdcFee;
|
|
172
351
|
const pct = ((q.fee / q.amountIn) * 100).toFixed(2);
|
|
173
352
|
const tag = selected ? chalk.green(" ← selected") : "";
|
|
174
353
|
const gasTag = q.gasIncluded
|
|
@@ -189,39 +368,39 @@ export function registerFundsBridgeCommands(parent, isJson) {
|
|
|
189
368
|
console.log(` From: ${senderAddress}`);
|
|
190
369
|
console.log(` To: ${recipientAddress}\n`);
|
|
191
370
|
}
|
|
192
|
-
if (
|
|
371
|
+
if (dryRun) {
|
|
193
372
|
if (isJson())
|
|
194
|
-
return printJson(jsonOk({ quotes, selected: selectedQuote.provider }));
|
|
373
|
+
return printJson(jsonOk({ dryRun: true, sent: false, quotes: quotes.map(quoteJson), selected: selectedQuote.provider }));
|
|
195
374
|
console.log(chalk.yellow(" [DRY RUN] Transaction not executed.\n"));
|
|
196
375
|
return;
|
|
197
376
|
}
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
console.log(chalk.red(msg));
|
|
207
|
-
if (needsDstGas) {
|
|
208
|
-
console.log(chalk.yellow("\n Tip: use --fast to skip destination gas (Circle auto-relays)\n"));
|
|
209
|
-
}
|
|
210
|
-
process.exitCode = 1;
|
|
211
|
-
return;
|
|
377
|
+
// Own-address rule (same as the Relay path): a live bridge only moves funds from and to this
|
|
378
|
+
// wallet's own addresses. --sender / --recipient may name them, nothing else.
|
|
379
|
+
const sameAddr = (chain, a, b) => chain === "solana" ? a === b : a.toLowerCase() === b.toLowerCase();
|
|
380
|
+
const ownSender = await addressOf(srcChain);
|
|
381
|
+
if (!sameAddr(srcChain, senderAddress, ownSender)) {
|
|
382
|
+
throw new PerpError("POLICY_DENIED", `Bridging signs with this wallet's own ${srcChain} address ${ownSender}; --sender ${senderAddress} is refused. Nothing was sent`, {
|
|
383
|
+
remediation: "Omit --sender (the wallet's own address on the source chain is used)",
|
|
384
|
+
});
|
|
212
385
|
}
|
|
386
|
+
const ownRecipient = await addressOf(dstChain);
|
|
387
|
+
if (!sameAddr(dstChain, recipientAddress, ownRecipient)) {
|
|
388
|
+
throw new PerpError("POLICY_DENIED", `Bridging only pays this wallet's own ${dstChain} address ${ownRecipient}; --recipient ${recipientAddress} is refused. Nothing was sent`, {
|
|
389
|
+
remediation: "Omit --recipient (the wallet's own address on the destination chain is used)",
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
// Gas balance preflight check (before any signer is loaded)
|
|
393
|
+
const needsDstGas = !selectedQuote.gasIncluded;
|
|
394
|
+
assertBridgeGas(await checkBridgeGasBalance(srcChain, senderAddress, dstChain, recipientAddress, needsDstGas), needsDstGas);
|
|
395
|
+
// Master signers (live only): OWS vault signer, or a raw-key signer. The destination signer
|
|
396
|
+
// claims on the destination chain (CCTP EVM → Solana standard finality).
|
|
397
|
+
const signer = await loadBridgeSigner(srcChain, bridgeVenueForChain(srcChain), credOpts);
|
|
398
|
+
const dstSigner = await loadBridgeSigner(dstChain, bridgeVenueForChain(dstChain), credOpts);
|
|
213
399
|
const providerName = PROVIDER_NAMES[selectedQuote.provider] ?? selectedQuote.provider;
|
|
214
400
|
if (!isJson())
|
|
215
401
|
console.log(chalk.yellow(` Executing via ${providerName}...\n`));
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
result = await executeBestBridge(srcChain, dstChain, amount, signerKey, senderAddress, recipientAddress, dstSignerKey, chosenProvider, !!opts.fast);
|
|
219
|
-
logExecution({ type: "bridge", exchange: "bridge", symbol: "USDC", side: `${srcChain}->${dstChain}`, size: String(amount), status: "success", dryRun: false, meta: { provider: result.provider, txHash: result.txHash, fast: !!opts.fast } });
|
|
220
|
-
}
|
|
221
|
-
catch (err) {
|
|
222
|
-
logExecution({ type: "bridge", exchange: "bridge", symbol: "USDC", side: `${srcChain}->${dstChain}`, size: String(amount), status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err) });
|
|
223
|
-
throw err;
|
|
224
|
-
}
|
|
402
|
+
// The provider shown as "selected" is the one executed.
|
|
403
|
+
const result = await executeLogged(`${srcChain}->${dstChain}`, amount, !!opts.fast, () => executeBestBridge(getNetwork(), srcChain, dstChain, amount, signer, senderAddress, recipientAddress, dstSigner, selectedQuote.provider, !!opts.fast));
|
|
225
404
|
if (isJson())
|
|
226
405
|
return printJson(jsonOk(result));
|
|
227
406
|
console.log(chalk.green(" Bridge transaction submitted!"));
|
|
@@ -233,76 +412,56 @@ export function registerFundsBridgeCommands(parent, isJson) {
|
|
|
233
412
|
console.log(chalk.gray(`\n Funds arrive in ~${selectedQuote.estimatedTime}s on ${dstChain}.\n`));
|
|
234
413
|
});
|
|
235
414
|
// ── bridge between exchanges (shortcut) ──
|
|
236
|
-
bridge
|
|
237
|
-
.
|
|
238
|
-
.
|
|
239
|
-
.requiredOption("--from <exchange>", "Source exchange (pacifica, hyperliquid, lighter)")
|
|
415
|
+
supportsDryRun(bridge.command("exchange"))
|
|
416
|
+
.description("Bridge the master wallet's own USDC between the chains two exchanges settle on (wallet → wallet; does not withdraw from or deposit into Pacifica / Lighter — see --help; supports --dry-run: quotes only, nothing sent)")
|
|
417
|
+
.requiredOption("--from <exchange>", `Source exchange (${Object.keys(EXCHANGE_TO_CHAIN).join(", ")})`)
|
|
240
418
|
.requiredOption("--to <exchange>", "Destination exchange")
|
|
241
419
|
.requiredOption("--amount <amount>", "USDC amount")
|
|
242
420
|
.option("--provider <name>", "Force provider: cctp, relay, debridge (default: cheapest)")
|
|
243
421
|
.option("--fast", "Use fast finality (~1-2 min, $1-1.3 fee, auto-relay)")
|
|
244
|
-
.
|
|
245
|
-
|
|
422
|
+
.addHelpText("after", [
|
|
423
|
+
"",
|
|
424
|
+
"What this moves:",
|
|
425
|
+
...Object.entries(EXCHANGE_TO_CHAIN).map(([ex, chain]) => ` ${ex.padEnd(12)} → ${chain}`),
|
|
426
|
+
" pacifica / lighter: the master wallet's own USDC on Solana / Arbitrum (NOT the exchange account).",
|
|
427
|
+
" Withdraw first (perp funds withdraw ...) and deposit after arrival (perp funds deposit ...).",
|
|
428
|
+
" hyperliquid: HyperCore itself — --from spends the HyperCore spot USDC balance,",
|
|
429
|
+
" --to credits the HyperCore perps balance (CCTP forwarder).",
|
|
430
|
+
"Same as: perp funds bridge send --from <chain> --to <chain> (sender / recipient = the master's own addresses).",
|
|
431
|
+
].join("\n"))
|
|
432
|
+
.action(async (opts, cmd) => {
|
|
433
|
+
const dryRun = isDryRun(cmd);
|
|
434
|
+
// Mainnet-only routes: refuse -n testnet before any key is loaded.
|
|
435
|
+
assertBridgeNetwork(getNetwork());
|
|
246
436
|
const srcExchange = opts.from.toLowerCase();
|
|
247
437
|
const dstExchange = opts.to.toLowerCase();
|
|
248
438
|
const srcChain = EXCHANGE_TO_CHAIN[srcExchange];
|
|
249
439
|
const dstChain = EXCHANGE_TO_CHAIN[dstExchange];
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (
|
|
258
|
-
throw new
|
|
440
|
+
for (const [ex, chain] of [[srcExchange, srcChain], [dstExchange, dstChain]]) {
|
|
441
|
+
if (chain === undefined) {
|
|
442
|
+
throw new PerpError("INVALID_PARAMS", `Unknown exchange for bridging: ${ex}`, {
|
|
443
|
+
remediation: `Use one of: ${Object.keys(EXCHANGE_TO_CHAIN).join(", ")} (see: perp funds bridge chains)`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (srcChain === dstChain) {
|
|
448
|
+
throw new PerpError("INVALID_PARAMS", `${srcExchange} and ${dstExchange} are both on ${srcChain} — no bridge needed`, {
|
|
449
|
+
remediation: "Pick exchanges on different chains (see: perp funds bridge chains)",
|
|
450
|
+
});
|
|
259
451
|
}
|
|
260
|
-
|
|
452
|
+
assertCctpRouteSupported(srcChain, dstChain);
|
|
453
|
+
const chosenProvider = parseProvider(opts.provider);
|
|
454
|
+
const amount = parseUsdcAmount(opts.amount, "--amount"); // exact decimal string (≤ 6 decimals)
|
|
261
455
|
if (!isJson())
|
|
262
456
|
console.log(chalk.cyan(`\n Bridge: $${amount} USDC ${srcExchange} (${srcChain}) → ${dstExchange} (${dstChain})\n`));
|
|
263
|
-
//
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (srcChain === "solana") {
|
|
270
|
-
const pk = await loadPrivateKey("pacifica");
|
|
271
|
-
senderAddress = parseSolanaKeypair(pk).publicKey.toBase58();
|
|
272
|
-
signerKey = pk;
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
const pk = await loadPrivateKey(srcExchange);
|
|
276
|
-
const { ethers } = await import("ethers");
|
|
277
|
-
senderAddress = new ethers.Wallet(pk).address;
|
|
278
|
-
signerKey = pk;
|
|
279
|
-
}
|
|
280
|
-
// Destination
|
|
281
|
-
let dstSignerKey;
|
|
282
|
-
if (dstChain === "solana") {
|
|
283
|
-
const pk = await loadPrivateKey("pacifica");
|
|
284
|
-
recipientAddress = parseSolanaKeypair(pk).publicKey.toBase58();
|
|
285
|
-
dstSignerKey = pk;
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
const pk = await loadPrivateKey(dstExchange);
|
|
289
|
-
const { ethers } = await import("ethers");
|
|
290
|
-
recipientAddress = new ethers.Wallet(pk).address;
|
|
291
|
-
dstSignerKey = pk;
|
|
292
|
-
}
|
|
457
|
+
// The master wallet's own public addresses (OWS: vault account list; nothing is decrypted).
|
|
458
|
+
const credOpts = rootCredOpts(cmd);
|
|
459
|
+
const srcVenue = bridgeVenueForChain(srcChain, srcExchange);
|
|
460
|
+
const dstVenue = bridgeVenueForChain(dstChain, dstExchange);
|
|
461
|
+
const senderAddress = await bridgeAddress(srcChain, srcVenue, credOpts);
|
|
462
|
+
const recipientAddress = await bridgeAddress(dstChain, dstVenue, credOpts);
|
|
293
463
|
const quotes = await getAllQuotes(srcChain, dstChain, amount, senderAddress, recipientAddress);
|
|
294
|
-
|
|
295
|
-
throw new Error(`No bridge available for ${srcChain} → ${dstChain}`);
|
|
296
|
-
let selectedQuote;
|
|
297
|
-
if (chosenProvider) {
|
|
298
|
-
const match = quotes.find(q => q.provider === chosenProvider);
|
|
299
|
-
if (!match)
|
|
300
|
-
throw new Error(`Provider "${chosenProvider}" not available for this route`);
|
|
301
|
-
selectedQuote = match;
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
selectedQuote = quotes[0];
|
|
305
|
-
}
|
|
464
|
+
const selectedQuote = pickQuote(quotes, chosenProvider, srcChain, dstChain);
|
|
306
465
|
const PROVIDER_NAMES = {
|
|
307
466
|
cctp: "Circle CCTP",
|
|
308
467
|
relay: "Relay",
|
|
@@ -312,7 +471,8 @@ export function registerFundsBridgeCommands(parent, isJson) {
|
|
|
312
471
|
const rows = quotes.map((q) => {
|
|
313
472
|
const name = PROVIDER_NAMES[q.provider] ?? q.provider;
|
|
314
473
|
const selected = q.provider === selectedQuote.provider;
|
|
315
|
-
const
|
|
474
|
+
const usdcFee = q.fee <= 0 && !q.nativeFee ? chalk.green("FREE") : `$${formatUsd(q.fee)}`;
|
|
475
|
+
const feeStr = q.nativeFee ? `${usdcFee} + ${q.nativeFee.amount} ${q.nativeFee.symbol}${q.nativeFee.usd !== null ? ` (≈$${formatUsd(q.nativeFee.usd)})` : " (unpriced)"}` : usdcFee;
|
|
316
476
|
const pct = ((q.fee / q.amountIn) * 100).toFixed(2);
|
|
317
477
|
const tag = selected ? chalk.green(" ← selected") : "";
|
|
318
478
|
const gasTag = q.gasIncluded
|
|
@@ -331,39 +491,22 @@ export function registerFundsBridgeCommands(parent, isJson) {
|
|
|
331
491
|
console.log(chalk.yellow(` ⚠ ${selectedQuote.gasNote}`));
|
|
332
492
|
}
|
|
333
493
|
}
|
|
334
|
-
if (
|
|
494
|
+
if (dryRun) {
|
|
335
495
|
if (isJson())
|
|
336
|
-
return printJson(jsonOk({ quotes, selected: selectedQuote.provider }));
|
|
496
|
+
return printJson(jsonOk({ dryRun: true, sent: false, quotes: quotes.map(quoteJson), selected: selectedQuote.provider }));
|
|
337
497
|
console.log(chalk.yellow(" [DRY RUN] Not executed.\n"));
|
|
338
498
|
return;
|
|
339
499
|
}
|
|
340
|
-
// Gas balance preflight check
|
|
500
|
+
// Gas balance preflight check (before any signer is loaded)
|
|
341
501
|
const needsDstGas = !selectedQuote.gasIncluded;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (isJson())
|
|
346
|
-
return printJson({ ok: false, error: { code: "INSUFFICIENT_BALANCE", message: "Insufficient gas for transaction", details: gasCheck.errors }, meta: { timestamp: new Date().toISOString() } });
|
|
347
|
-
console.log(chalk.red("\n Insufficient gas for bridge:\n"));
|
|
348
|
-
console.log(chalk.red(msg));
|
|
349
|
-
if (needsDstGas) {
|
|
350
|
-
console.log(chalk.yellow("\n Tip: use --fast to skip destination gas (Circle auto-relays)\n"));
|
|
351
|
-
}
|
|
352
|
-
process.exitCode = 1;
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
502
|
+
assertBridgeGas(await checkBridgeGasBalance(srcChain, senderAddress, dstChain, recipientAddress, needsDstGas), needsDstGas);
|
|
503
|
+
const signer = await loadBridgeSigner(srcChain, srcVenue, credOpts);
|
|
504
|
+
const dstSigner = await loadBridgeSigner(dstChain, dstVenue, credOpts);
|
|
355
505
|
const providerName = PROVIDER_NAMES[selectedQuote.provider] ?? selectedQuote.provider;
|
|
356
506
|
if (!isJson())
|
|
357
507
|
console.log(chalk.yellow(` Executing via ${providerName}...\n`));
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
result = await executeBestBridge(srcChain, dstChain, amount, signerKey, senderAddress, recipientAddress, dstSignerKey, chosenProvider, !!opts.fast);
|
|
361
|
-
logExecution({ type: "bridge", exchange: "bridge", symbol: "USDC", side: `${srcExchange}->${dstExchange}`, size: String(amount), status: "success", dryRun: false, meta: { provider: result.provider, txHash: result.txHash, fast: !!opts.fast } });
|
|
362
|
-
}
|
|
363
|
-
catch (err) {
|
|
364
|
-
logExecution({ type: "bridge", exchange: "bridge", symbol: "USDC", side: `${srcExchange}->${dstExchange}`, size: String(amount), status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err) });
|
|
365
|
-
throw err;
|
|
366
|
-
}
|
|
508
|
+
// The provider shown as "selected" is the one executed.
|
|
509
|
+
const result = await executeLogged(`${srcExchange}->${dstExchange}`, amount, !!opts.fast, () => executeBestBridge(getNetwork(), srcChain, dstChain, amount, signer, senderAddress, recipientAddress, dstSigner, selectedQuote.provider, !!opts.fast));
|
|
367
510
|
if (isJson())
|
|
368
511
|
return printJson(jsonOk(result));
|
|
369
512
|
console.log(chalk.green(" Bridge submitted!"));
|
|
@@ -371,10 +514,14 @@ export function registerFundsBridgeCommands(parent, isJson) {
|
|
|
371
514
|
console.log(` Provider: ${result.provider}`);
|
|
372
515
|
if (result.receiveTxHash) {
|
|
373
516
|
console.log(` Receive: ${result.receiveTxHash} (${dstChain})`);
|
|
374
|
-
console.log(chalk.green(` USDC arrived on ${dstExchange}!`));
|
|
375
517
|
}
|
|
376
|
-
|
|
377
|
-
|
|
518
|
+
if (dstChain === "hyperliquid") {
|
|
519
|
+
console.log(chalk.gray(`\n USDC is credited to the HyperCore perps balance on arrival.\n`));
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
console.log(chalk.gray(`\n This moved wallet USDC on ${dstChain} only — the ${dstExchange} account is not funded yet.`));
|
|
523
|
+
console.log(chalk.gray(` After arrival, deposit separately: perp funds deposit ${dstExchange} --help\n`));
|
|
524
|
+
}
|
|
378
525
|
});
|
|
379
526
|
// ── bridge status ──
|
|
380
527
|
bridge
|