Skip to content

Finalize an auction ​

The exact contract function is:

solidity
finalizeAuction(
  uint256 auctionId,
  string imageURI,
  string title,
  string description
)

Any account may finalize an ended auction that has not already been finalized. Finalization may occur later and out of order; the resulting Tablet still represents the contract-derived day.

Auction with a winning submission ​

For a non-empty auction, recover the winning content from confirmed SubmissionCreated events emitted by the deployed TABLETS contract. Verify the content against the winning submission and its content hash before simulation.

On successful finalization:

  • the winning content becomes the Record;
  • the numbered Tablet NFT is minted to the original author; and
  • the winning CLAY is sent to the configured burn address.

Example: finalize an auction with a winner ​

ts
const finalizedWinner = await finalizeWinner(12n);
ts
import {
  encodeFunctionData,
  getAddress,
  parseEventLogs,
  type Address,
  type Hash,
} from "viem";

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

import {
  DEPLOYMENT_BLOCK,
  TABLETS_ADDRESS,
  assertPin,
  contentHash,
  getActiveAccount,
  invariant,
  only,
  pinConfirmedBlock,
  pinLatestBlock,
  publicClient,
  waitForExactTransaction,
  walletClient,
  type EntryContent,
} from "./tablets-client.js";

export type FinalizedWinner = {
  readonly transactionHash: Hash;
  readonly blockNumber: bigint;
  readonly auctionId: bigint;
  readonly submissionId: bigint;
  readonly recipient: Address;
  readonly winningBid: bigint;
  readonly content: EntryContent;
  readonly submissionTransactionHash: Hash;
};

