Appearance
Transaction workflow and errors
Every protocol write follows the same validation sequence:
- select and verify the deployment;
- identify the active account and chain;
- read transaction-critical state at one specific current block;
- read every precondition and amount from that context;
- construct the exact call;
- simulate the call against the same block snapshot;
- confirm that the block hash used for simulation has not changed;
- request one wallet signature;
- wait for a confirmed receipt;
- verify sender, destination, value and calldata;
- decode the expected TABLETS or CLAY event; and
- re-read state at the confirmed receipt block when the operation requires a postcondition check.
A CLAY approval and a TABLETS write are separate transactions. After an approval is confirmed, refresh every auction fact that could change the required protocol call. Do not carry a stale bid or increase amount into the second transaction.
Handling simulations and custom errors
A failed simulation must stop the flow before signature. Decode the custom error and its arguments with the published ABI, then classify the result:
- refresh state: the leader, minimum, author bid, cutoff or finalization state changed;
- correct input: content, identifier, byte limit or image-reference validation failed;
- change account or authorization: the caller is not the submission author or authorized upgrade account;
- wait: the auction has not ended or refund eligibility is not yet available; or
- stop: the operation is already completed, upgrades are frozen, the Record already exists or another terminal invariant applies.
Do not retry a reverted call automatically. Read the latest contract state and construct a new transaction only after addressing the reported error.
Receipt and event validation
A transaction hash is not proof of success. Confirm the receipt status, destination and input, then require the event expected for the operation:
Approvalfor bounded CLAY approval;SubmissionCreatedforsubmit;BidIncreasedforincreaseBid;RecordFinalizedforfinalizeAuction; andRefundWithdrawnforwithdrawRefund.
ts
export async function waitForExactTransaction(
hash: Hash,
expected: ExactWrite,
): Promise<TransactionReceipt> {
const receipt = await publicClient.waitForTransactionReceipt({
hash,
confirmations: confirmationPolicy.receiptConfirmations,
});
invariant(receipt.status === "success", "Transaction reverted");
invariant(
receipt.to && getAddress(receipt.to) === expected.to,
"Receipt destination mismatch",
);
const transaction = await publicClient.getTransaction({ hash });
invariant(
getAddress(transaction.from) === expected.account,
"Sender mismatch",
);
invariant(
transaction.to && getAddress(transaction.to) === expected.to,
"Transaction destination mismatch",
);
invariant(transaction.value === 0n, "Unexpected native value");
invariant(
transaction.input.toLowerCase() === expected.input.toLowerCase(),
"Transaction input mismatch",
);
return receipt;
}
export function contentHash(
imageURI: string,
title: string,
description: string,
): Hex {
return keccak256(
encodeAbiParameters(
[{ type: "string" }, { type: "string" }, { type: "string" }],
[imageURI, title, description],
),
);
}
function revertData(reason: unknown): Hex | null {
if (typeof reason !== "object" || reason === null) return null;
const direct = (reason as { data?: unknown }).data;
if (typeof direct === "string" && /^0x[0-9a-f]+$/iu.test(direct)) {
return direct as Hex;
}
const cause = (reason as { cause?: unknown }).cause;
if (typeof cause === "object" && cause !== null) {
const nested = (cause as { data?: unknown }).data;
if (typeof nested === "string" && /^0x[0-9a-f]+$/iu.test(nested)) {
return nested as Hex;
}
}
return null;
}
export function explainTabletsError(reason: unknown): string {
const data = revertData(reason);
if (!data) return "The TABLETS call failed before a transaction was sent.";
try {
const decoded = decodeErrorResult({ abi: tabletsAbi, data });
switch (decoded.errorName) {
case "BidBelowRequiredTotal":
return "The supplied total no longer leads. Read current state and rebuild the transaction.";
case "BidIncreaseBelowRequiredDelta":
return "The supplied increase no longer leads. Read current state and rebuild the transaction.";
case "SubmissionAuctionEnded":
return "Entries are closed for this auction.";
case "AuctionNotEnded":
return "The auction has not reached its cutoff.";
case "AuctionAlreadyFinalized":
return "The auction is already finalized.";
case "NotSubmissionOwner":
return "Only the Entry author can increase it.";
case "RefundNotAvailable":
return "This refund is not currently available.";
case "WinningContentHashMismatch":
return "The supplied winning content does not match the stored commitment.";
default:
return `TABLETS rejected the call with ${decoded.errorName}.`;
}
} catch {
return "TABLETS rejected the call with an unrecognized custom error.";
}
}The full client reference contains the TypeScript reference helper, including the error-decoding, receipt and event-validation functions described above.