diff --git a/libs/adapters/src/rabbitmq/configuration/rascalConfig.ts b/libs/adapters/src/rabbitmq/configuration/rascalConfig.ts index 0fe7ce6a5c8..31a69cf52e2 100644 --- a/libs/adapters/src/rabbitmq/configuration/rascalConfig.ts +++ b/libs/adapters/src/rabbitmq/configuration/rascalConfig.ts @@ -140,6 +140,12 @@ export function getAllRascalConfigs( arguments: queueOptions, }, }, + [RascalQueues.UserReferrals]: { + ...queueConfig, + options: { + arguments: queueOptions, + }, + }, [RascalQueues.FarcasterWorkerPolicy]: { ...queueConfig, options: { @@ -224,6 +230,12 @@ export function getAllRascalConfigs( RascalRoutingKeys.XpProjectionUserMentioned, ], }, + [RascalBindings.UserReferrals]: { + source: RascalExchanges.MessageRelayer, + destination: RascalQueues.UserReferrals, + destinationType: 'queue', + bindingKeys: [RascalRoutingKeys.UserReferralsSignUpFlowCompleted], + }, [RascalBindings.FarcasterWorkerPolicy]: { source: RascalExchanges.MessageRelayer, destination: RascalQueues.FarcasterWorkerPolicy, @@ -264,6 +276,10 @@ export function getAllRascalConfigs( queue: RascalQueues.XpProjection, ...subscriptionConfig, }, + [RascalSubscriptions.UserReferrals]: { + queue: RascalQueues.UserReferrals, + ...subscriptionConfig, + }, [RascalSubscriptions.FarcasterWorkerPolicy]: { queue: RascalQueues.FarcasterWorkerPolicy, ...subscriptionConfig, diff --git a/libs/adapters/src/rabbitmq/rabbitMQConfig.ts b/libs/adapters/src/rabbitmq/rabbitMQConfig.ts index 91cd3db7106..f702ba095d9 100644 --- a/libs/adapters/src/rabbitmq/rabbitMQConfig.ts +++ b/libs/adapters/src/rabbitmq/rabbitMQConfig.ts @@ -75,6 +75,7 @@ export function getRabbitMQConfig( RascalQueues.ContestWorkerPolicy, RascalQueues.ContestProjection, RascalQueues.XpProjection, + RascalQueues.UserReferrals, RascalQueues.FarcasterWorkerPolicy, RascalQueues.DiscordBotPolicy, ]); @@ -85,6 +86,7 @@ export function getRabbitMQConfig( RascalBindings.ContestWorkerPolicy, RascalBindings.ContestProjection, RascalBindings.XpProjection, + RascalBindings.UserReferrals, RascalBindings.FarcasterWorkerPolicy, RascalBindings.DiscordBotPolicy, ]); @@ -98,6 +100,7 @@ export function getRabbitMQConfig( RascalSubscriptions.ContestWorkerPolicy, RascalSubscriptions.ContestProjection, RascalSubscriptions.XpProjection, + RascalSubscriptions.UserReferrals, RascalSubscriptions.FarcasterWorkerPolicy, RascalSubscriptions.DiscordBotPolicy, ]); diff --git a/libs/adapters/src/rabbitmq/types.ts b/libs/adapters/src/rabbitmq/types.ts index d313bac6578..bc615433a80 100644 --- a/libs/adapters/src/rabbitmq/types.ts +++ b/libs/adapters/src/rabbitmq/types.ts @@ -18,6 +18,7 @@ export enum RascalSubscriptions { ContestWorkerPolicy = BrokerSubscriptions.ContestWorkerPolicy, ContestProjection = BrokerSubscriptions.ContestProjection, XpProjection = BrokerSubscriptions.XpProjection, + UserReferrals = BrokerSubscriptions.UserReferrals, FarcasterWorkerPolicy = BrokerSubscriptions.FarcasterWorkerPolicy, } @@ -35,6 +36,7 @@ export enum RascalQueues { ContestWorkerPolicy = 'ContestWorkerPolicyQueue', ContestProjection = 'ContestProjection', XpProjection = 'XpProjection', + UserReferrals = 'UserReferrals', FarcasterWorkerPolicy = 'FarcasterWorkerPolicyQueue', } @@ -47,6 +49,7 @@ export enum RascalBindings { ContestWorkerPolicy = 'ContestWorkerPolicy', ContestProjection = 'ContestProjection', XpProjection = 'XpProjection', + UserReferrals = 'UserReferrals', FarcasterWorkerPolicy = 'FarcasterWorkerPolicy', } @@ -89,6 +92,8 @@ export enum RascalRoutingKeys { XpProjectionCommentUpvoted = EventNames.CommentUpvoted, XpProjectionUserMentioned = EventNames.UserMentioned, + UserReferralsSignUpFlowCompleted = EventNames.SignUpFlowCompleted, + FarcasterWorkerPolicyCastCreated = EventNames.FarcasterCastCreated, FarcasterWorkerPolicyReplyCastCreated = EventNames.FarcasterReplyCastCreated, FarcasterWorkerPolicyVoteCreated = EventNames.FarcasterVoteCreated, diff --git a/libs/core/src/ports/interfaces.ts b/libs/core/src/ports/interfaces.ts index 76e36508e55..c1d8d7ac15e 100644 --- a/libs/core/src/ports/interfaces.ts +++ b/libs/core/src/ports/interfaces.ts @@ -210,6 +210,7 @@ export enum BrokerSubscriptions { ContestProjection = 'ContestProjection', FarcasterWorkerPolicy = 'FarcasterWorkerPolicy', XpProjection = 'XpProjection', + UserReferrals = 'UserReferrals', } /** diff --git a/libs/model/src/models/referral.ts b/libs/model/src/models/referral.ts index c1aa26bccd1..bdc6c1872d7 100644 --- a/libs/model/src/models/referral.ts +++ b/libs/model/src/models/referral.ts @@ -43,7 +43,7 @@ export const Referral = ( defaultValue: 0, }, created_on_chain_timestamp: { - type: Sequelize.INTEGER, + type: Sequelize.BIGINT, allowNull: true, }, created_off_chain_at: { diff --git a/libs/model/src/models/referral_fee.ts b/libs/model/src/models/referral_fee.ts index 26e6cd37124..78d5c80382d 100644 --- a/libs/model/src/models/referral_fee.ts +++ b/libs/model/src/models/referral_fee.ts @@ -37,7 +37,7 @@ export const ReferralFee = ( allowNull: false, }, transaction_timestamp: { - type: Sequelize.INTEGER, + type: Sequelize.BIGINT, allowNull: false, }, }, diff --git a/libs/model/src/policies/handleReferralFeeDistributed.ts b/libs/model/src/policies/handleReferralFeeDistributed.ts index 1d9f1cdd71b..50be2711714 100644 --- a/libs/model/src/policies/handleReferralFeeDistributed.ts +++ b/libs/model/src/policies/handleReferralFeeDistributed.ts @@ -1,10 +1,8 @@ -import { getBlock } from '@hicommonwealth/evm-protocols'; import { models } from '@hicommonwealth/model'; import { chainEvents, events } from '@hicommonwealth/schemas'; import { ZERO_ADDRESS } from '@hicommonwealth/shared'; import { BigNumber } from 'ethers'; import { z } from 'zod'; -import { chainNodeMustExist } from './utils'; export async function handleReferralFeeDistributed( event: z.infer, @@ -29,13 +27,6 @@ export async function handleReferralFeeDistributed( return; } else if (existingFee) return; - const chainNode = await chainNodeMustExist(event.eventSource.ethChainId); - - const { block } = await getBlock({ - rpc: chainNode.private_url! || chainNode.url!, - blockHash: event.rawLog.blockHash, - }); - const feeAmount = Number(BigNumber.from(referrerReceivedAmount).toBigInt()) / 1e18; @@ -48,7 +39,7 @@ export async function handleReferralFeeDistributed( distributed_token_address: tokenAddress, referrer_recipient_address: referrerAddress, referrer_received_amount: feeAmount, - transaction_timestamp: Number(block.timestamp), + transaction_timestamp: Number(event.block.timestamp), }, { transaction }, ); diff --git a/libs/model/src/policies/handleReferralSet.ts b/libs/model/src/policies/handleReferralSet.ts index c30140763e4..6dfebf55bef 100644 --- a/libs/model/src/policies/handleReferralSet.ts +++ b/libs/model/src/policies/handleReferralSet.ts @@ -1,8 +1,6 @@ -import { getBlock } from '@hicommonwealth/evm-protocols'; import { models } from '@hicommonwealth/model'; import { chainEvents, events } from '@hicommonwealth/schemas'; import { z } from 'zod'; -import { chainNodeMustExist } from './utils'; export async function handleReferralSet( event: z.infer, @@ -34,20 +32,13 @@ export async function handleReferralSet( return; } - const chainNode = await chainNodeMustExist(event.eventSource.ethChainId); - - const { block } = await getBlock({ - rpc: chainNode.private_url! || chainNode.url!, - blockHash: event.rawLog.blockHash, - }); - // Triggered when an incomplete Referral (off-chain only) was created during user sign up if (existingReferral && existingReferral?.eth_chain_id === null) { await existingReferral.update({ eth_chain_id: event.eventSource.ethChainId, transaction_hash: event.rawLog.transactionHash, namespace_address: namespaceAddress, - created_on_chain_timestamp: Number(block.timestamp), + created_on_chain_timestamp: Number(event.block.timestamp), }); } // Triggered when the referral was set on-chain only (user didn't sign up i.e. no incomplete Referral) @@ -65,7 +56,7 @@ export async function handleReferralSet( referee_address: event.rawLog.address, referrer_address: referrerAddress, referrer_received_eth_amount: 0, - created_on_chain_timestamp: Number(block.timestamp), + created_on_chain_timestamp: Number(event.block.timestamp), }); } } diff --git a/libs/model/src/user/GetUserReferralFees.query.ts b/libs/model/src/user/GetUserReferralFees.query.ts new file mode 100644 index 00000000000..5045bb92b73 --- /dev/null +++ b/libs/model/src/user/GetUserReferralFees.query.ts @@ -0,0 +1,46 @@ +import { type Query } from '@hicommonwealth/core'; +import * as schemas from '@hicommonwealth/schemas'; +import { QueryTypes } from 'sequelize'; +import { z } from 'zod'; +import { models } from '../database'; + +export function GetUserReferralFees(): Query< + typeof schemas.GetUserReferralFees +> { + return { + ...schemas.GetUserReferralFees, + auth: [], + secure: true, + body: async ({ actor }) => { + return await models.sequelize.query< + z.infer + >( + ` +WITH +referrer_addresses AS ( + SELECT DISTINCT address + FROM "Addresses" + WHERE user_id = :user_id AND address LIKE '0x%' +) +SELECT + eth_chain_id, + transaction_hash, + namespace_address, + distributed_token_address, + referrer_recipient_address, + referrer_received_amount, + CAST(transaction_timestamp AS DOUBLE PRECISION) as transaction_timestamp +FROM "ReferralFees" +WHERE referrer_recipient_address IN (SELECT * FROM referrer_addresses); + `, + { + type: QueryTypes.SELECT, + raw: true, + replacements: { + user_id: actor.user.id, + }, + }, + ); + }, + }; +} diff --git a/libs/model/src/user/GetUserReferrals.query.ts b/libs/model/src/user/GetUserReferrals.query.ts index 94d41b6704d..31b77c3cf84 100644 --- a/libs/model/src/user/GetUserReferrals.query.ts +++ b/libs/model/src/user/GetUserReferrals.query.ts @@ -16,20 +16,37 @@ export function GetUserReferrals(): Query { return await models.sequelize.query>( ` - WITH referrer_addresses AS (SELECT DISTINCT address - FROM "Addresses" - WHERE user_id = :user_id - AND address LIKE '0x%'), - referrals AS (SELECT * - FROM "Referrals" - WHERE referrer_address IN (SELECT * FROM referrer_addresses)), - referee_addresses AS (SELECT DISTINCT A.address, A.user_id - FROM "Addresses" A - JOIN referrals ON referee_address = A.address) - SELECT R.*, U.id as referee_user_id, U.profile as referee_profile - FROM referrals R - JOIN referee_addresses RA ON RA.address = R.referee_address - JOIN "Users" U ON U.id = RA.user_id; +WITH +referrer_addresses AS ( + SELECT DISTINCT address + FROM "Addresses" + WHERE user_id = :user_id AND address LIKE '0x%'), +referrals AS ( + SELECT + id, + eth_chain_id, + transaction_hash, + namespace_address, + referee_address, + referrer_address, + referrer_received_eth_amount, + CAST(created_on_chain_timestamp AS DOUBLE PRECISION) as created_on_chain_timestamp, + created_off_chain_at, + updated_at + FROM "Referrals" + WHERE referrer_address IN (SELECT * FROM referrer_addresses)), +referee_addresses AS ( + SELECT DISTINCT A.address, A.user_id + FROM "Addresses" A + JOIN referrals ON referee_address = A.address +) +SELECT + R.*, + U.id as referee_user_id, + U.profile as referee_profile +FROM referrals R + JOIN referee_addresses RA ON RA.address = R.referee_address + JOIN "Users" U ON U.id = RA.user_id; `, { type: QueryTypes.SELECT, diff --git a/libs/model/src/user/index.ts b/libs/model/src/user/index.ts index 661b4ca73fb..8456cef3c49 100644 --- a/libs/model/src/user/index.ts +++ b/libs/model/src/user/index.ts @@ -5,6 +5,7 @@ export * from './GetNewContent.query'; export * from './GetUser.query'; export * from './GetUserAddresses.query'; export * from './GetUserProfile.query'; +export * from './GetUserReferralFees.query'; export * from './GetUserReferrals.query'; export * from './GetXps.query'; export * from './SearchUserProfiles.query'; diff --git a/libs/model/test/referral/referral-lifecycle.spec.ts b/libs/model/test/referral/referral-lifecycle.spec.ts index 11c2a3c3286..93dba3eb258 100644 --- a/libs/model/test/referral/referral-lifecycle.spec.ts +++ b/libs/model/test/referral/referral-lifecycle.spec.ts @@ -1,10 +1,59 @@ +import { BigNumber } from '@ethersproject/bignumber'; import { Actor, command, dispose, query } from '@hicommonwealth/core'; -import { GetUserReferrals } from 'model/src/user/GetUserReferrals.query'; +import { EvmEventSignatures } from '@hicommonwealth/evm-protocols'; +import * as schemas from '@hicommonwealth/schemas'; +import { ZERO_ADDRESS } from '@hicommonwealth/shared'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { z } from 'zod'; +import { models } from '../../src/database'; +import { ChainEventPolicy } from '../../src/policies'; import { seed } from '../../src/tester'; -import { UpdateUser, UserReferrals } from '../../src/user'; +import { GetUserReferralFees, UpdateUser, UserReferrals } from '../../src/user'; +import { GetUserReferrals } from '../../src/user/GetUserReferrals.query'; import { drainOutbox, seedCommunity } from '../utils'; +function chainEvent( + transactionHash: string, + address: string, + eventSignature: string, + parsedArgs: unknown[], +) { + return { + event_name: 'ChainEventCreated', + event_payload: { + eventSource: { + ethChainId: 1, + eventSignature, + }, + parsedArgs, + rawLog: { + blockNumber: 1, + blockHash: '0x1', + transactionIndex: 1, + removed: false, + address, + data: '0x', + topics: [], + transactionHash, + logIndex: 1, + }, + block: { + number: 1, + hash: '0x1', + logsBloom: '0x1', + nonce: '0x1', + parentHash: '0x1', + timestamp: new Date().getTime(), + miner: '0x1', + gasLimit: 1, + gasUsed: 1, + }, + }, + created_at: new Date(), + updated_at: new Date(), + }; +} + describe('Referral lifecycle', () => { let admin: Actor; let member: Actor; @@ -61,43 +110,164 @@ describe('Referral lifecycle', () => { }, }); + // creates "partial" platform entries for referrals await drainOutbox(['SignUpFlowCompleted'], UserReferrals); - // get referrals + const expectedReferrals: z.infer[] = [ + { + eth_chain_id: null, + transaction_hash: null, + namespace_address: null, + referee_address: member.address!, + referrer_address: admin.address!, + referrer_received_eth_amount: 0, + created_on_chain_timestamp: null, + created_off_chain_at: expect.any(Date), + updated_at: expect.any(Date), + referee_user_id: member.user.id!, + referee_profile: { name: 'member' }, + }, + { + eth_chain_id: null, + transaction_hash: null, + namespace_address: null, + referee_address: nonMember.address!, + referrer_address: admin.address!, + referrer_received_eth_amount: 0, + created_on_chain_timestamp: null, + created_off_chain_at: expect.any(Date), + updated_at: expect.any(Date), + referee_user_id: nonMember.user.id!, + referee_profile: { name: 'non-member' }, + }, + ]; + + // get "partial" platform entries for referrals const referrals = await query(GetUserReferrals(), { actor: admin, payload: {}, }); + expect(referrals).toMatchObject(expectedReferrals); + + // simulate on-chain transactions that occur when referees + // deploy a new namespace with a referral link (ReferralSet) + const namespaceAddress = '0x0000000000000000000000000000000000000001'; + const chainEvents1 = [ + chainEvent( + '0x1', + member.address!, // referee + EvmEventSignatures.Referrals.ReferralSet, + [ + namespaceAddress, + admin.address, // referrer + ], + ), + chainEvent( + '0x2', + nonMember.address!, // referee + EvmEventSignatures.Referrals.ReferralSet, + [ + namespaceAddress, + admin.address, // referrer + ], + ), + ]; + await models.Outbox.bulkCreate(chainEvents1); + + // syncs "partial" platform entries for referrals with on-chain transactions + await drainOutbox(['ChainEventCreated'], ChainEventPolicy); + + expectedReferrals[0].eth_chain_id = 1; + expectedReferrals[0].transaction_hash = '0x1'; + expectedReferrals[0].namespace_address = namespaceAddress; + expectedReferrals[0].created_on_chain_timestamp = + chainEvents1[0].event_payload.block.timestamp; + expectedReferrals[1].eth_chain_id = 1; + expectedReferrals[1].transaction_hash = '0x2'; + expectedReferrals[1].namespace_address = namespaceAddress; + expectedReferrals[1].created_on_chain_timestamp = + chainEvents1[1].event_payload.block.timestamp; + + // get referrals again with tx attributes + const referrals2 = await query(GetUserReferrals(), { + actor: admin, + payload: {}, + }); + expect(referrals2).toMatchObject(expectedReferrals); + + // simulate on-chain transactions that occur when + // referral fees are distributed to the referrer + const checkpoint = new Date(); + const fee1 = 1; + const fee2 = 2; + const ethMul = BigNumber.from(10).pow(18); + const hex1 = BigNumber.from(fee1).mul(ethMul).toHexString(); + const hex2 = BigNumber.from(fee2).mul(ethMul).toHexString(); + await models.Outbox.bulkCreate([ + chainEvent( + '0x3', + member.address!, + EvmEventSignatures.Referrals.FeeDistributed, + [ + namespaceAddress, + ZERO_ADDRESS, + { hex: hex1, type: 'BigNumber' }, // total amount distributed + admin.address, // referrer address + { hex: hex1, type: 'BigNumber' }, // referrer received amount + ], + ), + chainEvent( + '0x4', + nonMember.address!, + EvmEventSignatures.Referrals.FeeDistributed, + [ + namespaceAddress, + ZERO_ADDRESS, + { hex: hex2, type: 'BigNumber' }, // total amount distributed + admin.address, // referrer address + { hex: hex2, type: 'BigNumber' }, // referrer received amount + ], + ), + ]); - expect(referrals?.length).toBe(2); - - let ref = referrals!.at(0)!; - expect(ref).toMatchObject({ - eth_chain_id: null, - transaction_hash: null, - namespace_address: null, - referee_address: member.address, - referrer_address: admin.address, - referrer_received_eth_amount: 0, - created_on_chain_timestamp: null, - created_off_chain_at: expect.any(Date), - updated_at: expect.any(Date), - referee_user_id: member.user.id, - referee_profile: { name: 'member' }, + // syncs referral fees + await drainOutbox(['ChainEventCreated'], ChainEventPolicy, checkpoint); + + expectedReferrals[0].referrer_received_eth_amount = fee1; + expectedReferrals[1].referrer_received_eth_amount = fee2; + + // get referrals again with fees + const referrals3 = await query(GetUserReferrals(), { + actor: admin, + payload: {}, }); - ref = referrals!.at(1)!; - expect(ref).toMatchObject({ - eth_chain_id: null, - transaction_hash: null, - namespace_address: null, - referee_address: nonMember.address, - referrer_address: admin.address, - referrer_received_eth_amount: 0, - created_on_chain_timestamp: null, - created_off_chain_at: expect.any(Date), - updated_at: expect.any(Date), - referee_user_id: nonMember.user.id!, - referee_profile: { name: 'non-member' }, + expect(referrals3).toMatchObject(expectedReferrals); + + // get referral fees + const expectedReferralFees = [ + { + eth_chain_id: 1, + transaction_hash: '0x3', + namespace_address: namespaceAddress, + distributed_token_address: ZERO_ADDRESS, + referrer_recipient_address: admin.address, + referrer_received_amount: fee1, + transaction_timestamp: expect.any(Number), + }, + { + eth_chain_id: 1, + transaction_hash: '0x4', + namespace_address: namespaceAddress, + distributed_token_address: ZERO_ADDRESS, + referrer_recipient_address: admin.address, + referrer_received_amount: fee2, + transaction_timestamp: expect.any(Number), + }, + ]; + const referralFees = await query(GetUserReferralFees(), { + actor: admin, + payload: {}, }); + expect(referralFees).toMatchObject(expectedReferralFees); }); }); diff --git a/libs/model/test/utils/outbox-drain.ts b/libs/model/test/utils/outbox-drain.ts index ecce15e1ea3..2c6667eacbc 100644 --- a/libs/model/test/utils/outbox-drain.ts +++ b/libs/model/test/utils/outbox-drain.ts @@ -1,4 +1,4 @@ -import { Projection, handleEvent } from '@hicommonwealth/core'; +import { Policy, Projection, handleEvent } from '@hicommonwealth/core'; import { Events, events } from '@hicommonwealth/schemas'; import { Op } from 'sequelize'; import { ZodUndefined } from 'zod'; @@ -12,7 +12,9 @@ type EventSchemas = typeof events; */ export async function drainOutbox( events: E[], - factory: () => Projection<{ [Name in E]: EventSchemas[Name] }, ZodUndefined>, + factory: () => + | Projection<{ [Name in E]: EventSchemas[Name] }, ZodUndefined> + | Policy<{ [Name in E]: EventSchemas[Name] }, ZodUndefined>, from?: Date, ) { const drained = await models.Outbox.findAll({ @@ -26,12 +28,12 @@ export async function drainOutbox( }, order: [['created_at', 'ASC']], }); - const projection = factory(); + const handler = factory(); for (const { event_name, event_payload } of drained) { console.log( `>>> ${event_name} >>> ${factory.name} >>> ${JSON.stringify(event_payload)}`, ); - await handleEvent(projection, { + await handleEvent(handler, { name: event_name, payload: event_payload, }); diff --git a/libs/schemas/src/events/events.schemas.ts b/libs/schemas/src/events/events.schemas.ts index 43f303b25d2..d88f7d12797 100644 --- a/libs/schemas/src/events/events.schemas.ts +++ b/libs/schemas/src/events/events.schemas.ts @@ -190,6 +190,17 @@ const ChainEventCreatedBase = z.object({ transactionHash: z.string(), logIndex: z.number(), }), + block: z.object({ + number: z.number(), + hash: z.string(), + logsBloom: z.string(), + nonce: z.string().optional(), + parentHash: z.string(), + timestamp: z.number(), + miner: z.string(), + gasLimit: z.number(), + gasUsed: z.number(), + }), }); /** diff --git a/libs/schemas/src/queries/user.schemas.ts b/libs/schemas/src/queries/user.schemas.ts index 041d1a0763e..116b8d55430 100644 --- a/libs/schemas/src/queries/user.schemas.ts +++ b/libs/schemas/src/queries/user.schemas.ts @@ -1,6 +1,6 @@ import { ChainBase, Roles } from '@hicommonwealth/shared'; import { z } from 'zod'; -import { Referral, User } from '../entities'; +import { Referral, ReferralFees, User } from '../entities'; import { Tags } from '../entities/tag.schemas'; import { UserProfile } from '../entities/user.schemas'; import { XpLog } from '../entities/xp.schemas'; @@ -86,18 +86,23 @@ export const GetUserAddresses = { ), }; -export const ReferralView = z.array( - Referral.extend({ - referee_user_id: PG_INT, - referee_profile: UserProfile, - }), -); +export const ReferralView = Referral.extend({ + referee_user_id: PG_INT, + referee_profile: UserProfile, +}); export const GetUserReferrals = { input: z.object({ user_id: PG_INT.optional() }), output: z.array(ReferralView), }; +export const ReferralFeesView = ReferralFees; + +export const GetUserReferralFees = { + input: z.object({}), + output: z.array(ReferralFeesView), +}; + export const XpLogView = XpLog.extend({ user_profile: UserProfile, creator_profile: UserProfile.nullish(), diff --git a/packages/commonwealth/server/api/user.ts b/packages/commonwealth/server/api/user.ts index 264f0ce725b..4f1b2ed285d 100644 --- a/packages/commonwealth/server/api/user.ts +++ b/packages/commonwealth/server/api/user.ts @@ -61,5 +61,6 @@ export const trpcRouter = trpc.router({ getUserAddresses: trpc.query(User.GetUserAddresses, trpc.Tag.User), searchUserProfiles: trpc.query(User.SearchUserProfiles, trpc.Tag.User), getUserReferrals: trpc.query(User.GetUserReferrals, trpc.Tag.User), + getUserReferralFees: trpc.query(User.GetUserReferralFees, trpc.Tag.User), getXps: trpc.query(User.GetXps, trpc.Tag.User), }); diff --git a/packages/commonwealth/server/bindings/bootstrap.ts b/packages/commonwealth/server/bindings/bootstrap.ts index b06cff6b7bd..c82fa40dc32 100644 --- a/packages/commonwealth/server/bindings/bootstrap.ts +++ b/packages/commonwealth/server/bindings/bootstrap.ts @@ -107,6 +107,15 @@ export async function bootstrapBindings( BrokerSubscriptions.XpProjection, ); + const userReferralsProjectionSubRes = await brokerInstance.subscribe( + BrokerSubscriptions.UserReferrals, + User.UserReferrals(), + ); + checkSubscriptionResponse( + userReferralsProjectionSubRes, + BrokerSubscriptions.UserReferrals, + ); + const farcasterWorkerSubRes = await brokerInstance.subscribe( BrokerSubscriptions.FarcasterWorkerPolicy, FarcasterWorker(), diff --git a/packages/commonwealth/server/migrations/20250108123200-update-referral-fee-timestamp.js b/packages/commonwealth/server/migrations/20250108123200-update-referral-fee-timestamp.js new file mode 100644 index 00000000000..7247a20e1d3 --- /dev/null +++ b/packages/commonwealth/server/migrations/20250108123200-update-referral-fee-timestamp.js @@ -0,0 +1,18 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface) { + await queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.sequelize.query( + ` + ALTER TABLE "Referrals" ALTER COLUMN "created_on_chain_timestamp" TYPE BIGINT; + ALTER TABLE "ReferralFees" ALTER COLUMN "transaction_timestamp" TYPE BIGINT; + `, + { transaction }, + ); + }); + }, + + async down() {}, +}; diff --git a/packages/commonwealth/server/workers/evmChainEvents/logProcessing.ts b/packages/commonwealth/server/workers/evmChainEvents/logProcessing.ts index af7c952eb45..b0fbb193afa 100644 --- a/packages/commonwealth/server/workers/evmChainEvents/logProcessing.ts +++ b/packages/commonwealth/server/workers/evmChainEvents/logProcessing.ts @@ -1,7 +1,13 @@ import { Log } from '@ethersproject/providers'; import { logger as _logger, stats } from '@hicommonwealth/core'; import { ethers } from 'ethers'; -import { AbiSignatures, ContractSources, EvmEvent, EvmSource } from './types'; +import { + AbiSignatures, + ContractSources, + EvmBlockDetails, + EvmEvent, + EvmSource, +} from './types'; const logger = _logger(import.meta); @@ -41,7 +47,11 @@ export async function getLogs({ contractAddresses: string[]; startingBlockNum: number; endingBlockNum: number; -}): Promise<{ logs: Log[]; lastBlockNum: number }> { +}): Promise<{ + logs: Log[]; + lastBlockNum: number; + blockDetails: Record; +}> { let startBlock = startingBlockNum; const provider = getProvider(rpc); @@ -54,12 +64,12 @@ export async function getLogs({ endingBlockNum, }, ); - return { logs: [], lastBlockNum: endingBlockNum }; + return { logs: [], lastBlockNum: endingBlockNum, blockDetails: {} }; } if (contractAddresses.length === 0) { logger.error(`No contracts given`); - return { logs: [], lastBlockNum: endingBlockNum }; + return { logs: [], lastBlockNum: endingBlockNum, blockDetails: {} }; } // limit the number of blocks to fetch to avoid rate limiting on some public EVM nodes like Celo @@ -96,6 +106,27 @@ export async function getLogs({ }, ]); + const blockNumbers = [...new Set(logs.map((l) => l.blockNumber))]; + const blockDetails = await Promise.all( + blockNumbers.map(async (blockNumber) => { + const block = await provider.send('eth_getBlockByNumber', [ + blockNumber, + false, + ]); + return { + number: parseInt(block.number, 16), + hash: block.hash, + logsBloom: block.logsBloom, + parentHash: block.parentHash, + miner: block.miner, + nonce: block.nonce ? block.nonce.toString() : undefined, + timestamp: parseInt(block.timestamp, 16), + gasLimit: parseInt(block.gasLimit, 16), + gasUsed: parseInt(block.gasUsed, 16), + } as EvmBlockDetails; + }), + ); + const formattedLogs: Log[] = logs.map((log) => ({ ...log, blockNumber: parseInt(log.blockNumber, 16), @@ -103,7 +134,14 @@ export async function getLogs({ logIndex: parseInt(log.logIndex, 16), })); - return { logs: formattedLogs, lastBlockNum: endingBlockNum }; + return { + logs: formattedLogs, + lastBlockNum: endingBlockNum, + blockDetails: blockDetails.reduce((map, details) => { + map[details.number] = details; + return map; + }, {}), + }; } export async function parseLogs( @@ -167,15 +205,22 @@ export async function getEvents( startingBlockNum: number, endingBlockNum: number, ): Promise<{ events: EvmEvent[]; lastBlockNum: number }> { - const { logs, lastBlockNum } = await getLogs({ + const { logs, lastBlockNum, blockDetails } = await getLogs({ rpc: evmSource.rpc, maxBlockRange: evmSource.maxBlockRange, contractAddresses: Object.keys(evmSource.contracts), startingBlockNum, endingBlockNum, }); + const events = await parseLogs(evmSource.contracts, logs); - return { events, lastBlockNum }; + return { + events: events.map((e) => ({ + ...e, + block: blockDetails[e.rawLog.blockNumber], + })), + lastBlockNum, + }; } /** diff --git a/packages/commonwealth/server/workers/evmChainEvents/types.ts b/packages/commonwealth/server/workers/evmChainEvents/types.ts index fffd2a3a147..bc0604b43cb 100644 --- a/packages/commonwealth/server/workers/evmChainEvents/types.ts +++ b/packages/commonwealth/server/workers/evmChainEvents/types.ts @@ -4,6 +4,18 @@ import { AbiType } from '@hicommonwealth/shared'; import { ethers } from 'ethers'; import { z } from 'zod'; +export type EvmBlockDetails = { + number: number; + hash: string; + logsBloom: string; + nonce?: string; + parentHash: string; + timestamp: number; + miner: string; + gasLimit: number; + gasUsed: number; +}; + export type EvmEvent = { eventSource: { ethChainId: number; @@ -11,6 +23,7 @@ export type EvmEvent = { }; parsedArgs: ethers.utils.Result; rawLog: Log; + block?: EvmBlockDetails; }; const sourceType = EvmEventSource.extend({ diff --git a/packages/commonwealth/test/integration/commonwealthConsumer/chainEventCreatedPolicy.spec.ts b/packages/commonwealth/test/integration/commonwealthConsumer/chainEventCreatedPolicy.spec.ts index ebaf208ea52..d0e8118e625 100644 --- a/packages/commonwealth/test/integration/commonwealthConsumer/chainEventCreatedPolicy.spec.ts +++ b/packages/commonwealth/test/integration/commonwealthConsumer/chainEventCreatedPolicy.spec.ts @@ -61,6 +61,17 @@ async function processValidStakeTransaction() { eventSignature: '0xfc13c9a8a9a619ac78b803aecb26abdd009182411d51a986090f82519d88a89e', }, + block: { + number: 0x1, + hash: '0x1', + logsBloom: '0x1', + nonce: '0x1', + parentHash: '0x1', + timestamp: 1673369600, + miner: '0x0000000000000000000000000000000000000000', + gasLimit: 0, + gasUsed: 0, + }, }, }; await processChainEventCreated(context);