export async function finalizeWinner(
  auctionId: bigint,
): Promise<FinalizedWinner> {
  invariant(auctionId > 0n, "auctionId must be positive");
  const account = await getActiveAccount();

  // Recover content from a sufficiently confirmed block.
  const confirmedPin = await pinConfirmedBlock();
  const [confirmedCurrentAuctionId, confirmedAuction] = await Promise.all([
    publicClient.readContract({
      address: TABLETS_ADDRESS,
      abi: tabletsAbi,
      functionName: "currentAuctionId",
      blockNumber: confirmedPin.number,
    }),
    publicClient.readContract({
      address: TABLETS_ADDRESS,
      abi: tabletsAbi,
      functionName: "getAuction",
      args: [auctionId],
      blockNumber: confirmedPin.number,
    }),
  ]);
  invariant(
    auctionId < confirmedCurrentAuctionId && !confirmedAuction.finalized,
    "Auction is not confirmed-ended and unfinalized",
  );
  invariant(
    confirmedAuction.leaderSubmissionId !== 0n,
    "Auction has no winning entry; use the empty-finalization path",
  );

  const submission = await publicClient.readContract({
    address: TABLETS_ADDRESS,
    abi: tabletsAbi,
    functionName: "getSubmission",
    args: [confirmedAuction.leaderSubmissionId],
    blockNumber: confirmedPin.number,
  });
  invariant(
    submission.auctionId === auctionId &&
      submission.bidAmount === confirmedAuction.leaderBid,
    "Leader submission state is inconsistent",
  );

  const created = only(
    await publicClient.getContractEvents({
      address: TABLETS_ADDRESS,
      abi: tabletsAbi,
      eventName: "SubmissionCreated",
      args: {
        auctionId,
        submissionId: confirmedAuction.leaderSubmissionId,
      },
      fromBlock: DEPLOYMENT_BLOCK,
      toBlock: confirmedPin.number,
      strict: true,
    }),
    "confirmed SubmissionCreated provenance event",
  );
  invariant(
    created.blockHash &&
      created.transactionHash &&
      created.removed !== true &&
      created.args.auctionId === auctionId &&
      created.args.submissionId === confirmedAuction.leaderSubmissionId &&
      getAddress(created.args.submitter) === getAddress(submission.submitter),
    "SubmissionCreated provenance identity mismatch",
  );
  const sourceReceipt = await publicClient.getTransactionReceipt({
    hash: created.transactionHash,
  });
  invariant(
    sourceReceipt.status === "success" &&
      sourceReceipt.blockHash.toLowerCase() ===
        created.blockHash.toLowerCase() &&
      sourceReceipt.to !== null &&
      getAddress(sourceReceipt.to) === TABLETS_ADDRESS &&
      sourceReceipt.logs.some(
        (log) =>
          getAddress(log.address) === TABLETS_ADDRESS &&
          log.logIndex === created.logIndex,
      ),
    "SubmissionCreated receipt provenance mismatch",
  );

  const content = {
    imageURI: created.args.imageURI,
    title: created.args.title,
    description: created.args.description,
  } as const;
  const recoveredHash = contentHash(
    content.imageURI,
    content.title,
    content.description,
  );
  invariant(
    recoveredHash.toLowerCase() === created.args.contentHash.toLowerCase() &&
      recoveredHash.toLowerCase() === submission.contentHash.toLowerCase(),
    "Recovered winning content does not match its onchain commitment",
  );
  await assertPin(confirmedPin);

  // Refresh the mutable auction facts immediately before simulation.
  const freshPin = await pinLatestBlock();
  const [currentAuctionId, auction, freshSubmission] = await Promise.all([
    publicClient.readContract({
      address: TABLETS_ADDRESS,
      abi: tabletsAbi,
      functionName: "currentAuctionId",
      blockNumber: freshPin.number,
    }),
    publicClient.readContract({
      address: TABLETS_ADDRESS,
      abi: tabletsAbi,
      functionName: "getAuction",
      args: [auctionId],
      blockNumber: freshPin.number,
    }),
    publicClient.readContract({
      address: TABLETS_ADDRESS,
      abi: tabletsAbi,
      functionName: "getSubmission",
      args: [confirmedAuction.leaderSubmissionId],
      blockNumber: freshPin.number,
    }),
  ]);
  invariant(
    auctionId < currentAuctionId &&
      !auction.finalized &&
      auction.leaderSubmissionId === confirmedAuction.leaderSubmissionId &&
      auction.leaderBid === confirmedAuction.leaderBid &&
      freshSubmission.contentHash.toLowerCase() === recoveredHash.toLowerCase(),
    "Winner or auction state changed; refresh state and rebuild the finalization transaction",
  );

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

  const input = encodeFunctionData({
    abi: tabletsAbi,
    functionName: "finalizeAuction",
    args: [auctionId, content.imageURI, content.title, content.description],
  });
  const transactionHash = await walletClient.writeContract(request);
  const receipt = await waitForExactTransaction(transactionHash, {
    account,
    to: TABLETS_ADDRESS,
    input,
  });
  const finalized = only(
    parseEventLogs({
      abi: tabletsAbi,
      eventName: "RecordFinalized",
      logs: receipt.logs.filter(
        (log) => getAddress(log.address) === TABLETS_ADDRESS,
      ),
      strict: true,
    }),
    "RecordFinalized event",
  );
  invariant(
    finalized.args.auctionId === auctionId &&
      finalized.args.submissionId === auction.leaderSubmissionId &&
      getAddress(finalized.args.recipient) ===
        getAddress(submission.submitter) &&
      finalized.args.kind === 2 &&
      getAddress(finalized.args.finalizer) === account &&
      finalized.args.winningBid === auction.leaderBid,
    "RecordFinalized winner event mismatch",
  );

  const [record, owner] = await Promise.all([
    publicClient.readContract({
      address: TABLETS_ADDRESS,
      abi: tabletsAbi,
      functionName: "getRecord",
      args: [auctionId],
      blockNumber: receipt.blockNumber,
    }),
    publicClient.readContract({
      address: TABLETS_ADDRESS,
      abi: tabletsAbi,
      functionName: "ownerOf",
      args: [auctionId],
      blockNumber: receipt.blockNumber,
    }),
  ]);
  invariant(
    record.kind === 2 &&
      record.submissionId === auction.leaderSubmissionId &&
      record.imageURI === content.imageURI &&
      record.title === content.title &&
      record.description === content.description &&
      getAddress(owner) === getAddress(submission.submitter),
    "Receipt-pinned winning Record mismatch",
  );

  return {
    transactionHash,
    blockNumber: receipt.blockNumber,
    auctionId,
    submissionId: finalized.args.submissionId,
    recipient: getAddress(finalized.args.recipient),
    winningBid: finalized.args.winningBid,
    content,
    submissionTransactionHash: created.transactionHash,
  };
}

Auction with no submissions ​

An ended auction with no submissions can also be finalized. The finalizer must use the exact empty-auction arguments required by the contract. The contract creates the configured empty Record and mints the Tablet to the finalization caller.

Example: finalize an empty auction ​

ts
const finalizedEmpty = await finalizeEmptyAuction(13n);
ts
import {
  encodeFunctionData,
  getAddress,
  parseEventLogs,
  type Address,
  type Hash,
} from "viem";

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

import {
  DEPLOYMENT_BLOCK,
  TABLETS_ADDRESS,
  assertPin,
  getActiveAccount,
  invariant,
  only,
  pinConfirmedBlock,
  pinLatestBlock,
  publicClient,
  waitForExactTransaction,
  walletClient,
  type EntryContent,
} from "./tablets-client.js";

export type FinalizedEmptyAuction = {
  readonly transactionHash: Hash;
  readonly blockNumber: bigint;
  readonly auctionId: bigint;
  readonly recipient: Address;
  readonly content: EntryContent;
};

