Added wallet-identity check before allocation
This commit is contained in:
@@ -238,6 +238,10 @@ export async function createOrRecoverMoneroDepositSubaddress(
|
||||
return { allocation: toAllocation(current.rows[0]), replayed: true };
|
||||
}
|
||||
const allocation = current.rows[0];
|
||||
const liveWalletFingerprint = await rpc.verifyWallet();
|
||||
if (liveWalletFingerprint !== allocation.wallet_fingerprint) {
|
||||
throw new Error('Configured Monero wallet identity differs from the durable allocation generation');
|
||||
}
|
||||
const expectedLabel = `wikideck-deposit-v1:${allocation.allocation_id}`;
|
||||
const expectedIndex = Number(allocation.address_index);
|
||||
const existing = await rpc.getSubaddress(Number(allocation.account_index), expectedIndex);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { lstat, readFile } from 'node:fs/promises';
|
||||
import { z } from 'zod';
|
||||
import { parseAtomicXmr, XMR_RPC_UINT64_MAX } from '../../shared/xmr-amount.ts';
|
||||
@@ -20,6 +20,8 @@ const rpcEnvelopeSchema = z.object({
|
||||
const integerString = z.string().regex(/^(0|[1-9]\d{0,19})$/);
|
||||
const heightResultSchema = z.object({ height: integerString, status: z.literal('OK') }).passthrough();
|
||||
const addressResultSchema = z.object({ address: z.string().length(95), addresses: z.array(z.unknown()).optional(), status: z.literal('OK') }).passthrough();
|
||||
const walletInfoResultSchema = z.object({ address: z.string().length(95), network_type: z.literal('Mainnet'),
|
||||
wallet_type: z.literal('Watch only') }).passthrough();
|
||||
const subaddressEntrySchema = z.object({ address: z.string().length(95), label: z.string().max(128),
|
||||
address_index: integerString, used: z.boolean() }).passthrough();
|
||||
const subaddressReadResultSchema = z.object({ address: z.string().length(95),
|
||||
@@ -131,10 +133,10 @@ export async function loadMoneroObserverConfig(env: NodeJS.ProcessEnv = process.
|
||||
|
||||
/** Restricted read-only surface. No generic RPC method, subaddress creation, transfer or sweep API exists. */
|
||||
function createWalletRpc(config: z.infer<typeof configSchema>, transport: MoneroFixtureTransport,
|
||||
allowedMethods: readonly ('get_height' | 'get_address' | 'get_transfers' | 'create_address')[]) {
|
||||
allowedMethods: readonly ('get_height' | 'get_address' | 'get_transfers' | 'get_wallet_info' | 'create_address')[]) {
|
||||
const endpoint = validateEndpoint(config.url);
|
||||
const authorization = `Basic ${Buffer.from(`${config.username}:${config.password}`).toString('base64')}`;
|
||||
async function rpc(method: 'get_height' | 'get_address' | 'get_transfers' | 'create_address', params: Record<string, unknown> = {}) {
|
||||
async function rpc(method: 'get_height' | 'get_address' | 'get_transfers' | 'get_wallet_info' | 'create_address', params: Record<string, unknown> = {}) {
|
||||
if (!allowedMethods.includes(method)) throw new Error('Monero wallet-RPC method is not allowed for this client');
|
||||
const id = randomUUID(), controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), RPC_TIMEOUT_MS); timeout.unref();
|
||||
@@ -219,18 +221,35 @@ export function createMoneroWalletObserver(config: z.infer<typeof configSchema>,
|
||||
}
|
||||
|
||||
export type MoneroDepositAddressRpc = {
|
||||
verifyWallet(): Promise<string>;
|
||||
getSubaddress(accountIndex: number, addressIndex: number): Promise<{ address: string; label: string } | null>;
|
||||
createSubaddress(accountIndex: number, label: string): Promise<{ address: string; addressIndex: number }>;
|
||||
};
|
||||
|
||||
/** Stable, non-secret identity of the wallet generation, bound to its verified mainnet primary address. */
|
||||
export async function moneroWalletFingerprint(primaryAddress: string): Promise<string> {
|
||||
await validateXmrAddress(primaryAddress);
|
||||
return createHash('sha256').update(JSON.stringify(['wikideck-monero-wallet-generation-v1', primaryAddress])).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow address-allocation capability; it can only inspect or create one subaddress at a
|
||||
* time. Keep this credential/service separate from API and withdrawal capabilities. This
|
||||
* Narrow address-allocation capability; it requires a verified watch-only mainnet wallet and
|
||||
* can only inspect or create one subaddress at a time. Keep this credential/service separate
|
||||
* from API and withdrawal capabilities. This
|
||||
* client is intentionally not attached to startup, routes, UI, or automatic crediting.
|
||||
*/
|
||||
export function createMoneroDepositAddressRpc(config: z.infer<typeof configSchema>, transport: MoneroFixtureTransport = fetch) {
|
||||
const rpc = createWalletRpc(config, transport, ['get_address', 'create_address']);
|
||||
const rpc = createWalletRpc(config, transport, ['get_address', 'get_wallet_info', 'create_address']);
|
||||
return {
|
||||
async verifyWallet(): Promise<string> {
|
||||
const [infoRaw, addressRaw] = await Promise.all([rpc('get_wallet_info'), rpc('get_address', { account_index: 0 })]);
|
||||
const info = walletInfoResultSchema.parse(infoRaw);
|
||||
const address = addressResultSchema.parse(addressRaw);
|
||||
await validateXmrAddress(info.address);
|
||||
await validateXmrAddress(address.address);
|
||||
if (info.address !== address.address) throw new Error('Monero wallet primary address identity mismatch');
|
||||
return moneroWalletFingerprint(info.address);
|
||||
},
|
||||
async getSubaddress(accountIndex: number, addressIndex: number): Promise<{ address: string; label: string } | null> {
|
||||
if (![accountIndex, addressIndex].every(value => Number.isSafeInteger(value) && value >= 0 && value <= 0xffff_ffff)) {
|
||||
throw new Error('Invalid Monero subaddress index');
|
||||
|
||||
Reference in New Issue
Block a user