Skip to content

Commit

Permalink
Add script to reconstruct points from transactions (#4809)
Browse files Browse the repository at this point in the history
* Add script to reconstruct points from transactions

* Update scripts for fetching and issuing points

* Update OP

* Fix points getters and setters

* Remove defaulting to prisma

* Implement PR feedback

* Update points scripts

* Refresh points from history:

* Fix points

* Cleanup scripts

* Cleanup

* Fix send points unit test

* Add in unit test for fetching points stats history

* Extra assertion

* Cleanup
  • Loading branch information
motechFR authored Oct 16, 2024
1 parent 4bbc16f commit 3d9137a
Show file tree
Hide file tree
Showing 12 changed files with 643 additions and 94 deletions.
54 changes: 54 additions & 0 deletions apps/scoutgamecron/src/scripts/issuePoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { log } from "@charmverse/core/log";
import { prisma } from "@charmverse/core/prisma-client";
import { currentSeason, getLastWeek } from "@packages/scoutgame/dates";
import {sendPoints} from '@packages/scoutgame/points/sendPoints'
import {refreshPointStatsFromHistory} from '@packages/scoutgame/points/refreshPointStatsFromHistory'

const fids: number[] = [];

const description = `Friends of Scout Game`;

async function issuePoints({points}: {points: number}) {
for (const fid of fids) {
log.info(`Issuing points to fid: ${fid}`);

const scout = await prisma.scout.findFirstOrThrow({
where: {
farcasterId: fid
},
include: {
pointsReceived: {
where: {
event: {
description: {
contains: description,
mode: 'insensitive'
}
}
}
}
}
});

if (scout.pointsReceived.length > 0) {
log.info(`Points already issued to fid: ${fid}`);
continue;
}

await prisma.$transaction(async (tx) => {

await sendPoints({
builderId: scout.id,
points,
claimed: true,
description: `Friends of Scout Game`,
hideFromNotifications: true,
season: currentSeason,
week: getLastWeek(),
tx
});

await refreshPointStatsFromHistory({ userIdOrUsername: scout.id, tx });
}, {timeout: 15000});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { log } from "@charmverse/core/log";
import { prisma } from "@charmverse/core/prisma-client";
import { refreshPointStatsFromHistory } from "@packages/scoutgame/points/refreshPointStatsFromHistory";
import { prettyPrint } from "@packages/utils/strings";



async function refreshPointsFromTransactionHistory() {
const scouts = await prisma.scout.findMany({
select: { id: true, username: true },
orderBy: {
id: 'asc'
},
where: {
createdAt: {
lte: new Date('2024-10-14')
}
}
});

for (let i = 0; i < scouts.length; i++) {
const scout = scouts[i];
try {
log.info(`Fixing points for ${scout.username} ${i + 1} / ${scouts.length}`);
const stats = await refreshPointStatsFromHistory({ userIdOrUsername: scout.id });
log.info(`Successfully fixed points for ${scout.username}. New balance: ${stats.balance}`);
} catch (error) {
log.error(`Failed to fix points for ${scout.username}: ${prettyPrint(error)}`);
}
}
}
3 changes: 0 additions & 3 deletions apps/scoutgamecron/src/scripts/syncUserNFTsFromOnchainData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,3 @@ async function syncUserNFTsFromOnchainData({username, scoutId}: {username?: stri
await handlePendingTransaction({ pendingTransactionId: pendingTx.id });
}
}


// syncUserNFTsFromOnchainData({ username: 'cryptomobile' }).then(console.log)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { prisma } from '@charmverse/core/prisma-client';
import { builderPointsShare, scoutPointsShare } from '@packages/scoutgame/builderNfts/constants';
import { calculateEarnableScoutPointsForRank } from '@packages/scoutgame/points/calculatePoints';
import { updatePointsEarned } from '@packages/scoutgame/points/updatePointsEarned';
import { incrementPointsEarnedStats } from '@packages/scoutgame/points/updatePointsEarned';
import { v4 } from 'uuid';

export async function processScoutPointsPayout({
Expand Down Expand Up @@ -96,6 +96,7 @@ export async function processScoutPointsPayout({
data: {
value: scoutPoints,
recipientId: scoutId,

eventId: builderEventId,
activities: {
create: {
Expand All @@ -107,7 +108,7 @@ export async function processScoutPointsPayout({
}
}
});
await updatePointsEarned({
await incrementPointsEarnedStats({
userId: scoutId,
season,
scoutPoints,
Expand All @@ -129,7 +130,7 @@ export async function processScoutPointsPayout({
}
}
}),
updatePointsEarned({
incrementPointsEarnedStats({
userId: builderId,
season,
builderPoints,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { prisma } from '@charmverse/core/prisma-client';
import { prettyPrint } from '@packages/utils/strings';
import { createPublicClient, http, parseEventLogs } from 'viem';
import { mainnet } from 'viem/chains';
import { optimism } from 'viem/chains';

import { realOptimismMainnetBuildersContract } from './constants';

Expand Down Expand Up @@ -47,8 +46,8 @@ const contractAbi = [transferSingle, builderScouted];

// Set up your client for the desired chain
const client = createPublicClient({
chain: mainnet,
transport: http(`https://opt-mainnet.g.alchemy.com/v2/vTjY0u9L7uoxZQ5GtOw4yKwn7WJelMXp`)
chain: optimism,
transport: http(`https://opt-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`)
});

// Contract address and the event signature for filtering logs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { InvalidInputError } from '@charmverse/core/errors';
import type { PointsReceipt, Prisma, Scout } from '@charmverse/core/prisma-client';
import { prisma } from '@charmverse/core/prisma-client';
import { jest } from '@jest/globals';
import { v4 as uuid } from 'uuid';

import { mockScout } from '../../testing/database';
import type { PointStats } from '../getPointStatsFromHistory';
import { getPointStatsFromHistory } from '../getPointStatsFromHistory';

describe('getPointStatsFromHistory', () => {
let user: Scout;

beforeAll(async () => {
user = await mockScout();
});

it('should return point stats when valid UUID is provided', async () => {
const stats = await getPointStatsFromHistory({ userIdOrUsername: user.id });
expect(stats).toMatchObject({
userId: user.id,
pointsSpent: expect.any(Number),
pointsReceivedAsScout: expect.any(Number),
pointsReceivedAsBuilder: expect.any(Number),
bonusPointsReceived: expect.any(Number),
claimedPoints: expect.any(Number),
unclaimedPoints: expect.any(Number),
balance: expect.any(Number)
});
});

it('should return point stats when valid username is provided', async () => {
const stats = await getPointStatsFromHistory({ userIdOrUsername: user.username });
expect(stats).toMatchObject({
userId: user.id,
pointsSpent: expect.any(Number),
pointsReceivedAsScout: expect.any(Number),
pointsReceivedAsBuilder: expect.any(Number),
bonusPointsReceived: expect.any(Number),
claimedPoints: expect.any(Number),
unclaimedPoints: expect.any(Number),
balance: expect.any(Number)
});
});

it('should return detailed point stats, with a balance calculated based on points claimed minus claimed points (unclaimed points not in balance)', async () => {
const pointsSpentRecords = [{ value: 100 }, { value: 50 }];

const pointsSpent = 100 + 50;

const pointsReceivedAsBuilderRecords = [
{ value: 80, claimedAt: new Date() },
{ value: 90, claimedAt: new Date() }
];

const pointsReceivedAsScoutRecords = [{ value: 120 }, { value: 240, claimedAt: new Date() }];

const bonusPointsReceivedRecords = [{ value: 40 }];

const allPointsReceivedRecords = [
...pointsReceivedAsBuilderRecords,
...pointsReceivedAsScoutRecords,
...bonusPointsReceivedRecords
];

const claimedPoints = allPointsReceivedRecords.reduce((acc, record) => {
if ((record as Pick<PointsReceipt, 'claimedAt' | 'value'>).claimedAt) {
return acc + record.value;
}
return acc;
}, 0);

jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(pointsSpentRecords as PointsReceipt[]);

jest
.spyOn(prisma.pointsReceipt, 'findMany')
.mockResolvedValueOnce(pointsReceivedAsBuilderRecords as PointsReceipt[]);

jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(pointsReceivedAsScoutRecords as PointsReceipt[]);

jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(bonusPointsReceivedRecords as PointsReceipt[]);

jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(allPointsReceivedRecords as PointsReceipt[]);

const pointStats = await getPointStatsFromHistory({ userIdOrUsername: user.id });

// Sanity check that the points add up
expect(pointStats.claimedPoints + pointStats.unclaimedPoints).toEqual(
pointStats.pointsReceivedAsBuilder + pointStats.pointsReceivedAsScout + pointStats.bonusPointsReceived
);

expect(pointStats).toEqual<PointStats>({
balance: claimedPoints - pointsSpent,
bonusPointsReceived: 40,
claimedPoints,
pointsReceivedAsBuilder: 170,
pointsReceivedAsScout: 360,
pointsSpent: 150,
unclaimedPoints: 160,
userId: user.id
});
});

it('should throw InvalidInputError when userIdOrUsername is empty', async () => {
await expect(getPointStatsFromHistory({ userIdOrUsername: '' })).rejects.toThrow(InvalidInputError);
});

it('should throw an error when userIdOrUsername is invalid UUID and does not exist as a username', async () => {
const nonExistentUserId = uuid();
await expect(getPointStatsFromHistory({ userIdOrUsername: nonExistentUserId })).rejects.toThrow();
});

it('should throw an assertion error if point records for individual categories do not match the full list of point records', async () => {
const pointsSpentRecords = [{ value: 100 }, { value: 50 }];
const pointsReceivedAsBuilderRecords = [{ value: 80 }, { value: 90 }];
const pointsReceivedAsScoutRecords = [{ value: 120 }];
const bonusPointsReceivedRecords = [{ value: 40 }];
const allPointsReceivedRecords = [
...pointsReceivedAsBuilderRecords,
// Scout points are missing, so we expected an error
// ...pointsReceivedAsScoutRecords,
...bonusPointsReceivedRecords
];

jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(pointsSpentRecords as PointsReceipt[]); // Mismatch points
//
jest
.spyOn(prisma.pointsReceipt, 'findMany')
.mockResolvedValueOnce(pointsReceivedAsBuilderRecords as PointsReceipt[]); // Mismatch points

jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(pointsReceivedAsScoutRecords as PointsReceipt[]); // Mismatch points

jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(bonusPointsReceivedRecords as PointsReceipt[]); // Mismatch points

jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(allPointsReceivedRecords as PointsReceipt[]); // Mismatch points
await expect(getPointStatsFromHistory({ userIdOrUsername: user.id })).rejects.toThrow();
});
});
20 changes: 17 additions & 3 deletions packages/scoutgame/src/points/__tests__/sendPoints.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { prisma } from '@charmverse/core/prisma-client';

import { currentSeason } from '../../dates';
import { mockBuilder } from '../../testing/database';
import { sendPoints } from '../sendPoints';

Expand All @@ -10,7 +11,10 @@ describe('sendPoints', () => {
await sendPoints({
builderId: builder.id,
points: mockPoints,
hideFromNotifications: true
hideFromNotifications: true,
claimed: true,
description: `Test points`,
season: currentSeason
});
const updated = await prisma.scout.findUnique({
where: {
Expand All @@ -19,11 +23,17 @@ describe('sendPoints', () => {
select: {
currentBalance: true,
userSeasonStats: true,
userAllTimeStats: true,
activities: true
}
});
expect(updated?.currentBalance).toBe(mockPoints);

// Earned as not provided, so stats should not be affected
expect(updated?.userSeasonStats[0]).toBeUndefined();
expect(updated?.userAllTimeStats[0]).toBeUndefined();

// No activities should be created so that a notification doesn't happen
expect(updated?.activities[0]).toBeUndefined();
});

Expand All @@ -33,7 +43,9 @@ describe('sendPoints', () => {
await sendPoints({
builderId: builder.id,
points: mockPoints,
earnedAsBuilder: true
description: 'Test description',
claimed: true,
earnedAs: 'builder'
});
const updated = await prisma.scout.findUnique({
where: {
Expand All @@ -42,11 +54,13 @@ describe('sendPoints', () => {
select: {
currentBalance: true,
userSeasonStats: true,
activities: true
activities: true,
events: true
}
});
expect(updated?.currentBalance).toBe(mockPoints);
expect(updated?.userSeasonStats[0].pointsEarnedAsBuilder).toBe(mockPoints);
expect(updated?.activities[0].type).toBe('points');
expect(updated?.events[0].description).toBe('Test description');
});
});
Loading

0 comments on commit 3d9137a

Please sign in to comment.