diff --git a/src/adapter/bridges/BridgeApi.ts b/src/adapter/bridges/BridgeApi.ts index 6268d1386e..036a520c18 100644 --- a/src/adapter/bridges/BridgeApi.ts +++ b/src/adapter/bridges/BridgeApi.ts @@ -23,6 +23,9 @@ import { BRIDGE_API_DESTINATION_TOKEN_SYMBOLS, roundAmountToSend, mapAsync, + createFormatFunction, + floatToBN, + ZERO_BYTES, } from "../../utils"; import { TransferTokenParams } from "../utils"; import ERC20_ABI from "../../common/abi/MinimalERC20.json"; @@ -30,6 +33,7 @@ import ERC20_ABI from "../../common/abi/MinimalERC20.json"; export class BridgeApi extends BaseBridgeAdapter { protected api: BridgeApiClient; protected l1TokenInfo: TokenInfo; + protected dstCurrency: string; constructor( l2chainId: number, @@ -47,7 +51,7 @@ export class BridgeApi extends BaseBridgeAdapter { this.l2Bridge = new Contract(BRIDGE_API_DESTINATION_TOKENS[this.l2chainId], ERC20_ABI, l2SignerOrProvider); // We need to fetch some API configuration details from environment. - const { BRIDGE_API_BASE, BRIDGE_API_KEY, BRIDGE_CUSTOMER_ID } = process.env; + const { BRIDGE_API_BASE = "https://api.bridge.xyz", BRIDGE_API_KEY, BRIDGE_CUSTOMER_ID } = process.env; assert(isDefined(BRIDGE_API_BASE), "BRIDGE_API_BASE must be set in the environment"); assert(isDefined(BRIDGE_API_KEY), "BRIDGE_API_KEY must be set in the environment"); @@ -63,6 +67,7 @@ export class BridgeApi extends BaseBridgeAdapter { ); this.l1TokenInfo = getTokenInfo(l1Token, this.hubChainId); + this.dstCurrency = BRIDGE_API_DESTINATION_TOKEN_SYMBOLS[this.getL2Bridge().address]; } async constructL1ToL2Txn( @@ -70,7 +75,6 @@ export class BridgeApi extends BaseBridgeAdapter { _l1Token: EvmAddress, l2Token: Address, _amount: BigNumber, - // eslint-disable-next-line @typescript-eslint/no-unused-vars _optionalParams?: TransferTokenParams ): Promise { const amount = roundAmountToSend(_amount, this.l1TokenInfo.decimals, 2); // The bridge API only deals with values up to 2 decimals. @@ -82,10 +86,12 @@ export class BridgeApi extends BaseBridgeAdapter { if (amount.lt(BRIDGE_API_MINIMUMS[this.hubChainId]?.[this.l2chainId] ?? toBN(Number.MAX_SAFE_INTEGER))) { throw new Error(`Cannot bridge to ${getNetworkName(this.l2chainId)} due to invalid amount ${amount}`); } + const formatter = createFormatFunction(2, 4, false, this.l1TokenInfo.decimals); const transferRouteAddress = await this.api.createTransferRouteEscrowAddress( toAddress, this.l1TokenInfo.symbol, - BRIDGE_API_DESTINATION_TOKEN_SYMBOLS[this.getL2Bridge().address] + BRIDGE_API_DESTINATION_TOKEN_SYMBOLS[this.getL2Bridge().address], + formatter(amount) ); return Promise.resolve({ contract: this.getL1Bridge(), @@ -96,7 +102,7 @@ export class BridgeApi extends BaseBridgeAdapter { async queryL1BridgeInitiationEvents( _l1Token: EvmAddress, - _fromAddress: EvmAddress, + fromAddress: EvmAddress, toAddress: Address, eventConfig: EventSearchConfig ): Promise { @@ -110,24 +116,30 @@ export class BridgeApi extends BaseBridgeAdapter { statusesGrouped, }); + const initialPendingRebalances = await this.api.filterInitiatedTransfers( + pendingTransfers.filter((pendingTransfer) => pendingTransfer.destination.currency === this.dstCurrency), + fromAddress, + eventConfig, + this.l1Signer.provider + ); const pendingRebalances = await mapAsync( - pendingTransfers.filter((pendingTransfer) => { - const destinationAddress = toAddressType(pendingTransfer.destination.to_address, this.l2chainId); + initialPendingRebalances.filter(({ destination, source_deposit_instructions }) => { + const destinationAddress = toAddressType(destination.to_address, this.l2chainId); return ( destinationAddress.eq(toAddress) && - pendingTransfer.state !== "awaiting_funds" && - pendingTransfer.state !== "payment_processed" && - pendingTransfer.source_deposit_instructions.currency === this.l1TokenInfo.symbol.toLowerCase() + source_deposit_instructions.currency === this.l1TokenInfo.symbol.toLowerCase() ); }), - async ({ receipt }) => { - const transaction = await this.l1Signer.provider.getTransactionReceipt(receipt.source_tx_hash); + async (pendingTransfer) => { + const transaction = isDefined(pendingTransfer?.receipt?.source_tx_hash) + ? await this.l1Signer.provider.getTransactionReceipt(pendingTransfer.receipt.source_tx_hash) + : undefined; return { - txnRef: receipt.source_tx_hash, + txnRef: pendingTransfer.receipt?.source_tx_hash ?? ZERO_BYTES, logIndex: 0, // logIndex is zero since the only call for initiation is a `Transfer`. - txnIndex: transaction?.transactionIndex, - blockNumber: transaction?.blockNumber, - amount: toBN(Math.floor(Number(receipt.final_amount) * 10 ** this.l1TokenInfo.decimals)), + txnIndex: transaction?.transactionIndex ?? 0, + blockNumber: transaction?.blockNumber ?? 0, + amount: floatToBN(pendingTransfer.receipt?.final_amount ?? pendingTransfer.amount, this.l1TokenInfo.decimals), }; } ); @@ -137,13 +149,9 @@ export class BridgeApi extends BaseBridgeAdapter { } async queryL2BridgeFinalizationEvents( - // eslint-disable-next-line @typescript-eslint/no-unused-vars _l1Token: EvmAddress, - // eslint-disable-next-line @typescript-eslint/no-unused-vars _fromAddress: EvmAddress, - // eslint-disable-next-line @typescript-eslint/no-unused-vars _toAddress: Address, - // eslint-disable-next-line @typescript-eslint/no-unused-vars _eventConfig: EventSearchConfig ): Promise { return Promise.resolve({}); diff --git a/src/adapter/l2Bridges/BaseL2BridgeAdapter.ts b/src/adapter/l2Bridges/BaseL2BridgeAdapter.ts index 7eb115873a..811594fb88 100644 --- a/src/adapter/l2Bridges/BaseL2BridgeAdapter.ts +++ b/src/adapter/l2Bridges/BaseL2BridgeAdapter.ts @@ -8,6 +8,7 @@ import { Address, getHubPoolAddress, getSpokePoolAddress, + getTranslatedTokenAddress, SVMProvider, SolanaTransaction, isDefined, @@ -48,6 +49,14 @@ export abstract class BaseL2BridgeAdapter { return DEFAULT_PENDING_WITHDRAWAL_LOOKBACK_PERIOD_SECONDS; } + /** + * Returns the L2 token address this bridge operates on. + * Override in subclasses that use a non-canonical L2 token (e.g. BridgeApi using pathUSD). + */ + getL2Token(): Address { + return getTranslatedTokenAddress(this.l1Token, this.hubChainId, this.l2chainId); + } + abstract constructWithdrawToL1Txns( toAddress: Address, l2Token: Address, diff --git a/src/adapter/l2Bridges/BridgeApi.ts b/src/adapter/l2Bridges/BridgeApi.ts new file mode 100644 index 0000000000..7ab6856635 --- /dev/null +++ b/src/adapter/l2Bridges/BridgeApi.ts @@ -0,0 +1,126 @@ +import { + Address, + BigNumber, + bnZero, + Contract, + EventSearchConfig, + getNetworkName, + Signer, + EvmAddress, + CHAIN_IDs, + BRIDGE_API_MINIMUMS, + toBN, + BridgeApiClient, + getTokenInfo, + floatToBN, + isDefined, + assert, + BRIDGE_API_DESTINATION_TOKEN_SYMBOLS, + getTimestampForBlock, + toAddressType, + BRIDGE_API_DESTINATION_TOKENS, + createFormatFunction, + roundAmountToSend, +} from "../../utils"; +import { BaseL2BridgeAdapter } from "./BaseL2BridgeAdapter"; +import { AugmentedTransaction } from "../../clients/TransactionClient"; +import { TokenInfo } from "../../interfaces"; +import ERC20_ABI from "../../common/abi/MinimalERC20.json"; + +export class BridgeApi extends BaseL2BridgeAdapter { + protected api: BridgeApiClient; + protected l1TokenInfo: TokenInfo; + protected l2TokenInfo: TokenInfo; + + constructor(l2chainId: number, hubChainId: number, l2Signer: Signer, l1Signer: Signer, l1Token: EvmAddress) { + if (hubChainId !== CHAIN_IDs.MAINNET) { + throw new Error("Cannot define a Bridge API bridge for a non-production network"); + } + super(l2chainId, hubChainId, l2Signer, l1Signer, l1Token); + + // We need to fetch some API configuration details from environment. + const { BRIDGE_API_BASE = "https://api.bridge.xyz", BRIDGE_API_KEY, BRIDGE_CUSTOMER_ID } = process.env; + + assert(isDefined(BRIDGE_API_BASE), "BRIDGE_API_BASE must be set in the environment"); + assert(isDefined(BRIDGE_API_KEY), "BRIDGE_API_KEY must be set in the environment"); + assert(isDefined(BRIDGE_CUSTOMER_ID), "BRIDGE_CUSTOMER_ID must be set in the environment"); + + this.api = new BridgeApiClient( + BRIDGE_API_BASE, + BRIDGE_API_KEY, + BRIDGE_CUSTOMER_ID, + this.l2chainId, + this.hubChainId + ); + + this.l1TokenInfo = getTokenInfo(l1Token, this.hubChainId); + this.l2TokenInfo = getTokenInfo( + toAddressType(BRIDGE_API_DESTINATION_TOKENS[this.l2chainId], this.l2chainId), + this.l2chainId + ); + } + + override getL2Token(): Address { + return toAddressType(BRIDGE_API_DESTINATION_TOKENS[this.l2chainId], this.l2chainId); + } + + async constructWithdrawToL1Txns( + toAddress: EvmAddress, + l2Token: EvmAddress, + l1Token: EvmAddress, + _amount: BigNumber + ): Promise { + const amount = roundAmountToSend(_amount, this.l2TokenInfo.decimals, 2); + // If amount is less than the network minimums, then throw. + if (amount.lt(BRIDGE_API_MINIMUMS[this.l2chainId]?.[this.hubChainId] ?? toBN(Number.MAX_SAFE_INTEGER))) { + throw new Error(`Cannot bridge to ${getNetworkName(this.hubChainId)} due to invalid amount ${amount}`); + } + const formatter = createFormatFunction(2, 4, false, this.l2TokenInfo.decimals); + const l2TokenSymbol = BRIDGE_API_DESTINATION_TOKEN_SYMBOLS[l2Token.toNative()]; + const transferRouteSource = await this.api.createTransferRouteEscrowAddress( + toAddress, + l2TokenSymbol, + this.l1TokenInfo.symbol, + formatter(amount) + ); + const l2TokenContract = new Contract(l2Token.toNative(), ERC20_ABI, this.l2Signer); + const transferTxn = { + contract: l2TokenContract, + method: "transfer", + chainId: this.l2chainId, + args: [transferRouteSource, amount], + nonMulticall: true, + canFailInSimulation: false, + value: bnZero, + message: `🎰 Withdrew ${getNetworkName(this.l2chainId)} ${this.l2TokenInfo.symbol} to L1`, + mrkdwn: `Withdrew ${formatter(amount.toString())} ${this.l2TokenInfo.symbol} from ${getNetworkName( + this.l2chainId + )} to L1`, + }; + return [transferTxn]; + } + + // @dev We do not filter on origin/destination tokens since there is only one bridge API destination token for any destination chain. + // e.g. For Tempo, we only use Bridge for pathUSD; other tokens are rebalanced via other methods, so + // if there is an outstanding transfer from Tempo to Ethereum, then this must be a pathUSD transfer. + async getL2PendingWithdrawalAmount( + l2EventConfig: EventSearchConfig, + l1EventConfig: EventSearchConfig, + fromAddress: EvmAddress, + l2Token: EvmAddress + ): Promise { + const fromTimestamp = await getTimestampForBlock(this.l1Signer.provider, l1EventConfig.from); + const allTransfers = await this.api.getAllTransfersInRange(fromAddress, fromTimestamp * 1000); + + const allInitiatedTransfers = await this.api.filterInitiatedTransfers( + allTransfers, + fromAddress, + l2EventConfig, + this.l2Signer.provider + ); + return allInitiatedTransfers.reduce((acc, transfer) => { + const { decimals: l2TokenDecimals } = getTokenInfo(l2Token, this.l2chainId); + return acc.add(floatToBN(transfer.receipt?.final_amount ?? transfer.amount, l2TokenDecimals)); + }, bnZero); + } +} diff --git a/src/adapter/l2Bridges/TokenSplitterBridge.ts b/src/adapter/l2Bridges/TokenSplitterBridge.ts new file mode 100644 index 0000000000..2b1f92b24a --- /dev/null +++ b/src/adapter/l2Bridges/TokenSplitterBridge.ts @@ -0,0 +1,68 @@ +import { + BigNumber, + ConvertDecimals, + EventSearchConfig, + getTokenInfo, + Signer, + EvmAddress, + getTranslatedTokenAddress, + SolanaTransaction, +} from "../../utils"; +import { BaseL2BridgeAdapter } from "./BaseL2BridgeAdapter"; +import { AugmentedTransaction } from "../../clients/TransactionClient"; +import { L2_TOKEN_SPLITTER_BRIDGES } from "../../common"; + +export class TokenSplitterBridge extends BaseL2BridgeAdapter { + protected bridge1; + protected bridge2; + + constructor(l2chainId: number, hubChainId: number, l2Signer: Signer, l1Signer: Signer, l1Token: EvmAddress) { + super(l2chainId, hubChainId, l2Signer, l1Signer, l1Token); + + const [bridge1Constructor, bridge2Constructor] = L2_TOKEN_SPLITTER_BRIDGES[this.l2chainId][this.l1Token.toNative()]; + this.bridge1 = new bridge1Constructor(l2chainId, hubChainId, l2Signer, l1Signer, l1Token); + this.bridge2 = new bridge2Constructor(l2chainId, hubChainId, l2Signer, l1Signer, l1Token); + } + + getRouteForL2Token(l2Token: EvmAddress): BaseL2BridgeAdapter { + return getTranslatedTokenAddress(this.l1Token, this.hubChainId, this.l2chainId).eq(l2Token) + ? this.bridge1 + : this.bridge2; + } + + async constructWithdrawToL1Txns( + toAddress: EvmAddress, + l2Token: EvmAddress, + l1Token: EvmAddress, + amount: BigNumber + ): Promise { + return this.getRouteForL2Token(l2Token).constructWithdrawToL1Txns(toAddress, l2Token, l1Token, amount); + } + + async getL2PendingWithdrawalAmount( + l2EventConfig: EventSearchConfig, + l1EventConfig: EventSearchConfig, + fromAddress: EvmAddress, + l2Token: EvmAddress + ): Promise { + const [bridge1Pending, bridge2Pending] = await Promise.all([ + this.bridge1.getL2PendingWithdrawalAmount(l2EventConfig, l1EventConfig, fromAddress, l2Token), + this.bridge2.getL2PendingWithdrawalAmount(l2EventConfig, l1EventConfig, fromAddress, l2Token), + ]); + + // Each bridge may return amounts denominated in its own L2 token's decimals. + // Normalize both to the decimals of the requested l2Token before summing. + const targetDecimals = getTokenInfo(l2Token, this.l2chainId).decimals; + const bridge1Decimals = getTokenInfo(this.bridge1.getL2Token(), this.l2chainId).decimals; + const bridge2Decimals = getTokenInfo(this.bridge2.getL2Token(), this.l2chainId).decimals; + + const normalizedBridge1 = ConvertDecimals(bridge1Decimals, targetDecimals)(bridge1Pending); + const normalizedBridge2 = ConvertDecimals(bridge2Decimals, targetDecimals)(bridge2Pending); + + return normalizedBridge1.add(normalizedBridge2); + } + + public override requiredTokenApprovals(): { token: EvmAddress; bridge: EvmAddress }[] { + return [...this.bridge1.requiredTokenApprovals(), ...this.bridge2.requiredTokenApprovals()]; + } +} diff --git a/src/adapter/l2Bridges/index.ts b/src/adapter/l2Bridges/index.ts index ce85cf1257..fab8df8fe0 100644 --- a/src/adapter/l2Bridges/index.ts +++ b/src/adapter/l2Bridges/index.ts @@ -7,3 +7,5 @@ export * from "./OpStackUSDCBridge"; export * from "./OpStackWethBridge"; export * from "./UsdcCCTPBridge"; export * from "./SolanaUsdcCCTPBridge"; +export * from "./BridgeApi"; +export * from "./TokenSplitterBridge"; diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 588ec3ad21..1f6d4082eb 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -53,6 +53,8 @@ import { UsdcCCTPBridge as L2UsdcCCTPBridge, BinanceCEXNativeBridge as L2BinanceCEXNativeBridge, SolanaUsdcCCTPBridge as L2SolanaUsdcCCTPBridge, + BridgeApi as L2BridgeApi, + TokenSplitterBridge as L2TokenSplitterBridge, } from "../adapter/l2Bridges"; import { CONTRACT_ADDRESSES } from "./ContractAddresses"; import { HyperlaneXERC20Bridge } from "../adapter/bridges/HyperlaneXERC20Bridge"; @@ -607,6 +609,15 @@ export const TOKEN_SPLITTER_BRIDGES: Record< }, }; +export const L2_TOKEN_SPLITTER_BRIDGES: Record< + number, + Record, L2BridgeConstructor]> +> = { + [CHAIN_IDs.TEMPO]: { + [TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET]]: [OFTL2Bridge, L2BridgeApi], + }, +}; + export const CUSTOM_L2_BRIDGE: Record>> = { [CHAIN_IDs.LISK]: { [TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET]]: L2OpStackUSDCBridge, @@ -657,7 +668,7 @@ export const CUSTOM_L2_BRIDGE: Record { - const l1TokenSymbol = _l1TokenSymbol.toLowerCase(); - const l2TokenSymbol = _l2TokenSymbol.toLowerCase(); + const srcTokenSymbol = _srcTokenSymbol.toLowerCase(); + const dstTokenSymbol = _dstTokenSymbol.toLowerCase(); const data = { on_behalf_of: `${this.customerId}`, source: { payment_rail: this.srcNetwork, - currency: l1TokenSymbol, + currency: srcTokenSymbol, + amount: normalizedAmount, }, + amount: normalizedAmount, destination: { payment_rail: this.dstNetwork, - currency: l2TokenSymbol, + currency: dstTokenSymbol, to_address: toAddress.toNative(), }, return_instructions: { @@ -101,7 +126,6 @@ export class BridgeApiClient { }, features: { allow_any_from_address: true, - flexible_amount: true, }, }; const idempotencyKey = String(Date.now()); @@ -113,6 +137,70 @@ export class BridgeApiClient { return transferRequestData.source_deposit_instructions.to_address; } + async filterInitiatedTransfers( + deposits: BridgeResponse[], + fromAddress: Address, + eventConfig: EventSearchConfig, + originProvider: Provider + ): Promise { + const getOriginTokenInfo = (deposit: BridgeResponse) => { + const originToken = Object.entries(BRIDGE_API_DESTINATION_TOKEN_SYMBOLS).find( + ([, bridgeSymbol]) => bridgeSymbol === deposit.source_deposit_instructions.currency + ); + assert(isDefined(originToken)); + return getTokenInfo(toAddressType(originToken[0], this.srcNetworkId), this.srcNetworkId); + }; + + // Deduplicate queries by toAddress to avoid redundant event fetches. + const seen = new Set(); + const uniqueDeposits = deposits.filter((deposit) => { + const { to_address: toAddress, currency: originToken } = deposit.source_deposit_instructions; + const seenKey = `${toAddress}:${originToken}`; + if (seen.has(seenKey)) { + return false; + } + seen.add(seenKey); + return true; + }); + // Map containing all origin chain transfers for a specific deposit address/token address combo. + const cachedTransfers: Record> = {}; + await mapAsync(uniqueDeposits, async (deposit) => { + const { address: originTokenAddress } = getOriginTokenInfo(deposit); + const toAddress = deposit.source_deposit_instructions.to_address; + const tokenContract = new Contract(originTokenAddress.toNative(), ERC20_ABI, originProvider); + cachedTransfers[toAddress] ??= {}; + cachedTransfers[toAddress][originTokenAddress.toNative()] = await paginatedEventQuery( + tokenContract, + tokenContract.filters.Transfer(fromAddress.toNative(), toAddress), + eventConfig + ); + }); + + // Sort so that confirmed deposits (state != "awaiting_funds") match events first, + // since they are guaranteed to have a corresponding transfer event. + const sortedDeposits = [...deposits].sort((a, b) => { + const aWaiting = a.state === "awaiting_funds" ? 1 : 0; + const bWaiting = b.state === "awaiting_funds" ? 1 : 0; + return aWaiting - bWaiting; + }); + + return sortedDeposits.filter((deposit) => { + const { decimals: originTokenDecimals, address: originTokenAddress } = getOriginTokenInfo(deposit); + const expectedAmount = floatToBN(deposit.receipt?.final_amount ?? deposit.amount, originTokenDecimals); + const toAddress = deposit.source_deposit_instructions.to_address; + const events = cachedTransfers[toAddress][originTokenAddress.toNative()]; + const matchIdx = events.findIndex((log) => expectedAmount.eq(log.args.value)); + if (matchIdx === -1) { + return false; + } + // Remove matched event so it can't be double-matched to another deposit. + events.splice(matchIdx, 1); + // Completed transfers must still consume their event above, but should be + // excluded from the outstanding result set. + return deposit.state !== "payment_processed"; + }); + } + defaultHeaders(): RawAxiosRequestHeaders { return { "Api-Key": `${this.bridgeApiKey}`, @@ -125,7 +213,7 @@ export class BridgeApiClient { const response = await axios.get(`${this.bridgeApiBase}/${endpoint}`, { headers }); return response.data; } catch (e) { - this.logger.debug({ + this.logger?.debug({ at: "BridgeApi#_get", message: "Failed to query bridge API", endpoint, @@ -149,7 +237,7 @@ export class BridgeApiClient { const response = await axios.post(`${this.bridgeApiBase}/${endpoint}`, data, { headers }); return response.data; } catch (e) { - this.logger.debug({ + this.logger?.debug({ at: "BridgeApi#_post", message: "Failed to post to bridge API", endpoint, diff --git a/test/generic-adapters/BridgeApi.ts b/test/generic-adapters/BridgeApi.ts index 38f1ee3504..dd9d343cb7 100644 --- a/test/generic-adapters/BridgeApi.ts +++ b/test/generic-adapters/BridgeApi.ts @@ -1,13 +1,19 @@ import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; import { utils } from "@across-protocol/sdk"; import { BridgeApi } from "../../src/adapter/bridges/BridgeApi"; -import { BridgeResponse, BRIDGE_API_DESTINATION_TOKENS, BRIDGE_API_MINIMUMS } from "../../src/utils/BridgeUtils"; import { ethers, randomAddress, expect, createSpyLogger, sinon, toBN } from "../utils"; -import { EvmAddress, toBNWei } from "../../src/utils/SDKUtils"; +import { + EvmAddress, + toBNWei, + BridgeResponse, + BRIDGE_API_DESTINATION_TOKENS, + BRIDGE_API_MINIMUMS, + BridgeApiClient, +} from "../../src/utils"; import * as sdkUtils from "../../src/utils/SDKUtils"; // Minimal mock for the BridgeApiClient used inside BridgeApi. -class MockBridgeApiClient { +class MockBridgeApiClient extends BridgeApiClient { public transfersToReturn: BridgeResponse[] = []; public escrowAddressToReturn = randomAddress(); @@ -20,6 +26,16 @@ class MockBridgeApiClient { async createTransferRouteEscrowAddress(_toAddress: unknown, _l1Symbol: string, _l2Symbol: string) { return this.escrowAddressToReturn; } + + async filterInitiatedTransfers( + deposits: BridgeResponse[], + _fromAddress: Address, + _eventConfig: EventSearchConfig, + _originChainId: number, + _originProvider: Provider + ): Promise { + return deposits.filter((deposit) => deposit.state !== "payment_processed"); + } } // Mock adapter that exposes internals for testing. @@ -152,9 +168,14 @@ describe("Cross Chain Adapter: BridgeApi", async function () { expect(event.txnIndex).to.equal(0); }); - it("filters out transfers with awaiting_funds state", async function () { + it("does not filter out transfers with awaiting_funds state", async function () { mockApi.transfersToReturn = [ - makeBridgeResponse({ state: "awaiting_funds", toAddress: monitoredEoa }), + makeBridgeResponse({ + state: "awaiting_funds", + toAddress: monitoredEoa, + finalAmount: "100", + sourceTxHash: undefined, + }), makeBridgeResponse({ state: "funds_received", toAddress: monitoredEoa, @@ -170,8 +191,9 @@ describe("Cross Chain Adapter: BridgeApi", async function () { searchConfig ); - expect(events[l2TokenAddress]).to.have.lengthOf(1); - expect(events[l2TokenAddress][0].txnRef).to.equal("0x1"); + expect(events[l2TokenAddress]).to.have.lengthOf(2); + expect(events[l2TokenAddress][0].txnRef).to.equal("0x0"); + expect(events[l2TokenAddress][1].txnRef).to.equal("0x1"); }); it("filters out transfers with payment_processed state", async function () {