Planning
This commit is contained in:
+21
-13
@@ -14,8 +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';
|
||||
import { type MarketAction, type MarketInfo } from '../shared/market.ts';
|
||||
import { marketMessageV3, type MarketActionV3, type MarketActivityV3, type MarketListingV3, type MarketOrderV3 } from '../shared/market-v3.ts';
|
||||
import { StateClient } from '../shared/rpc-client.ts';
|
||||
import { createBackup,restoreBackup } from '../server/recovery.ts';
|
||||
import { Ledger } from '../server/testnet/ledger.ts';
|
||||
@@ -139,9 +139,17 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
}
|
||||
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)) });
|
||||
const atomicFromCents = (cents: number) => ((BigInt(cents) * 1_000_000_000_000n + 14999n) / 15000n).toString();
|
||||
let current: MarketActionV3;
|
||||
if (action.type === 'list') current = { type: 'list', cardHash: action.cardHash, sequence: action.sequence,
|
||||
kind: action.kind, priceAtomicXmr: atomicFromCents(action.priceCents), endsAt: action.endsAt, feeBps: action.feeBps };
|
||||
else if (action.type === 'buy') current = { type: 'buy', listingId: action.listingId,
|
||||
priceAtomicXmr: atomicFromCents(action.priceCents), feeBps: action.feeBps };
|
||||
else if (action.type === 'bid') current = { type: 'bid', listingId: action.listingId, priceAtomicXmr: atomicFromCents(action.priceCents) };
|
||||
else current = action;
|
||||
const terms = { version: 3 as const, network: NETWORK, chainId: CHAIN_ID, marketId: info.marketId, actor: who.owner,
|
||||
publicKey: who.publicKey, nonce: randomUUID(), expiresAt: Date.now() + 60000, action: current };
|
||||
return json<T>(base, '/api/market/command', 200, { ...terms, signature: who.signed(marketMessageV3(terms)) });
|
||||
}
|
||||
async function waitFor<T>(label: string, read: () => Promise<T>, predicate: (value: T) => boolean): Promise<T> {
|
||||
const until = Date.now() + 25_000;
|
||||
@@ -264,15 +272,15 @@ 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');
|
||||
const market = await json<{ info: MarketInfo; listings: MarketListingV3[] }>(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',
|
||||
const sale = await marketCommand<MarketListingV3>(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,
|
||||
assert.equal((await json<{ listings: MarketListingV3[] }>(first.base, '/api/market')).listings[0].id, sale.id);
|
||||
const order = await marketCommand<MarketOrderV3>(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');
|
||||
@@ -292,7 +300,7 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
{ ...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');
|
||||
assert.equal((await marketCommand<MarketActivityV3>(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);
|
||||
@@ -330,10 +338,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' });
|
||||
const settling = await marketCommand<MarketActivityV3>(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((await json<{ listings: MarketListingV3[] }>(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);
|
||||
@@ -383,7 +391,7 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
assert.deepEqual(await pagedClient.load(),restored);
|
||||
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.equal((await marketCommand<MarketActivityV3>(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,
|
||||
|
||||
+108
-22
@@ -7,8 +7,9 @@ import { tmpdir } from 'node:os';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { Market, marketConfig } from '../server/market/store.ts';
|
||||
import { mockQuote, paymentMode } from '../server/market/payments.ts';
|
||||
import { ADDITIONAL_MARKET_FEE, marketMessage, parseUsd, type MarketAction, type MarketListing, type MarketOrder,
|
||||
type MarketActivity } from '../shared/market.ts';
|
||||
import { ADDITIONAL_MARKET_FEE, marketMessage, parseUsd, type MarketAction } from '../shared/market.ts';
|
||||
import { marketActionV3Schema, marketMessageV3, type MarketActionV3, type MarketActivityV3,
|
||||
type MarketListingV3, type MarketOrderV3 } from '../shared/market-v3.ts';
|
||||
import { NETWORK, nftTransferMessage, type NftTransfer, type TestnetSnapshot, type TestnetBlock } from '../shared/testnet.ts';
|
||||
import { address, CHAIN_ID, GENESIS_HASH, transferId, verifySignature } from '../server/testnet/consensus.ts';
|
||||
|
||||
@@ -38,20 +39,35 @@ async function fixture(t: { after(fn: () => Promise<void>): void }) {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'wikideck-market-'));
|
||||
const chain = chainFixture();
|
||||
let time = 1_800_000_000_000;
|
||||
let market = await Market.open(dir, chain, { feeBps: 250, confirmations: 2, usdCentsPerXmr: 15000 }, () => time);
|
||||
let market = await Market.open(dir, chain, { feeBps: 250, confirmations: 2 }, () => time);
|
||||
t.after(async () => { market.close(); await rm(dir, { recursive: true, force: true }); });
|
||||
const envelope = (who: typeof alice, action: MarketAction) => {
|
||||
const terms = { network: NETWORK, chainId: CHAIN_ID, marketId: market.id, actor: who.actor,
|
||||
publicKey: who.publicKey, nonce: randomUUID(), expiresAt: time + 60000, action };
|
||||
return { ...terms, signature: who.signed(marketMessage(terms)) };
|
||||
};
|
||||
const run = <T = MarketListing | MarketOrder>(who: typeof alice, action: MarketAction) => market.execute(envelope(who, action)) as Promise<T>;
|
||||
const list = (kind: 'fixed' | 'auction' = 'fixed') => run<MarketListing>(alice, { type: 'list', cardHash: chain.state.blocks[0].tokenId,
|
||||
const atomicFromCents = (cents: number) => ((BigInt(cents) * 1_000_000_000_000n + 14999n) / 15000n).toString();
|
||||
const toV3 = (action: MarketAction): MarketActionV3 => {
|
||||
if (action.type === 'list') return { type: 'list', cardHash: action.cardHash, sequence: action.sequence,
|
||||
kind: action.kind, priceAtomicXmr: atomicFromCents(action.priceCents), endsAt: action.endsAt, feeBps: action.feeBps };
|
||||
if (action.type === 'bid') return { type: 'bid', listingId: action.listingId, priceAtomicXmr: atomicFromCents(action.priceCents) };
|
||||
if (action.type === 'buy') return { type: 'buy', listingId: action.listingId,
|
||||
priceAtomicXmr: atomicFromCents(action.priceCents), feeBps: action.feeBps };
|
||||
return action;
|
||||
};
|
||||
const envelopeV3 = (who: typeof alice, action: MarketActionV3) => {
|
||||
const terms = { version: 3 as const, network: NETWORK, chainId: CHAIN_ID, marketId: market.id, actor: who.actor,
|
||||
publicKey: who.publicKey, nonce: randomUUID(), expiresAt: time + 60000, action };
|
||||
return { ...terms, signature: who.signed(marketMessageV3(terms)) };
|
||||
};
|
||||
const run = <T = MarketListingV3 | MarketOrderV3>(who: typeof alice, action: MarketAction) =>
|
||||
market.execute(envelopeV3(who, toV3(action))) as Promise<T>;
|
||||
const list = (kind: 'fixed' | 'auction' = 'fixed') => run<MarketListingV3>(alice, { type: 'list', cardHash: chain.state.blocks[0].tokenId,
|
||||
sequence: 0, kind, priceCents: 1500, endsAt: kind === 'auction' ? time + 60000 : null, feeBps: 250 });
|
||||
const buy = (l: MarketListing, who = bob, priceCents = l.priceCents) => run<MarketOrder>(who,
|
||||
const buy = (l: MarketListingV3, who = bob, priceCents = 1500) => run<MarketOrderV3>(who,
|
||||
{ type: 'buy', listingId: l.id, priceCents, feeBps: l.feeBps });
|
||||
const orders = async (who = bob) => (await run<MarketActivity>(who, { type: 'activity' })).orders;
|
||||
const delivery = (o: MarketOrder, to = bob) => {
|
||||
const orders = async (who = bob) => (await run<MarketActivityV3>(who, { type: 'activity' })).orders;
|
||||
const delivery = (o: Pick<MarketOrderV3, 'cardHash' | 'sequence'>, to = bob) => {
|
||||
const terms = { network: NETWORK, chainId: CHAIN_ID, cardHash: o.cardHash, from: alice.actor, to: to.actor, sequence: o.sequence };
|
||||
return { ...terms, publicKey: alice.publicKey, signature: alice.signed(nftTransferMessage(terms)) };
|
||||
};
|
||||
@@ -60,10 +76,10 @@ async function fixture(t: { after(fn: () => Promise<void>): void }) {
|
||||
chain.state.blocks.push({ ...chain.state.blocks[0], height, transactions: [tx] });
|
||||
chain.state.transfers.push(tx); chain.state.pendingTransfers = [];
|
||||
};
|
||||
return { dir, chain, envelope, run, list, buy, orders, delivery, confirm,
|
||||
return { dir, chain, envelope, envelopeV3, run, list, buy, orders, delivery, confirm,
|
||||
get market() { return market; }, get now() { return time; }, advance: (ms: number) => { time += ms; },
|
||||
restart: async () => { market.close(); market = await Market.open(dir, chain,
|
||||
{ feeBps: 500, confirmations: 5, usdCentsPerXmr: 20000 }, () => time); } };
|
||||
{ feeBps: 500, confirmations: 5 }, () => time); } };
|
||||
}
|
||||
|
||||
test('USD and mock XMR accounting use bounded integer amounts and exact fee splitting', () => {
|
||||
@@ -95,29 +111,99 @@ test('USD and mock XMR accounting use bounded integer amounts and exact fee spli
|
||||
}
|
||||
});
|
||||
|
||||
test('market commands bind actor, instance, network, expiry, and every listing term', async t => {
|
||||
test('versioned market commands bind actor, instance, network, expiry, currency, and every price term', async t => {
|
||||
const f = await fixture(t);
|
||||
const original = f.envelope(alice, { type: 'list', cardHash: f.chain.state.blocks[0].tokenId, sequence: 0,
|
||||
kind: 'fixed', priceCents: 1500, endsAt: null, feeBps: 250 });
|
||||
const original = f.envelopeV3(alice, { type: 'list', cardHash: f.chain.state.blocks[0].tokenId, sequence: 0,
|
||||
kind: 'fixed', priceAtomicXmr: '100000000000', endsAt: null, feeBps: 250 });
|
||||
for (const bad of [
|
||||
{ ...original, actor: bob.actor }, { ...original, marketId: randomUUID() },
|
||||
{ ...original, chainId: '0'.repeat(64) }, { ...original, expiresAt: f.now - 1 },
|
||||
{ ...original, expiresAt: f.now + 600_000 },
|
||||
{ ...original, action: { ...original.action, priceCents: 1 } },
|
||||
{ ...original, action: { ...original.action, priceAtomicXmr: '1' } },
|
||||
]) await assert.rejects(async () => f.market.execute(bad), /signature/);
|
||||
await assert.rejects(async () => f.market.execute({ ...original, network: 'wrong-network' }));
|
||||
const first = await f.market.execute(original);
|
||||
const message = JSON.parse(marketMessage(original));
|
||||
assert.equal(message[1], 'market-command-v2');
|
||||
const message = JSON.parse(marketMessageV3(original));
|
||||
assert.equal(message[1], 'market-command-v3');
|
||||
assert.deepEqual(message.at(-1), [100, ADDITIONAL_MARKET_FEE.address]);
|
||||
message[message.length - 1] = [0, ADDITIONAL_MARKET_FEE.address];
|
||||
await assert.rejects(async () => f.market.execute({ ...original, signature: alice.signed(JSON.stringify(message)) }), /signature/);
|
||||
assert.deepEqual(await f.market.execute(original), first);
|
||||
const changed = { ...original, action: { ...original.action, priceCents: 1600 } };
|
||||
await assert.rejects(async () => f.market.execute({ ...changed, signature: alice.signed(marketMessage(changed)) }), /Nonce/);
|
||||
const changed = { ...original, action: { ...original.action, priceAtomicXmr: '200000000000' } };
|
||||
await assert.rejects(async () => f.market.execute({ ...changed, signature: alice.signed(marketMessageV3(changed)) }), /Nonce/);
|
||||
const legacy = f.envelope(alice, { type: 'list', cardHash: f.chain.state.blocks[1].tokenId, sequence: 0,
|
||||
kind: 'fixed', priceCents: 1500, endsAt: null, feeBps: 250 });
|
||||
assert.equal(JSON.parse(marketMessage(legacy))[1], 'market-command-v2');
|
||||
await assert.rejects(f.market.execute(legacy), /USD listing terms are retired/);
|
||||
assert.equal(f.market.browse().listings.length, 1);
|
||||
});
|
||||
|
||||
test('legacy USD listings are read-only until seller retires them with v3 authorization', async t => {
|
||||
const f = await fixture(t), cardHash = f.chain.state.blocks[0].tokenId, id = randomUUID();
|
||||
const legacy = { id, cardHash, sequence: 0, seller: alice.actor, title: 'Historical USD listing',
|
||||
image: 'https://upload.wikimedia.org/test.jpg', kind: 'fixed' as const, priceCents: 1500,
|
||||
feeBps: 250, createdAt: f.now, endsAt: null, status: 'open' as const, highestBid: null };
|
||||
const db = new DatabaseSync(join(f.dir, 'testnet', 'market.sqlite'));
|
||||
try { db.prepare('INSERT INTO listings VALUES(?,?,?,?)').run(id, cardHash, 'open', JSON.stringify(legacy)); }
|
||||
finally { db.close(); }
|
||||
const activity = await f.run<MarketActivityV3>(alice, { type: 'activity' });
|
||||
assert.equal(activity.listings[0].id, id);
|
||||
assert.equal((activity.listings[0] as typeof legacy).priceCents, 1500);
|
||||
assert.equal(activity.listings[0].status, 'invalid');
|
||||
await assert.rejects(f.run(bob, { type: 'buy', listingId: id, priceCents: 1500, feeBps: 250 }), /Legacy USD listings are read-only/);
|
||||
await assert.rejects(f.run(alice, { type: 'bid', listingId: id, priceCents: 1500 }), /Legacy USD listings are read-only/);
|
||||
const retired = await f.market.execute(f.envelopeV3(alice, { type: 'retire-legacy-listing', listingId: id })) as MarketListingV3;
|
||||
assert.equal(retired.status, 'cancelled');
|
||||
assert.equal((retired as unknown as typeof legacy).priceCents, 1500);
|
||||
});
|
||||
|
||||
test('legacy reserved USD orders retain signed terms across current-XMR upgrade and restart', async t => {
|
||||
const f = await fixture(t), listingId = randomUUID(), orderId = randomUUID(), cardHash = f.chain.state.blocks[0].tokenId;
|
||||
const listing = { id: listingId, cardHash, sequence: 0, seller: alice.actor, title: 'Legacy in-flight order',
|
||||
image: 'https://upload.wikimedia.org/test.jpg', kind: 'fixed' as const, priceCents: 1500, feeBps: 250,
|
||||
createdAt: f.now, endsAt: null, status: 'reserved' as const, highestBid: null,
|
||||
additionalFee: { bps: 100, address: ADDITIONAL_MARKET_FEE.address } };
|
||||
const quote = mockQuote(1500, 250, 15000);
|
||||
const order = { id: orderId, listingId, seller: alice.actor, buyer: bob.actor, cardHash, sequence: 0,
|
||||
priceCents: 1500, feeBps: 250, createdAt: f.now, payBy: f.now + 30 * 60_000,
|
||||
deliverBy: f.now + 30 * 60_000 + 48 * 60 * 60_000, status: 'awaiting-payment' as const,
|
||||
...quote, payout: 'not-started' as const, deliveryConfirmations: 0, requiredConfirmations: 2 };
|
||||
const db = new DatabaseSync(join(f.dir, 'testnet', 'market.sqlite'));
|
||||
try {
|
||||
db.prepare('INSERT INTO listings VALUES(?,?,?,?)').run(listingId, cardHash, 'reserved', JSON.stringify(listing));
|
||||
db.prepare('INSERT INTO orders VALUES(?,?)').run(orderId, JSON.stringify(order));
|
||||
} finally { db.close(); }
|
||||
await assert.rejects(f.run(bob, { type: 'mock-pay', orderId }), /Legacy USD orders keep their original terms/);
|
||||
const historical = await f.market.execute(f.envelope(bob, { type: 'mock-pay', orderId })) as typeof order;
|
||||
assert.equal(historical.status, 'paid');
|
||||
assert.equal(historical.priceCents, 1500);
|
||||
assert.equal(historical.invoice.amountXmr, '0.100000000000');
|
||||
await f.restart();
|
||||
const restored = (await f.run<MarketActivityV3>(bob, { type: 'activity' })).orders[0] as typeof order;
|
||||
assert.equal(restored.priceCents, 1500);
|
||||
assert.equal(restored.invoice.amountXmr, '0.100000000000');
|
||||
assert.equal(restored.status, 'paid');
|
||||
});
|
||||
|
||||
test('native-XMR listing and checkout accept one atomic unit without conversion or fee minimums', async t => {
|
||||
const f = await fixture(t);
|
||||
const listing = await f.market.execute(f.envelopeV3(alice, { type: 'list', cardHash: f.chain.state.blocks[0].tokenId,
|
||||
sequence: 0, kind: 'fixed', priceAtomicXmr: '1', endsAt: null, feeBps: 250 })) as MarketListingV3;
|
||||
assert.equal(listing.priceAtomicXmr, '1');
|
||||
const order = await f.market.execute(f.envelopeV3(bob, { type: 'buy', listingId: listing.id,
|
||||
priceAtomicXmr: '1', feeBps: 250 })) as MarketOrderV3;
|
||||
assert.equal(order.invoice.amountXmr, '0.000000000001');
|
||||
assert.equal(order.feeXmr, '0.000000000000');
|
||||
assert.equal(order.additionalFee?.amountXmr, '0.000000000000');
|
||||
assert.equal(order.sellerNetXmr, '0.000000000001');
|
||||
const terms = { type: 'list', cardHash: f.chain.state.blocks[1].tokenId, sequence: 0,
|
||||
kind: 'fixed', priceAtomicXmr: '0', endsAt: null, feeBps: 250 };
|
||||
assert.equal(marketActionV3Schema.safeParse(terms).success, false);
|
||||
for (const invalid of ['01', '+1', '1.0', '1e0', '9'.repeat(23)]) {
|
||||
assert.equal(marketActionV3Schema.safeParse({ ...terms, priceAtomicXmr: invalid }).success, false);
|
||||
}
|
||||
});
|
||||
|
||||
test('listings reject unknown, immature, stale, foreign, duplicate, and pending cards', async t => {
|
||||
const f = await fixture(t);
|
||||
const base = { type: 'list' as const, cardHash: f.chain.state.blocks[0].tokenId, sequence: 0,
|
||||
@@ -129,7 +215,7 @@ test('listings reject unknown, immature, stale, foreign, duplicate, and pending
|
||||
f.chain.state.blocks.pop();
|
||||
await assert.rejects(f.run(alice, base), /Card/);
|
||||
f.chain.state.blocks.push({ ...f.chain.state.blocks[0], height: 2 });
|
||||
f.chain.state.pendingTransfers.push(f.delivery({ cardHash: base.cardHash, sequence: 0 } as MarketOrder));
|
||||
f.chain.state.pendingTransfers.push(f.delivery({ cardHash: base.cardHash, sequence: 0 }));
|
||||
await assert.rejects(f.run(alice, base), /pending/);
|
||||
f.chain.state.pendingTransfers = [];
|
||||
const listing = await f.list();
|
||||
@@ -143,10 +229,10 @@ test('listings reject unknown, immature, stale, foreign, duplicate, and pending
|
||||
test('fixed-price reservations are exclusive, idempotent, private and expire without charging', async t => {
|
||||
const f = await fixture(t), listing = await f.list();
|
||||
await assert.rejects(f.buy(listing, alice), /own card/);
|
||||
const request = f.envelope(bob, { type: 'buy', listingId: listing.id, priceCents: 1500, feeBps: 250 });
|
||||
const request = f.envelopeV3(bob, { type: 'buy', listingId: listing.id, priceAtomicXmr: '100000000000', feeBps: 250 });
|
||||
const results = await Promise.allSettled([f.market.execute(request), f.buy(listing, carol)]);
|
||||
assert.equal(results[0].status, 'fulfilled'); assert.equal(results[1].status, 'rejected');
|
||||
const order = (results[0] as PromiseFulfilledResult<MarketOrder>).value;
|
||||
const order = (results[0] as PromiseFulfilledResult<MarketOrderV3>).value;
|
||||
assert.deepEqual(await f.market.execute(request), order);
|
||||
assert.equal((await f.orders(carol)).length, 0);
|
||||
assert.equal((await f.orders(alice)).length, 1);
|
||||
@@ -172,7 +258,7 @@ test('auction bids are signed, increasing, deadline-bound, uncancellable, and wi
|
||||
await assert.rejects(f.buy(l, bob, 1600), /winner/);
|
||||
await assert.rejects(f.buy(l, carol, 1500), /terms changed/);
|
||||
const order = await f.buy(l, carol, 1600);
|
||||
assert.equal(order.priceCents, 1600); assert.equal(order.payBy, f.now + 30 * 60000);
|
||||
assert.equal(order.priceAtomicXmr, '106666666667'); assert.equal(order.payBy, f.now + 30 * 60000);
|
||||
assert.equal(order.additionalFee?.bps, 100);
|
||||
assert.equal(order.additionalFee?.address, ADDITIONAL_MARKET_FEE.address);
|
||||
});
|
||||
|
||||
@@ -109,4 +109,4 @@ test('mock USD caller retains its original independently rounded amounts', () =>
|
||||
assert.equal(parseXmr(quote.sellerNetXmr), gross - gross * BigInt(bps) / 10000n - gross / 100n);
|
||||
assert.equal(quote.invoice.address, null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user