Trades
This commit is contained in:
+39
-9
@@ -1,29 +1,40 @@
|
||||
import type { Article } from './protocol.ts';
|
||||
import type { MintStatsSnapshot } from './enrichment.ts';
|
||||
|
||||
export const NETWORK = 'wikideck-randomx-testnet-5' as const;
|
||||
export const NETWORK = 'wikideck-randomx-testnet-6' as const;
|
||||
export const CONSENSUS = {
|
||||
version: 5, targetSeconds: 15 * 60, retargetInterval: 10, medianWindow: 11,
|
||||
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, coinbaseMaturity: 2, maxBlockBytes: 64 * 1024,
|
||||
articleSource: 'wikipedia-random-with-image-and-stats-v1',
|
||||
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: 5; network: typeof NETWORK; chainId: string; height: number;
|
||||
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: NftTransfer[];
|
||||
hash: string; tokenId: string; powHash: string; signature: string; transactions: TestnetTransaction[];
|
||||
};
|
||||
export type MiningTemplate = {
|
||||
header: BlockHeader; transactions: NftTransfer[]; blob: string; nonceOffset: number;
|
||||
header: BlockHeader; transactions: TestnetTransaction[]; blob: string; nonceOffset: number;
|
||||
};
|
||||
export type NftState = Record<string, { owner: string; sequence: number; mintedHeight: number; pageId: number }>;
|
||||
/** Persistent overlay: a block stores only NFT entries it changed, sharing its parent's state. */
|
||||
@@ -31,12 +42,21 @@ export type NftStateLayer = { kind: 'layer'; parent: NftStateView; changes: NftS
|
||||
export type NftStateView = NftState | NftStateLayer;
|
||||
export type TestnetSnapshot = {
|
||||
network: typeof NETWORK; chainId: string; genesis: string; tip: string; cumulativeWork: string;
|
||||
blocks: TestnetBlock[]; transfers: NftTransfer[]; pendingTransfers: NftTransfer[];
|
||||
blocks: TestnetBlock[]; transfers: TestnetTransaction[]; pendingTransfers: TestnetTransaction[];
|
||||
peers: number; algorithm: 'RandomX'; maturity: number;
|
||||
};
|
||||
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<NftTransfer, 'publicKey' | 'signature'>): string {
|
||||
return JSON.stringify([NETWORK, 'nft-transfer', t.chainId, t.cardHash, t.from, t.to, t.sequence]);
|
||||
}
|
||||
export function nftTradeMessage(t: Pick<NftTrade, 'network' | 'chainId' | 'proposer' | 'counterparty' | 'offered' | 'requested'>): 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];
|
||||
}
|
||||
@@ -54,8 +74,18 @@ export function headerMessage(h: BlockHeader): string {
|
||||
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<TestnetSnapshot, 'blocks' | 'transfers'>, hash: string): string | undefined {
|
||||
let owner = state.blocks.find(b => b.tokenId === hash)?.owner;
|
||||
for (const t of state.transfers) if (t.cardHash === hash) owner = t.to;
|
||||
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;
|
||||
}
|
||||
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
import { NETWORK, CONSENSUS } from './testnet.ts';
|
||||
|
||||
const hex = z.string().regex(/^[a-f0-9]{64}$/);
|
||||
const signature = z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/).length(88);
|
||||
const leg = z.object({ cardHash: hex, sequence: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER) }).strict();
|
||||
|
||||
export const tradeOfferSchema = z.object({
|
||||
format: z.literal('wikideck-trade-offer'),
|
||||
version: z.literal(1),
|
||||
network: z.literal(NETWORK),
|
||||
chainId: hex,
|
||||
proposer: hex,
|
||||
counterparty: hex,
|
||||
offered: z.array(leg).min(1).max(CONSENSUS.maxTradeCards),
|
||||
requested: z.array(leg).min(1).max(CONSENSUS.maxTradeCards),
|
||||
proposerPublicKey: z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/).max(256),
|
||||
proposerSignature: signature,
|
||||
}).strict().superRefine((offer, ctx) => {
|
||||
if (offer.proposer === offer.counterparty) ctx.addIssue({ code: 'custom', message: 'Trade parties must differ' });
|
||||
const ids = [...offer.offered, ...offer.requested].map(item => item.cardHash);
|
||||
if (new Set(ids).size !== ids.length) ctx.addIssue({ code: 'custom', message: 'Each NFT may appear only once in a trade' });
|
||||
});
|
||||
export type TradeOffer = z.infer<typeof tradeOfferSchema>;
|
||||
@@ -1,5 +1,5 @@
|
||||
// Browser/Node WebCrypto only: this module never reads storage or sends data.
|
||||
const NETWORK = 'wikideck-randomx-testnet-5';
|
||||
const NETWORK = 'wikideck-randomx-testnet-6';
|
||||
const ITERATIONS = 600_000;
|
||||
const MAX_BYTES = 32 * 1024;
|
||||
const encoder = new TextEncoder();
|
||||
@@ -7,7 +7,7 @@ const encoder = new TextEncoder();
|
||||
export type WalletBackup = {
|
||||
format: 'wikideck-wallet-backup';
|
||||
version: 1;
|
||||
network: 'wikideck-randomx-testnet-5';
|
||||
network: 'wikideck-randomx-testnet-6';
|
||||
kdf: 'PBKDF2-SHA256';
|
||||
iterations: 600000;
|
||||
cipher: 'AES-256-GCM';
|
||||
|
||||
Reference in New Issue
Block a user