Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save dtmrc/1028d07c436fe97a029bcf74acd5830e to your computer and use it in GitHub Desktop.
Save dtmrc/1028d07c436fe97a029bcf74acd5830e to your computer and use it in GitHub Desktop.
Transaction crawling for a list of mints from a given verified creator
// This is alpha code trying to replace a getProgramAccounts call with a Transaction Crawling algorithm to find all mints from a Metaplex Candy Machine.
// DISCLAIMER: Use at your own risk.
//
// This code was originally based on the code from @samuelvanderwaal in https://github.com/metaplex-foundation/get-collection/blob/39116b680301c23c91e3960578c4a37c1c8e07c3/get-collection-ts/index.ts
// The purpose of that code is different, but I learnt a lot from it.
//
// Also got some inspiration from @0xAlice's code in https://github.com/penta-fun/sol-nft-tools/blob/8931ae422fa47b79008d57b01a30aeefd97b8cb6/util/get-nft-mints.ts
import { ConfirmedSignatureInfo, Connection, PublicKey } from "@solana/web3.js";
import { programs } from "@metaplex/js";
import fs from "fs";
import { MetadataData } from "@metaplex-foundation/mpl-token-metadata";
import { pRateLimit } from "p-ratelimit";
const {
metadata: { Metadata }
} = programs;
// Create a shared rate limiter to play nice with the RPC server
const rateLimiter = pRateLimit({
interval: 1000, // 1000 ms == 1 second
rate: 20, // API calls per interval
concurrency: 20, // no more than this running at once
// maxDelay: 2000 // an API call delayed > maxDelay ms is rejected
})
async function main() {
const connection = new Connection("https://ssc-dao.genesysgo.net", "finalized");
const creator_id = new PublicKey(process.argv.slice(2, 3)[0]);
const metaplexTokenMetadataId = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
console.log("Getting signatures...");
let allSignatures: ConfirmedSignatureInfo[] = [];
// This returns the first 1000, so we need to loop through until we run out of signatures to get.
let signatures = await connection.getSignaturesForAddress(creator_id, undefined, 'finalized');
allSignatures.push(...signatures);
do {
let options = {
before: signatures[signatures.length - 1].signature
};
signatures = await connection.getSignaturesForAddress(
creator_id,
options,
'finalized'
);
allSignatures.push(...signatures);
} while (signatures.length > 0);
console.log(`Found ${allSignatures.length} signatures`);
let metadataAddresses = new Set<PublicKey>();
let mintAddresses = new Set<string>();
console.log("Getting transaction data...");
const promises = allSignatures.filter(sigInfo => !sigInfo.err).map(sigInfo => (
rateLimiter(() => {
console.log("Fetching", sigInfo.signature.toString())
return connection.getTransaction(sigInfo.signature, {commitment: 'finalized'})
})
))
const transactions = (await Promise.all(promises)).filter(tx => !!tx);
console.log(`Got ${transactions.length} transactions`);
console.log("Parsing transaction data...");
for (const tx of transactions) {
let allIxs = [];
let accountKeys = tx!.transaction.message.accountKeys.map((p) => p.toString());
const innerInstructions = tx!.meta?.innerInstructions
if (innerInstructions) {
for (const iix of innerInstructions) {
allIxs.push(...iix.instructions)
}
}
allIxs.push(...tx!.transaction.message.instructions)
for (const ix of allIxs) {
// Filter for the following instructions in the Metaplex Token Metadata Program to find the mint:
// (based on Enum positions in https://github.com/metaplex-foundation/metaplex-program-library/blob/f55338bafa957e958329b358d2b9670c98263338/token-metadata/program/src/instruction.rs#L80)
// * 0 ("1" in base58): CreateMetadataAccount
// * 15 ("G" in base58): CreateMetadataAccountV2
const ixIndex = ix.data.slice(0, 1)
if (
// (ix.data.slice(0, 1) == "1" || ix.data.slice(0, 1) == "G") &&
["1", "G"].includes(ixIndex) &&
accountKeys[ix.programIdIndex] == metaplexTokenMetadataId
) {
let metadataAddressIndex = ix.accounts[0];
let metadata_address = tx!.transaction.message.accountKeys[metadataAddressIndex];
metadataAddresses.add(metadata_address);
}
}
}
console.log(`Found ${metadataAddresses.size} metadata addressess`);
// Split the collection in chunks to make a single RPC call per chunk
const chunks = mapChunks(Array.from(metadataAddresses), 100).map(async (accounts) => {
const metadataAccounts = await rateLimiter(() => connection.getMultipleAccountsInfo(accounts, 'finalized'))
for (const account of metadataAccounts) {
let metadata = await MetadataData.deserialize(account!.data as Buffer) as MetadataData;
const verified_creator = metadata.data.creators?.find(c => c.verified && c.address == creator_id.toString())
if (verified_creator)
mintAddresses.add(metadata.mint);
}
})
await Promise.all(chunks)
let mints: string[] = Array.from(mintAddresses);
fs.writeFileSync(`${creator_id}_mints.json`, JSON.stringify(mints, null, 2));
}
function mapChunks(arr: any[], size: number) {
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
)
}
main().then(() => console.log("Success"));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment