chore: checkpoint in-flight work across domains Sweep of accumulated uncommitted work on design/dex-p2p-architecture into one checkpoint (multiple sessions share this tree). Main areas: - anthemus: covenant-wiring follow-ups (engine, ExchangePanel), AgoraScreen seam - spv: ServerPool + p2p header transport (pool.ts, core/spv/*), wallet.ts wiring - walletconnect: responder/useConnect updates + connect-responder-compat test - wallet: send-tier-a rework + swap.ts, plus send-tier-a unit tests - asm: chipnet throughput/stress harness (scripts/chipnet-asm-*) + ASM_Native docs - app UI: screens/* and shell.css touch-ups; sat -> bits copy migration - docs: new snapshots (INDEX regenerated), Parameters (WalletConnect/SSH/ASM), KnowledgeBase, WALLET-COORDINATION changelog Checkpoint only; not a release. Secrets (Keys/, .chipnet-keys*, .env) remain gitignored and were verified absent from the staged set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
92 lines
6 KiB
JavaScript
92 lines
6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-asm-sequencer.mjs — prototype the QUEUE/SEQUENCER for one backed asset,
|
|
* one category, one reservoir. N swap requests from different addresses "arrive
|
|
* simultaneously"; instead of each building against the same confirmed reservoir
|
|
* (→ 1 wins, N-1 collide), the sequencer processes them in order and CHAINS them:
|
|
* request k spends the reservoir coin request k-1 just created. Result: N/N
|
|
* accepted, zero collisions, on one category.
|
|
*
|
|
* Prototype funding model: a coordinator wallet funds each claim's miner fee and
|
|
* the backed asset (ASM2) is delivered to the requesting address (gift-claim,
|
|
* recipient ≠ payer). In production each user funds their own input and signs it;
|
|
* the sequencer only orders + relays, and the covenant still enforces the peg —
|
|
* the sequencer can order but cannot cheat. Chipnet only.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { TestNetWallet } from 'mainnet-js';
|
|
import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript';
|
|
|
|
const ROOT = join(import.meta.dirname, '..');
|
|
const ARTIFACT = JSON.parse(readFileSync(join(ROOT, 'contracts/dist/DeviantOpenMintFT.json'), 'utf8'));
|
|
const TOK = JSON.parse(readFileSync(join(ROOT, 'ChipnetTests', '.chipnet-asm2-token.json'), 'utf8')); // one backed asset
|
|
const COORD = (process.env.COORD ?? 'd').toLowerCase();
|
|
const USERS = (process.env.USERS ?? 'c,e,f,g,b').split(',').map((s) => s.trim().toLowerCase());
|
|
const AMOUNT = 60n, RATE = BigInt(TOK.rateSats), FEE_BPS = BigInt(TOK.founderFeeBps), MINER = 3000n;
|
|
|
|
const KEY = (w) => join(ROOT, w === 'a' ? '.chipnet-keys.json' : `.chipnet-keys-${w}.json`);
|
|
const hexToBin = (h) => { const o = new Uint8Array(h.length / 2); for (let i = 0; i < o.length; i++) o[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16); return o; };
|
|
const rev = (h) => { const d = hexToBin(h); const o = new Uint8Array(d.length); for (let i = 0; i < d.length; i++) o[i] = d[d.length - 1 - i]; return o; };
|
|
const p2 = (pkh) => { const o = new Uint8Array(25); o[0] = 0x76; o[1] = 0xa9; o[2] = 0x14; o.set(pkh, 3); o[23] = 0x88; o[24] = 0xac; return o; };
|
|
|
|
async function main() {
|
|
const provider = new ElectrumNetworkProvider('chipnet');
|
|
const contract = new Contract(ARTIFACT, [rev(TOK.categoryId), RATE, FEE_BPS, hexToBin(TOK.founderLockingBytecodeHex)], { provider });
|
|
const { wif } = JSON.parse(readFileSync(KEY(COORD), 'utf8'));
|
|
const coord = await TestNetWallet.fromWIF(wif);
|
|
const coordSig = new SignatureTemplate(coord.privateKey);
|
|
|
|
// Load the requesting users (recipients of the backed asset).
|
|
const users = [];
|
|
for (const u of USERS) {
|
|
const { wif: w } = JSON.parse(readFileSync(KEY(u), 'utf8'));
|
|
const wal = await TestNetWallet.fromWIF(w);
|
|
users.push({ label: u.toUpperCase(), lb: p2(wal.publicKeyHash), tokenaddr: wal.tokenaddr });
|
|
}
|
|
|
|
const gross = AMOUNT * RATE, fee = (gross * FEE_BPS) / 10_000n;
|
|
let reservoir = (await provider.getUtxos(contract.tokenAddress)).find((x) => x.token?.nft?.capability === 'minting' && x.token?.category === TOK.categoryId && x.token?.amount > 0n);
|
|
if (!reservoir) throw new Error('reservoir not found');
|
|
let coordBch = (await provider.getUtxos(coord.cashaddr)).filter((x) => !x.token).sort((a, b) => Number(b.satoshis) - Number(a.satoshis))[0];
|
|
if (!coordBch) throw new Error('coordinator has no BCH');
|
|
|
|
const arriveISO = new Date().toISOString();
|
|
console.log(`\nSEQUENCER — one backed asset (${TOK.name}, 1 category), one reservoir ${reservoir.txid.slice(0, 12)}…:${reservoir.vout}`);
|
|
console.log(`${users.length} swap requests ARRIVE SIMULTANEOUSLY @ ${arriveISO}: [${users.map((u) => u.label).join(', ')}]`);
|
|
console.log(`(naive parallel would be 1/${users.length}; the sequencer queues + chains them)\n`);
|
|
|
|
const results = [];
|
|
for (let i = 0; i < users.length; i++) {
|
|
const u = users[i];
|
|
const change = BigInt(coordBch.satoshis) - gross - fee - MINER;
|
|
if (change < 546n) { results.push({ label: u.label, txid: null, err: 'coordinator out of BCH' }); break; }
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(reservoir, contract.unlock.claim(AMOUNT, u.lb));
|
|
tb.addInput(coordBch, coordSig.unlockP2PKH());
|
|
tb.addOutput({ to: contract.tokenAddress, amount: 1000n, token: { amount: reservoir.token.amount - AMOUNT, category: TOK.categoryId, nft: { capability: 'minting', commitment: '' } } });
|
|
tb.addOutput({ to: u.tokenaddr, amount: gross, token: { amount: AMOUNT, category: TOK.categoryId } });
|
|
tb.addOutput({ to: TOK.founderAddress, amount: fee });
|
|
tb.addOutput({ to: coord.cashaddr, amount: change });
|
|
try {
|
|
const spent = `${reservoir.txid.slice(0, 10)}…:${reservoir.vout}`;
|
|
const sent = await tb.send();
|
|
const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
|
|
results.push({ label: u.label, txid, spent });
|
|
// advance the queue state (chain onto this tx's outputs)
|
|
reservoir = { txid, vout: 0, satoshis: 1000n, token: { amount: reservoir.token.amount - AMOUNT, category: TOK.categoryId, nft: { capability: 'minting', commitment: '' } } };
|
|
coordBch = { txid, vout: 3, satoshis: change };
|
|
} catch (e) {
|
|
results.push({ label: u.label, txid: null, err: (e?.message ?? String(e)).replace(/\s+/g, ' ').slice(0, 120) });
|
|
}
|
|
}
|
|
|
|
console.log('order | user | spent reservoir state | delivered');
|
|
results.forEach((r, i) => console.log(` ${i + 1} | ${r.label} | ${r.spent ?? '—'} | ${r.txid ? '✓ ' + AMOUNT + ' ' + TOK.name + ' ' + r.txid.slice(0, 16) + '…' : '✗ ' + r.err}`));
|
|
const ok = results.filter((r) => r.txid).length;
|
|
console.log(`\n=== SEQUENCER RESULT ===`);
|
|
console.log(` ${ok}/${users.length} requests settled — one category, one reservoir, ZERO collisions.`);
|
|
console.log(` Each request spent the reservoir coin the previous one created (a chain), so`);
|
|
console.log(` simultaneous arrival → ordered settlement. Submission is parallel; settlement is a queue.`);
|
|
}
|
|
main().then(() => process.exit(0)).catch((e) => { console.error('ERROR:', e?.message ?? e); process.exit(1); });
|