EVM Wallet Assets

Monitoring wallet assets using contract storage parsing

This function uses SendBlocks storage parsing to notify you when any of your wallet's assets change without relying on the contract emitting an event. This is useful for monitoring the balance of a wallet holding a token that doesn't emit a Transfer event when the balance changes.

Note that storage changes are many and varied and not all of them are relevant to your use case. The function below needs to sift out the changes to variables that don't map to changes in the assets your wallet is holding. After sifting, the function returns both the raw values in storage and the balance returned by the contract's balanceOf function. This way, you can have maximum flexibility in processing the current and previous balance data.

This function should be run with an address trigger on the address of the wallet (EOA) you want to monitor. Take the ENS Deployer address as an example:

{
    "type": "TRIGGER_TYPE_ADDRESS",
    "address": "0x283af0b28c62c092c9727f1ee09c02ca627eb7f5"
}
import { ethers } from "https://cdn.skypack.dev/[email protected]";

// This function should be created with a trigger of type "Address" and a wallet's address as the trigger data.
export async function triggerHandler(context, data) {
    if (context.dataType !== "DATA_TYPE_STORAGE_ACCESS") {
        return;
    }

    const walletAddress = context.trigger.address.address;
    // This is an imperfect way to filter out irrelevant storage accesses.
    if (!isRelevantStorageAccess(data, walletAddress)) {
        return;
    }

    // Extract the relevant token address from the storage access.
    const tokenAddress = data.storageAddress;
    const tokenContact = await getTokenContract(tokenAddress);

    let storageDiff = BigInt(data.finalValue) - BigInt(data.initialValue);
    storageDiff = storageDiff < 0 ? -storageDiff : storageDiff;

    let walletBalance;
    try {
        walletBalance = await tokenContact
            .balanceOf(walletAddress, {
                blockTag: context.blockNumber,
            })
            .then((result) => result.toHexString());
    } catch {
        walletBalance = "Unknown";
    }

    const tokenIdentity = await queryTokenInfo(tokenContact);
    return {
        tokenAddress: tokenAddress,
        tokenSymbol: tokenIdentity.symbol,
        tokenName: tokenIdentity.name,
        tokenDecimals: tokenIdentity.decimals,

        walletAddress: walletAddress,
        initialStorage: data.initialValue,
        finalStorage: data.finalValue,
        storageDiff: "0x" + storageDiff.toString(16),

        balance: walletBalance,

        context: context,
    };
}

function isRelevantStorageAccess(data, walletAddress) {
    // Lowercase the address to make sure the address isn't ERC55 encoded.
    // Remove the "0x" prefix to make sure the address can show up inside the data.
    const sanitizedAddress = walletAddress.toLowerCase().replace("0x", "");

    // Check that the address is of type mapping.
    if (data.type !== "mapping") {
        return false;
    }

    // Check that the number of keys is 1 (otherwise this is unlikely to be a balance mapping).
    if (data.keys.length !== 1) {
        return false;
    }

    // Check that the address is the key of the mapping.
    return data.keys[0].includes(sanitizedAddress);
}

async function getTokenContract(tokenAddress) {
    const providerString = "http://chain-provider";
    const provider = new ethers.providers.JsonRpcProvider(providerString);

    const erc20Abi = [
        "function balanceOf(address owner) view returns (uint256)",
        "function symbol() view returns (string)",
        "function name() view returns (string)",
        "function decimals() view returns (uint8)",
    ];
    return new ethers.Contract(tokenAddress, erc20Abi, provider);
}

async function queryTokenInfo(tokenContract) {
    let symbol;
    try {
        symbol = await tokenContract.symbol();
    } catch (error) {
        symbol = "Unknown";
    }

    let name;
    try {
        name = await tokenContract.name();
    } catch (error) {
        name = "Unknown";
    }

    let decimals;
    try {
        decimals = await tokenContract.decimals();
    } catch (error) {
        decimals = "Unknown";
    }

    return {
        symbol: symbol,
        name: name,
        decimals: decimals,
    };
}