Skip to content

Commit

Permalink
Add bonus export to admin (#4864)
Browse files Browse the repository at this point in the history
* add celo export

* add query for moxie report

* add moxie api key

* make req in parallel
  • Loading branch information
mattcasey authored Oct 21, 2024
1 parent 9edccb3 commit 62546f5
Show file tree
Hide file tree
Showing 17 changed files with 386 additions and 50 deletions.
3 changes: 2 additions & 1 deletion .ebstalk.apps.env/scoutgameadmin.env
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ AUTH_SECRET="{{pull:secretsmanager:/io.cv.app/prd/auth_secret:SecretString:auth_
GITHUB_ACCESS_TOKEN="{{pull:secretsmanager:/io.cv.app/prd/github:SecretString:scoutgame_github_access_token}}"
NEYNAR_API_KEY="{{pull:secretsmanager:/io.cv.app/prd/neynar:SecretString:neynar_api_key}}"
REACT_APP_DECENT_API_KEY="{{pull:secretsmanager:/io.cv.app/prd/decent:SecretString:decent_api_key}}"
ALCHEMY_API_KEY="{{pull:secretsmanager:/io.cv.app/prd/alchemy:SecretString:alchemy_api_key}}"
ALCHEMY_API_KEY="{{pull:secretsmanager:/io.cv.app/prd/alchemy:SecretString:alchemy_api_key}}"
AIRSTACK_API_KEY="{{pull:secretsmanager:/io.cv.app/prd/farcaster:SecretString:airstack_api_key}}"
54 changes: 54 additions & 0 deletions apps/scoutgameadmin/app/api/partners/celo/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { prisma } from '@charmverse/core/prisma-client';
import { getLastWeek } from '@packages/scoutgame/dates';

import { respondWithTSV } from 'lib/nextjs/respondWithTSV';

export const dynamic = 'force-dynamic';

export async function GET() {
const lastWeek = getLastWeek();
const events = await prisma.builderEvent.findMany({
where: {
githubEvent: {
repo: {
bonusPartner: 'celo'
}
},
type: 'merged_pull_request',
week: lastWeek
},
orderBy: {
createdAt: 'asc'
},
select: {
createdAt: true,
builder: {
select: {
email: true,
username: true
}
},
githubEvent: {
select: {
createdAt: true,
url: true,
repo: {
select: {
name: true,
owner: true
}
}
}
}
}
});
const rows = events.map((event) => ({
'Farcaster Name': event.builder.username,
Email: event.builder.email,
Repo: `${event.githubEvent!.repo.owner}/${event.githubEvent!.repo.name}`,
Date: event.createdAt.toDateString(),
Link: event.githubEvent!.url
}));

return respondWithTSV(rows, 'scout_users_export.tsv');
}
202 changes: 202 additions & 0 deletions apps/scoutgameadmin/app/api/partners/moxie/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { log } from '@charmverse/core/log';
import { prisma } from '@charmverse/core/prisma-client';
import { currentSeason, getLastWeek } from '@packages/scoutgame/dates';
import { RateLimit } from 'async-sema';
import { uniq } from 'lodash';

import { respondWithTSV } from 'lib/nextjs/respondWithTSV';

export const dynamic = 'force-dynamic';

type MoxieBonusRow = {
'Builder FID': number;
'Builder username': string;
'Builder event': string;
'Scout FID': number;
'Scout email': string;
'Scout username': string;
};

export async function GET() {
const builders = await prisma.scout.findMany({
where: {
builderStatus: 'approved'
},
orderBy: {
farcasterId: 'asc'
},
select: {
farcasterId: true,
username: true,
events: {
where: {
type: {
in: ['merged_pull_request', 'daily_commit']
},
week: getLastWeek()
}
},
builderNfts: {
where: {
season: currentSeason
},
select: {
nftSoldEvents: {
select: {
scout: {
select: {
farcasterId: true,
username: true
}
}
}
}
}
}
}
});
const rows: MoxieBonusRow[] = [];

await Promise.all(
builders.map(async (builder) => {
if (builder.farcasterId && builder.events.length > 0) {
// TODO: record moxie fan token data so we dont have to look it up again
const moxieNft = await getMoxieFanToken(builder.farcasterId);
if (moxieNft) {
const scoutFids = builder.builderNfts
.map((nft) => nft.nftSoldEvents.map((e) => e.scout.farcasterId))
.flat()
.filter(Boolean);
for (const scoutFid of uniq(scoutFids)) {
const fanTokenAmount = await getMoxieFanTokenAmount({
builderFid: builder.farcasterId,
scoutFid: scoutFid!
});
const scout = await prisma.scout.findUnique({
where: {
farcasterId: scoutFid!
}
});
if (fanTokenAmount && scout) {
// console.log('found scout with fan token', builder.farcasterId, scoutFid, fanTokenAmount);
rows.push({
'Scout FID': scoutFid!,
'Scout email': scout.email || '',
'Scout username': scout.username,
'Builder FID': builder.farcasterId,
'Builder username': builder.username,
'Builder event':
(builder.events[0]!.type === 'merged_pull_request' ? `PR on ` : `Commit on `) +
builder.events[0]!.createdAt.toDateString()
});
}
}
}
}
})
);

return respondWithTSV(rows, 'moxie_bonus_report.tsv');
}

type MoxieFanToken = {
currentPrice: number;
currentPriceInWei: number;
dailyVolumeChange: number;
fanTokenAddress: string;
fanTokenName: string;
fanTokenSymbol: string;
lockedTvl: number;
tlv: number;
tokenLockedAmount: number;
tokenUnlockedAmount: number;
totalSupply: number;
uniqueHolders: number;
unlockedTvl: number;
};

export async function getMoxieFanToken(farcasterId: number): Promise<MoxieFanToken | null> {
const query = `
query MyQuery {
MoxieFanTokens(
input: {filter: {fanTokenSymbol: {_eq: "fid:${farcasterId}"}}, blockchain: ALL}
) {
MoxieFanToken {
currentPrice
currentPriceInWei
dailyVolumeChange
fanTokenAddress
fanTokenName
fanTokenSymbol
lockedTvl
tlv
tokenLockedAmount
tokenUnlockedAmount
totalSupply
uniqueHolders
unlockedTvl
}
}
}
`;
const data = await getGQLQuery(query);
return data.data.MoxieFanTokens.MoxieFanToken?.[0] || null;
}

export async function getMoxieFanTokenAmount({
builderFid,
scoutFid
}: {
builderFid: number;
scoutFid: number;
}): Promise<number> {
const query = `
query GetPortfolioInfo {
MoxieUserPortfolios(
input: {
filter: {
fid: {_eq: "${scoutFid}"},
fanTokenSymbol: {
# Fan Token to check, symbol will be based on types:
# - User: fid:<FID>
# - Channel: cid:<CHANNEL-ID>
# - Network: id:farcaster
_eq: "fid:${builderFid}"
}
}
}
) {
MoxieUserPortfolio {
amount: totalUnlockedAmount
}
}
}
`;
const data = await getGQLQuery(query);
// console.log('data', data);
return data.data.MoxieUserPortfolios.MoxieUserPortfolio?.[0]?.amount || 0;
}

// at most, 10 req per second
// Moxy's rate limit is 3000/min and burst of 300/second.
// @source https://docs.airstack.xyz/airstack-docs-and-faqs/api-capabilities#rate-limits
const rateLimiter = RateLimit(50);

async function getGQLQuery(query: string) {
await rateLimiter();
const response = await fetch('https://api.airstack.xyz/gql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: process.env.AIRSTACK_API_KEY as string
},
body: JSON.stringify({ query })
});

