Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ jobs:
if: always()
working-directory: apps/api
env:
# Live test: skips the hermetic preload, whose fetch guard would block these calls.
RUN_LIVE_TESTS: "1"
VORTEX_QUOTE_SMOKE_URLS: https://api-staging.vortexfinance.co,https://api.vortexfinance.co
run: bun test src/tests/deployed-quotes.e2e.test.ts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { privateKeyToAccount } from "viem/accounts";
import * as evmFundingNamespace from "../core/evm-funding";
import * as partnerPricingNamespace from "../../../partners/partner-pricing.service";
import type { QuoteTicketAttributes } from "../../../../../models/quoteTicket.model";
import { installFakeChainIdRpc } from "../../../../../test-utils/fake-world/fake-evm";
import Big from "big.js";
import { decodeFunctionData, erc20Abi, parseTransaction } from "viem";
import type { FlowMetadata } from "../core/metadata";
Expand Down Expand Up @@ -310,13 +311,15 @@ describe("BRL onramp Base cross-chain transactions", () => {
type: EphemeralAccountType.EVM
};

const presignedTxs = await signUnsignedTransactions(blocks.unsignedTxs, { evmEphemeral });
// viem asks the RPC for eth_chainId before signing; any other network use hits the fetch guard.
const restoreChainIdRpc = installFakeChainIdRpc();
const presignedTxs = await signUnsignedTransactions(blocks.unsignedTxs, { evmEphemeral }).finally(restoreChainIdRpc);
expect(presignedTxs.length).toBeGreaterThanOrEqual(blocks.unsignedTxs.length);
expect(presignedTxs.every(tx => typeof tx.txData === "string" && tx.txData.startsWith("0x"))).toBe(true);
const destinationTransfer = presignedTxs.find(tx => tx.phase === "destinationTransfer");
expect(destinationTransfer).toBeDefined();
expect(parseTransaction(destinationTransfer?.txData as `0x${string}`).maxFeePerGas).toBe(3_000_000_000n);
}, 60_000);
});

it("preserves 18-decimal BSC USDT precision in the destination transfer", async () => {
const amountRaw = "17500000000000000000";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { privateKeyToAccount } from "viem/accounts";
import * as evmFundingNamespace from "../core/evm-funding";
import * as partnerPricingNamespace from "../../../partners/partner-pricing.service";
import type { QuoteTicketAttributes } from "../../../../../models/quoteTicket.model";
import { installFakeChainIdRpc } from "../../../../../test-utils/fake-world/fake-evm";

const sharedReal = { ...sharedNamespace };
const evmFundingReal = { ...evmFundingNamespace };
Expand Down Expand Up @@ -185,7 +186,9 @@ describe("EUR onramp Base cross-chain transactions", () => {
});

const evmEphemeral = { address: EPHEMERAL, secret: PRIVATE_KEY, type: EphemeralAccountType.EVM };
const signed = await signUnsignedTransactions(prepared.unsignedTxs, { evmEphemeral });
// viem asks the RPC for eth_chainId before signing; any other network use hits the fetch guard.
const restoreChainIdRpc = installFakeChainIdRpc();
const signed = await signUnsignedTransactions(prepared.unsignedTxs, { evmEphemeral }).finally(restoreChainIdRpc);
expect(signed.length).toBeGreaterThanOrEqual(prepared.unsignedTxs.length);
expect(prepared.stateMeta.phaseFlow).toEqual([
"initial",
Expand Down Expand Up @@ -236,5 +239,5 @@ describe("EUR onramp Base cross-chain transactions", () => {
expect(prepared.unsignedTxs.find(tx => tx.phase === "nablaApprove")?.txData).toMatchObject({ data: "0xc1" });
expect(prepared.unsignedTxs.find(tx => tx.phase === "squidRouterSwap")?.txData).toMatchObject({ data: "0xa2" });
expect(prepared.unsignedTxs.find(tx => tx.phase === "backupSquidRouterSwap")?.txData).toMatchObject({ data: "0xb2" });
}, 60_000);
});
});
36 changes: 35 additions & 1 deletion apps/api/src/test-utils/fake-world/fake-evm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EvmClientManager, type EvmNetworks } from "@vortexfi/shared";
import { keccak256 } from "viem";
import { keccak256, toHex } from "viem";
import { arbitrum, avalanche, base, baseSepolia, bsc, mainnet, polygon, polygonAmoy } from "viem/chains";

export interface RecordedEvmTx {
network: string;
Expand Down Expand Up @@ -35,6 +36,14 @@ const CHAIN_IDS: Record<string, number> = {

const MAX_UINT256 = 2n ** 256n - 1n;

// viem's default public RPC (what a bare http() transport targets, URL-normalized like viem does) → chain id.
const DEFAULT_RPC_CHAIN_IDS = new Map<string, number>(
[arbitrum, avalanche, base, baseSepolia, bsc, mainnet, polygon, polygonAmoy].map(chain => [
new URL(chain.rpcUrls.default.http[0]).href,
chain.id
])
);

/**
* In-memory EVM world standing in for EvmClientManager. Balances are a simple
* ledger keyed by network/token/holder; transactions are recorded, assigned a
Expand Down Expand Up @@ -311,6 +320,31 @@ export class FakeEvm {
}
}

/**
* Client-side signing (signUnsignedTransactions, also used by the SDK) builds its own viem wallet
* clients outside the EvmClientManager seam, and viem's signTransaction asks the RPC for
* eth_chainId before signing locally. Answers exactly that call for viem's default public RPCs;
* every other request falls through to the fetch guard. Returns the restore function.
*/
export function installFakeChainIdRpc(): () => void {
const guardedFetch = globalThis.fetch;
const fake = (async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
const chainId = DEFAULT_RPC_CHAIN_IDS.get(url);
if (chainId !== undefined && typeof init?.body === "string") {
const payload = JSON.parse(init.body) as { id?: number; method?: string };
if (payload.method === "eth_chainId") {
return Response.json({ id: payload.id ?? 1, jsonrpc: "2.0", result: toHex(chainId) });
}
}
return guardedFetch(input, init);
}) as typeof fetch;
globalThis.fetch = Object.assign(fake, guardedFetch);
return () => {
globalThis.fetch = guardedFetch;
};
}

export function installFakeEvm(): { fakeEvm: FakeEvm; restore: () => void } {
const original = EvmClientManager.getInstance;
const fakeEvm = new FakeEvm();
Expand Down
23 changes: 23 additions & 0 deletions apps/api/src/test-utils/fake-world/fetch-guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from "bun:test";
import { createPublicClient, http } from "viem";
import { base } from "viem/chains";
import { installFakeChainIdRpc } from "./fake-evm";

// No installFakeWorld here: the preload alone must keep viem RPC traffic off the network.
describe("fetch guard", () => {
const client = createPublicClient({ chain: base, transport: http(undefined, { retryCount: 0 }) });

it("rejects viem RPC calls to a real chain", async () => {
await expect(client.getChainId()).rejects.toThrow(/Hermetic test violation/);
});

it("lets the fake answer eth_chainId and nothing else", async () => {
const restore = installFakeChainIdRpc();
try {
expect(await client.getChainId()).toBe(base.id);
await expect(client.getBlockNumber()).rejects.toThrow(/Hermetic test violation/);
} finally {
restore();
}
});
});
11 changes: 7 additions & 4 deletions apps/api/src/test-utils/fake-world/fetch-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
* Replaces global fetch with a guard that only allows loopback traffic
* (the in-process test app). Any other HTTP call is a hermeticity violation
* and fails with a descriptive error instead of silently reaching a real
* service.
* service. viem's http transport goes through global fetch, so RPC calls are
* covered too. The test preload installs it for every hermetic run.
*/
let originalFetch: typeof fetch | null = null;

const LOOPBACK_PATTERN = /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:|\/|$)/;

export function installFetchGuard(): void {
/** Returns the uninstaller; a no-op when an outer caller (the preload) already owns the guard. */
export function installFetchGuard(): () => void {
if (originalFetch) {
return;
return () => undefined;
}
const realFetch = globalThis.fetch;
originalFetch = realFetch;
Expand All @@ -29,9 +31,10 @@ export function installFetchGuard(): void {
}) as typeof fetch;

globalThis.fetch = Object.assign(guard, realFetch);
return uninstallFetchGuard;
}

export function uninstallFetchGuard(): void {
function uninstallFetchGuard(): void {
if (originalFetch) {
globalThis.fetch = originalFetch;
originalFetch = null;
Expand Down
8 changes: 4 additions & 4 deletions apps/api/src/test-utils/fake-world/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { ApiManager } from "@vortexfi/shared";
import { type FakeAlfredpay, type FakeBrla, type FakeMykobo, installFakeAnchors } from "./fake-anchors";
import { installBackgroundWorkTracking } from "./fake-background-work";
import { type FakeEvm, installFakeEvm } from "./fake-evm";
import { type FakeEvm, installFakeChainIdRpc, installFakeEvm } from "./fake-evm";
import { type FakeMonerium, installFakeMonerium } from "./fake-monerium";
import { type FakePrices, installFakePrices } from "./fake-prices";
import { type FakeSquidRouter, installFakeSquidRouter } from "./fake-squidrouter";
import { installFetchGuard, uninstallFetchGuard } from "./fetch-guard";
import { installFetchGuard } from "./fetch-guard";

export type { FakeAlfredpay, FakeBrla, FakeEvm, FakeMonerium, FakeMykobo, FakePrices, FakeSquidRouter };
export { installFetchGuard, uninstallFetchGuard };
export { installFakeChainIdRpc, installFetchGuard };

export interface FakeWorld {
evm: FakeEvm;
Expand All @@ -28,7 +28,7 @@ export interface FakeWorld {
* test files in one process, so leaked patches bleed into other files.
*/
export function installFakeWorld(): FakeWorld {
installFetchGuard();
const uninstallFetchGuard = installFetchGuard();
const { fakeEvm, restore: restoreEvm } = installFakeEvm();
const { fakeAlfredpay, fakeBrla, fakeMykobo, restore: restoreAnchors } = installFakeAnchors();
const { fakeMonerium, restore: restoreMonerium } = installFakeMonerium();
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/test-utils/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ if (!process.env.RUN_LIVE_TESTS) {
process.env.SUBSIDY_SETTLEMENT_DELAY_MS = "25";
process.env.PHASE_SETTLEMENT_RETRY_BACKOFF_MS = "25";

// Any real network call (fetch, viem RPC) fails loudly, not only in fake-world tests.
const { installFetchGuard } = await import("./fake-world/fetch-guard");
installFetchGuard();

// Close the shared Sequelize pool after the whole run so lingering pg
// connections don't surface as unhandled "Connection terminated" errors.
const { afterAll } = await import("bun:test");
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/tests/aaa-leak-probe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ describe("leak canary: no test file leaked a singleton patch", () => {
}
});

it("global fetch is not a leftover fetch guard", () => {
expect(globalThis.fetch.name, "the fetch guard was left installed").toBe("fetch");
it("global fetch is the preload's fetch guard, not a leftover shim", () => {
expect(globalThis.fetch.name, "global fetch was left patched").toBe(process.env.RUN_LIVE_TESTS ? "fetch" : "guard");
});

it("shared getRoute is not a leftover FakeSquidRouter stub", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import QuoteTicket from "../../models/quoteTicket.model";
import RampState from "../../models/rampState.model";
import { resetTestDatabase, setupTestDatabase } from "../../test-utils/db";
import { createTestTaxId, createTestUser, updatePartnerPricing } from "../../test-utils/factories";
import { type FakeWorld, installFakeWorld } from "../../test-utils/fake-world";
import { type FakeWorld, installFakeChainIdRpc, installFakeWorld } from "../../test-utils/fake-world";
import { installFakeSupabaseAuth, testUserToken } from "../../test-utils/fake-world/fake-auth";
import { startTestApp, type TestApp } from "../../test-utils/test-app";

Expand All @@ -41,33 +41,6 @@ const USDC_ON_ARBITRUM = requireToken(Networks.Arbitrum, EvmToken.USDC).erc20Add
const BRLA_ON_BASE = requireToken(Networks.Base, EvmToken.BRLA).erc20AddressSourceChain as `0x${string}`;

const TAX_ID = "12345678901";
const BASE_CHAIN_ID_HEX = "0x2105";
const ARBITRUM_CHAIN_ID_HEX = "0xa4b1";

function installChainIdShim(): { restore: () => void } {
const guardedFetch = globalThis.fetch;
const shim = (async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
if (typeof init?.body === "string") {
try {
const payload = JSON.parse(init.body) as { id?: number; method?: string };
if (payload.method === "eth_chainId") {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
const chainId = url.includes("base") ? BASE_CHAIN_ID_HEX : ARBITRUM_CHAIN_ID_HEX;
return Response.json({ id: payload.id ?? 1, jsonrpc: "2.0", result: chainId });
}
} catch {
// Not a JSON-RPC request; retain the hermetic fetch guard below.
}
}
return guardedFetch(input, init);
}) as typeof fetch;
globalThis.fetch = Object.assign(shim, guardedFetch);
return {
restore: () => {
globalThis.fetch = guardedFetch;
}
};
}

// Unlike the direct pix→BRLA-on-Base corridor, the full swap-and-bridge chain
// executes here: Nabla swaps the minted BRLA into USDC on Base, the squid
Expand Down Expand Up @@ -128,12 +101,12 @@ interface DestinationFundingExpectation {
describe("BRL onramp cross-chain corridor (pix → Base mint+swap → USDC on Arbitrum)", () => {
let world: FakeWorld;
let auth: { restore: () => void };
let chainIdShim: { restore: () => void };
let restoreChainIdRpc: () => void;
let app: TestApp;

beforeAll(async () => {
world = installFakeWorld();
chainIdShim = installChainIdShim();
restoreChainIdRpc = installFakeChainIdRpc();
auth = installFakeSupabaseAuth();
await setupTestDatabase();
app = await startTestApp();
Expand All @@ -142,7 +115,7 @@ describe("BRL onramp cross-chain corridor (pix → Base mint+swap → USDC on Ar
afterAll(async () => {
await app?.close();
auth?.restore();
chainIdShim?.restore();
restoreChainIdRpc?.();
world?.restore();
});

Expand Down
34 changes: 4 additions & 30 deletions apps/api/src/tests/corridors/eur-onramp-monerium.scenario.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import QuoteTicket from "../../models/quoteTicket.model";
import RampState from "../../models/rampState.model";
import { resetTestDatabase, setupTestDatabase } from "../../test-utils/db";
import { createTestUser, updatePartnerPricing } from "../../test-utils/factories";
import { type FakeWorld, installFakeWorld } from "../../test-utils/fake-world";
import { type FakeWorld, installFakeChainIdRpc, installFakeWorld } from "../../test-utils/fake-world";
import { installFakeSupabaseAuth, testUserToken } from "../../test-utils/fake-world/fake-auth";
import { startTestApp, type TestApp } from "../../test-utils/test-app";

Expand All @@ -49,35 +49,9 @@ const USDC_ON_ARBITRUM = requireToken(Networks.Arbitrum, EvmToken.USDC).erc20Add
const EURE_ON_POLYGON = MONERIUM_ISSUE_NETWORKS[Networks.Polygon].eureAddress;

const PROFILE_ID = "9e6a92a5-5f6d-48aa-a57b-0f8ae8eb745d";
const CHAIN_ID_HEX: Record<string, string> = { arbitrum: "0xa4b1", polygon: "0x89" };
/** Fake Uniswap quoter: 1 EURe (18 decimals) buys 1.16 USDC (6 decimals). */
const EURE_USDC_RATE_MICRO = 1_160_000n;

function installChainIdShim(): { restore: () => void } {
const guardedFetch = globalThis.fetch;
const shim = (async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
if (typeof init?.body === "string") {
try {
const payload = JSON.parse(init.body) as { id?: number; method?: string };
if (payload.method === "eth_chainId") {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
const chainId = Object.entries(CHAIN_ID_HEX).find(([name]) => url.includes(name))?.[1] ?? CHAIN_ID_HEX.arbitrum;
return Response.json({ id: payload.id ?? 1, jsonrpc: "2.0", result: chainId });
}
} catch {
// Not a JSON-RPC request; retain the hermetic fetch guard below.
}
}
return guardedFetch(input, init);
}) as typeof fetch;
globalThis.fetch = Object.assign(shim, guardedFetch);
return {
restore: () => {
globalThis.fetch = guardedFetch;
}
};
}

const HAPPY_PATH_PHASES: RampPhase[] = [
"initial",
"moneriumOnrampMint",
Expand Down Expand Up @@ -129,13 +103,13 @@ interface CorridorSetup {
describe("EUR onramp Monerium corridor (sepa → Polygon mint+swap → USDC on Arbitrum)", () => {
let world: FakeWorld;
let auth: { restore: () => void };
let chainIdShim: { restore: () => void };
let restoreChainIdRpc: () => void;
let app: TestApp;
const originalIssueFee = config.monerium.issueFeeEur;

beforeAll(async () => {
world = installFakeWorld();
chainIdShim = installChainIdShim();
restoreChainIdRpc = installFakeChainIdRpc();
auth = installFakeSupabaseAuth();
await setupTestDatabase();
app = await startTestApp();
Expand All @@ -146,7 +120,7 @@ describe("EUR onramp Monerium corridor (sepa → Polygon mint+swap → USDC on A
config.monerium.issueFeeEur = originalIssueFee;
await app?.close();
auth?.restore();
chainIdShim?.restore();
restoreChainIdRpc?.();
world?.restore();
});

Expand Down
Loading
Loading