perp-cli 0.3.3
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/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/__tests__/alert-logic.test.d.ts +1 -0
- package/dist/__tests__/alert-logic.test.js +107 -0
- package/dist/__tests__/arb-auto-3dex.test.d.ts +1 -0
- package/dist/__tests__/arb-auto-3dex.test.js +397 -0
- package/dist/__tests__/arb-history-stats.test.d.ts +1 -0
- package/dist/__tests__/arb-history-stats.test.js +176 -0
- package/dist/__tests__/arb-logic.test.d.ts +1 -0
- package/dist/__tests__/arb-logic.test.js +84 -0
- package/dist/__tests__/arb-manage.test.d.ts +1 -0
- package/dist/__tests__/arb-manage.test.js +253 -0
- package/dist/__tests__/arb-new-features.test.d.ts +1 -0
- package/dist/__tests__/arb-new-features.test.js +457 -0
- package/dist/__tests__/arb-sizing.test.d.ts +1 -0
- package/dist/__tests__/arb-sizing.test.js +48 -0
- package/dist/__tests__/arb-state.test.d.ts +1 -0
- package/dist/__tests__/arb-state.test.js +284 -0
- package/dist/__tests__/arb-userflow.test.d.ts +1 -0
- package/dist/__tests__/arb-userflow.test.js +945 -0
- package/dist/__tests__/arb-utils.test.d.ts +1 -0
- package/dist/__tests__/arb-utils.test.js +264 -0
- package/dist/__tests__/bot-conditions.test.d.ts +1 -0
- package/dist/__tests__/bot-conditions.test.js +341 -0
- package/dist/__tests__/client-id-tracker.test.d.ts +1 -0
- package/dist/__tests__/client-id-tracker.test.js +137 -0
- package/dist/__tests__/commands/new-atomic-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/new-atomic-commands.test.js +502 -0
- package/dist/__tests__/commands/order-intent.test.d.ts +1 -0
- package/dist/__tests__/commands/order-intent.test.js +600 -0
- package/dist/__tests__/commands/trade-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/trade-commands.test.js +821 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +86 -0
- package/dist/__tests__/cross-chain-margin.test.d.ts +1 -0
- package/dist/__tests__/cross-chain-margin.test.js +287 -0
- package/dist/__tests__/dex-asset-map.test.d.ts +1 -0
- package/dist/__tests__/dex-asset-map.test.js +191 -0
- package/dist/__tests__/errors.test.d.ts +1 -0
- package/dist/__tests__/errors.test.js +110 -0
- package/dist/__tests__/event-stream.test.d.ts +1 -0
- package/dist/__tests__/event-stream.test.js +276 -0
- package/dist/__tests__/exchanges/interface.test.d.ts +1 -0
- package/dist/__tests__/exchanges/interface.test.js +132 -0
- package/dist/__tests__/exchanges/mock-adapter.d.ts +69 -0
- package/dist/__tests__/exchanges/mock-adapter.js +137 -0
- package/dist/__tests__/execution-log.test.d.ts +1 -0
- package/dist/__tests__/execution-log.test.js +106 -0
- package/dist/__tests__/funding-calc.test.d.ts +1 -0
- package/dist/__tests__/funding-calc.test.js +71 -0
- package/dist/__tests__/funding-history.test.d.ts +1 -0
- package/dist/__tests__/funding-history.test.js +343 -0
- package/dist/__tests__/funding-rates.test.d.ts +1 -0
- package/dist/__tests__/funding-rates.test.js +342 -0
- package/dist/__tests__/funding.test.d.ts +1 -0
- package/dist/__tests__/funding.test.js +173 -0
- package/dist/__tests__/gap-logic.test.d.ts +1 -0
- package/dist/__tests__/gap-logic.test.js +43 -0
- package/dist/__tests__/hip3-dex.test.d.ts +1 -0
- package/dist/__tests__/hip3-dex.test.js +234 -0
- package/dist/__tests__/integration/agent-features.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/agent-features.integration.test.js +553 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.js +246 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.js +453 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.js +812 -0
- package/dist/__tests__/integration/bridge.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge.integration.test.js +309 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.js +202 -0
- package/dist/__tests__/integration/dex-arb.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/dex-arb.integration.test.js +116 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.js +205 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.js +147 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.js +79 -0
- package/dist/__tests__/integration/lighter.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/lighter.integration.test.js +53 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +9 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.js +236 -0
- package/dist/__tests__/integration/order-verification.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/order-verification.integration.test.js +321 -0
- package/dist/__tests__/integration/pacifica.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/pacifica.integration.test.js +75 -0
- package/dist/__tests__/integration/response-shapes.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/response-shapes.integration.test.js +278 -0
- package/dist/__tests__/liquidity.test.d.ts +1 -0
- package/dist/__tests__/liquidity.test.js +225 -0
- package/dist/__tests__/plan-executor.test.d.ts +1 -0
- package/dist/__tests__/plan-executor.test.js +314 -0
- package/dist/__tests__/position-history.test.d.ts +1 -0
- package/dist/__tests__/position-history.test.js +367 -0
- package/dist/__tests__/retry.test.d.ts +1 -0
- package/dist/__tests__/retry.test.js +310 -0
- package/dist/__tests__/risk-assessment.test.d.ts +1 -0
- package/dist/__tests__/risk-assessment.test.js +145 -0
- package/dist/__tests__/security-adversarial.test.d.ts +1 -0
- package/dist/__tests__/security-adversarial.test.js +574 -0
- package/dist/__tests__/strategies.test.d.ts +1 -0
- package/dist/__tests__/strategies.test.js +539 -0
- package/dist/__tests__/trade-execution.test.d.ts +1 -0
- package/dist/__tests__/trade-execution.test.js +129 -0
- package/dist/__tests__/trade-validator.test.d.ts +1 -0
- package/dist/__tests__/trade-validator.test.js +655 -0
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +76 -0
- package/dist/api/public/hyperliquid.d.ts +18 -0
- package/dist/api/public/hyperliquid.js +82 -0
- package/dist/api/public/index.d.ts +8 -0
- package/dist/api/public/index.js +8 -0
- package/dist/api/public/lighter.d.ts +24 -0
- package/dist/api/public/lighter.js +100 -0
- package/dist/api/public/pacifica.d.ts +17 -0
- package/dist/api/public/pacifica.js +54 -0
- package/dist/api/public/urls.d.ts +12 -0
- package/dist/api/public/urls.js +33 -0
- package/dist/arb/history-stats.d.ts +44 -0
- package/dist/arb/history-stats.js +135 -0
- package/dist/arb/index.d.ts +4 -0
- package/dist/arb/index.js +4 -0
- package/dist/arb/sizing.d.ts +23 -0
- package/dist/arb/sizing.js +96 -0
- package/dist/arb/state.d.ts +51 -0
- package/dist/arb/state.js +112 -0
- package/dist/arb/utils.d.ts +81 -0
- package/dist/arb/utils.js +267 -0
- package/dist/arb-history-stats.d.ts +5 -0
- package/dist/arb-history-stats.js +5 -0
- package/dist/arb-sizing.d.ts +5 -0
- package/dist/arb-sizing.js +5 -0
- package/dist/arb-state.d.ts +5 -0
- package/dist/arb-state.js +5 -0
- package/dist/arb-utils.d.ts +5 -0
- package/dist/arb-utils.js +5 -0
- package/dist/bot/conditions.d.ts +32 -0
- package/dist/bot/conditions.js +141 -0
- package/dist/bot/config.d.ts +76 -0
- package/dist/bot/config.js +160 -0
- package/dist/bot/engine.d.ts +8 -0
- package/dist/bot/engine.js +519 -0
- package/dist/bot/presets.d.ts +11 -0
- package/dist/bot/presets.js +296 -0
- package/dist/bridge-engine.d.ts +133 -0
- package/dist/bridge-engine.js +1487 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.js +99 -0
- package/dist/cli-spec.d.ts +50 -0
- package/dist/cli-spec.js +75 -0
- package/dist/client-id-tracker.d.ts +25 -0
- package/dist/client-id-tracker.js +76 -0
- package/dist/commands/account.d.ts +3 -0
- package/dist/commands/account.js +425 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +386 -0
- package/dist/commands/alert.d.ts +2 -0
- package/dist/commands/alert.js +421 -0
- package/dist/commands/analytics.d.ts +3 -0
- package/dist/commands/analytics.js +311 -0
- package/dist/commands/arb/index.d.ts +3 -0
- package/dist/commands/arb/index.js +921 -0
- package/dist/commands/arb-auto.d.ts +54 -0
- package/dist/commands/arb-auto.js +1328 -0
- package/dist/commands/arb-manage.d.ts +5 -0
- package/dist/commands/arb-manage.js +5 -0
- package/dist/commands/arb.d.ts +2 -0
- package/dist/commands/arb.js +347 -0
- package/dist/commands/backtest.d.ts +2 -0
- package/dist/commands/backtest.js +327 -0
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +412 -0
- package/dist/commands/bridge.d.ts +2 -0
- package/dist/commands/bridge.js +396 -0
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.js +176 -0
- package/dist/commands/deposit.d.ts +4 -0
- package/dist/commands/deposit.js +573 -0
- package/dist/commands/dex.d.ts +3 -0
- package/dist/commands/dex.js +114 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +136 -0
- package/dist/commands/funding.d.ts +2 -0
- package/dist/commands/funding.js +347 -0
- package/dist/commands/gap.d.ts +2 -0
- package/dist/commands/gap.js +305 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +67 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +235 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +266 -0
- package/dist/commands/jobs.d.ts +2 -0
- package/dist/commands/jobs.js +133 -0
- package/dist/commands/manage.d.ts +4 -0
- package/dist/commands/manage.js +309 -0
- package/dist/commands/market.d.ts +3 -0
- package/dist/commands/market.js +225 -0
- package/dist/commands/plan.d.ts +3 -0
- package/dist/commands/plan.js +95 -0
- package/dist/commands/portfolio.d.ts +3 -0
- package/dist/commands/portfolio.js +169 -0
- package/dist/commands/rebalance.d.ts +3 -0
- package/dist/commands/rebalance.js +293 -0
- package/dist/commands/risk.d.ts +3 -0
- package/dist/commands/risk.js +169 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +202 -0
- package/dist/commands/settings.d.ts +2 -0
- package/dist/commands/settings.js +102 -0
- package/dist/commands/stream.d.ts +5 -0
- package/dist/commands/stream.js +123 -0
- package/dist/commands/trade.d.ts +3 -0
- package/dist/commands/trade.js +1273 -0
- package/dist/commands/wallet.d.ts +14 -0
- package/dist/commands/wallet.js +602 -0
- package/dist/commands/withdraw.d.ts +3 -0
- package/dist/commands/withdraw.js +187 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +68 -0
- package/dist/cross-chain-margin.d.ts +46 -0
- package/dist/cross-chain-margin.js +107 -0
- package/dist/dashboard/server.d.ts +80 -0
- package/dist/dashboard/server.js +340 -0
- package/dist/dashboard/ui.d.ts +4 -0
- package/dist/dashboard/ui.js +538 -0
- package/dist/dashboard/ws-feeds.d.ts +29 -0
- package/dist/dashboard/ws-feeds.js +660 -0
- package/dist/dex-asset-map.d.ts +80 -0
- package/dist/dex-asset-map.js +201 -0
- package/dist/errors.d.ts +109 -0
- package/dist/errors.js +84 -0
- package/dist/event-stream.d.ts +25 -0
- package/dist/event-stream.js +168 -0
- package/dist/exchanges/hyperliquid.d.ts +212 -0
- package/dist/exchanges/hyperliquid.js +931 -0
- package/dist/exchanges/interface.d.ts +95 -0
- package/dist/exchanges/interface.js +5 -0
- package/dist/exchanges/lighter.d.ts +159 -0
- package/dist/exchanges/lighter.js +793 -0
- package/dist/exchanges/pacifica.d.ts +51 -0
- package/dist/exchanges/pacifica.js +248 -0
- package/dist/execution-log.d.ts +36 -0
- package/dist/execution-log.js +102 -0
- package/dist/funding/history.d.ts +63 -0
- package/dist/funding/history.js +266 -0
- package/dist/funding/index.d.ts +3 -0
- package/dist/funding/index.js +3 -0
- package/dist/funding/normalize.d.ts +39 -0
- package/dist/funding/normalize.js +66 -0
- package/dist/funding/rates.d.ts +45 -0
- package/dist/funding/rates.js +172 -0
- package/dist/funding-history.d.ts +5 -0
- package/dist/funding-history.js +5 -0
- package/dist/funding-rates.d.ts +5 -0
- package/dist/funding-rates.js +5 -0
- package/dist/funding.d.ts +5 -0
- package/dist/funding.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +458 -0
- package/dist/jobs.d.ts +37 -0
- package/dist/jobs.js +152 -0
- package/dist/liquidity.d.ts +34 -0
- package/dist/liquidity.js +100 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +1206 -0
- package/dist/pacifica/client.d.ts +111 -0
- package/dist/pacifica/client.js +310 -0
- package/dist/pacifica/constants.d.ts +27 -0
- package/dist/pacifica/constants.js +47 -0
- package/dist/pacifica/deposit.d.ts +14 -0
- package/dist/pacifica/deposit.js +78 -0
- package/dist/pacifica/index.d.ts +6 -0
- package/dist/pacifica/index.js +11 -0
- package/dist/pacifica/signing.d.ts +49 -0
- package/dist/pacifica/signing.js +97 -0
- package/dist/pacifica/types/account.d.ts +42 -0
- package/dist/pacifica/types/account.js +1 -0
- package/dist/pacifica/types/index.d.ts +6 -0
- package/dist/pacifica/types/index.js +6 -0
- package/dist/pacifica/types/lake.d.ts +18 -0
- package/dist/pacifica/types/lake.js +1 -0
- package/dist/pacifica/types/market.d.ts +64 -0
- package/dist/pacifica/types/market.js +1 -0
- package/dist/pacifica/types/order.d.ts +92 -0
- package/dist/pacifica/types/order.js +1 -0
- package/dist/pacifica/types/position.d.ts +25 -0
- package/dist/pacifica/types/position.js +1 -0
- package/dist/pacifica/types/ws.d.ts +34 -0
- package/dist/pacifica/types/ws.js +41 -0
- package/dist/pacifica/ws-client.d.ts +42 -0
- package/dist/pacifica/ws-client.js +180 -0
- package/dist/plan-executor.d.ts +48 -0
- package/dist/plan-executor.js +280 -0
- package/dist/position-history.d.ts +68 -0
- package/dist/position-history.js +222 -0
- package/dist/rebalance.d.ts +64 -0
- package/dist/rebalance.js +142 -0
- package/dist/retry.d.ts +74 -0
- package/dist/retry.js +129 -0
- package/dist/risk.d.ts +48 -0
- package/dist/risk.js +156 -0
- package/dist/settings.d.ts +19 -0
- package/dist/settings.js +45 -0
- package/dist/shared-api.d.ts +5 -0
- package/dist/shared-api.js +5 -0
- package/dist/strategies/dca.d.ts +25 -0
- package/dist/strategies/dca.js +114 -0
- package/dist/strategies/funding-arb.d.ts +15 -0
- package/dist/strategies/funding-arb.js +281 -0
- package/dist/strategies/grid.d.ts +34 -0
- package/dist/strategies/grid.js +185 -0
- package/dist/strategies/trailing-stop.d.ts +17 -0
- package/dist/strategies/trailing-stop.js +121 -0
- package/dist/strategies/twap.d.ts +20 -0
- package/dist/strategies/twap.js +78 -0
- package/dist/trade-validator.d.ts +39 -0
- package/dist/trade-validator.js +154 -0
- package/dist/utils.d.ts +38 -0
- package/dist/utils.js +110 -0
- package/package.json +63 -0
- package/skills/perp-cli/SKILL.md +149 -0
- package/skills/perp-cli/references/commands.md +143 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict Bridge Integration Tests — comprehensive verification using real keys.
|
|
3
|
+
*
|
|
4
|
+
* Uses .env keys for address derivation and balance checks.
|
|
5
|
+
* NO transactions are executed. NO funds are spent.
|
|
6
|
+
*
|
|
7
|
+
* Core verification: "sender pays only" — all bridge fees are deducted from
|
|
8
|
+
* the source USDC amount. The recipient receives amountOut without paying
|
|
9
|
+
* any gas or fees on the destination chain.
|
|
10
|
+
*
|
|
11
|
+
* Tests:
|
|
12
|
+
* 1. Key derivation & address consistency
|
|
13
|
+
* 2. Real balance checks on all chains
|
|
14
|
+
* 3. CCTP quote API for all routes (including HyperCore)
|
|
15
|
+
* 4. HyperCore CCTP fees API validation
|
|
16
|
+
* 5. deBridge quote API with real addresses
|
|
17
|
+
* 6. Relay quote API with real addresses
|
|
18
|
+
* 7. getAllQuotes aggregation correctness
|
|
19
|
+
* 8. getBestQuote selection logic
|
|
20
|
+
* 9. Cross-provider quote comparison
|
|
21
|
+
* 10. Edge cases and error handling
|
|
22
|
+
* 11-14. On-chain / API verification
|
|
23
|
+
* 15. SENDER-PAYS-ONLY: full 4-chain matrix verification
|
|
24
|
+
* 16. Complete route matrix (all 12 directional pairs)
|
|
25
|
+
*/
|
|
26
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
27
|
+
import { config } from "dotenv";
|
|
28
|
+
import { CHAIN_IDS, USDC_ADDRESSES, CCTP_DOMAINS, EXCHANGE_TO_CHAIN, getCctpQuote, getDebridgeQuote, getRelayQuote, getAllQuotes, getBestQuote, getEvmUsdcBalance, getSolanaUsdcBalance, checkBridgeBalance, getNativeGasBalance, checkBridgeGasBalance, } from "../../bridge-engine.js";
|
|
29
|
+
// Load .env
|
|
30
|
+
config();
|
|
31
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
32
|
+
// ── Derive real addresses from .env keys ──
|
|
33
|
+
let solanaAddress;
|
|
34
|
+
let evmAddress;
|
|
35
|
+
describe("Strict Bridge Integration Tests", { timeout: 120000 }, () => {
|
|
36
|
+
// ══════════════════════════════════════════════════════════
|
|
37
|
+
// 0. Setup: derive addresses from private keys
|
|
38
|
+
// ══════════════════════════════════════════════════════════
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
// Derive Solana address
|
|
41
|
+
const { Keypair } = await import("@solana/web3.js");
|
|
42
|
+
const bs58 = await import("bs58");
|
|
43
|
+
const solanaKey = process.env.pk ?? process.env.PACIFICA_PRIVATE_KEY;
|
|
44
|
+
if (!solanaKey)
|
|
45
|
+
throw new Error("Missing 'pk' or 'PACIFICA_PRIVATE_KEY' in .env");
|
|
46
|
+
const keypair = Keypair.fromSecretKey(bs58.default.decode(solanaKey));
|
|
47
|
+
solanaAddress = keypair.publicKey.toBase58();
|
|
48
|
+
// Derive EVM address
|
|
49
|
+
const { ethers } = await import("ethers");
|
|
50
|
+
const evmKey = process.env.HL_PRIVATE_KEY;
|
|
51
|
+
if (!evmKey)
|
|
52
|
+
throw new Error("Missing 'HL_PRIVATE_KEY' in .env");
|
|
53
|
+
const wallet = new ethers.Wallet(evmKey);
|
|
54
|
+
evmAddress = wallet.address;
|
|
55
|
+
});
|
|
56
|
+
// ══════════════════════════════════════════════════════════
|
|
57
|
+
// 1. Key & Address Validation
|
|
58
|
+
// ══════════════════════════════════════════════════════════
|
|
59
|
+
describe("1. Key derivation & address format", () => {
|
|
60
|
+
it("Solana address is valid base58 (32-44 chars)", () => {
|
|
61
|
+
expect(solanaAddress).toBeDefined();
|
|
62
|
+
expect(solanaAddress.length).toBeGreaterThanOrEqual(32);
|
|
63
|
+
expect(solanaAddress.length).toBeLessThanOrEqual(44);
|
|
64
|
+
expect(solanaAddress).toMatch(/^[1-9A-HJ-NP-Za-km-z]+$/);
|
|
65
|
+
});
|
|
66
|
+
it("EVM address is valid checksummed address", () => {
|
|
67
|
+
expect(evmAddress).toBeDefined();
|
|
68
|
+
expect(evmAddress).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
69
|
+
});
|
|
70
|
+
it("HL_PRIVATE_KEY and LIGHTER_PRIVATE_KEY derive same EVM address", async () => {
|
|
71
|
+
const { ethers } = await import("ethers");
|
|
72
|
+
const hlWallet = new ethers.Wallet(process.env.HL_PRIVATE_KEY);
|
|
73
|
+
const lighterWallet = new ethers.Wallet(process.env.LIGHTER_PRIVATE_KEY);
|
|
74
|
+
expect(hlWallet.address).toBe(lighterWallet.address);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
// ══════════════════════════════════════════════════════════
|
|
78
|
+
// 2. Configuration Consistency
|
|
79
|
+
// ══════════════════════════════════════════════════════════
|
|
80
|
+
describe("2. Configuration consistency", () => {
|
|
81
|
+
it("EXCHANGE_TO_CHAIN maps correctly", () => {
|
|
82
|
+
expect(EXCHANGE_TO_CHAIN.pacifica).toBe("solana");
|
|
83
|
+
expect(EXCHANGE_TO_CHAIN.hyperliquid).toBe("hyperliquid");
|
|
84
|
+
expect(EXCHANGE_TO_CHAIN.lighter).toBe("arbitrum");
|
|
85
|
+
});
|
|
86
|
+
it("all standard chains have chain IDs", () => {
|
|
87
|
+
expect(CHAIN_IDS.solana).toBe(7565164);
|
|
88
|
+
expect(CHAIN_IDS.arbitrum).toBe(42161);
|
|
89
|
+
expect(CHAIN_IDS.base).toBe(8453);
|
|
90
|
+
});
|
|
91
|
+
it("all standard chains have USDC addresses", () => {
|
|
92
|
+
for (const chain of ["solana", "arbitrum", "base"]) {
|
|
93
|
+
expect(USDC_ADDRESSES[chain]).toBeDefined();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
it("CCTP domains include hyperliquid (19)", () => {
|
|
97
|
+
expect(CCTP_DOMAINS.solana).toBe(5);
|
|
98
|
+
expect(CCTP_DOMAINS.arbitrum).toBe(3);
|
|
99
|
+
expect(CCTP_DOMAINS.base).toBe(6);
|
|
100
|
+
expect(CCTP_DOMAINS.hyperliquid).toBe(19);
|
|
101
|
+
});
|
|
102
|
+
it("no unsupported chains leak into config", () => {
|
|
103
|
+
const supportedChains = ["solana", "arbitrum", "base"];
|
|
104
|
+
for (const chain of Object.keys(CHAIN_IDS)) {
|
|
105
|
+
expect(supportedChains).toContain(chain);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
it("CCTP domains only contain supported chains + hyperliquid", () => {
|
|
109
|
+
const allowed = ["solana", "arbitrum", "base", "hyperliquid"];
|
|
110
|
+
for (const chain of Object.keys(CCTP_DOMAINS)) {
|
|
111
|
+
expect(allowed).toContain(chain);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
// ══════════════════════════════════════════════════════════
|
|
116
|
+
// 3. Real Balance Checks
|
|
117
|
+
// ══════════════════════════════════════════════════════════
|
|
118
|
+
describe("3. Real balance checks", () => {
|
|
119
|
+
it("Solana USDC balance query succeeds", async () => {
|
|
120
|
+
const balance = await getSolanaUsdcBalance(solanaAddress);
|
|
121
|
+
expect(typeof balance).toBe("number");
|
|
122
|
+
expect(balance).toBeGreaterThanOrEqual(0);
|
|
123
|
+
});
|
|
124
|
+
it("Arbitrum USDC balance query succeeds", async () => {
|
|
125
|
+
const balance = await getEvmUsdcBalance("arbitrum", evmAddress);
|
|
126
|
+
expect(typeof balance).toBe("number");
|
|
127
|
+
expect(balance).toBeGreaterThanOrEqual(0);
|
|
128
|
+
});
|
|
129
|
+
it("Base USDC balance query succeeds", async () => {
|
|
130
|
+
const balance = await getEvmUsdcBalance("base", evmAddress);
|
|
131
|
+
expect(typeof balance).toBe("number");
|
|
132
|
+
expect(balance).toBeGreaterThanOrEqual(0);
|
|
133
|
+
});
|
|
134
|
+
it("checkBridgeBalance returns consistent shape", async () => {
|
|
135
|
+
const result = await checkBridgeBalance("solana", solanaAddress, 1);
|
|
136
|
+
expect(typeof result.balance).toBe("number");
|
|
137
|
+
expect(typeof result.sufficient).toBe("boolean");
|
|
138
|
+
expect(result.sufficient).toBe(result.balance >= 1);
|
|
139
|
+
});
|
|
140
|
+
it("unsupported chain balance throws", async () => {
|
|
141
|
+
await expect(getEvmUsdcBalance("fakenet", evmAddress)).rejects.toThrow();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
// ══════════════════════════════════════════════════════════
|
|
145
|
+
// 3b. Native Gas Balance Checks
|
|
146
|
+
// ══════════════════════════════════════════════════════════
|
|
147
|
+
describe("3b. Native gas balance checks", () => {
|
|
148
|
+
it("Solana SOL balance query succeeds", async () => {
|
|
149
|
+
const balance = await getNativeGasBalance("solana", solanaAddress);
|
|
150
|
+
expect(typeof balance).toBe("number");
|
|
151
|
+
expect(balance).toBeGreaterThanOrEqual(0);
|
|
152
|
+
});
|
|
153
|
+
it("Arbitrum ETH balance query succeeds", async () => {
|
|
154
|
+
const balance = await getNativeGasBalance("arbitrum", evmAddress);
|
|
155
|
+
expect(typeof balance).toBe("number");
|
|
156
|
+
expect(balance).toBeGreaterThanOrEqual(0);
|
|
157
|
+
});
|
|
158
|
+
it("Base ETH balance query succeeds", async () => {
|
|
159
|
+
const balance = await getNativeGasBalance("base", evmAddress);
|
|
160
|
+
expect(typeof balance).toBe("number");
|
|
161
|
+
expect(balance).toBeGreaterThanOrEqual(0);
|
|
162
|
+
});
|
|
163
|
+
it("unsupported chain gas balance throws", async () => {
|
|
164
|
+
await expect(getNativeGasBalance("fakenet", evmAddress)).rejects.toThrow();
|
|
165
|
+
});
|
|
166
|
+
it("checkBridgeGasBalance: src-only check (fast mode)", async () => {
|
|
167
|
+
const result = await checkBridgeGasBalance("arbitrum", evmAddress, "base", evmAddress, false);
|
|
168
|
+
expect(typeof result.ok).toBe("boolean");
|
|
169
|
+
expect(Array.isArray(result.errors)).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
it("checkBridgeGasBalance: src+dst check (standard mode)", async () => {
|
|
172
|
+
const result = await checkBridgeGasBalance("arbitrum", evmAddress, "base", evmAddress, true);
|
|
173
|
+
expect(typeof result.ok).toBe("boolean");
|
|
174
|
+
expect(Array.isArray(result.errors)).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
it("checkBridgeGasBalance: returns errors array for insufficient gas", async () => {
|
|
177
|
+
// Use an address unlikely to have gas on both chains
|
|
178
|
+
const emptyAddr = "0x000000000000000000000000000000000000dEaD";
|
|
179
|
+
const result = await checkBridgeGasBalance("arbitrum", emptyAddr, "base", emptyAddr, true);
|
|
180
|
+
// Shape is correct regardless of balance
|
|
181
|
+
expect(typeof result.ok).toBe("boolean");
|
|
182
|
+
expect(Array.isArray(result.errors)).toBe(true);
|
|
183
|
+
if (!result.ok) {
|
|
184
|
+
expect(result.errors[0]).toMatch(/arbitrum|base/);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
// ══════════════════════════════════════════════════════════
|
|
189
|
+
// 4. CCTP Quotes — Standard Routes
|
|
190
|
+
// ══════════════════════════════════════════════════════════
|
|
191
|
+
describe("4. CCTP standard quotes", () => {
|
|
192
|
+
const standardRoutes = [
|
|
193
|
+
["solana", "arbitrum"],
|
|
194
|
+
["solana", "base"],
|
|
195
|
+
["arbitrum", "base"],
|
|
196
|
+
["arbitrum", "solana"],
|
|
197
|
+
["base", "arbitrum"],
|
|
198
|
+
["base", "solana"],
|
|
199
|
+
];
|
|
200
|
+
for (const [src, dst] of standardRoutes) {
|
|
201
|
+
it(`${src} → ${dst}: valid CCTP quote`, async () => {
|
|
202
|
+
const quote = await getCctpQuote(src, dst, 100);
|
|
203
|
+
expect(quote.provider).toBe("cctp");
|
|
204
|
+
expect(quote.srcChain).toBe(src);
|
|
205
|
+
expect(quote.dstChain).toBe(dst);
|
|
206
|
+
expect(quote.amountIn).toBe(100);
|
|
207
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
208
|
+
expect(quote.amountOut).toBeLessThanOrEqual(100);
|
|
209
|
+
expect(quote.fee).toBeGreaterThanOrEqual(0);
|
|
210
|
+
expect(quote.fee).toBeLessThan(5); // max $5 relay fee
|
|
211
|
+
expect(quote.estimatedTime).toBeGreaterThan(0);
|
|
212
|
+
expect(Math.abs(quote.fee - (quote.amountIn - quote.amountOut))).toBeLessThan(0.001);
|
|
213
|
+
expect(quote.raw).toBeDefined();
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
it("large amount ($100k) quote is valid", async () => {
|
|
217
|
+
const quote = await getCctpQuote("arbitrum", "base", 100000);
|
|
218
|
+
expect(quote.amountOut).toBeGreaterThan(99900); // fee should be tiny relative to amount
|
|
219
|
+
expect(quote.fee).toBeLessThan(5);
|
|
220
|
+
});
|
|
221
|
+
it("tiny amount ($0.01) quote is valid", async () => {
|
|
222
|
+
const quote = await getCctpQuote("arbitrum", "base", 0.01);
|
|
223
|
+
expect(quote.amountOut).toBeGreaterThan(-1);
|
|
224
|
+
expect(quote.fee).toBeLessThan(1);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
// ══════════════════════════════════════════════════════════
|
|
228
|
+
// 5. CCTP Quotes — HyperCore Routes
|
|
229
|
+
// ══════════════════════════════════════════════════════════
|
|
230
|
+
describe("5. HyperCore CCTP quotes", () => {
|
|
231
|
+
it("solana → hyperliquid: valid HyperCore deposit quote", async () => {
|
|
232
|
+
const quote = await getCctpQuote("solana", "hyperliquid", 500);
|
|
233
|
+
expect(quote.provider).toBe("cctp");
|
|
234
|
+
expect(quote.srcChain).toBe("solana");
|
|
235
|
+
expect(quote.dstChain).toBe("hyperliquid");
|
|
236
|
+
expect(quote.amountIn).toBe(500);
|
|
237
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
238
|
+
expect(quote.amountOut).toBeLessThan(500);
|
|
239
|
+
expect(quote.fee).toBeGreaterThan(0);
|
|
240
|
+
expect(quote.fee).toBeLessThan(5); // should be ~$0.25 (1bp + forwarding)
|
|
241
|
+
expect(quote.estimatedTime).toBe(65); // Solana source
|
|
242
|
+
expect(quote.gasIncluded).toBe(true);
|
|
243
|
+
expect(quote.raw).toHaveProperty("type", "cctp-hypercore");
|
|
244
|
+
expect(quote.raw).toHaveProperty("maxFee");
|
|
245
|
+
});
|
|
246
|
+
it("arbitrum → hyperliquid: valid HyperCore deposit quote", async () => {
|
|
247
|
+
const quote = await getCctpQuote("arbitrum", "hyperliquid", 1000);
|
|
248
|
+
expect(quote.provider).toBe("cctp");
|
|
249
|
+
expect(quote.srcChain).toBe("arbitrum");
|
|
250
|
+
expect(quote.dstChain).toBe("hyperliquid");
|
|
251
|
+
expect(quote.amountIn).toBe(1000);
|
|
252
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
253
|
+
expect(quote.fee).toBeGreaterThan(0);
|
|
254
|
+
expect(quote.fee).toBeLessThan(5);
|
|
255
|
+
expect(quote.estimatedTime).toBe(60); // EVM-to-EVM fast
|
|
256
|
+
expect(quote.gasIncluded).toBe(true);
|
|
257
|
+
expect(quote.raw).toHaveProperty("type", "cctp-hypercore");
|
|
258
|
+
});
|
|
259
|
+
it("base → hyperliquid: valid HyperCore deposit quote", async () => {
|
|
260
|
+
const quote = await getCctpQuote("base", "hyperliquid", 200);
|
|
261
|
+
expect(quote.provider).toBe("cctp");
|
|
262
|
+
expect(quote.dstChain).toBe("hyperliquid");
|
|
263
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
264
|
+
expect(quote.fee).toBeLessThan(5);
|
|
265
|
+
expect(quote.estimatedTime).toBe(60);
|
|
266
|
+
});
|
|
267
|
+
it("hyperliquid → arbitrum: valid HyperCore withdrawal quote", async () => {
|
|
268
|
+
const quote = await getCctpQuote("hyperliquid", "arbitrum", 300);
|
|
269
|
+
expect(quote.provider).toBe("cctp");
|
|
270
|
+
expect(quote.srcChain).toBe("hyperliquid");
|
|
271
|
+
expect(quote.dstChain).toBe("arbitrum");
|
|
272
|
+
expect(quote.amountIn).toBe(300);
|
|
273
|
+
expect(quote.amountOut).toBe(299.80); // $0.20 forwarding fee
|
|
274
|
+
expect(quote.fee).toBe(0.20);
|
|
275
|
+
expect(quote.estimatedTime).toBe(60);
|
|
276
|
+
expect(quote.gasIncluded).toBe(true);
|
|
277
|
+
expect(quote.raw).toHaveProperty("type", "cctp-hypercore-withdraw");
|
|
278
|
+
});
|
|
279
|
+
it("hyperliquid → solana: valid HyperCore withdrawal quote", async () => {
|
|
280
|
+
const quote = await getCctpQuote("hyperliquid", "solana", 100);
|
|
281
|
+
expect(quote.provider).toBe("cctp");
|
|
282
|
+
expect(quote.srcChain).toBe("hyperliquid");
|
|
283
|
+
expect(quote.dstChain).toBe("solana");
|
|
284
|
+
expect(quote.fee).toBe(0.20);
|
|
285
|
+
expect(quote.amountOut).toBe(99.80);
|
|
286
|
+
});
|
|
287
|
+
it("HyperCore fees scale correctly with amount", async () => {
|
|
288
|
+
const small = await getCctpQuote("arbitrum", "hyperliquid", 10);
|
|
289
|
+
const large = await getCctpQuote("arbitrum", "hyperliquid", 10000);
|
|
290
|
+
// Protocol fee is 1bp, so large amount pays more
|
|
291
|
+
expect(large.fee).toBeGreaterThan(small.fee);
|
|
292
|
+
// But fee percentage should be similar (both ~1bp + flat forwarding)
|
|
293
|
+
const smallPct = small.fee / small.amountIn;
|
|
294
|
+
const largePct = large.fee / large.amountIn;
|
|
295
|
+
expect(smallPct).toBeGreaterThan(largePct); // smaller amounts pay higher % (flat fee dominates)
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
// ══════════════════════════════════════════════════════════
|
|
299
|
+
// 6. HyperCore Fees API Direct Validation
|
|
300
|
+
// ══════════════════════════════════════════════════════════
|
|
301
|
+
describe("6. HyperCore fees API", () => {
|
|
302
|
+
const FEES_API = "https://iris-api.circle.com/v2/burn/USDC/fees";
|
|
303
|
+
it("Solana (domain 5) → HyperCore (domain 19) fees API responds", async () => {
|
|
304
|
+
const res = await fetch(`${FEES_API}/5/19?forward=true&hyperCoreDeposit=true`);
|
|
305
|
+
expect(res.ok).toBe(true);
|
|
306
|
+
const data = await res.json();
|
|
307
|
+
expect(Array.isArray(data)).toBe(true);
|
|
308
|
+
expect(data.length).toBeGreaterThan(0);
|
|
309
|
+
const schedule = data[0];
|
|
310
|
+
expect(schedule).toHaveProperty("finalityThreshold");
|
|
311
|
+
expect(schedule).toHaveProperty("minimumFee");
|
|
312
|
+
});
|
|
313
|
+
it("Arbitrum (domain 3) → HyperCore (domain 19) fees API responds", async () => {
|
|
314
|
+
const res = await fetch(`${FEES_API}/3/19?forward=true&hyperCoreDeposit=true`);
|
|
315
|
+
expect(res.ok).toBe(true);
|
|
316
|
+
const data = await res.json();
|
|
317
|
+
expect(Array.isArray(data)).toBe(true);
|
|
318
|
+
expect(data.length).toBeGreaterThan(0);
|
|
319
|
+
});
|
|
320
|
+
it("Base (domain 6) → HyperCore (domain 19) fees API responds", async () => {
|
|
321
|
+
const res = await fetch(`${FEES_API}/6/19?forward=true&hyperCoreDeposit=true`);
|
|
322
|
+
expect(res.ok).toBe(true);
|
|
323
|
+
const data = await res.json();
|
|
324
|
+
expect(Array.isArray(data)).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
it("fees API returns forward fee structure", async () => {
|
|
327
|
+
const res = await fetch(`${FEES_API}/3/19?forward=true&hyperCoreDeposit=true`);
|
|
328
|
+
const data = await res.json();
|
|
329
|
+
// Look for schedule with forwardFee
|
|
330
|
+
const hasForwardFee = data.some(s => s.forwardFee !== undefined && s.forwardFee !== null);
|
|
331
|
+
// At minimum, all schedules should have finalityThreshold and minimumFee
|
|
332
|
+
for (const schedule of data) {
|
|
333
|
+
expect(typeof schedule.finalityThreshold).toBe("number");
|
|
334
|
+
expect(typeof schedule.minimumFee).toBe("number");
|
|
335
|
+
}
|
|
336
|
+
// Forward fee may or may not be present depending on API version
|
|
337
|
+
if (hasForwardFee) {
|
|
338
|
+
const withFee = data.find(s => s.forwardFee !== undefined);
|
|
339
|
+
const ff = withFee.forwardFee;
|
|
340
|
+
expect(typeof ff.low).toBe("number");
|
|
341
|
+
expect(typeof ff.med).toBe("number");
|
|
342
|
+
expect(typeof ff.high).toBe("number");
|
|
343
|
+
expect(ff.med).toBeGreaterThanOrEqual(ff.low);
|
|
344
|
+
expect(ff.high).toBeGreaterThanOrEqual(ff.med);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
// ══════════════════════════════════════════════════════════
|
|
349
|
+
// 7. deBridge DLN Quotes (with real addresses)
|
|
350
|
+
// ══════════════════════════════════════════════════════════
|
|
351
|
+
describe("7. deBridge quotes with real addresses", () => {
|
|
352
|
+
it("solana → arbitrum: quote with real wallet addresses", async () => {
|
|
353
|
+
const quote = await getDebridgeQuote("solana", "arbitrum", 100, solanaAddress, evmAddress);
|
|
354
|
+
expect(quote.provider).toBe("debridge");
|
|
355
|
+
expect(quote.srcChain).toBe("solana");
|
|
356
|
+
expect(quote.dstChain).toBe("arbitrum");
|
|
357
|
+
expect(quote.amountIn).toBe(100);
|
|
358
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
359
|
+
expect(quote.amountOut).toBeLessThanOrEqual(100);
|
|
360
|
+
expect(quote.fee).toBeGreaterThanOrEqual(0);
|
|
361
|
+
expect(quote.estimatedTime).toBeGreaterThan(0);
|
|
362
|
+
expect(Math.abs(quote.fee - (quote.amountIn - quote.amountOut))).toBeLessThan(0.001);
|
|
363
|
+
});
|
|
364
|
+
it("arbitrum → base: EVM-to-EVM with real addresses", async () => {
|
|
365
|
+
await wait(1500);
|
|
366
|
+
const quote = await getDebridgeQuote("arbitrum", "base", 200, evmAddress, evmAddress);
|
|
367
|
+
expect(quote.provider).toBe("debridge");
|
|
368
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
369
|
+
expect(quote.amountOut).toBeLessThanOrEqual(200);
|
|
370
|
+
});
|
|
371
|
+
it("base → solana: reverse route with real addresses", async () => {
|
|
372
|
+
await wait(1500);
|
|
373
|
+
const quote = await getDebridgeQuote("base", "solana", 50, evmAddress, solanaAddress);
|
|
374
|
+
expect(quote.provider).toBe("debridge");
|
|
375
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
376
|
+
});
|
|
377
|
+
it("unsupported chain throws", async () => {
|
|
378
|
+
await expect(getDebridgeQuote("hyperliquid", "arbitrum", 100, evmAddress, evmAddress)).rejects.toThrow(/Unsupported chain/i);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
// ══════════════════════════════════════════════════════════
|
|
382
|
+
// 8. Relay Quotes (with real addresses)
|
|
383
|
+
// ══════════════════════════════════════════════════════════
|
|
384
|
+
describe("8. Relay quotes with real addresses", () => {
|
|
385
|
+
it("solana → arbitrum: Relay quote with real addresses", async () => {
|
|
386
|
+
const quote = await getRelayQuote("solana", "arbitrum", 100, solanaAddress, evmAddress);
|
|
387
|
+
expect(quote.provider).toBe("relay");
|
|
388
|
+
expect(quote.srcChain).toBe("solana");
|
|
389
|
+
expect(quote.dstChain).toBe("arbitrum");
|
|
390
|
+
expect(quote.amountIn).toBe(100);
|
|
391
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
392
|
+
expect(quote.amountOut).toBeLessThanOrEqual(100);
|
|
393
|
+
expect(quote.estimatedTime).toBeGreaterThan(0);
|
|
394
|
+
});
|
|
395
|
+
it("arbitrum → base: EVM-to-EVM Relay quote", async () => {
|
|
396
|
+
await wait(1000);
|
|
397
|
+
const quote = await getRelayQuote("arbitrum", "base", 500, evmAddress, evmAddress);
|
|
398
|
+
expect(quote.provider).toBe("relay");
|
|
399
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
400
|
+
});
|
|
401
|
+
it("base → solana: reverse Relay quote", async () => {
|
|
402
|
+
await wait(1000);
|
|
403
|
+
const quote = await getRelayQuote("base", "solana", 200, evmAddress, solanaAddress);
|
|
404
|
+
expect(quote.provider).toBe("relay");
|
|
405
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
// ══════════════════════════════════════════════════════════
|
|
409
|
+
// 9. getAllQuotes Aggregation
|
|
410
|
+
// ══════════════════════════════════════════════════════════
|
|
411
|
+
describe("9. getAllQuotes aggregation", () => {
|
|
412
|
+
it("arbitrum → base: returns multiple providers sorted by amountOut", async () => {
|
|
413
|
+
await wait(2000);
|
|
414
|
+
const quotes = await getAllQuotes("arbitrum", "base", 1000, evmAddress, evmAddress);
|
|
415
|
+
expect(quotes.length).toBeGreaterThanOrEqual(1);
|
|
416
|
+
// Should be sorted by amountOut descending (best first)
|
|
417
|
+
for (let i = 1; i < quotes.length; i++) {
|
|
418
|
+
expect(quotes[i - 1].amountOut).toBeGreaterThanOrEqual(quotes[i].amountOut);
|
|
419
|
+
}
|
|
420
|
+
// All quotes should have consistent shape
|
|
421
|
+
for (const q of quotes) {
|
|
422
|
+
expect(["cctp", "debridge", "relay"]).toContain(q.provider);
|
|
423
|
+
expect(q.srcChain).toBe("arbitrum");
|
|
424
|
+
expect(q.dstChain).toBe("base");
|
|
425
|
+
expect(q.amountIn).toBe(1000);
|
|
426
|
+
expect(q.amountOut).toBeGreaterThan(0);
|
|
427
|
+
expect(typeof q.fee).toBe("number");
|
|
428
|
+
expect(typeof q.estimatedTime).toBe("number");
|
|
429
|
+
}
|
|
430
|
+
// CCTP should be present (it's always available for supported routes)
|
|
431
|
+
const cctpQuote = quotes.find(q => q.provider === "cctp");
|
|
432
|
+
expect(cctpQuote).toBeDefined();
|
|
433
|
+
});
|
|
434
|
+
it("solana → arbitrum: includes CCTP + at least one other provider", async () => {
|
|
435
|
+
await wait(2000);
|
|
436
|
+
const quotes = await getAllQuotes("solana", "arbitrum", 200, solanaAddress, evmAddress);
|
|
437
|
+
expect(quotes.length).toBeGreaterThanOrEqual(2);
|
|
438
|
+
const providers = new Set(quotes.map(q => q.provider));
|
|
439
|
+
expect(providers.has("cctp")).toBe(true);
|
|
440
|
+
// Should have debridge or relay too
|
|
441
|
+
expect(providers.size).toBeGreaterThanOrEqual(2);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
// ══════════════════════════════════════════════════════════
|
|
445
|
+
// 10. getBestQuote Selection
|
|
446
|
+
// ══════════════════════════════════════════════════════════
|
|
447
|
+
describe("10. getBestQuote selection", () => {
|
|
448
|
+
it("best quote is cheapest provider for standard chains", async () => {
|
|
449
|
+
const quote = await getBestQuote("arbitrum", "base", 500, evmAddress, evmAddress);
|
|
450
|
+
// Best quote should be either CCTP or Relay (both valid, sorted by amountOut)
|
|
451
|
+
expect(["cctp", "relay"]).toContain(quote.provider);
|
|
452
|
+
expect(quote.amountOut).toBeGreaterThanOrEqual(498);
|
|
453
|
+
expect(quote.fee).toBeLessThan(2);
|
|
454
|
+
});
|
|
455
|
+
it("solana → arbitrum: CCTP preferred", async () => {
|
|
456
|
+
const quote = await getBestQuote("solana", "arbitrum", 1000, solanaAddress, evmAddress);
|
|
457
|
+
expect(quote.provider).toBe("cctp");
|
|
458
|
+
expect(quote.fee).toBeLessThan(2);
|
|
459
|
+
});
|
|
460
|
+
it("quote amountIn - amountOut ≈ fee", async () => {
|
|
461
|
+
const quote = await getBestQuote("base", "solana", 100, evmAddress, solanaAddress);
|
|
462
|
+
const calculatedFee = quote.amountIn - quote.amountOut;
|
|
463
|
+
expect(Math.abs(calculatedFee - quote.fee)).toBeLessThan(0.01);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
// ══════════════════════════════════════════════════════════
|
|
467
|
+
// 11. Cross-Provider Comparison
|
|
468
|
+
// ══════════════════════════════════════════════════════════
|
|
469
|
+
describe("11. Cross-provider fee comparison", () => {
|
|
470
|
+
it("CCTP is cheaper than deBridge for standard routes", async () => {
|
|
471
|
+
await wait(2000);
|
|
472
|
+
const cctp = await getCctpQuote("arbitrum", "base", 1000);
|
|
473
|
+
const debridge = await getDebridgeQuote("arbitrum", "base", 1000, evmAddress, evmAddress);
|
|
474
|
+
expect(cctp.fee).toBeLessThanOrEqual(debridge.fee);
|
|
475
|
+
expect(cctp.amountOut).toBeGreaterThanOrEqual(debridge.amountOut);
|
|
476
|
+
});
|
|
477
|
+
it("HyperCore CCTP fee is reasonable ($0.20-$2 range)", async () => {
|
|
478
|
+
const quote = await getCctpQuote("arbitrum", "hyperliquid", 1000);
|
|
479
|
+
expect(quote.fee).toBeGreaterThanOrEqual(0.10);
|
|
480
|
+
expect(quote.fee).toBeLessThan(3);
|
|
481
|
+
});
|
|
482
|
+
it("HyperCore withdrawal fee is fixed at $0.20", async () => {
|
|
483
|
+
const q100 = await getCctpQuote("hyperliquid", "arbitrum", 100);
|
|
484
|
+
const q10000 = await getCctpQuote("hyperliquid", "arbitrum", 10000);
|
|
485
|
+
expect(q100.fee).toBe(0.20);
|
|
486
|
+
expect(q10000.fee).toBe(0.20);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
// ══════════════════════════════════════════════════════════
|
|
490
|
+
// 12. Error Handling & Edge Cases
|
|
491
|
+
// ══════════════════════════════════════════════════════════
|
|
492
|
+
describe("12. Error handling", () => {
|
|
493
|
+
it("CCTP quote for unsupported chain throws", async () => {
|
|
494
|
+
await expect(getCctpQuote("polygon", "arbitrum", 100)).rejects.toThrow(/not supported/i);
|
|
495
|
+
});
|
|
496
|
+
it("deBridge zero amount throws", async () => {
|
|
497
|
+
await wait(1500);
|
|
498
|
+
await expect(getDebridgeQuote("solana", "arbitrum", 0, solanaAddress, evmAddress)).rejects.toThrow();
|
|
499
|
+
});
|
|
500
|
+
it("deBridge negative amount throws", async () => {
|
|
501
|
+
await wait(1500);
|
|
502
|
+
await expect(getDebridgeQuote("arbitrum", "base", -100, evmAddress, evmAddress)).rejects.toThrow();
|
|
503
|
+
});
|
|
504
|
+
it("CCTP same-chain quote still valid", async () => {
|
|
505
|
+
const quote = await getCctpQuote("arbitrum", "arbitrum", 100);
|
|
506
|
+
expect(quote.provider).toBe("cctp");
|
|
507
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
508
|
+
});
|
|
509
|
+
it("all quotes return gasIncluded field", async () => {
|
|
510
|
+
const cctp = await getCctpQuote("arbitrum", "base", 100);
|
|
511
|
+
expect(typeof cctp.gasIncluded).toBe("boolean");
|
|
512
|
+
const hypercore = await getCctpQuote("arbitrum", "hyperliquid", 100);
|
|
513
|
+
expect(hypercore.gasIncluded).toBe(true);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
// ══════════════════════════════════════════════════════════
|
|
517
|
+
// 13. HyperCore CctpForwarder Contract Verification
|
|
518
|
+
// ══════════════════════════════════════════════════════════
|
|
519
|
+
describe("13. HyperCore on-chain verification", () => {
|
|
520
|
+
const HYPERCORE_FORWARDER = "0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757";
|
|
521
|
+
const HYPERCORE_RPC = "https://rpc.hyperliquid.xyz/evm";
|
|
522
|
+
it("CctpForwarder contract exists on HyperEVM", async () => {
|
|
523
|
+
const res = await fetch(HYPERCORE_RPC, {
|
|
524
|
+
method: "POST",
|
|
525
|
+
headers: { "Content-Type": "application/json" },
|
|
526
|
+
body: JSON.stringify({
|
|
527
|
+
jsonrpc: "2.0", id: 1,
|
|
528
|
+
method: "eth_getCode",
|
|
529
|
+
params: [HYPERCORE_FORWARDER, "latest"],
|
|
530
|
+
}),
|
|
531
|
+
});
|
|
532
|
+
const json = await res.json();
|
|
533
|
+
// Contract should have code (not "0x")
|
|
534
|
+
expect(json.result).toBeDefined();
|
|
535
|
+
expect(json.result.length).toBeGreaterThan(2);
|
|
536
|
+
expect(json.result).not.toBe("0x");
|
|
537
|
+
});
|
|
538
|
+
it("CctpExtension contract exists on Arbitrum", async () => {
|
|
539
|
+
const CCTP_EXTENSION = "0xA95d9c1F655341597C94393fDdc30cf3c08E4fcE";
|
|
540
|
+
const res = await fetch("https://arb1.arbitrum.io/rpc", {
|
|
541
|
+
method: "POST",
|
|
542
|
+
headers: { "Content-Type": "application/json" },
|
|
543
|
+
body: JSON.stringify({
|
|
544
|
+
jsonrpc: "2.0", id: 1,
|
|
545
|
+
method: "eth_getCode",
|
|
546
|
+
params: [CCTP_EXTENSION, "latest"],
|
|
547
|
+
}),
|
|
548
|
+
});
|
|
549
|
+
const json = await res.json();
|
|
550
|
+
expect(json.result.length).toBeGreaterThan(2);
|
|
551
|
+
expect(json.result).not.toBe("0x");
|
|
552
|
+
});
|
|
553
|
+
it("HyperEVM RPC is reachable (eth_chainId)", async () => {
|
|
554
|
+
const res = await fetch(HYPERCORE_RPC, {
|
|
555
|
+
method: "POST",
|
|
556
|
+
headers: { "Content-Type": "application/json" },
|
|
557
|
+
body: JSON.stringify({
|
|
558
|
+
jsonrpc: "2.0", id: 1,
|
|
559
|
+
method: "eth_chainId",
|
|
560
|
+
params: [],
|
|
561
|
+
}),
|
|
562
|
+
});
|
|
563
|
+
const json = await res.json();
|
|
564
|
+
expect(json.result).toBeDefined();
|
|
565
|
+
// HyperEVM chain ID = 999 (0x3e7) or similar
|
|
566
|
+
const chainId = parseInt(json.result, 16);
|
|
567
|
+
expect(chainId).toBeGreaterThan(0);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
// ══════════════════════════════════════════════════════════
|
|
571
|
+
// 14. CCTP V2 Iris Attestation API
|
|
572
|
+
// ══════════════════════════════════════════════════════════
|
|
573
|
+
describe("14. CCTP V2 Iris attestation API", () => {
|
|
574
|
+
it("V2 relay fee API responds for all source domains", async () => {
|
|
575
|
+
const routes = [
|
|
576
|
+
[5, 3], // solana → arbitrum
|
|
577
|
+
[3, 6], // arbitrum → base
|
|
578
|
+
[6, 5], // base → solana
|
|
579
|
+
[3, 19], // arbitrum → hyperliquid
|
|
580
|
+
[5, 19], // solana → hyperliquid
|
|
581
|
+
];
|
|
582
|
+
for (const [src, dst] of routes) {
|
|
583
|
+
const res = await fetch(`https://iris-api.circle.com/v2/burn/USDC/fees/${src}/${dst}`);
|
|
584
|
+
expect(res.ok).toBe(true);
|
|
585
|
+
const data = await res.json();
|
|
586
|
+
expect(Array.isArray(data)).toBe(true);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
// ══════════════════════════════════════════════════════════
|
|
591
|
+
// 15. SENDER-PAYS-ONLY — Core Invariant
|
|
592
|
+
// ══════════════════════════════════════════════════════════
|
|
593
|
+
//
|
|
594
|
+
// The fundamental rule: ALL bridge costs (protocol fee, relay fee,
|
|
595
|
+
// gas cost) are deducted from the sender's USDC amount.
|
|
596
|
+
// The recipient receives amountOut without paying anything.
|
|
597
|
+
//
|
|
598
|
+
// This is verified by checking that every quote has:
|
|
599
|
+
// - gasIncluded === true (auto-relay, no manual dst TX needed)
|
|
600
|
+
// - fee === amountIn - amountOut (fee is deducted from sent amount)
|
|
601
|
+
// - amountOut > 0 and amountOut <= amountIn
|
|
602
|
+
//
|
|
603
|
+
// ══════════════════════════════════════════════════════════
|
|
604
|
+
describe("15. SENDER-PAYS-ONLY verification", () => {
|
|
605
|
+
// All 4 chains: arbitrum, base, solana, hyperliquid
|
|
606
|
+
// Standard routes (6 pairs among arb/base/sol)
|
|
607
|
+
const standardRoutes = [
|
|
608
|
+
["arbitrum", "base"],
|
|
609
|
+
["arbitrum", "solana"],
|
|
610
|
+
["base", "arbitrum"],
|
|
611
|
+
["base", "solana"],
|
|
612
|
+
["solana", "arbitrum"],
|
|
613
|
+
["solana", "base"],
|
|
614
|
+
];
|
|
615
|
+
// HyperCore routes (deposit into + withdrawal from)
|
|
616
|
+
const hyperCoreDepositRoutes = [
|
|
617
|
+
["arbitrum", "hyperliquid"],
|
|
618
|
+
["base", "hyperliquid"],
|
|
619
|
+
["solana", "hyperliquid"],
|
|
620
|
+
];
|
|
621
|
+
const hyperCoreWithdrawRoutes = [
|
|
622
|
+
["hyperliquid", "arbitrum"],
|
|
623
|
+
["hyperliquid", "base"],
|
|
624
|
+
["hyperliquid", "solana"],
|
|
625
|
+
];
|
|
626
|
+
const ALL_ROUTES = [
|
|
627
|
+
...standardRoutes,
|
|
628
|
+
...hyperCoreDepositRoutes,
|
|
629
|
+
...hyperCoreWithdrawRoutes,
|
|
630
|
+
];
|
|
631
|
+
for (const [src, dst] of ALL_ROUTES) {
|
|
632
|
+
it(`${src} → ${dst}: fee deducted from sender's USDC`, async () => {
|
|
633
|
+
const amount = 100;
|
|
634
|
+
const quote = await getCctpQuote(src, dst, amount);
|
|
635
|
+
// 1. Fee is correctly calculated
|
|
636
|
+
expect(quote.fee).toBeGreaterThanOrEqual(0);
|
|
637
|
+
expect(Math.abs(quote.fee - (quote.amountIn - quote.amountOut))).toBeLessThan(0.001);
|
|
638
|
+
// 2. Recipient gets amountOut
|
|
639
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
640
|
+
expect(quote.amountOut).toBeLessThanOrEqual(amount);
|
|
641
|
+
// 3. Gas note present
|
|
642
|
+
expect(quote.gasNote).toBeDefined();
|
|
643
|
+
expect(quote.gasNote.length).toBeGreaterThan(0);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
it("fast=true: all standard routes use auto-relay (gasIncluded=true)", async () => {
|
|
647
|
+
for (const [src, dst] of standardRoutes) {
|
|
648
|
+
const quote = await getCctpQuote(src, dst, 100, true);
|
|
649
|
+
expect(quote.gasIncluded).toBe(true);
|
|
650
|
+
const raw = quote.raw;
|
|
651
|
+
expect(Number(raw.maxFee)).toBeGreaterThan(0);
|
|
652
|
+
expect(raw.fast).toBe(true);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
it("fast=false (default): forwarding routes have gasIncluded=true, Solana dst has gasIncluded=false", async () => {
|
|
656
|
+
for (const [src, dst] of standardRoutes) {
|
|
657
|
+
const quote = await getCctpQuote(src, dst, 100);
|
|
658
|
+
const raw = quote.raw;
|
|
659
|
+
expect(raw.fast).toBe(false);
|
|
660
|
+
// Forwarding available for all routes except →Solana
|
|
661
|
+
if (dst === "solana") {
|
|
662
|
+
expect(quote.gasIncluded).toBe(false);
|
|
663
|
+
expect(raw.forwarding).toBe(false);
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
expect(quote.gasIncluded).toBe(true);
|
|
667
|
+
expect(raw.forwarding).toBe(true);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
it("fast=true fees are higher than standard fees", async () => {
|
|
672
|
+
const standard = await getCctpQuote("arbitrum", "base", 1000, false);
|
|
673
|
+
const fast = await getCctpQuote("arbitrum", "base", 1000, true);
|
|
674
|
+
expect(fast.fee).toBeGreaterThan(standard.fee);
|
|
675
|
+
expect(fast.estimatedTime).toBeLessThan(standard.estimatedTime);
|
|
676
|
+
});
|
|
677
|
+
it("HyperCore deposits: CctpForwarder auto-deposits (always gasIncluded)", async () => {
|
|
678
|
+
for (const [src, dst] of hyperCoreDepositRoutes) {
|
|
679
|
+
const quote = await getCctpQuote(src, dst, 100);
|
|
680
|
+
expect(quote.gasIncluded).toBe(true);
|
|
681
|
+
expect(quote.gasNote).toContain("CctpForwarder");
|
|
682
|
+
const raw = quote.raw;
|
|
683
|
+
expect(raw.type).toBe("cctp-hypercore");
|
|
684
|
+
expect(Number(raw.maxFee)).toBeGreaterThan(0);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
it("HyperCore withdrawals: HL handles forwarding (always gasIncluded)", async () => {
|
|
688
|
+
for (const [src, dst] of hyperCoreWithdrawRoutes) {
|
|
689
|
+
const quote = await getCctpQuote(src, dst, 100);
|
|
690
|
+
expect(quote.gasIncluded).toBe(true);
|
|
691
|
+
expect(quote.gasNote).toContain("HyperCore");
|
|
692
|
+
expect(quote.fee).toBe(0.20);
|
|
693
|
+
const raw = quote.raw;
|
|
694
|
+
expect(raw.type).toBe("cctp-hypercore-withdraw");
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
it("deBridge quotes include gas in fee (sender-pays)", async () => {
|
|
698
|
+
await wait(2000);
|
|
699
|
+
const quote = await getDebridgeQuote("arbitrum", "base", 100, evmAddress, evmAddress);
|
|
700
|
+
expect(quote.fee).toBeGreaterThan(0);
|
|
701
|
+
expect(Math.abs(quote.fee - (quote.amountIn - quote.amountOut))).toBeLessThan(0.001);
|
|
702
|
+
expect(quote.gasIncluded).toBe(true);
|
|
703
|
+
});
|
|
704
|
+
it("Relay quotes include gas in fee (solver pays dst gas)", async () => {
|
|
705
|
+
await wait(1000);
|
|
706
|
+
const quote = await getRelayQuote("arbitrum", "base", 100, evmAddress, evmAddress);
|
|
707
|
+
expect(quote.fee).toBeGreaterThanOrEqual(0);
|
|
708
|
+
expect(Math.abs(quote.fee - (quote.amountIn - quote.amountOut))).toBeLessThan(0.001);
|
|
709
|
+
expect(quote.gasIncluded).toBe(true);
|
|
710
|
+
});
|
|
711
|
+
it("getAllQuotes: fee math is correct for every provider", async () => {
|
|
712
|
+
await wait(2000);
|
|
713
|
+
const quotes = await getAllQuotes("arbitrum", "base", 500, evmAddress, evmAddress);
|
|
714
|
+
for (const q of quotes) {
|
|
715
|
+
expect(q.fee).toBeGreaterThanOrEqual(0);
|
|
716
|
+
expect(Math.abs(q.fee - (q.amountIn - q.amountOut))).toBeLessThan(0.01);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
// ══════════════════════════════════════════════════════════
|
|
721
|
+
// 16. Complete 4-Chain Route Matrix
|
|
722
|
+
// ══════════════════════════════════════════════════════════
|
|
723
|
+
//
|
|
724
|
+
// Verify every possible directional pair among:
|
|
725
|
+
// arbitrum, base, solana, hyperliquid (12 pairs total)
|
|
726
|
+
//
|
|
727
|
+
// For each: valid quote, correct fee math, gasIncluded
|
|
728
|
+
// ══════════════════════════════════════════════════════════
|
|
729
|
+
describe("16. Complete 4-chain route matrix", () => {
|
|
730
|
+
const chains = ["arbitrum", "base", "solana", "hyperliquid"];
|
|
731
|
+
const amount = 250;
|
|
732
|
+
for (const src of chains) {
|
|
733
|
+
for (const dst of chains) {
|
|
734
|
+
if (src === dst)
|
|
735
|
+
continue;
|
|
736
|
+
it(`[MATRIX] ${src} → ${dst}: valid quote with sender-pays`, async () => {
|
|
737
|
+
const quote = await getCctpQuote(src, dst, amount);
|
|
738
|
+
// Basic shape
|
|
739
|
+
expect(quote.provider).toBe("cctp");
|
|
740
|
+
expect(quote.srcChain).toBe(src);
|
|
741
|
+
expect(quote.dstChain).toBe(dst);
|
|
742
|
+
expect(quote.amountIn).toBe(amount);
|
|
743
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
744
|
+
expect(quote.amountOut).toBeLessThanOrEqual(amount);
|
|
745
|
+
// Fee invariant: fee = amountIn - amountOut
|
|
746
|
+
expect(Math.abs(quote.fee - (amount - quote.amountOut))).toBeLessThan(0.001);
|
|
747
|
+
// gasIncluded: true for HyperCore, forwarding (EVM dst), and fast
|
|
748
|
+
// Only →Solana in standard mode has gasIncluded=false (no forwarding)
|
|
749
|
+
if (dst === "solana" && src !== "hyperliquid") {
|
|
750
|
+
expect(quote.gasIncluded).toBe(false);
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
expect(quote.gasIncluded).toBe(true);
|
|
754
|
+
}
|
|
755
|
+
// Reasonable fee (< $5 for $250)
|
|
756
|
+
expect(quote.fee).toBeLessThan(5);
|
|
757
|
+
// Time estimate is positive
|
|
758
|
+
expect(quote.estimatedTime).toBeGreaterThan(0);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
it("[MATRIX] fee comparison: HyperCore deposit > standard CCTP > HyperCore withdrawal", async () => {
|
|
763
|
+
const standardFee = (await getCctpQuote("arbitrum", "base", 1000)).fee;
|
|
764
|
+
const depositFee = (await getCctpQuote("arbitrum", "hyperliquid", 1000)).fee;
|
|
765
|
+
const withdrawFee = (await getCctpQuote("hyperliquid", "arbitrum", 1000)).fee;
|
|
766
|
+
// HyperCore deposit has protocol + forwarding fee (> standard)
|
|
767
|
+
expect(depositFee).toBeGreaterThanOrEqual(standardFee);
|
|
768
|
+
// HyperCore withdrawal is fixed $0.20
|
|
769
|
+
expect(withdrawFee).toBe(0.20);
|
|
770
|
+
});
|
|
771
|
+
it("[MATRIX] ETA comparison: fast mode Solana routes are slower than fast EVM-only", async () => {
|
|
772
|
+
const evmToEvmFast = (await getCctpQuote("arbitrum", "base", 100, true)).estimatedTime;
|
|
773
|
+
const solToEvmFast = (await getCctpQuote("solana", "arbitrum", 100, true)).estimatedTime;
|
|
774
|
+
// Fast finality: Solana ~90s, EVM-only ~60s
|
|
775
|
+
expect(solToEvmFast).toBeGreaterThanOrEqual(evmToEvmFast);
|
|
776
|
+
// Standard finality: both positive
|
|
777
|
+
const evmToEvmStd = (await getCctpQuote("arbitrum", "base", 100)).estimatedTime;
|
|
778
|
+
const solToEvmStd = (await getCctpQuote("solana", "arbitrum", 100)).estimatedTime;
|
|
779
|
+
expect(evmToEvmStd).toBeGreaterThan(0);
|
|
780
|
+
expect(solToEvmStd).toBeGreaterThan(0);
|
|
781
|
+
});
|
|
782
|
+
it("[MATRIX] all standard relay fee APIs respond for 4-chain matrix", async () => {
|
|
783
|
+
// Verify Circle's fee API works for every domain pair we support
|
|
784
|
+
const domains = [
|
|
785
|
+
{ chain: "arbitrum", domain: 3 },
|
|
786
|
+
{ chain: "base", domain: 6 },
|
|
787
|
+
{ chain: "solana", domain: 5 },
|
|
788
|
+
{ chain: "hyperliquid", domain: 19 },
|
|
789
|
+
];
|
|
790
|
+
const results = [];
|
|
791
|
+
for (const src of domains) {
|
|
792
|
+
for (const dst of domains) {
|
|
793
|
+
if (src.chain === dst.chain)
|
|
794
|
+
continue;
|
|
795
|
+
const url = dst.domain === 19
|
|
796
|
+
? `https://iris-api.circle.com/v2/burn/USDC/fees/${src.domain}/${dst.domain}?forward=true&hyperCoreDeposit=true`
|
|
797
|
+
: `https://iris-api.circle.com/v2/burn/USDC/fees/${src.domain}/${dst.domain}`;
|
|
798
|
+
const res = await fetch(url);
|
|
799
|
+
if (res.ok) {
|
|
800
|
+
results.push(`${src.chain}→${dst.chain}: OK`);
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
results.push(`${src.chain}→${dst.chain}: ${res.status}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
// At minimum, all standard routes should work
|
|
808
|
+
const okCount = results.filter(r => r.includes("OK")).length;
|
|
809
|
+
expect(okCount).toBeGreaterThanOrEqual(6); // at least standard 6 pairs
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
});
|