Appearance
Getting started
Requirements
- Node.js 20 or newer.
- Viem 2.55.18 or newer in the 2.x release line.
- An EntryPoint v0.7-compatible Viem smart account on Ink Mainnet.
Then choose one mode:
- Native sponsorship requires a sponsorship-enabled API key stored only on a trusted backend.
- Public prepaid mode requires USDC, USD₮0, or USDG in the smart account and a small amount of native ETH for that token's initial approval, as described below.
In prepaid mode, the ERC-20 is reimbursement, not Ink's protocol gas token. The paymaster deposits native ETH into EntryPoint, pays the operation's native gas, and separately charges the smart account in the selected reimbursement token.
Install
Install the published 0.2.2 release or a compatible newer 0.2.x release:
bash
pnpm add @hellomoon/ink-paymaster@^0.2.2 viemThe examples below use USDC, the default reimbursement token. For USD₮0 and USDG selection, allowances, and transfers, see Payment tokens.
Inside this repository, install dependencies and type-check the canonical examples with:
bash
pnpm --dir sdk install
pnpm --dir sdk typecheck:examplesFund the smart account
Send the selected reimbursement token to the smart-account address. Also send it enough native ETH for one owner-paid UserOperation. The ETH is only needed to establish or replenish that token's paymaster allowance; subsequent eligible operations can use the prepaid paymaster.
Do not approve from the owner EOA. The reimbursement tokens belong to the smart account, so the smart account must be the caller of the selected token's approve function.
Approve the paymaster
This is a required first-use transaction. The public paymaster cannot pay for its own approval: before the allowance exists, it has no way to collect reimbursement from the smart account.
Create a Bundler Client without a paymaster and send the approval as an owner-paid UserOperation:
ts
import {createPublicClient, defineChain, http} from "viem";
import {createBundlerClient} from "viem/account-abstraction";
import {
createInkPublicPaymasterClient,
encodePublicPaymasterApproval,
INK_PUBLIC_PAYMASTER,
} from "@hellomoon/ink-paymaster";
// Supply a production-grade Ink RPC from your application's configuration.
const inkRpcUrl = process.env.INK_RPC_URL!;
const ink = defineChain({
id: INK_PUBLIC_PAYMASTER.chainId,
name: INK_PUBLIC_PAYMASTER.chainName,
nativeCurrency: {name: "Ether", symbol: "ETH", decimals: 18},
rpcUrls: {default: {http: [inkRpcUrl]}},
});
const publicClient = createPublicClient({chain: ink, transport: http(inkRpcUrl)});
const service = createInkPublicPaymasterClient();
const ownerPaidBundlerClient = createBundlerClient({
account,
chain: ink,
client: publicClient,
transport: http(service.bundlerUrl),
// Deliberately no `paymaster` here.
});
const approvalHash = await ownerPaidBundlerClient.sendUserOperation({
calls: [{
to: INK_PUBLIC_PAYMASTER.token.address,
value: 0n,
data: encodePublicPaymasterApproval(10_000_000n), // finite 10 USDC allowance
}],
});
await ownerPaidBundlerClient.waitForUserOperationReceipt({hash: approvalHash});The SDK helper encodes the inner USDC.approve(paymaster, amount) call. Viem wraps it in the smart account's execution format and signs the UserOperation. The complete type-checked version is sdk/examples/encode-approval.ts.
Use a finite allowance appropriate for expected usage. Paymaster precharges consume allowance, so monitor and replenish it before it drops below an operation's maxTokenCost.
Check readiness
ts
import {createPublicClient, http} from "viem";
import {
getPublicPaymasterAccountState,
INK_PUBLIC_PAYMASTER,
} from "@hellomoon/ink-paymaster";
const publicClient = createPublicClient({transport: http(process.env.INK_RPC_URL!)});
const state = await getPublicPaymasterAccountState(publicClient, account.address);
console.log({
usdcBalance: state.usdcBalance,
allowance: state.paymasterAllowance,
refundCredit: state.refundCredit,
paymaster: INK_PUBLIC_PAYMASTER.paymaster,
});Do not continue until both usdcBalance and paymasterAllowance cover the maxTokenCost ceiling selected for the operation. USDC uses six decimal places: 100_000n means 0.1 USDC.
Send an operation
The complete, type-checked implementation lives in sdk/examples/happy-path.ts. Its core submission is intentionally small:
ts
const bundlerClient = createBundlerClient({
account,
chain: ink,
client: publicClient,
transport: http(service.bundlerUrl),
paymaster: createInkViemPaymaster(service),
paymasterContext: {
maxTokenCost: 100_000n,
idempotencyKey: `checkout-${orderId}`,
},
});
const hash = await bundlerClient.sendUserOperation({
calls: [{to: destination, data: "0x", value: 0n}],
});
const receipt = await bundlerClient.waitForUserOperationReceipt({hash});Viem asks the smart-account implementation to convert calls into account-specific callData, obtains the prepaid quote, estimates gas, requests the account signature, and submits the final UserOperation to the public Alto route.
Record the result
Persist the business operation ID, idempotency key, UserOperation hash, and EntryPoint transaction hash. Those fields make retries and support investigations much easier.
Example: transfer USDC with paymaster gas
This example transfers 1.25 native Ink USDC. The smart account calls the USDC contract, while the public paymaster supplies native gas through EntryPoint and separately receives a bounded USDC reimbursement.
ts
import {encodeFunctionData, parseUnits} from "viem";
import {INK_PUBLIC_PAYMASTER} from "@hellomoon/ink-paymaster";
const amount = parseUnits("1.25", INK_PUBLIC_PAYMASTER.token.decimals);
const transferData = encodeFunctionData({
abi: [{
type: "function",
name: "transfer",
stateMutability: "nonpayable",
inputs: [
{name: "recipient", type: "address"},
{name: "amount", type: "uint256"},
],
outputs: [{type: "bool"}],
}],
functionName: "transfer",
args: [recipient, amount],
});
const hash = await bundlerClient.sendUserOperation({
calls: [{
to: INK_PUBLIC_PAYMASTER.token.address,
value: 0n,
data: transferData,
}],
});
const receipt = await bundlerClient.waitForUserOperationReceipt({hash});
console.log(receipt.receipt.transactionHash);The account needs enough USDC for both the 1.25 transfer and the selected maxTokenCost ceiling; its paymaster allowance only needs to cover maxTokenCost. The complete type-checked version, including these readiness checks, is sdk/examples/usdc-transfer.ts.