Skip to content

Commit

Permalink
fix: airdrop in batches
Browse files Browse the repository at this point in the history
  • Loading branch information
ncomerci committed Aug 30, 2023
1 parent 9ec500f commit 7b82c76
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 33 deletions.
22 changes: 15 additions & 7 deletions src/back/jobs/BadgeAirdrop.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Env } from '@dcl/ui-env'
import logger from 'decentraland-gatsby/dist/entities/Development/logger'

import { config } from '../../config'
import { BadgesService } from '../../services/BadgesService'
import AirdropJobModel, { AirdropJobAttributes } from '../models/AirdropJob'

Expand All @@ -17,16 +19,22 @@ async function runQueuedAirdropJobs() {
const { id, badge_spec, recipients } = pendingJob
const airdropOutcome = await BadgesService.giveBadgeToUsers(badge_spec, recipients)
logger.log('Airdrop Outcome', airdropOutcome)
await AirdropJobModel.update<AirdropJobAttributes>(
{
...airdropOutcome,
updated_at: new Date(),
},
{ id }
await Promise.all(
airdropOutcome.map((outcome) =>
AirdropJobModel.update<AirdropJobAttributes>(
{
...outcome,
updated_at: new Date(),
},
{ id }
)
)
)
})
}

async function giveAndRevokeLandOwnerBadges() {
await BadgesService.giveAndRevokeLandOwnerBadges()
if (config.getEnv() === Env.PRODUCTION) {
await BadgesService.giveAndRevokeLandOwnerBadges()
}
}
2 changes: 1 addition & 1 deletion src/back/routes/badges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async function getBadges(req: Request<{ address: string }>): Promise<UserBadges>
return await BadgesService.getBadges(address)
}

