Skip to content

Commit

Permalink
refactor: Replace ethers Event with Log (#738)
Browse files Browse the repository at this point in the history
The ethers Event type is an extension of the underlying Log type, which
is actually the type that contains almost all of the event data that the 
SDK needs for Across. Since Event extends Log, it's safe to narrow the 
scope of the RPC data down to a Log type. This significantly reduces the 
delta to viem's Log type and is a key stepping stone for viem and ethers
to be used interchangeably for RPC interfacing & transport.

It additionally seems like this simplification might also support Solana
events (...with some eventual coercion).

It should be noted that this change touches some code that makes a lot 
of assumptions about the event types that are being handled, and there
are some warts. I've tried not to make things any less safe than they
already are, but this code is generally in need of a refactoring to
improve type safety.
  • Loading branch information
pxrl committed Sep 20, 2024
1 parent bd12d82 commit d81a057
Show file tree
Hide file tree
Showing 15 changed files with 147 additions and 185 deletions.
4 changes: 3 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ module.exports = {
{
"patterns": [
{ group: ["@ethersproject/bignumber"], message: "Use 'src/utils/BigNumberUtils' instead" },
{ group: ["@ethersproject/contracts"], importNames: ["Event"], message: "Use Log from 'src/interfaces/Common' instead" },
],
"paths": [
{ name: "ethers", importNames: ["BigNumber"], message: "Use 'src/utils/BigNumberUtils' instead" }
{ name: "ethers", importNames: ["BigNumber"], message: "Use 'src/utils/BigNumberUtils' instead" },
{ name: "ethers", importNames: ["Event"], message: "Use Log from 'src/interfaces/Common' instead" }
]
}
],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@across-protocol/sdk",
"author": "UMA Team",
"version": "3.1.36",
"version": "3.2.0",
"license": "AGPL-3.0",
"homepage": "https://docs.across.to/reference/sdk",
"files": [
Expand Down
19 changes: 9 additions & 10 deletions src/clients/AcrossConfigStoreClient/AcrossConfigStoreClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { utils, across } from "@uma/sdk";
import assert from "assert";
import { Contract, Event } from "ethers";
import { Contract } from "ethers";
import winston from "winston";
import { isError } from "../../typeguards";
import {
Expand All @@ -14,7 +14,6 @@ import {
paginatedEventQuery,
sortEventsAscendingInPlace,
sortEventsDescending,
spreadEvent,
spreadEventWithBlockNumber,
toBN,
utf8ToHex,
Expand All @@ -25,8 +24,10 @@ import {
DisabledChainsUpdate,
GlobalConfigUpdate,
LiteChainsIdListUpdate,
Log,
ParsedTokenConfig,
RouteRateModelUpdate,
SortableEvent,
SpokePoolTargetBalance,
SpokeTargetBalanceUpdate,
TokenConfig,
Expand All @@ -41,8 +42,8 @@ type ConfigStoreUpdateSuccess = {
chainId: number;
searchEndBlock: number;
events: {
updatedTokenConfigEvents: Event[];
updatedGlobalConfigEvents: Event[];
updatedTokenConfigEvents: Log[];
updatedGlobalConfigEvents: Log[];
globalConfigUpdateTimes: number[];
};
};
Expand Down Expand Up @@ -430,12 +431,10 @@ export class AcrossConfigStoreClient extends BaseAbstractClient {

// Save new Global config updates.
for (let i = 0; i < updatedGlobalConfigEvents.length; i++) {
const event = updatedGlobalConfigEvents[i];
const args = {
blockNumber: event.blockNumber,
transactionIndex: event.transactionIndex,
logIndex: event.logIndex,
...spreadEvent(event.args),
const args = spreadEventWithBlockNumber(updatedGlobalConfigEvents[i]) as SortableEvent & {
key: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
};

if (args.key === utf8ToHex(GLOBAL_CONFIG_STORE_KEYS.MAX_RELAYER_REPAYMENT_LEAF_SIZE)) {
Expand Down
15 changes: 7 additions & 8 deletions src/clients/HubPoolClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from "assert";
import { Contract, Event, EventFilter } from "ethers";
import { Contract, EventFilter } from "ethers";
import _ from "lodash";
import winston from "winston";
import { DEFAULT_CACHING_SAFE_LAG, DEFAULT_CACHING_TTL } from "../constants";
Expand All @@ -13,6 +13,7 @@ import {
DisputedRootBundle,
ExecutedRootBundle,
L1Token,
Log,
LpToken,
PendingRootBundle,
ProposedRootBundle,
Expand All @@ -38,7 +39,6 @@ import {
paginatedEventQuery,
shouldCache,
sortEventsDescending,
spreadEvent,
spreadEventWithBlockNumber,
toBN,
getTokenInfo,
Expand All @@ -52,7 +52,7 @@ type HubPoolUpdateSuccess = {
success: true;
currentTime: number;
pendingRootBundleProposal: PendingRootBundle;
events: Record<string, Event[]>;
events: Record<string, Log[]>;
searchEndBlock: number;
};
type HubPoolUpdateFailure = {
Expand Down Expand Up @@ -901,11 +901,10 @@ export class HubPoolClient extends BaseAbstractClient {
// only run iff a new token has been enabled. Will only append iff the info is not there already.
// Filter out any duplicate addresses. This might happen due to enabling, disabling and re-enabling a token.
if (eventsToQuery.includes("L1TokenEnabledForLiquidityProvision")) {
const uniqueL1Tokens = [
...Array.from(
new Set(events["L1TokenEnabledForLiquidityProvision"].map((event) => spreadEvent(event.args).l1Token))
),
];
const uniqueL1Tokens = dedupArray(
events["L1TokenEnabledForLiquidityProvision"].map((event) => String(event.args["l1Token"]))
);

const [tokenInfo, lpTokenInfo] = await Promise.all([
Promise.all(uniqueL1Tokens.map((l1Token: string) => fetchTokenInfo(l1Token, this.hubPool.provider))),
Promise.all(
Expand Down
76 changes: 39 additions & 37 deletions src/clients/SpokePoolClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Contract, Event, EventFilter } from "ethers";
import { Contract, EventFilter } from "ethers";
import winston from "winston";
import {
AnyObject,
Expand All @@ -20,22 +20,20 @@ import {
spreadEventWithBlockNumber,
} from "../utils/EventUtils";
import { validateFillForDeposit } from "../utils/FlowUtils";

import { ZERO_ADDRESS } from "../constants";
import {
Deposit,
DepositWithBlock,
Fill,
FillStatus,
FillWithBlock,
FilledV3RelayEvent,
Log,
RelayData,
RelayerRefundExecutionWithBlock,
RootBundleRelayWithBlock,
SlowFillRequestWithBlock,
SpeedUpWithBlock,
TokensBridged,
V3FundsDepositedEvent,
} from "../interfaces";
import { SpokePool } from "../typechain";
import { getNetworkName } from "../utils/NetworkUtils";
Expand All @@ -50,7 +48,7 @@ type SpokePoolUpdateSuccess = {
oldestTime: number;
firstDepositId: number;
latestDepositId: number;
events: Event[][];
events: Log[][];
searchEndBlock: number;
};
type SpokePoolUpdateFailure = {
Expand Down Expand Up @@ -510,7 +508,7 @@ export class SpokePoolClient extends BaseAbstractClient {
}

// Sort all events to ensure they are stored in a consistent order.
events.forEach((events: Event[]) => sortEventsAscendingInPlace(events));
events.forEach((events: Log[]) => sortEventsAscendingInPlace(events));

return {
success: true,
Expand Down Expand Up @@ -549,9 +547,7 @@ export class SpokePoolClient extends BaseAbstractClient {
}

if (eventsToQuery.includes("V3FundsDeposited")) {
const depositEvents = [
...((queryResults[eventsToQuery.indexOf("V3FundsDeposited")] ?? []) as V3FundsDepositedEvent[]),
];
const depositEvents = queryResults[eventsToQuery.indexOf("V3FundsDeposited")] ?? [];
if (depositEvents.length > 0) {
this.log("debug", `Using ${depositEvents.length} newly queried deposit events for chain ${this.chainId}`, {
earliestEvent: depositEvents[0].blockNumber,
Expand All @@ -560,15 +556,25 @@ export class SpokePoolClient extends BaseAbstractClient {

// For each deposit, resolve its quoteTimestamp to a block number on the HubPool.
// Don't bother filtering for uniqueness; the HubPoolClient handles this efficienctly.
const quoteBlockNumbers = await this.getBlockNumbers(depositEvents.map(({ args }) => args.quoteTimestamp));
const quoteBlockNumbers = await this.getBlockNumbers(
depositEvents.map(({ args }) => Number(args["quoteTimestamp"]))
);
for (const event of depositEvents) {
const rawDeposit = spreadEventWithBlockNumber(event);
const quoteBlockNumber = quoteBlockNumbers[Number(event.args["quoteTimestamp"])];

// Derive and append the common properties that are not part of the onchain event.
const quoteBlockNumber = quoteBlockNumbers[event.args.quoteTimestamp];
const deposit = { ...(rawDeposit as DepositWithBlock), originChainId: this.chainId, quoteBlockNumber };
const deposit = {
...spreadEventWithBlockNumber(event),
quoteBlockNumber,
originChainId: this.chainId,
// The following properties are placeholders to be updated immediately.
fromLiteChain: true,
toLiteChain: true,
} as DepositWithBlock;

deposit.fromLiteChain = this.isOriginLiteChain(deposit);
deposit.toLiteChain = this.isDestinationLiteChain(deposit);

if (deposit.outputToken === ZERO_ADDRESS) {
deposit.outputToken = this.getDestinationTokenForDeposit(deposit);
}
Expand All @@ -587,16 +593,12 @@ export class SpokePoolClient extends BaseAbstractClient {
}
}

// TODO: When validating fills with deposits for the purposes of UBA flows, do we need to consider
// speed ups as well? For example, do we need to also consider that the speed up is before the fill
// timestamp to be applied for the fill? My brain hurts.
// Update deposits with speed up requests from depositor.
if (eventsToQuery.includes("RequestedSpeedUpV3Deposit")) {
const speedUpEvents = [...(queryResults[eventsToQuery.indexOf("RequestedSpeedUpV3Deposit")] ?? [])];
const speedUpEvents = queryResults[eventsToQuery.indexOf("RequestedSpeedUpV3Deposit")] ?? [];

for (const event of speedUpEvents) {
const rawEvent = spreadEventWithBlockNumber(event);
const speedUp = { ...rawEvent, originChainId: this.chainId } as SpeedUpWithBlock;
const speedUp = { ...spreadEventWithBlockNumber(event), originChainId: this.chainId } as SpeedUpWithBlock;
assign(this.speedUps, [speedUp.depositor, speedUp.depositId], [speedUp]);

// Find deposit hash matching this speed up event and update the deposit data associated with the hash,
Expand All @@ -615,10 +617,11 @@ export class SpokePoolClient extends BaseAbstractClient {
if (eventsToQuery.includes("RequestedV3SlowFill")) {
const slowFillRequests = queryResults[eventsToQuery.indexOf("RequestedV3SlowFill")];
for (const event of slowFillRequests) {
const slowFillRequest: SlowFillRequestWithBlock = {
...(spreadEventWithBlockNumber(event) as SlowFillRequestWithBlock),
const slowFillRequest = {
...spreadEventWithBlockNumber(event),
destinationChainId: this.chainId,
};
} as SlowFillRequestWithBlock;

const relayDataHash = getRelayDataHash(slowFillRequest, this.chainId);
if (this.slowFillRequests[relayDataHash] !== undefined) {
continue;
Expand All @@ -628,7 +631,7 @@ export class SpokePoolClient extends BaseAbstractClient {
}

if (eventsToQuery.includes("FilledV3Relay")) {
const fillEvents = [...((queryResults[eventsToQuery.indexOf("FilledV3Relay")] ?? []) as FilledV3RelayEvent[])];
const fillEvents = queryResults[eventsToQuery.indexOf("FilledV3Relay")] ?? [];

if (fillEvents.length > 0) {
this.log("debug", `Using ${fillEvents.length} newly queried fill events for chain ${this.chainId}`, {
Expand All @@ -640,9 +643,9 @@ export class SpokePoolClient extends BaseAbstractClient {
// test that the types are complete. A broader change in strategy for safely unpacking events will be introduced.
for (const event of fillEvents) {
const fill = {
...(spreadEventWithBlockNumber(event) as FillWithBlock),
...spreadEventWithBlockNumber(event),
destinationChainId: this.chainId,
};
} as FillWithBlock;

assign(this.fills, [fill.originChainId], [fill]);
assign(this.depositHashesToFills, [this.getDepositHash(fill)], [fill]);
Expand Down Expand Up @@ -808,7 +811,7 @@ export class SpokePoolClient extends BaseAbstractClient {
);
const tStop = Date.now();

const event = (query as V3FundsDepositedEvent[]).find((deposit) => deposit.args.depositId === depositId);
const event = query.find(({ args }) => args["depositId"] === depositId);
if (event === undefined) {
const srcChain = getNetworkName(this.chainId);
const dstChain = getNetworkName(destinationChainId);
Expand All @@ -817,19 +820,18 @@ export class SpokePoolClient extends BaseAbstractClient {
` between ${srcChain} blocks [${searchBounds.low}, ${searchBounds.high}]`
);
}
const partialDeposit = spreadEventWithBlockNumber(event) as DepositWithBlock;
const quoteBlockNumber = await this.getBlockNumber(partialDeposit.quoteTimestamp);

// Append destination token and realized lp fee to deposit.
const deposit: DepositWithBlock = {
...partialDeposit,
const deposit = {
...spreadEventWithBlockNumber(event),
originChainId: this.chainId,
quoteBlockNumber,
outputToken:
partialDeposit.outputToken === ZERO_ADDRESS
? this.getDestinationTokenForDeposit({ ...partialDeposit, originChainId: this.chainId })
: partialDeposit.outputToken,
};
quoteBlockNumber: await this.getBlockNumber(Number(event.args["quoteTimestamp"])),
fromLiteChain: true, // To be updated immediately afterwards.
toLiteChain: true, // To be updated immediately afterwards.
} as DepositWithBlock;

if (deposit.outputToken === ZERO_ADDRESS) {
deposit.outputToken = this.getDestinationTokenForDeposit(deposit);
}
deposit.fromLiteChain = this.isOriginLiteChain(deposit);
deposit.toLiteChain = this.isDestinationLiteChain(deposit);

Expand Down
22 changes: 11 additions & 11 deletions src/clients/mocks/MockConfigStoreClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import assert from "assert";
import winston from "winston";
import { Contract, Event, ethers } from "ethers";
import { EventSearchConfig, MakeOptional, isDefined, utf8ToHex } from "../../utils";
import { Contract, ethers } from "ethers";
import { Log } from "../../interfaces";
import { getCurrentTime, EventSearchConfig, MakeOptional, isDefined, utf8ToHex } from "../../utils";
import {
AcrossConfigStoreClient,
ConfigStoreUpdate,
Expand Down Expand Up @@ -60,7 +61,7 @@ export class MockConfigStoreClient extends AcrossConfigStoreClient {
this.configStoreVersion = version;
}

async _update(): Promise<ConfigStoreUpdate> {
_update(): Promise<ConfigStoreUpdate> {
// Backwards compatibility for pre-existing MockConfigStoreClient users.
if (this.eventManager === null) {
return super._update();
Expand All @@ -72,23 +73,22 @@ export class MockConfigStoreClient extends AcrossConfigStoreClient {
// Ensure an array for every requested event exists, in the requested order.
// All requested event types must be populated in the array (even if empty).
const globalConfigUpdateTimes: number[] = [];
const _events: Event[][] = eventNames.map(() => []);
const _events: Log[][] = eventNames.map(() => []);
for (const event of this.eventManager.getEvents().flat()) {
const idx = eventNames.indexOf(event.event as string);
if (idx !== -1) {
_events[idx].push(event);
}

if (event.event === "UpdatedGlobalConfig") {
const block = await event.getBlock();
globalConfigUpdateTimes.push(block.timestamp);
globalConfigUpdateTimes.push(getCurrentTime());
}
}

// Transform 2d-events array into a record.
const events = Object.fromEntries(eventNames.map((eventName, idx) => [eventName, _events[idx]]));

return {
return Promise.resolve({
success: true,
chainId: this.chainId as number,
searchEndBlock: this.eventSearchConfig.toBlock || latestBlockSearched,
Expand All @@ -97,22 +97,22 @@ export class MockConfigStoreClient extends AcrossConfigStoreClient {
globalConfigUpdateTimes,
updatedTokenConfigEvents: events["UpdatedTokenConfig"],
},
};
});
}

updateGlobalConfig(key: string, value: string, overrides: EventOverrides = {}): Event {
updateGlobalConfig(key: string, value: string, overrides: EventOverrides = {}): Log {
return this.generateConfig("UpdatedGlobalConfig", utf8ToHex(key), value, overrides);
}

updateTokenConfig(key: string, value: string, overrides: EventOverrides = {}): Event {
updateTokenConfig(key: string, value: string, overrides: EventOverrides = {}): Log {
// Verify that the key is a valid address
if (ethers.utils.isAddress(key) === false) {
throw new Error(`Invalid address: ${key}`);
}
return this.generateConfig("UpdatedTokenConfig", key, value, overrides);
}

private generateConfig(event: string, key: string, value: string, overrides: EventOverrides = {}): Event {
private generateConfig(event: string, key: string, value: string, overrides: EventOverrides = {}): Log {
assert(this.eventManager !== null);

const topics = [key, value];
Expand Down
Loading

0 comments on commit d81a057

Please sign in to comment.