Skip to content

Submit ​

The exact contract function is:

solidity
submit(
  string imageURI,
  string title,
  string description,
  uint256 initialBid
)

Each address may submit once for a represented day. The submission contains a title, description and image reference and is backed with CLAY.

Before calling submit:

  1. verify that the auction is open and not finalized;
  2. verify that the author has not already submitted;
  3. validate the exact UTF-8 byte limits and image-reference rules;
  4. read the protocol minimum and current leader from the same current block;
  5. derive the required bid total using the exact contract rule;
  6. verify CLAY balance and allowance;
  7. if required, approve only the bounded CLAY amount to the TABLETS proxy;
  8. refresh auction state after approval;
  9. simulate the TABLETS call; and
  10. request the signature only if the simulation succeeds and the auction state used to construct the call has not changed.

The CLAY approval is a call to the CLAY token contract. It is not the TABLETS NFT function approve(address,uint256).

submit accepts an initial bid at or above the required total. The example uses the minimum required amount to limit the CLAY approval and escrowed amount.

Example: approve bounded CLAY and submit ​

ts
const submitted = await submitEntry({
  imageURI: "ipfs://bafy...",
  title: "A title encoded as UTF-8",
  description: "An example entry description.",
});
ts
import {
  encodeFunctionData,
  getAddress,
  parseEventLogs,
  type Hash,
} from "viem";

import { tabletsAbi } from "@tablets/abi";

import {
  readCurrentAuction,
  type CurrentAuctionQuote,
} from "./read-current-auction.js";
import {
  TABLETS_ADDRESS,
  assertPin,
  contentHash,
  ensureBoundedAllowance,
  getActiveAccount,
  invariant,
  only,
  publicClient,
  readTokenFunds,
  validateEntryContent,
  waitForExactTransaction,
  walletClient,
  type EntryContent,
} from "./tablets-client.js";

export type SubmittedEntry = {
  readonly transactionHash: Hash;
  readonly approvalHash: Hash | null;
  readonly blockNumber: bigint;
  readonly auctionId: bigint;
  readonly submissionId: bigint;
  readonly contentHash: Hash;
  readonly initialBid: bigint;
};

function sameSubmitFacts(
  before: CurrentAuctionQuote,
  after: CurrentAuctionQuote,
): boolean {
  return (
    before.auctionId === after.auctionId &&
    before.cutoff === after.cutoff &&
    before.minimumBid === after.minimumBid &&
    before.leaderSubmissionId === after.leaderSubmissionId &&
    before.leaderBid === after.leaderBid &&
    before.accountSubmissionId === after.accountSubmissionId &&
    before.requiredSubmitTotal === after.requiredSubmitTotal
  );
}

export async function submitEntry(
  content: EntryContent,
): Promise<SubmittedEntry> {
  validateEntryContent(content);
  const account = await getActiveAccount();

  const quote = await readCurrentAuction(account);
  invariant(quote.canSubmit, "This account already submitted today");
  const requiredInitialBid = quote.requiredSubmitTotal;

  const approvalHash = await ensureBoundedAllowance(
    account,
    requiredInitialBid,
  );

  // Approval mined in a separate transaction, so refresh every contract fact.
  const refreshed = await readCurrentAuction(account);
  invariant(
    sameSubmitFacts(quote, refreshed),
    "Auction state changed; refresh the quote before continuing",
  );
  const funds = await readTokenFunds(
    account,
    requiredInitialBid,
    refreshed.block,
  );
  invariant(
    requiredInitialBid === 0n || funds.allowance >= requiredInitialBid,
    "Bounded allowance is no longer sufficient",
  );

  const { request } = await publicClient.simulateContract({
    address: TABLETS_ADDRESS,
    abi: tabletsAbi,
    functionName: "submit",
    args: [
      content.imageURI,
      content.title,
      content.description,
      requiredInitialBid,
    ],
    account,
    blockNumber: refreshed.block.number,
  });
  await assertPin(refreshed.block);

  const input = encodeFunctionData({
    abi: tabletsAbi,
    functionName: "submit",
    args: [
      content.imageURI,
      content.title,
      content.description,
      requiredInitialBid,
    ],
  });
  const transactionHash = await walletClient.writeContract(request);
  const receipt = await waitForExactTransaction(transactionHash, {
    account,
    to: TABLETS_ADDRESS,
    input,
  });

  const event = only(
    parseEventLogs({
      abi: tabletsAbi,
      eventName: "SubmissionCreated",
      logs: receipt.logs.filter(
        (log) => getAddress(log.address) === TABLETS_ADDRESS,
      ),
      strict: true,
    }),
    "SubmissionCreated event",
  );
  const expectedContentHash = contentHash(
    content.imageURI,
    content.title,
    content.description,
  );
  invariant(
    event.args.auctionId === refreshed.auctionId &&
      getAddress(event.args.submitter) === account &&
      event.args.contentHash.toLowerCase() ===
        expectedContentHash.toLowerCase() &&
      event.args.imageURI === content.imageURI &&
      event.args.title === content.title &&
      event.args.description === content.description &&
      event.args.initialBid === requiredInitialBid,
    "SubmissionCreated event mismatch",
  );

  const stored = await publicClient.readContract({
    address: TABLETS_ADDRESS,
    abi: tabletsAbi,
    functionName: "getSubmission",
    args: [event.args.submissionId],
    blockNumber: receipt.blockNumber,
  });
  invariant(
    stored.auctionId === refreshed.auctionId &&
      getAddress(stored.submitter) === account &&
      stored.contentHash.toLowerCase() === expectedContentHash.toLowerCase() &&
      stored.bidAmount === requiredInitialBid &&
      !stored.refundWithdrawn,
    "Receipt-pinned submission state mismatch",
  );

  return {
    transactionHash,
    approvalHash,
    blockNumber: receipt.blockNumber,
    auctionId: event.args.auctionId,
    submissionId: event.args.submissionId,
    contentHash: expectedContentHash,
    initialBid: event.args.initialBid,
  };
}

After confirmation, decode SubmissionCreated and record the returned submission ID. A transaction hash alone is not proof that the submission succeeded.