Skip to content

Transaction workflow and errors ​

Every protocol write follows the same validation sequence:

  1. select and verify the deployment;
  2. identify the active account and chain;
  3. read transaction-critical state at one specific current block;
  4. read every precondition and amount from that context;
  5. construct the exact call;
  6. simulate the call against the same block snapshot;
  7. confirm that the block hash used for simulation has not changed;
  8. request one wallet signature;
  9. wait for a confirmed receipt;
  10. verify sender, destination, value and calldata;
  11. decode the expected TABLETS or CLAY event; and
  12. 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:

  • Approval for bounded CLAY approval;
  • SubmissionCreated for submit;
  • BidIncreased for increaseBid;
  • RecordFinalized for finalizeAuction; and
  • RefundWithdrawn for withdrawRefund.
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.