Trades
This commit is contained in:
@@ -13,8 +13,8 @@ import { PeerNetwork } from '../server/testnet/peers.ts';
|
||||
import { CHAIN_ID, GENESIS_HASH, GENESIS_TIME, MAX_TARGET, address, makeTemplate, miningBlob,
|
||||
mintId, sha256, stateRoot, transferId, transition, workFor } from '../server/testnet/consensus.ts';
|
||||
import { NETWORK, CONSENSUS, blockClaimMessage, nftTransferMessage, testnetOwnerOf,
|
||||
type MiningTemplate, type NftState, type NftStateLayer, type NftStateView,
|
||||
type NftTransfer, type TestnetBlock } from '../shared/testnet.ts';
|
||||
nftTradeMessage, type MiningTemplate, type NftState, type NftStateLayer, type NftStateView,
|
||||
type NftTrade, type NftTransfer, type TestnetBlock } from '../shared/testnet.ts';
|
||||
import { deriveStats } from '../shared/enrichment.ts';
|
||||
import type { PowEngine } from '../server/testnet/randomx.ts';
|
||||
|
||||
@@ -77,6 +77,15 @@ function transfer(cardHash: string, from: Identity = alice, to: Identity = bob,
|
||||
sequence, publicKey: from.publicKey };
|
||||
return { ...tx, signature: from.sign(nftTransferMessage(tx)) };
|
||||
}
|
||||
function trade(offered: TestnetBlock[], requested: TestnetBlock[], proposer: Identity = alice,
|
||||
counterparty: Identity = bob): NftTrade {
|
||||
const terms = { network: NETWORK, chainId: CHAIN_ID, proposer: proposer.owner, counterparty: counterparty.owner,
|
||||
offered: offered.map(block => ({ cardHash: block.tokenId, sequence: 0 })),
|
||||
requested: requested.map(block => ({ cardHash: block.tokenId, sequence: 0 })) };
|
||||
const message = nftTradeMessage(terms);
|
||||
return { type: 'trade', ...terms, proposerPublicKey: proposer.publicKey, proposerSignature: proposer.sign(message),
|
||||
counterpartyPublicKey: counterparty.publicKey, counterpartySignature: counterparty.sign(message) };
|
||||
}
|
||||
async function fixture(t: { after: (fn: () => Promise<void>) => void }) {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'wikideck-testnet-'));
|
||||
const engine = new FakePow();
|
||||
@@ -192,6 +201,51 @@ test('NFT maturity, ordered mempool, block commitments and replay protection', a
|
||||
await assert.rejects(restarted.submitTransfer(transfer(minted.tokenId, carol, alice, 0)), /owner\/sequence/);
|
||||
});
|
||||
|
||||
test('signed multi-card trade is validated, mempooled, mined, and applied atomically', async t => {
|
||||
const { ledger } = await fixture(t);
|
||||
const chain = branch();
|
||||
const aliceFirst = await extend(chain, alice); await ledger.submit(aliceFirst);
|
||||
const bobFirst = await extend(chain, bob); await ledger.submit(bobFirst);
|
||||
const aliceSecond = await extend(chain, alice); await ledger.submit(aliceSecond);
|
||||
const bobSecond = await extend(chain, bob); await ledger.submit(bobSecond);
|
||||
await ledger.submit(await extend(chain, alice)); // All four NFTs are mature at the next inclusion height.
|
||||
|
||||
const offered = [aliceFirst, aliceSecond], requested = [bobFirst, bobSecond];
|
||||
const invalid = trade(offered, requested);
|
||||
const badTerms = { ...invalid, requested: invalid.requested.map((leg, index) =>
|
||||
index === 1 ? { ...leg, sequence: 1 } : leg) };
|
||||
const badMessage = nftTradeMessage(badTerms);
|
||||
const badSequence = { ...badTerms, proposerSignature: alice.sign(badMessage), counterpartySignature: bob.sign(badMessage) };
|
||||
const before = ledger.snapshot();
|
||||
await assert.rejects(ledger.submitTransaction(badSequence), /missing, stale, or immature/);
|
||||
const badSignature = { ...invalid, proposerSignature: bob.sign(nftTradeMessage(invalid)) };
|
||||
await assert.rejects(ledger.submitTransaction(badSignature), /signature/i);
|
||||
assert.deepEqual(ledger.snapshot().pendingTransfers, []);
|
||||
for (const block of offered) assert.equal(testnetOwnerOf(before, block.tokenId), alice.owner);
|
||||
for (const block of requested) assert.equal(testnetOwnerOf(before, block.tokenId), bob.owner);
|
||||
|
||||
const bundle = trade(offered, requested);
|
||||
await ledger.submitTransaction(bundle);
|
||||
assert.deepEqual(ledger.snapshot().pendingTransfers, [bundle]);
|
||||
const template = ledger.template(alice.owner, alice.publicKey, articleFor(ledger.snapshot().blocks), statsSnapshot());
|
||||
assert.deepEqual(template.transactions, [bundle]);
|
||||
await ledger.submit(await mine(template));
|
||||
const after = ledger.snapshot();
|
||||
assert.equal(after.pendingTransfers.length, 0);
|
||||
for (const block of offered) assert.equal(testnetOwnerOf(after, block.tokenId), bob.owner);
|
||||
for (const block of requested) assert.equal(testnetOwnerOf(after, block.tokenId), alice.owner);
|
||||
assert.deepEqual(after.transfers.at(-1), bundle);
|
||||
const deck = ledger.deck(alice.owner, 0, 2);
|
||||
assert.equal(deck.total, after.blocks.filter(block => testnetOwnerOf(after, block.tokenId) === alice.owner).length);
|
||||
assert.equal(deck.nfts.length, 2);
|
||||
assert.equal(deck.next, 2);
|
||||
assert.deepEqual(ledger.deck(alice.owner, deck.next!, 2).nfts.map(item => item.block.tokenId).sort(),
|
||||
ledger.deck(alice.owner).nfts.slice(2).map(item => item.block.tokenId).sort());
|
||||
assert.deepEqual(ledger.deck(alice.owner).nfts.map(item => item.block.tokenId).sort(),
|
||||
after.blocks.filter(block => testnetOwnerOf(after, block.tokenId) === alice.owner)
|
||||
.map(block => block.tokenId).sort());
|
||||
});
|
||||
|
||||
test('reorg rolls back NFT ownership and requeues only still-valid disconnected transfers in order', async t => {
|
||||
const { ledger, dir } = await fixture(t);
|
||||
const original = branch();
|
||||
|
||||
Reference in New Issue
Block a user