diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 7eddc8e7..a38d6657 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -1,9 +1,11 @@ import assert from "assert"; import { Contract, utils as ethersUtils } from "ethers"; -import { RelayData, SlowFillRequest, V2RelayData, V3Deposit, V3Fill, V3RelayData } from "../interfaces"; +import { CHAIN_IDs } from "../constants"; +import { FillStatus, RelayData, SlowFillRequest, V2RelayData, V3Deposit, V3Fill, V3RelayData } from "../interfaces"; import { SpokePoolClient } from "../clients"; import { isDefined } from "./TypeGuards"; import { isV2RelayData } from "./V3Utils"; +import { getNetworkName } from "./NetworkUtils"; /** * Find the block range that contains the deposit ID. This is a binary search that searches for the block range @@ -254,4 +256,94 @@ export function getV3RelayHashFromEvent(e: V3Deposit | V3Fill | SlowFillRequest) message: e.message, }; return getV3RelayHash(relayData, e.destinationChainId); -} \ No newline at end of file +} +/** + * Find the amount filled for a deposit at a particular block. + * @param spokePool SpokePool contract instance. + * @param relayData Deposit information that is used to complete a fill. + * @param blockTag Block tag (numeric or "latest") to query at. + * @returns The amount filled for the specified deposit at the requested block (or latest). + */ +export async function relayFillStatus( + spokePool: Contract, + relayData: V3RelayData, + blockTag?: number | "latest", + destinationChainId?: number +): Promise { + destinationChainId ??= await spokePool.chainId(); + const hash = getRelayDataHash(relayData, destinationChainId); + const _fillStatus = await spokePool.fillStatuses(hash, { blockTag }); + const fillStatus = Number(_fillStatus); + + if (![FillStatus.Unfilled, FillStatus.RequestedSlowFill, FillStatus.Filled].includes(fillStatus)) { + const { originChainId, depositId } = relayData; + throw new Error(`relayFillStatus: Unexpected fillStatus for ${originChainId} deposit ${depositId}`); + } + + return fillStatus; +} + +/** + * Find the block at which a fill was completed. + * @todo After SpokePool upgrade, this function can be simplified to use the FillStatus enum. + * @param spokePool SpokePool contract instance. + * @param relayData Deposit information that is used to complete a fill. + * @param lowBlockNumber The lower bound of the search. Must be bounded by SpokePool deployment. + * @param highBlocknumber Optional upper bound for the search. + * @returns The block number at which the relay was completed, or undefined. + */ +export async function findFillBlock( + spokePool: Contract, + relayData: V3RelayData, + lowBlockNumber: number, + highBlockNumber?: number +): Promise { + const { provider } = spokePool; + highBlockNumber ??= await provider.getBlockNumber(); + assert(highBlockNumber > lowBlockNumber, `Block numbers out of range (${lowBlockNumber} > ${highBlockNumber})`); + + // In production the chainId returned from the provider matches 1:1 with the actual chainId. Querying the provider + // object saves an RPC query becasue the chainId is cached by StaticJsonRpcProvider instances. In hre, the SpokePool + // may be configured with a different chainId than what is returned by the provider. + // @todo Sub out actual chain IDs w/ CHAIN_IDs constants + const destinationChainId = Object.values(CHAIN_IDs).includes(relayData.originChainId) + ? (await provider.getNetwork()).chainId + : Number(await spokePool.chainId()); + assert( + relayData.originChainId !== destinationChainId, + `Origin & destination chain IDs must not be equal (${destinationChainId})` + ); + + // Make sure the relay war completed within the block range supplied by the caller. + const [initialFillStatus, finalFillStatus] = ( + await Promise.all([ + relayFillStatus(spokePool, relayData, lowBlockNumber, destinationChainId), + relayFillStatus(spokePool, relayData, highBlockNumber, destinationChainId), + ]) + ).map(Number); + + if (finalFillStatus !== FillStatus.Filled) { + return undefined; // Wasn't filled within the specified block range. + } + + // Was filled earlier than the specified lowBlock. This is an error by the caller. + if (initialFillStatus === FillStatus.Filled) { + const { depositId, originChainId } = relayData; + const [srcChain, dstChain] = [getNetworkName(originChainId), getNetworkName(destinationChainId)]; + throw new Error(`${srcChain} deposit ${depositId} filled on ${dstChain} before block ${lowBlockNumber}`); + } + + // Find the leftmost block where filledAmount equals the deposit amount. + do { + const midBlockNumber = Math.floor((highBlockNumber + lowBlockNumber) / 2); + const fillStatus = await relayFillStatus(spokePool, relayData, midBlockNumber, destinationChainId); + + if (fillStatus === FillStatus.Filled) { + highBlockNumber = midBlockNumber; + } else { + lowBlockNumber = midBlockNumber + 1; + } + } while (lowBlockNumber < highBlockNumber); + + return lowBlockNumber; +} diff --git a/test/SpokePoolClient.ValidateFill.ts b/test/SpokePoolClient.ValidateFill.ts index 9b639cf4..e6294d85 100644 --- a/test/SpokePoolClient.ValidateFill.ts +++ b/test/SpokePoolClient.ValidateFill.ts @@ -1,13 +1,22 @@ -import assert from "assert"; -import { RelayData } from "../src/interfaces"; +import { FillStatus } from "../src/interfaces"; import { SpokePoolClient } from "../src/clients"; -import { bnZero, bnOne, InvalidFill, validateFillForDeposit, queryHistoricalDepositForFill } from "../src/utils"; import { + bnZero, + bnOne, + InvalidFill, + relayFillStatus, + validateFillForDeposit, + queryHistoricalDepositForFill, +} from "../src/utils"; +import { + assert, expect, toBNWei, ethers, SignerWithAddress, depositV2, + depositV3, + fillV3Relay, setupTokensForWallet, toBN, buildFill, @@ -28,7 +37,6 @@ import { mineRandomBlocks, winston, lastSpyLogIncludes, - relayFilledAmount, } from "./utils"; import { CHAIN_ID_TEST_LIST, repaymentChainId } from "./constants"; import { MockConfigStoreClient, MockHubPoolClient, MockSpokePoolClient } from "./mocks"; @@ -105,25 +113,27 @@ describe("SpokePoolClient: Fill Validation", function () { await spokePool_1.setCurrentTime(await getLastBlockTime(spokePool_1.provider)); }); - it("Tracks v2 fill status", async function () { - const deposit = await buildDeposit(hubPoolClient, spokePool_1, erc20_1, depositor, destinationChainId); - - let filled = await relayFilledAmount(spokePool_2, deposit as RelayData); - expect(filled.eq(0)).is.true; - - await buildFill(spokePool_2, erc20_2, depositor, relayer, deposit, 1); - filled = await relayFilledAmount(spokePool_2, deposit as RelayData); - expect(filled.eq(deposit.amount)).is.true; - }); - - it.skip("Tracks v3 fill status", async function () { - const deposit = await buildDeposit(hubPoolClient, spokePool_1, erc20_1, depositor, destinationChainId); + it("Tracks v3 fill status", async function () { + const inputToken = erc20_1.address; + const inputAmount = toBNWei(1); + const outputToken = erc20_2.address; + const outputAmount = inputAmount.sub(bnOne); + const deposit = await depositV3( + spokePool_1, + destinationChainId, + depositor, + inputToken, + inputAmount, + outputToken, + outputAmount + ); - const filled = await relayFilledAmount(spokePool_2, deposit as RelayData); - expect(filled.eq(0)).is.true; + let filled = await relayFillStatus(spokePool_2, deposit); + expect(filled).to.equal(FillStatus.Unfilled); - await buildFill(spokePool_2, erc20_2, depositor, relayer, deposit, 1); - // @todo: Verify fill + await fillV3Relay(spokePool_2, deposit, relayer); + filled = await relayFillStatus(spokePool_2, deposit); + expect(filled).to.equal(FillStatus.Filled); }); it("Accepts valid fills", async function () { @@ -425,7 +435,7 @@ describe("SpokePoolClient: Fill Validation", function () { expect(spokePoolClient1.getDeposits().length).to.equal(0); const historicalDeposit = await queryHistoricalDepositForFill(spokePoolClient1, fill); - assert(historicalDeposit.found === true, "Test is broken"); // Help tsc to narrow the discriminated union. + assert.equal(historicalDeposit.found, true, "Test is broken"); // Help tsc to narrow the discriminated union. expect(historicalDeposit.deposit.depositId).to.deep.equal(deposit.depositId); }); @@ -450,7 +460,7 @@ describe("SpokePoolClient: Fill Validation", function () { expect(spokePoolClient1.getDeposits().length).to.equal(0); const historicalDeposit = await queryHistoricalDepositForFill(spokePoolClient1, fill); - assert(historicalDeposit.found === true, "Test is broken"); // Help tsc to narrow the discriminated union. + assert.equal(historicalDeposit.found, true, "Test is broken"); // Help tsc to narrow the discriminated union. expect(historicalDeposit.deposit.depositId).to.deep.equal(deposit.depositId); }); @@ -500,7 +510,7 @@ describe("SpokePoolClient: Fill Validation", function () { expect(fill.depositId < spokePoolClient1.firstDepositIdForSpokePool).is.true; const search = await queryHistoricalDepositForFill(spokePoolClient1, fill); - assert(search.found === false, "Test is broken"); // Help tsc to narrow the discriminated union. + assert.equal(search.found, false, "Test is broken"); // Help tsc to narrow the discriminated union. expect(search.code).to.equal(InvalidFill.DepositIdInvalid); expect(lastSpyLogIncludes(spy, "Queried RPC for deposit")).is.not.true; }); @@ -526,7 +536,7 @@ describe("SpokePoolClient: Fill Validation", function () { expect(fill.depositId > spokePoolClient1.lastDepositIdForSpokePool).is.true; const search = await queryHistoricalDepositForFill(spokePoolClient1, fill); - assert(search.found === false, "Test is broken"); // Help tsc to narrow the discriminated union. + assert.equal(search.found, false, "Test is broken"); // Help tsc to narrow the discriminated union. expect(search.code).to.equal(InvalidFill.DepositIdInvalid); expect(lastSpyLogIncludes(spy, "Queried RPC for deposit")).is.not.true; }); @@ -540,7 +550,7 @@ describe("SpokePoolClient: Fill Validation", function () { await Promise.all([spokePoolClient1.update(), spokePoolClient2.update()]); const search = await queryHistoricalDepositForFill(spokePoolClient1, fill); - assert(search.found === false, "Test is broken"); // Help tsc to narrow the discriminated union. + assert.equal(search.found, false, "Test is broken"); // Help tsc to narrow the discriminated union. expect(search.code).to.equal(InvalidFill.FillMismatch); }); diff --git a/test/SpokePoolClient.fills.ts b/test/SpokePoolClient.fills.ts index 8e722fc6..8661d200 100644 --- a/test/SpokePoolClient.fills.ts +++ b/test/SpokePoolClient.fills.ts @@ -1,12 +1,14 @@ import hre from "hardhat"; +import { ZERO_ADDRESS, EMPTY_MESSAGE } from "../src/constants"; import { SpokePoolClient } from "../src/clients"; -import { Deposit, RelayData } from "../src/interfaces"; -import { getNetworkName } from "../src/utils"; +import { Deposit, V3Deposit } from "../src/interfaces"; +import { bnOne, findFillBlock, getCurrentTime, getNetworkName } from "../src/utils"; import { assertPromiseError, Contract, SignerWithAddress, buildFill, + fillV3Relay, createSpyLogger, deploySpokePoolWithToken, destinationChainId, @@ -14,7 +16,6 @@ import { expect, originChainId, setupTokensForWallet, - findFillBlock, toBNWei, } from "./utils"; @@ -27,6 +28,13 @@ const originChainId2 = originChainId + 1; let spokePoolClient: SpokePoolClient; describe("SpokePoolClient: Fills", function () { + const message = EMPTY_MESSAGE; + const exclusivityDeadline = 0; + const exclusiveRelayer = ZERO_ADDRESS; + + let quoteTimestamp: number; + let fillDeadline: number; + beforeEach(async function () { [, depositor, relayer1, relayer2] = await ethers.getSigners(); ({ spokePool, erc20, destErc20, weth, deploymentBlock } = await deploySpokePoolWithToken( @@ -45,6 +53,9 @@ describe("SpokePoolClient: Fills", function () { await setupTokensForWallet(spokePool, relayer1, [erc20, destErc20], weth, 10); await setupTokensForWallet(spokePool, relayer2, [erc20, destErc20], weth, 10); + + quoteTimestamp = Number(await spokePool.getCurrentTime()); + fillDeadline = quoteTimestamp + 600; }); it("Correctly fetches fill data single fill, single chain", async function () { @@ -57,10 +68,10 @@ describe("SpokePoolClient: Fills", function () { originChainId, destinationChainId, relayerFeePct: toBNWei("0.01"), - quoteTimestamp: Date.now(), + quoteTimestamp, realizedLpFeePct: toBNWei("0.01"), destinationToken: destErc20.address, - message: "0x", + message, }; await buildFill(spokePool, destErc20, depositor, relayer1, deposit, 1); await buildFill(spokePool, destErc20, depositor, relayer1, { ...deposit, depositId: 1 }, 1); @@ -77,10 +88,10 @@ describe("SpokePoolClient: Fills", function () { originChainId, destinationChainId, relayerFeePct: toBNWei("0.01"), - quoteTimestamp: Date.now(), + quoteTimestamp, realizedLpFeePct: toBNWei("0.01"), destinationToken: destErc20.address, - message: "0x", + message, }; // Do 6 deposits. 2 for the first depositor on chain1, 1 for the first depositor on chain2, 1 for the second @@ -107,22 +118,26 @@ describe("SpokePoolClient: Fills", function () { expect(spokePoolClient.getFillsForRelayer(relayer2.address).length).to.equal(3); }); - it("Correctly locates the block number for a FilledRelay event", async function () { + it("Correctly locates the block number for a FilledV3Relay event", async function () { const nBlocks = 1_000; + const inputAmount = toBNWei(1); - const deposit: Deposit = { + const quoteTimestamp = getCurrentTime(); + const deposit: V3Deposit = { depositId: 0, - depositor: depositor.address, - recipient: depositor.address, - originToken: erc20.address, - amount: toBNWei("1"), originChainId, destinationChainId, - relayerFeePct: toBNWei("0.01"), - quoteTimestamp: Date.now(), - realizedLpFeePct: toBNWei("0.01"), - destinationToken: destErc20.address, - message: "0x", + depositor: depositor.address, + recipient: depositor.address, + inputToken: erc20.address, + inputAmount, + outputToken: destErc20.address, + outputAmount: inputAmount.sub(1), + quoteTimestamp, + message, + fillDeadline, + exclusivityDeadline, + exclusiveRelayer, }; // Submit the fill randomly within the next `nBlocks` blocks. @@ -132,33 +147,37 @@ describe("SpokePoolClient: Fills", function () { for (let i = 0; i < nBlocks; ++i) { const blockNumber = await spokePool.provider.getBlockNumber(); if (blockNumber === targetFillBlock - 1) { - await buildFill(spokePool, destErc20, depositor, relayer1, deposit, 1); + const { blockNumber: fillBlockNumber } = await fillV3Relay(spokePool, deposit, relayer1); + expect(fillBlockNumber).to.equal(targetFillBlock); continue; } await hre.network.provider.send("evm_mine"); } - const fillBlock = await findFillBlock(spokePool, deposit as RelayData, startBlock); + const fillBlock = await findFillBlock(spokePool, deposit, startBlock); expect(fillBlock).to.equal(targetFillBlock); }); it("FilledRelay block search: bounds checking", async function () { const nBlocks = 100; + const inputAmount = toBNWei(1); - const deposit: Deposit = { + const deposit: V3Deposit = { depositId: 0, - depositor: depositor.address, - recipient: depositor.address, - originToken: erc20.address, - amount: toBNWei("1"), originChainId, destinationChainId, - relayerFeePct: toBNWei("0.01"), - quoteTimestamp: Date.now(), - realizedLpFeePct: toBNWei("0.01"), - destinationToken: destErc20.address, - message: "0x", + depositor: depositor.address, + recipient: depositor.address, + inputToken: erc20.address, + inputAmount, + outputToken: destErc20.address, + outputAmount: inputAmount.sub(bnOne), + quoteTimestamp, + message, + fillDeadline, + exclusivityDeadline, + exclusiveRelayer, }; const startBlock = await spokePool.provider.getBlockNumber(); @@ -167,23 +186,22 @@ describe("SpokePoolClient: Fills", function () { } // No fill has been made, so expect an undefined fillBlock. - const fillBlock = await findFillBlock(spokePool, deposit as RelayData, startBlock); + const fillBlock = await findFillBlock(spokePool, deposit, startBlock); expect(fillBlock).to.be.undefined; - await buildFill(spokePool, destErc20, depositor, relayer1, deposit, 1); - const lateBlockNumber = await spokePool.provider.getBlockNumber(); + const { blockNumber: lateBlockNumber } = await fillV3Relay(spokePool, deposit, relayer1); await hre.network.provider.send("evm_mine"); // Now search for the fill _after_ it was filled and expect an exception. const srcChain = getNetworkName(deposit.originChainId); await assertPromiseError( - findFillBlock(spokePool, deposit as RelayData, lateBlockNumber), + findFillBlock(spokePool, deposit, lateBlockNumber), `${srcChain} deposit ${deposit.depositId} filled on ` ); // Should assert if highBlock <= lowBlock. await assertPromiseError( - findFillBlock(spokePool, deposit as RelayData, await spokePool.provider.getBlockNumber()), + findFillBlock(spokePool, deposit, await spokePool.provider.getBlockNumber()), "Block numbers out of range" ); }); diff --git a/test/utils/SpokePoolUtils.ts b/test/utils/SpokePoolUtils.ts index c071476d..05722d08 100644 --- a/test/utils/SpokePoolUtils.ts +++ b/test/utils/SpokePoolUtils.ts @@ -1,25 +1,13 @@ -import { BigNumber, Contract } from "."; import { DepositWithBlock, Fill, - FillStatus, FillType, - RelayData, V2DepositWithBlock, V2Fill, V3DepositWithBlock, V3Fill, - V3RelayData, } from "../../src/interfaces"; -import { assert } from "chai"; -import { - bnZero, - getNetworkName, - getRelayDataHash, - getRelayDataOutputAmount, - isV2Deposit, - isV2RelayData, -} from "../../src/utils"; +import { bnZero, isV2Deposit } from "../../src/utils"; export function fillFromDeposit(deposit: DepositWithBlock, relayer: string): Fill { return isV2Deposit(deposit) ? v2FillFromDeposit(deposit, relayer) : v3FillFromDeposit(deposit, relayer); @@ -79,114 +67,3 @@ export function v3FillFromDeposit(deposit: V3DepositWithBlock, relayer: string): return fill; } - -/** - * Find the amount filled for a deposit at a particular block. - * @param spokePool SpokePool contract instance. - * @param relayData Deposit information that is used to complete a fill. - * @param blockTag Block tag (numeric or "latest") to query at. - * @returns The amount filled for the specified deposit at the requested block (or latest). - */ -export async function relayFilledAmount( - spokePool: Contract, - relayData: RelayData, - blockTag?: number | "latest" -): Promise { - const hash = getRelayDataHash(relayData); - - if (isV2RelayData(relayData)) { - const fills = await spokePool.queryFilter( - await spokePool.filters.FilledRelay( - null, - null, - null, - null, - relayData.originChainId, - null, - null, - null, - relayData.depositId, - null, - null, - null, - null, - null, - null - ) - ); - // TODO: For this to be safe in production, you'd need to get the hash of the events - // to match against `hash`, but since this is used in tests only we can just match on originChainId and depositId. - if (fills.length === 0) return bnZero; - if (blockTag === "latest") return fills[fills.length - 1].args?.totalFilledAmount; - else { - // Return latest totalFilled amount before blockTag which would be equivalent to the total filled amount - // as of the block tag. - return ( - fills.find((e) => { - if (blockTag === undefined) return e.args?.totalFilledAmount; - else if (e.blockNumber <= blockTag) return e.args?.totalFilledAmount; - })?.args?.totalFilledAmount ?? bnZero - ); - } - } - - const fillStatus = await spokePool.fillStatuses(hash, { blockTag }); - - // @note: If the deposit was updated then the fill amount may be _less_ than outputAmount. - // @todo: Remove V3RelayData type assertion once RelayData type is unionised. - return fillStatus === FillStatus.Filled ? (relayData as V3RelayData).outputAmount : bnZero; -} - -/** - * Find the block at which a fill was completed. - * @todo After SpokePool upgrade, this function can be simplified to use the FillStatus enum. - * @param spokePool SpokePool contract instance. - * @param relayData Deposit information that is used to complete a fill. - * @param lowBlockNumber The lower bound of the search. Must be bounded by SpokePool deployment. - * @param highBlocknumber Optional upper bound for the search. - * @returns The block number at which the relay was completed, or undefined. - */ -export async function findFillBlock( - spokePool: Contract, - relayData: RelayData, - lowBlockNumber: number, - highBlockNumber?: number -): Promise { - const { provider } = spokePool; - highBlockNumber ??= await provider.getBlockNumber(); - assert(highBlockNumber > lowBlockNumber, `Block numbers out of range (${lowBlockNumber} > ${highBlockNumber})`); - const { chainId: destinationChainId } = await provider.getNetwork(); - - // Make sure the relay is 100% completed within the block range supplied by the caller. - const [initialFillAmount, finalFillAmount] = await Promise.all([ - relayFilledAmount(spokePool, relayData, lowBlockNumber), - relayFilledAmount(spokePool, relayData, highBlockNumber), - ]); - - // Wasn't filled within the specified block range. - const relayAmount = getRelayDataOutputAmount(relayData); - if (finalFillAmount.lt(relayAmount)) { - return undefined; - } - - // Was filled earlier than the specified lowBlock.. This is an error by the caller. - if (initialFillAmount.eq(relayAmount)) { - const { depositId, originChainId } = relayData; - const [srcChain, dstChain] = [getNetworkName(originChainId), getNetworkName(destinationChainId)]; - throw new Error(`${srcChain} deposit ${depositId} filled on ${dstChain} before block ${lowBlockNumber}`); - } - - // Find the leftmost block where filledAmount equals the deposit amount. - do { - const midBlockNumber = Math.floor((highBlockNumber + lowBlockNumber) / 2); - const filledAmount = await relayFilledAmount(spokePool, relayData, midBlockNumber); - - if (filledAmount.eq(relayAmount)) { - highBlockNumber = midBlockNumber; - } else { - lowBlockNumber = midBlockNumber + 1; - } - } while (lowBlockNumber < highBlockNumber); - - return lowBlockNumber; -} diff --git a/test/utils/utils.ts b/test/utils/utils.ts index 473a3c25..52bd7a30 100644 --- a/test/utils/utils.ts +++ b/test/utils/utils.ts @@ -5,7 +5,7 @@ import { GLOBAL_CONFIG_STORE_KEYS, HubPoolClient, } from "../../src/clients"; -import { V2Deposit, V2Fill } from "../../src/interfaces"; +import { V2Deposit, V2Fill, V3Deposit, V3DepositWithBlock, V3FillWithBlock } from "../../src/interfaces"; import { bnUint32Max, bnZero, @@ -310,18 +310,135 @@ export async function buildV2DepositStruct( }; } -// export function buildV3Deposit( -// _hubPoolClient: HubPoolClient, -// _spokePool: Contract, -// _destinationChainId: number, -// _recipientAndDepositor: SignerWithAddress, -// _inputToken: Contract, -// _inputAmount: BigNumber, -// _outputToken: Contract, -// _outputAmount: BigNumber -// ): Promise { -// throw new Error("not supported"); -// } +export async function depositV3( + spokePool: Contract, + destinationChainId: number, + signer: SignerWithAddress, + inputToken: string, + inputAmount: BigNumber, + outputToken: string, + outputAmount: BigNumber, + opts: { + destinationChainId?: number; + recipient?: string; + quoteTimestamp?: number; + message?: string; + fillDeadline?: number; + exclusivityDeadline?: number; + exclusiveRelayer?: string; + } = {} +): Promise { + const depositor = signer.address; + const recipient = opts.recipient ?? depositor; + + const [spokePoolTime, fillDeadlineBuffer] = ( + await Promise.all([spokePool.getCurrentTime(), spokePool.fillDeadlineBuffer()]) + ).map((n) => Number(n)); + + const quoteTimestamp = opts.quoteTimestamp ?? spokePoolTime; + const message = opts.message ?? EMPTY_MESSAGE; + const fillDeadline = opts.fillDeadline ?? spokePoolTime + fillDeadlineBuffer; + const exclusivityDeadline = opts.exclusivityDeadline ?? 0; + const exclusiveRelayer = opts.exclusiveRelayer ?? zeroAddress; + + await spokePool + .connect(signer) + .depositV3( + depositor, + recipient, + inputToken, + outputToken, + inputAmount, + outputAmount, + destinationChainId, + exclusiveRelayer, + quoteTimestamp, + fillDeadline, + exclusivityDeadline, + message + ); + + const [events, originChainId] = await Promise.all([ + spokePool.queryFilter(spokePool.filters.V3FundsDeposited()), + spokePool.chainId(), + ]); + + const lastEvent = events.at(-1); + const args = lastEvent?.args; + assert.exists(args); + + const { blockNumber, transactionHash, transactionIndex, logIndex } = lastEvent!; + + return { + depositId: args!.depositId, + originChainId: Number(originChainId), + destinationChainId: Number(args!.destinationChainId), + depositor: args!.depositor, + recipient: args!.recipient, + inputToken: args!.inputToken, + inputAmount: args!.inputAmount, + outputToken: args!.outputToken, + outputAmount: args!.outputAmount, + quoteTimestamp: args!.quoteTimestamp, + message: args!.message, + fillDeadline: args!.fillDeadline, + exclusivityDeadline: args!.exclusivityDeadline, + exclusiveRelayer: args!.exclusiveRelayer, + quoteBlockNumber: 0, // @todo + blockNumber, + transactionHash, + transactionIndex, + logIndex, + }; +} + +export async function fillV3Relay( + spokePool: Contract, + deposit: Omit, + signer: SignerWithAddress, + repaymentChainId?: number +): Promise { + const destinationChainId = Number(await spokePool.chainId()); + assert.notEqual(deposit.originChainId, destinationChainId); + + await spokePool.connect(signer).fillV3Relay(deposit, repaymentChainId ?? destinationChainId); + + const events = await spokePool.queryFilter(spokePool.filters.FilledV3Relay()); + 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, + relayer: args.relayer, + repaymentChainId: Number(args.repaymentChainId), + updatableRelayData: { + recipient: args.relayExecutionInfo.recipient, + message: args.relayExecutionInfo.message, + outputAmount: args.relayExecutionInfo.outputAmount, + fillType: args.relayExecutionInfo.fillType, + }, + blockNumber, + transactionHash, + transactionIndex, + logIndex, + }; +} // @note To be deprecated post-v3. export async function buildDeposit(