Skip to content

Commit

Permalink
Data for missing purchase events (#4773)
Browse files Browse the repository at this point in the history
* script to create pending transaction

* Add utility for getting transactions

* Simplify transactions code

* Finalise script for debugging transactions

* Add script for getting missing transaction data

* Cleanup methods for fetching onchain data

* Add script for auto-fixing NFTs

* Cleaup names

* Fix types

* Delete old script

---------

Co-authored-by: mattcasey <mattwad@gmail.com>
  • Loading branch information
motechFR and mattcasey authored Oct 10, 2024
1 parent bcbca36 commit 580f168
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 25 deletions.
18 changes: 0 additions & 18 deletions apps/scoutgame/scripts/query.ts

This file was deleted.

65 changes: 65 additions & 0 deletions apps/scoutgamecron/src/scripts/syncUserNFTsFromOnchainData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

import { log } from '@charmverse/core/log';
import { prisma } from '@charmverse/core/prisma-client';
import { optimismUsdcContractAddress, realOptimismMainnetBuildersContract } from '@packages/scoutgame/builderNfts/constants';
import { getOnchainPurchaseEvents } from '@packages/scoutgame/builderNfts/getOnchainPurchaseEvents';
import { getTokenPurchasePrice } from '@packages/scoutgame/builderNfts/getTokenPurchasePrice';
import { handlePendingTransaction } from '@packages/scoutgame/builderNfts/handlePendingTransaction';
import { savePendingTransaction } from '@packages/scoutgame/savePendingTransaction';

async function syncUserNFTsFromOnchainData({username, scoutId}: {username?: string, scoutId?: string}): Promise<void> {
if (!username && !scoutId) {
throw new Error('Either username or scoutId must be provided');
} else if (username && scoutId) {
throw new Error('Only one of username or scoutId can be provided');
}

const scout = await prisma.scout.findFirstOrThrow({
where: {
id: scoutId,
username
}
});

const userPurchases = await getOnchainPurchaseEvents({ scoutId: scout.id });

const txRequiringReconciliation = userPurchases.filter(p => !p.nftPurchase);

for (let i = 0; i < txRequiringReconciliation.length; i++) {

log.info(`Processing missing tx ${i+1} / ${txRequiringReconciliation.length}`)

const tx = txRequiringReconciliation[i];
const expectedPrice = tx.pendingTransaction?.targetAmountReceived ?? await getTokenPurchasePrice({
args: {
amount: BigInt(tx.amount),
tokenId: BigInt(tx.tokenId)
},
blockNumber: BigInt(tx.blockNumber) - BigInt(1)
});

const pendingTx = tx.pendingTransaction ?? await savePendingTransaction({
user: {
scoutId: scout.id,
walletAddress: tx.transferEvent.to
},
transactionInfo: {
destinationChainId: 10,
sourceChainId: 10,
sourceChainTxHash: tx.txHash
},
purchaseInfo: {
quotedPriceCurrency: optimismUsdcContractAddress,
builderContractAddress: realOptimismMainnetBuildersContract,
tokenId: parseInt(tx.tokenId),
quotedPrice: Number(expectedPrice.toString()),
tokenAmount: Number(tx.amount)
}
});

await handlePendingTransaction({ pendingTransactionId: pendingTx.id });
}
}


syncUserNFTsFromOnchainData({ username: 'cryptomobile' }).then(console.log)
6 changes: 4 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions packages/scoutgame/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"./builderNfts/artwork/generateNftImage": "./src/builderNfts/artwork/generateNftImage.tsx"
},
"dependencies": {
"@packages/github": "^0.0.0",
"@packages/onchain": "^0.0.0"
"@packages/onchain": "^0.0.0",
"@packages/utils": "^1.0.0",
"@packages/github": "^0.0.0"
}
}
2 changes: 1 addition & 1 deletion packages/scoutgame/src/builderNfts/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const devOptimismSepoliaBuildersContract = '0x2f6093b70562729952bf379633dee3e899
const devOptimismMainnetBuildersContract = '0x1d305a06cb9dbdc32e08c3d230889acb9fe8a4dd';

