Skip to content
Draft
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
18 changes: 13 additions & 5 deletions src/services/ibex/webhook-server/routes/crypto-receive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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) {
Expand Down Expand Up @@ -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 }
Original file line number Diff line number Diff line change
@@ -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" })
})
})