Implemented the next deposit-observation step without activating live deposits
This commit is contained in:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user