Starknet Wallet Assets

Monitoring wallet assets in Starknet

Starknet shares many similarities with Ethereum, but great care is needed when it comes to monitoring wallet assets. The function below monitors emitted events and notifies you when any of your wallet's assets change.

Like the EVM version, the function should be set with an address trigger on the address of the wallet (EOA) you want to monitor:

{
    "type": "TRIGGER_TYPE_ADDRESS",
    "address": "0x2e0af29598b407c8716b17f6d2795eca1b471413fa03fb145a5e33722184067"
}
import { Contract, RpcProvider, shortString, uint256 } from "npm:[email protected]";

const providerString = "http://chain-provider";

const transferEventSignature = "0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9";

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

    // An errors list that we will return to notify the user of any issues.
    const errors = [];

    const walletAddress = trigger.address.address;
    if (!isRelevantEvent(data, walletAddress)) {
        return;
    }

    // The token emitted the event.
    const tokenAddress = data.from_address;
    const tokenContact = await getTokenContract(tokenAddress);

    let walletBalance;
    try {
        walletBalance = await queryBalanceOf(tokenContact, walletAddress, Number(context.blockNumber));
    } catch (error) {
        return {
            msg: "Ignoring token without balanceOf",
            context: context,
            tokenAddress: tokenAddress,
            walletAddress: walletAddress,
            error: error,
        };
    }

    const tokenIdentity = await querySymbolAndName(tokenContact, Number(context.blockNumber));
    errors.push(...tokenIdentity.errors);

    return {
        tokenAddress: tokenAddress,
        tokenSymbol: tokenIdentity.symbol,
        tokenName: tokenIdentity.name,

        walletAddress: walletAddress,
        balance: walletBalance,

        context: context,
        errors: errors,
    };
}

function isRelevantEvent(event, walletAddress) {
    // Remove the 0x prefix and leading zeros from the wallet address.
    const sanitizedAddress = walletAddress.toLowerCase().replace("0x", "").replace(/^0+/, "");

    // Check that the event is a transfer event.
    if (event.keys.length < 1 || event.keys[0] !== transferEventSignature) {
        return false;
    }

    // Check that the address is either the sender or the receiver.
    return event.data[0].includes(sanitizedAddress) || event.data[1].includes(sanitizedAddress);
}

async function getTokenContract(tokenAddress) {
    const provider = new RpcProvider({ nodeUrl: providerString });

    const exampleTokenInstance = "0x0637368573bcc403975c13d736d7b044134d520ae3475579c548e00ab45dfdac";
    const tokenAbi = await provider.getClassAt(exampleTokenInstance, "latest").then((result) => result.abi);

    return await new Contract(tokenAbi, tokenAddress, provider);
}

async function queryBalanceOf(tokenContract, walletAddress, blockNumber) {
    return await tokenContract
        .balanceOf(walletAddress, { blockIdentifier: blockNumber })
        .then((result) => "0x" + uint256.uint256ToBN(result.balance).toString(16));
}

async function querySymbolAndName(tokenContract, blockNumber) {
    const errors = [];
    let symbol;
    try {
        symbol = await tokenContract
            .symbol({ blockIdentifier: blockNumber })
            .then((result) => shortString.decodeShortString(result.symbol));
    } catch (error) {
        symbol = "Unknown";
        errors.push(error);
    }

    let name;
    try {
        name = await tokenContract
            .name({ blockIdentifier: blockNumber })
            .then((result) => shortString.decodeShortString(result.name));
    } catch (error) {
        name = "Unknown";
        errors.push(error);
    }

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