Skip to content

Commit

Permalink
feat: Support querying relay fill status as an array (#578)
Browse files Browse the repository at this point in the history
This is an efficient way of determining bulk fill status in a single RPC 
call. Both the relayer and dataworker currently make these requests per 
deposit, so this will reduce RPC consumption and latency.
  • Loading branch information
pxrl authored Mar 7, 2024
1 parent fe72fa4 commit 6a2e783
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 4 deletions.
33 changes: 31 additions & 2 deletions src/utils/SpokeUtils.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
Expand Down
51 changes: 50 additions & 1 deletion test/SpokePoolClient.ValidateFill.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -18,6 +19,7 @@ import {
depositV2,
depositV3,
fillV3Relay,
requestV3SlowFill,
setupTokensForWallet,
toBN,
buildFill,
Expand Down Expand Up @@ -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);
Expand Down
49 changes: 48 additions & 1 deletion test/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -410,6 +418,45 @@ export async function depositV3(
};
}

export async function requestV3SlowFill(
spokePool: Contract,
relayData: V3RelayData,
signer: SignerWithAddress
): Promise<SlowFillRequestWithBlock> {
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<V3Deposit, "destinationChainId">,
Expand Down

0 comments on commit 6a2e783

Please sign in to comment.