Auto Claim

This is an auto-claim demo using Anchor in Node.js, you can refer to it and expand upon it.

import * as dotenv from 'dotenv';
import {
    Connection,
    Keypair,
    PublicKey,
} from '@solana/web3.js';
import bs58 from 'bs58';
import {
    TOKEN_2022_PROGRAM_ID,
} from '@solana/spl-token';
import { AnchorProvider, Program, Wallet } from '@coral-xyz/anchor';
import idl from './claim.json' with { type: 'json' };
import { PROGRAM_ID } from './utils/constants.js';
import BN from "bn.js";
import { delay, formatBNAmount } from './utils/tools.js';
import { userClaim } from './z_send_user_claim.js';

dotenv.config();

const connection = new Connection(process.env.CLAIM_RPC_URL, { commitment: "confirmed" });

export async function claim() {
    // Important Warning: If your wallet's private key is exposed in any way, all your assets will be lost.
    // .env file content like this: SOLANA_PRIVATE_KEYS=private_key1,private_key2,private_key3...
    const privateKeysString = process.env.SOLANA_PRIVATE_KEYS;
    const privateKeys = privateKeysString.split(',').map(key => key.trim());

    // Build an anchor program for each wallet
    let walletPrograms = [];
    for (const privateKey of privateKeys) {
        try {
            const keypair = Keypair.fromSecretKey(bs58.decode(privateKey));
            const wallet = new Wallet(keypair);
            const provider = new AnchorProvider(connection, wallet, { commitment: "confirmed" });
            const program = new Program(idl, provider);
            walletPrograms.push({ wallet, program });
        } catch (error) {
            console.error(`Error processing key: ${privateKey}`, error);
        }
    }

    // Concurrent processing of each wallet
    await Promise.all(walletPrograms.map(async (walletProgram) => {
        await startMonitoring(walletProgram);
    }));
}

// Start monitoring and claims for a wallet
async function startMonitoring(walletProgram) {
    const walletPubkey = walletProgram.wallet.publicKey;
    console.log(`Starting monitoring for wallet: ${walletPubkey.toString()}`);

    // Maps and arrays for state
    const currentTokens = new Map(); // mint => info
    const abortFunctions = new Map(); // mint => abort
    let subscriptionId; // Only one subscription for wallet

    const triggerUpdate = () => {
        updateHoldings().then(() => {
        }).catch(error => {
            console.error('Error in updateHoldings:', error);
        });
    };

    const updateHoldings = async () => {
        try {
            const newTokensList = await fetchUserHoldTokens(connection, walletPubkey);
            const newTokens = new Map(newTokensList.map(t => [t.mintAddress, { tokenAccount: t.tokenAccount, uiAmount: t.uiAmount }]));

            // Find removed mints
            for (const mint of currentTokens.keys()) {
                if (!newTokens.has(mint)) {
                    const abort = abortFunctions.get(mint);
                    if (abort) {
                        abort();
                        abortFunctions.delete(mint);
                        console.log(`Delete claim for wallet: ${walletPubkey.toString()}, token: ${mint}`);
                    }
                }
            }

            // Find new mints
            for (const [mint, info] of newTokens) {
                if (!currentTokens.has(mint)) {
                    // Start new claim loop
                    console.log(`Starting new claim for wallet: ${walletPubkey.toString()}, token: ${mint}`);
                    const mintPubkey = new PublicKey(mint);
                    const [claimAuth] = PublicKey.findProgramAddressSync(
                        [Buffer.from("auth_seed"), mintPubkey.toBuffer()],
                        PROGRAM_ID
                    );
                    const claimAuthorityInfo = await walletProgram.program.account.claimAuthority.getAccountInfo(claimAuth);
                    if (claimAuthorityInfo) {
                        const claimAuthorityData = walletProgram.program.coder.accounts.decode('claimAuthority', claimAuthorityInfo.data);
                        const abort = await claimOne(walletProgram, claimAuthorityData, info.tokenAccount);
                        abortFunctions.set(mint, abort);
                    } else {
                        console.log(`No claim authority for token: ${mint}`);
                    }
                }
            }

            // Update current
            currentTokens.clear();
            for (const [k, v] of newTokens) {
                currentTokens.set(k, v);
            }
        } catch (error) {
            console.error(`Error updating holdings for wallet: ${walletPubkey.toString()}`, error);
        }
    };

    // Initial setup
    await updateHoldings();

    // Subscribe only to wallet main account
    subscriptionId = connection.onAccountChange(walletPubkey, triggerUpdate, { commitment: 'confirmed' });

    // Cleanup on exit (optional, for long-running process)
    process.on('SIGINT', () => {
        if (subscriptionId !== undefined) {
            connection.removeAccountChangeListener(subscriptionId);
        }
        process.exit(0);
    });
}

