Removed markets

This commit is contained in:
2026-10-28 17:09:58 -04:00
parent 216a419c0b
commit eea2deb1fb
11 changed files with 122 additions and 105 deletions
+50 -34
View File
@@ -16,6 +16,7 @@ import { wikipediaEnrichment } from '../enrichment.ts';
import { snapshotStats } from '../../shared/enrichment.ts';
import { wikipediaArticles } from '../wiki.ts';
import { Market, marketConfig } from '../market/store.ts';
import { paymentMode } from '../market/payments.ts';
import { marketCommandSchema } from '../../shared/market.ts';
import { marketCommandV3Schema } from '../../shared/market-v3.ts';
import { custodyCommandSchema } from '../../shared/custody.ts';
@@ -40,9 +41,18 @@ import { trustedProxies, operationsPort, startOperations } from '../operations.t
const root = fileURLToPath(new URL('../../', import.meta.url));
assertLaunchAllowed();
const paymentConfig = marketConfig(); // Fail before opening node resources if live payments are requested.
await validateMarketPolicy(paymentConfig); // Reject invalid recipient checksum/network before opening node resources.
const marketRuntimeSetting = process.env.WIKIDECK_MARKET_RUNTIME_ENABLED ?? '0';
if (marketRuntimeSetting !== '0' && marketRuntimeSetting !== '1') {
throw new Error('WIKIDECK_MARKET_RUNTIME_ENABLED must be exactly 0 or 1');
}
const marketRuntimeEnabled = marketRuntimeSetting === '1';
const custodyConfig = custodyMode(); // Explicit opt-in sandbox only. Unknown/live modes fail before opening resources.
paymentMode(); // Keep real-payment modes fail-closed even when the optional market runtime is off.
let paymentConfig: ReturnType<typeof marketConfig> | undefined;
if (marketRuntimeEnabled || custodyConfig === 'sandbox') {
paymentConfig = marketConfig();
await validateMarketPolicy(paymentConfig); // Reject invalid recipient checksum/network before opening node resources.
}
const canonicalObserverMode = process.env.WIKIDECK_CUSTODY_CANONICAL_REHEARSAL;
if (canonicalObserverMode !== undefined && canonicalObserverMode !== '0' && canonicalObserverMode !== '1') {
throw new Error('WIKIDECK_CUSTODY_CANONICAL_REHEARSAL must be exactly 0 or 1');
@@ -86,8 +96,8 @@ 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, Date.now, { worker: true }); }
let market: Market | undefined;
try { if (marketRuntimeEnabled) market = await Market.open(dataDir, ledger, paymentConfig!, Date.now, { worker: true }); }
catch(e) {peers.close();ledger.close();engine.close();await lock.close();await unlink(lockPath);throw e;}
let custody: CustodyMarket | PostgresCustody | undefined;
let custodyObserver: CanonicalObserver | undefined;
@@ -97,7 +107,7 @@ let moneroDepositContext: MainnetEnrollmentContext | undefined;
let moneroWithdrawalPool: Pool | undefined;
let moneroWithdrawalCustodianId: string | undefined;
try {
if (custodyConfig === 'sandbox') custody = await openCustodyStorage(dataDir, { ...paymentConfig,
if (custodyConfig === 'sandbox') custody = await openCustodyStorage(dataDir, { ...paymentConfig!,
card: (hash, owner, sequence) => {
const nft = ledger.nft(hash);
if (!nft || nft.owner !== owner || nft.confirmations < CONSENSUS.coinbaseMaturity || nft.sequence !== sequence ||
@@ -147,7 +157,7 @@ try {
await verifyMainnetCustodyIdentity(moneroWithdrawalPool, moneroWithdrawalCustodianId);
await moneroWithdrawalPool.query('SELECT 1 FROM wk_custody.monero_withdrawal_intents LIMIT 0');
}
} catch(e) {await moneroWithdrawalPool?.end();await moneroDepositPool?.end();await custodyObserver?.close();await custody?.close();await market.close();peers.close();ledger.close();engine.close();await lock.close();await unlink(lockPath);throw e;}
} catch(e) {await moneroWithdrawalPool?.end();await moneroDepositPool?.end();await custodyObserver?.close();await custody?.close();await market?.close();peers.close();ledger.close();engine.close();await lock.close();await unlink(lockPath);throw e;}
const app = express();
app.disable('x-powered-by');
if(proxyTrust.length)app.set('trust proxy',proxyTrust);
@@ -181,7 +191,23 @@ app.get('/api/canonical',(req,res)=>{
app.get('/api/network',(_req,res)=>res.json(launchManifest()));
app.get('/api/chain',(_req,res)=>res.json(ledger.legacySnapshot(limits.legacyBlocks)));
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))));
if (market) {
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 current = marketCommandV3Schema.safeParse(req.body);
const command = current.success ? current.data : 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) {
if(error instanceof RpcError)throw 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/custody', async (req,res)=> {
if (!custody) { res.json({ enabled: false }); return; }
try { res.json(await custody.browse(Number(req.query.offset ?? 0))); }
@@ -252,20 +278,6 @@ if (withdrawalRequestMode === '1' && moneroWithdrawalPool && moneroWithdrawalCus
}
});
}
app.post('/api/market/command',rateLimit({windowMs:60000,limit:60,standardHeaders:'draft-8',legacyHeaders:false}),async(req,res)=>{
const current = marketCommandV3Schema.safeParse(req.body);
const command = current.success ? current.data : 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) {
if(error instanceof RpcError)throw 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;
@@ -359,7 +371,7 @@ const server=app.listen(port,host,()=>{
const operations=startOperations(opsPort,()=>({network:NETWORK,paymentMode:'mock',
ledger:ledger.diagnostics(),native:engine.diagnostics(),
peers:{configured:peers.status().length,connected:peers.status().filter(p=>p.connected).length},
rpc:{api:apiBudget.status(),p2p:peerBudget.status()},market:market.diagnostics(),
rpc:{api:apiBudget.status(),p2p:peerBudget.status()},market:market?.diagnostics() ?? { enabled:false },
canonicalObserver:custodyObserver?.status(),
custody: custody && 'diagnostics' in custody ? custody.diagnostics() : { backend: custody ? 'sqlite-sandbox' : 'off' }}),resolve(dataDir,PROFILE));
server.maxConnections=256;
@@ -368,16 +380,16 @@ server.requestTimeout=15000;
server.keepAliveTimeout=5000;
server.setTimeout(30000,socket=>socket.destroy());
// 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;
let catchupPaused=false;
let catchupImmediate:NodeJS.Immediate|undefined;
let marketTimer:NodeJS.Timeout|undefined;
let marketCatchupTimer:NodeJS.Timeout|undefined;
// Drain durable catch-up without page-load dependence. Each pass is a smaller synchronous
// SQLite slice, then setImmediate yields so HTTP and peer work can run between passes.
const advanceMarketCatchup=()=>{
catchupImmediate=undefined;
if(stopping||catchupPaused||market.isReconciliationWorkerBusy()||!market.hasReconciliationBacklog())return;
if(!market||stopping||catchupPaused||market.isReconciliationWorkerBusy()||!market.hasReconciliationBacklog())return;
if(market.isReconciliationWorkerFailed()){
catchupPaused=true;console.error('Marketplace reconciliation worker failed; operator review required.');return;
}
@@ -387,17 +399,21 @@ const advanceMarketCatchup=()=>{
catchupImmediate=setImmediate(advanceMarketCatchup);catchupImmediate.unref();
}
};
const marketCatchupTimer=setInterval(()=>{
if(!stopping&&!catchupPaused&&!catchupImmediate&&market.hasReconciliationBacklog()){
catchupImmediate=setImmediate(advanceMarketCatchup);catchupImmediate.unref();
}
},250);
marketCatchupTimer.unref();
if (market) {
marketTimer=setInterval(()=>{try {market!.browse();}catch{console.error('Marketplace reconciliation failed; operator review required.');}},15000);
marketTimer.unref();
marketCatchupTimer=setInterval(()=>{
if(!stopping&&!catchupPaused&&!catchupImmediate&&market!.hasReconciliationBacklog()){
catchupImmediate=setImmediate(advanceMarketCatchup);catchupImmediate.unref();
}
},250);
marketCatchupTimer.unref();
}
for(const signal of ['SIGTERM','SIGINT']as const)process.once(signal,()=>{
if(stopping)return;stopping=true;peers.close();operations.close();clearInterval(marketTimer);clearInterval(marketCatchupTimer);
if(stopping)return;stopping=true;peers.close();operations.close();if(marketTimer)clearInterval(marketTimer);if(marketCatchupTimer)clearInterval(marketCatchupTimer);
if(catchupImmediate)clearImmediate(catchupImmediate);
const observerDrain=custodyObserver?.close()??Promise.resolve();
const timeout=setTimeout(()=>process.exit(1),25000);timeout.unref();
server.close(async()=>{await observerDrain;await moneroWithdrawalPool?.end();await moneroDepositPool?.end();await custody?.close();await market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});clearTimeout(timeout);process.exit(0);});
server.close(async()=>{await observerDrain;await moneroWithdrawalPool?.end();await moneroDepositPool?.end();await custody?.close();await market?.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});clearTimeout(timeout);process.exit(0);});
});
server.on('error',async()=>{stopping=true;clearInterval(marketTimer);clearInterval(marketCatchupTimer);if(catchupImmediate)clearImmediate(catchupImmediate);operations.close();peers.close();await custodyObserver?.close();await moneroWithdrawalPool?.end();await moneroDepositPool?.end();await custody?.close();await market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});process.exit(1);});
server.on('error',async()=>{stopping=true;if(marketTimer)clearInterval(marketTimer);if(marketCatchupTimer)clearInterval(marketCatchupTimer);if(catchupImmediate)clearImmediate(catchupImmediate);operations.close();peers.close();await custodyObserver?.close();await moneroWithdrawalPool?.end();await moneroDepositPool?.end();await custody?.close();await market?.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});process.exit(1);});