Custodial financial storage

This commit is contained in:
2026-10-20 15:21:47 -04:00
parent e48081f041
commit c254c4c537
19 changed files with 1041 additions and 14 deletions
+10 -8
View File
@@ -19,6 +19,8 @@ import { marketCommandSchema } from '../../shared/market.ts';
import { marketCommandV3Schema } from '../../shared/market-v3.ts';
import { custodyCommandSchema } from '../../shared/custody.ts';
import { CustodyMarket, CustodyError, custodyMode } from '../market/custody.ts';
import { openCustodyStorage } from '../market/custody-storage.ts';
import type { PostgresCustody } from '../market/postgres-custody.ts';
import { IS_MAINNET, PROFILE } from '../../shared/network.ts';
import { assertLaunchAllowed, launchManifest } from '../launch.ts';
import { resourceConfig, RpcBudget, RpcError } from '../resources.ts';
@@ -52,9 +54,9 @@ catch(e) {ledger.close();engine.close();await lock.close();await unlink(lockPath
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;}
let custody: CustodyMarket | undefined;
let custody: CustodyMarket | PostgresCustody | undefined;
try {
if (custodyConfig === 'sandbox') custody = await CustodyMarket.open(dataDir, { feeBps: paymentConfig.feeBps,
if (custodyConfig === 'sandbox') custody = await openCustodyStorage(dataDir, { feeBps: paymentConfig.feeBps,
card: (hash, owner, sequence) => {
const nft = ledger.nft(hash);
if (!nft || nft.owner !== owner || nft.confirmations < CONSENSUS.coinbaseMaturity || nft.sequence !== sequence ||
@@ -96,19 +98,19 @@ 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))));
app.get('/api/custody', (req,res)=> {
app.get('/api/custody', async (req,res)=> {
if (!custody) { res.json({ enabled: false }); return; }
try { res.json(custody.browse(Number(req.query.offset ?? 0))); }
try { res.json(await custody.browse(Number(req.query.offset ?? 0))); }
catch (error) {
if (error instanceof CustodyError) res.status(400).json({ error: error.message });
else res.status(503).json({ error: 'Custody sandbox unavailable; operator review required.' });
}
});
app.post('/api/custody/command', rateLimit({windowMs:60000,limit:30,standardHeaders:'draft-8',legacyHeaders:false}), (req,res)=> {
app.post('/api/custody/command', rateLimit({windowMs:60000,limit:30,standardHeaders:'draft-8',legacyHeaders:false}), async (req,res)=> {
if (!custody) { res.status(503).json({ error: 'Custody sandbox is disabled on this host.' }); return; }
const parsed = custodyCommandSchema.safeParse(req.body);
if (!parsed.success) { res.status(400).json({ error: 'Invalid custody command schema.' }); return; }
try { res.json(custody.execute(parsed.data)); }
try { res.json(await custody.execute(parsed.data)); }
catch (error) {
if (error instanceof CustodyError) res.status(400).json({ error: error.message });
else res.status(503).json({ error: 'Custody state conflict; refresh activity before retrying. Operator review may be required.' });
@@ -234,6 +236,6 @@ let stopping=false;
for(const signal of ['SIGTERM','SIGINT']as const)process.once(signal,()=>{
if(stopping)return;stopping=true;peers.close();operations.close();clearInterval(marketTimer);
const timeout=setTimeout(()=>process.exit(1),25000);timeout.unref();
server.close(async()=>{custody?.close();market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});clearTimeout(timeout);process.exit(0);});
server.close(async()=>{await custody?.close();market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});clearTimeout(timeout);process.exit(0);});
});
server.on('error',async()=>{clearInterval(marketTimer);operations.close();peers.close();custody?.close();market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});process.exit(1);});
server.on('error',async()=>{clearInterval(marketTimer);operations.close();peers.close();await custody?.close();market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});process.exit(1);});