-
Notifications
You must be signed in to change notification settings - Fork 3
feat: index MerkleDistributor Claimed events
#110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| * getSamplesBetween(1, 9, 3) //returns [[1, 3], [4, 7], [8, 9]] | ||
| * ``` | ||
| */ | ||
| private getSamplesBetween = (min: number, max: number, size: number) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to look into using well-tested utility libs like lodash for these libs
nicholaspai
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approving so as not to block but want to see @amateima review on the higher level architecture changes that I'm not as familiar with.
The Claimed event is the correct event to query. Are there other event props that would be useful to add before we deploy the contract?
AFAICT it has everything that is needed. |
james-a-morris
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left one nit and two questions about the database queries. Looks great 😎
src/modules/configuration/index.ts
Outdated
| chainId: Number(process.env.MERKLE_DISTRIBUTOR_CHAIN_ID || "5"), | ||
| referralsStartWindowIndex: Number(process.env.REFERRALS_START_WINDOW_INDEX || "1"), | ||
| startBlockNumber: 7866869, | ||
| startBlockNumber: 7884371, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this variable be stored in the ENV?
| MERKLE_DISTRIBUTOR_CHAIN_ID= | ||
| MERKLE_DISTRIBUTOR_ADDRESS= |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should take into consideration the possibility of having multiple MerkleDistributor contracts in the future. Maybe we should replace these env vars with a single variable: MERKLE_DISTRIBUTOR_CONTRACTS=[{ address, chainId, ... }]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, I also initially thought about that. But I couldn't think of a use case for supporting multiple MerkleDistributor contracts 🤔 My thought process was:
- The referral rewards are based on a single aggregation of indexed deposits. If we would have multiple types of rewards then I can see a use case. For example, referral rewards of type
Aare distributed via a different MD than rewards of typeB. - Therefore given a single type of referral reward, one configurable MD made more sense to me. It is still configurable, so if we want to change our stage env to use mainnet deployment, we could still do that.
| # MerkleDistributor overrides | ||
| MERKLE_DISTRIBUTOR_CHAIN_ID= | ||
| MERKLE_DISTRIBUTOR_ADDRESS= | ||
| REFERRALS_START_WINDOW_INDEX= |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what REFERRALS_START_WINDOW_INDEX is used for
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I primarily added this to make the offset to determine which claims to consider for the rewards reset configurable. E.g. only consider Claimed events starting from window index 1. Maybe the intent gets clearer here.
But now that I think about it, we would need to re-run a migration if that env var changes. So maybe this isn't a valid use case anymore.
I also thought this could be good for testing purposes where we could shift windows for different rounds of Airdrop + Referrals claims in single MD deployment. E.g.
- 1st testing round Airdrop window index = 0, referrals start index = 1
- 2nd testing round referrals start index = 2
- 3rd round official QA airdrop = 3, referrals = 4
migrations/1667312452969-Claim.ts
Outdated
| await queryRunner.query( | ||
| `ALTER TABLE "merkle_distributor_recipient" DROP CONSTRAINT "UK_merkle_distributor_recipient_merkleDistributorWindowId_addre"`, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this constraint being dropped?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also wondered about this. But it seems that Postgres has a max identifier length of 63 bytes and the originally defined key is of length 65. So it truncates it when created but doesn't when dynamically created by TypeORM. I will change the key to be shorter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should not occur in the future. See 3615420
migrations/1667312452969-Claim.ts
Outdated
| await queryRunner.query( | ||
| `CREATE TABLE "claim" ("id" SERIAL NOT NULL, "caller" character varying NOT NULL, "accountIndex" integer NOT NULL, "windowIndex" integer NOT NULL, "account" character varying NOT NULL, "rewardToken" character varying NOT NULL, "blockNumber" integer NOT NULL, "claimedAt" TIMESTAMP NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UK_claim_windowIndex_accountIndex" UNIQUE ("windowIndex", "accountIndex"), CONSTRAINT "PK_466b305cc2e591047fa1ce58f81" PRIMARY KEY ("id"))`, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can we format the query to use multiple lines? (see other migration files). It makes the queries easier to read
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, yea can do that. Is there a way to automate this somehow in TypeORM? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Manually changed the format for now. See 3615420
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can do that manually only :(
migrations/1667312452969-Claim.ts
Outdated
| await queryRunner.query( | ||
| `ALTER TABLE "merkle_distributor_recipient" ADD CONSTRAINT "UK_merkle_distributor_recipient_merkleDistributorWindowId_address" UNIQUE ("merkleDistributorWindowId", "address")`, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this constraint is already added in a previous migration here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to #110 (comment)
src/modules/scraper/service.ts
Outdated
| const chainId = this.appConfig.values.web3.merkleDistributor.chainId; | ||
| const configStartBlockNumber = this.appConfig.values.web3.merkleDistributor.startBlockNumber; | ||
| const provider = this.providers.getProvider(chainId); | ||
| const latestBlock = await provider.getBlock("latest"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can avoid fetching the latest blocks again, since they were already fetched in the publishBlocks() function. We can call the getLatestBlocks() at the beginning of the run() function and pass the result to this.publishBlocks() and this.publishMerkleDistributorBlocks()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/modules/scraper/service.ts
Outdated
| await this.publishBlocks(); | ||
| } | ||
| if (this.appConfig.values.enableMerkleDistributorEventsProcessing) { | ||
| await this.publishMerkleDistributorBlocks(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just an observation, but it's weird that this.publishBlocks() and this.publishMerkleDistributorBlocks() looks different. Basically they do the same thing (only the repositories and the configured start block are different), but one is a 10 line function and the other one looks more complicated than it actually is. I don't have a problem leaving the code as it is, but I see an opportunity here in organising the code a little bit better. We can chat in private about it. It's clear that the original code wasn't as flexible and modular as it had to be
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I refactored it a bit to reuse the same functions. 0cb04e3
| const supportedChainIds = Object.keys(this.appConfig.values.web3.providers); | ||
|
|
||
| for (const chainId of supportedChainIds) { | ||
| if (this.appConfig.values.web3.providers[chainId]) { | ||
| const provider = new ethers.providers.JsonRpcProvider(this.appConfig.values.web3.providers[chainId]); | ||
| this.providers[chainId] = provider; | ||
| } | ||
| } | ||
|
|
||
| for (const chainId of Object.keys(this.getProviders())) { | ||
| const spokePool = SpokePool__factory.connect( | ||
| appConfig.values.web3.spokePoolContracts[parseInt(chainId)].address, | ||
| this.getProvider(parseInt(chainId)), | ||
| ); | ||
| this.spokePoolEventQueriers[chainId] = new SpokePoolEventsQuerier(spokePool); | ||
| } | ||
| this.setProviders(); | ||
| this.setSpokePoolEventQueriers(); | ||
| this.setMerkleDistributorEventQuerier(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice touch 👍
| for (const chainIdStr of Object.keys(this.getProviders())) { | ||
| const chainId = parseInt(chainIdStr); | ||
| const spokePoolAddress = this.appConfig.values.web3.spokePoolContracts[chainId]?.address; | ||
| if (spokePoolAddress) { | ||
| const spokePool = SpokePool__factory.connect( | ||
| this.appConfig.values.web3.spokePoolContracts[chainId].address, | ||
| this.getProvider(chainId), | ||
| ); | ||
| this.spokePoolEventQueriers[chainId] = new SpokePoolEventsQuerier(spokePool); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A similar approach should be used for instantiating the MerkleDistributorEventQuerier objects. As a side note, setSpokePoolEventQueriers is not bullet proof either because it allows having defined only a single SpokePool contract per chain. Soon we'll have deployed a second SpokePool contract on mainnet. This is why an array for the MerkleDistributor contracts is better than the object that I used to define the config for the SpokePool contracts
| do { | ||
| const blockRangeSizeAtStart = this.blockRangeSize; | ||
| try { | ||
| retryWithLowerBatchSize = false; | ||
| events = []; | ||
|
|
||
| if (this.blockRangeSize) { | ||
| const intervals = this.getSamplesBetween(from, to, this.blockRangeSize); | ||
| // query events only for the first interval to make sure block range is fine | ||
| const [intervalStart, intervalEnd] = intervals[0]; | ||
| const newEvents = await this.contract.queryFilter(filters, intervalStart, intervalEnd); | ||
| events.push(...newEvents); | ||
|
|
||
| // query the rest of block intervals in parallel in order to get the events | ||
| const newEventsList = await Promise.all( | ||
| intervals | ||
| .slice(1) | ||
| .map(([intervalStart, intervalEnd]) => this.contract.queryFilter(filters, intervalStart, intervalEnd)), | ||
| ); | ||
| events.push(...newEventsList.flat()); | ||
| } else { | ||
| const newEvents = await this.contract.queryFilter(filters, from, to); | ||
| events.push(...newEvents); | ||
| } | ||
| } catch (error) { | ||
| if ( | ||
| (error as Web3Error).error?.code === Web3ErrorCode.BLOCK_RANGE_TOO_LARGE || | ||
| (error as Web3Error).error?.code === Web3ErrorCode.EXCEEDED_MAXIMUM_BLOCK_RANGE || | ||
| (error as Web3Error).error?.code === Web3ErrorCode.LOG_RESPONSE_SIZE_EXCEEDED || | ||
| (error as Web3Error).error?.code === Web3ErrorCode.LOG_RESPONSE_SIZE_EXCEEDED_2 | ||
| ) { | ||
| // make sure the block range size wasn't modified by a parallel function call | ||
| if (this.blockRangeSize === blockRangeSizeAtStart) { | ||
| const newBlockRangeSize = this.blockRangeSize ? this.blockRangeSize / 2 : DEFAULT_BLOCK_RANGE; | ||
| this.logger.warn(`lowering block range size from ${this.blockRangeSize} to ${newBlockRangeSize}`); | ||
| this.blockRangeSize = newBlockRangeSize; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I see this logic has not been modified, only moved, Right? I just want to double check because I don't see any difference and this is a crucial part used to query the events
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly. This is only copied and not modified.
This adds the required entities and consumers for processing
Claimedevents of the MerkleDistributor contract.