Claim income from a script
A script that claims every asset and every drop for a holder, and a claim the holder signs and anyone sends.
On this page
A holder's income is claimed from the token, in each asset or in all at once, and its drops from the token's drop box. Both can be signed by the holder and sent by anyone else.
In the app
Holders claim on the token's page, https://hookr.fun/revenue/<address>: income one asset at a time or every asset together, and drops from the drop box. Portfolio has Claim all and Sync for each revenue token held.
The calls
claimable(holder, i)for each assetibelowassetCount(), and the drop box'sclaimable(dropId, holder)for each drop below itsdropCount().claimAll(to)pays every asset with something due, andclaim(asset, to)pays one.claimAllreverts as a whole if one asset's payout fails, so fall back toclaimasset by asset. A claim that would pay nothing reverts withNothingToClaim.claim(dropIds, to)on the drop box pays the holder's share of each listed drop, each once.
to can be any address but zero, the token and its drop box, and it must be able to take ETH when an asset is ETH. A claim pays the lesser of what is owed and what the token holds; the rest stays owed.
In code
With client.ts and abis.ts:
import { createWalletClient, http, type Address } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { robinhood } from "viem/chains";
import { client } from "./client";
import { dropBoxAbi, tokenAbi } from "./abis";
const TOKEN: Address = "0x…"; // the revenue token
const holder = privateKeyToAccount(process.env.HOLDER_KEY as `0x${string}`);
const wallet = createWalletClient({ account: holder, chain: robinhood, transport: http() });
const t = { address: TOKEN, abi: tokenAbi } as const;
// Income: every asset with something due, in one call, paid to the holder.
const assets = await client.readContract({ ...t, functionName: "assetCount" });
const due = await Promise.all(
Array.from({ length: Number(assets) }, (_, i) => client.readContract({ ...t, functionName: "claimable", args: [holder.address, BigInt(i)] })),
);
if (due.some((amount) => amount > 0n)) {
const { request } = await client.simulateContract({ ...t, account: holder, functionName: "claimAll", args: [holder.address] });
await client.waitForTransactionReceipt({ hash: await wallet.writeContract(request) });
}
// Drops: every drop this holder has a share of, from the token's drop box, in one call.
const box = await client.readContract({ ...t, functionName: "dropBox" });
const drops = await client.readContract({ address: box, abi: dropBoxAbi, functionName: "dropCount" });
const mine: bigint[] = [];
for (let id = 0n; id < drops; id++) {
const share = await client.readContract({ address: box, abi: dropBoxAbi, functionName: "claimable", args: [id, holder.address] });
if (share > 0n) mine.push(id);
}
if (mine.length > 0) {
const { request } = await client.simulateContract({ address: box, abi: dropBoxAbi, account: holder, functionName: "claim", args: [mine, holder.address] });
await client.waitForTransactionReceipt({ hash: await wallet.writeContract(request) });
}Without gas
The holder signs a ClaimAll under the token's EIP-712 domain, its ERC-20 name, version "1", chain 4663 and its address, and anyone sends claimAllWithSig. The claim pays only the to the holder signed, uses its random nonce once and is refused past its deadline.
import { createWalletClient, http, toHex, type Address } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { robinhood } from "viem/chains";
import { client } from "./client";
import { tokenAbi } from "./abis";
const TOKEN: Address = "0x…"; // the revenue token
const holder = privateKeyToAccount(process.env.HOLDER_KEY as `0x${string}`); // holds no ETH: the relayer pays the gas
// The holder signs a claim of every asset, paid to the address it names and nowhere else.
const nonce = toHex(crypto.getRandomValues(new Uint8Array(32))); // random, used once
const deadline = BigInt(Math.floor(Date.now() / 1000)) + 3_600n;
const signature = await holder.signTypedData({
domain: { name: await client.readContract({ address: TOKEN, abi: tokenAbi, functionName: "name" }), version: "1", chainId: robinhood.id, verifyingContract: TOKEN },
types: {
ClaimAll: [
{ name: "holder", type: "address" },
{ name: "to", type: "address" },
{ name: "nonce", type: "bytes32" },
{ name: "deadline", type: "uint256" },
],
},
primaryType: "ClaimAll",
message: { holder: holder.address, to: holder.address, nonce, deadline },
});
// Anyone sends it and pays the gas: here, a relayer account of your own.
const relayer = privateKeyToAccount(process.env.RELAYER_KEY as `0x${string}`);
const { request } = await client.simulateContract({
account: relayer,
address: TOKEN,
abi: tokenAbi,
functionName: "claimAllWithSig",
args: [holder.address, holder.address, nonce, deadline, signature],
});
const hash = await createWalletClient({ account: relayer, chain: robinhood, transport: http() }).writeContract(request);
await client.waitForTransactionReceipt({ hash });- Hookr runs no relayer: your app, the holder's wallet or any relayer sends the signed call and pays its gas.
- A signed claim that would pay nothing reverts and leaves its nonce unused, so it can be sent again once income is due.
cancelAuthorization, signed by the holder, spends a nonce that should never be used. - One asset is
Claim(holder, asset, to, nonce, deadline)withclaimWithSig. Drops areClaimDrops(holder, dropIds, to, nonce, deadline)with the drop box'sclaimWithSig, under the box's domain: "Hookr Drop Box", version "1", chain 4663 and the box. - The deadline is checked against the chain's clock, and a signature can come from a contract wallet through ERC-1271.