Appearance
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,
};
}