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.getBlock(slotrawConfig?)という方法を活用です。

もう一つはweb3.Connection.getSignaturesForAddress(addressoptions?commitment?)

 

2.1. ブロックスキャンでログを取得する

fromBlockからtoBlockまで、getBlockでブロックでのトランザクション詳細を取得し、filter条件と照らし合わせます。一致するのを残します。
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を節約できます。デメリットとしては、getBlockの反応の遅さです。getBlockを並列処理で、多少効率がアップできたが、限界があります。

この方法で、最新のブロックでのログを監視するのは不向きです。ご存知のように、solanaのブロックの生成間隔はただの0.4sです。getPastLogsはブロックの生成速度に追い掛けできません。


出典:https://usa.visa.com/solutions/crypto/deep-dive-on-solana.html

 

2.2. SPLプログラムからログを取得する

getSignaturesForAddress方法はあるアドレスと関わる全てのトランザクション署名(transaction signature, トランザクションIDのこと。solanaではトランザクションの最初目の署名をトランザクションの唯一性を保たれる)を取得できます。getSignaturesForAddress(<SPL address>options?commitment?)は全てのトークンのトランザクション署名を取得できます。(SPL Token-2022 Programの場合、SPL addressは「TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb」となります。SPL Token Programの場合「TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA」となります。)

トランザクション署名を引数としてgetParsedTransactionsに渡り、getParsedTransactionsでトランザクションの詳細を取得できます。

getSignaturesForAddressは返却するtransaction signaturesのリミットは1000件なので、options.before, options.untilを活用することで、全てのトランザクション署名を取得できます。

 
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
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事