Ledger-side credit and withdrawal-intent paths
This commit is contained in:
+61
-3
@@ -20,11 +20,15 @@ import { marketCommandSchema } from '../../shared/market.ts';
|
||||
import { marketCommandV3Schema } from '../../shared/market-v3.ts';
|
||||
import { custodyCommandSchema } from '../../shared/custody.ts';
|
||||
import { moneroDepositAddressRequestSchema } from '../../shared/monero-deposit.ts';
|
||||
import { mainnetCustodyBalanceRequestSchema, mainnetWithdrawalRequestSchema,
|
||||
mainnetWithdrawalCancellationSchema } from '../../shared/monero-withdrawal.ts';
|
||||
import { CustodyMarket, CustodyError, custodyMode } from '../market/custody.ts';
|
||||
import { openCustodyStorage } from '../market/custody-storage.ts';
|
||||
import { PostgresCustody } from '../market/postgres-custody.ts';
|
||||
import { CanonicalObserver } from '../market/canonical-observer.ts';
|
||||
import { enrollMoneroDepositAddress, type MainnetEnrollmentContext } from '../market/monero-deposit-enrollment.ts';
|
||||
import { cancelMainnetWithdrawal, readMainnetCustodyBalance, reserveMainnetWithdrawal } from '../market/mainnet-withdrawal.ts';
|
||||
import { verifyMainnetCustodyIdentity } from '../market/mainnet-custody-identity.ts';
|
||||
import { createMoneroDepositAddressRpc, loadMoneroObserverConfig } from '../market/monero-observer.ts';
|
||||
import { loadMoneroMtlsTransport, loadMoneroMtlsTransportConfig } from '../market/monero-mtls-transport.ts';
|
||||
import { postgresConfig } from '../market/postgres-config.ts';
|
||||
@@ -50,6 +54,13 @@ if (depositEnrollmentMode !== '0' && depositEnrollmentMode !== '1') {
|
||||
if (depositEnrollmentMode === '1' && !IS_MAINNET) {
|
||||
throw new Error('Monero deposit enrollment requires the mainnet profile');
|
||||
}
|
||||
const withdrawalRequestMode = process.env.WIKIDECK_MONERO_WITHDRAWAL_REQUESTS ?? '0';
|
||||
if (withdrawalRequestMode !== '0' && withdrawalRequestMode !== '1') {
|
||||
throw new Error('WIKIDECK_MONERO_WITHDRAWAL_REQUESTS must be exactly 0 or 1');
|
||||
}
|
||||
if (withdrawalRequestMode === '1' && !IS_MAINNET) {
|
||||
throw new Error('Monero withdrawal requests require the mainnet profile');
|
||||
}
|
||||
if (canonicalObserverMode === '1' && (process.env.NODE_ENV === 'production' || custodyConfig !== 'sandbox' ||
|
||||
process.env.WIKIDECK_CUSTODY_STORAGE !== 'postgres' || process.env.WIKIDECK_PG_DISPOSABLE_TEST !== '1')) {
|
||||
throw new Error('Canonical observer rehearsal requires sandbox custody, PostgreSQL, and disposable test isolation');
|
||||
@@ -83,6 +94,8 @@ let custodyObserver: CanonicalObserver | undefined;
|
||||
let moneroDepositPool: Pool | undefined;
|
||||
let moneroDepositRpc: ReturnType<typeof createMoneroDepositAddressRpc> | undefined;
|
||||
let moneroDepositContext: MainnetEnrollmentContext | undefined;
|
||||
let moneroWithdrawalPool: Pool | undefined;
|
||||
let moneroWithdrawalCustodianId: string | undefined;
|
||||
try {
|
||||
if (custodyConfig === 'sandbox') custody = await openCustodyStorage(dataDir, { ...paymentConfig,
|
||||
card: (hash, owner, sequence) => {
|
||||
@@ -123,7 +136,18 @@ try {
|
||||
moneroDepositContext = { custodianId, chainId: CHAIN_ID, walletGenerationId, walletFingerprint: actualFingerprint,
|
||||
accountIndex: Number(accountText) };
|
||||
}
|
||||
} catch(e) {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;}
|
||||
if (withdrawalRequestMode === '1') {
|
||||
moneroWithdrawalCustodianId = z.string().uuid().parse(process.env.WIKIDECK_MONERO_WITHDRAWAL_CUSTODIAN_ID);
|
||||
const urlFile = process.env.WIKIDECK_MONERO_WITHDRAWAL_PG_URL_FILE;
|
||||
const caFile = process.env.WIKIDECK_MONERO_WITHDRAWAL_PG_CA_FILE;
|
||||
if (!urlFile || !caFile) throw new Error('Monero withdrawal request PostgreSQL URL and CA files are required');
|
||||
moneroWithdrawalPool = new Pool(await postgresConfig({ WIKIDECK_CUSTODY_PG_URL_FILE: urlFile,
|
||||
WIKIDECK_CUSTODY_PG_CA_FILE: caFile, WIKIDECK_CUSTODY_PG_TRANSPORT: 'tls' }));
|
||||
moneroWithdrawalPool.on('error', () => console.error('Monero withdrawal request database unavailable.'));
|
||||
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;}
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
if(proxyTrust.length)app.set('trust proxy',proxyTrust);
|
||||
@@ -194,6 +218,40 @@ if (depositEnrollmentMode === '1' && moneroDepositContext && moneroDepositRpc &&
|
||||
}
|
||||
});
|
||||
}
|
||||
if (withdrawalRequestMode === '1' && moneroWithdrawalPool && moneroWithdrawalCustodianId) {
|
||||
app.get('/api/monero/withdrawal-config', (_req,res) => {
|
||||
res.json({ network: 'wikideck-randomx-mainnet-1', chainId: CHAIN_ID,
|
||||
custodianId: moneroWithdrawalCustodianId, withdrawalRequestsEnabled: true,
|
||||
transactionSigningEnabled: false, transactionRelayEnabled: false });
|
||||
});
|
||||
app.post('/api/monero/custody-balance', rateLimit({windowMs:60000,limit:20,standardHeaders:'draft-8',legacyHeaders:false}), async (req,res) => {
|
||||
const parsed = mainnetCustodyBalanceRequestSchema.safeParse(req.body);
|
||||
if (!parsed.success) { res.status(400).json({ error: 'Invalid signed mainnet custody balance request.' }); return; }
|
||||
try { res.json(await readMainnetCustodyBalance(moneroWithdrawalPool!, parsed.data)); }
|
||||
catch { res.status(403).json({ error: 'Mainnet custody balance is unavailable for this signed request.' }); }
|
||||
});
|
||||
app.post('/api/monero/withdrawals', rateLimit({windowMs:60000,limit:5,standardHeaders:'draft-8',legacyHeaders:false}), async (req,res) => {
|
||||
const parsed = mainnetWithdrawalRequestSchema.safeParse(req.body);
|
||||
if (!parsed.success) { res.status(400).json({ error: 'Invalid signed mainnet withdrawal request.' }); return; }
|
||||
try {
|
||||
const result = await reserveMainnetWithdrawal(moneroWithdrawalPool!, parsed.data);
|
||||
res.status(202).json({ ...result, status: result.status === 'reserved' ? 'reserved-not-sent' : result.status,
|
||||
transactionSigningEnabled: false, transactionRelayEnabled: false });
|
||||
} catch {
|
||||
res.status(409).json({ error: 'Withdrawal request could not be reserved; verify signed terms and available balance.' });
|
||||
}
|
||||
});
|
||||
app.post('/api/monero/withdrawals/cancel', rateLimit({windowMs:60000,limit:5,standardHeaders:'draft-8',legacyHeaders:false}), async (req,res) => {
|
||||
const parsed = mainnetWithdrawalCancellationSchema.safeParse(req.body);
|
||||
if (!parsed.success) { res.status(400).json({ error: 'Invalid signed withdrawal cancellation.' }); return; }
|
||||
try {
|
||||
const result = await cancelMainnetWithdrawal(moneroWithdrawalPool!, parsed.data);
|
||||
res.json({ ...result, transactionSigningEnabled: false, transactionRelayEnabled: false });
|
||||
} catch {
|
||||
res.status(409).json({ error: 'Withdrawal cancellation could not be applied; it may already be cancelled or under operator processing.' });
|
||||
}
|
||||
});
|
||||
}
|
||||
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);
|
||||
@@ -340,6 +398,6 @@ for(const signal of ['SIGTERM','SIGINT']as const)process.once(signal,()=>{
|
||||
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 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 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;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);});
|
||||
|
||||
Reference in New Issue
Block a user