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/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/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 2a0f1289d..95052f6fb 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"; +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; @@ -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", () => { 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(); }); 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/`)