// Check which token-2022 your wallet holds in.
const fetchUserHoldTokens = async (connection, walletPublicKey) => {
    const tokenAccounts = await connection.getParsedTokenAccountsByOwner(
        walletPublicKey,
        {
            programId: TOKEN_2022_PROGRAM_ID,
        },
        'confirmed'
    );

    // Use Map to store the account with the maximum uiAmount for each mint
    const mintMap = new Map();

    tokenAccounts.value.forEach((account) => {
        const { mint, tokenAmount } = account.account.data.parsed.info;
        const uiAmount = tokenAmount.uiAmount;

        if (uiAmount > 100000) {
            const current = mintMap.get(mint);
            if (!current || uiAmount > current.uiAmount) {
                mintMap.set(mint, {
                    tokenAccount: account.pubkey,
                    mintAddress: mint,
                    uiAmount: uiAmount
                });
            }
        }
    });

    return Array.from(mintMap.values());
};

// claim transaction
const claimOne = async (walletProgram, claimAuthorityData, tokenAccount) => {
    let aborted = false;
    const controller = new AbortController();
    const abort = () => { controller.abort(); aborted = true; };

    (async () => {
        let isFirst = true;
        while (!aborted) {
            try {
                // This PDA stores the data from the user's claim. It is created when the user first claims. 
                // You can close it by calling the `close_user_state` directive, thus releasing the SOL, 
                // but this must be done after `claim_at + claim_interval`.
                const [userState] = PublicKey.findProgramAddressSync(
                    [
                        Buffer.from("user_state_seed"),
                        walletProgram.wallet.publicKey.toBuffer(),
                        claimAuthorityData.mint.toBuffer()  // This is the same token address
                    ],
                    PROGRAM_ID
                );
                const userStateInfo = await walletProgram.program.account.userState.getAccountInfo(userState);
                if (userStateInfo) {
                    const userStateData = walletProgram.program.coder.accounts.decode('userState', userStateInfo.data);
                    // This only prints some prompts.
                    if (!isFirst) {
                        if (userStateData.claimed && userStateData.claimAmount.gt(new BN(0))) {
                            console.log(`Claim ${formatBNAmount(userStateData.claimAmount, claimAuthorityData.claimMintDecimals)} ${claimAuthorityData.claimFeedSymbol}. wallet: ${walletProgram.wallet.publicKey.toString()}, token: ${claimAuthorityData.mint.toString()}`)
                        } else {
                            console.log(`Claim nothing. wallet: ${walletProgram.wallet.publicKey.toString()}, token: ${claimAuthorityData.mint.toString()}`)
                        }
                    }

                    const now = Math.floor(Date.now() / 1000);
                    const nextClaimTime = userStateData.validAt.toNumber();
                    const waitTime = nextClaimTime - now;
                    if (waitTime > 0) {
                        await delay(waitTime + 1, controller.signal).catch(() => { });
                        if (aborted) break;
                    }
                }

                await userClaim(connection, walletProgram, claimAuthorityData, tokenAccount);
                isFirst = false;
            } catch (error) {
                console.error('Error in claimOne loop:', error);
                await delay(1);
                if (aborted) break;
            }
        }
    })();

    return abort;
}

claim().catch(console.error);

Last updated