-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Data for missing purchase events (#4773)
* 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
Showing
10 changed files
with
321 additions
and
25 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
65 changes: 65 additions & 0 deletions
65
apps/scoutgamecron/src/scripts/syncUserNFTsFromOnchainData.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
181 changes: 181 additions & 0 deletions
181
packages/scoutgame/src/builderNfts/getOnchainPurchaseEvents.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
46
packages/scoutgame/src/builderNfts/getTokenPurchasePrice.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,4 +41,4 @@ async function query() { | |
|
||
} | ||
|
||
query(); | ||
// query(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |