From 1b08abeb44997b49b395ab45299be3276d148f37 Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Wed, 9 Oct 2024 23:32:30 -0700 Subject: [PATCH] DistributorV2 --- apps/distributor/src/app.ts | 27 +- apps/distributor/src/distributorv2.test.ts | 318 +++++++++++++++++++++ apps/distributor/src/distributorv2.ts | 248 ++++++++-------- apps/distributor/src/weights.ts | 13 + 4 files changed, 489 insertions(+), 117 deletions(-) create mode 100644 apps/distributor/src/distributorv2.test.ts diff --git a/apps/distributor/src/app.ts b/apps/distributor/src/app.ts index ef9dc4377..d02e90e67 100644 --- a/apps/distributor/src/app.ts +++ b/apps/distributor/src/app.ts @@ -4,6 +4,7 @@ import { DistributorV1Worker } from './distributor' import { StandardMerkleTree } from '@openzeppelin/merkle-tree' import { selectAll } from 'app/utils/supabase/selectAll' import { supabaseAdmin } from './supabase' +import { DistributorV2Worker } from './distributorv2' const logger = pino({ level: process.env.LOG_LEVEL || 'info', @@ -11,7 +12,8 @@ const logger = pino({ }) // Initialize DistributorWorker -const distributorV1Worker = new DistributorV1Worker(logger) +const distributorV1Worker = new DistributorV1Worker(logger, false) +const distributorV2Worker = new DistributorV2Worker(logger) // Initialize Express app const app = express() @@ -32,6 +34,13 @@ distributorRouter.get('/v1', async (req: Request, res: Response) => { }) }) +distributorRouter.get('/v2', async (req: Request, res: Response) => { + res.json({ + distributor: true, + ...distributorV2Worker.toJSON(), + }) +}) + // Middleware for checking authorization const checkAuthorization = (req: Request, res: Response, next: () => void) => { if (!req.headers.authorization?.includes(process.env.SUPABASE_SERVICE_ROLE as string)) { @@ -117,6 +126,22 @@ distributorRouter.post('/v1', checkAuthorization, async (req, res) => { }) }) +distributorRouter.post('/v2', checkAuthorization, async (req, res) => { + const { id } = req.body as { id: string } + logger.info({ id }, 'Received request to calculate distribution') + try { + await distributorV2Worker.calculateDistribution(id) + } catch (err) { + logger.error(err, 'Error while calculating distribution') + throw err + } + + res.json({ + distributor: true, + id: id, + }) +}) + app.use('/distributor', distributorRouter) export default app diff --git a/apps/distributor/src/distributorv2.test.ts b/apps/distributor/src/distributorv2.test.ts new file mode 100644 index 000000000..de0cf8eba --- /dev/null +++ b/apps/distributor/src/distributorv2.test.ts @@ -0,0 +1,318 @@ +// @ts-expect-error set __DEV__ for code shared between server and client +globalThis.__DEV__ = true + +import { describe, expect, it, mock } from 'bun:test' +import request from 'supertest' +import app from './app' +import { supabaseAdmin } from './supabase' +import pino from 'pino' +import { DistributorV2Worker } from './distributorv2' +import type { Tables } from '@my/supabase/database.types' + +describe('Root Route', () => { + it('should return correct response for the root route', async () => { + const res = await request(app).get('/') + + expect(res.statusCode).toBe(200) + expect(res.body).toEqual({ root: true }) + }) +}) + +describe('Distributor Route', () => { + it('should reject unauthorized requests', async () => { + const res = await request(app).post('/distributor/v2') + + expect(res.statusCode).toBe(401) + expect(res.body).toEqual('Unauthorized') + }) + + it('should handle authorization correctly', async () => { + const res = await request(app).get('/distributor/v2') + + expect(res.statusCode).toBe(200) + expect(res.body).toMatchObject({ + distributor: true, + running: true, + }) + }) + + it.skip('should perform distributor logic correctly', async () => { + const { data: distribution, error } = await supabaseAdmin + .from('distributions') + .select( + `*, + distribution_verification_values (*)` + ) + .order('number', { ascending: false }) + .limit(1) + .single() + + if (error) { + throw error + } + + if (!distribution) { + throw new Error('No distributions found') + } + + expect(distribution).toBeDefined() + + const res = await request(app) + .post('/distributor/v2') + .set('Content-Type', 'application/json') + .set('Authorization', `Bearer ${process.env.SUPABASE_SERVICE_ROLE}`) + .send({ id: distribution.number }) + + expect(res.statusCode).toBe(200) + expect(res.body).toMatchObject({ + distributor: true, + id: distribution.id, + }) + }) + + it.skip('should return a merkle root', async () => { + const res = await request(app) + .post('/distributor/merkle') + .set('Authorization', `Bearer ${process.env.SUPABASE_SERVICE_ROLE}`) + .send({ id: '4' }) + + expect(res.statusCode).toBe(200) + expect({ + root: res.body.root, + total: res.body.total, + }).toMatchSnapshot('distribution 4 merkle root') + }) +}) + +describe('Distributor V2 Worker', () => { + it('should calculate distribution shares', async () => { + const distribution = { + id: 4, + number: 4, + amount: 10000, + hodler_pool_bips: 10000, + bonus_pool_bips: 0, + fixed_pool_bips: 10000, + name: 'Distribution #4', + description: 'Fourth distributions of 900,000,000 SEND tokens to early hodlers', + qualification_start: '2024-04-08T00:00:00+00:00', + qualification_end: '2024-04-21T00:00:00+00:00', + claim_end: '2024-05-31T23:59:59+00:00', + hodler_min_balance: 100000, + created_at: '2024-04-06T16:49:02.569245+00:00', + updated_at: '2024-04-06T16:49:02.569245+00:00', + snapshot_block_num: 13261327, + chain_id: 845337, + distribution_verification_values: [ + { + type: 'tag_referral', + fixed_value: 50, + bips_value: 0, + multiplier_min: 1.5, + multiplier_max: 2.5, + multiplier_step: 0.1, + distribution_id: 4, + }, + { + type: 'total_tag_referrals', + fixed_value: 0, + bips_value: 0, + multiplier_min: 1.0, + multiplier_max: 2.0, + multiplier_step: 0.01, + distribution_id: 4, + }, + { + type: 'create_passkey', + fixed_value: 200, + bips_value: 0, + distribution_id: 4, + }, + { + type: 'tag_registration', + fixed_value: 100, + bips_value: 0, + distribution_id: 4, + created_at: '2024-04-06T16:49:02.569245+00:00', + updated_at: '2024-04-06T16:49:02.569245+00:00', + }, + { + type: 'send_ten', + fixed_value: 100, + bips_value: 0, + distribution_id: 4, + created_at: '2024-04-06T16:49:02.569245+00:00', + updated_at: '2024-04-06T16:49:02.569245+00:00', + }, + { + type: 'send_one_hundred', + fixed_value: 200, + bips_value: 0, + distribution_id: 4, + created_at: '2024-04-06T16:49:02.569245+00:00', + updated_at: '2024-04-06T16:49:02.569245+00:00', + }, + ], + } as Tables<'distributions'> & { + distribution_verification_values: Tables<'distribution_verification_values'>[] + } + const user_id = crypto.randomUUID() + const user_id2 = crypto.randomUUID() + const bobAddr = '0xb0b0000000000000000000000000000000000000' + const aliceAddr = '0xalice000000000000000000000000000000000000' + + const createDistributionShares = mock( + (distributionId: number, shares: Tables<'distribution_shares'>[]) => { + return Promise.resolve({ + data: null, + error: null, + }) + } + ) + + mock.module('./supabase', () => ({ + fetchDistribution: mock((id: string) => { + return Promise.resolve({ + data: distribution, + error: null, + }) + }), + /* + Back of the napkin + Pool = 10,000 + Fixed + Bobs = 200 + 200 + 100 + 100 + 50 = 650 * 1.5 * 1.01 = 985 + Alices = 100 + 100 * 1.05 = 205 + Hodlers = 10,000 - 985 - 205 = 8810 + Bobs = 8810 * 1,000,000 /1,500,000 = 5873 + Alices = 8810 * 500,000 /1,500,000 = 2937 + */ + fetchAllVerifications: mock((distributionId: number) => { + return Promise.resolve({ + data: [ + { user_id, type: 'create_passkey' }, + { + user_id, + type: 'tag_referral', + }, + + { + user_id, + type: 'tag_registration', + }, + { + user_id, + type: 'send_ten', + }, + { + user_id, + type: 'send_one_hundred', + }, + { + user_id, + type: 'total_tag_referrals', + metadata: { + value: 2, + }, + }, + // alice only has tag_registration + { + user_id: user_id2, + type: 'tag_registration', + }, + { + user_id: user_id2, + type: 'send_ten', + }, + { + user_id: user_id2, + type: 'total_tag_referrals', + metadata: { + value: 5, + }, + }, + ], + count: 9, + error: null, + }) + }), + fetchAllHodlers: mock((distributionId: number) => { + return Promise.resolve({ + data: [ + { + address: bobAddr, + created_at: '2024-04-06T16:49:02.569245+00:00', + user_id, + }, + { + address: aliceAddr, + created_at: '2024-04-06T16:49:02.569245+00:00', + user_id: user_id2, + }, + ], + error: null, + }) + }), + createDistributionShares, + })) + + mock.module('./wagmi', () => ({ + fetchAllBalances: mock(({ addresses, distribution }) => { + return [ + Promise.resolve({ + user_id, + address: bobAddr, + balance: '1000000', + }), + // alice has half of the balance of bob + Promise.resolve({ + user_id: user_id2, + address: aliceAddr, + balance: '500000', + }), + ] + }), + isMerkleDropActive: mock((distribution) => { + return Promise.resolve(false) + }), + })) + + const logger = pino({ + level: 'silent', + }) + const distributor = new DistributorV2Worker(logger, false) + await distributor.calculateDistribution('4') + + //Expected values are a little different than back of the napkin because of rounding + //Keep an eye on this, may need to investigate if we see distro problems + const expectedShares = [ + { + address: bobAddr, + distribution_id: 4, + user_id, + amount: '6856', + bonus_pool_amount: '0', // Always 0 in V2 + fixed_pool_amount: '984', + hodler_pool_amount: '5872', + }, + { + address: aliceAddr, + distribution_id: 4, + user_id: user_id2, + amount: '3144', + bonus_pool_amount: '0', // Always 0 in V2 + fixed_pool_amount: '208', + hodler_pool_amount: '2936', + }, + ] + expect(createDistributionShares).toHaveBeenCalled() + + // @ts-expect-error supabase-js does not support bigint + expect(createDistributionShares.mock.calls[0]).toEqual([distribution.id, expectedShares]) + + // expected share amounts cannot exceed the total distribution amount + const totalDistributionAmount = BigInt(distribution.amount) + const totalShareAmounts = expectedShares.reduce((acc, share) => acc + BigInt(share.amount), 0n) + expect(totalShareAmounts).toBeLessThanOrEqual(totalDistributionAmount) + }) +}) diff --git a/apps/distributor/src/distributorv2.ts b/apps/distributor/src/distributorv2.ts index 480136250..cbbf7196f 100644 --- a/apps/distributor/src/distributorv2.ts +++ b/apps/distributor/src/distributorv2.ts @@ -9,7 +9,14 @@ import { supabaseAdmin, } from './supabase' import { fetchAllBalances, isMerkleDropActive } from './wagmi' -import { calculateWeights, calculatePercentageWithBips, PERC_DENOM } from './weights' +import { calculateWeights, calculateMultiplier, PERC_DENOM } from './weights' + +type Multiplier = { + value: number + min: number + max: number + step: number +} const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) @@ -28,6 +35,14 @@ const jsonBigint = (key, value) => { return value } +/** + * Changes from V1: + * Fixed Pool Calculation: In V2, fixed pool amounts are calculated first from the total distribution amount, whereas V1 calculated hodler, bonus, and fixed pools separately. + * Removal of Bips: V2 no longer uses holder and bonus bips (basis points) for calculations, simplifying the distribution logic. + * Bonus Shares Elimination: In V2, bonus shares are always 0, effectively removing the bonus pool concept that existed in V1. + * Multiplier System: V2 introduces a new multiplier system, particularly for referrals and certain verification types + */ + export class DistributorV2Worker { private log: Logger private running: boolean @@ -103,9 +118,6 @@ export class DistributorV2Worker { } } - /** - * Calculates distribution shares for a single distribution. - */ private async _calculateDistributionShares( distribution: Tables<'distributions'> & { distribution_verification_values: Tables<'distribution_verification_values'>[] @@ -113,7 +125,6 @@ export class DistributorV2Worker { ): Promise { const log = this.log.child({ distribution_id: distribution.id }) - // verify tranche is not created when in production if (await isMerkleDropActive(distribution)) { throw new Error('Tranche is active. Cannot calculate distribution shares.') } @@ -140,7 +151,6 @@ export class DistributorV2Worker { } log.info(`Found ${verifications.length} verifications.`) - // log.debug({ verifications }) if (log.isLevelEnabled('debug')) { await Bun.write( 'dist/verifications.json', @@ -155,12 +165,21 @@ export class DistributorV2Worker { acc[verification.type] = { fixedValue: BigInt(verification.fixed_value), bipsValue: BigInt(verification.bips_value), + multiplier_min: verification.multiplier_min, + multiplier_max: verification.multiplier_max, + multiplier_step: verification.multiplier_step, } return acc }, {} as Record< Database['public']['Enums']['verification_type'], - { fixedValue?: bigint; bipsValue?: bigint } + { + fixedValue?: bigint + bipsValue?: bigint + multiplier_min: number + multiplier_max: number + multiplier_step: number + } > ) const verificationsByUserId = verifications.reduce( @@ -173,7 +192,6 @@ export class DistributorV2Worker { ) log.info(`Found ${Object.keys(verificationsByUserId).length} users with verifications.`) - // log.debug({ verificationsByUserId }) if (log.isLevelEnabled('debug')) { await Bun.write( 'dist/verificationsByUserId.json', @@ -211,7 +229,6 @@ export class DistributorV2Worker { ) log.info(`Found ${hodlerAddresses.length} addresses.`) - // log.debug({ hodlerAddresses }) if (log.isLevelEnabled('debug')) { await Bun.write( 'dist/hodlerAddresses.json', @@ -231,13 +248,13 @@ export class DistributorV2Worker { ) }) + // Filter out hodler with not enough send token balance let minBalanceAddresses: { user_id: string; address: `0x${string}`; balance: string }[] = [] for await (const batch of batches) { minBalanceAddresses = minBalanceAddresses.concat(...batch) } log.info(`Found ${minBalanceAddresses.length} balances.`) - // log.debug({ balances }) // Filter out hodler with not enough send token balance minBalanceAddresses = minBalanceAddresses.filter( @@ -247,7 +264,6 @@ export class DistributorV2Worker { log.info( `Found ${minBalanceAddresses.length} balances after filtering hodler_min_balance of ${distribution.hodler_min_balance}` ) - // log.debug({ balances }) if (log.isLevelEnabled('debug')) { await Bun.write( @@ -258,12 +274,10 @@ export class DistributorV2Worker { }) } - // Calculate hodler pool share weights + // Calculate fixed pool share weights const distAmt = BigInt(distribution.amount) - const hodlerPoolBips = BigInt(distribution.hodler_pool_bips) - const fixedPoolBips = BigInt(distribution.fixed_pool_bips) - const bonusPoolBips = BigInt(distribution.bonus_pool_bips) - const hodlerPoolAvailableAmount = calculatePercentageWithBips(distAmt, hodlerPoolBips) + const fixedPoolAvailableAmount = distAmt + const minBalanceByAddress: Record = minBalanceAddresses.reduce( (acc, balance) => { acc[balance.address] = BigInt(balance.balance) @@ -271,77 +285,111 @@ export class DistributorV2Worker { }, {} as Record ) - const { totalWeight, weightPerSend, poolWeights, weightedShares } = calculateWeights( - minBalanceAddresses, - hodlerPoolAvailableAmount - ) - - log.info( - { totalWeight, hodlerPoolAvailableAmount, weightPerSend }, - `Calculated ${Object.keys(poolWeights).length} weights.` - ) - // log.debug({ poolWeights }) - if (log.isLevelEnabled('debug')) { - await Bun.write('dist/poolWeights.json', JSON.stringify(poolWeights, jsonBigint, 2)).catch( - (e) => { - log.error(e, 'Error writing poolWeights.json') - } - ) - } - if (totalWeight === 0n) { - log.warn('Total weight is 0. Skipping distribution.') - return - } - - const fixedPoolAvailableAmount = calculatePercentageWithBips(distAmt, fixedPoolBips) let fixedPoolAllocatedAmount = 0n const fixedPoolAmountsByAddress: Record = {} - const bonusPoolBipsByAddress: Record = {} - const maxBonusPoolBips = (bonusPoolBips * PERC_DENOM) / hodlerPoolBips // 3500*10000/6500 = 5384.615384615385% 1.53X for (const [userId, verifications] of Object.entries(verificationsByUserId)) { const hodler = hodlerAddressesByUserId[userId] - if (!hodler || !hodler.address) { - continue - } + if (!hodler || !hodler.address) continue const { address } = hodler - if (!minBalanceByAddress[address]) { - continue - } + if (!minBalanceByAddress[address]) continue + + let userFixedAmount = 0n + let totalReferrals = 0 + const multipliers: Record = {} + for (const verification of verifications) { - const { fixedValue, bipsValue } = verificationValues[verification.type] - if (fixedValue && fixedPoolAllocatedAmount + fixedValue <= fixedPoolAvailableAmount) { - if (fixedPoolAmountsByAddress[address] === undefined) { - fixedPoolAmountsByAddress[address] = 0n + const verificationValue = verificationValues[verification.type] + if (!verificationValue) continue + + // Calculate fixed amount + if (verificationValue.fixedValue) { + userFixedAmount += verificationValue.fixedValue + } + + // Initialize or update multiplier info + if (!multipliers[verification.type] && verificationValue.multiplier_min) { + multipliers[verification.type] = { + value: 1.0, + min: verificationValue.multiplier_min, + max: verificationValue.multiplier_max, + step: verificationValue.multiplier_step, } - fixedPoolAmountsByAddress[address] += fixedValue - fixedPoolAllocatedAmount += fixedValue } - if (bipsValue) { - bonusPoolBipsByAddress[address] = (bonusPoolBipsByAddress[address] || 0n) as bigint - bonusPoolBipsByAddress[address] += bipsValue - bonusPoolBipsByAddress[address] = - (bonusPoolBipsByAddress[address] as bigint) > maxBonusPoolBips - ? maxBonusPoolBips - : (bonusPoolBipsByAddress[address] as bigint) // cap at max bonus pool bips + const multiplierInfo = multipliers[verification.type] + if (!multiplierInfo) continue + + // Calculate multipliers + switch (verification.type) { + case 'total_tag_referrals': { + // @ts-expect-error this is json + totalReferrals = verification.metadata?.value ?? 0 + // Minus 1 from the count so 1 = multiplier min + if (totalReferrals > 0n) { + multiplierInfo.value = Math.min( + multiplierInfo.min + (totalReferrals - 1) * multiplierInfo.step, + multiplierInfo.max + ) + } else { + multiplierInfo.value = 0 + } + + break + } + case 'tag_referral': { + multiplierInfo.value = Math.max(multiplierInfo.value, multiplierInfo.min) + // Count tag_referral verifications + const tagReferralCount = verifications.filter((v) => v.type === 'tag_referral').length + // Increase multiplier for each additional tag_referral. Minus 1 from the count so 1 = multiplier min + for (let i = 1; i < tagReferralCount; i++) { + multiplierInfo.value = Math.min( + multiplierInfo.min + (tagReferralCount - 1) * multiplierInfo.step, + multiplierInfo.max + ) + } + break + } } } + + // Calculate the final multiplier + const finalMultiplier = Object.values(multipliers).reduce( + (acc, info) => acc * info.value, + 1.0 + ) + + // Apply the multiplier to the fixed amount + userFixedAmount = + (userFixedAmount * BigInt(Math.round(finalMultiplier * Number(PERC_DENOM)))) / PERC_DENOM + + if ( + userFixedAmount > 0n && + fixedPoolAllocatedAmount + userFixedAmount <= fixedPoolAvailableAmount + ) { + fixedPoolAmountsByAddress[address] = + (fixedPoolAmountsByAddress[address] || 0n) + userFixedAmount + fixedPoolAllocatedAmount += userFixedAmount + + // Log or save the multipliers for each verification type + log.debug({ userId, address, multipliers, finalMultiplier }, 'User multipliers') + } } - const hodlerShares = Object.values(weightedShares) - let totalAmount = 0n - let totalHodlerPoolAmount = 0n - let totalBonusPoolAmount = 0n - let totalFixedPoolAmount = 0n + // Calculate hodler pool share weights + const hodlerPoolAvailableAmount = distAmt - fixedPoolAllocatedAmount + + let hodlerShares: { address: string; amount: bigint }[] = [] + if (hodlerPoolAvailableAmount > 0n) { + const { weightedShares } = calculateWeights(minBalanceAddresses, hodlerPoolAvailableAmount) + hodlerShares = Object.values(weightedShares) + } + + const totalAmount = 0n + const totalHodlerPoolAmount = 0n + const totalBonusPoolAmount = 0n + const totalFixedPoolAmount = 0n - log.info( - { - maxBonusPoolBips, - }, - 'Calculated fixed & bonus pool amounts.' - ) - // log.debug({ hodlerShares, fixedPoolAmountsByAddress, bonusPoolBipsByAddress }) if (log.isLevelEnabled('debug')) { await Bun.write('dist/hodlerShares.json', JSON.stringify(hodlerShares, jsonBigint, 2)).catch( (e) => { @@ -354,53 +402,24 @@ export class DistributorV2Worker { ).catch((e) => { log.error(e, 'Error writing fixedPoolAmountsByAddress.json') }) - await Bun.write( - 'dist/bonusPoolBipsByAddress.json', - JSON.stringify(bonusPoolBipsByAddress, jsonBigint, 2) - ).catch((e) => { - log.error(e, 'Error writing bonusPoolBipsByAddress.json') - }) } - const shares = hodlerShares - .map((share) => { - const userId = hodlerUserIdByAddress[share.address] - const bonusBips = bonusPoolBipsByAddress[share.address] || 0n - const hodlerPoolAmount = share.amount - const bonusPoolAmount = calculatePercentageWithBips(hodlerPoolAmount, bonusBips) - const fixedPoolAmount = fixedPoolAmountsByAddress[share.address] || 0n - const amount = hodlerPoolAmount + bonusPoolAmount + fixedPoolAmount - totalAmount += amount - totalHodlerPoolAmount += hodlerPoolAmount - totalBonusPoolAmount += bonusPoolAmount - totalFixedPoolAmount += fixedPoolAmount - - if (!userId) { - log.debug({ share }, 'Hodler not found for address. Skipping share.') - return null - } - // log.debug( - // { - // address: share.address, - // balance: balancesByAddress[share.address], - // amount: amount, - // bonusBips, - // hodlerPoolAmount, - // bonusPoolAmount, - // fixedPoolAmount, - // }, - // 'Calculated share.' - // ) + const shares = Object.entries(fixedPoolAmountsByAddress) + .map(([address, fixedAmount]) => { + const hodlerShare = hodlerShares.find((share) => share.address === address) + const hodlerAmount = hodlerShare ? hodlerShare.amount : 0n + const totalAmount = fixedAmount + hodlerAmount + const userId = hodlerUserIdByAddress[address] // @ts-expect-error supabase-js does not support bigint return { - address: share.address, + address, distribution_id: distribution.id, user_id: userId, - amount: amount.toString(), - bonus_pool_amount: bonusPoolAmount.toString(), - fixed_pool_amount: fixedPoolAmount.toString(), - hodler_pool_amount: hodlerPoolAmount.toString(), + amount: totalAmount.toString(), + fixed_pool_amount: fixedAmount.toString(), + hodler_pool_amount: hodlerAmount.toString(), + bonus_pool_amount: '0', } as Tables<'distribution_shares'> }) .filter(Boolean) as Tables<'distribution_shares'>[] @@ -414,14 +433,12 @@ export class DistributorV2Worker { totalFixedPoolAmount, fixedPoolAllocatedAmount, fixedPoolAvailableAmount, - maxBonusPoolBips, name: distribution.name, shares: shares.length, }, 'Distribution totals' ) log.info(`Calculated ${shares.length} shares.`) - // log.debug({ shares }) if (log.isLevelEnabled('debug')) { await Bun.write('dist/shares.json', JSON.stringify(shares, jsonBigint, 2)).catch((e) => { log.error(e, 'Error writing shares.json') @@ -434,7 +451,6 @@ export class DistributorV2Worker { ) } - // ensure share amounts do not exceed the total distribution amount, ideally this should be done in the database const totalShareAmounts = shares.reduce((acc, share) => acc + BigInt(share.amount), 0n) if (totalShareAmounts > distAmt) { throw new Error('Share amounts exceed total distribution amount') @@ -456,7 +472,7 @@ export class DistributorV2Worker { } catch (error) { this.log.error(error, `Error processing block. ${(error as Error).message}`) } - await sleep(60_000) // sleep for 1 minute + await sleep(60_000) } this.log.info('Distributor stopped.') diff --git a/apps/distributor/src/weights.ts b/apps/distributor/src/weights.ts index 0b3b18ef1..685ac9b08 100644 --- a/apps/distributor/src/weights.ts +++ b/apps/distributor/src/weights.ts @@ -33,6 +33,19 @@ export function calculatePercentageWithBips(value: bigint, bips: bigint) { return percentage / PERC_DENOM } +export function calculateMultiplier( + totalReferrals: bigint, + verificationValue: { multiplier_min: number; multiplier_max: number; multiplier_step: number } +): bigint { + const { multiplier_min, multiplier_max, multiplier_step } = verificationValue + const steps = Math.min( + Number(totalReferrals), + Math.floor((multiplier_max - multiplier_min) / multiplier_step) + ) + const multiplier = multiplier_min + steps * multiplier_step + return BigInt(Math.floor(multiplier * Number(PERC_DENOM))) +} + /** * Given a list of balances and a distribution amount, calculate the distribution weights and share amounts. */