async function airdrop(req: WithAuth): Promise<AirdropOutcome> {
async function airdrop(req: WithAuth): Promise<AirdropOutcome[]> {
const user = req.auth!
const recipients: string[] = req.body.recipients
const badgeSpecCid = req.body.badgeSpecCid
Expand Down
2 changes: 1 addition & 1 deletion src/clients/Governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@ export class Governance extends API {
}

async airdropBadge(badgeSpecCid: string, recipients: string[]) {
const response = await this.fetch<ApiResponse<AirdropOutcome>>(
const response = await this.fetch<ApiResponse<AirdropOutcome[]>>(
`/badges/airdrop/`,
this.options().method('POST').authorization({ sign: true }).json({
badgeSpecCid,
Expand Down
6 changes: 6 additions & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,9 @@ export function getVestingContractUrl(address: string) {
const VESTING_DASHBOARD_URL = env('GATSBY_VESTING_DASHBOARD_URL')
return VESTING_DASHBOARD_URL.replace('%23', '#').concat(address.toLowerCase())
}

export function splitArray<Type>(array: Type[], chunkSize: number): Type[][] {
return Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, index) =>
array.slice(index * chunkSize, (index + 1) * chunkSize)
)
}
59 changes: 35 additions & 24 deletions src/services/BadgesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import AirdropJobModel, { AirdropJobStatus, AirdropOutcome } from '../back/model
import { OtterspaceBadge, OtterspaceSubgraph } from '../clients/OtterspaceSubgraph'
import { SnapshotGraphql } from '../clients/SnapshotGraphql'
import { LAND_OWNER_BADGE_SPEC_CID, LEGISLATOR_BADGE_SPEC_CID } from '../constants'
import { storeBadgeSpec } from '../entities/Badges/storeBadgeSpec'
import {
ActionResult,
ActionStatus,
Expand All @@ -21,7 +20,7 @@ import CoauthorModel from '../entities/Coauthor/model'
import { CoauthorStatus } from '../entities/Coauthor/types'
import { ProposalAttributes, ProposalType } from '../entities/Proposal/types'
import { getChecksumAddress, isSameAddress } from '../entities/Snapshot/utils'
import { inBackground } from '../helpers'
import { inBackground, splitArray } from '../helpers'
import { ErrorCategory } from '../utils/errorCategories'

import { ErrorService } from './ErrorService'
Expand Down Expand Up @@ -83,7 +82,7 @@ export class BadgesService {
return ipfsLink.replace('ipfs://', 'https://ipfs.io/ipfs/')
}

public static async giveBadgeToUsers(badgeCid: string, users: string[]): Promise<AirdropOutcome> {
public static async giveBadgeToUsers(badgeCid: string, users: string[]): Promise<AirdropOutcome[]> {
try {
const { usersWithoutBadge, usersWithBadgesToReinstate } = await this.getUsersWithoutBadge(badgeCid, users)
if (usersWithBadgesToReinstate.length > 0) {
Expand All @@ -93,15 +92,15 @@ export class BadgesService {
}

if (usersWithoutBadge.length === 0) {
return { status: AirdropJobStatus.FAILED, error: ErrorReason.NoUserWithoutBadge }
return [{ status: AirdropJobStatus.FAILED, error: ErrorReason.NoUserWithoutBadge }]
}
const usersWhoVoted = await this.getUsersWhoVoted(usersWithoutBadge)
if (usersWhoVoted.length === 0) {
return { status: AirdropJobStatus.FAILED, error: ErrorReason.NoUserHasVoted }
return [{ status: AirdropJobStatus.FAILED, error: ErrorReason.NoUserHasVoted }]
}
return await this.airdropWithRetry(badgeCid, usersWhoVoted)
} catch (e) {
return { status: AirdropJobStatus.FAILED, error: JSON.stringify(e) }
return [{ status: AirdropJobStatus.FAILED, error: JSON.stringify(e) }]
}
}

Expand Down Expand Up @@ -137,20 +136,31 @@ export class BadgesService {
recipients: string[],
retries = 3,
pumpGas = false
): Promise<AirdropOutcome> {
try {
await airdrop(badgeCid, recipients, pumpGas)
return { status: AirdropJobStatus.FINISHED, error: '' }
} catch (error: any) {
if (retries > 0) {
logger.log(`Retrying airdrop... Attempts left: ${retries}`, error)
const pumpGas = this.isTransactionUnderpricedError(error)
return await this.airdropWithRetry(badgeCid, recipients, retries - 1, pumpGas)
} else {
logger.error('Airdrop failed after maximum retries', error)
return { status: AirdropJobStatus.FAILED, error: JSON.stringify(error) }
): Promise<AirdropOutcome[]> {
const _airdropWithRetry = async (
badgeCid: string,
recipients: string[],
retries = 3,
pumpGas = false
): Promise<AirdropOutcome> => {
try {
await airdrop(badgeCid, recipients, pumpGas)
return { status: AirdropJobStatus.FINISHED, error: '' }
} catch (error: any) {
if (retries > 0) {
logger.log(`Retrying airdrop... Attempts left: ${retries}`, error)
const pumpGas = this.isTransactionUnderpricedError(error)
return await _airdropWithRetry(badgeCid, recipients, retries - 1, pumpGas)
} else {
logger.error('Airdrop failed after maximum retries', error)
return { status: AirdropJobStatus.FAILED, error: JSON.stringify(error) }
}
}
}

return await Promise.all(
splitArray(recipients, 50).map((recipients) => _airdropWithRetry(badgeCid, recipients, retries, pumpGas))
)
}

private static isTransactionUnderpricedError(error: any) {
Expand All @@ -173,17 +183,18 @@ export class BadgesService {

static async giveAndRevokeLandOwnerBadges() {
const landOwnerAddresses = await getLandOwnerAddresses()
const { status, error } = await BadgesService.giveBadgeToUsers(LAND_OWNER_BADGE_SPEC_CID, landOwnerAddresses)
const outcomes = await BadgesService.giveBadgeToUsers(LAND_OWNER_BADGE_SPEC_CID, landOwnerAddresses)
const failedOutcomes = outcomes.filter((outcome) => outcome.status === AirdropJobStatus.FAILED)
if (
status === AirdropJobStatus.FAILED &&
error !== ErrorReason.NoUserWithoutBadge &&
error !== ErrorReason.NoUserHasVoted
failedOutcomes.length > 0 &&
failedOutcomes[0].error !== ErrorReason.NoUserWithoutBadge &&
failedOutcomes[0].error !== ErrorReason.NoUserHasVoted
) {
console.error('Unable to give LandOwner badges', error)
console.error('Unable to give LandOwner badges', failedOutcomes)

ErrorService.report('Unable to give LandOwner badges', {
category: ErrorCategory.Badges,
error,
failedOutcomes,
})
}

Expand Down

0 comments on commit 7b82c76

Please sign in to comment.