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.
Files changed (363) hide show
  1. package/README.md +247 -27
  2. package/dist/_stderr-filter.js +0 -1
  3. package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
  4. package/dist/agent-wallet/lighter-keystore.js +92 -10
  5. package/dist/agent-wallet/passphrase.d.ts +1 -1
  6. package/dist/agent-wallet/passphrase.js +16 -3
  7. package/dist/agent-wallet/store.d.ts +37 -4
  8. package/dist/agent-wallet/store.js +132 -0
  9. package/dist/agent-wallet/venue-keystore.d.ts +14 -0
  10. package/dist/agent-wallet/venue-keystore.js +121 -0
  11. package/dist/api/public/hyperliquid.js +4 -4
  12. package/dist/api/public/lighter.js +5 -5
  13. package/dist/api/public/pacifica.d.ts +0 -1
  14. package/dist/api/public/pacifica.js +4 -13
  15. package/dist/api/public/urls.d.ts +9 -1
  16. package/dist/api/public/urls.js +35 -13
  17. package/dist/arb/file-lock.d.ts +11 -0
  18. package/dist/arb/file-lock.js +74 -0
  19. package/dist/arb/index.d.ts +2 -2
  20. package/dist/arb/index.js +2 -2
  21. package/dist/arb/pending-entry.d.ts +32 -0
  22. package/dist/arb/pending-entry.js +77 -0
  23. package/dist/arb/sizing.d.ts +6 -13
  24. package/dist/arb/sizing.js +12 -51
  25. package/dist/arb/state.d.ts +45 -12
  26. package/dist/arb/state.js +214 -42
  27. package/dist/arb/utils.d.ts +20 -6
  28. package/dist/arb/utils.js +43 -39
  29. package/dist/bot/conditions.d.ts +4 -4
  30. package/dist/bot/conditions.js +11 -6
  31. package/dist/bot/config.d.ts +17 -1
  32. package/dist/bot/config.js +48 -12
  33. package/dist/bot/engine.d.ts +61 -2
  34. package/dist/bot/engine.js +610 -144
  35. package/dist/bot/fill-size.d.ts +7 -0
  36. package/dist/bot/fill-size.js +18 -0
  37. package/dist/bot/index.d.ts +1 -1
  38. package/dist/bot/index.js +1 -1
  39. package/dist/bot/perp-order.d.ts +99 -0
  40. package/dist/bot/perp-order.js +299 -0
  41. package/dist/bot/presets.d.ts +7 -0
  42. package/dist/bot/presets.js +17 -0
  43. package/dist/bot/quarantine.d.ts +75 -0
  44. package/dist/bot/quarantine.js +190 -0
  45. package/dist/bot/quote-rules.d.ts +23 -0
  46. package/dist/bot/quote-rules.js +69 -0
  47. package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
  48. package/dist/bot/strategies/avellaneda-mm.js +72 -70
  49. package/dist/bot/strategies/basis-arb.js +2 -0
  50. package/dist/bot/strategies/dca-strategy.d.ts +2 -0
  51. package/dist/bot/strategies/dca-strategy.js +5 -0
  52. package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
  53. package/dist/bot/strategies/funding-arb-strategy.js +571 -380
  54. package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
  55. package/dist/bot/strategies/funding-arb-utils.js +159 -195
  56. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  57. package/dist/bot/strategies/funding-arb-v2.js +258 -47
  58. package/dist/bot/strategies/funding-auto.js +215 -194
  59. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
  60. package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
  61. package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
  62. package/dist/bot/strategies/spot-perp-executor.js +319 -120
  63. package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
  64. package/dist/bot/strategies/spot-perp-state.js +70 -0
  65. package/dist/bot/strategy-types.d.ts +6 -0
  66. package/dist/bot/tui/BotDashboard.d.ts +7 -2
  67. package/dist/bot/tui/BotDashboard.js +6 -3
  68. package/dist/bridge-engine.d.ts +120 -19
  69. package/dist/bridge-engine.js +710 -422
  70. package/dist/bridge-signers.d.ts +54 -0
  71. package/dist/bridge-signers.js +161 -0
  72. package/dist/cache.d.ts +4 -1
  73. package/dist/cache.js +11 -0
  74. package/dist/cli-flags.d.ts +50 -0
  75. package/dist/cli-flags.js +104 -0
  76. package/dist/cli-program.d.ts +35 -0
  77. package/dist/cli-program.js +303 -0
  78. package/dist/cli-spec.js +1 -1
  79. package/dist/client-id-tracker.js +11 -2
  80. package/dist/commands/account.d.ts +7 -1
  81. package/dist/commands/account.js +70 -92
  82. package/dist/commands/agent-import.d.ts +114 -0
  83. package/dist/commands/agent-import.js +794 -0
  84. package/dist/commands/agent.d.ts +232 -2
  85. package/dist/commands/agent.js +1858 -955
  86. package/dist/commands/alerts.d.ts +1 -0
  87. package/dist/commands/alerts.js +31 -28
  88. package/dist/commands/arb/index.d.ts +60 -1
  89. package/dist/commands/arb/index.js +381 -384
  90. package/dist/commands/arb-auto.d.ts +132 -4
  91. package/dist/commands/arb-auto.js +1474 -967
  92. package/dist/commands/arb.d.ts +3 -1
  93. package/dist/commands/arb.js +62 -30
  94. package/dist/commands/bot.js +127 -47
  95. package/dist/commands/bridge.d.ts +2 -1
  96. package/dist/commands/bridge.js +339 -192
  97. package/dist/commands/builder.d.ts +55 -0
  98. package/dist/commands/builder.js +387 -0
  99. package/dist/commands/funds-onchain.d.ts +32 -0
  100. package/dist/commands/funds-onchain.js +248 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +925 -245
  103. package/dist/commands/history.js +102 -54
  104. package/dist/commands/init.d.ts +107 -15
  105. package/dist/commands/init.js +1116 -249
  106. package/dist/commands/jobs.d.ts +2 -1
  107. package/dist/commands/jobs.js +77 -15
  108. package/dist/commands/manage.js +136 -91
  109. package/dist/commands/market.d.ts +4 -1
  110. package/dist/commands/market.js +35 -32
  111. package/dist/commands/outcome.d.ts +10 -0
  112. package/dist/commands/outcome.js +396 -0
  113. package/dist/commands/plan.js +52 -12
  114. package/dist/commands/portfolio.d.ts +1 -0
  115. package/dist/commands/portfolio.js +60 -72
  116. package/dist/commands/rebalance.d.ts +2 -1
  117. package/dist/commands/rebalance.js +33 -43
  118. package/dist/commands/risk.js +46 -19
  119. package/dist/commands/settings.d.ts +1 -0
  120. package/dist/commands/settings.js +246 -51
  121. package/dist/commands/trade.d.ts +50 -0
  122. package/dist/commands/trade.js +1943 -695
  123. package/dist/commands/wallet.d.ts +30 -1
  124. package/dist/commands/wallet.js +357 -459
  125. package/dist/config.d.ts +60 -9
  126. package/dist/config.js +252 -82
  127. package/dist/constants.d.ts +29 -4
  128. package/dist/constants.js +62 -20
  129. package/dist/dashboard/server.d.ts +2 -0
  130. package/dist/dashboard/server.js +9 -6
  131. package/dist/dashboard/ui.js +6 -4
  132. package/dist/dashboard/ws-feeds.js +20 -8
  133. package/dist/dex-asset-map.js +5 -3
  134. package/dist/errors.d.ts +122 -0
  135. package/dist/errors.js +194 -0
  136. package/dist/event-stream.js +75 -19
  137. package/dist/evm-master-tx.d.ts +59 -0
  138. package/dist/evm-master-tx.js +154 -0
  139. package/dist/exchanges/adapter-network.d.ts +10 -0
  140. package/dist/exchanges/adapter-network.js +10 -0
  141. package/dist/exchanges/arcus/adapter.d.ts +196 -0
  142. package/dist/exchanges/arcus/adapter.js +1092 -0
  143. package/dist/exchanges/arcus/agent.d.ts +55 -0
  144. package/dist/exchanges/arcus/agent.js +188 -0
  145. package/dist/exchanges/arcus/client.d.ts +63 -0
  146. package/dist/exchanges/arcus/client.js +285 -0
  147. package/dist/exchanges/arcus/public.d.ts +65 -0
  148. package/dist/exchanges/arcus/public.js +138 -0
  149. package/dist/exchanges/arcus/signing.d.ts +176 -0
  150. package/dist/exchanges/arcus/signing.js +288 -0
  151. package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
  152. package/dist/exchanges/arcus/spot-chain.js +241 -0
  153. package/dist/exchanges/arcus/spot-router.d.ts +177 -0
  154. package/dist/exchanges/arcus/spot-router.js +183 -0
  155. package/dist/exchanges/arcus/spot.d.ts +109 -0
  156. package/dist/exchanges/arcus/spot.js +645 -0
  157. package/dist/exchanges/aster-funds.d.ts +127 -0
  158. package/dist/exchanges/aster-funds.js +252 -0
  159. package/dist/exchanges/aster-nonce.d.ts +2 -0
  160. package/dist/exchanges/aster-nonce.js +107 -0
  161. package/dist/exchanges/aster-spot.d.ts +117 -0
  162. package/dist/exchanges/aster-spot.js +686 -0
  163. package/dist/exchanges/aster-typed-data.d.ts +133 -7
  164. package/dist/exchanges/aster-typed-data.js +121 -7
  165. package/dist/exchanges/aster.d.ts +240 -6
  166. package/dist/exchanges/aster.js +1139 -195
  167. package/dist/exchanges/builder-fee.d.ts +254 -0
  168. package/dist/exchanges/builder-fee.js +421 -0
  169. package/dist/exchanges/canonical-symbols.d.ts +32 -0
  170. package/dist/exchanges/canonical-symbols.js +96 -0
  171. package/dist/exchanges/capabilities.d.ts +7 -1
  172. package/dist/exchanges/capabilities.js +7 -0
  173. package/dist/exchanges/hl-signing.d.ts +165 -0
  174. package/dist/exchanges/hl-signing.js +158 -0
  175. package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
  176. package/dist/exchanges/hyperliquid-outcome.js +646 -0
  177. package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
  178. package/dist/exchanges/hyperliquid-spot.js +308 -117
  179. package/dist/exchanges/hyperliquid.d.ts +462 -57
  180. package/dist/exchanges/hyperliquid.js +1575 -449
  181. package/dist/exchanges/index.d.ts +6 -0
  182. package/dist/exchanges/index.js +4 -0
  183. package/dist/exchanges/interface.d.ts +63 -10
  184. package/dist/exchanges/interface.js +18 -3
  185. package/dist/exchanges/ioc-outcome.d.ts +44 -0
  186. package/dist/exchanges/ioc-outcome.js +57 -0
  187. package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
  188. package/dist/exchanges/legacy-trading-v2.js +940 -0
  189. package/dist/exchanges/lighter-public.d.ts +43 -0
  190. package/dist/exchanges/lighter-public.js +276 -0
  191. package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
  192. package/dist/exchanges/lighter-rh-deposit.js +185 -0
  193. package/dist/exchanges/lighter-signer.d.ts +123 -0
  194. package/dist/exchanges/lighter-signer.js +215 -0
  195. package/dist/exchanges/lighter-spot.d.ts +91 -13
  196. package/dist/exchanges/lighter-spot.js +595 -233
  197. package/dist/exchanges/lighter.d.ts +611 -39
  198. package/dist/exchanges/lighter.js +1890 -649
  199. package/dist/exchanges/ondo/adapter.d.ts +152 -0
  200. package/dist/exchanges/ondo/adapter.js +811 -0
  201. package/dist/exchanges/ondo/agent.d.ts +31 -0
  202. package/dist/exchanges/ondo/agent.js +101 -0
  203. package/dist/exchanges/ondo/client.d.ts +60 -0
  204. package/dist/exchanges/ondo/client.js +297 -0
  205. package/dist/exchanges/ondo/funds.d.ts +95 -0
  206. package/dist/exchanges/ondo/funds.js +198 -0
  207. package/dist/exchanges/ondo/public.d.ts +95 -0
  208. package/dist/exchanges/ondo/public.js +214 -0
  209. package/dist/exchanges/ondo/spot.d.ts +127 -0
  210. package/dist/exchanges/ondo/spot.js +392 -0
  211. package/dist/exchanges/outcome-interface.d.ts +140 -0
  212. package/dist/exchanges/outcome-interface.js +16 -0
  213. package/dist/exchanges/pacifica-spot.d.ts +83 -0
  214. package/dist/exchanges/pacifica-spot.js +324 -0
  215. package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
  216. package/dist/exchanges/pacifica-typed-data.js +57 -9
  217. package/dist/exchanges/pacifica.d.ts +293 -4
  218. package/dist/exchanges/pacifica.js +1044 -90
  219. package/dist/exchanges/precision.d.ts +47 -0
  220. package/dist/exchanges/precision.js +209 -0
  221. package/dist/exchanges/qfex/adapter.d.ts +341 -0
  222. package/dist/exchanges/qfex/adapter.js +1386 -0
  223. package/dist/exchanges/qfex/agent.d.ts +58 -0
  224. package/dist/exchanges/qfex/agent.js +188 -0
  225. package/dist/exchanges/qfex/client.d.ts +54 -0
  226. package/dist/exchanges/qfex/client.js +138 -0
  227. package/dist/exchanges/qfex/errors.d.ts +70 -0
  228. package/dist/exchanges/qfex/errors.js +236 -0
  229. package/dist/exchanges/qfex/public.d.ts +122 -0
  230. package/dist/exchanges/qfex/public.js +268 -0
  231. package/dist/exchanges/qfex/signing.d.ts +160 -0
  232. package/dist/exchanges/qfex/signing.js +283 -0
  233. package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
  234. package/dist/exchanges/qfex/ws-transport.js +798 -0
  235. package/dist/exchanges/rate-limiter.d.ts +12 -0
  236. package/dist/exchanges/rate-limiter.js +64 -0
  237. package/dist/exchanges/registry.d.ts +14 -2
  238. package/dist/exchanges/registry.js +20 -32
  239. package/dist/exchanges/slippage.d.ts +37 -0
  240. package/dist/exchanges/slippage.js +70 -0
  241. package/dist/exchanges/spot-factory.d.ts +166 -0
  242. package/dist/exchanges/spot-factory.js +431 -0
  243. package/dist/exchanges/spot-interface.d.ts +98 -4
  244. package/dist/exchanges/spot-interface.js +27 -1
  245. package/dist/exchanges/spot-result.d.ts +79 -0
  246. package/dist/exchanges/spot-result.js +273 -0
  247. package/dist/exchanges/trading-v2.d.ts +117 -0
  248. package/dist/exchanges/trading-v2.js +301 -0
  249. package/dist/exchanges/venue-types.d.ts +231 -0
  250. package/dist/exchanges/venue-types.js +87 -0
  251. package/dist/exchanges/venue.d.ts +211 -0
  252. package/dist/exchanges/venue.js +132 -0
  253. package/dist/exchanges/venues/arcus.d.ts +8 -0
  254. package/dist/exchanges/venues/arcus.js +109 -0
  255. package/dist/exchanges/venues/aster.d.ts +49 -0
  256. package/dist/exchanges/venues/aster.js +176 -0
  257. package/dist/exchanges/venues/create-helpers.d.ts +18 -0
  258. package/dist/exchanges/venues/create-helpers.js +199 -0
  259. package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
  260. package/dist/exchanges/venues/hyperliquid.js +126 -0
  261. package/dist/exchanges/venues/index.d.ts +16 -0
  262. package/dist/exchanges/venues/index.js +26 -0
  263. package/dist/exchanges/venues/lighter-family.d.ts +92 -0
  264. package/dist/exchanges/venues/lighter-family.js +166 -0
  265. package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
  266. package/dist/exchanges/venues/lighter-rh.js +58 -0
  267. package/dist/exchanges/venues/lighter.d.ts +6 -0
  268. package/dist/exchanges/venues/lighter.js +55 -0
  269. package/dist/exchanges/venues/ondo.d.ts +29 -0
  270. package/dist/exchanges/venues/ondo.js +136 -0
  271. package/dist/exchanges/venues/pacifica.d.ts +8 -0
  272. package/dist/exchanges/venues/pacifica.js +95 -0
  273. package/dist/exchanges/venues/qfex.d.ts +39 -0
  274. package/dist/exchanges/venues/qfex.js +140 -0
  275. package/dist/execution-log.js +10 -2
  276. package/dist/funding/legacy-public.d.ts +31 -0
  277. package/dist/funding/legacy-public.js +369 -0
  278. package/dist/funding/normalize.d.ts +28 -16
  279. package/dist/funding/normalize.js +61 -29
  280. package/dist/funding/rates.d.ts +46 -11
  281. package/dist/funding/rates.js +228 -277
  282. package/dist/guardrail/perp-guardrail.js +0 -0
  283. package/dist/index.js +156 -684
  284. package/dist/jobs.d.ts +84 -6
  285. package/dist/jobs.js +239 -41
  286. package/dist/landing.d.ts +7 -2
  287. package/dist/landing.js +19 -3
  288. package/dist/mcp-server.d.ts +14 -4
  289. package/dist/mcp-server.js +821 -403
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/onchain-balances.d.ts +40 -0
  293. package/dist/onchain-balances.js +173 -0
  294. package/dist/pacifica/client.d.ts +41 -10
  295. package/dist/pacifica/client.js +167 -63
  296. package/dist/pacifica/constants.d.ts +0 -1
  297. package/dist/pacifica/constants.js +0 -5
  298. package/dist/pacifica/deposit.d.ts +30 -3
  299. package/dist/pacifica/deposit.js +119 -2
  300. package/dist/pacifica/faucet.d.ts +29 -0
  301. package/dist/pacifica/faucet.js +100 -0
  302. package/dist/pacifica/sign-tx.d.ts +15 -0
  303. package/dist/pacifica/sign-tx.js +41 -0
  304. package/dist/pacifica/types/account.d.ts +28 -4
  305. package/dist/pacifica/types/lake.d.ts +20 -5
  306. package/dist/pacifica/types/lake.js +4 -0
  307. package/dist/pacifica/types/market.d.ts +5 -0
  308. package/dist/pacifica/types/order.d.ts +4 -1
  309. package/dist/pacifica/types/position.d.ts +9 -1
  310. package/dist/plan-executor.d.ts +10 -5
  311. package/dist/plan-executor.js +165 -54
  312. package/dist/position-history.js +13 -0
  313. package/dist/rebalance.d.ts +26 -2
  314. package/dist/rebalance.js +86 -7
  315. package/dist/relay-swap.d.ts +232 -0
  316. package/dist/relay-swap.js +630 -0
  317. package/dist/risk.js +5 -1
  318. package/dist/secret-env.d.ts +15 -0
  319. package/dist/secret-env.js +36 -0
  320. package/dist/settings.d.ts +169 -6
  321. package/dist/settings.js +219 -38
  322. package/dist/signer/ows-evm.d.ts +13 -0
  323. package/dist/signer/ows-evm.js +22 -0
  324. package/dist/smart-order.d.ts +9 -0
  325. package/dist/smart-order.js +53 -8
  326. package/dist/strategies/dca.d.ts +6 -2
  327. package/dist/strategies/dca.js +66 -26
  328. package/dist/strategies/funding-arb.d.ts +11 -1
  329. package/dist/strategies/funding-arb.js +202 -89
  330. package/dist/strategies/grid.d.ts +31 -0
  331. package/dist/strategies/grid.js +89 -11
  332. package/dist/strategies/pending-stop.d.ts +8 -0
  333. package/dist/strategies/pending-stop.js +12 -0
  334. package/dist/strategies/split-order.d.ts +45 -17
  335. package/dist/strategies/split-order.js +157 -91
  336. package/dist/strategies/trailing-stop.d.ts +2 -1
  337. package/dist/strategies/trailing-stop.js +52 -6
  338. package/dist/strategies/twap.d.ts +6 -1
  339. package/dist/strategies/twap.js +39 -11
  340. package/dist/trade-validator.d.ts +22 -0
  341. package/dist/trade-validator.js +119 -15
  342. package/dist/utils/numeric.d.ts +46 -0
  343. package/dist/utils/numeric.js +79 -0
  344. package/dist/utils.d.ts +19 -0
  345. package/dist/utils.js +96 -19
  346. package/dist/withdraw-gate.d.ts +49 -0
  347. package/dist/withdraw-gate.js +131 -0
  348. package/package.json +15 -6
  349. package/skills/perp-cli/SKILL.md +159 -55
  350. package/skills/perp-cli/references/agent-operations.md +112 -58
  351. package/skills/perp-cli/references/commands.md +269 -73
  352. package/skills/perp-cli/references/strategies.md +34 -31
  353. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  354. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  355. package/skills/perp-cli/scripts/preflight.sh +21 -14
  356. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  357. package/skills/perp-cli/scripts/validate-arb.sh +31 -15
  358. package/vendor/lighter-signer/SHA256SUMS +2 -0
  359. package/vendor/lighter-signer/VERSION +8 -0
  360. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  361. package/vendor/lighter-signer/wasm_exec.js +575 -0
  362. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  363. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
