This commit is contained in:
2026-10-19 21:47:38 -04:00
parent f8eeae826f
commit b1d2b5c10e
17 changed files with 549 additions and 92 deletions
+27 -4
View File
@@ -6,7 +6,8 @@ import { fileURLToPath } from 'node:url';
import { mkdir, open, unlink } from 'node:fs/promises';
import { z } from 'zod';
import { NETWORK } from '../../shared/testnet.ts';
import { CHAIN_ID, blockSchema, transferSchema } from './consensus.ts';
import { testnetOwnerOf, transactionCardIds } from '../../shared/testnet.ts';
import { CHAIN_ID, blockSchema, tradeSchema, transferSchema, transactionSchema } from './consensus.ts';
import { RandomX } from './randomx.ts';
import { Ledger } from './ledger.ts';
import { PeerNetwork } from './peers.ts';
@@ -53,6 +54,17 @@ app.use(['/api','/p2p'],(req,res,next)=>{
app.get('/api/state',(_req,res)=>res.json(ledger.snapshot()));
app.get('/api/chain',(_req,res)=>res.json(ledger.snapshot()));
app.get('/api/peers',(_req,res)=>res.json({peers:peers.status()}));
app.get('/api/decks/:address', rateLimit({windowMs:60000,limit:60,standardHeaders:'draft-8',legacyHeaders:false}), (req,res)=>{
if(typeof req.params.address!=='string'||!/^[a-f0-9]{64}$/.test(req.params.address)){
res.status(400).json({error:'Invalid wallet address'});return;
}
const offset=Number(req.query.offset??0),limit=Number(req.query.limit??24);
if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(limit)||limit<1||limit>24){
res.status(400).json({error:'Invalid deck page'});return;
}
res.setHeader('Cache-Control','no-store');
res.json(ledger.deck(req.params.address,offset,limit));
});
// Optional live data is intentionally not used by genesis, block validation or NFT ownership.
app.get('/api/wikipedia/:pageId', rateLimit({windowMs:60000,limit:60,standardHeaders:'draft-8',legacyHeaders:false}), async(req,res)=>{
if(typeof req.params.pageId!=='string'||!/^[1-9]\d{0,9}$/.test(req.params.pageId)){res.status(400).json({error:'Invalid Wikipedia page ID'});return;}
@@ -71,8 +83,8 @@ app.get('/p2p/mempool',(_req,res)=>res.json({transactions:ledger.snapshot().pend
app.get('/api/nfts/:id', (req,res)=>{
const s=ledger.snapshot();const block=s.blocks.find(b=>b.tokenId===req.params.id);
if(!block){res.status(404).json({error:'NFT not found on the canonical chain'});return;}
let owner=block.owner,sequence=0;
for(const tx of s.transfers)if(tx.cardHash===block.tokenId){owner=tx.to;sequence++;}
const owner=testnetOwnerOf(s,block.tokenId)??block.owner;
const sequence=s.transfers.filter(tx=>transactionCardIds(tx).includes(block.tokenId)).length;
res.json({network:NETWORK,chainId:CHAIN_ID,tokenId:block.tokenId,owner,sequence,blockHash:block.hash,
confirmations:s.blocks.length-block.height+1,article:block.article,statsSnapshot:block.statsSnapshot,
stateRoot:s.blocks.at(-1)?.stateRoot});
@@ -95,12 +107,23 @@ app.post(['/api/blocks','/p2p/block'],workLimit,async(req,res)=>{
res.status(result.accepted?201:200).json(result);
if(result.accepted)void peers.broadcastBlock(block).catch(()=>{});
});
app.post(['/api/transfer','/p2p/transaction'],workLimit,async(req,res)=>{
app.post('/api/transfer',workLimit,async(req,res)=>{
const tx=transferSchema.parse(req.body);
const id=await ledger.submitTransfer(tx);
res.status(202).json({id,status:'pending',message:'Ownership changes only when this transaction is mined.'});
void peers.broadcastTransfer(tx).catch(()=>{});
});
app.post('/api/trades',workLimit,async(req,res)=>{
const trade=tradeSchema.parse(req.body);
const id=await ledger.submitTransaction(trade);
res.status(202).json({id,status:'pending',message:'The complete multi-card trade is pending; ownership changes atomically when it is mined.'});
void peers.broadcastTransaction(trade).catch(()=>{});
});
app.post('/p2p/transaction',workLimit,async(req,res)=>{
const transaction=transactionSchema.parse(req.body);
const id=await ledger.submitTransaction(transaction);
res.status(202).json({id,status:'pending'});
});
app.use(['/api','/p2p'],(_req,res)=>{res.status(404).json({error:'Unknown testnet endpoint'});});
app.use(express.static(resolve(root,'dist')));
app.get('/{*path}',(_req,res)=>res.sendFile(resolve(root,'dist/index.html')));