This commit is contained in:
2026-10-20 10:06:38 -04:00
parent 55f26d3fad
commit 2dd7382004
19 changed files with 643 additions and 11 deletions
+206
View File
@@ -0,0 +1,206 @@
import { DatabaseSync } from 'node:sqlite';
import { mkdir,open,unlink,chmod } from 'node:fs/promises';
import { join } from 'node:path';
import { z } from 'zod';
import { ADDITIONAL_MARKET_FEE } from '../../shared/market.ts';
import { NETWORK } from '../../shared/network.ts';
import { CHAIN_ID,sha256,verifySignature,transactionId } from '../testnet/consensus.ts';
import { atomicXmr,fiatCents,validateXmrAddress } from './xmr.ts';
import { xmrString } from './payments.ts';
import type { SettlementProcessor,ProcessorQuote,InvoiceObservation } from './shkeeper.ts';
const hex=z.string().regex(/^[a-f0-9]{64}$/);
const termsSchema=z.object({id:z.string().uuid(),network:z.literal(NETWORK),chainId:z.literal(CHAIN_ID),
cardHash:hex,sequence:z.number().int().nonnegative().safe(),seller:hex,buyer:hex,sellerPublicKey:z.string().max(256),
buyerPublicKey:z.string().max(256),sellerXmr:z.string().length(95),priceCents:z.number().int().min(1).max(100_000_000),
feeBps:z.number().int().min(0).max(5000),expiresAt:z.number().int().nonnegative().safe(),
deliveryConfirmations:z.number().int().min(2).max(1000)}).strict();
export type SettlementTerms=z.infer<typeof termsSchema>;
export function sellerSettlementMessage(t:SettlementTerms){return JSON.stringify([NETWORK,'settlement-lab-terms-v1',
t.chainId,t.id,t.cardHash,t.sequence,t.seller,t.buyer,t.sellerPublicKey,t.buyerPublicKey,t.sellerXmr,t.priceCents,t.feeBps,
t.expiresAt,t.deliveryConfirmations,ADDITIONAL_MARKET_FEE.bps,ADDITIONAL_MARKET_FEE.address]);}
type Leg={amount:string;destination:string;state:'ready'|'submitting'|'pending'|'complete'|'ambiguous'|'review'|'skipped';taskId?:string;txids?:string[]};
export type LabSettlement={terms:SettlementTerms;sellerSignature:string;
status:'draft'|'invoice-submitting'|'quote'|'awaiting-payment'|'paid'|'paying'|'complete'|'review';
quote?:ProcessorQuote;quoteDigest?:string;buyerSignature?:string;paidObservedAt?:number;paidTxids?:string[];
hostingXmr?:string;seller?:Leg;fee?:Leg;reviewReason?:string};
export function buyerInvoiceMessage(s:LabSettlement){
if(!s.quote||!s.seller||!s.fee)throw new Error('No invoice quote to authorize');
return JSON.stringify([NETWORK,'settlement-lab-invoice-v1',s.terms.chainId,s.terms.id,sha256(sellerSettlementMessage(s.terms)),
s.quote.id,'XMR','USD',s.terms.priceCents,s.quote.address,s.quote.amountXmr,s.quote.exchangeRate,s.terms.expiresAt,
s.hostingXmr,s.seller.amount,s.seller.destination,s.fee.amount,s.fee.destination]);
}
export type DeliveryEvidence={transferId:string|null;confirmations:number;tip:string};
/** Trusted consensus-reader boundary, NOT a boolean submitted by an HTTP client. */
export type DeliveryReader=(terms:SettlementTerms)=>Promise<DeliveryEvidence>;
/** Offline settlement research journal. NOT imported by app/Market and NOT part of production
* backup format. No public routes, worker, callback or CLI activation. Test fixtures only. */
export class SettlementLab {
private readonly db:DatabaseSync;
private readonly processor:SettlementProcessor;
private readonly delivery:DeliveryReader;
private readonly now:()=>number;
private readonly release:()=>Promise<void>;
private queue:Promise<unknown>=Promise.resolve();private queued=0;
private constructor(db:DatabaseSync,processor:SettlementProcessor,delivery:DeliveryReader,now:()=>number,release:()=>Promise<void>){
this.db=db;this.processor=processor;this.delivery=delivery;this.now=now;this.release=release;
}
static async open(directory:string,processor:SettlementProcessor,delivery:DeliveryReader,now=Date.now){
await mkdir(directory,{recursive:true,mode:0o700});const file=join(directory,'settlement-lab.sqlite'),lockPath=file+'.lock';
const lock=await open(lockPath,'wx',0o600);let db:DatabaseSync|undefined;
try{
db=new DatabaseSync(file);
db.exec(`PRAGMA journal_mode=WAL;PRAGMA synchronous=FULL;PRAGMA busy_timeout=5000;
CREATE TABLE IF NOT EXISTS lab_meta(key TEXT PRIMARY KEY,value TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS lab_orders(id TEXT PRIMARY KEY,data TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS lab_tasks(id TEXT PRIMARY KEY,order_id TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS lab_txids(id TEXT PRIMARY KEY,order_id TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS lab_incoming(id TEXT PRIMARY KEY,order_id TEXT NOT NULL);`);
const identity=JSON.stringify([NETWORK,CHAIN_ID,'offline-settlement-lab-v1']);
const row=db.prepare("SELECT value FROM lab_meta WHERE key='identity'").get() as {value:string}|undefined;
if(row&&row.value!==identity)throw new Error('Settlement lab network/schema mismatch');
if(!row)db.prepare('INSERT INTO lab_meta VALUES(?,?)').run('identity',identity);
if(process.platform!=='win32')await chmod(file,0o600);
// A committed intent with no saved task ID must never be replayed after restart.
const update=db.prepare('UPDATE lab_orders SET data=? WHERE id=?');
db.exec('BEGIN IMMEDIATE');
try{
for(const row of db.prepare('SELECT id,data FROM lab_orders').all() as {id:string;data:string}[]){
const s=JSON.parse(row.data) as LabSettlement;let uncertain=s.status==='invoice-submitting';
for(const leg of [s.seller,s.fee])if(leg?.state==='submitting'){leg.state='ambiguous';uncertain=true;}
if(uncertain){s.status='review';s.reviewReason='restart-after-unresolved-processor-intent';update.run(JSON.stringify(s),row.id);}
}
db.exec('COMMIT');
}catch(error){db.exec('ROLLBACK');throw error;}
return new SettlementLab(db,processor,delivery,now,async()=>{await lock.close();await unlink(lockPath);});
}catch(error){db?.close();await lock.close();await unlink(lockPath);throw error;}
}
async close(){await this.queue;this.db.close();await this.release();}
get(id:string):LabSettlement{
z.string().uuid().parse(id);const row=this.db.prepare('SELECT data FROM lab_orders WHERE id=?').get(id) as {data:string}|undefined;
if(!row)throw new Error('Unknown lab settlement');return JSON.parse(row.data);
}
private save(s:LabSettlement){this.db.prepare('UPDATE lab_orders SET data=? WHERE id=?').run(JSON.stringify(s),s.terms.id);}
private hold(s:LabSettlement,reason:string){s.status='review';s.reviewReason=reason;this.save(s);return s;}
private serialize<T>(fn:()=>Promise<T>):Promise<T>{
if(this.queued>=32)return Promise.reject(new Error('Settlement lab busy'));
this.queued++;const run=this.queue.then(async()=>{try{return await fn();}finally{this.queued--;}});
this.queue=run.catch(()=>undefined);return run;
}
create(raw:unknown,sellerSignature:string){
const terms=termsSchema.parse(raw);
return this.serialize(async()=>{
if(terms.seller===terms.buyer||terms.expiresAt<=this.now()||terms.expiresAt>this.now()+30*60000||
!verifySignature(terms.sellerPublicKey,terms.seller,sellerSettlementMessage(terms),sellerSignature))throw new Error('Invalid seller terms or expiry');
await validateXmrAddress(terms.sellerXmr);await validateXmrAddress(ADDITIONAL_MARKET_FEE.address);
if((this.db.prepare('SELECT COUNT(*) AS n FROM lab_orders').get() as {n:number}).n>=1000)throw new Error('Lab journal capacity reached');
const s:LabSettlement={terms,sellerSignature,status:'draft'};
this.db.prepare('INSERT INTO lab_orders VALUES(?,?)').run(terms.id,JSON.stringify(s));return s;
});
}
issueInvoice(id:string){return this.serialize(async()=>{
const s=this.get(id);if(s.status!=='draft')return s;
if(this.now()>=s.terms.expiresAt)return this.hold(s,'invoice-deadline');
s.status='invoice-submitting';this.save(s); // Durable intent BEFORE network-shaped I/O, no automatic POST retry.
try{
s.quote=await this.processor.createInvoice(id,s.terms.priceCents);
await validateXmrAddress(s.quote.address);const gross=atomicXmr(s.quote.amountXmr);
if(!s.quote.id||gross<=0n)throw new Error('Invalid quote');
const hosting=gross*BigInt(s.terms.feeBps)/10000n,fee=gross*100n/10000n;
s.hostingXmr=xmrString(hosting);
s.seller={amount:xmrString(gross-hosting-fee),destination:s.terms.sellerXmr,state:'ready'};
s.fee={amount:xmrString(fee),destination:ADDITIONAL_MARKET_FEE.address,state:fee===0n?'skipped':'ready'};
s.quoteDigest=sha256(buyerInvoiceMessage(s));s.status='quote';this.save(s);return s;
}catch{return this.hold(s,'invoice-create-ambiguous-or-invalid');}
});}
approveQuote(id:string,signature:string){return this.serialize(async()=>{
const s=this.get(id);
if(s.status!=='quote'||this.now()>=s.terms.expiresAt||
!verifySignature(s.terms.buyerPublicKey,s.terms.buyer,buyerInvoiceMessage(s),signature))throw new Error('Invalid buyer invoice authorization');
s.buyerSignature=signature;s.status='awaiting-payment';this.save(s);return s;
});}
private async observe(s:LabSettlement,observation?:InvoiceObservation):Promise<'paid'|'waiting'|'review'>{
let o:InvoiceObservation;
try{o=observation??await this.processor.invoice(s.terms.id);}catch{return 'review';}
if(!s.quote||o.external_id!==s.terms.id||o.fiat!=='USD'||fiatCents(o.amount_fiat)!==s.terms.priceCents)return 'review';
let confirmed=0n,all=0n;const seen=new Set<string>();
for(const tx of o.txs){
if(tx.addr!==s.quote.address||tx.crypto!=='XMR'||seen.has(tx.txid))return 'review';
seen.add(tx.txid);const amount=atomicXmr(tx.amount);all+=amount;if(tx.status==='CONFIRMED')confirmed+=amount;
}
const gross=atomicXmr(s.quote.amountXmr);
if(s.paidTxids&&JSON.stringify([...seen].sort())!==JSON.stringify([...s.paidTxids].sort()))return 'review';
if(o.status==='OVERPAID'||all>gross||fiatCents(o.balance_fiat)>s.terms.priceCents)return 'review';
if(o.status==='PAID'&&confirmed===gross&&all===gross&&fiatCents(o.balance_fiat)===s.terms.priceCents)return 'paid';
if(o.status==='UNPAID'&&all===0n&&fiatCents(o.balance_fiat)===0)return 'waiting';
if(o.status==='PARTIAL'&&all<gross)return 'waiting';
if(all===gross&&confirmed<gross)return 'waiting';
return 'review';
}
pollPayment(id:string){return this.serialize(async()=>{
const s=this.get(id);if(s.status!=='awaiting-payment')return s;
if(this.now()>=s.terms.expiresAt)return this.hold(s,'late-or-expired-payment-requires-review');
let observation:InvoiceObservation;
try{observation=await this.processor.invoice(s.terms.id);}catch{return this.hold(s,'invoice-verification-failed');}
const observed=await this.observe(s,observation);
if(observed==='review')return this.hold(s,'invoice-verification-failed');
if(this.now()>=s.terms.expiresAt)return this.hold(s,'payment-observed-after-deadline-requires-review');
if(observed==='paid'){
// Independent observations must not allocate the same Monero transaction to two orders.
this.db.exec('BEGIN IMMEDIATE');
try{
for(const tx of observation.txs)this.db.prepare('INSERT INTO lab_incoming VALUES(?,?)').run(tx.txid,s.terms.id);
s.status='paid';s.paidObservedAt=this.now();s.paidTxids=observation.txs.map(tx=>tx.txid);this.save(s);this.db.exec('COMMIT');
}catch{this.db.exec('ROLLBACK');return this.hold(s,'incoming-transaction-already-allocated-or-storage-conflict');}
}return s;
});}
private expectedDelivery(t:SettlementTerms){return transactionId({network:NETWORK,chainId:CHAIN_ID,cardHash:t.cardHash,
sequence:t.sequence,from:t.seller,to:t.buyer,publicKey:'',signature:''});}
private async ready(s:LabSettlement){
const evidence=await this.delivery(structuredClone(s.terms));
return evidence.transferId===this.expectedDelivery(s.terms)&&Number.isSafeInteger(evidence.confirmations)&&
evidence.confirmations>=s.terms.deliveryConfirmations&&/^[a-f0-9]{64}$/.test(evidence.tip);
}
advance(id:string){return this.serialize(async()=>{
const s=this.get(id);if(!['paid','paying','complete'].includes(s.status))return s;
// Reads before each payout are risk controls, not cross-chain atomicity or finality.
if(await this.observe(s)!=='paid')return this.hold(s,'paid-invoice-no-longer-verifiable');
if(!await this.ready(s)){
if(s.status==='paid')return s; // Payment confirmed; still waiting for adequate NFT depth.
return this.hold(s,'delivery-reorganized-or-insufficient');
}
if(s.status==='complete')return s;
s.status='paying';this.save(s);
for(const leg of [s.seller!,s.fee!]){
if(leg.state==='complete'||leg.state==='skipped')continue;
if(leg.state==='ambiguous'||leg.state==='review'||leg.state==='submitting')return this.hold(s,'payout-intent-requires-operator-reconciliation');
if(leg.state==='ready'){
if(!await this.ready(s)||await this.observe(s)!=='paid')return this.hold(s,'settlement-evidence-changed-before-payout');
leg.state='submitting';this.save(s);
try{
leg.taskId=await this.processor.payout(leg.amount,leg.destination);z.string().uuid().parse(leg.taskId);
this.db.exec('BEGIN IMMEDIATE');
try{this.db.prepare('INSERT INTO lab_tasks VALUES(?,?)').run(leg.taskId,s.terms.id);leg.state='pending';this.save(s);this.db.exec('COMMIT');}
catch(error){this.db.exec('ROLLBACK');throw error;}
}
catch{leg.state='ambiguous';return this.hold(s,'payout-create-outcome-unknown-never-retry-automatically');}
}
let task;
try{task=await this.processor.task(leg.taskId!);}catch{return s;} // Known task GET can be polled, no new payout POST.
if(task.status==='PENDING')return s;
const result=task.result;
if(task.status!=='SUCCESS'||result.length!==1||result[0].status!=='success'||result[0].dest!==leg.destination||
atomicXmr(result[0].amount)!==atomicXmr(leg.amount)||!result[0].txids?.length||
result[0].txids.some(tx=>!(/^[a-f0-9]{64}$/.test(tx)))||new Set(result[0].txids).size!==result[0].txids.length){
leg.state='review';return this.hold(s,'payout-task-failed-partial-or-mismatched');
}
this.db.exec('BEGIN IMMEDIATE');
try{
for(const txid of result[0].txids)this.db.prepare('INSERT INTO lab_txids VALUES(?,?)').run(txid,s.terms.id);
leg.state='complete';leg.txids=result[0].txids;this.save(s);this.db.exec('COMMIT');
}catch{this.db.exec('ROLLBACK');leg.state='review';return this.hold(s,'payout-transaction-already-allocated-or-storage-conflict');}
}
s.status='complete';this.save(s);return s;
});}
}
+86
View File
@@ -0,0 +1,86 @@
import { z } from 'zod';
import { validateXmrAddress, atomicXmr, fiatCents } from './xmr.ts';
const identifier=z.union([z.string().min(1).max(128),z.number().int().nonnegative().safe()]).transform(String);
const decimal=z.string().min(1).max(32);
const external=z.string().uuid();
const transaction=z.object({addr:z.string().length(95),amount:decimal,crypto:z.literal('XMR'),
status:z.enum(['CONFIRMED','UNCONFIRMED']),txid:z.string().regex(/^[a-f0-9]{64}$/)});
export const invoiceObservation=z.object({external_id:external,fiat:z.literal('USD'),amount_fiat:decimal,
balance_fiat:decimal,status:z.enum(['UNPAID','PARTIAL','PAID','OVERPAID']),txs:z.array(transaction).max(256)});
export type InvoiceObservation=z.infer<typeof invoiceObservation>;
export type ProcessorQuote={id:string;address:string;amountXmr:string;exchangeRate:string};
export type PayoutResult={status:'PENDING'|'FAILURE'|'SUCCESS';result:{amount:string;dest:string;status:'success'|'error';txids?:string[]}[]};
/** Explicit injection only. There is deliberately NO default/global fetch or credential-env loader.
* This interface exercises wire contracts with offline fake responses, not a live activation gate. */
export type FixtureTransport=(url:string,init:RequestInit)=>Promise<Response>;
export interface SettlementProcessor {
createInvoice(id:string,priceCents:number):Promise<ProcessorQuote>;
invoice(id:string):Promise<InvoiceObservation>;
payout(amount:string,destination:string):Promise<string>;
task(id:string):Promise<PayoutResult>;
}
export function createLiveProcessor():never{
throw new Error('Live SHKeeper activation is not implemented or approved. Mock mode only.');
}
export function createSHKeeperHarness(transport:FixtureTransport,credentials:{apiKey:string;username:string;password:string}):SettlementProcessor{
// Ephemeral fixtures are supplied by tests. Application startup never imports/constructs this harness.
if(!transport||typeof transport!=='function')throw new Error('Explicit offline fixture transport required');
for(const value of Object.values(credentials))if(!value||value.length>1024||/[\r\n\x00]/.test(value))throw new Error('Invalid fixture credentials');
if(credentials.username.includes(':'))throw new Error('Invalid Basic username');
const origin='https://shkeeper.eom.dev';
async function request(path:string,basic:boolean,body?:unknown):Promise<unknown>{
const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),5000);
const aborted=new Promise<never>((_,reject)=>{controller.signal.addEventListener('abort',()=>reject(new Error('Processor request deadline exceeded')),{once:true});});
const operation=(async()=>{
try{
const response=await transport(origin+path,{method:body===undefined?'GET':'POST',redirect:'error',signal:controller.signal,
headers:{Accept:'application/json',...(basic?{Authorization:`Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64')}`}:{'X-Shkeeper-Api-Key':credentials.apiKey}),
...(body===undefined?{}:{'Content-Type':'application/json'})},...(body===undefined?{}:{body:JSON.stringify(body)})});
if(!response.ok||response.redirected||!response.body||!/^application\/json(?:\s*;|$)/i.test(response.headers.get('content-type')??'')){
await response.body?.cancel();throw new Error('Processor response contract rejected');
}
const length=response.headers.get('content-length');
if(length!==null&&(!/^\d+$/.test(length)||Number(length)>256*1024)){await response.body.cancel();throw new Error('Processor response limit');}
const reader=response.body.getReader(),chunks:Uint8Array[]=[];let size=0;
try{for(;;){const part=await reader.read();if(part.done)break;size+=part.value.byteLength;if(size>256*1024)throw new Error('Processor response limit');chunks.push(part.value);}}
finally{await reader.cancel();reader.releaseLock();}
return JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(Buffer.concat(chunks)));
}catch{throw new Error('Processor operation failed or response could not be verified');} // Never include credentials/upstream body.
})();
try{return await Promise.race([operation,aborted]);}finally{clearTimeout(timer);}
}
return {
async createInvoice(id,priceCents){
external.parse(id);if(!Number.isSafeInteger(priceCents)||priceCents<1||priceCents>100_000_000)throw new Error('Invalid invoice price');
const amount=`${Math.floor(priceCents/100)}.${String(priceCents%100).padStart(2,'0')}`;
const raw=await request('/api/v1/XMR/payment_request',false,{external_id:id,fiat:'USD',amount,
callback_url:`https://market.invalid/disabled-callback/${id}`}); // No live callback route or token.
const q=z.object({status:z.literal('success'),id:identifier,wallet:z.string().length(95),amount:decimal,
exchange_rate:decimal,recalculate_after:z.union([z.literal(0),z.literal('0')])}).parse(raw);
await validateXmrAddress(q.wallet);if(atomicXmr(q.amount)<=0n||!/^\d{1,12}(\.\d{1,12})?$/.test(q.exchange_rate)||Number(q.exchange_rate)<=0)throw new Error('Invalid processor quote');
return {id:q.id,address:q.wallet,amountXmr:q.amount,exchangeRate:q.exchange_rate};
},
async invoice(id){
external.parse(id);
const data=z.object({status:z.literal('success'),invoices:z.array(invoiceObservation).max(2)}).parse(await request(`/api/v1/invoices/${encodeURIComponent(id)}`,false));
if(data.invoices.length!==1||data.invoices[0].external_id!==id)throw new Error('Invoice identity is missing or ambiguous');
const invoice=data.invoices[0];fiatCents(invoice.amount_fiat);fiatCents(invoice.balance_fiat);
const seen=new Set<string>();
for(const tx of invoice.txs){await validateXmrAddress(tx.addr);atomicXmr(tx.amount);if(seen.has(tx.txid))throw new Error('Duplicate invoice transaction');seen.add(tx.txid);}
return invoice;
},
async payout(amount,destination){
if(atomicXmr(amount)<=0n)throw new Error('Payout must be positive');await validateXmrAddress(destination);
const result=z.object({task_id:z.string().uuid()}).parse(await request('/api/v1/XMR/payout',true,{amount,destination,fee:'2'}));
return result.task_id;
},
async task(id){
z.string().uuid().parse(id);
const result=z.object({status:z.enum(['PENDING','FAILURE','SUCCESS']),result:z.array(z.object({amount:decimal,dest:z.string().length(95),
status:z.enum(['success','error']),txids:z.array(z.string().regex(/^[a-f0-9]{64}$/)).max(64).optional()})).max(2).default([])}).parse(await request(`/api/v1/XMR/task/${id}`,true));
for(const row of result.result){atomicXmr(row.amount);await validateXmrAddress(row.dest);}
return result;
},
};
}
+30
View File
@@ -0,0 +1,30 @@
import { keccak } from 'hash-wasm';
const alphabet='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const decodedSizes:Record<number,number>={2:1,3:2,5:3,6:4,7:5,9:6,10:7,11:8};
/** Support only mainnet standard/subaddresses. Integrated/payment-ID addresses need a separate policy. */
export async function validateXmrAddress(address:string):Promise<string>{
if(typeof address!=='string'||address.length!==95)throw new Error('Expected a mainnet standard XMR address or subaddress');
const bytes:number[]=[];
for(let start=0;start<address.length;start+=11){
const chunk=address.slice(start,start+11),size=decodedSizes[chunk.length];
if(!size)throw new Error('Invalid XMR Base58 block');
let value=0n;
for(const c of chunk){const index=alphabet.indexOf(c);if(index<0)throw new Error('Invalid XMR Base58 alphabet');value=value*58n+BigInt(index);}
const output=Array<number>(size).fill(0);
for(let i=size-1;i>=0;i--){output[i]=Number(value&255n);value>>=8n;}
if(value)throw new Error('XMR Base58 block overflow');bytes.push(...output);
}
if(bytes.length!==69||![18,42].includes(bytes[0]))throw new Error('XMR address network/type is not supported');
const decoded=Uint8Array.from(bytes),expected=(await keccak(decoded.slice(0,-4),256)).slice(0,8);
if(Buffer.from(decoded.slice(-4)).toString('hex')!==expected)throw new Error('XMR checksum mismatch');
return address;
}
export function atomicXmr(value:string):bigint{
if(typeof value!=='string'||!/^(0|[1-9]\d{0,9})(\.\d{1,12})?$/.test(value))throw new Error('Invalid bounded XMR decimal');
const [whole,fraction='']=value.split('.');return BigInt(whole)*1_000_000_000_000n+BigInt(fraction.padEnd(12,'0'));
}
export function fiatCents(value:string):number{
if(typeof value!=='string'||!/^(0|[1-9]\d{0,6})(\.\d{1,2})?$/.test(value))throw new Error('Invalid USD decimal');
const [whole,fraction='']=value.split('.');return Number(whole)*100+Number(fraction.padEnd(2,'0'));
}
+1 -1
View File
@@ -23,4 +23,4 @@ export function checkOperations(raw:unknown,now=Date.now()){
else if(disk.data.availableBytes<512*1024*1024)warnings.push('disk-low');
return {healthy:warnings.length===0,warnings,
note:'Local threshold hints only, not consensus readiness or launch approval. Reorg/rejection counters and native RSS need external time-series monitoring.'};
}
}
+1 -1
View File
@@ -60,4 +60,4 @@ export function startOperations(port: number | undefined, snapshot: ()=>unknown,
server.on('error',()=>{clearInterval(diskTimer);delay.disable();console.error('Private operations listener failed; check local monitoring configuration.');});
server.listen(port,'127.0.0.1');
return {close(){clearInterval(diskTimer);delay.disable();server.closeAllConnections();server.close();},address:()=>server.address() as AddressInfo|null};
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ export function launchPreflight(env: NodeJS.ProcessEnv = process.env, now = Date
{ id: 'public-node-resource-controls', status: 'blocked', detail: 'Stage-1 global RPC budgets, bounded pages/client history and read caching exist. In-memory DAG growth, full-history UI/reconciliation and production-scale load/capacity evidence remain unresolved.' },
{ id: 'consensus-launch-calibration', status: 'blocked', detail: 'Stage-3 bounded offline native benchmarks and deterministic retarget/timestamp analysis exist. Representative network hashrate, stochastic/multi-host adversarial evidence, reviewed launch rules/genesis and independent economic-security review remain missing.' },
{ id: 'wallet-key-protection', status: 'blocked', detail: 'Stage-2 encrypted vault storage, explicit migration/unlock and lock-aware signing exist. Legacy plaintext copies, unlocked-page compromise, migration/lifecycle coverage and independent production wallet/recovery review remain unresolved.' },
{ id: 'live-market-settlement', status: 'blocked', detail: 'No live SHKeeper adapter or reviewed payout/refund/reorg system exists. Mock payment cannot constitute real payment for valuable cards.' },
{ id: 'live-market-settlement', status: 'blocked', detail: 'Stage-5 offline SHKeeper wire/settlement prototypes and fake-processor failure tests exist. No live transport, production journal/UI/backup integration, reviewed confirmation/refund/custody policy or payment activation is enabled. Mock payment is not real payment.' },
{ id: 'release-security-review', status: 'unverified', detail: 'Independent consensus/application review and release-specific dependency/native/container scans require external evidence; an operator environment flag is not evidence.' },
{ id: 'deployment-rehearsal', status: 'unverified', detail: 'Stage-4 explicit proxy trust, private diagnostics/checker and stopped-node paired backup/restore with local native replay tests exist. Actual HTTPS/proxy topology, independent hosts/miners, alert delivery, off-host recovery and on-call procedures remain unverified.' },
{ id: 'fee-recipient-control', status: 'unverified', detail: 'Confirm the fixed public XMR fee address, checksum and recipient control out of band before any future live payout. No keys or proof are requested here.' },
+1 -1
View File
@@ -101,4 +101,4 @@ export async function restoreBackup(directory:string,destination:string){
return {network:NETWORK,chainId:CHAIN_ID,restored:true,proofsRevalidated:false,
warning:'SQLite/hash checks only. Replay with the reference engine in an isolated reviewed rehearsal before production use.'};
});
}
}