Skip to content

Instantly share code, notes, and snippets.

@gayleQN
Last active October 27, 2025 16:09
Show Gist options
  • Select an option

  • Save gayleQN/ce5fb7c31032cccc050679d8bc3f48b8 to your computer and use it in GitHub Desktop.

Select an option

Save gayleQN/ce5fb7c31032cccc050679d8bc3f48b8 to your computer and use it in GitHub Desktop.
EVM Wallet Monitoring with Key-Value Store List
async function main(stream) {
const LIST_NAME = "wallets";
const TRANSFER_TOPIC =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
// --- helpers ---
const toArray = (x) => (Array.isArray(x) ? x : []);
const lower = (s) => (typeof s === "string" ? s.toLowerCase() : null);
const normalizeAddr = (s) => (s ? lower(s) : null);
const topicToAddr = (topic) => {
if (typeof topic !== "string") return null;
const hex = topic.startsWith("0x") ? topic.slice(2) : topic;
if (hex.length < 40) return null;
return "0x" + hex.slice(-40).toLowerCase();
};
const isTransferLog = (log) =>
lower(log?.topics?.[0]) === TRANSFER_TOPIC;
try {
const blocks = toArray(stream?.data);
const matchingTransactions = [];
const matchingReceipts = [];
for (const block of blocks) {
const txs = toArray(block?.block?.transactions);
const receipts = toArray(block?.receipts);
// Collect all candidate addresses for this block
const candidates = [];
// tx from/to
for (const tx of txs) {
const f = normalizeAddr(tx?.from);
const t = normalizeAddr(tx?.to);
if (f) candidates.push(f);
if (t) candidates.push(t);
}
// ERC20 Transfer indexed topics (from/to)
for (const receipt of receipts) {
for (const log of toArray(receipt?.logs)) {
if (!isTransferLog(log)) continue;
const fromIdx = topicToAddr(log?.topics?.[1]);
const toIdx = topicToAddr(log?.topics?.[2]);
if (fromIdx) candidates.push(fromIdx);
if (toIdx) candidates.push(toIdx);
}
}
// Batch check once per block
const uniq = Array.from(new Set(candidates));
const batchResults =
uniq.length > 0
? await qnLib.qnContainsListItems(LIST_NAME, uniq)
: [];
const hitMap = new Map(uniq.map((a, i) => [a, !!batchResults[i]]));
// Match transactions
for (const tx of txs) {
const f = normalizeAddr(tx?.from);
const t = normalizeAddr(tx?.to);
if ((f && hitMap.get(f)) || (t && hitMap.get(t))) {
matchingTransactions.push(tx);
}
}
// Match receipts (any matching indexed from/to in logs)
for (const receipt of receipts) {
const logs = toArray(receipt?.logs);
let anyHit = false;
for (const log of logs) {
if (!isTransferLog(log)) continue;
const fromIdx = topicToAddr(log?.topics?.[1]);
const toIdx = topicToAddr(log?.topics?.[2]);
if ((fromIdx && hitMap.get(fromIdx)) || (toIdx && hitMap.get(toIdx))) {
anyHit = true;
break;
}
}
if (anyHit) matchingReceipts.push(receipt);
}
}
if (matchingTransactions.length === 0 && matchingReceipts.length === 0) {
return null;
}
return {
transactions: matchingTransactions,
receipts: matchingReceipts,
};
} catch (e) {
return { error: e?.message || String(e) };
}
}
@gayleQN
Copy link
Author

gayleQN commented Oct 16, 2025

Updated to use qnContainsListItems for bulk matching

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment