Skip to content

Read a finalized Record ​

A finalized auction creates a Record and a numbered Tablet NFT. Read contract state to distinguish a winning Record from an empty Record and to identify the represented day.

Useful reads include:

  • the Record for a Tablet number;
  • the represented date and Tokyo day number;
  • the NFT metadata URI;
  • the current NFT owner; and
  • the original author or empty-auction finalizer from the relevant submission and finalization event.

The current NFT owner may differ from the original author after a transfer.

Example: read Record, provenance and current ownership ​

ts
const record = await readFinalizedRecord(12n);
ts
import { getAddress, type Address, type Hash, type Hex } from "viem";

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

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

const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

export type RecordMetadata = {
  readonly name: string;
  readonly description: string;
  readonly image: string;
  readonly date: string;
};

export type FinalizedRecordRead = {
  readonly block: { readonly number: bigint; readonly hash: Hash };
  readonly recordId: bigint;
  readonly kind: "winning" | "empty";
  readonly representedDate: string;
  readonly content: EntryContent;
  readonly tokenURI: string;
  readonly metadata: RecordMetadata;
  readonly currentOwner: Address;
  readonly originalAuthor: Address | null;
  readonly submissionId: bigint;
  readonly winningBid: bigint;
  readonly contentHash: Hex | null;
  readonly finalizer: Address;
  readonly finalizationTransactionHash: Hash;
  readonly submissionTransactionHash: Hash | null;
  readonly ownershipTransferCount: number;
};

function decodeMetadata(tokenURI: string): RecordMetadata {
  const prefix = "data:application/json;base64,";
  invariant(tokenURI.startsWith(prefix), "Unexpected tokenURI encoding");
  const binary = atob(tokenURI.slice(prefix.length));
  const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
  const value: unknown = JSON.parse(
    new TextDecoder("utf-8", { fatal: true }).decode(bytes),
  );
  invariant(value && typeof value === "object", "Metadata is not an object");
  const metadata = value as Record<string, unknown>;
  invariant(
    Object.keys(metadata).sort().join(",") === "date,description,image,name" &&
      typeof metadata.name === "string" &&
      typeof metadata.description === "string" &&
      typeof metadata.image === "string" &&
      typeof metadata.date === "string",
    "Metadata does not have the exact four-string schema",
  );
  return {
    name: metadata.name,
    description: metadata.description,
    image: metadata.image,
    date: metadata.date,
  };
}