const realOptimismSepoliaBuildersContract = '0x0b7342761a10e1b14df427681b967e67f5e6cef9';
const realOptimismMainnetBuildersContract = '0x743ec903fe6d05e73b19a6db807271bb66100e83';
export const realOptimismMainnetBuildersContract = '0x743ec903fe6d05e73b19a6db807271bb66100e83';

export function getBuilderContractAddress(): `0x${string}` {
return (env('BUILDER_NFT_CONTRACT_ADDRESS') || process.env.REACT_APP_BUILDER_NFT_CONTRACT_ADDRESS) as `0x${string}`;
Expand Down
181 changes: 181 additions & 0 deletions packages/scoutgame/src/builderNfts/getOnchainPurchaseEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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 { realOptimismMainnetBuildersContract } from './constants';

const transferSingle = {
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'operator', type: 'address' },
{ indexed: true, internalType: 'address', name: 'from', type: 'address' },
{ indexed: true, internalType: 'address', name: 'to', type: 'address' },
{ indexed: false, internalType: 'uint256', name: 'id', type: 'uint256' },
{ indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }
],
name: 'TransferSingle',
type: 'event'
};

const builderScouted = {
anonymous: false,
inputs: [
{ indexed: false, internalType: 'uint256', name: 'tokenId', type: 'uint256' },
{ indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
{ indexed: false, internalType: 'string', name: 'scout', type: 'string' }
],
name: 'BuilderScouted',
type: 'event'
};

type TransferSingleEvent = {
eventName: 'TransferSingle';
args: { operator: string; from: string; to: string; id: string; value: string };
transactionHash: string;
blockNumber: string;
};

type BuilderScoutedEvent = {
eventName: 'BuilderScouted';
args: { tokenId: string; amount: string; scout: string };
transactionHash: string;
blockNumber: string;
};

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`)
});

// Contract address and the event signature for filtering logs
const contractAddress = realOptimismMainnetBuildersContract;

// Function to get logs for the contract and parse them against the ABI
async function getAndParseLogs() {
const logs = await client.getLogs({
address: contractAddress,
fromBlock: 126062456n,
toBlock: 'latest'
});

const parsedLogs = parseEventLogs({ abi: contractAbi, logs, eventName: ['BuilderScouted', 'TransferSingle'] });

return parsedLogs;
}

type ParsedLogs = Awaited<ReturnType<typeof getAndParseLogs>>;

type SimplifiedGroupedEvent = {
scoutId: string;
amount: string;
tokenId: string;
txHash: string;
blockNumber: string;
transferEvent: {
from: string;
to: string;
operator: string;
value: string;
};
builderScoutedEvent: {
scout: string;
amount: string;
};
};

function groupEventsByTransactionHash(events: ParsedLogs): SimplifiedGroupedEvent[] {
const eventMap: Record<string, Partial<SimplifiedGroupedEvent>> = {};

for (const baseEvent of events) {
const event = baseEvent as ParsedLogs[number] & {
eventName: 'TransferSingle' | 'BuilderScouted';
args: BuilderScoutedEvent['args'] | TransferSingleEvent['args'];
};
const { transactionHash, blockNumber } = event;

if (!eventMap[transactionHash]) {
eventMap[transactionHash] = { txHash: transactionHash, blockNumber: blockNumber as any };
}

if (event.eventName === 'TransferSingle') {
const transferSingleEvent = event as any as TransferSingleEvent;
eventMap[transactionHash].transferEvent = {
from: transferSingleEvent.args.from,
to: transferSingleEvent.args.to,
operator: transferSingleEvent.args.operator,
value: transferSingleEvent.args.value
};
eventMap[transactionHash].tokenId = transferSingleEvent.args.id;
} else if (event.eventName === 'BuilderScouted') {
const builderScoutedEvent = event as any as BuilderScoutedEvent;
eventMap[transactionHash].builderScoutedEvent = {
scout: builderScoutedEvent.args.scout,
amount: builderScoutedEvent.args.amount
};
eventMap[transactionHash].scoutId = builderScoutedEvent.args.scout;
eventMap[transactionHash].amount = builderScoutedEvent.args.amount;
}
}

return Object.values(eventMap).map((entry) => ({
scoutId: entry.scoutId!,
amount: entry.amount!,
tokenId: entry.tokenId!,
txHash: entry.txHash!,
blockNumber: entry.blockNumber!,
transferEvent: entry.transferEvent!,
builderScoutedEvent: entry.builderScoutedEvent!
}));
}

export async function getOnchainPurchaseEvents({ scoutId }: { scoutId: string }) {
const logs = await getAndParseLogs();

const groupedEvents = groupEventsByTransactionHash(logs as any);

const nftPurchases = await prisma.nFTPurchaseEvent.findMany({
where: {
scoutId
},
select: {
txHash: true,
tokensPurchased: true,
paidInPoints: true,
pointsValue: true
}
});

const pendingTransactions = await prisma.pendingNftTransaction.findMany({
where: {
userId: scoutId
},
select: {
id: true,
sourceChainTxHash: true,
sourceChainId: true,
destinationChainTxHash: true,
destinationChainId: true,
tokenAmount: true,
targetAmountReceived: true
}
});

const mappedEvents = groupedEvents
.filter((event) => event.scoutId === scoutId)
.map((event) => {
const nftPurchase = nftPurchases.find((nft) => nft.txHash === event.txHash) ?? null;
const pendingTransaction =
pendingTransactions.find(
(tx) => tx.sourceChainTxHash === event.txHash || tx.destinationChainTxHash === event.txHash
) ?? null;
return { ...event, nftPurchase, pendingTransaction };
});

return mappedEvents;
}

// getOnchainPurchaseEvents({ scoutId: 'bb2e2785-ebbb-441c-9909-df87f0cac5c4' }).then(prettyPrint);
46 changes: 46 additions & 0 deletions packages/scoutgame/src/builderNfts/getTokenPurchasePrice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { getPublicClient } from '@packages/onchain/getPublicClient';
import { decodeFunctionResult, encodeFunctionData } from 'viem';
import { optimism } from 'viem/chains';

import { getBuilderContractAddress } from './constants';

/**
* Optional block number to query the contract at, enabling past pricing data
*/
export async function getTokenPurchasePrice(params: {
args: { tokenId: bigint; amount: bigint };
blockNumber?: bigint;
}): Promise<bigint> {
const abi = [
{
inputs: [
{ internalType: 'uint256', name: 'tokenId', type: 'uint256' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' }
],
name: 'getTokenPurchasePrice',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function'
}
];

const txData = encodeFunctionData({
abi,
functionName: 'getTokenPurchasePrice',
args: [params.args.tokenId, params.args.amount]
});

const { data } = await getPublicClient(optimism.id).call({
to: getBuilderContractAddress(),
data: txData,
blockNumber: params.blockNumber
});

const result = decodeFunctionResult({
abi,
functionName: 'getTokenPurchasePrice',
data: data as `0x${string}`
});

return result as bigint;
}
2 changes: 1 addition & 1 deletion packages/scoutgame/src/scripts/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ async function query() {

}

query();
// query();
3 changes: 2 additions & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"./react": "./src/react.ts",
"./types": "./src/types.ts",
"./dates": "./src/dates.ts",
"./http": "./src/http.ts"
"./http": "./src/http.ts",
"./strings": "./src/strings.ts"
}
}
18 changes: 18 additions & 0 deletions packages/utils/src/strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { log } from '@charmverse/core/log';

export function prettyPrint(input: any): string {
if (!input) {
return '';
}

const pretty =
typeof input === 'object'
? JSON.stringify(input, (key, value) => (typeof value === 'bigint' ? value.toString() : value), 2)
: input.toString
? input.toString()
: input;

log.info(pretty);

return pretty;
}

0 comments on commit 580f168

Please sign in to comment.