Markets
This commit is contained in:
@@ -6,11 +6,12 @@ import { NETWORK, CONSENSUS, headerMessage, blockClaimMessage, nftTransferMessag
|
||||
import type { Article } from '../../shared/protocol.ts';
|
||||
import { deriveStats, type MintStatsSnapshot } from '../../shared/enrichment.ts';
|
||||
import type { PowEngine } from './randomx.ts';
|
||||
import { NETWORK_GENESIS_TIME } from '../../shared/network.ts';
|
||||
|
||||
export const sha256 = (data: string | Uint8Array) => createHash('sha256').update(data).digest('hex');
|
||||
export const MAX_TARGET = `0fff${'f'.repeat(60)}`;
|
||||
// Consensus manifest: an independently running node derives exactly the same genesis.
|
||||
export const GENESIS_TIME = 1_792_411_200;
|
||||
export const GENESIS_TIME = NETWORK_GENESIS_TIME;
|
||||
export const CHAIN_ID = sha256(JSON.stringify([NETWORK, CONSENSUS, GENESIS_TIME, MAX_TARGET]));
|
||||
export const GENESIS_HASH = sha256(`${NETWORK}:genesis:${CHAIN_ID}`);
|
||||
export const INITIAL_KEY = sha256(`${NETWORK}:initial-key:${CHAIN_ID}`);
|
||||
|
||||
+31
-5
@@ -14,12 +14,18 @@ import { PeerNetwork } from './peers.ts';
|
||||
import { wikipediaEnrichment } from '../enrichment.ts';
|
||||
import { snapshotStats } from '../../shared/enrichment.ts';
|
||||
import { wikipediaArticles } from '../wiki.ts';
|
||||
import { Market, marketConfig } from '../market/store.ts';
|
||||
import { marketCommandSchema } from '../../shared/market.ts';
|
||||
import { IS_MAINNET, PROFILE } from '../../shared/network.ts';
|
||||
import { assertLaunchAllowed, launchManifest } from '../launch.ts';
|
||||
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url));
|
||||
assertLaunchAllowed();
|
||||
const paymentConfig = marketConfig(); // Fail before opening node resources if live payments are requested.
|
||||
const dataDir = process.env.WIKIDECK_DATA_DIR ?? resolve(root,'data');
|
||||
await mkdir(dataDir,{recursive:true});
|
||||
// Never permit two processes to rewrite the same ledger. A stale lock requires operator review.
|
||||
const lockPath = resolve(dataDir,'testnet-node.lock');
|
||||
const lockPath = resolve(dataDir,`${PROFILE}-node.lock`);
|
||||
const lock = await open(lockPath,'wx',0o600).catch(()=>{throw new Error('Data directory locked. Stop its other node; remove stale testnet-node.lock only after verifying no node is running.');});
|
||||
await lock.writeFile(String(process.pid));
|
||||
let engine: RandomX;
|
||||
@@ -32,6 +38,9 @@ let peers: PeerNetwork;
|
||||
try { peers = new PeerNetwork({ ledger, urls:(process.env.WIKIDECK_PEERS ?? '').split(',').map(s=>s.trim()).filter(Boolean),
|
||||
allowPrivate:process.env.WIKIDECK_ALLOW_PRIVATE_PEERS === '1' }); }
|
||||
catch(e) {ledger.close();engine.close();await lock.close();await unlink(lockPath);throw e;}
|
||||
let market: Market;
|
||||
try { market = await Market.open(dataDir, ledger, paymentConfig); }
|
||||
catch(e) {peers.close();ledger.close();engine.close();await lock.close();await unlink(lockPath);throw e;}
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
app.use(helmet({contentSecurityPolicy:{directives:{defaultSrc:["'self'"],scriptSrc:["'self'"],
|
||||
@@ -52,8 +61,22 @@ app.use(['/api','/p2p'],(req,res,next)=>{
|
||||
next();
|
||||
});
|
||||
app.get('/api/state',(_req,res)=>res.json(ledger.snapshot()));
|
||||
app.get('/api/network',(_req,res)=>res.json(launchManifest()));
|
||||
app.get('/api/chain',(_req,res)=>res.json(ledger.snapshot()));
|
||||
app.get('/api/peers',(_req,res)=>res.json({peers:peers.status()}));
|
||||
app.get('/api/market', (req,res)=>res.json(market.browse(Number(req.query.offset ?? 0))));
|
||||
app.post('/api/market/command',rateLimit({windowMs:60000,limit:60,standardHeaders:'draft-8',legacyHeaders:false}),async(req,res)=>{
|
||||
const command = marketCommandSchema.parse(req.body);
|
||||
try {
|
||||
const result = await market.execute(command);
|
||||
res.json(result);
|
||||
if(command.action.type==='deliver')void peers.broadcastTransfer(command.action.transfer).catch(()=>{});
|
||||
} catch(error) {
|
||||
// Marketplace storage/payment internals must never leak through the generic node error handler.
|
||||
if(error instanceof Error && !/SQLITE|constraint|database/i.test(error.message))res.status(400).json({error:error.message.slice(0,200)});
|
||||
else res.status(409).json({error:'Marketplace state conflict. Refresh and retry.'});
|
||||
}
|
||||
});
|
||||
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;
|
||||
@@ -135,13 +158,16 @@ app.use((err:Error & {status?:number},_req:express.Request,res:express.Response,
|
||||
});
|
||||
const port=Number(process.env.PORT??3001),host=process.env.HOST??'127.0.0.1';
|
||||
const server=app.listen(port,host,()=>{
|
||||
const a=server.address();console.log(`Wikideck RandomX testnet: http://${host}:${typeof a==='object'&&a?a.port:port} · chain ${CHAIN_ID} · ${ledger.snapshot().blocks.length} blocks`);
|
||||
const a=server.address();console.log(`Wikideck RandomX ${IS_MAINNET?'mainnet candidate':'testnet'}: http://${host}:${typeof a==='object'&&a?a.port:port} · chain ${CHAIN_ID} · ${ledger.snapshot().blocks.length} blocks`);
|
||||
peers.start();
|
||||
});
|
||||
// Advance simulated payout/expiry state even when no browser is open. No real funds are sent.
|
||||
const marketTimer=setInterval(()=>{try {market.browse();}catch{console.error('Marketplace reconciliation failed; operator review required.');}},15000);
|
||||
marketTimer.unref();
|
||||
let stopping=false;
|
||||
for(const signal of ['SIGTERM','SIGINT']as const)process.once(signal,()=>{
|
||||
if(stopping)return;stopping=true;peers.close();
|
||||
if(stopping)return;stopping=true;peers.close();clearInterval(marketTimer);
|
||||
const timeout=setTimeout(()=>process.exit(1),25000);timeout.unref();
|
||||
server.close(async()=>{ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});clearTimeout(timeout);process.exit(0);});
|
||||
server.close(async()=>{market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});clearTimeout(timeout);process.exit(0);});
|
||||
});
|
||||
server.on('error',async()=>{peers.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});process.exit(1);});
|
||||
server.on('error',async()=>{clearInterval(marketTimer);peers.close();market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});process.exit(1);});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { CHAIN_ID, GENESIS_HASH, applyTransactionOverlay, blockSchema, makeTempl
|
||||
import type { PowEngine } from './randomx.ts';
|
||||
import type { Article } from '../../shared/protocol.ts';
|
||||
import type { MintStatsSnapshot } from '../../shared/enrichment.ts';
|
||||
import { PROFILE } from '../../shared/network.ts';
|
||||
|
||||
const MAX_PENDING = 256;
|
||||
const MAX_DECK_PAGE = 24;
|
||||
@@ -41,7 +42,7 @@ export class Ledger {
|
||||
private constructor(file: string, db: DatabaseSync, pow: PowEngine) { this.file = file; this.db = db; this.pow = pow; }
|
||||
|
||||
static async open(dir: string, pow: PowEngine): Promise<Ledger> {
|
||||
const directory = join(dir, 'testnet');
|
||||
const directory = join(dir, PROFILE);
|
||||
await mkdir(directory, { recursive: true, mode: 0o700 });
|
||||
const file = join(directory, 'ledger.sqlite');
|
||||
let databaseExists = false;
|
||||
|
||||
@@ -6,6 +6,9 @@ import { decryptWallet } from '../../shared/wallet-backup.ts';
|
||||
import { blockClaimMessage, CONSENSUS, NETWORK, type MiningTemplate, type TestnetBlock } from '../../shared/testnet.ts';
|
||||
import { CHAIN_ID, sha256, miningBlob, mintId, blockSchema } from './consensus.ts';
|
||||
import { RandomX } from './randomx.ts';
|
||||
import { assertLaunchAllowed } from '../launch.ts';
|
||||
|
||||
assertLaunchAllowed();
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const arg = (key: string) => { const i = args.indexOf(key); return i < 0 ? undefined : args[i+1]; };
|
||||
|
||||
Reference in New Issue
Block a user