Skip to content

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

  1. claimable(holder, i) for each asset i below assetCount(), and the drop box's claimable(dropId, holder) for each drop below its dropCount().
  2. claimAll(to) pays every asset with something due, and claim(asset, to) pays one. claimAll reverts as a whole if one asset's payout fails, so fall back to claim asset by asset. A claim that would pay nothing reverts with NothingToClaim.
  3. 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) with claimWithSig. Drops are ClaimDrops(holder, dropIds, to, nonce, deadline) with the drop box's claimWithSig, 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.