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
+13 -6
View File
@@ -1,7 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { ADDITIONAL_MARKET_FEE } from '../shared/market.ts';
import { createMoneroWalletObserver, type MoneroFixtureTransport } from '../server/market/monero-observer.ts';
import { createMoneroWalletObserver, loadMoneroObserverConfig, type MoneroFixtureTransport } from '../server/market/monero-observer.ts';
const config = { network: 'mainnet' as const, url: 'http://127.0.0.1:18083/json_rpc', username: 'fixture', password: 'not-a-live-secret' };
function response(id: string, result: Record<string, unknown>, status = 200, type = 'application/json') {
@@ -23,15 +23,16 @@ function fakeTransport(incoming: Record<string, unknown>[] = []): { transport: M
}
test('read-only Monero observer preserves uint64 amount text and returns observations without crediting balances', async () => {
const { transport, calls } = fakeTransport([{ txid: 'a'.repeat(64), amount: '9007199254740993', height: 91,
const { transport, calls } = fakeTransport([{ txid: 'a'.repeat(64), address: ADDITIONAL_MARKET_FEE.address,
amount: '9007199254740993', height: 91,
timestamp: 1_800_000_000, unlock_time: 0, locked: false, double_spend_seen: false,
subaddr_index: { major: 0, minor: 7 }, address: ADDITIONAL_MARKET_FEE.address }]);
subaddr_index: { major: 0, minor: 7 } }]);
const observer = createMoneroWalletObserver(config, transport);
const page = await observer.scanIncoming(90);
assert.equal(page.network, 'mainnet'); assert.equal(page.walletHeight, 100);
assert.equal(page.primaryAddress, ADDITIONAL_MARKET_FEE.address);
assert.equal(page.incoming.length, 1);
assert.deepEqual(page.incoming[0], { observationId: `${'a'.repeat(64)}:0:7`, transactionId: 'a'.repeat(64),
assert.deepEqual(page.incoming[0], { observationId: `${'a'.repeat(64)}:0:7`, transactionId: 'a'.repeat(64), address: ADDITIONAL_MARKET_FEE.address,
amountAtomicXmr: '9007199254740993', height: 91, confirmations: 10, timestamp: 1_800_000_000,
unlockTime: '0', accountIndex: 0, addressIndex: 7, locked: false, doubleSpendSeen: false });
assert.deepEqual(calls.map(call => call.method).sort(), ['get_address', 'get_height', 'get_transfers']);
@@ -43,6 +44,7 @@ test('read-only Monero observer preserves uint64 amount text and returns observa
test('observer refuses non-loopback plaintext, URL credentials and malformed height ranges', async () => {
assert.throws(() => createMoneroWalletObserver({ ...config, url: 'http://wallet.example/json_rpc' }), /HTTPS/);
assert.throws(() => createMoneroWalletObserver({ ...config, url: 'http://localhost/json_rpc' }), /HTTPS/);
assert.throws(() => createMoneroWalletObserver({ ...config, url: 'https://user:pass@wallet.example/json_rpc' }), /bare/);
const { transport } = fakeTransport(); const observer = createMoneroWalletObserver(config, transport);
await assert.rejects(observer.scanIncoming(-1), /height/);
@@ -54,12 +56,12 @@ test('observer rejects unsafe amounts, duplicate observations, malformed transfe
const body = JSON.parse(init.body as string) as { id: string; method: string };
if (body.method === 'get_height') return response(body.id, { height: 100, status: 'OK' });
if (body.method === 'get_address') return response(body.id, { address: ADDITIONAL_MARKET_FEE.address, status: 'OK' });
return new Response(`{"jsonrpc":"2.0","id":"${body.id}","result":{"status":"OK","in":[{"txid":"${'b'.repeat(64)}","amount":18446744073709551616,"height":91,"timestamp":1,"unlock_time":0,"locked":false,"double_spend_seen":false,"subaddr_index":{"major":0,"minor":1}}]}}`,
return new Response(`{"jsonrpc":"2.0","id":"${body.id}","result":{"status":"OK","in":[{"txid":"${'b'.repeat(64)}","address":"${ADDITIONAL_MARKET_FEE.address}","amount":18446744073709551616,"height":91,"timestamp":1,"unlock_time":0,"locked":false,"double_spend_seen":false,"subaddr_index":{"major":0,"minor":1}}]}}`,
{ headers: { 'content-type': 'application/json' } });
};
await assert.rejects(createMoneroWalletObserver(config, unsafeTransport).scanIncoming(90), /amount/);
const duplicate = { txid: 'c'.repeat(64), amount: '1', height: 91, timestamp: 1, unlock_time: 0,
const duplicate = { txid: 'c'.repeat(64), address: ADDITIONAL_MARKET_FEE.address, amount: '1', height: 91, timestamp: 1, unlock_time: 0,
locked: true, double_spend_seen: false, subaddr_index: { major: 0, minor: 1 } };
const duplicateFixture = fakeTransport([duplicate, duplicate]);
await assert.rejects(createMoneroWalletObserver(config, duplicateFixture.transport).scanIncoming(90), /Duplicate/);
@@ -86,3 +88,8 @@ test('observer refuses redirects, wrong content type and oversized RPC response
error instanceof Error && !error.message.includes(config.password));
}
});
test('Monero RPC configuration is opt-in and inaccessible without a protected external config file', async () => {
await assert.rejects(loadMoneroObserverConfig({}), /disabled/);
await assert.rejects(loadMoneroObserverConfig({ WIKIDECK_MONERO_OBSERVER: '1' }), /config file is required/);
});