@@ -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
- export function registerFundsBridgeCommands(parent, isJson) {
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.gray("\n Bridge powered by deBridge DLN (~2s settlement)\n"));
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
- const amount = parseFloat(opts.amount);
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, opts.sender ?? "0x0000000000000000000000000000000000000000", opts.recipient ?? "0x0000000000000000000000000000000000000000");
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
- console.log(chalk.red(" No providers available for this route.\n"));
49
- return;
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 feeStr = q.fee <= 0 ? chalk.green("FREE") : `$${formatUsd(q.fee)}`;
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
- .command("send")
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 amount")
92
- .option("--sender <address>", "Source address (auto-detected from key)")
93
- .option("--recipient <address>", "Destination address")
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
- .option("--dry-run", "Show quotes without executing")
97
- .action(async (opts) => {
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
- const amount = parseFloat(opts.amount);
101
- if (isNaN(amount) || amount <= 0)
102
- throw new Error("Invalid amount");
103
- const chosenProvider = opts.provider?.toLowerCase();
104
- if (chosenProvider && !["cctp", "relay", "debridge"].includes(chosenProvider)) {
105
- throw new Error(`Invalid provider: ${opts.provider}. Use cctp, relay, or debridge.`);
106
- }
107
- // Load the appropriate private key
108
- const { loadPrivateKey, parseSolanaKeypair } = await import("../config.js");
109
- let senderAddress;
110
- let recipientAddress;
111
- let signerKey;
112
- if (srcChain === "solana") {
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
- if (quotes.length === 0)
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 feeStr = q.fee <= 0 ? chalk.green("FREE") : `$${formatUsd(q.fee)}`;
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 (opts.dryRun) {
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
- // Gas balance preflight check
199
- const needsDstGas = !selectedQuote.gasIncluded;
200
- const gasCheck = await checkBridgeGasBalance(srcChain, senderAddress, dstChain, recipientAddress, needsDstGas);
201
- if (!gasCheck.ok) {
202
- const msg = gasCheck.errors.map(e => ` ✗ ${e}`).join("\n");
203
- if (isJson())
204
- return printJson({ ok: false, error: { code: "INSUFFICIENT_BALANCE", message: "Insufficient gas for transaction", details: gasCheck.errors }, meta: { timestamp: new Date().toISOString() } });
205
- console.log(chalk.red("\n Insufficient gas for bridge:\n"));
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
- let result;
217
- try {
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
- .command("exchange")
238
- .description("Bridge USDC between exchanges (shortcut)")
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
- .option("--dry-run", "Show quotes without executing")
245
- .action(async (opts) => {
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
- if (!srcChain)
251
- throw new Error(`Unknown exchange: ${srcExchange}`);
252
- if (!dstChain)
253
- throw new Error(`Unknown exchange: ${dstExchange}`);
254
- if (srcChain === dstChain)
255
- throw new Error("Same chain — no bridge needed");
256
- const chosenProvider = opts.provider?.toLowerCase();
257
- if (chosenProvider && !["cctp", "relay", "debridge"].includes(chosenProvider)) {
258
- throw new Error(`Invalid provider: ${opts.provider}. Use cctp, relay, or debridge.`);
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
- const amount = parseFloat(opts.amount);
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
- // Load keys
264
- const { loadPrivateKey, parseSolanaKeypair } = await import("../config.js");
265
- let senderAddress;
266
- let recipientAddress;
267
- let signerKey;
268
- // Source
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
- if (quotes.length === 0)
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 feeStr = q.fee <= 0 ? chalk.green("FREE") : `$${formatUsd(q.fee)}`;
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 (opts.dryRun) {
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
- const gasCheck = await checkBridgeGasBalance(srcChain, senderAddress, dstChain, recipientAddress, needsDstGas);
343
- if (!gasCheck.ok) {
344
- const msg = gasCheck.errors.map(e => ` ✗ ${e}`).join("\n");
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
- let result;
359
- try {
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
- console.log(chalk.gray(`\n After arrival, deposit into ${dstExchange}:`));
377
- console.log(chalk.gray(` perp funds deposit ${dstExchange} ${opts.amount}\n`));
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