Separate identity-only pilot custody foundation
This commit is contained in:
@@ -18,6 +18,19 @@ The independent private chain uses a fixed pilot genesis time and its own determ
|
||||
|
||||
This stack does **not** connect to the operator's Monero daemon, create real Monero deposit addresses, scan a wallet, approve real credits, construct or relay withdrawals, or settle NFT sales against active consensus. Having a full node supplies only the blockchain daemon; it is not a Wallet-RPC service, watch-only deposit wallet, or spending wallet. Deposit enrollment and withdrawal request flags remain rejected outside the mainnet profile, and mainnet remains separately gated. Do not expose the service publicly or solicit funds.
|
||||
|
||||
### Pilot custody identity groundwork (schema v10)
|
||||
|
||||
Schema v10 adds an immutable, identity-only binding with separate fields for the Wikideck market network/chain (`wikideck-randomx-pilot-1` and the pilot chain ID), Monero network (`mainnet`), custodian UUID, policy version and initialization time. It intentionally does not widen the existing allocation or withdrawal table constraints, initialize the mock sandbox's `custody_meta` identity, create addresses, grant runtime privileges, or enable any HTTP funding routes.
|
||||
|
||||
Use a **separate fresh custody database** for this identity exercise. The existing pilot Compose database contains mock sandbox custody and is not eligible; importing or reinterpreting its mock journals as Monero liabilities is prohibited. An operator migration is explicitly gated; identity initialization also refuses populated financial, mock, observer, allocation, credit, release or withdrawal tables.
|
||||
|
||||
After an operator creates/provisions a distinct empty database and its schema-owner connection, the CLI workflow is:
|
||||
|
||||
1. With the `pilot` profile selected, run `custody:db migrate-pilot-schema` with `WIKIDECK_PILOT_SCHEMA_MIGRATION=explicit-pilot-schema-migration-only`.
|
||||
2. Run `custody:db initialize-pilot-custody-identity <CUSTODIAN_UUID>` with `WIKIDECK_PILOT_MODE=private-mock-only`, `WIKIDECK_PILOT_CUSTODY_IDENTITY=initialize-pilot-custody-v1`, and `WIKIDECK_PILOT_CUSTODY_OPERATIONS=identity-only-no-funds`.
|
||||
|
||||
The schema migration does not grant runtime roles. The identity command is one-time and replay-idempotent only for the same pilot chain and custodian. The CLI refuses other custody actions on the pilot profile; mainnet keeps its existing separate controls. This is groundwork only: no customer deposits, financial posting, withdrawal intent or send is enabled by these commands.
|
||||
|
||||
### Reusing the operator-hosted daemon
|
||||
|
||||
The next integration should point the observer/gateway to the existing daemon rather than deploy or synchronize another one. An operator-triggered, read-only `get_info` probe is now available as a first connectivity check. It checks the expected Monero network, synchronized/local height, and daemon status; it does not open a wallet or provide a financial proof. The probe is opt-in and does not run on app startup.
|
||||
|
||||
@@ -7,9 +7,11 @@ Updated: 2026-10-22. Implementation plan for a private, operator-supervised func
|
||||
- **Step 1 started; mock-only foundation implemented.** Added a distinct `pilot` profile with its own network/genesis identity, explicit private-mock-only startup opt-in, separate local ledger/market/custody paths and browser wallet database, and a loopback-bound PostgreSQL sandbox stack. Setup and limits are in [the pilot deployment guide](real-xmr-pilot-deployment.md). Testnet v6 and mainnet candidate identities/gates were left intact.
|
||||
- **Operator infrastructure update:** a Monero full node is hosted on the operator's network and is intended to be reused; the pilot does not need to deploy or synchronize another daemon. Its network mode, RPC reachability from the pilot host/container, authentication/transport boundary and sync health still need verification. This does not provide Wallet-RPC, deposit observation, address allocation or a spending wallet.
|
||||
- **Next integration slice implemented:** added an operator-triggered, read-only `monerod get_info` probe, opt-in Compose check service, and mocked regression coverage. The Linux test image builds and focused probe/deployment tests pass (6/6). The probe validates expected Mainnet/Stagenet identity and synchronization, but has not been run against the operator's node because its private endpoint/configuration has not been provided. It does not enable wallet access, addresses, deposits, or credits. See [the probe setup and security boundary](real-xmr-pilot-deployment.md).
|
||||
- **Pilot identity/schema slice implemented:** schema v10 additively records a separate identity binding `wikideck-randomx-pilot-1` + current pilot chain ID to Monero `mainnet`, with an immutable `identity-only` activation status. Explicit pilot-only schema-migration and identity-initialization CLI actions require exact no-funds acknowledgements, reject non-pilot profiles, and refuse any target DB with pre-existing mock/financial/deposit data. No runtime roles/grants or deposit/withdrawal routes were added. Existing v1-v9 migration definitions/digests remain unchanged.
|
||||
- **Validation:** serial offline suite passed (latest run exited 0); PostgreSQL integration suite passed 42/42, including v1/v2 upgrades through v10, one-time pilot identity/replay/mismatch checks, dirty-database rejection, and no role access to the identity table. The isolated pilot identity operation itself requires a separate fresh database and has not been run outside disposable tests.
|
||||
- **Step 1 is not complete.** The current pilot stack does not connect to that external daemon and still has no Wallet-RPC gateway, observer, real-fund deposit or withdrawal path. It is useful for isolated UI/mock-flow work only and must not receive funds.
|
||||
- **Validation:** pilot-specific network/startup/storage/Compose tests pass (10/10); pilot runtime Docker image builds. The attempted serial full offline suite had one native RandomX HTTP peer-gossip convergence failure in `tests/api.test.ts`; the other reported tests passed. Treat full-suite status as not yet clean and rerun after the next increment.
|
||||
- **Steps 2–5:** not started. Existing deposit/withdrawal APIs still require the separately gated mainnet profile; sale-lock transactions remain candidate-only and are not accepted by active consensus.
|
||||
- **Steps 2–5:** not started. Existing deposit/withdrawal APIs still require the separately gated mainnet profile; the new pilot record is identity metadata only. Sale-lock transactions remain candidate-only and are not accepted by active consensus.
|
||||
|
||||
## Finish line
|
||||
|
||||
@@ -22,7 +24,7 @@ Use the existing pooled-custody architecture, PostgreSQL accounting, native Mone
|
||||
## Reuse what exists
|
||||
|
||||
- Exact atomic-XMR prices and fee calculations.
|
||||
- PostgreSQL schema v9 and accounting/reorg records.
|
||||
- PostgreSQL schema v10 and accounting/reorg records.
|
||||
- Signed deposit enrollment, observation worker and reviewed credit/release commands.
|
||||
- Signed balance reads and withdrawal reservation/cancellation APIs.
|
||||
- Marketplace/custody UI and executable sale-lock/accounting models.
|
||||
@@ -43,7 +45,7 @@ One Linux host, one authoritative market writer, one treasury tool/worker, fixed
|
||||
- Add one pilot deployment definition using existing Docker patterns: app, PostgreSQL, observer and private Wallet-RPC gateway. Reuse the operator-hosted full node over a verified private network connection; do not add a duplicate daemon service. Connect the observer/watch-only wallet and separate spending wallet explicitly. Keep daemon and Wallet-RPC endpoints private, authenticated and unavailable from the public Internet; supply credentials through protected files/secrets, not checked-in Compose values.
|
||||
- Include schema initialization, role grants and required configuration in a short setup procedure. Keep spending keys outside the web app and Wallet RPC off the public Internet.
|
||||
|
||||
**Progress:** A separate mock-only pilot network/profile, launch opt-in, profile-isolated local storage, PostgreSQL Compose deployment and setup guide now exist. An opt-in read-only daemon probe is implemented to verify private connectivity and `get_info` health from the Compose network; it still needs the operator's protected endpoint config and a successful real-node run. It does not connect the observer or Wallet-RPC. Wallet roles and all real-fund integrations remain outstanding. See [pilot deployment status and startup](real-xmr-pilot-deployment.md).
|
||||
**Progress:** A separate mock-only pilot network/profile, launch opt-in, profile-isolated local storage, PostgreSQL deployment, and opt-in read-only daemon probe now exist. The probe awaits the operator's protected endpoint config. Additive schema v10 and a pilot-only CLI bind the separate Wikideck pilot chain to Monero Mainnet in a fresh identity-only database; this grants no runtime access and cannot be used to enroll or credit deposits. Observer/Wallet-RPC roles and all real-fund integrations remain outstanding. See [pilot deployment status and startup](real-xmr-pilot-deployment.md).
|
||||
|
||||
**Result:** start the stack, open the UI, create two wallets and obtain a test card; verify the hosted daemon's intended network, synchronization and private RPC reachability from the pilot runtime. Do not start a duplicate daemon. Prepare the watch-only/observer and separately controlled spending-wallet services while application development proceeds. A Stagenet rehearsal still requires a Stagenet daemon endpoint; an operator's Mainnet daemon cannot substitute for it.
|
||||
|
||||
@@ -118,4 +120,4 @@ Keep the functional essentials: signed authorization, correct network binding, p
|
||||
|
||||
## Next coding task
|
||||
|
||||
**Run the read-only daemon check from the pilot Compose network once its protected endpoint config is available, then design the pilot's separate Wikideck-chain/Monero-mainnet financial identity and schema/role contract before connecting deposits.** No duplicate daemon is planned. Existing observer/deposit contracts are mainnet-specific, so do not simply enable their flags on the pilot. Keep the deployment mock-only until network binding, reviewed credit, actual withdrawal execution, recovery, and sale settlement are connected. Continue with the operator treasury tool and active sale-lock settlement as parallel implementation tracks where the identity contract permits. Completion means a buyer purchasing a card and the seller receiving withdrawable XMR—not another scaffold or release checklist.
|
||||
**Next: verify the pilot identity contract on its dedicated database, then design a separate pilot deposit request/observation adapter and migration/role boundaries without enabling HTTP enrollment.** Run the read-only daemon check when the protected endpoint config is available; no duplicate daemon is planned. Existing observer/deposit contracts are mainnet-Wikideck-specific, so do not simply enable their flags on the pilot. Keep deployment mock-only until network binding, reviewed credit, actual withdrawal execution, recovery, and sale settlement are connected. Continue treasury execution and active sale-lock settlement as parallel tracks once their shared identity boundary is defined. Completion means a buyer purchasing a card and the seller receiving withdrawable XMR—not another scaffold or release checklist.
|
||||
|
||||
+24
-5
@@ -7,11 +7,13 @@ import { migrateCustody, grantCustodyRuntime, grantMoneroObserverRuntime,
|
||||
import { initializeMainnetCustodyIdentity, parseMainnetCustodianId } from '../server/market/mainnet-custody-identity.ts';
|
||||
import { importSqliteCustody } from '../server/market/custody-import.ts';
|
||||
import { PostgresCustody } from '../server/market/postgres-custody.ts';
|
||||
import { IS_MAINNET } from '../shared/network.ts';
|
||||
import { IS_MAINNET, IS_PILOT } from '../shared/network.ts';
|
||||
import { initializePilotCustodyIdentity, parsePilotCustodianId } from '../server/market/pilot-custody-identity.ts';
|
||||
import { resolveCustodyIncident } from '../server/market/custody-incident.ts';
|
||||
import { loadMoneroDepositReviewApproval, loadMoneroDepositReviewPolicy } from '../server/market/monero-deposit-review-config.ts';
|
||||
import { assertCustodyDbActionAllowedOnNetwork, assertMainnetSchemaMigrationAcknowledged,
|
||||
assertMainnetCustodyOperationAcknowledged, parseCustodyDbArguments } from '../server/market/custody-db-args.ts';
|
||||
assertMainnetCustodyOperationAcknowledged, assertPilotCustodyActionAllowed, assertPilotSchemaMigrationAcknowledged,
|
||||
assertPilotCustodyIdentityAcknowledged, parseCustodyDbArguments } from '../server/market/custody-db-args.ts';
|
||||
import { prepareDepositCreditReview, postReviewedDepositPendingCredit, prepareDepositCreditReleaseReview,
|
||||
releaseReviewedDepositCredit, reconcileDepositCreditReorgHolds, prepareDepositReorgResolution,
|
||||
resolveReleasedDepositReorgHold, type DepositCreditApproval, type DepositCreditReleaseApproval,
|
||||
@@ -21,14 +23,19 @@ import { prepareDepositCreditReview, postReviewedDepositPendingCredit, prepareDe
|
||||
const commandArgs = process.argv.slice(2);
|
||||
const depositActions = ['deposit-prepare-credit', 'deposit-stage-credit', 'deposit-prepare-release', 'deposit-release',
|
||||
'deposit-reconcile-holds', 'deposit-prepare-resolve-hold', 'deposit-resolve-hold'];
|
||||
const custodyActions = ['migrate', 'migrate-mainnet-schema', 'initialize-mainnet-custody-identity', 'grant-runtime', 'grant-monero-observer-runtime',
|
||||
const custodyActions = ['migrate', 'migrate-mainnet-schema', 'initialize-mainnet-custody-identity', 'migrate-pilot-schema',
|
||||
'initialize-pilot-custody-identity', 'grant-runtime', 'grant-monero-observer-runtime',
|
||||
'grant-monero-withdrawal-runtime',
|
||||
'grant-monero-deposit-allocator-runtime', 'grant-monero-credit-runtime',
|
||||
'import-sqlite', 'audit', 'resolve-integrity'];
|
||||
const { action, argument, extraArgument } = parseCustodyDbArguments(commandArgs);
|
||||
assertCustodyDbActionAllowedOnNetwork(action, IS_MAINNET);
|
||||
assertPilotCustodyActionAllowed(action, IS_PILOT);
|
||||
assertMainnetSchemaMigrationAcknowledged(action, process.env.WIKIDECK_MAINNET_SCHEMA_MIGRATION);
|
||||
assertMainnetCustodyOperationAcknowledged(action, process.env.WIKIDECK_MAINNET_CUSTODY_OPERATIONS);
|
||||
assertPilotSchemaMigrationAcknowledged(action, process.env.WIKIDECK_PILOT_SCHEMA_MIGRATION);
|
||||
assertPilotCustodyIdentityAcknowledged(action, process.env.WIKIDECK_PILOT_CUSTODY_IDENTITY,
|
||||
process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS);
|
||||
if (action === 'initialize-mainnet-custody-identity' && process.env.WIKIDECK_MAINNET_CUSTODY_IDENTITY !== 'initialize-mainnet-custody-v1') {
|
||||
throw new Error('Mainnet custody identity initialization requires WIKIDECK_MAINNET_CUSTODY_IDENTITY=initialize-mainnet-custody-v1');
|
||||
}
|
||||
@@ -41,11 +48,13 @@ try {
|
||||
} else {
|
||||
const pool = new Pool(config);
|
||||
try {
|
||||
if (action === 'migrate' || action === 'migrate-mainnet-schema') {
|
||||
if (action === 'migrate' || action === 'migrate-mainnet-schema' || action === 'migrate-pilot-schema') {
|
||||
await migrateCustody(pool);
|
||||
console.log(action === 'migrate-mainnet-schema'
|
||||
? 'Mainnet custody schema verified/migrated; no runtime grants, deposits, or payments enabled.'
|
||||
: 'Custody PostgreSQL schema verified/migrated. No funds activated.');
|
||||
: action === 'migrate-pilot-schema'
|
||||
? 'Pilot custody schema verified/migrated; no pilot identity, runtime grants, deposits, or payments enabled.'
|
||||
: 'Custody PostgreSQL schema verified/migrated. No funds activated.');
|
||||
}
|
||||
if (action === 'initialize-mainnet-custody-identity') {
|
||||
if (!IS_MAINNET || extraArgument) throw new Error('Expected mainnet initialize-mainnet-custody-identity CUSTODIAN_UUID');
|
||||
@@ -53,6 +62,16 @@ try {
|
||||
console.log(JSON.stringify({ initialized: true, network: identity.network, chainId: identity.chainId,
|
||||
custodianId: identity.id, crediting: 'reviewed-pending-only', withdrawals: 'disabled' }));
|
||||
}
|
||||
if (action === 'initialize-pilot-custody-identity') {
|
||||
if (!IS_PILOT || process.env.WIKIDECK_PILOT_MODE !== 'private-mock-only' || extraArgument) {
|
||||
throw new Error('Expected pilot profile, private-mock-only mode, and one custodian UUID');
|
||||
}
|
||||
const identity = await initializePilotCustodyIdentity(pool, parsePilotCustodianId(argument ?? ''));
|
||||
console.log(JSON.stringify({ initialized: true, marketNetwork: identity.marketNetwork,
|
||||
marketChainId: identity.marketChainId, moneroNetwork: identity.moneroNetwork,
|
||||
custodianId: identity.custodianId, activation: identity.activationStatus,
|
||||
deposits: 'disabled', withdrawals: 'disabled' }));
|
||||
}
|
||||
if (action === 'grant-runtime') { await grantCustodyRuntime(pool, argument ?? ''); console.log('Runtime grants applied; no credentials printed.'); }
|
||||
if (action === 'grant-monero-observer-runtime') {
|
||||
await grantMoneroObserverRuntime(pool, argument ?? '');
|
||||
|
||||
@@ -8,4 +8,4 @@ try {
|
||||
console.error(JSON.stringify({ component: 'monero-daemon-probe', ready: false,
|
||||
reason: error instanceof Error ? error.message : 'Probe failed' }));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ export const CUSTODY_DB_ACTION_ARGUMENT_COUNTS = {
|
||||
migrate: 0,
|
||||
'migrate-mainnet-schema': 0,
|
||||
'initialize-mainnet-custody-identity': 1,
|
||||
'migrate-pilot-schema': 0,
|
||||
'initialize-pilot-custody-identity': 1,
|
||||
'grant-runtime': 1,
|
||||
'grant-monero-observer-runtime': 1,
|
||||
'grant-monero-deposit-allocator-runtime': 1,
|
||||
@@ -50,6 +52,29 @@ export function assertMainnetCustodyOperationAcknowledged(action: CustodyDbActio
|
||||
}
|
||||
}
|
||||
|
||||
export function assertPilotCustodyActionAllowed(action: CustodyDbAction, isPilot: boolean): void {
|
||||
const pilotActions = ['migrate-pilot-schema', 'initialize-pilot-custody-identity'];
|
||||
if ((isPilot && !pilotActions.includes(action)) || (!isPilot && pilotActions.includes(action))) {
|
||||
throw new Error(isPilot
|
||||
? 'Pilot profile permits only pilot schema migration and identity-only initialization through this CLI'
|
||||
: 'Pilot custody operation requires the pilot profile');
|
||||
}
|
||||
}
|
||||
|
||||
export function assertPilotSchemaMigrationAcknowledged(action: CustodyDbAction, acknowledgement: string | undefined): void {
|
||||
if (action === 'migrate-pilot-schema' && acknowledgement !== 'explicit-pilot-schema-migration-only') {
|
||||
throw new Error('Pilot schema migration requires explicit pilot schema-migration acknowledgement');
|
||||
}
|
||||
}
|
||||
|
||||
export function assertPilotCustodyIdentityAcknowledged(action: CustodyDbAction, identityAcknowledgement: string | undefined,
|
||||
operationAcknowledgement: string | undefined): void {
|
||||
if (action === 'initialize-pilot-custody-identity' &&
|
||||
(identityAcknowledgement !== 'initialize-pilot-custody-v1' || operationAcknowledgement !== 'identity-only-no-funds')) {
|
||||
throw new Error('Pilot identity initialization requires exact identity-only/no-funds acknowledgements');
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCustodyDbArguments(args: readonly string[]): CustodyDbArguments {
|
||||
const [action, ...values] = args;
|
||||
if (!action || !Object.hasOwn(CUSTODY_DB_ACTION_ARGUMENT_COUNTS, action)) {
|
||||
|
||||
Executable
+119
@@ -0,0 +1,119 @@
|
||||
import type { Pool, PoolClient } from 'pg';
|
||||
import { IS_PILOT, NETWORK } from '../../shared/network.ts';
|
||||
import { CHAIN_ID } from '../testnet/consensus.ts';
|
||||
import { CUSTODY_WRITE_LOCK, verifyCustodySchema } from './postgres-schema.ts';
|
||||
|
||||
export type PilotCustodyIdentity = {
|
||||
version: 1;
|
||||
mode: 'pilot-custody';
|
||||
policy: 'pilot-custody-v1';
|
||||
marketNetwork: 'wikideck-randomx-pilot-1';
|
||||
marketChainId: string;
|
||||
moneroNetwork: 'mainnet';
|
||||
custodianId: string;
|
||||
activationStatus: 'identity-only';
|
||||
initializedAt: number;
|
||||
};
|
||||
|
||||
function validUuid(value: string): boolean {
|
||||
return /^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value);
|
||||
}
|
||||
|
||||
function requirePilot(custodianId: string, initializedAt: number): void {
|
||||
if (!IS_PILOT || NETWORK !== 'wikideck-randomx-pilot-1' || !validUuid(custodianId)) {
|
||||
throw new Error('Pilot custody identity requires the pilot profile and a valid custodian UUID');
|
||||
}
|
||||
if (!Number.isSafeInteger(initializedAt) || initializedAt < 0) throw new Error('Invalid pilot identity initialization time');
|
||||
}
|
||||
|
||||
function identityFromRow(row: Record<string, unknown>): PilotCustodyIdentity {
|
||||
const identity = {
|
||||
version: Number(row.version), mode: row.mode, policy: row.policy,
|
||||
marketNetwork: row.market_network, marketChainId: row.market_chain_id,
|
||||
moneroNetwork: row.monero_network, custodianId: row.custodian_id,
|
||||
activationStatus: row.activation_status, initializedAt: Number(row.initialized_at),
|
||||
} as PilotCustodyIdentity;
|
||||
if (identity.version !== 1 || identity.mode !== 'pilot-custody' || identity.policy !== 'pilot-custody-v1' ||
|
||||
identity.marketNetwork !== 'wikideck-randomx-pilot-1' || identity.marketChainId !== CHAIN_ID ||
|
||||
identity.moneroNetwork !== 'mainnet' || !validUuid(identity.custodianId) ||
|
||||
identity.activationStatus !== 'identity-only' || !Number.isSafeInteger(identity.initializedAt) || identity.initializedAt < 0) {
|
||||
throw new Error('Stored pilot custody identity is invalid or inconsistent');
|
||||
}
|
||||
return identity;
|
||||
}
|
||||
|
||||
async function requireEmptyFinancialDatabase(client: PoolClient): Promise<void> {
|
||||
const result = await client.query(`SELECT
|
||||
EXISTS(SELECT 1 FROM wk_custody.custody_meta) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.accounts) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.journals) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.entries) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.listings) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.orders) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.withdrawals) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.receipts) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.outbox) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.credits) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.monero_observer_state) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.monero_transfer_observations) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.monero_observation_scans) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.monero_deposit_allocations) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.monero_deposit_credit_events) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.monero_deposit_reorg_holds) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.monero_deposit_release_events) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.monero_deposit_reorg_resolutions) OR
|
||||
EXISTS(SELECT 1 FROM wk_custody.monero_withdrawal_intents) AS populated`);
|
||||
if (result.rows[0].populated) {
|
||||
throw new Error('Pilot identity requires a separate empty custody database; existing mock or financial data cannot be reused');
|
||||
}
|
||||
}
|
||||
|
||||
/** One-time identity binding only. It does not enable deposit allocation, observation, credit, or withdrawal roles. */
|
||||
export async function initializePilotCustodyIdentity(pool: Pool, custodianId: string,
|
||||
initializedAt = Date.now()): Promise<PilotCustodyIdentity> {
|
||||
requirePilot(custodianId, initializedAt);
|
||||
const expected: PilotCustodyIdentity = { version: 1, mode: 'pilot-custody', policy: 'pilot-custody-v1',
|
||||
marketNetwork: 'wikideck-randomx-pilot-1', marketChainId: CHAIN_ID, moneroNetwork: 'mainnet',
|
||||
custodianId: custodianId.toLowerCase(), activationStatus: 'identity-only', initializedAt };
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await client.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL synchronous_commit=on");
|
||||
await client.query('SELECT pg_advisory_xact_lock($1::bigint)', [CUSTODY_WRITE_LOCK]);
|
||||
await verifyCustodySchema(client);
|
||||
const current = await client.query('SELECT * FROM wk_custody.pilot_custody_identity WHERE singleton=1 FOR UPDATE');
|
||||
if (current.rowCount) {
|
||||
const identity = identityFromRow(current.rows[0]);
|
||||
if (identity.version !== expected.version || identity.mode !== expected.mode || identity.policy !== expected.policy ||
|
||||
identity.marketNetwork !== expected.marketNetwork || identity.marketChainId !== expected.marketChainId ||
|
||||
identity.moneroNetwork !== expected.moneroNetwork || identity.custodianId !== expected.custodianId) {
|
||||
throw new Error('Pilot custody database is already bound to a different market, Monero network, or custodian');
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
return identity;
|
||||
}
|
||||
await requireEmptyFinancialDatabase(client);
|
||||
await client.query(`INSERT INTO wk_custody.pilot_custody_identity
|
||||
(singleton,version,mode,policy,market_network,market_chain_id,monero_network,custodian_id,activation_status,initialized_at)
|
||||
VALUES(1,$1,$2,$3,$4,$5,$6,$7,$8,$9)`, [expected.version, expected.mode, expected.policy,
|
||||
expected.marketNetwork, expected.marketChainId, expected.moneroNetwork, expected.custodianId,
|
||||
expected.activationStatus, expected.initializedAt]);
|
||||
await client.query('COMMIT');
|
||||
return expected;
|
||||
} catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; }
|
||||
finally { client.release(); }
|
||||
}
|
||||
|
||||
export async function verifyPilotCustodyIdentity(pool: Pool, custodianId: string): Promise<PilotCustodyIdentity> {
|
||||
requirePilot(custodianId, 0);
|
||||
const result = await pool.query('SELECT * FROM wk_custody.pilot_custody_identity WHERE singleton=1');
|
||||
if (result.rowCount !== 1) throw new Error('Pilot custody identity is not initialized');
|
||||
const identity = identityFromRow(result.rows[0]);
|
||||
if (identity.custodianId !== custodianId.toLowerCase()) throw new Error('Pilot custody identity does not match configured custodian');
|
||||
return identity;
|
||||
}
|
||||
|
||||
export function parsePilotCustodianId(value: string): string {
|
||||
if (!validUuid(value)) throw new Error('Invalid pilot custodian UUID');
|
||||
return value.toLowerCase();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import type { Pool, PoolClient } from 'pg';
|
||||
|
||||
export const CUSTODY_PG_VERSION = 9;
|
||||
export const CUSTODY_PG_VERSION = 10;
|
||||
export const CUSTODY_WRITE_LOCK = '7140824361';
|
||||
export const MONERO_OBSERVER_WORKER_LOCK = '7140824362';
|
||||
export const CUSTODY_SCHEMA_V1 = `
|
||||
@@ -372,9 +372,32 @@ REVOKE ALL ON FUNCTION wk_custody.monero_withdrawal_intents_immutable() FROM PUB
|
||||
`;
|
||||
export const CUSTODY_SCHEMA_V9_DIGEST = createHash('sha256')
|
||||
.update(`${CUSTODY_SCHEMA_V1}\n-- custody migration v2 --\n${CUSTODY_MIGRATION_V2}\n-- custody migration v3 --\n${CUSTODY_MIGRATION_V3}\n-- custody migration v4 --\n${CUSTODY_MIGRATION_V4}\n-- custody migration v5 --\n${CUSTODY_MIGRATION_V5}\n-- custody migration v6 --\n${CUSTODY_MIGRATION_V6}\n-- custody migration v7 --\n${CUSTODY_MIGRATION_V7}\n-- custody migration v8 --\n${CUSTODY_MIGRATION_V8}\n-- custody migration v9 --\n${CUSTODY_MIGRATION_V9}`).digest('hex');
|
||||
export const CUSTODY_MIGRATION_V10 = `
|
||||
CREATE TABLE wk_custody.pilot_custody_identity(
|
||||
singleton smallint PRIMARY KEY DEFAULT 1 CHECK(singleton=1),
|
||||
version integer NOT NULL CHECK(version=1),
|
||||
mode text NOT NULL CHECK(mode='pilot-custody'),
|
||||
policy text NOT NULL CHECK(policy='pilot-custody-v1'),
|
||||
market_network text NOT NULL CHECK(market_network='wikideck-randomx-pilot-1'),
|
||||
market_chain_id text NOT NULL CHECK(market_chain_id ~ '^[a-f0-9]{64}$'),
|
||||
monero_network text NOT NULL CHECK(monero_network='mainnet'),
|
||||
custodian_id uuid NOT NULL UNIQUE,
|
||||
activation_status text NOT NULL CHECK(activation_status='identity-only'),
|
||||
initialized_at bigint NOT NULL CHECK(initialized_at>=0)
|
||||
);
|
||||
CREATE FUNCTION wk_custody.pilot_custody_identity_immutable() RETURNS trigger LANGUAGE plpgsql
|
||||
SET search_path=wk_custody,pg_catalog AS $$
|
||||
BEGIN RAISE EXCEPTION 'Pilot custody identity is immutable'; END $$;
|
||||
CREATE TRIGGER pilot_custody_identity_immutable BEFORE UPDATE OR DELETE ON wk_custody.pilot_custody_identity
|
||||
FOR EACH ROW EXECUTE FUNCTION wk_custody.pilot_custody_identity_immutable();
|
||||
REVOKE ALL ON wk_custody.pilot_custody_identity FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION wk_custody.pilot_custody_identity_immutable() FROM PUBLIC;
|
||||
`;
|
||||
export const CUSTODY_SCHEMA_V10_DIGEST = createHash('sha256')
|
||||
.update(`${CUSTODY_SCHEMA_V1}\n-- custody migration v2 --\n${CUSTODY_MIGRATION_V2}\n-- custody migration v3 --\n${CUSTODY_MIGRATION_V3}\n-- custody migration v4 --\n${CUSTODY_MIGRATION_V4}\n-- custody migration v5 --\n${CUSTODY_MIGRATION_V5}\n-- custody migration v6 --\n${CUSTODY_MIGRATION_V6}\n-- custody migration v7 --\n${CUSTODY_MIGRATION_V7}\n-- custody migration v8 --\n${CUSTODY_MIGRATION_V8}\n-- custody migration v9 --\n${CUSTODY_MIGRATION_V9}\n-- custody migration v10 --\n${CUSTODY_MIGRATION_V10}`).digest('hex');
|
||||
/** Retained alias for callers that need the initial schema definition in disposable fixtures. */
|
||||
export const CUSTODY_SCHEMA = CUSTODY_SCHEMA_V1;
|
||||
export const CUSTODY_SCHEMA_DIGEST = CUSTODY_SCHEMA_V9_DIGEST;
|
||||
export const CUSTODY_SCHEMA_DIGEST = CUSTODY_SCHEMA_V10_DIGEST;
|
||||
|
||||
/** Explicit operator migration; the HTTP runtime never creates or upgrades schema. */
|
||||
export async function migrateCustody(pool: Pool): Promise<void> {
|
||||
@@ -427,6 +450,11 @@ export async function migrateCustody(pool: Pool): Promise<void> {
|
||||
if (current === 8) {
|
||||
await c.query(CUSTODY_MIGRATION_V9);
|
||||
await c.query('INSERT INTO wk_custody.schema_versions VALUES(9,$1)', [CUSTODY_SCHEMA_V9_DIGEST]);
|
||||
current = 9;
|
||||
}
|
||||
if (current === 9) {
|
||||
await c.query(CUSTODY_MIGRATION_V10);
|
||||
await c.query('INSERT INTO wk_custody.schema_versions VALUES(10,$1)', [CUSTODY_SCHEMA_V10_DIGEST]);
|
||||
}
|
||||
if (current > CUSTODY_PG_VERSION) throw new Error('Custody PostgreSQL schema is newer than this application');
|
||||
await verifyCustodySchema(c);
|
||||
@@ -442,7 +470,7 @@ async function verifyCustodySchemaHistory(c: PoolClient): Promise<number> {
|
||||
const rows = await c.query('SELECT version,digest FROM wk_custody.schema_versions ORDER BY version');
|
||||
const expected = [CUSTODY_SCHEMA_V1_DIGEST, CUSTODY_SCHEMA_V2_DIGEST, CUSTODY_SCHEMA_V3_DIGEST,
|
||||
CUSTODY_SCHEMA_V4_DIGEST, CUSTODY_SCHEMA_V5_DIGEST, CUSTODY_SCHEMA_V6_DIGEST, CUSTODY_SCHEMA_V7_DIGEST,
|
||||
CUSTODY_SCHEMA_V8_DIGEST, CUSTODY_SCHEMA_V9_DIGEST];
|
||||
CUSTODY_SCHEMA_V8_DIGEST, CUSTODY_SCHEMA_V9_DIGEST, CUSTODY_SCHEMA_V10_DIGEST];
|
||||
if (!rows.rowCount || rows.rowCount > expected.length || rows.rows.some((row, index) =>
|
||||
Number(row.version) !== index + 1 || row.digest !== expected[index])) {
|
||||
throw new Error('Custody PostgreSQL schema version/digest history mismatch; explicit migration required');
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { assertCustodyDbActionAllowedOnNetwork, assertMainnetSchemaMigrationAcknowledged,
|
||||
assertMainnetCustodyOperationAcknowledged, parseCustodyDbArguments } from '../server/market/custody-db-args.ts';
|
||||
assertMainnetCustodyOperationAcknowledged, assertPilotCustodyActionAllowed,
|
||||
assertPilotSchemaMigrationAcknowledged, assertPilotCustodyIdentityAcknowledged,
|
||||
parseCustodyDbArguments } from '../server/market/custody-db-args.ts';
|
||||
|
||||
test('custody database CLI parses two-argument deposit operations without shifting arguments', () => {
|
||||
assert.deepEqual(parseCustodyDbArguments(['deposit-prepare-credit', 'a'.repeat(64), `${'b'.repeat(64)}:${'c'.repeat(64)}`]), {
|
||||
@@ -19,6 +21,9 @@ test('custody database CLI parses two-argument deposit operations without shifti
|
||||
assert.deepEqual(parseCustodyDbArguments(['grant-monero-withdrawal-runtime', 'monero_withdrawal']), {
|
||||
action: 'grant-monero-withdrawal-runtime', argument: 'monero_withdrawal', extraArgument: undefined,
|
||||
});
|
||||
assert.deepEqual(parseCustodyDbArguments(['initialize-pilot-custody-identity', 'AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA']), {
|
||||
action: 'initialize-pilot-custody-identity', argument: 'AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA', extraArgument: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('mainnet custody CLI permits only scoped Monero infrastructure grants and schema migration', () => {
|
||||
@@ -53,6 +58,22 @@ test('mainnet reviewed deposit operations require exact credit-only acknowledgem
|
||||
assert.doesNotThrow(() => assertMainnetCustodyOperationAcknowledged('grant-monero-withdrawal-runtime', undefined));
|
||||
});
|
||||
|
||||
test('pilot custody CLI permits only additive schema migration and identity-only initialization', () => {
|
||||
assert.doesNotThrow(() => assertPilotCustodyActionAllowed('migrate-pilot-schema', true));
|
||||
assert.doesNotThrow(() => assertPilotCustodyActionAllowed('initialize-pilot-custody-identity', true));
|
||||
assert.throws(() => assertPilotCustodyActionAllowed('grant-monero-observer-runtime', true), /only pilot schema migration/);
|
||||
assert.throws(() => assertPilotCustodyActionAllowed('deposit-stage-credit', true), /only pilot schema migration/);
|
||||
assert.throws(() => assertPilotCustodyActionAllowed('initialize-pilot-custody-identity', false), /requires the pilot profile/);
|
||||
assert.throws(() => assertPilotSchemaMigrationAcknowledged('migrate-pilot-schema', undefined), /acknowledgement/);
|
||||
assert.throws(() => assertPilotSchemaMigrationAcknowledged('migrate-pilot-schema', 'yes'), /acknowledgement/);
|
||||
assert.doesNotThrow(() => assertPilotSchemaMigrationAcknowledged('migrate-pilot-schema', 'explicit-pilot-schema-migration-only'));
|
||||
assert.throws(() => assertPilotCustodyIdentityAcknowledged('initialize-pilot-custody-identity', undefined, undefined), /no-funds acknowledgements/);
|
||||
assert.throws(() => assertPilotCustodyIdentityAcknowledged('initialize-pilot-custody-identity',
|
||||
'initialize-pilot-custody-v1', 'reviewed-credit-only'), /no-funds acknowledgements/);
|
||||
assert.doesNotThrow(() => assertPilotCustodyIdentityAcknowledged('initialize-pilot-custody-identity',
|
||||
'initialize-pilot-custody-v1', 'identity-only-no-funds'));
|
||||
});
|
||||
|
||||
test('custody database CLI rejects unknown, missing, empty, and excess arguments', () => {
|
||||
for (const args of [[], ['unknown'], ['audit', 'extra'], ['deposit-prepare-credit', 'wallet'],
|
||||
['deposit-prepare-credit', 'wallet', 'observation', 'extra'], ['deposit-release', '']]) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { migrateCustody, grantCustodyRuntime, grantMoneroObserverRuntime, CUSTOD
|
||||
grantMoneroDepositCreditRuntime, grantMoneroDepositAllocatorRuntime, grantMoneroWithdrawalRuntime, CUSTODY_MIGRATION_V2,
|
||||
CUSTODY_SCHEMA_V2_DIGEST } from '../../server/market/postgres-schema.ts';
|
||||
import { parseMainnetCustodianId } from '../../server/market/mainnet-custody-identity.ts';
|
||||
import { initializePilotCustodyIdentity, parsePilotCustodianId } from '../../server/market/pilot-custody-identity.ts';
|
||||
import { readMainnetCustodyBalance, reserveMainnetWithdrawal, cancelMainnetWithdrawal } from '../../server/market/mainnet-withdrawal.ts';
|
||||
import { mainnetCustodyBalanceMessage, mainnetWithdrawalCancellationMessage, mainnetWithdrawalMessage,
|
||||
type MainnetCustodyBalanceRequestUnsigned, type MainnetWithdrawalRequestUnsigned,
|
||||
@@ -205,7 +206,7 @@ test('explicit PostgreSQL migrations are idempotent and runtime role cannot alte
|
||||
assert.equal((await f.market.audit()).assetsAtomicXmr, '1000000000000');
|
||||
});
|
||||
|
||||
test('custody schema v1 upgrades additively through v9 without dropping existing metadata', async t => {
|
||||
test('custody schema v1 upgrades additively through v10 without dropping existing metadata', async t => {
|
||||
const admin = new Pool(config);
|
||||
await admin.query('DROP SCHEMA IF EXISTS wk_custody CASCADE');
|
||||
const client = await admin.connect();
|
||||
@@ -219,13 +220,14 @@ test('custody schema v1 upgrades additively through v9 without dropping existing
|
||||
try {
|
||||
await migrateCustody(admin); await migrateCustody(admin);
|
||||
const versions = await admin.query('SELECT version FROM wk_custody.schema_versions ORDER BY version');
|
||||
assert.deepEqual(versions.rows.map(row => Number(row.version)), [1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
assert.deepEqual(versions.rows.map(row => Number(row.version)), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
assert.equal((await admin.query("SELECT value FROM wk_custody.custody_meta WHERE key='v1-preserved-marker'")).rows[0].value,
|
||||
'existing-test-data');
|
||||
for (const table of ['monero_observer_state', 'monero_transfer_observations', 'monero_observation_events', 'monero_observation_scans']) {
|
||||
assert.ok((await admin.query('SELECT to_regclass($1) AS name', [`wk_custody.${table}`])).rows[0].name);
|
||||
}
|
||||
assert.ok((await admin.query("SELECT to_regclass('wk_custody.monero_deposit_allocations') AS name")).rows[0].name);
|
||||
assert.ok((await admin.query("SELECT to_regclass('wk_custody.pilot_custody_identity') AS name")).rows[0].name);
|
||||
for (const table of ['monero_deposit_credit_events', 'monero_deposit_reorg_holds',
|
||||
'monero_deposit_release_events', 'monero_deposit_reorg_resolutions']) {
|
||||
assert.ok((await admin.query('SELECT to_regclass($1) AS name', [`wk_custody.${table}`])).rows[0].name);
|
||||
@@ -233,7 +235,7 @@ test('custody schema v1 upgrades additively through v9 without dropping existing
|
||||
} finally { await admin.end(); }
|
||||
});
|
||||
|
||||
test('custody schema v2 upgrades explicitly through v9 deposit and withdrawal lifecycle tables', async t => {
|
||||
test('custody schema v2 upgrades explicitly through v10 deposit and withdrawal lifecycle tables', async t => {
|
||||
const admin = new Pool(config);
|
||||
await admin.query('DROP SCHEMA IF EXISTS wk_custody CASCADE');
|
||||
const client = await admin.connect();
|
||||
@@ -246,14 +248,82 @@ test('custody schema v2 upgrades explicitly through v9 deposit and withdrawal li
|
||||
await client.query('COMMIT');
|
||||
await migrateCustody(admin); await migrateCustody(admin);
|
||||
const versions = await admin.query('SELECT version FROM wk_custody.schema_versions ORDER BY version');
|
||||
assert.deepEqual(versions.rows.map(row => Number(row.version)), [1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
assert.deepEqual(versions.rows.map(row => Number(row.version)), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
assert.equal((await admin.query("SELECT value FROM wk_custody.custody_meta WHERE key='v2-marker'")).rows[0].value, 'preserve');
|
||||
assert.ok((await admin.query("SELECT to_regclass('wk_custody.monero_deposit_release_events') AS name")).rows[0].name);
|
||||
assert.ok((await admin.query("SELECT to_regclass('wk_custody.monero_withdrawal_intents') AS name")).rows[0].name);
|
||||
assert.ok((await admin.query("SELECT to_regclass('wk_custody.pilot_custody_identity') AS name")).rows[0].name);
|
||||
} catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; }
|
||||
finally { client.release(); await admin.end(); }
|
||||
});
|
||||
|
||||
test('pilot custody identity binds the private market chain to Monero Mainnet without enabling funds', async t => {
|
||||
assert.equal(parsePilotCustodianId('AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA'), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa');
|
||||
assert.throws(() => parsePilotCustodianId('not-a-uuid'), /Invalid pilot custodian UUID/);
|
||||
const admin = new Pool(config), database = `pilot_identity_${randomUUID().replaceAll('-', '')}`;
|
||||
await admin.query(`CREATE DATABASE "${database}"`);
|
||||
const target = new Pool({ ...config, database });
|
||||
const run = promisify(execFile), custodianId = randomUUID(), runtimeRole = `pilot_identity_${randomUUID().replaceAll('-', '')}`;
|
||||
const code = `import { Pool } from 'pg';
|
||||
import { initializePilotCustodyIdentity } from './server/market/pilot-custody-identity.ts';
|
||||
const pool=new Pool({host:'127.0.0.1',database:process.env.TEST_DATABASE,user:'postgres',ssl:false});
|
||||
try { console.log(JSON.stringify(await initializePilotCustodyIdentity(pool,process.env.TEST_CUSTODIAN_ID,1234))); }
|
||||
finally { await pool.end(); }`;
|
||||
const env = { ...process.env, WIKIDECK_NETWORK: 'pilot', WIKIDECK_PILOT_MODE: 'private-mock-only',
|
||||
TEST_DATABASE: database, TEST_CUSTODIAN_ID: custodianId };
|
||||
try {
|
||||
await migrateCustody(target);
|
||||
await admin.query(`CREATE ROLE "${runtimeRole}" LOGIN`);
|
||||
assert.equal((await target.query('SELECT has_table_privilege($1,$2,$3) AS allowed',
|
||||
[runtimeRole, 'wk_custody.pilot_custody_identity', 'SELECT'])).rows[0].allowed, false);
|
||||
const first = JSON.parse((await run(process.execPath, ['--experimental-strip-types', '--input-type=module', '-e', code], { env })).stdout.trim());
|
||||
const replay = JSON.parse((await run(process.execPath, ['--experimental-strip-types', '--input-type=module', '-e', code], { env })).stdout.trim());
|
||||
assert.equal(first.mode, 'pilot-custody');
|
||||
assert.equal(first.marketNetwork, 'wikideck-randomx-pilot-1');
|
||||
assert.equal(first.moneroNetwork, 'mainnet');
|
||||
assert.equal(first.activationStatus, 'identity-only');
|
||||
assert.equal(first.custodianId, custodianId);
|
||||
assert.deepEqual(replay, first);
|
||||
await assert.rejects(run(process.execPath, ['--experimental-strip-types', '--input-type=module', '-e', code], {
|
||||
env: { ...env, TEST_CUSTODIAN_ID: randomUUID() },
|
||||
}));
|
||||
await assert.rejects(run(process.execPath, ['--experimental-strip-types', '--input-type=module', '-e', code], {
|
||||
env: { ...env, WIKIDECK_NETWORK: 'testnet' },
|
||||
}));
|
||||
const stored = await target.query('SELECT * FROM wk_custody.pilot_custody_identity');
|
||||
assert.equal(stored.rowCount, 1);
|
||||
assert.equal((await target.query('SELECT count(*)::int AS n FROM wk_custody.journals')).rows[0].n, 0);
|
||||
assert.equal((await target.query('SELECT count(*)::int AS n FROM wk_custody.monero_deposit_allocations')).rows[0].n, 0);
|
||||
assert.equal((await target.query('SELECT count(*)::int AS n FROM wk_custody.monero_withdrawal_intents')).rows[0].n, 0);
|
||||
assert.equal((await target.query("SELECT count(*)::int AS n FROM wk_custody.custody_meta WHERE key='identity'")).rows[0].n, 0);
|
||||
} finally {
|
||||
await target.end();
|
||||
await admin.query(`DROP DATABASE IF EXISTS "${database}" WITH (FORCE)`);
|
||||
await admin.query(`DROP ROLE IF EXISTS "${runtimeRole}"`);
|
||||
await admin.end();
|
||||
}
|
||||
});
|
||||
|
||||
test('pilot custody identity refuses a database containing pre-existing mock or financial data', async t => {
|
||||
const admin = new Pool(config), database = `pilot_identity_dirty_${randomUUID().replaceAll('-', '')}`;
|
||||
await admin.query(`CREATE DATABASE "${database}"`);
|
||||
const target = new Pool({ ...config, database }), run = promisify(execFile);
|
||||
try {
|
||||
await migrateCustody(target);
|
||||
await target.query("INSERT INTO wk_custody.accounts(id,normal) VALUES('pre-existing-mock','credit')");
|
||||
const code = `import { Pool } from 'pg';
|
||||
import { initializePilotCustodyIdentity } from './server/market/pilot-custody-identity.ts';
|
||||
const p=new Pool({host:'127.0.0.1',database:process.env.TEST_DATABASE,user:'postgres',ssl:false});
|
||||
try { await initializePilotCustodyIdentity(p,process.env.TEST_CUSTODIAN_ID,1234); }
|
||||
finally { await p.end(); }`;
|
||||
await assert.rejects(run(process.execPath, ['--experimental-strip-types', '--input-type=module', '-e', code], {
|
||||
env: { ...process.env, WIKIDECK_NETWORK: 'pilot', WIKIDECK_PILOT_MODE: 'private-mock-only',
|
||||
TEST_DATABASE: database, TEST_CUSTODIAN_ID: randomUUID() },
|
||||
}), /separate empty custody database/);
|
||||
assert.equal((await target.query('SELECT count(*)::int AS n FROM wk_custody.pilot_custody_identity')).rows[0].n, 0);
|
||||
} finally { await target.end(); await admin.query(`DROP DATABASE IF EXISTS "${database}" WITH (FORCE)`); await admin.end(); }
|
||||
});
|
||||
|
||||
test('mainnet custody identity bootstrap is one-time, chain-bound, and does not open withdrawals', async t => {
|
||||
assert.equal(parseMainnetCustodianId('AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA'), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa');
|
||||
assert.throws(() => parseMainnetCustodianId('not-a-uuid'), /Invalid mainnet custodian UUID/);
|
||||
|
||||
Reference in New Issue
Block a user