2024.04.08
Solanaのログの取得
こんにちは。次世代システム研究室のL.W.です。
Solanaでのトークンには、ウォレットに関連する全ての取引を引き出す必要がありますか?あるトークンに関連する全てのミント取引、トランスファー取引、バーン取引などを見たいですか?または、FT、NFTの取引履歴を見たいですか?
Solanaでのトークンのログの取得方法はEthereumのと異なります。まとめてみましたが、共有します。
1.Ethereumでのログの取得方法
ライブラリのEthers.jsとWeb3.jsの両方もログの取得の簡単の方法を提供しています。
1.1. Ethers.js
スマートコントラクトを単位に、該当コントラクトと関わるあるブロック高からあるブロック高までのログを取得できます。オプションに、特定のアドレスまたは特定な異弁をフィルタリング条件として使えます。
使用例としては、以下のようです。
async function getPastLogs(fromBlock,toBlock,filter) { console.log(`Getting your contract...`); const contractAddress: string = '<your contract addresss>'; const contractAbi: string = '<your contract abi>'; // you can get the abi from your contract json files. contract = new ethers.Contract(contractAddress, contractAbi, provider); console.log(`Getting the events...`); let events = await contract.queryFilter('filter', fromBlock, toBlock); console.log(events); }
filterの埋め方はここを参照できます。
1.2. Web3.js
web3.eth.getPastLogs(options [, callback])という方法があります。
ここ方法の素晴らしいところは、optionsで任意のコントラクト アドレスを指定することができることです。
これで任意のコントラクトのログを取得することができます。
使用例としては、以下のようです。
web3.eth.getPastLogs({
fromBlock: <fromブロック高>,
toBlock: <toブロック高>,
address: "<your contract address>",
topics: ["<your topics>"]
})
.then(console.log);
2.Solanaでのログの取得方法
solanaもjavascript sdkのweb3.jsを提供していますが、中では、Ethereumのように、あるブロック高からあるブロック高までのある条件のログを取得する方法がサポートされていないです。
ただ、solanaのweb3.jsの提供する方法の組み合わせで、間接にEthereumのようなgetPastLogs(fromBlock, toBlock, filter)を実現できます。
方法が二つがあります。一つ目は、web3.Connection.get
もう一つはweb3.Connection.get
2.1. ブロックスキャンでログを取得する
fromBlockからtoBlockまで、get
public static getPastLogsFromScaningSlots = async ( from: string | undefined, to: string | undefined, filter:{ tokenAddressess: string[], eventName?: string, fromAddress?:string|undefined, toAddress?:string|undefined, includeErrors?:boolean, commitment?:string } ): Promise<(DecodedLog)[]> => { const tokenAddressess = filter.tokenAddressess; const logs: DecodedLog[] = []; let fromSlot = from? parseInt(from) : 0; const latestSlot = await Solana.getSlot(filter.commitment as web3.Commitment); const toSlot = to? parseInt(to) : latestSlot; // get event_max_scan_slot_account from config if(fromSlot == 0){ fromSlot = latestSlot - config.get<number>('block_scan.event_max_scan_slot_account') + 1; } const getVersionedBlockConfig = { commitment: filter.commitment as web3.Finality, maxSupportedTransactionVersion: 0, } //fromSlotからtoslotまでのgetParsedBlockを取得する。parallel_get_transactionsで並列処理する。 const api_batch_size = config.get<number>('api_batch_size') || 40; for (let i = fromSlot; i < toSlot+1; i+=api_batch_size) { const endIndex = i+api_batch_size < toSlot+1 ? i+api_batch_size : toSlot+1; const parsedBlockResponsePromises: Promise<(web3.ParsedBlockResponse | web3.ParsedAccountsModeBlockResponse | web3.ParsedNoneModeBlockResponse | null)>[] = []; const slots:number[] = []; for (let j = i; j < endIndex; j++) { const block:Promise<web3.ParsedBlockResponse | web3.ParsedAccountsModeBlockResponse | web3.ParsedNoneModeBlockResponse | null> = connection.getParsedBlock(j, getVersionedBlockConfig); parsedBlockResponsePromises.push(block); slots.push(j); } const results = await Promise.allSettled(parsedBlockResponsePromises); for (const [index, result] of results.entries()) { if (result.status === "fulfilled") { const block = result.value; if (!block) continue; const logsFromBlock = await this.parseBlock(slots[index], block as web3.ParsedBlockResponse, tokenAddressess,filter) logs.push(...logsFromBlock); } else { console.log(`Debug: slot ${slots[index]} is skipped. Reason: `, result.reason); } } } return logs; } public static parseBlock = async ( slot: number, block: web3.ParsedBlockResponse, filter:{ tokenAddresses: string[], addressToSymbol: Record<string, SymbolType>, eventName?: string, fromAddress?: string | undefined, toAddress?: string | undefined, includeErrors?:boolean, commitment?:string } ): Promise<(DecodedLog)[]> => { const logs: DecodedLog[] = []; for (const transaction of block.transactions) { const confirmedSignatureInfo:web3.ConfirmedSignatureInfo = { signature: transaction.transaction.signatures[0], slot: slot, blockTime: block.blockTime, err: transaction.meta?transaction.meta?.err : null, memo: null, confirmationStatus: 'finalized', } const parsedTransactionWithMeta: web3.ParsedTransactionWithMeta = { slot: slot, transaction: transaction.transaction, meta: transaction.meta, version: transaction.version, } const eventInfo = await this.parseTxs(filter.tokenAddresses, confirmedSignatureInfo, parsedTransactionWithMeta, filter); logs.push(...eventInfo); } return logs; }
※ parseTxs方法でフィルタリングを行う予定です。詳細は下文で説明します。
この方法のメリットは、ログの取得にproviderのCUを節約できます。デメリットとしては、get
この方法で、最新のブロックでのログを監視するのは不向きです。ご存知のように、solanaのブロックの生成間隔はただの0.4sです。getPastLogsはブロックの生成速度に追い掛けできません。
出典:https://usa.visa.com/solutions/crypto/deep-dive-on-solana.html
2.2. SPLプログラムからログを取得する
get
トランザクション署名を引数としてgetParsedTransactionsに渡り、getParsedTransactionsでトランザクションの詳細を取得できます。
get
public static getPastLogsFromSPLProgram = async ( from: string | undefined, to: string | undefined, filter:{ tokenAddressess: string[], eventName?: string, fromAddress?:string|undefined, toAddress?:string|undefined, includeErrors?:boolean, commitment?:string } ): Promise<(DecodedLog)[]> => { const tokenAddressess = filter.tokenAddressess; const logs: DecodedLog[] = []; // get transactions from TOKEN_PROGRAM_ID const txs = await this.getTransactionsForAddress(TOKEN_PROGRAM_ID.toBase58(), from, to, filter.includeErrors, filter.commitment); // txs loop for (let i:number = 0; i < txs.signatureInfos.length; i++) { const parsedTransactionWithMeta = txs.transactions[i]; if(!parsedTransactionWithMeta) continue; const eventInfo = await this.parseTxs(tokenAddressess, txs.signatureInfos[i], parsedTransactionWithMeta, filter); logs.push(...eventInfo); } return logs; } const getTransactionsForAddress = async (address:string, from:string | undefined, to:string | undefined, includeErr: boolean = true, commitment: string = 'finalized'): Promise<{transactions: (web3.ParsedTransactionWithMeta | null)[], signatureInfos: web3.ConfirmedSignatureInfo[]}> => { // maximum transaction signatures to return (between 1 and 1,000). let cnt = 1000; let before = undefined; let beforeSlot = 0; let fromSlot = from? parseInt(from) : 0; // get event_max_scan_slot_account from config if(fromSlot == 0){ const latestSlot = await getSlot(); fromSlot = latestSlot - config.get<number>('block_scan.event_max_scan_slot_account') + 1; //debug console.log(`Debug: fromSlot: ${fromSlot}, toSlot: ${latestSlot}, scanAccount: ${latestSlot - fromSlot + 1}}`); } const toSlot = to? parseInt(to) : 0; const allSignatureInfo: web3.ConfirmedSignatureInfo[] = []; let valid = true; while (cnt == 1000) { let signatureInfo = await getSignaturesForAddress( address, "1000", before, undefined, commitment as web3.Finality, ); // signatureInfo loop //signatureInfoをループし、fromSlotとtoSlotの間のsignatureを取得する for (const signature of signatureInfo) { const slot = signature.slot; // 結果の中では、slotは降順になっているはずですが、そうではない時があるので、slotがbeforeSlotより大きい場合は、ループを終了する if(beforeSlot != 0 && slot > beforeSlot){ valid = false; break; } beforeSlot = slot; if (slot < fromSlot || (toSlot != 0 && slot > toSlot)) continue; // err transactionを含めない場合、err transactionの場合は、continueする if (!includeErr && signature.err) continue; allSignatureInfo.push(signature); } if(!valid) break; // fromより前のsignatureが存在ない場合、ループを終了する if (signatureInfo.length != 0 && signatureInfo[signatureInfo.length-1].slot < fromSlot) break; cnt = signatureInfo.length; before = cnt != 0 ? signatureInfo[signatureInfo.length-1].signature : undefined; } //debug console.log(`Debug: count of signatures for fetch transaction is ${allSignatureInfo.length}`); // the maximum batch request size is 1000 //const transactions = await getTransactions(allSignatureInfo.map((signatureInfo) => signatureInfo.signature), commitment); // 1000件ずつ、getTransactionsを実行する const transactions: (web3.ParsedTransactionWithMeta | null)[] = []; const transactionsPromises: Promise<(web3.ParsedTransactionWithMeta | null)[]>[] = []; const parallel_get_transactions = config.get<number>('parallel_get_transactions') || 50; for (let i = 0; i < allSignatureInfo.length; i+=parallel_get_transactions) { const endIndex = i + parallel_get_transactions < allSignatureInfo.length ? i + parallel_get_transactions : allSignatureInfo.length; const signaturesInfo = allSignatureInfo.slice(i, endIndex); const signatures = signaturesInfo.map((signatureInfo) => signatureInfo.signature); //debug console.log(`Debug: split allSignatureInfo from ${i} to ${endIndex}`); const transactionsTmp = await getTransactions(signatures, commitment); transactions.push(...transactionsTmp); //transactionsPromises.push(getTransactions(signatures, commitment)); } //const transactions = (await Promise.all(transactionsPromises)).flat(); return {transactions, signatureInfos: allSignatureInfo}; } const getTransactions = async (signatures:string[], commitment: string = 'finalized'): Promise<(web3.ParsedTransactionWithMeta | null)[]> => { const txs = await connection.getParsedTransactions(signatures, {commitment: commitment as web3.Finality, maxSupportedTransactionVersion: 0}); return txs; } const getSignaturesForAddress = async (address:string, limit:string | undefined, before:string | undefined, until:string | undefined, commitment: string = 'finalized'): Promise<Array<web3.ConfirmedSignatureInfo>> => { let beforeSlot = 0; const allSignatureInfo: web3.ConfirmedSignatureInfo[] = []; const signatures = await connection.getSignaturesForAddress( new web3.PublicKey(address), {limit: limit ? parseInt(limit) : 1000, before, until}, commitment as web3.Finality, ); //debug console.log(`Debug: the reuslt of getSignaturesForAddress is ${signatures.length}`); // 結果の中では、slotは降順になっているはずですが、そうではない時があるので、slotがbeforeSlotより大きい場合は、ループを終了する for (const signature of signatures) { const slot = signature.slot; if(beforeSlot != 0 && slot > beforeSlot){ break; } beforeSlot = slot; allSignatureInfo.push(signature); } //debug console.log(`Debug: count of valid signatures is ${allSignatureInfo.length}`); return allSignatureInfo; }
この方法で、最新のブロックでのログを監視するのは使えますが、providerのCUが掛かります。
parseTxs方法でフィルタリングを行います。
public static parseTxs = async ( tokenAddresses: string[], confirmedSignatureInfo:web3.ConfirmedSignatureInfo, parsedTransactionWithMeta: web3.ParsedTransactionWithMeta, filter:{ eventName?: string, tokenAddresses: string[], fromAddress?: string | undefined, toAddress?: string | undefined, commitment?:string, includeErrors:boolean } ): Promise<(DecodedLog)[]> => { let type = ''; let programId = TOKEN_PROGRAM_ID.toBase58(); // SPL address, token or token-2022 const rerurnDecodedLogs: DecodedLog[] = []; let obkectTokenAddress = ''; // error check if(!filter.includeErrors && !!confirmedSignatureInfo.err){ return rerurnDecodedLogs; } // parse transaction.meta.transaction.message.instructions const instructions: (web3.ParsedInstruction | web3.PartiallyDecodedInstruction)[] = parsedTransactionWithMeta.transaction.message.instructions; // parse parsedTransactionWithMeta.meta?.innerInstructions?.instructions const innerInstructions: (web3.ParsedInstruction | web3.PartiallyDecodedInstruction)[] = parsedTransactionWithMeta.meta?.innerInstructions?.reduce((acc, cur) => acc.concat(cur.instructions), [] as (web3.ParsedInstruction | web3.PartiallyDecodedInstruction)[]) ?? []; const allInstructions = instructions.concat(innerInstructions); // instructions each loop for (const instruction of allInstructions) { // check parsed if (!('parsed' in instruction)){ continue; } const typeString = instruction.parsed?.type as string; //spl-associated-token-account if(typeString == 'create'){ programId = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; }else{ programId = TOKEN_PROGRAM_ID.toBase58(); } // check programId and mint if (instruction.programId.toBase58() != programId || (('mint' in instruction.parsed?.info) && !tokenAddresses.includes(instruction.parsed?.info.mint))) { continue; } // parse type if(filter.eventName && !typeString.includes(filter.eventName)){ continue; } // 他のfilterがあれば追加 // ... const decodedLog = { name: filter.eventName, eventType: typeString, blockTime: confirmedSignatureInfo.blockTime ?? 0, signature: confirmedSignatureInfo.signature, slot: confirmedSignatureInfo.slot, confirmationStatus: confirmedSignatureInfo.confirmationStatus as string, memo: confirmedSignatureInfo.memo, info: ('parsed' in instruction) ? instruction.parsed?.info : undefined, version: parsedTransactionWithMeta.version as string, }; rerurnDecodedLogs.push(decodedLog); } return rerurnDecodedLogs; }
3.まとめ
最近はsolanaでのMEME coinが流行っていますね。
上の二つの方法でMEME coinを追跡することができますので、使ってみてくださいね。
4.最後に
次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。
皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD