From 175b6dbeea7e249636098cc8f3766349534a5d8c Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Fri, 9 Oct 2026 09:36:34 +0200 Subject: [PATCH 1/4] test(api): keep client-side signing tests off public RPCs The BRL and EUR Base cross-chain transaction tests call the real signUnsignedTransactions, whose viem wallet clients ask viem's default public RPC for eth_chainId before signing locally. A public Base RPC outage (HTTP 521) therefore failed an unrelated PR. A fake-world helper now answers eth_chainId for those default RPCs and passes every other request through. --- ...ramp-base-cross-chain.transactions.test.ts | 7 ++-- ...ramp-base-cross-chain.transactions.test.ts | 7 ++-- .../api/src/test-utils/fake-world/fake-evm.ts | 36 ++++++++++++++++++- apps/api/src/test-utils/fake-world/index.ts | 4 +-- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/apps/api/src/api/services/phases/blocks/__tests__/brl-onramp-base-cross-chain.transactions.test.ts b/apps/api/src/api/services/phases/blocks/__tests__/brl-onramp-base-cross-chain.transactions.test.ts index abb8c1b6a..f02564086 100644 --- a/apps/api/src/api/services/phases/blocks/__tests__/brl-onramp-base-cross-chain.transactions.test.ts +++ b/apps/api/src/api/services/phases/blocks/__tests__/brl-onramp-base-cross-chain.transactions.test.ts @@ -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"; @@ -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"; diff --git a/apps/api/src/api/services/phases/blocks/__tests__/eur-onramp-base-cross-chain.transactions.test.ts b/apps/api/src/api/services/phases/blocks/__tests__/eur-onramp-base-cross-chain.transactions.test.ts index d2b4cef3f..9d8efaf0e 100644 --- a/apps/api/src/api/services/phases/blocks/__tests__/eur-onramp-base-cross-chain.transactions.test.ts +++ b/apps/api/src/api/services/phases/blocks/__tests__/eur-onramp-base-cross-chain.transactions.test.ts @@ -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 }; @@ -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", @@ -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); + }); }); diff --git a/apps/api/src/test-utils/fake-world/fake-evm.ts b/apps/api/src/test-utils/fake-world/fake-evm.ts index c65b1568b..1e103d396 100644 --- a/apps/api/src/test-utils/fake-world/fake-evm.ts +++ b/apps/api/src/test-utils/fake-world/fake-evm.ts @@ -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; @@ -35,6 +36,14 @@ const CHAIN_IDS: Record = { 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( + [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 @@ -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[0], init?: Parameters[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(); diff --git a/apps/api/src/test-utils/fake-world/index.ts b/apps/api/src/test-utils/fake-world/index.ts index 2a0f1289d..076796c86 100644 --- a/apps/api/src/test-utils/fake-world/index.ts +++ b/apps/api/src/test-utils/fake-world/index.ts @@ -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"; export type { FakeAlfredpay, FakeBrla, FakeEvm, FakeMonerium, FakeMykobo, FakePrices, FakeSquidRouter }; -export { installFetchGuard, uninstallFetchGuard }; +export { installFakeChainIdRpc, installFetchGuard, uninstallFetchGuard }; export interface FakeWorld { evm: FakeEvm; From f8103409c494d559f63243e0bd7ff7d04709bd58 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Fri, 9 Oct 2026 09:36:34 +0200 Subject: [PATCH 2/4] test(api): install the fetch guard for every hermetic test file The guard already covers viem's http transport, which goes through global fetch, but only fake-world tests installed it, so unit tests could reach real chains unnoticed. The preload now owns the guard; installFakeWorld no longer uninstalls a guard it did not install. --- .../test-utils/fake-world/fetch-guard.test.ts | 23 +++++++++++++++++++ .../src/test-utils/fake-world/fetch-guard.ts | 11 +++++---- apps/api/src/test-utils/fake-world/index.ts | 6 ++--- apps/api/src/test-utils/preload.ts | 4 ++++ apps/api/src/tests/aaa-leak-probe.test.ts | 4 ++-- 5 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 apps/api/src/test-utils/fake-world/fetch-guard.test.ts diff --git a/apps/api/src/test-utils/fake-world/fetch-guard.test.ts b/apps/api/src/test-utils/fake-world/fetch-guard.test.ts new file mode 100644 index 000000000..4024d8563 --- /dev/null +++ b/apps/api/src/test-utils/fake-world/fetch-guard.test.ts @@ -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(); + } + }); +}); diff --git a/apps/api/src/test-utils/fake-world/fetch-guard.ts b/apps/api/src/test-utils/fake-world/fetch-guard.ts index ba431afc5..8a2b5d71d 100644 --- a/apps/api/src/test-utils/fake-world/fetch-guard.ts +++ b/apps/api/src/test-utils/fake-world/fetch-guard.ts @@ -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; @@ -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; diff --git a/apps/api/src/test-utils/fake-world/index.ts b/apps/api/src/test-utils/fake-world/index.ts index 076796c86..95052f6fb 100644 --- a/apps/api/src/test-utils/fake-world/index.ts +++ b/apps/api/src/test-utils/fake-world/index.ts @@ -5,10 +5,10 @@ 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 { installFakeChainIdRpc, installFetchGuard, uninstallFetchGuard }; +export { installFakeChainIdRpc, installFetchGuard }; export interface FakeWorld { evm: FakeEvm; @@ -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(); diff --git a/apps/api/src/test-utils/preload.ts b/apps/api/src/test-utils/preload.ts index 690778c57..809fb8ced 100644 --- a/apps/api/src/test-utils/preload.ts +++ b/apps/api/src/test-utils/preload.ts @@ -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"); diff --git a/apps/api/src/tests/aaa-leak-probe.test.ts b/apps/api/src/tests/aaa-leak-probe.test.ts index b76058717..11b9cfab7 100644 --- a/apps/api/src/tests/aaa-leak-probe.test.ts +++ b/apps/api/src/tests/aaa-leak-probe.test.ts @@ -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", () => { From 92f9cb15f13cdfe95183a622ab7ae67c5f4f8f2c Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Fri, 9 Oct 2026 09:36:35 +0200 Subject: [PATCH 3/4] test(api): replace per-file eth_chainId shims with the shared fake --- .../brl-onramp-crosschain.scenario.test.ts | 35 ++------------- .../eur-onramp-monerium.scenario.test.ts | 34 ++------------ .../sdk-contract.alfredpay-offramp.test.ts | 38 ++-------------- .../sdk-contract.alfredpay-onramp.test.ts | 38 ++-------------- .../src/tests/sdk-contract.offramp.test.ts | 37 ++-------------- apps/api/src/tests/sdk-contract.test.ts | 44 +++---------------- 6 files changed, 26 insertions(+), 200 deletions(-) diff --git a/apps/api/src/tests/corridors/brl-onramp-crosschain.scenario.test.ts b/apps/api/src/tests/corridors/brl-onramp-crosschain.scenario.test.ts index bb91a68e4..8b4dba82f 100644 --- a/apps/api/src/tests/corridors/brl-onramp-crosschain.scenario.test.ts +++ b/apps/api/src/tests/corridors/brl-onramp-crosschain.scenario.test.ts @@ -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"; @@ -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[0], init?: Parameters[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 @@ -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(); @@ -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(); }); diff --git a/apps/api/src/tests/corridors/eur-onramp-monerium.scenario.test.ts b/apps/api/src/tests/corridors/eur-onramp-monerium.scenario.test.ts index be2d59b19..493790ac1 100644 --- a/apps/api/src/tests/corridors/eur-onramp-monerium.scenario.test.ts +++ b/apps/api/src/tests/corridors/eur-onramp-monerium.scenario.test.ts @@ -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"; @@ -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 = { 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[0], init?: Parameters[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", @@ -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(); @@ -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(); }); diff --git a/apps/api/src/tests/sdk-contract.alfredpay-offramp.test.ts b/apps/api/src/tests/sdk-contract.alfredpay-offramp.test.ts index 79c614371..f5bbf6125 100644 --- a/apps/api/src/tests/sdk-contract.alfredpay-offramp.test.ts +++ b/apps/api/src/tests/sdk-contract.alfredpay-offramp.test.ts @@ -21,11 +21,10 @@ import QuoteTicket from "../models/quoteTicket.model"; import RampState from "../models/rampState.model"; import { resetTestDatabase, setupTestDatabase } from "../test-utils/db"; import { createTestAlfredpayCustomer, createTestApiKey, createTestUser } from "../test-utils/factories"; -import { type FakeWorld, installFakeWorld } from "../test-utils/fake-world"; +import { type FakeWorld, installFakeChainIdRpc, installFakeWorld } from "../test-utils/fake-world"; import { startTestApp, type TestApp } from "../test-utils/test-app"; const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; -const POLYGON_CHAIN_ID_HEX = "0x89"; // 137 interface CurrencyCase { fiat: FiatToken; @@ -116,35 +115,6 @@ const FULL_LIFECYCLE_CASES: LifecycleCase[] = [ } ]; -/** - * Same shim as the other SDK contract tests: the SDK's viem wallet client - * issues one eth_chainId RPC before signing locally. The Alfredpay SELL - * corridor only ephemeral-signs on Polygon (the source-of-funds transfer is - * user-broadcast, never SDK-signed), so answer with the Polygon chain id. - */ -function installChainIdShim(): { restore: () => void } { - const guardedFetch = globalThis.fetch; - const shim = (async (input: Parameters[0], init?: Parameters[1]) => { - if (typeof init?.body === "string") { - try { - 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: POLYGON_CHAIN_ID_HEX }); - } - } catch { - // not JSON — let the guarded fetch decide - } - } - return guardedFetch(input, init); - }) as typeof fetch; - globalThis.fetch = Object.assign(shim, guardedFetch); - return { - restore: () => { - globalThis.fetch = guardedFetch; - } - }; -} - /** * SDK ↔ API contract tests for the Alfredpay SELL rail (USDT on Polygon → * USD/MXN/COP/ARS bank payouts): the real SDK lists the user's registered @@ -155,19 +125,19 @@ function installChainIdShim(): { restore: () => void } { */ describe("SDK ↔ API contract (Alfredpay offramps, USDT on Polygon → bank payout)", () => { let world: FakeWorld; - let chainIdShim: { restore: () => void }; + let restoreChainIdRpc: () => void; let app: TestApp; beforeAll(async () => { world = installFakeWorld(); - chainIdShim = installChainIdShim(); + restoreChainIdRpc = installFakeChainIdRpc(); await setupTestDatabase(); app = await startTestApp(); }); afterAll(async () => { await app?.close(); - chainIdShim?.restore(); + restoreChainIdRpc?.(); world?.restore(); }); diff --git a/apps/api/src/tests/sdk-contract.alfredpay-onramp.test.ts b/apps/api/src/tests/sdk-contract.alfredpay-onramp.test.ts index 0ff347cda..4be2df9d7 100644 --- a/apps/api/src/tests/sdk-contract.alfredpay-onramp.test.ts +++ b/apps/api/src/tests/sdk-contract.alfredpay-onramp.test.ts @@ -18,11 +18,10 @@ import QuoteTicket from "../models/quoteTicket.model"; import RampState from "../models/rampState.model"; import { resetTestDatabase, setupTestDatabase } from "../test-utils/db"; import { createTestAlfredpayCustomer, createTestApiKey, createTestUser } from "../test-utils/factories"; -import { type FakeWorld, installFakeWorld } from "../test-utils/fake-world"; +import { type FakeWorld, installFakeChainIdRpc, installFakeWorld } from "../test-utils/fake-world"; import { startTestApp, type TestApp } from "../test-utils/test-app"; const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; -const POLYGON_CHAIN_ID_HEX = "0x89"; // 137 interface CurrencyCase { fiat: FiatToken; @@ -98,35 +97,6 @@ const FULL_LIFECYCLE_CASES: CurrencyCase[] = [ } ]; -/** - * Same shim as the other SDK contract tests: the SDK's viem wallet client - * issues one eth_chainId RPC before signing locally with the ephemeral key. - * The Alfredpay BUY corridors only ephemeral-sign on Polygon (the anchor mints - * USDT there), so answer with the Polygon chain id. - */ -function installChainIdShim(): { restore: () => void } { - const guardedFetch = globalThis.fetch; - const shim = (async (input: Parameters[0], init?: Parameters[1]) => { - if (typeof init?.body === "string") { - try { - 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: POLYGON_CHAIN_ID_HEX }); - } - } catch { - // not JSON — let the guarded fetch decide - } - } - return guardedFetch(input, init); - }) as typeof fetch; - globalThis.fetch = Object.assign(shim, guardedFetch); - return { - restore: () => { - globalThis.fetch = guardedFetch; - } - }; -} - /** * SDK ↔ API contract tests for the Alfredpay BUY rail (fiat → USDT on * Polygon): the real @vortexfi/sdk drives the real in-process API. The full @@ -138,19 +108,19 @@ function installChainIdShim(): { restore: () => void } { */ describe("SDK ↔ API contract (Alfredpay onramps, fiat → USDT on Polygon)", () => { let world: FakeWorld; - let chainIdShim: { restore: () => void }; + let restoreChainIdRpc: () => void; let app: TestApp; beforeAll(async () => { world = installFakeWorld(); - chainIdShim = installChainIdShim(); + restoreChainIdRpc = installFakeChainIdRpc(); await setupTestDatabase(); app = await startTestApp(); }); afterAll(async () => { await app?.close(); - chainIdShim?.restore(); + restoreChainIdRpc?.(); world?.restore(); }); diff --git a/apps/api/src/tests/sdk-contract.offramp.test.ts b/apps/api/src/tests/sdk-contract.offramp.test.ts index 1723f5735..d282dbf51 100644 --- a/apps/api/src/tests/sdk-contract.offramp.test.ts +++ b/apps/api/src/tests/sdk-contract.offramp.test.ts @@ -26,7 +26,7 @@ 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 { startTestApp, type TestApp } from "../test-utils/test-app"; function requireToken(network: Networks.Base | Networks.Polygon, token: EvmToken) { @@ -43,35 +43,6 @@ const BRLA_ON_BASE = requireToken(Networks.Base, EvmToken.BRLA).erc20AddressSour const TAX_ID = "12345678901"; const RECEIVER_TAX_ID = "12345678900"; const PIX_KEY = "test-pix-key"; -const BASE_CHAIN_ID_HEX = "0x2105"; // 8453 - -/** - * Same shim as sdk-contract.test.ts: the SDK's viem wallet client issues one - * eth_chainId RPC before signing locally; the SELL corridor only ephemeral-signs - * on Base (the Polygon squid leg is user-broadcast, never SDK-signed). - */ -function installChainIdShim(): { restore: () => void } { - const guardedFetch = globalThis.fetch; - const shim = (async (input: Parameters[0], init?: Parameters[1]) => { - if (typeof init?.body === "string") { - try { - 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: BASE_CHAIN_ID_HEX }); - } - } catch { - // not JSON — let the guarded fetch decide - } - } - return guardedFetch(input, init); - }) as typeof fetch; - globalThis.fetch = Object.assign(shim, guardedFetch); - return { - restore: () => { - globalThis.fetch = guardedFetch; - } - }; -} /** * SDK ↔ API contract tests for the SELL direction and the user-transaction @@ -84,19 +55,19 @@ function installChainIdShim(): { restore: () => void } { */ describe("SDK ↔ API contract (BRL offramp, USDC on Polygon → pix)", () => { let world: FakeWorld; - let chainIdShim: { restore: () => void }; + let restoreChainIdRpc: () => void; let app: TestApp; beforeAll(async () => { world = installFakeWorld(); - chainIdShim = installChainIdShim(); + restoreChainIdRpc = installFakeChainIdRpc(); await setupTestDatabase(); app = await startTestApp(); }); afterAll(async () => { await app?.close(); - chainIdShim?.restore(); + restoreChainIdRpc?.(); world?.restore(); }); diff --git a/apps/api/src/tests/sdk-contract.test.ts b/apps/api/src/tests/sdk-contract.test.ts index 7d9850a1f..d30efbe81 100644 --- a/apps/api/src/tests/sdk-contract.test.ts +++ b/apps/api/src/tests/sdk-contract.test.ts @@ -14,7 +14,7 @@ import { VortexSdk, VortexSdkError } from "../../../../packages/sdk/src"; import RampState from "../models/rampState.model"; import { resetTestDatabase, setupTestDatabase } from "../test-utils/db"; import { createTestApiKey, createTestTaxId, createTestUser } from "../test-utils/factories"; -import { type FakeWorld, installFakeWorld } from "../test-utils/fake-world"; +import { type FakeWorld, installFakeChainIdRpc, installFakeWorld } from "../test-utils/fake-world"; import { startTestApp, type TestApp } from "../test-utils/test-app"; function requireBrlaOnBase() { @@ -28,40 +28,6 @@ const brlaTokenDetails = requireBrlaOnBase(); const BRLA_ON_BASE = brlaTokenDetails.erc20AddressSourceChain as `0x${string}`; const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; -const BASE_CHAIN_ID_HEX = "0x2105"; // 8453 - -/** - * The SDK signs EVM transactions through a real viem wallet client, and viem's - * signTransaction issues a single eth_chainId RPC before signing locally with - * the ephemeral key. Answer that one call in-memory (the corridor only signs on - * Base) and let every other request fall through to the fetch guard, so any - * genuine network use still fails loudly. The SDK's NetworkManager itself is - * inert here: it only opens chain WebSockets when signing Pendulum, Moonbeam, - * or Hydration transactions, and the direct BRL→BRLA-on-Base corridor produces - * a single Base EVM transaction. - */ -function installChainIdShim(): { restore: () => void } { - const guardedFetch = globalThis.fetch; - const shim = (async (input: Parameters[0], init?: Parameters[1]) => { - if (typeof init?.body === "string") { - try { - 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: BASE_CHAIN_ID_HEX }); - } - } catch { - // not JSON — let the guarded fetch decide - } - } - return guardedFetch(input, init); - }) as typeof fetch; - globalThis.fetch = Object.assign(shim, guardedFetch); - return { - restore: () => { - globalThis.fetch = guardedFetch; - } - }; -} /** * SDK ↔ API contract tests: the real @vortexfi/sdk (imported from source) @@ -71,19 +37,21 @@ function installChainIdShim(): { restore: () => void } { */ describe("SDK ↔ API contract (BRL onramp, pix → BRLA on Base)", () => { let world: FakeWorld; - let chainIdShim: { restore: () => void }; + let restoreChainIdRpc: () => void; let app: TestApp; beforeAll(async () => { world = installFakeWorld(); - chainIdShim = installChainIdShim(); + // The SDK's NetworkManager stays inert: it only opens chain WebSockets for Pendulum, + // Moonbeam, or Hydration signing, and this corridor signs a single Base EVM transaction. + restoreChainIdRpc = installFakeChainIdRpc(); await setupTestDatabase(); app = await startTestApp(); }); afterAll(async () => { await app?.close(); - chainIdShim?.restore(); + restoreChainIdRpc?.(); world?.restore(); }); From 198fc76b29aad3ba42768150e2e3c96feafb4348 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Fri, 9 Oct 2026 10:42:14 +0200 Subject: [PATCH 4/4] ci(repo): run the deployed quote smoke test in live mode The test preload now installs the fetch guard for every hermetic run, which would reject the smoke test's calls to the deployed APIs. --- .github/workflows/e2e.yml | 2 ++ docs/operations-testing.md | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 78b3d24cb..64205ee26 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -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 diff --git a/docs/operations-testing.md b/docs/operations-testing.md index 17f07dde0..53fc98215 100644 --- a/docs/operations-testing.md +++ b/docs/operations-testing.md @@ -277,9 +277,11 @@ Tests that hit real RPCs or sandboxes (e.g. XCM dry-runs in `packages/shared`) a `RUN_LIVE_TESTS=1` via `describe.skipIf`. They are for local debugging and optional nightly runs, never PR-blocking. -The e2e workflow sets `VORTEX_QUOTE_SMOKE_URLS` for `deployed-quotes.e2e.test.ts`, which requires -successful cross-chain BUY and SELL quotes from both staging and production. Ordinary local and -PR-blocking API runs leave the variable unset, so this deployment smoke test does not make them +The e2e workflow sets `VORTEX_QUOTE_SMOKE_URLS` and `RUN_LIVE_TESTS=1` for +`deployed-quotes.e2e.test.ts`, which requires successful cross-chain BUY and SELL quotes from both +staging and production. `RUN_LIVE_TESTS=1` is required: without it the test preload installs the +fetch guard, which rejects the calls to the deployed APIs. Ordinary local and PR-blocking API runs +leave `VORTEX_QUOTE_SMOKE_URLS` unset, so this deployment smoke test does not make them network-dependent. ### External API contracts (`apps/api/src/tests/contracts/`)