Interacting with smart contracts
Reading contract state
Reads go straight to Arc and need no wallet:
const total = await publicClient.readContract({
address: CONTRACT,
abi,
functionName: "totalSupply",
});
Batch several reads with multicall:
const results = await publicClient.multicall({
contracts: [
{ address: CONTRACT, abi, functionName: "price" },
{ address: CONTRACT, abi, functionName: "stock" },
],
});
Writing to contracts
const hash = await walletClient.writeContract({
account,
chain: walletClient.chain,
address: CONTRACT,
abi,
functionName: "pay",
args: [orderId],
value: parseEther("1.50"), // native USDC, 18 decimals
});
The user sees a confirmation in CrackPay and approves with their passkey. The promise resolves once the transaction is final; a revert rejects it. Gas is sponsored, so pass no gas options.
Design payments for one confirmation
Prefer a payable function that takes native USDC:
function pay(bytes32 orderId) external payable {
// msg.value is USDC with 18 decimals
}
One transaction, one confirmation, and CrackPay shows the exact amount leaving the user's balance.
Taking tokens
For EURC, or anything built on transferFrom, send approve then your call, and
await each. Approve the exact amount, never an unlimited one. A listed app may only
ask a token to approve one of its own listed contracts. See
Send a transaction.
Contract events
Your events are in the receipt's logs:
import { parseEventLogs } from "viem";
const receipt = await publicClient.getTransactionReceipt({ hash });
const events = parseEventLogs({ abi, logs: receipt.logs, eventName: "Paid" });
The receipt's from is a bundler and its to is the ERC-4337 EntryPoint, not
your contract. That is how account abstraction works.
Contracts that accept CrackPay users
Your contract's caller is a smart contract account. It must:
- Not require
tx.origin == msg.sender. - Not reject callers that have code (
extcodesize,isContract). - Not depend on an ECDSA signature from the user. Use
msg.sender. - Treat
msg.valueas 18-decimal USDC and never mix it with a 6-decimalbalanceOfin arithmetic. - Expect sends to the zero address to revert on Arc.
Contract address management
Keep addresses in one config module, per network:
export const contracts = {
5042: { shop: "0x…" }, // Arc mainnet
5042002: { shop: "0x…" }, // Arc Testnet
} as const;
For listing, every contract your app sends transactions to must be named in your listing and verified on the Arc explorer (mainnet, testnet) for the network you list on.
Gas estimation
You do not need it. CrackPay sponsors gas and ignores gas fields.