Markets
This commit is contained in:
+32
-2
@@ -6,7 +6,7 @@ import { constants, existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { delimiter, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { generateKeyPairSync, sign } from 'node:crypto';
|
||||
import { generateKeyPairSync, randomUUID, sign } from 'node:crypto';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
import { RandomX } from '../server/testnet/randomx.ts';
|
||||
@@ -14,6 +14,8 @@ import { address, CHAIN_ID, GENESIS_HASH, MAX_TARGET, miningBlob, mintId, sha256
|
||||
transferId, workFor } from '../server/testnet/consensus.ts';
|
||||
import { NETWORK, CONSENSUS, blockClaimMessage, nftTransferMessage,
|
||||
type MiningTemplate, type NftTransfer, type TestnetBlock, type TestnetSnapshot } from '../shared/testnet.ts';
|
||||
import { marketMessage, type MarketAction, type MarketInfo, type MarketListing, type MarketOrder,
|
||||
type MarketActivity } from '../shared/market.ts';
|
||||
|
||||
const root = fileURLToPath(new URL('../', import.meta.url));
|
||||
const wikipediaMock = resolve(root, 'tests', 'mock-wikipedia.mjs');
|
||||
@@ -81,7 +83,8 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
env: { ...process.env, HOST: '127.0.0.1', PORT: '0', NODE_ENV: 'production',
|
||||
WIKIDECK_DATA_DIR: dataDir, RANDOMX_BINARY: binary,
|
||||
WIKIDECK_PEERS: peer ?? '', WIKIDECK_ALLOW_PRIVATE_PEERS: peer ? '1' : '0',
|
||||
PUBLIC_ORIGIN: 'http://127.0.0.1:3001' },
|
||||
PUBLIC_ORIGIN: 'http://127.0.0.1:3001', WIKIDECK_PAYMENT_MODE: 'mock',
|
||||
WIKIDECK_MARKET_FEE_BPS: '250', WIKIDECK_MARKET_CONFIRMATIONS: '2' },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let output = '';
|
||||
@@ -131,6 +134,11 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
return (await request(base, path, expected, body)).json() as Promise<T>;
|
||||
}
|
||||
const snapshot = (base: string) => json<TestnetSnapshot>(base, '/api/chain');
|
||||
async function marketCommand<T>(base: string, info: MarketInfo, who: ReturnType<typeof wallet>, action: MarketAction) {
|
||||
const terms = { network: NETWORK, chainId: CHAIN_ID, marketId: info.marketId, actor: who.owner,
|
||||
publicKey: who.publicKey, nonce: randomUUID(), expiresAt: Date.now() + 60000, action };
|
||||
return json<T>(base, '/api/market/command', 200, { ...terms, signature: who.signed(marketMessage(terms)) });
|
||||
}
|
||||
async function waitFor<T>(label: string, read: () => Promise<T>, predicate: (value: T) => boolean): Promise<T> {
|
||||
const until = Date.now() + 25_000;
|
||||
let value: T;
|
||||
@@ -248,6 +256,19 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
assert.equal(aliceDeck.tip, block2.hash);
|
||||
assert.equal(aliceDeck.total, 2);
|
||||
assert.deepEqual(aliceDeck.nfts.map(item => item.block.tokenId), [block1.tokenId, block2.tokenId]);
|
||||
const market = await json<{ info: MarketInfo; listings: MarketListing[] }>(first.base, '/api/market');
|
||||
assert.equal(market.info.paymentMode, 'mock');
|
||||
assert.deepEqual(market.listings, []);
|
||||
await (await request(first.base, '/api/market/command', 400, {})).text();
|
||||
await (await request(first.base, '/api/market/command', 403, {}, { Origin: 'https://evil.example' })).text();
|
||||
const sale = await marketCommand<MarketListing>(first.base, market.info, alice, { type: 'list',
|
||||
cardHash: block1.tokenId, sequence: 0, kind: 'fixed', priceCents: 1500, endsAt: null, feeBps: 250 });
|
||||
assert.equal((await json<{ listings: MarketListing[] }>(first.base, '/api/market')).listings[0].id, sale.id);
|
||||
const order = await marketCommand<MarketOrder>(first.base, market.info, bob, { type: 'buy', listingId: sale.id,
|
||||
priceCents: 1500, feeBps: 250 });
|
||||
assert.equal(order.invoice.address, null);
|
||||
assert.equal(order.invoice.amountXmr, '0.100000000000');
|
||||
await marketCommand(first.base, market.info, bob, { type: 'mock-pay', orderId: order.id });
|
||||
await (await request(first.base, `/api/decks/${'z'.repeat(64)}`, 400)).text();
|
||||
|
||||
// A truly independent process reconstructs the canonical chain over HTTP and checks real work.
|
||||
@@ -262,6 +283,8 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
const badTransfer = await json<{ error: string }>(second.base, '/api/transfer', 400,
|
||||
{ ...transfer, signature: tamperSignature(transfer.signature) });
|
||||
assert.match(badTransfer.error, /signature/i);
|
||||
await marketCommand(first.base, market.info, alice, { type: 'deliver', orderId: order.id, transfer });
|
||||
assert.equal((await marketCommand<MarketActivity>(first.base, market.info, bob, { type: 'activity' })).orders[0].status, 'delivery-pending');
|
||||
const pending = { id: transferId(transfer), status: 'pending',
|
||||
message: 'Ownership changes only when this transaction is mined.' };
|
||||
assert.deepEqual(await json(second.base, '/api/transfer', 202, transfer), pending);
|
||||
@@ -299,6 +322,10 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
assert.deepEqual(canonical.blocks, [block1, block2, block3]);
|
||||
assert.deepEqual(canonical.transfers, [transfer]);
|
||||
assert.deepEqual(canonical.pendingTransfers, []);
|
||||
const settling = await marketCommand<MarketActivity>(first.base, market.info, bob, { type: 'activity' });
|
||||
assert.equal(settling.orders[0].deliveryConfirmations, 1);
|
||||
assert.equal(settling.orders[0].payout, 'not-started');
|
||||
assert.equal((await json<{ listings: MarketListing[] }>(second.base, '/api/market')).listings.length, 0);
|
||||
assert.equal(canonical.cumulativeWork, (3n * workFor(MAX_TARGET)).toString());
|
||||
for (const node of [first, second]) {
|
||||
const replay = await json<{ error: string }>(node.base, '/api/transfer', 400, transfer);
|
||||
@@ -331,6 +358,9 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
}
|
||||
first = await start(firstDir); // Ledger.open revalidates every persisted block using native RandomX.
|
||||
const restored = await snapshot(first.base);
|
||||
const restoredMarket = await json<{ info: MarketInfo }>(first.base, '/api/market');
|
||||
assert.equal(restoredMarket.info.marketId, market.info.marketId);
|
||||
assert.equal((await marketCommand<MarketActivity>(first.base, market.info, bob, { type: 'activity' })).orders[0].id, order.id);
|
||||
assert.deepEqual(restored, { ...canonical, peers: 0 });
|
||||
assert.deepEqual(await json(first.base, `/api/nfts/${block1.tokenId}`), {
|
||||
network: NETWORK, chainId: CHAIN_ID, tokenId: block1.tokenId, owner: bob.owner, sequence: 1,
|
||||
|
||||
Reference in New Issue
Block a user