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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for bridge engine — READ-ONLY operations only.
|
|
3
|
+
*
|
|
4
|
+
* Tests real API calls to:
|
|
5
|
+
* - deBridge DLN quote API (GET, no signing)
|
|
6
|
+
* - Circle CCTP fee API (GET)
|
|
7
|
+
* - deBridge status API (GET)
|
|
8
|
+
* - getCctpQuote() (pure calculation, no RPC)
|
|
9
|
+
* - getBestQuote() (CCTP preferred, deBridge fallback)
|
|
10
|
+
*
|
|
11
|
+
* NO transactions are executed. NO funds are spent.
|
|
12
|
+
*
|
|
13
|
+
* Note: deBridge API has strict rate limits (~5 req/min).
|
|
14
|
+
* Tests are structured to minimize API calls and run sequentially.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect } from "vitest";
|
|
17
|
+
import { getDebridgeQuote, getCctpQuote, getBestQuote, checkDebridgeStatus, CHAIN_IDS, USDC_ADDRESSES, EXCHANGE_TO_CHAIN, } from "../../bridge-engine.js";
|
|
18
|
+
// Dummy addresses for quote-only calls (never used for signing)
|
|
19
|
+
const DUMMY_EVM = "0x0000000000000000000000000000000000000001";
|
|
20
|
+
const DUMMY_SOLANA = "11111111111111111111111111111111";
|
|
21
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
22
|
+
describe("Bridge Integration — Read-Only", { timeout: 60000 }, () => {
|
|
23
|
+
// ══════════════════════════════════════════════════════════
|
|
24
|
+
// Constants & Configuration (no API calls)
|
|
25
|
+
// ══════════════════════════════════════════════════════════
|
|
26
|
+
describe("chain constants", () => {
|
|
27
|
+
it("all chains have valid chain IDs", () => {
|
|
28
|
+
for (const [chain, id] of Object.entries(CHAIN_IDS)) {
|
|
29
|
+
expect(typeof id).toBe("number");
|
|
30
|
+
expect(id).toBeGreaterThan(0);
|
|
31
|
+
expect(chain.length).toBeGreaterThan(0);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
it("all USDC addresses are valid format", () => {
|
|
35
|
+
for (const [chain, addr] of Object.entries(USDC_ADDRESSES)) {
|
|
36
|
+
if (chain === "solana") {
|
|
37
|
+
expect(addr.length).toBeGreaterThan(30);
|
|
38
|
+
expect(addr.length).toBeLessThan(50);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
it("exchange-to-chain mapping covers all exchanges", () => {
|
|
46
|
+
expect(EXCHANGE_TO_CHAIN.pacifica).toBe("solana");
|
|
47
|
+
expect(EXCHANGE_TO_CHAIN.hyperliquid).toBe("hyperliquid");
|
|
48
|
+
expect(EXCHANGE_TO_CHAIN.lighter).toBe("arbitrum");
|
|
49
|
+
});
|
|
50
|
+
it("CCTP-supported chains have matching USDC addresses", () => {
|
|
51
|
+
for (const chain of ["solana", "arbitrum", "base"]) {
|
|
52
|
+
expect(USDC_ADDRESSES[chain]).toBeDefined();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
// ══════════════════════════════════════════════════════════
|
|
57
|
+
// Circle CCTP V2 Quotes (local calculation + fee API)
|
|
58
|
+
// No deBridge API calls — these are safe from rate limiting
|
|
59
|
+
// ══════════════════════════════════════════════════════════
|
|
60
|
+
describe("CCTP V2 quotes", () => {
|
|
61
|
+
it("solana → arbitrum: forwarding fee (standard finality)", async () => {
|
|
62
|
+
const quote = await getCctpQuote("solana", "arbitrum", 500);
|
|
63
|
+
expect(quote.provider).toBe("cctp");
|
|
64
|
+
expect(quote.fee).toBeLessThan(1); // forwarding ~$0.22
|
|
65
|
+
expect(quote.amountOut).toBeGreaterThan(499);
|
|
66
|
+
expect(quote.estimatedTime).toBeGreaterThan(0);
|
|
67
|
+
});
|
|
68
|
+
it("arbitrum → base: forwarding fee (L2 to L2)", async () => {
|
|
69
|
+
const quote = await getCctpQuote("arbitrum", "base", 200);
|
|
70
|
+
expect(quote.provider).toBe("cctp");
|
|
71
|
+
expect(quote.fee).toBeLessThan(1); // forwarding ~$0.22
|
|
72
|
+
expect(quote.estimatedTime).toBeGreaterThan(0);
|
|
73
|
+
});
|
|
74
|
+
it("base → solana: relay fee (no forwarding for Solana dst)", async () => {
|
|
75
|
+
const quote = await getCctpQuote("base", "solana", 100);
|
|
76
|
+
expect(quote.provider).toBe("cctp");
|
|
77
|
+
expect(quote.fee).toBeLessThan(1);
|
|
78
|
+
expect(quote.estimatedTime).toBeGreaterThan(0);
|
|
79
|
+
});
|
|
80
|
+
it("CCTP quote for tiny amount ($0.01): still valid", async () => {
|
|
81
|
+
const quote = await getCctpQuote("arbitrum", "base", 0.01);
|
|
82
|
+
expect(quote.amountOut).toBeGreaterThan(-1);
|
|
83
|
+
expect(quote.fee).toBeLessThan(1);
|
|
84
|
+
});
|
|
85
|
+
it("getBestQuote selects cheapest provider", async () => {
|
|
86
|
+
const q1 = await getBestQuote("arbitrum", "base", 1000, DUMMY_EVM, DUMMY_EVM);
|
|
87
|
+
expect(["cctp", "relay"]).toContain(q1.provider);
|
|
88
|
+
expect(q1.fee).toBeLessThan(2);
|
|
89
|
+
const q2 = await getBestQuote("solana", "arbitrum", 500, DUMMY_SOLANA, DUMMY_EVM);
|
|
90
|
+
expect(["cctp", "relay"]).toContain(q2.provider);
|
|
91
|
+
expect(q2.fee).toBeLessThan(2);
|
|
92
|
+
});
|
|
93
|
+
it("all CCTP quotes have consistent shape", async () => {
|
|
94
|
+
const routes = [
|
|
95
|
+
["solana", "arbitrum"],
|
|
96
|
+
["arbitrum", "base"],
|
|
97
|
+
["base", "solana"],
|
|
98
|
+
];
|
|
99
|
+
for (const [src, dst] of routes) {
|
|
100
|
+
const q = await getCctpQuote(src, dst, 100);
|
|
101
|
+
expect(q.provider).toBe("cctp");
|
|
102
|
+
expect(typeof q.srcChain).toBe("string");
|
|
103
|
+
expect(typeof q.dstChain).toBe("string");
|
|
104
|
+
expect(typeof q.amountIn).toBe("number");
|
|
105
|
+
expect(typeof q.amountOut).toBe("number");
|
|
106
|
+
expect(typeof q.fee).toBe("number");
|
|
107
|
+
expect(typeof q.estimatedTime).toBe("number");
|
|
108
|
+
expect(q.amountIn).toBeGreaterThanOrEqual(q.amountOut);
|
|
109
|
+
expect(q.raw).toBeDefined();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
// ══════════════════════════════════════════════════════════
|
|
114
|
+
// deBridge DLN Quote API (real HTTP calls — rate limited)
|
|
115
|
+
// Consolidated into fewer tests to avoid 429 errors
|
|
116
|
+
// ══════════════════════════════════════════════════════════
|
|
117
|
+
describe("deBridge DLN quotes (sequential, rate-limit aware)", () => {
|
|
118
|
+
it("solana → arbitrum: valid quote with fee breakdown", async () => {
|
|
119
|
+
const quote = await getDebridgeQuote("solana", "arbitrum", 100, DUMMY_SOLANA, DUMMY_EVM);
|
|
120
|
+
expect(quote.provider).toBe("debridge");
|
|
121
|
+
expect(quote.srcChain).toBe("solana");
|
|
122
|
+
expect(quote.dstChain).toBe("arbitrum");
|
|
123
|
+
expect(quote.amountIn).toBe(100);
|
|
124
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
125
|
+
expect(quote.amountOut).toBeLessThanOrEqual(100);
|
|
126
|
+
expect(quote.fee).toBeGreaterThanOrEqual(0);
|
|
127
|
+
expect(quote.fee).toBeLessThan(10); // < 10% for $100
|
|
128
|
+
expect(quote.estimatedTime).toBeGreaterThan(0);
|
|
129
|
+
expect(quote.raw).toBeDefined();
|
|
130
|
+
// Verify fee = amountIn - amountOut
|
|
131
|
+
expect(Math.abs(quote.fee - (quote.amountIn - quote.amountOut))).toBeLessThan(0.001);
|
|
132
|
+
});
|
|
133
|
+
it("arbitrum → solana: reverse route works", async () => {
|
|
134
|
+
await wait(1500); // respect rate limit
|
|
135
|
+
const quote = await getDebridgeQuote("arbitrum", "solana", 50, DUMMY_EVM, DUMMY_SOLANA);
|
|
136
|
+
expect(quote.provider).toBe("debridge");
|
|
137
|
+
expect(quote.amountIn).toBe(50);
|
|
138
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
139
|
+
expect(quote.amountOut).toBeLessThanOrEqual(50);
|
|
140
|
+
});
|
|
141
|
+
it("EVM-to-EVM route and small amount work", async () => {
|
|
142
|
+
await wait(1500);
|
|
143
|
+
// Test EVM-to-EVM
|
|
144
|
+
const quote = await getDebridgeQuote("base", "arbitrum", 200, DUMMY_EVM, DUMMY_EVM);
|
|
145
|
+
expect(quote.provider).toBe("debridge");
|
|
146
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
147
|
+
expect(quote.estimatedTime).toBeGreaterThan(0);
|
|
148
|
+
});
|
|
149
|
+
it("unsupported chain throws immediately (no API call)", async () => {
|
|
150
|
+
await expect(getDebridgeQuote("fakenet", "arbitrum", 100, DUMMY_EVM, DUMMY_EVM)).rejects.toThrow(/Unsupported chain/i);
|
|
151
|
+
});
|
|
152
|
+
it("zero amount: deBridge rejects", async () => {
|
|
153
|
+
await wait(1500);
|
|
154
|
+
await expect(getDebridgeQuote("solana", "arbitrum", 0, DUMMY_SOLANA, DUMMY_EVM)).rejects.toThrow();
|
|
155
|
+
});
|
|
156
|
+
it("getBestQuote returns valid quote (base → arbitrum)", async () => {
|
|
157
|
+
const quote = await getBestQuote("base", "arbitrum", 100, DUMMY_EVM, DUMMY_EVM);
|
|
158
|
+
expect(["cctp", "relay", "debridge"]).toContain(quote.provider);
|
|
159
|
+
expect(quote.amountOut).toBeGreaterThan(98);
|
|
160
|
+
expect(quote.fee).toBeLessThan(2);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
// ══════════════════════════════════════════════════════════
|
|
164
|
+
// CCTP vs deBridge Comparison (single deBridge call)
|
|
165
|
+
// ══════════════════════════════════════════════════════════
|
|
166
|
+
describe("CCTP vs deBridge comparison", () => {
|
|
167
|
+
it("CCTP is cheaper but slower than deBridge", async () => {
|
|
168
|
+
await wait(2000);
|
|
169
|
+
const cctp = await getCctpQuote("arbitrum", "base", 1000);
|
|
170
|
+
const debridge = await getDebridgeQuote("arbitrum", "base", 1000, DUMMY_EVM, DUMMY_EVM);
|
|
171
|
+
// CCTP is free, deBridge has fees
|
|
172
|
+
expect(cctp.fee).toBeLessThanOrEqual(debridge.fee);
|
|
173
|
+
expect(cctp.amountOut).toBeGreaterThanOrEqual(debridge.amountOut);
|
|
174
|
+
// deBridge is faster (~2s vs CCTP ~60-900s)
|
|
175
|
+
expect(debridge.estimatedTime).toBeLessThan(cctp.estimatedTime);
|
|
176
|
+
// Both have valid shapes
|
|
177
|
+
for (const q of [cctp, debridge]) {
|
|
178
|
+
expect(q.provider).toMatch(/^(cctp|debridge)$/);
|
|
179
|
+
expect(typeof q.srcChain).toBe("string");
|
|
180
|
+
expect(typeof q.dstChain).toBe("string");
|
|
181
|
+
expect(typeof q.amountIn).toBe("number");
|
|
182
|
+
expect(typeof q.amountOut).toBe("number");
|
|
183
|
+
expect(typeof q.fee).toBe("number");
|
|
184
|
+
expect(typeof q.estimatedTime).toBe("number");
|
|
185
|
+
expect(q.raw).toBeDefined();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
// ══════════════════════════════════════════════════════════
|
|
190
|
+
// deBridge Status API (single call)
|
|
191
|
+
// ══════════════════════════════════════════════════════════
|
|
192
|
+
describe("deBridge status check", () => {
|
|
193
|
+
it("non-existent order: returns response or 404", async () => {
|
|
194
|
+
await wait(1500);
|
|
195
|
+
try {
|
|
196
|
+
const status = await checkDebridgeStatus("0x0000000000000000000000000000000000000000000000000000000000000000");
|
|
197
|
+
expect(status).toBeDefined();
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
expect(String(err)).toMatch(/failed|404|not found|429/i);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
it("invalid order ID format: throws error", async () => {
|
|
204
|
+
await wait(1500);
|
|
205
|
+
try {
|
|
206
|
+
await checkDebridgeStatus("not-a-valid-order-id");
|
|
207
|
+
// If it doesn't throw, it should return something
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
expect(err).toBeDefined();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
// ══════════════════════════════════════════════════════════
|
|
215
|
+
// Edge Cases (no API calls)
|
|
216
|
+
// ══════════════════════════════════════════════════════════
|
|
217
|
+
describe("edge cases (offline)", () => {
|
|
218
|
+
it("CCTP same-chain doesn't throw", async () => {
|
|
219
|
+
const quote = await getCctpQuote("arbitrum", "arbitrum", 100);
|
|
220
|
+
expect(quote.provider).toBe("cctp");
|
|
221
|
+
expect(quote.fee).toBeLessThan(1);
|
|
222
|
+
expect(quote.amountOut).toBeGreaterThan(99);
|
|
223
|
+
});
|
|
224
|
+
it("CCTP various amounts", async () => {
|
|
225
|
+
for (const amt of [0.01, 1, 100, 1000000]) {
|
|
226
|
+
const q = await getCctpQuote("arbitrum", "base", amt);
|
|
227
|
+
// Forwarding fee ~$0.22
|
|
228
|
+
expect(q.amountOut).toBeGreaterThanOrEqual(amt - 0.50);
|
|
229
|
+
expect(q.fee).toBeLessThan(1);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
it("getBestQuote: all routes return valid cheapest provider", async () => {
|
|
233
|
+
const routes = [
|
|
234
|
+
["solana", "arbitrum", DUMMY_SOLANA, DUMMY_EVM],
|
|
235
|
+
["arbitrum", "base", DUMMY_EVM, DUMMY_EVM],
|
|
236
|
+
["base", "solana", DUMMY_EVM, DUMMY_SOLANA],
|
|
237
|
+
];
|
|
238
|
+
for (const [src, dst, sender, recipient] of routes) {
|
|
239
|
+
const quote = await getBestQuote(src, dst, 100, sender, recipient);
|
|
240
|
+
expect(["cctp", "relay", "debridge"]).toContain(quote.provider);
|
|
241
|
+
expect(quote.amountIn).toBeGreaterThanOrEqual(quote.amountOut);
|
|
242
|
+
expect(quote.amountOut).toBeGreaterThan(0);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
// ══════════════════════════════════════════════════════════
|
|
247
|
+
// CLI Command Integration (via process spawn)
|
|
248
|
+
// ══════════════════════════════════════════════════════════
|
|
249
|
+
describe("CLI bridge commands", () => {
|
|
250
|
+
const CLI_CWD = "/Users/hik/Documents/GitHub/pacifica/packages/cli";
|
|
251
|
+
const CLI_CMD = "npx tsx src/index.ts";
|
|
252
|
+
function runCliSafe(args) {
|
|
253
|
+
const { execSync } = require("child_process");
|
|
254
|
+
try {
|
|
255
|
+
const stdout = execSync(`${CLI_CMD} ${args}`, {
|
|
256
|
+
encoding: "utf-8",
|
|
257
|
+
cwd: CLI_CWD,
|
|
258
|
+
timeout: 25000,
|
|
259
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
|
260
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
261
|
+
});
|
|
262
|
+
return { stdout, stderr: "", exitCode: 0 };
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
const e = err;
|
|
266
|
+
return {
|
|
267
|
+
stdout: e.stdout ?? "",
|
|
268
|
+
stderr: e.stderr ?? "",
|
|
269
|
+
exitCode: e.status ?? 1,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
it("bridge chains: returns chain list as JSON", () => {
|
|
274
|
+
const { stdout } = runCliSafe("--json bridge chains");
|
|
275
|
+
const parsed = JSON.parse(stdout);
|
|
276
|
+
expect(parsed.ok).toBe(true);
|
|
277
|
+
expect(parsed.data.chains).toBeDefined();
|
|
278
|
+
expect(parsed.data.usdc).toBeDefined();
|
|
279
|
+
expect(parsed.data.exchanges).toBeDefined();
|
|
280
|
+
expect(parsed.data.chains.solana).toBe(7565164);
|
|
281
|
+
expect(parsed.data.chains.arbitrum).toBe(42161);
|
|
282
|
+
});
|
|
283
|
+
it("bridge chains: text mode has chain names", () => {
|
|
284
|
+
const { stdout } = runCliSafe("bridge chains");
|
|
285
|
+
expect(stdout).toContain("solana");
|
|
286
|
+
expect(stdout).toContain("arbitrum");
|
|
287
|
+
expect(stdout).toContain("ethereum");
|
|
288
|
+
});
|
|
289
|
+
it("bridge quote: CCTP route returns JSON (no deBridge API call)", () => {
|
|
290
|
+
// arbitrum → ethereum uses CCTP, avoids deBridge rate limit
|
|
291
|
+
const { stdout } = runCliSafe("--json bridge quote --from arbitrum --to ethereum --amount 500");
|
|
292
|
+
const parsed = JSON.parse(stdout);
|
|
293
|
+
expect(parsed.ok).toBe(true);
|
|
294
|
+
expect(parsed.data.provider).toBe("cctp");
|
|
295
|
+
expect(parsed.data.srcChain).toBe("arbitrum");
|
|
296
|
+
expect(parsed.data.dstChain).toBe("ethereum");
|
|
297
|
+
expect(parsed.data.amountIn).toBe(500);
|
|
298
|
+
expect(parsed.data.fee).toBeLessThan(1); // forwarding fee ~$0.22
|
|
299
|
+
expect(parsed.data.estimatedTime).toBeGreaterThan(0);
|
|
300
|
+
});
|
|
301
|
+
it("bridge --help lists subcommands", () => {
|
|
302
|
+
const { stdout } = runCliSafe("bridge --help");
|
|
303
|
+
expect(stdout).toContain("chains");
|
|
304
|
+
expect(stdout).toContain("quote");
|
|
305
|
+
expect(stdout).toContain("send");
|
|
306
|
+
expect(stdout).toContain("status");
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { writeFileSync, unlinkSync, existsSync } from "fs";
|
|
3
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
4
|
+
const CLI_CWD = "/Users/hik/Documents/GitHub/pacifica/packages/cli";
|
|
5
|
+
const CLI_CMD = "npx tsx src/index.ts";
|
|
6
|
+
/** Temp files created during tests, cleaned up in afterAll */
|
|
7
|
+
const tempFiles = [];
|
|
8
|
+
function runCli(args) {
|
|
9
|
+
return execSync(`${CLI_CMD} ${args}`, {
|
|
10
|
+
encoding: "utf-8",
|
|
11
|
+
cwd: CLI_CWD,
|
|
12
|
+
timeout: 25000,
|
|
13
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function runCliSafe(args) {
|
|
17
|
+
try {
|
|
18
|
+
const stdout = execSync(`${CLI_CMD} ${args}`, {
|
|
19
|
+
encoding: "utf-8",
|
|
20
|
+
cwd: CLI_CWD,
|
|
21
|
+
timeout: 25000,
|
|
22
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
24
|
+
});
|
|
25
|
+
return { stdout, stderr: "", exitCode: 0 };
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const e = err;
|
|
29
|
+
return {
|
|
30
|
+
stdout: e.stdout ?? "",
|
|
31
|
+
stderr: e.stderr ?? "",
|
|
32
|
+
exitCode: e.status ?? 1,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function writeTempFile(name, content) {
|
|
37
|
+
const path = `/tmp/${name}`;
|
|
38
|
+
writeFileSync(path, content, "utf-8");
|
|
39
|
+
tempFiles.push(path);
|
|
40
|
+
return path;
|
|
41
|
+
}
|
|
42
|
+
afterAll(() => {
|
|
43
|
+
for (const f of tempFiles) {
|
|
44
|
+
if (existsSync(f)) {
|
|
45
|
+
try {
|
|
46
|
+
unlinkSync(f);
|
|
47
|
+
}
|
|
48
|
+
catch { /* ignore */ }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
describe("CLI E2E Integration Tests", { timeout: 30000 }, () => {
|
|
53
|
+
// ───────────────────── schema command ─────────────────────
|
|
54
|
+
describe("perp schema --json", () => {
|
|
55
|
+
let schema;
|
|
56
|
+
/** schema may be wrapped in envelope { ok, data } or raw */
|
|
57
|
+
function parseSchema(output) {
|
|
58
|
+
const parsed = JSON.parse(output);
|
|
59
|
+
return (parsed.data ?? parsed);
|
|
60
|
+
}
|
|
61
|
+
it("outputs valid JSON with expected top-level structure", () => {
|
|
62
|
+
const output = runCli("schema");
|
|
63
|
+
schema = parseSchema(output);
|
|
64
|
+
expect(schema).toHaveProperty("schemaVersion");
|
|
65
|
+
expect(schema).toHaveProperty("commands");
|
|
66
|
+
expect(schema).toHaveProperty("errorCodes");
|
|
67
|
+
expect(schema).toHaveProperty("exchanges");
|
|
68
|
+
expect(Array.isArray(schema.commands)).toBe(true);
|
|
69
|
+
expect(Array.isArray(schema.exchanges)).toBe(true);
|
|
70
|
+
expect(typeof schema.errorCodes).toBe("object");
|
|
71
|
+
});
|
|
72
|
+
it("commands array contains known command names", () => {
|
|
73
|
+
const output = runCli("schema");
|
|
74
|
+
schema = parseSchema(output);
|
|
75
|
+
const commandNames = schema.commands.map((c) => c.name);
|
|
76
|
+
expect(commandNames).toContain("market");
|
|
77
|
+
expect(commandNames).toContain("account");
|
|
78
|
+
expect(commandNames).toContain("trade");
|
|
79
|
+
expect(commandNames).toContain("arb");
|
|
80
|
+
expect(commandNames).toContain("plan");
|
|
81
|
+
});
|
|
82
|
+
it("errorCodes contains key error types with retryable flags", () => {
|
|
83
|
+
const output = runCli("schema");
|
|
84
|
+
schema = parseSchema(output);
|
|
85
|
+
const errorCodes = schema.errorCodes;
|
|
86
|
+
expect(errorCodes).toHaveProperty("INSUFFICIENT_BALANCE");
|
|
87
|
+
expect(errorCodes.INSUFFICIENT_BALANCE.retryable).toBe(false);
|
|
88
|
+
expect(errorCodes).toHaveProperty("RATE_LIMITED");
|
|
89
|
+
expect(errorCodes.RATE_LIMITED.retryable).toBe(true);
|
|
90
|
+
expect(errorCodes).toHaveProperty("TIMEOUT");
|
|
91
|
+
expect(errorCodes.TIMEOUT.retryable).toBe(true);
|
|
92
|
+
expect(errorCodes).toHaveProperty("EXCHANGE_UNREACHABLE");
|
|
93
|
+
expect(errorCodes.EXCHANGE_UNREACHABLE.retryable).toBe(true);
|
|
94
|
+
expect(errorCodes).toHaveProperty("UNKNOWN");
|
|
95
|
+
expect(errorCodes.UNKNOWN.retryable).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// ───────────────────── plan commands ─────────────────────
|
|
99
|
+
describe("perp plan example", () => {
|
|
100
|
+
it("outputs valid JSON with version 1.0 and steps array", () => {
|
|
101
|
+
const output = runCli("plan example");
|
|
102
|
+
const parsed = JSON.parse(output);
|
|
103
|
+
// plan example may be wrapped in envelope (ok/data) or raw
|
|
104
|
+
const plan = parsed.data ?? parsed;
|
|
105
|
+
expect(plan.version).toBe("1.0");
|
|
106
|
+
expect(Array.isArray(plan.steps)).toBe(true);
|
|
107
|
+
expect(plan.steps.length).toBeGreaterThan(0);
|
|
108
|
+
// Each step should have id, action, params
|
|
109
|
+
for (const step of plan.steps) {
|
|
110
|
+
expect(step).toHaveProperty("id");
|
|
111
|
+
expect(step).toHaveProperty("action");
|
|
112
|
+
expect(step).toHaveProperty("params");
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe("perp plan validate", () => {
|
|
117
|
+
it("succeeds for a valid plan (exit 0, output contains 'valid')", () => {
|
|
118
|
+
const validPlan = {
|
|
119
|
+
version: "1.0",
|
|
120
|
+
description: "Test plan",
|
|
121
|
+
steps: [
|
|
122
|
+
{
|
|
123
|
+
id: "step1",
|
|
124
|
+
action: "check_balance",
|
|
125
|
+
params: { minAvailable: 50 },
|
|
126
|
+
onFailure: "abort",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "step2",
|
|
130
|
+
action: "market_order",
|
|
131
|
+
params: { symbol: "ETH", side: "buy", size: "0.1" },
|
|
132
|
+
onFailure: "abort",
|
|
133
|
+
dependsOn: "step1",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
const filePath = writeTempFile("test-valid-plan.json", JSON.stringify(validPlan, null, 2));
|
|
138
|
+
const { stdout, exitCode } = runCliSafe(`plan validate ${filePath}`);
|
|
139
|
+
expect(exitCode).toBe(0);
|
|
140
|
+
// The human-readable output says "valid" or the JSON output includes valid:true
|
|
141
|
+
const lower = stdout.toLowerCase();
|
|
142
|
+
expect(lower).toContain("valid");
|
|
143
|
+
});
|
|
144
|
+
it("reports errors for an invalid plan (wrong version) with --json", () => {
|
|
145
|
+
const invalidPlan = {
|
|
146
|
+
version: "999.0",
|
|
147
|
+
steps: [
|
|
148
|
+
{
|
|
149
|
+
id: "bad",
|
|
150
|
+
action: "market_order",
|
|
151
|
+
params: {},
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
const filePath = writeTempFile("test-invalid-plan.json", JSON.stringify(invalidPlan, null, 2));
|
|
156
|
+
const { stdout, exitCode } = runCliSafe(`--json plan validate ${filePath}`);
|
|
157
|
+
// Should still exit 0 because validation itself succeeds (reports errors in JSON)
|
|
158
|
+
expect(exitCode).toBe(0);
|
|
159
|
+
const parsed = JSON.parse(stdout);
|
|
160
|
+
expect(parsed.ok).toBe(true);
|
|
161
|
+
// The data.valid should be false
|
|
162
|
+
expect(parsed.data.valid).toBe(false);
|
|
163
|
+
expect(Array.isArray(parsed.data.errors)).toBe(true);
|
|
164
|
+
expect(parsed.data.errors.length).toBeGreaterThan(0);
|
|
165
|
+
// Should mention version mismatch
|
|
166
|
+
const allErrors = parsed.data.errors.join(" ");
|
|
167
|
+
expect(allErrors).toContain("version");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
// ───────────────────── --json error wrapping ─────────────────────
|
|
171
|
+
describe("--json structured error output", () => {
|
|
172
|
+
it("outputs structured JSON error for a nonexistent plan file", () => {
|
|
173
|
+
// Use plan validate with a file that does not exist — this triggers
|
|
174
|
+
// withJsonErrors which wraps the ENOENT in the standard envelope.
|
|
175
|
+
const { stdout, exitCode } = runCliSafe("--json plan validate /tmp/__nonexistent_cli_test_file_99999.json");
|
|
176
|
+
expect(exitCode).toBe(0);
|
|
177
|
+
const parsed = JSON.parse(stdout);
|
|
178
|
+
expect(parsed.ok).toBe(false);
|
|
179
|
+
expect(parsed.error).toBeDefined();
|
|
180
|
+
expect(typeof parsed.error.code).toBe("string");
|
|
181
|
+
expect(typeof parsed.error.message).toBe("string");
|
|
182
|
+
expect(parsed.error.message).toContain("ENOENT");
|
|
183
|
+
expect(typeof parsed.error.retryable).toBe("boolean");
|
|
184
|
+
expect(parsed).toHaveProperty("meta");
|
|
185
|
+
expect(parsed.meta).toHaveProperty("timestamp");
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
// ───────────────────── help output ─────────────────────
|
|
189
|
+
describe("perp --help", () => {
|
|
190
|
+
it("includes all major commands in help output", () => {
|
|
191
|
+
const { stdout } = runCliSafe("--help");
|
|
192
|
+
const helpText = stdout.toLowerCase();
|
|
193
|
+
expect(helpText).toContain("schema");
|
|
194
|
+
expect(helpText).toContain("plan");
|
|
195
|
+
expect(helpText).toContain("trade");
|
|
196
|
+
expect(helpText).toContain("stream");
|
|
197
|
+
expect(helpText).toContain("market");
|
|
198
|
+
expect(helpText).toContain("account");
|
|
199
|
+
expect(helpText).toContain("arb");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { fetchAllDexAssets, findDexArbPairs, scanDexArb } from "../../dex-asset-map.js";
|
|
3
|
+
/**
|
|
4
|
+
* Integration tests for HIP-3 cross-dex arb scanning.
|
|
5
|
+
*
|
|
6
|
+
* Hits real Hyperliquid mainnet API (read-only, no private key needed).
|
|
7
|
+
*
|
|
8
|
+
* Run: pnpm --filter perp-cli test -- --testPathPattern dex-arb.integration
|
|
9
|
+
*/
|
|
10
|
+
describe("fetchAllDexAssets — live API", () => {
|
|
11
|
+
it("returns assets from native HL and at least 3 deployed dexes", async () => {
|
|
12
|
+
const assets = await fetchAllDexAssets();
|
|
13
|
+
expect(assets.length).toBeGreaterThan(100);
|
|
14
|
+
// Check we have native HL assets
|
|
15
|
+
const hlAssets = assets.filter(a => a.dex === "hl");
|
|
16
|
+
expect(hlAssets.length).toBeGreaterThan(100);
|
|
17
|
+
expect(hlAssets.find(a => a.base === "BTC")).toBeTruthy();
|
|
18
|
+
// Check we have at least xyz dex
|
|
19
|
+
const xyzAssets = assets.filter(a => a.dex === "xyz");
|
|
20
|
+
expect(xyzAssets.length).toBeGreaterThan(10);
|
|
21
|
+
expect(xyzAssets.find(a => a.base === "TSLA")).toBeTruthy();
|
|
22
|
+
// Verify data shape
|
|
23
|
+
for (const asset of assets.slice(0, 10)) {
|
|
24
|
+
expect(asset.markPrice).toBeGreaterThan(0);
|
|
25
|
+
expect(typeof asset.fundingRate).toBe("number");
|
|
26
|
+
expect(asset.dex).toBeTruthy();
|
|
27
|
+
expect(asset.base).toBeTruthy();
|
|
28
|
+
expect(asset.raw).toBeTruthy();
|
|
29
|
+
}
|
|
30
|
+
const dexes = new Set(assets.map(a => a.dex));
|
|
31
|
+
console.log(`Dexes found: ${[...dexes].join(", ")} (${dexes.size} total)`);
|
|
32
|
+
console.log(`Total active assets: ${assets.length}`);
|
|
33
|
+
}, 30000);
|
|
34
|
+
});
|
|
35
|
+
describe("findDexArbPairs — live data", () => {
|
|
36
|
+
it("finds TSLA arb pairs across dexes", async () => {
|
|
37
|
+
const assets = await fetchAllDexAssets();
|
|
38
|
+
const tslaAssets = assets.filter(a => a.base === "TSLA");
|
|
39
|
+
expect(tslaAssets.length).toBeGreaterThanOrEqual(2);
|
|
40
|
+
const pairs = findDexArbPairs(tslaAssets);
|
|
41
|
+
// C(n,2) pairs if n dexes have TSLA
|
|
42
|
+
expect(pairs.length).toBeGreaterThanOrEqual(1);
|
|
43
|
+
for (const p of pairs) {
|
|
44
|
+
expect(p.underlying).toBe("TSLA");
|
|
45
|
+
expect(p.priceGapPct).toBeLessThan(2); // prices should be very close
|
|
46
|
+
expect(p.long.dex).not.toBe(p.short.dex);
|
|
47
|
+
}
|
|
48
|
+
const dexes = new Set(tslaAssets.map(a => a.dex));
|
|
49
|
+
console.log(`TSLA on dexes: ${[...dexes].join(", ")} → ${pairs.length} arb pairs`);
|
|
50
|
+
for (const p of pairs.slice(0, 3)) {
|
|
51
|
+
console.log(` L:${p.long.dex} S:${p.short.dex} spread:${p.annualSpread.toFixed(1)}% gap:${p.priceGapPct.toFixed(3)}%`);
|
|
52
|
+
}
|
|
53
|
+
}, 30000);
|
|
54
|
+
it("finds BTC arb between hl native and hyna dex", async () => {
|
|
55
|
+
const assets = await fetchAllDexAssets();
|
|
56
|
+
const btcAssets = assets.filter(a => a.base === "BTC");
|
|
57
|
+
const hlBTC = btcAssets.find(a => a.dex === "hl");
|
|
58
|
+
const hynaBTC = btcAssets.find(a => a.dex === "hyna");
|
|
59
|
+
expect(hlBTC).toBeTruthy();
|
|
60
|
+
expect(hynaBTC).toBeTruthy();
|
|
61
|
+
// Prices should be very close (< 0.5%)
|
|
62
|
+
const gap = Math.abs(hlBTC.markPrice - hynaBTC.markPrice) / hlBTC.markPrice * 100;
|
|
63
|
+
expect(gap).toBeLessThan(0.5);
|
|
64
|
+
console.log(`BTC: hl=$${hlBTC.markPrice.toFixed(0)} hyna=$${hynaBTC.markPrice.toFixed(0)} gap:${gap.toFixed(4)}%`);
|
|
65
|
+
}, 30000);
|
|
66
|
+
it("correctly rejects USAR vs US500 (different products)", async () => {
|
|
67
|
+
const assets = await fetchAllDexAssets();
|
|
68
|
+
const usar = assets.find(a => a.base === "USAR");
|
|
69
|
+
const us500 = assets.find(a => a.base === "US500");
|
|
70
|
+
if (usar && us500) {
|
|
71
|
+
// Prices should be wildly different
|
|
72
|
+
const gap = Math.abs(usar.markPrice - us500.markPrice) / Math.min(usar.markPrice, us500.markPrice) * 100;
|
|
73
|
+
expect(gap).toBeGreaterThan(100); // way more than 5%
|
|
74
|
+
// findDexArbPairs should not match these
|
|
75
|
+
const pairs = findDexArbPairs([usar, us500]);
|
|
76
|
+
expect(pairs).toHaveLength(0);
|
|
77
|
+
console.log(`USAR=$${usar.markPrice.toFixed(2)} vs US500=$${us500.markPrice.toFixed(2)} → gap:${gap.toFixed(0)}% → correctly rejected`);
|
|
78
|
+
}
|
|
79
|
+
}, 30000);
|
|
80
|
+
});
|
|
81
|
+
describe("scanDexArb — live full scan", () => {
|
|
82
|
+
it("returns sorted arb opportunities", async () => {
|
|
83
|
+
const pairs = await scanDexArb({ minAnnualSpread: 5 });
|
|
84
|
+
expect(pairs.length).toBeGreaterThan(0);
|
|
85
|
+
// Should be sorted by annualSpread descending
|
|
86
|
+
for (let i = 1; i < pairs.length; i++) {
|
|
87
|
+
expect(pairs[i].annualSpread).toBeLessThanOrEqual(pairs[i - 1].annualSpread);
|
|
88
|
+
}
|
|
89
|
+
// All pairs should have different dexes
|
|
90
|
+
for (const p of pairs) {
|
|
91
|
+
expect(p.long.dex).not.toBe(p.short.dex);
|
|
92
|
+
}
|
|
93
|
+
// All pairs should have < 5% price gap
|
|
94
|
+
for (const p of pairs) {
|
|
95
|
+
expect(p.priceGapPct).toBeLessThan(5);
|
|
96
|
+
}
|
|
97
|
+
console.log(`Found ${pairs.length} arb opportunities (>5% annual spread)`);
|
|
98
|
+
console.log(`Top 5:`);
|
|
99
|
+
for (const p of pairs.slice(0, 5)) {
|
|
100
|
+
console.log(` ${p.underlying}: ${p.annualSpread.toFixed(1)}% [${p.long.dex}↔${p.short.dex}]`);
|
|
101
|
+
}
|
|
102
|
+
}, 30000);
|
|
103
|
+
it("no-native mode excludes HL base assets", async () => {
|
|
104
|
+
const withNative = await scanDexArb({ minAnnualSpread: 0, includeNative: true });
|
|
105
|
+
const withoutNative = await scanDexArb({ minAnnualSpread: 0, includeNative: false });
|
|
106
|
+
// Without native, should have fewer pairs (no hl↔dex pairs)
|
|
107
|
+
const nativePairs = withNative.filter(p => p.long.dex === "hl" || p.short.dex === "hl");
|
|
108
|
+
const nonNativePairs = withoutNative.filter(p => p.long.dex === "hl" || p.short.dex === "hl");
|
|
109
|
+
expect(nonNativePairs.length).toBe(0);
|
|
110
|
+
if (nativePairs.length > 0) {
|
|
111
|
+
expect(withNative.length).toBeGreaterThan(withoutNative.length);
|
|
112
|
+
}
|
|
113
|
+
console.log(`With native: ${withNative.length} pairs (${nativePairs.length} include HL native)`);
|
|
114
|
+
console.log(`Without native: ${withoutNative.length} pairs`);
|
|
115
|
+
}, 30000);
|
|
116
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests verifying JSON envelope consistency across all CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Every --json output must:
|
|
5
|
+
* 1. Be valid JSON (single object, no extra text)
|
|
6
|
+
* 2. Have ok: boolean
|
|
7
|
+
* 3. If ok=true: have data and meta.timestamp
|
|
8
|
+
* 4. If ok=false: have error.code, error.message, and meta.timestamp
|
|
9
|
+
*
|
|
10
|
+
* These tests spawn the real CLI process to catch any console.log leaks,
|
|
11
|
+
* chalk output in JSON mode, or missing envelope wrappers.
|
|
12
|
+
*/
|
|
13
|
+
import "dotenv/config";
|