if (!response.ok) {
log.debug('Error fetching Moxie NFT data:', { query, response });
throw new Error(`HTTP error! status: ${response.status}`);
}

return response.json();
}
14 changes: 4 additions & 10 deletions apps/scoutgameadmin/app/api/users/export/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { prisma } from '@charmverse/core/prisma-client';
import { currentSeason } from '@packages/scoutgame/dates';
import { stringify } from 'csv-stringify/sync';
import type { NextRequest } from 'next/server';

import { respondWithTSV } from 'lib/nextjs/respondWithTSV';

export const dynamic = 'force-dynamic';

type ScoutWithGithubUser = {
Expand All @@ -24,7 +25,7 @@ type ScoutWithGithubUser = {
weeklyBuilderRank?: number;
};

export async function GET(req: NextRequest) {
export async function GET() {
const users = await prisma.scout.findMany({
select: {
id: true,
Expand Down Expand Up @@ -66,13 +67,6 @@ export async function GET(req: NextRequest) {
nftsSold: user.userSeasonStats[0]?.nftsSold || 0,
weeklyBuilderRank: user.userWeeklyStats[0]?.rank || undefined
}));
const exportString = stringify(rows, { header: true, columns: Object.keys(rows[0]) });

return new Response(exportString, {
status: 200,
headers: {
'Content-Type': 'text/tsv',
'Content-Disposition': 'attachment; filename=scout_users_export.tsv'
}
});
return respondWithTSV(rows, 'scout_users_export.tsv');
}
3 changes: 3 additions & 0 deletions apps/scoutgameadmin/components/common/ExportButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ export function ExportButton({
children,
src,
filename,
onComplete,
...props
}: {
children: ReactNode;
src: string;
filename: string;
onComplete?: () => void;
} & ButtonProps) {
const { trigger, isMutating, error } = useGETtrigger<undefined, string>(src);
async function onClick() {
Expand All @@ -26,6 +28,7 @@ export function ExportButton({
document.body.appendChild(link);
link.click();
link.remove();
onComplete?.();
}
return (
<LoadingButton loading={isMutating} onClick={onClick} {...props}>
Expand Down
Loading

0 comments on commit 62546f5

Please sign in to comment.