From 21533d6cc541cabc24978d5f5dc4131955a65a71 Mon Sep 17 00:00:00 2001 From: Vandana Date: Tue, 12 May 2026 07:48:18 -0400 Subject: [PATCH] fix(bridge): align crypto receive webhook to ethereum usdt --- .../webhook-server/routes/crypto-receive.ts | 18 ++- .../routes/crypto-receive.spec.ts | 113 ++++++++++++++++++ 2 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 test/flash/unit/services/ibex/webhook-server/routes/crypto-receive.spec.ts diff --git a/src/services/ibex/webhook-server/routes/crypto-receive.ts b/src/services/ibex/webhook-server/routes/crypto-receive.ts index e104a52df..a2c5d1255 100644 --- a/src/services/ibex/webhook-server/routes/crypto-receive.ts +++ b/src/services/ibex/webhook-server/routes/crypto-receive.ts @@ -21,8 +21,16 @@ interface CryptoReceiveResult { const cryptoReceiveHandler = async (req: Request, res: Response) => { const { tx_hash, address, amount, currency, network } = req.body - - if (!tx_hash || !address || !amount || currency !== "USDT" || network !== "tron") { + const normalizedCurrency = String(currency || "").toUpperCase() + const normalizedNetwork = String(network || "").toLowerCase() + + if ( + !tx_hash || + !address || + !amount || + normalizedCurrency !== "USDT" || + normalizedNetwork !== "ethereum" + ) { baseLogger.warn( { tx_hash, address, amount, currency, network }, "Invalid crypto receive payload", @@ -44,8 +52,8 @@ const cryptoReceiveHandler = async (req: Request, res: Response) => { txHash: String(tx_hash), address: String(address), amount: String(amount), - currency: String(currency), - network: String(network), + currency: normalizedCurrency, + network: normalizedNetwork, accountId: account.id, }) if (ibexLog instanceof Error) { @@ -122,4 +130,4 @@ const cryptoReceiveHandler = async (req: Request, res: Response) => { router.post(paths.cryptoReceive, authenticate, logRequest, cryptoReceiveHandler) -export { paths, router } +export { cryptoReceiveHandler, paths, router } diff --git a/test/flash/unit/services/ibex/webhook-server/routes/crypto-receive.spec.ts b/test/flash/unit/services/ibex/webhook-server/routes/crypto-receive.spec.ts new file mode 100644 index 000000000..04d80bc2e --- /dev/null +++ b/test/flash/unit/services/ibex/webhook-server/routes/crypto-receive.spec.ts @@ -0,0 +1,113 @@ +jest.mock("@services/ibex/webhook-server/middleware", () => ({ + authenticate: jest.fn((_req, _res, next) => next()), + logRequest: jest.fn((_req, _res, next) => next()), +})) + +jest.mock("@services/mongoose/accounts", () => ({ + AccountsRepository: jest.fn(), +})) + +jest.mock("@services/mongoose/ibex-crypto-receive-log", () => ({ + createIbexCryptoReceiveLog: jest.fn(), +})) + +jest.mock("@app/wallets", () => ({ + listWalletsByAccountId: jest.fn(), +})) + +jest.mock("@services/logger", () => ({ + baseLogger: { info: jest.fn(), warn: jest.fn(), error: jest.fn() }, +})) + +jest.mock("@services/lock", () => ({ + LockService: jest.fn(), +})) + +import { cryptoReceiveHandler } from "@services/ibex/webhook-server/routes/crypto-receive" +import { AccountsRepository } from "@services/mongoose/accounts" +import { createIbexCryptoReceiveLog } from "@services/mongoose/ibex-crypto-receive-log" +import { listWalletsByAccountId } from "@app/wallets" +import { LockService } from "@services/lock" +import { WalletCurrency } from "@domain/shared" + +const ACCOUNT_ID = "account-001" as AccountId +const WALLET_ID = "wallet-usdt-001" as WalletId +const ADDRESS = "0xabc123" +const TX_HASH = "tx-001" + +const makeResponse = () => { + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + } + return res +} + +describe("cryptoReceiveHandler", () => { + beforeEach(() => { + jest.clearAllMocks() + ;(LockService as jest.Mock).mockReturnValue({ + lockPaymentHash: jest.fn((_hash, fn) => fn()), + }) + ;(AccountsRepository as jest.Mock).mockReturnValue({ + findByBridgeEthereumAddress: jest.fn().mockResolvedValue({ id: ACCOUNT_ID }), + }) + ;(createIbexCryptoReceiveLog as jest.Mock).mockResolvedValue({ id: "log-001" }) + ;(listWalletsByAccountId as jest.Mock).mockResolvedValue([ + { id: WALLET_ID, currency: WalletCurrency.Usdt }, + ]) + }) + + it("accepts Ethereum USDT receive webhooks and normalizes persisted currency/network", async () => { + const res = makeResponse() + + await cryptoReceiveHandler( + { + body: { + tx_hash: TX_HASH, + address: ADDRESS, + amount: "12.345678", + currency: "usdt", + network: "Ethereum", + }, + } as never, + res as never, + ) + + expect(AccountsRepository().findByBridgeEthereumAddress).toHaveBeenCalledWith(ADDRESS) + expect(createIbexCryptoReceiveLog).toHaveBeenCalledWith( + expect.objectContaining({ + txHash: TX_HASH, + address: ADDRESS, + amount: "12.345678", + currency: "USDT", + network: "ethereum", + accountId: ACCOUNT_ID, + }), + ) + expect(res.status).toHaveBeenCalledWith(200) + expect(res.json).toHaveBeenCalledWith({ status: "success" }) + }) + + it("rejects legacy Tron USDT receive webhooks for the ETH-USDT Cash Wallet path", async () => { + const res = makeResponse() + + await cryptoReceiveHandler( + { + body: { + tx_hash: TX_HASH, + address: ADDRESS, + amount: "12.345678", + currency: "USDT", + network: "tron", + }, + } as never, + res as never, + ) + + expect(LockService().lockPaymentHash).not.toHaveBeenCalled() + expect(createIbexCryptoReceiveLog).not.toHaveBeenCalled() + expect(res.status).toHaveBeenCalledWith(400) + expect(res.json).toHaveBeenCalledWith({ error: "Invalid payload" }) + }) +})