diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 3d7900bb6..51c78becf 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -1,12 +1,15 @@ import assert from "assert"; -import { Contract, PopulatedTransaction, utils as ethersUtils } from "ethers"; +import { BigNumber, BytesLike, Contract, PopulatedTransaction, providers, utils as ethersUtils } from "ethers"; import { CHAIN_IDs, ZERO_ADDRESS } from "../constants"; import { FillStatus, RelayData, SlowFillRequest, V2RelayData, V3Deposit, V3Fill, V3RelayData } from "../interfaces"; import { SpokePoolClient } from "../clients"; +import { toBN } from "./BigNumberUtils"; import { isDefined } from "./TypeGuards"; import { isV2RelayData } from "./V3Utils"; import { getNetworkName } from "./NetworkUtils"; +type BlockTag = providers.BlockTag; + /** * @param spokePool SpokePool Contract instance. * @param deposit V3Deopsit instance. @@ -307,12 +310,38 @@ export async function relayFillStatus( if (![FillStatus.Unfilled, FillStatus.RequestedSlowFill, FillStatus.Filled].includes(fillStatus)) { const { originChainId, depositId } = relayData; - throw new Error(`relayFillStatus: Unexpected fillStatus for ${originChainId} deposit ${depositId}`); + throw new Error(`relayFillStatus: Unexpected fillStatus for ${originChainId} deposit ${depositId} (${fillStatus})`); } return fillStatus; } +export async function fillStatusArray( + spokePool: Contract, + relayData: V3RelayData[], + blockTag: BlockTag = "latest" +): Promise<(FillStatus | undefined)[]> { + const fillStatuses = "fillStatuses"; + const destinationChainId = await spokePool.chainId(); + const queries = relayData.map((relayData) => { + const hash = getV3RelayHash(relayData, destinationChainId); + return spokePool.interface.encodeFunctionData(fillStatuses, [hash]); + }); + const multicall = await spokePool.callStatic.multicall(queries, { blockTag }); + const status = multicall.map( + (result: BytesLike) => spokePool.interface.decodeFunctionResult(fillStatuses, result)[0] + ); + + const bnUnfilled = toBN(FillStatus.Unfilled); + const bnFilled = toBN(FillStatus.Filled); + + return status.map((status: unknown) => { + return BigNumber.isBigNumber(status) && status.gte(bnUnfilled) && status.lte(bnFilled) + ? status.toNumber() + : undefined; + }); +} + /** * Find the block at which a fill was completed. * @todo After SpokePool upgrade, this function can be simplified to use the FillStatus enum. diff --git a/test/SpokePoolClient.ValidateFill.ts b/test/SpokePoolClient.ValidateFill.ts index 96ebbc9d1..e46088265 100644 --- a/test/SpokePoolClient.ValidateFill.ts +++ b/test/SpokePoolClient.ValidateFill.ts @@ -1,9 +1,10 @@ -import { FillStatus } from "../src/interfaces"; +import { FillStatus, V3DepositWithBlock } from "../src/interfaces"; import { SpokePoolClient } from "../src/clients"; import { bnZero, bnOne, InvalidFill, + fillStatusArray, relayFillStatus, validateFillForDeposit, queryHistoricalDepositForFill, @@ -18,6 +19,7 @@ import { depositV2, depositV3, fillV3Relay, + requestV3SlowFill, setupTokensForWallet, toBN, buildFill, @@ -134,6 +136,53 @@ describe("SpokePoolClient: Fill Validation", function () { expect(filled).to.equal(FillStatus.Filled); }); + it("Tracks bulk v3 fill status", async function () { + const deposits: V3DepositWithBlock[] = []; + const inputToken = erc20_1.address; + const inputAmount = toBNWei(1); + const outputToken = erc20_2.address; + const outputAmount = inputAmount.sub(bnOne); + + for (let i = 0; i < 5; ++i) { + const deposit = await depositV3( + spokePool_1, + destinationChainId, + depositor, + inputToken, + inputAmount, + outputToken, + outputAmount + ); + deposits.push(deposit); + } + expect(deposits.length).to.be.greaterThan(0); + + let fills = await fillStatusArray(spokePool_2, deposits); + expect(fills.length).to.equal(deposits.length); + fills.forEach((fillStatus) => expect(fillStatus).to.equal(FillStatus.Unfilled)); + + // Fill the first deposit and verify that the status updates correctly. + await fillV3Relay(spokePool_2, deposits[0], relayer); + fills = await fillStatusArray(spokePool_2, deposits); + expect(fills.length).to.equal(deposits.length); + expect(fills[0]).to.equal(FillStatus.Filled); + fills.slice(1).forEach((fillStatus) => expect(fillStatus).to.equal(FillStatus.Unfilled)); + + // Request a slow fill on the second deposit and verify that the status updates correctly. + await requestV3SlowFill(spokePool_2, deposits[1], relayer); + fills = await fillStatusArray(spokePool_2, deposits); + expect(fills.length).to.equal(deposits.length); + expect(fills[0]).to.equal(FillStatus.Filled); + expect(fills[1]).to.equal(FillStatus.RequestedSlowFill); + fills.slice(2).forEach((fillStatus) => expect(fillStatus).to.equal(FillStatus.Unfilled)); + + // Fill all outstanding deposits and verify that the status updates correctly. + await Promise.all(deposits.slice(1).map((deposit) => fillV3Relay(spokePool_2, deposit, relayer))); + fills = await fillStatusArray(spokePool_2, deposits); + expect(fills.length).to.equal(deposits.length); + fills.forEach((fillStatus) => expect(fillStatus).to.equal(FillStatus.Filled)); + }); + it("Accepts valid fills", async function () { const deposit = await buildDeposit(hubPoolClient, spokePool_1, erc20_1, depositor, destinationChainId); await buildFill(spokePool_2, erc20_2, depositor, relayer, deposit, 1); diff --git a/test/utils/utils.ts b/test/utils/utils.ts index 2d027a86d..eb02d82aa 100644 --- a/test/utils/utils.ts +++ b/test/utils/utils.ts @@ -5,7 +5,15 @@ import { GLOBAL_CONFIG_STORE_KEYS, HubPoolClient, } from "../../src/clients"; -import { V2Deposit, V2Fill, V3Deposit, V3DepositWithBlock, V3FillWithBlock } from "../../src/interfaces"; +import { + SlowFillRequestWithBlock, + V3RelayData, + V2Deposit, + V2Fill, + V3Deposit, + V3DepositWithBlock, + V3FillWithBlock, +} from "../../src/interfaces"; import { bnUint32Max, bnZero, @@ -410,6 +418,45 @@ export async function depositV3( }; } +export async function requestV3SlowFill( + spokePool: Contract, + relayData: V3RelayData, + signer: SignerWithAddress +): Promise { + const destinationChainId = Number(await spokePool.chainId()); + assert.notEqual(relayData.originChainId, destinationChainId); + + await spokePool.connect(signer).requestV3SlowFill(relayData); + + const events = await spokePool.queryFilter(spokePool.filters.RequestedV3SlowFill()); + const lastEvent = events.at(-1); + let args = lastEvent!.args; + assert.exists(args); + args = args!; + + const { blockNumber, transactionHash, transactionIndex, logIndex } = lastEvent!; + + return { + depositId: args.depositId, + originChainId: Number(args.originChainId), + destinationChainId, + depositor: args.depositor, + recipient: args.recipient, + inputToken: args.inputToken, + inputAmount: args.inputAmount, + outputToken: args.outputToken, + outputAmount: args.outputAmount, + message: args.message, + fillDeadline: args.fillDeadline, + exclusivityDeadline: args.exclusivityDeadline, + exclusiveRelayer: args.exclusiveRelayer, + blockNumber, + transactionHash, + transactionIndex, + logIndex, + }; +} + export async function fillV3Relay( spokePool: Contract, deposit: Omit,