-
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
Changes from all commits
6045fad
89645f1
07b2008
393163c
3615420
2c547f6
0cb04e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ WEB3_NODE_URL_10=https://optimism-mainnet.infura.io/v3/ | |
| WEB3_NODE_URL_288=https://boba-mainnet.gateway.pokt.network/v1/lb/ | ||
| WEB3_NODE_URL_42161=https://arbitrum-mainnet.infura.io/v3/ | ||
| WEB3_NODE_URL_137=https://polygon-mainnet.infura.io/v3/ | ||
| WEB3_NODE_URL_5=https://goerli.infura.io/v3/ | ||
| REFERRAL_DELIMITER_START_TIMESTAMP=1657290720 | ||
| ENABLE_SPOKE_POOLS_EVENTS_PROCESSING=false | ||
| ENABLE_REFERRALS_MATERIALIZED_VIEW_REFRESH=false | ||
|
|
@@ -27,3 +28,9 @@ DISCORD_CLIENT_ID=clientId | |
| DISCORD_CLIENT_SECRET=clientSecret | ||
| # the url accessed after the Discord authorization processed is fulfilled | ||
| DISCORD_REDIRECT_URI=http://localhost | ||
|
|
||
| # MerkleDistributor overrides | ||
| MERKLE_DISTRIBUTOR_CHAIN_ID= | ||
| MERKLE_DISTRIBUTOR_ADDRESS= | ||
| REFERRALS_START_WINDOW_INDEX= | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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.
|
||
| ENABLE_MERKLE_DISTRIBUTOR_EVENTS_PROCESSING=false | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { MigrationInterface, QueryRunner } from "typeorm"; | ||
|
|
||
| export class MerkleDistributorRecipient1667312452969 implements MigrationInterface { | ||
| name = "MerkleDistributorRecipient1667312452969"; | ||
|
|
||
| public async up(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(` | ||
| ALTER TABLE "merkle_distributor_recipient" | ||
| DROP CONSTRAINT "UK_merkle_distributor_recipient_merkleDistributorWindowId_addre" | ||
| `); | ||
| await queryRunner.query(` | ||
| ALTER TABLE "merkle_distributor_recipient" | ||
| ADD CONSTRAINT "UK_merkle_distributor_recipient_windowId_address" | ||
| UNIQUE ("merkleDistributorWindowId", "address") | ||
| `); | ||
| } | ||
|
|
||
| public async down(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(` | ||
| ALTER TABLE "merkle_distributor_recipient" | ||
| DROP CONSTRAINT "UK_merkle_distributor_recipient_windowId_address" | ||
| `); | ||
| await queryRunner.query(` | ||
| ALTER TABLE "merkle_distributor_recipient" | ||
| ADD CONSTRAINT "UK_merkle_distributor_recipient_merkleDistributorWindowId_addre" | ||
| UNIQUE ("merkleDistributorWindowId", "address") | ||
| `); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { MigrationInterface, QueryRunner } from "typeorm"; | ||
|
|
||
| export class MerkleDistributorProcessedBlock1667813310964 implements MigrationInterface { | ||
| name = "MerkleDistributorProcessedBlock1667813310964"; | ||
|
|
||
| public async up(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(` | ||
| CREATE TABLE "merkle_distributor_processed_block" ( | ||
| "id" SERIAL NOT NULL, | ||
| "chainId" integer NOT NULL, | ||
| "latestBlock" integer NOT NULL, | ||
| "createdAt" TIMESTAMP NOT NULL DEFAULT now(), | ||
| CONSTRAINT "PK_fb2eb512abaadb453e1cfef109e" PRIMARY KEY ("id")) | ||
| `); | ||
| } | ||
|
|
||
| public async down(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(`DROP TABLE "merkle_distributor_processed_block"`); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { MigrationInterface, QueryRunner } from "typeorm"; | ||
|
|
||
| export class Claim1667813310965 implements MigrationInterface { | ||
| name = "Claim1667813310965"; | ||
|
|
||
| public async up(queryRunner: QueryRunner): Promise<void> { | ||
| 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(), | ||
| "merkleDistributorWindowId" integer, | ||
| CONSTRAINT "UK_claim_windowIndex_accountIndex" UNIQUE ( | ||
| "windowIndex", | ||
| "accountIndex" | ||
| ), | ||
| CONSTRAINT "PK_466b305cc2e591047fa1ce58f81" PRIMARY KEY ("id")) | ||
| `); | ||
| await queryRunner.query(`CREATE INDEX "IX_claim_account" ON "claim" ("account")`); | ||
| await queryRunner.query(` | ||
| ALTER TABLE "claim" | ||
| ADD CONSTRAINT "FK_169ca2a2e031f01f62d81dbf1a0" | ||
| FOREIGN KEY ("merkleDistributorWindowId") | ||
| REFERENCES "merkle_distributor_window"("id") | ||
| ON DELETE NO ACTION ON UPDATE NO ACTION | ||
| `); | ||
| } | ||
|
|
||
| public async down(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(` | ||
| ALTER TABLE "claim" DROP CONSTRAINT "FK_169ca2a2e031f01f62d81dbf1a0" | ||
| `); | ||
| await queryRunner.query(`DROP TABLE "merkle_distributor_processed_block"`); | ||
| await queryRunner.query(`DROP INDEX "public"."IX_claim_account"`); | ||
| await queryRunner.query(`DROP TABLE "claim"`); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,7 +39,7 @@ export const configValues = () => ({ | |
| // 69: process.env.WEB3_NODE_URL_69, | ||
| // 4: process.env.WEB3_NODE_URL_4, | ||
| // 80001: process.env.WEB3_NODE_URL_80001, | ||
| // 5: process.env.WEB3_NODE_URL_5, | ||
| 5: process.env.WEB3_NODE_URL_5, | ||
| }, | ||
| spokePoolContracts: { | ||
| [ChainIds.mainnet]: { | ||
|
|
@@ -63,9 +63,16 @@ export const configValues = () => ({ | |
| startBlockNumber: 28604263, | ||
| }, | ||
| }, | ||
| merkleDistributor: { | ||
| address: process.env.MERKLE_DISTRIBUTOR_ADDRESS || "0xF633b72A4C2Fb73b77A379bf72864A825aD35b6D", // TODO: replace with mainnet | ||
| chainId: Number(process.env.MERKLE_DISTRIBUTOR_CHAIN_ID || "5"), | ||
| referralsStartWindowIndex: Number(process.env.REFERRALS_START_WINDOW_INDEX || "1"), | ||
| startBlockNumber: Number(process.env.MERKLE_DISTRIBUTOR_START_BLOCK || 7884371), | ||
| }, | ||
|
Comment on lines
+66
to
+71
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I mentioned in the .env.sample file, I think here we should have a data structure that allows declaring multiple MerkleDistributor contracts. I guess an array of objects fits the best
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I could do that. But do you have a use-case in mind for that? |
||
| }, | ||
| acxUsdPrice: 0.1, | ||
| enableSpokePoolsEventsProcessing: process.env.ENABLE_SPOKE_POOLS_EVENTS_PROCESSING === "true", | ||
| enableMerkleDistributorEventsProcessing: process.env.ENABLE_MERKLE_DISTRIBUTOR_EVENTS_PROCESSING === "true", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| enableReferralsMaterializedViewRefresh: process.env.ENABLE_REFERRALS_MATERIALIZED_VIEW_REFRESH === "true", | ||
| allowWalletRewardsEdit: process.env.ALLOW_WALLET_REWARDS_EDIT === "true", | ||
| stickyReferralAddressesMechanism: process.env.STICKY_REFERRAL_ADDRESSES_MECHANISM | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { OnQueueFailed, Process, Processor } from "@nestjs/bull"; | ||
| import { Logger } from "@nestjs/common"; | ||
| import { Job } from "bull"; | ||
| import { InjectRepository } from "@nestjs/typeorm"; | ||
| import { Repository, QueryFailedError } from "typeorm"; | ||
|
|
||
| import { EthProvidersService } from "../../../web3/services/EthProvidersService"; | ||
| import { MerkleDistributorBlocksEventsQueueMessage, ScraperQueue } from "."; | ||
| import { ClaimedEvent } from "@across-protocol/contracts-v2/dist/typechain/MerkleDistributor"; | ||
| import { Claim } from "../../model/claim.entity"; | ||
| import { utils } from "ethers"; | ||
|
|
||
| @Processor(ScraperQueue.MerkleDistributorBlocksEvents) | ||
| export class MerkleDistributorBlocksEventsConsumer { | ||
| private logger = new Logger(MerkleDistributorBlocksEventsConsumer.name); | ||
|
|
||
| constructor( | ||
| private providers: EthProvidersService, | ||
| @InjectRepository(Claim) private claimRepository: Repository<Claim>, | ||
| ) {} | ||
|
|
||
| @Process({ concurrency: 1 }) | ||
| private async process(job: Job<MerkleDistributorBlocksEventsQueueMessage>) { | ||
| const { chainId, from, to } = job.data; | ||
| const claimedEvents: ClaimedEvent[] = await this.providers.getMerkleDistributorQuerier().getClaimedEvents(from, to); | ||
| this.logger.log(`(${from}, ${to}) - chainId ${chainId} - found ${claimedEvents.length} ClaimedEvent`); | ||
|
|
||
| for (const event of claimedEvents) { | ||
| try { | ||
| const claim = await this.fromClaimedEventToClaim(event, chainId); | ||
| await this.claimRepository.insert(claim); | ||
| } catch (error) { | ||
| if (error instanceof QueryFailedError && error.driverError?.code === "23505") { | ||
| // Ignore duplicate key value violates unique constraint error. | ||
| this.logger.warn(error); | ||
| } else { | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private async fromClaimedEventToClaim(event: ClaimedEvent, chainId: number) { | ||
| const { blockNumber } = event; | ||
| const { caller, accountIndex, windowIndex, account, rewardToken } = event.args; | ||
| const blockTimestamp = (await this.providers.getCachedBlock(chainId, blockNumber)).date; | ||
|
|
||
| return this.claimRepository.create({ | ||
| caller, | ||
| accountIndex: accountIndex.toNumber(), | ||
| windowIndex: windowIndex.toNumber(), | ||
| account: utils.getAddress(account), | ||
| rewardToken: utils.getAddress(rewardToken), | ||
| blockNumber: blockNumber, | ||
| claimedAt: blockTimestamp, | ||
| }); | ||
| } | ||
|
|
||
| @OnQueueFailed() | ||
| private onQueueFailed(job: Job, error: Error) { | ||
| this.logger.error(`${ScraperQueue.MerkleDistributorBlocksEvents} ${JSON.stringify(job.data)} failed: ${error}`); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { JwtAuthGuard } from "../../../auth/entry-points/http/jwt.guard"; | |
| import { Role, Roles, RolesGuard } from "../../../auth/entry-points/http/roles"; | ||
| import { | ||
| BlocksEventsQueueMessage, | ||
| MerkleDistributorBlocksEventsQueueMessage, | ||
| DepositFilledDateQueueMessage, | ||
| DepositReferralQueueMessage, | ||
| ScraperQueue, | ||
|
|
@@ -19,6 +20,9 @@ export class ScraperController { | |
|
|
||
| @Post("scraper/blocks") | ||
| @ApiTags("scraper") | ||
| @Roles(Role.Admin) | ||
| @UseGuards(JwtAuthGuard, RolesGuard) | ||
| @ApiBearerAuth() | ||
| async processBlocks(@Req() req: Request, @Body() body: ProcessBlocksBody) { | ||
| const { chainId, from, to } = body; | ||
| await this.scraperQueuesService.publishMessage<BlocksEventsQueueMessage>(ScraperQueue.BlocksEvents, { | ||
|
|
@@ -28,6 +32,23 @@ export class ScraperController { | |
| }); | ||
| } | ||
|
|
||
| @Post("scraper/blocks/merkle-distributor") | ||
| @ApiTags("scraper") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add @Roles(Role.Admin)
@UseGuards(JwtAuthGuard, RolesGuard)
@ApiBearerAuth()to make this endpoint callable only with an admin JWT
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should I add these guards also to the normal
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| @Roles(Role.Admin) | ||
| @UseGuards(JwtAuthGuard, RolesGuard) | ||
| @ApiBearerAuth() | ||
| async processMerkleDistributorBlocks(@Req() req: Request, @Body() body: ProcessBlocksBody) { | ||
| const { chainId, from, to } = body; | ||
| await this.scraperQueuesService.publishMessage<MerkleDistributorBlocksEventsQueueMessage>( | ||
| ScraperQueue.MerkleDistributorBlocksEvents, | ||
| { | ||
| chainId, | ||
| from, | ||
| to, | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| @Post("scraper/prices") | ||
| @ApiTags("scraper") | ||
| async submitPricesJobs(@Body() body: ProcessPricesBody) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm"; | ||
|
|
||
| @Entity() | ||
| export class MerkleDistributorProcessedBlock { | ||
| @PrimaryGeneratedColumn() | ||
| id: number; | ||
|
|
||
| @Column() | ||
| chainId: number; | ||
|
|
||
| @Column() | ||
| latestBlock: number; | ||
|
|
||
| @CreateDateColumn() | ||
| createdAt: Date; | ||
james-a-morris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { | ||
| Column, | ||
| CreateDateColumn, | ||
| Entity, | ||
| Index, | ||
| PrimaryGeneratedColumn, | ||
| Unique, | ||
| UpdateDateColumn, | ||
| ManyToOne, | ||
| } from "typeorm"; | ||
| import { MerkleDistributorWindow } from "../../airdrop/model/merkle-distributor-window.entity"; | ||
|
|
||
| @Entity() | ||
| @Unique("UK_claim_windowIndex_accountIndex", ["windowIndex", "accountIndex"]) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| @Index("IX_claim_account", ["account"]) | ||
| export class Claim { | ||
| @PrimaryGeneratedColumn() | ||
| id: number; | ||
|
|
||
| @Column() | ||
| caller: string; | ||
|
|
||
| @Column() | ||
| accountIndex: number; | ||
|
|
||
| @Column() | ||
| windowIndex: number; | ||
|
|
||
| @Column() | ||
| account: string; | ||
|
|
||
| @Column() | ||
| rewardToken: string; | ||
|
|
||
| @Column() | ||
| blockNumber: number; | ||
james-a-morris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @Column() | ||
| claimedAt: Date; | ||
|
|
||
| @ManyToOne(() => MerkleDistributorWindow, (window) => window.claims) | ||
| merkleDistributorWindow: MerkleDistributorWindow; | ||
|
|
||
| @CreateDateColumn() | ||
| createdAt: Date; | ||
|
|
||
| @UpdateDateColumn() | ||
| updatedAt: Date; | ||
| } | ||
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
MerkleDistributorcontracts 🤔 My thought process was:Aare distributed via a different MD than rewards of typeB.