import type { Article } from './protocol.ts'; import type { MintStatsSnapshot } from './enrichment.ts'; import { NETWORK } from './network.ts'; export { NETWORK } from './network.ts'; export const CONSENSUS = { version: 6, targetSeconds: 15 * 60, retargetInterval: 10, medianWindow: 11, commitments: 'sha256-domain-separated-counted-merkle-v1', randomx: '1.2.1', powEndian: 'big', maxFutureSeconds: 120, epochBlocks: 2048, epochDelay: 64, maxTransactions: 64, maxTradeCards: 8, coinbaseMaturity: 2, maxBlockBytes: 64 * 1024, articleSource: 'wikipedia-random-with-image-and-stats-v1', atomicTrades: 'signed-two-party-bundle-v1', } as const; export type NftTransfer = { network: typeof NETWORK; chainId: string; cardHash: string; from: string; to: string; sequence: number; publicKey: string; signature: string; }; export type TradeLeg = { cardHash: string; sequence: number }; export type NftTrade = { type: 'trade'; network: typeof NETWORK; chainId: string; proposer: string; counterparty: string; offered: TradeLeg[]; requested: TradeLeg[]; proposerPublicKey: string; proposerSignature: string; counterpartyPublicKey: string; counterpartySignature: string; }; export type TestnetTransaction = NftTransfer | NftTrade; export function isNftTrade(transaction: TestnetTransaction): transaction is NftTrade { return (transaction as NftTrade).type === 'trade'; } export type BlockHeader = { version: 6; network: typeof NETWORK; chainId: string; height: number; previousHash: string; createdAt: number; article: Article; statsSnapshot: MintStatsSnapshot; owner: string; publicKey: string; nonce: number; target: string; key: string; transactionsRoot: string; stateRoot: string; }; export type TestnetBlock = BlockHeader & { hash: string; tokenId: string; powHash: string; signature: string; transactions: TestnetTransaction[]; }; export type MiningTemplate = { header: BlockHeader; transactions: TestnetTransaction[]; blob: string; nonceOffset: number; }; export type NftState = Record; /** Persistent overlay: a block stores only NFT entries it changed, sharing its parent's state. */ export type NftStateLayer = { kind: 'layer'; parent: NftStateView; changes: NftState }; export type NftStateView = NftState | NftStateLayer; export type TestnetSnapshot = { network: typeof NETWORK; chainId: string; genesis: string; tip: string; cumulativeWork: string; blocks: TestnetBlock[]; transfers: TestnetTransaction[]; pendingTransfers: TestnetTransaction[]; peers: number; algorithm: 'RandomX'; maturity: number; }; export type TestnetSummary = Omit & { height: number; revision: number }; export type CanonicalPage = { network: typeof NETWORK; chainId: string; tip: string; height: number; offset: number; next: number | null; blocks: TestnetBlock[] }; export type TestnetDeck = { network: typeof NETWORK; chainId: string; address: string; tip: string; offset: number; limit: number; total: number; next: number | null; nfts: { block: TestnetBlock; sequence: number; confirmations: number }[]; }; export function nftTransferMessage(t: Omit): string { return JSON.stringify([NETWORK, 'nft-transfer', t.chainId, t.cardHash, t.from, t.to, t.sequence]); } export function nftTradeMessage(t: Pick): string { return JSON.stringify([NETWORK, 'nft-trade', t.chainId, t.proposer, t.counterparty, t.offered.map(leg => [leg.cardHash, leg.sequence]), t.requested.map(leg => [leg.cardHash, leg.sequence])]); } export function articleTuple(a: Article): unknown[] { return [a.pageId, a.revisionId, a.title, a.extract, a.image ?? '', a.url, a.source, a.category]; } export function statsSnapshotTuple(s: MintStatsSnapshot): unknown[] { return [s.capturedAt, s.revisionId, s.pageBytes, s.languageCount, s.languagesTruncated, [s.pageviews.status, s.pageviews.months.map(item => [item.month, item.views]), s.pageviews.latestMonth, s.pageviews.total, s.pageviews.dailyAverage, s.pageviews.trendPercent], [s.stats.attention, s.stats.depth, s.stats.reach]]; } export function headerMessage(h: BlockHeader): string { return JSON.stringify([NETWORK, h.version, h.chainId, h.height, h.previousHash, h.createdAt, articleTuple(h.article), statsSnapshotTuple(h.statsSnapshot), h.owner, h.publicKey, h.target, h.key, h.transactionsRoot, h.stateRoot]); } export function blockClaimMessage(hash: string): string { return JSON.stringify([NETWORK, 'block-claim', hash]); } export function transactionCardIds(transaction: TestnetTransaction): string[] { return isNftTrade(transaction) ? [...transaction.offered, ...transaction.requested].map(leg => leg.cardHash) : [transaction.cardHash]; } export function testnetOwnerOf(state: Pick, hash: string): string | undefined { let owner = state.blocks.find(b => b.tokenId === hash)?.owner; for (const tx of state.transfers) { if (isNftTrade(tx)) { if (tx.offered.some(leg => leg.cardHash === hash)) owner = tx.counterparty; if (tx.requested.some(leg => leg.cardHash === hash)) owner = tx.proposer; } else if (tx.cardHash === hash) owner = tx.to; } return owner; }