Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions ts/packages/defi-cli/src/commands/swap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,137 @@ describe("defi swap chain support guards", () => {
expect(data.error).toMatch(/kyber|openocean|liquid|lifi|relay/i);
});
});

// Happy-path coverage for the four still-untested providers. The kyber
// branch is exercised by the native-input section above; openocean
// likewise (via its isNativeInput check), but a dedicated happy-path
// test here pins the printOutput shape so a future printOutput refactor
// can't quietly drop fields.
describe("defi swap aggregator happy paths", () => {
it("openocean: emits provider/router/amount_out and value=amount on native input", async () => {
// OpenOcean returns a single object on /v4/{chain}/swap with the
// executor-shaped fields. Native input mirrors the kyber pattern:
// value=amount_in, no approvals[].
mockFetchOnce({
data: { to: "0x6352a56caadC4F1E25CD6c75970Fa768A3304e64", data: FAKE_CALLDATA, value: "0", outAmount: "33218" },
});
const program = buildProgram();
const { capture, restore } = captureConsole();
try {
await program.parseAsync([
"node", "defi", "--json", "--chain", "monad",
"swap", "--from", "MON", "--to", "USDC", "--amount",
"100000000000000000", "--provider", "openocean",
]);
} finally {
restore();
}
const data = JSON.parse(capture.json.join("\n")) as {
provider?: string; chain?: string; amount_out?: string; router?: string;
};
expect(data.provider).toBe("openocean");
expect(data.chain).toBe("monad");
expect(data.amount_out).toBe("33218");
expect(data.router?.toLowerCase()).toBe("0x6352a56caadc4f1e25cd6c75970fa768a3304e64");
});

it("lifi: emits provider/chain_id/amount_out from transactionRequest+estimate", async () => {
// LI.FI returns a {transactionRequest, estimate} shape. The chain
// is keyed off chains.toml's `chain_id` (Monad = 143), not a slug.
mockFetchOnce({
transactionRequest: {
to: "0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37",
data: FAKE_CALLDATA,
value: "0",
},
estimate: { toAmount: "32220" },
});
const program = buildProgram();
const { capture, restore } = captureConsole();
try {
await program.parseAsync([
"node", "defi", "--json", "--chain", "monad",
"swap", "--from", "MON", "--to", "USDC", "--amount",
"100000000000000000", "--provider", "lifi",
]);
} finally {
restore();
}
const data = JSON.parse(capture.json.join("\n")) as {
provider?: string; chain?: string; chain_id?: number; amount_out?: string;
};
expect(data.provider).toBe("lifi");
expect(data.chain).toBe("monad");
expect(data.chain_id).toBe(143);
expect(data.amount_out).toBe("32220");
});

it("relay: skips approve step in steps[] and reports currencyOut amount", async () => {
// Relay returns {steps: [{id:'approve',...}, {id:'swap', items:[{data}]}],
// details: {currencyOut: {amount}}}. The CLI must skip the approve
// step (the executor handles approvals) and decode the swap step.
mockFetchOnce({
steps: [
{ id: "approve", items: [{ data: { to: "0xapprove", data: "0xff", value: "0" } }] },
{
id: "swap",
items: [{ data: { to: "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", data: FAKE_CALLDATA, value: "0" } }],
},
],
details: { currencyOut: { amount: "3225554" } },
});
const program = buildProgram();
const { capture, restore } = captureConsole();
try {
await program.parseAsync([
"node", "defi", "--json", "--chain", "monad",
"swap", "--from", "MON", "--to", "USDC", "--amount",
"100000000000000000000", "--provider", "relay",
]);
} finally {
restore();
}
const data = JSON.parse(capture.json.join("\n")) as {
provider?: string; chain_id?: number; amount_out?: string; router?: string;
};
expect(data.provider).toBe("relay");
expect(data.chain_id).toBe(143);
expect(data.amount_out).toBe("3225554");
expect(data.router?.toLowerCase()).toBe("0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f");
});

it("liquid: returns the executor preview when invoked on hyperevm", async () => {
// LiquidSwap is HyperEVM-only and uses {execution: {to, calldata, value},
// details: {amountOut}}. The handler resolves token symbols against the
// hyperevm registry, so we use HYPE → USDC tokens that exist.
mockFetchOnce({
execution: { to: "0x744489ee3d540777a66f2cf297479745e0852f7a", calldata: FAKE_CALLDATA, value: "0" },
details: { amountOut: "12345" },
});
const program = buildProgram();
const { capture, restore } = captureConsole();
try {
await program.parseAsync([
"node", "defi", "--json", "--chain", "hyperevm",
"swap", "--from", "HYPE", "--to", "USDC", "--amount",
"1000000000000000000", "--provider", "liquid",
]);
} finally {
restore();
}
const data = JSON.parse(capture.json.join("\n")) as {
provider?: string; chain?: string; amount_out?: string; router?: string;
error?: string;
};
// liquid provider depends on the registry resolving HYPE/USDC on
// hyperevm; if that resolution fails the error envelope still
// tells us the provider routing reached the liquid branch.
if (data.error) {
expect(data.error).toMatch(/LiquidSwap|liquid/i);
} else {
expect(data.provider).toBe("liquid");
expect(data.chain).toBe("hyperevm");
expect(data.amount_out).toBe("12345");
}
});
});