Wallet subaddress creation with retry recovery

This commit is contained in:
2026-10-21 15:12:05 -04:00
parent e70199ecb0
commit c8cf8245fc
6 changed files with 214 additions and 7 deletions
+29 -1
View File
@@ -17,7 +17,8 @@ import { CustodyMarket } from '../../server/market/custody.ts';
import { migrateCustody, grantCustodyRuntime, grantMoneroObserverRuntime, CUSTODY_SCHEMA_V1, CUSTODY_SCHEMA_V1_DIGEST,
CUSTODY_MIGRATION_V2, CUSTODY_SCHEMA_V2_DIGEST } from '../../server/market/postgres-schema.ts';
import { persistMoneroObservationScan } from '../../server/market/monero-observation-store.ts';
import { registerMoneroDepositSubaddress, reserveMoneroDepositAllocation } from '../../server/market/monero-deposit-allocation-store.ts';
import { createOrRecoverMoneroDepositSubaddress, registerMoneroDepositSubaddress,
reserveMoneroDepositAllocation } from '../../server/market/monero-deposit-allocation-store.ts';
import { importSqliteCustody } from '../../server/market/custody-import.ts';
import { postgresConfig, custodyStorage } from '../../server/market/postgres-config.ts';
import { openCustodyStorage } from '../../server/market/custody-storage.ts';
@@ -286,6 +287,33 @@ test('deposit allocation reserves unique beneficiary-bound indexes before RPC an
[first.allocation.allocationId]), /cannot be deleted/);
await assert.rejects(f.admin.query('UPDATE wk_custody.monero_deposit_allocations SET beneficiary=$2 WHERE allocation_id=$1',
[first.allocation.allocationId, other.actor]), /identity is immutable/);
const recoveryGeneration = randomUUID();
const recoveryReservation = await reserveMoneroDepositAllocation(f.admin, { ...input(randomUUID(), buyer.actor),
walletGenerationId: recoveryGeneration, walletFingerprint: 'a'.repeat(64) });
const walletAddresses = new Map<string, { address: string; label: string }>();
const recoveryAddress = await syntheticSubaddress(6);
let createCalls = 0;
const fakeRpc = {
async getSubaddress(accountIndex: number, addressIndex: number) {
return walletAddresses.get(`${accountIndex}:${addressIndex}`) ?? null;
},
async createSubaddress(accountIndex: number, label: string) {
createCalls++;
const row = { address: recoveryAddress, label };
walletAddresses.set(`${accountIndex}:${recoveryReservation.allocation.addressIndex}`, row);
if (createCalls === 1) throw new Error('fixture: RPC response lost after wallet persisted address');
return { address: row.address, addressIndex: recoveryReservation.allocation.addressIndex };
},
};
await assert.rejects(createOrRecoverMoneroDepositSubaddress(f.admin, fakeRpc,
recoveryReservation.allocation.allocationId, recoveryGeneration, reservedAt + 5), /response lost/);
const recovered = await createOrRecoverMoneroDepositSubaddress(f.admin, fakeRpc,
recoveryReservation.allocation.allocationId, recoveryGeneration, reservedAt + 6);
assert.equal(recovered.allocation.status, 'registered');
assert.equal(recovered.allocation.moneroAddress, recoveryAddress);
assert.equal(recovered.replayed, false);
assert.equal(createCalls, 1, 'retry must recover by deterministic wallet label rather than create a duplicate');
assert.equal((await f.admin.query('SELECT count(*)::int AS n FROM wk_custody.journals')).rows[0].n, 0,
'address allocation must never create financial postings');
} finally { await secondAllocatorProcess.end(); await custodyRuntime.end(); await observer.end(); }