Implemented the next deposit-observation step without activating live deposits

This commit is contained in:
2026-10-21 14:09:55 -04:00
parent 79abaa78f2
commit ee5dc35330
8 changed files with 395 additions and 24 deletions
+9 -5
View File
@@ -1,6 +1,5 @@
import { randomUUID } from 'node:crypto';
import { lstat, readFile } from 'node:fs/promises';
import { isIP } from 'node:net';
import { z } from 'zod';
import { parseAtomicXmr, XMR_RPC_UINT64_MAX } from '../../shared/xmr-amount.ts';
import { validateXmrAddress } from './xmr.ts';
@@ -23,6 +22,7 @@ const heightResultSchema = z.object({ height: integerString, status: z.literal('
const addressResultSchema = z.object({ address: z.string().length(95), addresses: z.array(z.unknown()).optional(), status: z.literal('OK') }).passthrough();
const incomingTransferSchema = z.object({
txid: z.string().regex(/^[a-f0-9]{64}$/),
address: z.string().length(95),
amount: integerString,
height: integerString,
timestamp: integerString,
@@ -39,6 +39,7 @@ const transfersResultSchema = z.object({
export type MoneroIncomingObservation = {
observationId: string;
transactionId: string;
address: string;
amountAtomicXmr: string;
height: number;
confirmations: number;
@@ -96,7 +97,7 @@ function validateEndpoint(value: string): URL {
if (url.username || url.password || url.search || url.hash || url.pathname !== '/json_rpc') {
throw new Error('Monero wallet-RPC endpoint must be a bare /json_rpc URL');
}
const loopback = ['localhost', '127.0.0.1', '::1', '[::1]'].includes(url.hostname) || isIP(url.hostname) === 6 && url.hostname === '[::1]';
const loopback = url.hostname === '127.0.0.1' || url.hostname === '[::1]';
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) {
throw new Error('Wallet RPC requires HTTPS, except loopback-only HTTP');
}
@@ -179,19 +180,22 @@ export function createMoneroWalletObserver(config: z.infer<typeof configSchema>,
const transferRaw = transfersResultSchema.parse(await rpc('get_transfers', {
in: true, filter_by_height: true, min_height: fromHeight, max_height: walletHeight,
}));
const incoming = transferRaw.in.map(item => {
if (transferRaw.in.length >= MAX_TRANSFERS) throw new Error('Monero transfer page reached its safety limit; refusing to advance scan cursor');
const incoming = await Promise.all(transferRaw.in.map(async item => {
const amount = BigInt(item.amount);
if (amount <= 0n || amount > XMR_RPC_UINT64_MAX) throw new Error('Invalid incoming Monero amount');
const address = item.address;
await validateXmrAddress(address);
const height = integer(item.height, BigInt(Number.MAX_SAFE_INTEGER), 'transfer height');
if (height < fromHeight || height > walletHeight) throw new Error('Wallet returned transfer outside requested height range');
const major = integer(item.subaddr_index.major, 0xffffffffn, 'account index');
const minor = integer(item.subaddr_index.minor, 0xffffffffn, 'address index');
return { observationId: `${item.txid}:${major}:${minor}`, transactionId: item.txid,
return { observationId: `${item.txid}:${major}:${minor}`, transactionId: item.txid, address,
amountAtomicXmr: amount.toString(), height, confirmations: walletHeight - height + 1,
timestamp: integer(item.timestamp, BigInt(Number.MAX_SAFE_INTEGER), 'timestamp'),
unlockTime: item.unlock_time, accountIndex: major, addressIndex: minor,
locked: item.locked, doubleSpendSeen: item.double_spend_seen };
});
}));
const unique = new Set(incoming.map(row => row.observationId));
if (unique.size !== incoming.length) throw new Error('Duplicate Monero wallet output observation');
return { network: 'mainnet', walletHeight, fromHeight, throughHeight: walletHeight, primaryAddress, incoming };