如何在Solana链交易数据中精准筛选获取NFT转账记录
Solana 接收NFT交易的筛选判断方法
不能仅通过是否属于SPL-Token交易判断NFT,所有Solana上的同质化代币(USDC、RAY等)同样基于SPL-Token标准实现,仅判断SPL交易会混入大量普通代币转账记录。
核心判断标识
需要同时满足以下3个条件,才能判定为有效NFT转账交易:
- 对应SPL Mint资产的基础参数校验:Mint账户的总供应量
supply严格等于1,小数位数decimals严格等于0,这是NFT非同质化的基础属性要求,可直接过滤掉99%以上的同质化代币 - 元数据标准校验:Solana生态绝大多数NFT遵循Metaplex标准,每个合规NFT的Mint地址会派生固定规则的元数据PDA账户,派生所用程序ID为
metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s,派生种子为["metadata", 元数据程序ID, Mint公钥],只有能查询到对应有效元数据账户的资产,才是正规NFT,可排除恶意发行的单枚零小数SPL代币冒充NFT的情况 - 交易有效性与方向校验:仅统计
meta.err为空的成功交易,且通过对比交易前后preTokenBalances和postTokenBalances的差值,确认目标钱包对应Token账户的资产余额增加量为1(零小数场景下1代表1枚完整NFT),避免统计到钱包向外转出NFT、或同笔交易内无关地址的代币变动。
现有代码的问题
你当前实现存在以下漏判、误判问题:
- 未对Mint资产做参数校验,会把所有SPL代币转账都纳入统计,包含大量普通同质化币记录
- 仅读取交易后余额,未对比交易前后余额差值,无法判断转账方向,会误计入钱包转出交易、同交易无关地址的代币记录
- 未过滤失败交易,链上回滚的失败交易也会被统计
- 未做Metaplex元数据校验,无法区分正规NFT和随意发行的单枚SPL测试币
修正后的参考实现代码
import { clusterApiUrl, Connection, PublicKey, ParsedTransactionWithMeta, } from '@solana/web3.js'; import fs from 'fs'; import path from 'path'; const WALLET = "<WALLET>"; // 接收NFT的钱包地址 const METADATA_PROGRAM_ID = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); (async () => { const connection = new Connection(clusterApiUrl('mainnet-beta')); const targetWallet = new PublicKey(WALLET); const nftTransfers = []; // 拉取地址相关交易签名 const signatures = await connection.getConfirmedSignaturesForAddress2(targetWallet, { limit: 100, }); for (const sigInfo of signatures) { const tx: ParsedTransactionWithMeta | null = await connection.getParsedTransaction(sigInfo.signature, 'finalized'); if (!tx || tx.meta?.err) continue; // 跳过失败交易、空交易 // 整理交易前余额映射:key为<Mint地址_owner地址>,value为余额 const preBalanceMap = new Map(); tx.meta.preTokenBalances?.forEach(balance => { const key = `${balance.mint}_${balance.owner}`; preBalanceMap.set(key, Number(balance.uiTokenAmount.amount)); }); // 遍历交易后余额,找目标钱包余额增加的记录 for (const postBalance of tx.meta.postTokenBalances || []) { if (postBalance.owner !== targetWallet.toString()) continue; const key = `${postBalance.mint}_${postBalance.owner}`; const preAmount = preBalanceMap.get(key) || 0; const postAmount = Number(postBalance.uiTokenAmount.amount); const delta = postAmount - preAmount; // 余额增加量不是1的直接跳过(NFT转1枚) if (delta !== 1) continue; const mintPubkey = new PublicKey(postBalance.mint); // 1. 校验Mint参数:supply=1,decimals=0 const mintInfo = await connection.getParsedAccountInfo(mintPubkey); if (!mintInfo.value) continue; const mintData = mintInfo.value.data as any; if ( mintData.parsed.info.decimals !== 0 || Number(mintData.parsed.info.supply) !== 1 ) continue; // 2. 校验Metaplex元数据账户存在 const [metadataPDA] = await PublicKey.findProgramAddress( [Buffer.from("metadata"), METADATA_PROGRAM_ID.toBuffer(), mintPubkey.toBuffer()], METADATA_PROGRAM_ID ); const metadataAccount = await connection.getAccountInfo(metadataPDA); if (!metadataAccount) continue; // 所有校验通过,记录为有效NFT转入 nftTransfers.push({ signature: sigInfo.signature, blockTime: tx.blockTime, mint: postBalance.mint, slot: tx.slot }); } } fs.writeFileSync( path.join(process.cwd(), 'nft-transfers.json'), JSON.stringify(nftTransfers, null, 2) ); })();
内容的提问来源于stack exchange,提问作者Ari
相关产品推荐
相关产品推荐

