Implemented and validated the separate Monero observation-worker path
This commit is contained in:
@@ -8,6 +8,7 @@ const MAX_CONFIG_BYTES = 4096;
|
||||
const MAX_RESPONSE_BYTES = 1024 * 1024;
|
||||
const RPC_TIMEOUT_MS = 5000;
|
||||
const MAX_TRANSFERS = 4096;
|
||||
const MAX_SCAN_RANGE = 100_000;
|
||||
const configSchema = z.object({
|
||||
network: z.literal('mainnet'),
|
||||
url: z.string().min(1).max(2048),
|
||||
@@ -83,6 +84,9 @@ export type MoneroObservationPage = {
|
||||
primaryAddress: string;
|
||||
incoming: MoneroIncomingObservation[];
|
||||
};
|
||||
export type MoneroObservationScanner = {
|
||||
scanIncoming(fromHeight: number, maxBlocks?: number): Promise<MoneroObservationPage>;
|
||||
};
|
||||
export type MoneroFixtureTransport = (url: string, init: RequestInit) => Promise<Response>;
|
||||
|
||||
class MoneroWalletRpcError extends Error {
|
||||
@@ -215,15 +219,17 @@ export function createMoneroWalletObserver(config: z.infer<typeof configSchema>,
|
||||
const result = addressResultSchema.parse(await rpc('get_address', { account_index: 0 }));
|
||||
return validateXmrAddress(result.address);
|
||||
},
|
||||
async scanIncoming(fromHeight: number): Promise<MoneroObservationPage> {
|
||||
if (!Number.isSafeInteger(fromHeight) || fromHeight < 0) throw new Error('Invalid Monero scan height');
|
||||
async scanIncoming(fromHeight: number, maxBlocks = MAX_SCAN_RANGE): Promise<MoneroObservationPage> {
|
||||
if (!Number.isSafeInteger(fromHeight) || fromHeight < 0 || !Number.isSafeInteger(maxBlocks) ||
|
||||
maxBlocks < 1 || maxBlocks > MAX_SCAN_RANGE) throw new Error('Invalid Monero scan range');
|
||||
const [heightRaw, primaryAddress] = await Promise.all([
|
||||
rpc('get_height'), this.verifyMainnetWallet(),
|
||||
]);
|
||||
const walletHeight = integer(heightResultSchema.parse(heightRaw).height, BigInt(Number.MAX_SAFE_INTEGER), 'height');
|
||||
if (fromHeight > walletHeight) throw new Error('Monero observer cursor is ahead of wallet height');
|
||||
const throughHeight = Math.min(walletHeight, fromHeight + maxBlocks - 1);
|
||||
const transferRaw = transfersResultSchema.parse(await rpc('get_transfers', {
|
||||
in: true, filter_by_height: true, min_height: fromHeight, max_height: walletHeight,
|
||||
in: true, filter_by_height: true, min_height: fromHeight, max_height: throughHeight,
|
||||
}));
|
||||
if (transferRaw.in.length >= MAX_TRANSFERS) throw new Error('Monero transfer page reached its safety limit; refusing to advance scan cursor');
|
||||
const transferMetadata = new Map<string, z.infer<typeof incomingTransferSchema>>();
|
||||
@@ -264,7 +270,7 @@ export function createMoneroWalletObserver(config: z.infer<typeof configSchema>,
|
||||
if (!metadata) continue;
|
||||
const height = integer(output.block_height, BigInt(Number.MAX_SAFE_INTEGER), 'output height');
|
||||
const expectedHeight = integer(metadata.height, BigInt(Number.MAX_SAFE_INTEGER), 'transfer height');
|
||||
if (height !== expectedHeight || height < fromHeight || height > walletHeight) continue;
|
||||
if (height !== expectedHeight || height < fromHeight || height > throughHeight) continue;
|
||||
const amount = BigInt(output.amount);
|
||||
if (amount <= 0n || amount > XMR_RPC_UINT64_MAX) throw new Error('Invalid incoming Monero output amount');
|
||||
const globalOutputIndex = integer(output.global_index, BigInt(Number.MAX_SAFE_INTEGER), 'global output index');
|
||||
@@ -290,7 +296,7 @@ export function createMoneroWalletObserver(config: z.infer<typeof configSchema>,
|
||||
if (totals.get(key) !== BigInt(metadata.amount)) throw new Error('Wallet transaction amount does not match its owned output set');
|
||||
await validateXmrAddress(metadata.address);
|
||||
}
|
||||
return { network: 'mainnet', walletHeight, fromHeight, throughHeight: walletHeight, primaryAddress, incoming };
|
||||
return { network: 'mainnet', walletHeight, fromHeight, throughHeight, primaryAddress, incoming };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user