export async function readFinalizedRecord(
  recordId: bigint,
): Promise<FinalizedRecordRead> {
  invariant(recordId > 0n, "recordId must be positive");
  const pin = await pinConfirmedBlock();
  const [record, auction, representedDate, tokenURI, currentOwner] =
    await Promise.all([
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "getRecord",
        args: [recordId],
        blockNumber: pin.number,
      }),
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "getAuction",
        args: [recordId],
        blockNumber: pin.number,
      }),
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "recordDate",
        args: [recordId],
        blockNumber: pin.number,
      }),
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "tokenURI",
        args: [recordId],
        blockNumber: pin.number,
      }),
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "ownerOf",
        args: [recordId],
        blockNumber: pin.number,
      }),
    ]);
  invariant(
    auction.finalized && (record.kind === 2 || record.kind === 3),
    "Record is not a finalized winning or empty Tablet",
  );

  const [finalizations, transfers] = await Promise.all([
    publicClient.getContractEvents({
      address: TABLETS_ADDRESS,
      abi: tabletsAbi,
      eventName: "RecordFinalized",
      args: { auctionId: recordId },
      fromBlock: DEPLOYMENT_BLOCK,
      toBlock: pin.number,
      strict: true,
    }),
    publicClient.getContractEvents({
      address: TABLETS_ADDRESS,
      abi: tabletsAbi,
      eventName: "Transfer",
      args: { tokenId: recordId },
      fromBlock: DEPLOYMENT_BLOCK,
      toBlock: pin.number,
      strict: true,
    }),
  ]);
  const finalized = only(finalizations, "RecordFinalized event");
  invariant(
    finalized.transactionHash &&
      finalized.blockHash &&
      finalized.removed !== true,
    "Finalization provenance is incomplete",
  );
  const finalizationReceipt = await publicClient.getTransactionReceipt({
    hash: finalized.transactionHash,
  });
  invariant(
    finalizationReceipt.status === "success" &&
      finalizationReceipt.to !== null &&
      getAddress(finalizationReceipt.to) === TABLETS_ADDRESS &&
      finalizationReceipt.blockHash.toLowerCase() ===
        finalized.blockHash.toLowerCase() &&
      finalizationReceipt.logs.some(
        (log) =>
          getAddress(log.address) === TABLETS_ADDRESS &&
          log.logIndex === finalized.logIndex,
      ),
    "Finalization receipt provenance mismatch",
  );

  invariant(transfers.length >= 1, "Record has no ERC-721 transfer history");
  const mint = transfers[0];
  const latestTransfer = transfers[transfers.length - 1];
  invariant(mint && latestTransfer, "Transfer history is incomplete");
  invariant(
    getAddress(mint.args.from) === getAddress(ZERO_ADDRESS) &&
      getAddress(mint.args.to) === getAddress(finalized.args.recipient) &&
      getAddress(latestTransfer.args.to) === getAddress(currentOwner),
    "Mint or current-owner provenance mismatch",
  );

  const metadata = decodeMetadata(tokenURI);
  invariant(
    metadata.name === record.title &&
      metadata.description === record.description &&
      metadata.image === record.imageURI &&
      metadata.date === representedDate,
    "tokenURI metadata differs from exact Record getters",
  );

  let originalAuthor: Address | null = null;
  let storedContentHash: Hex | null = null;
  let submissionTransactionHash: Hash | null = null;
  if (record.kind === 2) {
    invariant(
      record.submissionId !== 0n &&
        auction.winnerSubmissionId === record.submissionId &&
        finalized.args.submissionId === record.submissionId &&
        finalized.args.kind === 2 &&
        finalized.args.winningBid === auction.winningBid,
      "Winning Record and auction snapshot disagree",
    );
    const [submission, createdEvents] = await Promise.all([
      publicClient.readContract({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        functionName: "getSubmission",
        args: [record.submissionId],
        blockNumber: pin.number,
      }),
      publicClient.getContractEvents({
        address: TABLETS_ADDRESS,
        abi: tabletsAbi,
        eventName: "SubmissionCreated",
        args: { auctionId: recordId, submissionId: record.submissionId },
        fromBlock: DEPLOYMENT_BLOCK,
        toBlock: pin.number,
        strict: true,
      }),
    ]);
    const created = only(createdEvents, "SubmissionCreated event");
    invariant(
      created.transactionHash,
      "Submission transaction hash is missing",
    );
    const recomputed = contentHash(
      created.args.imageURI,
      created.args.title,
      created.args.description,
    );
    invariant(
      submission.auctionId === recordId &&
        getAddress(created.args.submitter) ===
          getAddress(submission.submitter) &&
        recomputed.toLowerCase() === submission.contentHash.toLowerCase() &&
        recomputed.toLowerCase() === created.args.contentHash.toLowerCase() &&
        record.imageURI === created.args.imageURI &&
        record.title === created.args.title &&
        record.description === created.args.description &&
        getAddress(finalized.args.recipient) ===
          getAddress(submission.submitter),
      "Winning submission provenance mismatch",
    );
    originalAuthor = getAddress(submission.submitter);
    storedContentHash = recomputed;
    submissionTransactionHash = created.transactionHash;
  } else {
    invariant(
      record.submissionId === 0n &&
        auction.winnerSubmissionId === 0n &&
        auction.winningBid === 0n &&
        finalized.args.submissionId === 0n &&
        finalized.args.kind === 3 &&
        finalized.args.winningBid === 0n,
      "Empty Record provenance mismatch",
    );
  }

  await assertPin(pin);
  return {
    block: { number: pin.number, hash: pin.hash },
    recordId,
    kind: record.kind === 2 ? "winning" : "empty",
    representedDate,
    content: {
      imageURI: record.imageURI,
      title: record.title,
      description: record.description,
    },
    tokenURI,
    metadata,
    currentOwner: getAddress(currentOwner),
    originalAuthor,
    submissionId: record.submissionId,
    winningBid: auction.winningBid,
    contentHash: storedContentHash,
    finalizer: getAddress(finalized.args.finalizer),
    finalizationTransactionHash: finalized.transactionHash,
    submissionTransactionHash,
    ownershipTransferCount: transfers.length,
  };
}