export async function finalizeEmptyAuction(
  auctionId: bigint,
): Promise<FinalizedEmptyAuction> {
  invariant(auctionId > 0n, "auctionId must be positive");
  const account = await getActiveAccount();

  const confirmedPin = await pinConfirmedBlock();
  const [confirmedCurrentAuctionId, confirmedAuction, submissions] =
    await Promise.all([
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "currentAuctionId",
        blockNumber: confirmedPin.number,
      }),
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "getAuction",
        args: [auctionId],
        blockNumber: confirmedPin.number,
      }),
      publicClient.getContractEvents({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        eventName: "SubmissionCreated",
        args: { auctionId },
        fromBlock: DEPLOYMENT_BLOCK,
        toBlock: confirmedPin.number,
        strict: true,
      }),
    ]);
  invariant(
    auctionId < confirmedCurrentAuctionId &&
      !confirmedAuction.finalized &&
      confirmedAuction.leaderSubmissionId === 0n &&
      confirmedAuction.leaderBid === 0n &&
      submissions.length === 0,
    "Auction is not proven confirmed-ended, unfinalized, and empty",
  );
  await assertPin(confirmedPin);

  const freshPin = await pinLatestBlock();
  const [currentAuctionId, auction, emptyImageURI, emptyTitle, prefix, suffix] =
    await Promise.all([
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "currentAuctionId",
        blockNumber: freshPin.number,
      }),
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "getAuction",
        args: [auctionId],
        blockNumber: freshPin.number,
      }),
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "emptyImageURI",
        blockNumber: freshPin.number,
      }),
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "emptyTitle",
        blockNumber: freshPin.number,
      }),
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "emptyDescriptionPrefix",
        blockNumber: freshPin.number,
      }),
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "emptyDescriptionSuffix",
        blockNumber: freshPin.number,
      }),
    ]);
  invariant(
    auctionId < currentAuctionId &&
      !auction.finalized &&
      auction.leaderSubmissionId === 0n &&
      auction.leaderBid === 0n,
    "Empty-auction state changed; refresh state and rebuild the finalization transaction",
  );

  // The contract requires exactly these three empty strings for this branch.
  const emptyArgs = [auctionId, "", "", ""] as const;
  const { request } = await publicClient.simulateContract({
    address: TABLETS_ADDRESS,
    abi: tabletsAbi,
    functionName: "finalizeAuction",
    args: emptyArgs,
    account,
    blockNumber: freshPin.number,
  });
  await assertPin(freshPin);

  const input = encodeFunctionData({
    abi: tabletsAbi,
    functionName: "finalizeAuction",
    args: emptyArgs,
  });
  const transactionHash = await walletClient.writeContract(request);
  const receipt = await waitForExactTransaction(transactionHash, {
    account,
    to: TABLETS_ADDRESS,
    input,
  });
  const finalized = only(
    parseEventLogs({
      abi: tabletsAbi,
      eventName: "RecordFinalized",
      logs: receipt.logs.filter(
        (log) => getAddress(log.address) === TABLETS_ADDRESS,
      ),
      strict: true,
    }),
    "empty RecordFinalized event",
  );
  invariant(
    finalized.args.auctionId === auctionId &&
      finalized.args.submissionId === 0n &&
      getAddress(finalized.args.recipient) === account &&
      finalized.args.kind === 3 &&
      getAddress(finalized.args.finalizer) === account &&
      finalized.args.winningBid === 0n,
    "Empty RecordFinalized event mismatch",
  );

  const [record, owner] = await Promise.all([
    publicClient.readContract({
      address: TABLETS_ADDRESS,
      abi: tabletsAbi,
      functionName: "getRecord",
      args: [auctionId],
      blockNumber: receipt.blockNumber,
    }),
    publicClient.readContract({
      address: TABLETS_ADDRESS,
      abi: tabletsAbi,
      functionName: "ownerOf",
      args: [auctionId],
      blockNumber: receipt.blockNumber,
    }),
  ]);
  invariant(
    record.kind === 3 &&
      record.submissionId === 0n &&
      record.imageURI === emptyImageURI &&
      record.title === emptyTitle &&
      record.description.startsWith(prefix) &&
      record.description.endsWith(suffix) &&
      getAddress(owner) === account,
    "Receipt-pinned empty Record mismatch",
  );

  return {
    transactionHash,
    blockNumber: receipt.blockNumber,
    auctionId,
    recipient: account,
    content: {
      imageURI: record.imageURI,
      title: record.title,
      description: record.description,
    },
  };
}

Before requesting a signature, both examples confirm that the auction has ended, has not been finalized and matches the requested auction.