Option 3 — the hub trading floor. Two real cross-chain IOUs minted by DeviantWrapMintIOU (τUSDt from Tron, εUSDC from Ethereum) are pooled on the validated SharePairAMM (ADR-0030) and swapped on BCH. All 4 checks PASS: both IOUs pooled, trader received τUSDt, constant product held, price discovered 1.00→1.44 εUSDC/τUSDt. Closes the settlement-hub loop: deposit (lock-and-mint) -> TRADE (this) -> withdraw (redeem). Names the BCH DEX **Anthemus** (Ἀνθεμοῦς, ancient Macedonian town; root ἄνθεμον "blossom" → a blossoming marketplace). Brand pair with Potidaea (the ASM): Potidaea = backed MBAs, fixed rate, no pool, infinite depth; Anthemus = DEX, Token-class + cross-chain, market price discovery (ADR-0016 routing). Use plain "Anthemus" (not "Anthemus Sea", an unrelated hotel mark); namespace clear. scripts/chipnet-hub-dex.mjs Parameters/DEX_Anthemus/anthemus_dex.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
164 lines
12 KiB
JavaScript
164 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-hub-dex.mjs — Build B option 3: the hub TRADING FLOOR.
|
|
*
|
|
* Closes the settlement-hub loop: deposit (lock-and-mint, validated B3/B-native/Sepolia)
|
|
* → TRADE (here) → withdraw (redeem, validated B4). Two real hub IOUs minted by the
|
|
* validated DeviantWrapMintIOU covenant (τUSDt + εUSDC) are pooled on the validated
|
|
* SharePairAMM (ADR-0030) and swapped against the constant-product curve on BCH.
|
|
*
|
|
* Token-class IOUs route to a P2P DEX, NOT ASM/Potidaea (ADR-0016). The source-chain
|
|
* locks behind these IOUs are validated separately (B2/B3 Tron, Sepolia); here the oracle
|
|
* attests them (as in chipnet-btc-iou-demo) so the trading layer is tested in isolation.
|
|
*
|
|
* Roles: C = LP/funder + bridge oracle-operator; B = trader; E = oracle key.
|
|
* Run: node --import tsx scripts/chipnet-hub-dex.mjs --yes
|
|
*/
|
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { TestNetWallet } from 'mainnet-js';
|
|
import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript';
|
|
import { secp256k1, sha256 as laSha256 } from '@bitauth/libauth';
|
|
|
|
const ROOT = join(import.meta.dirname, '..');
|
|
const WRAP_ART = join(ROOT, 'contracts/dist/DeviantWrapMintIOU.json');
|
|
const AMM_ART = join(ROOT, 'contracts/dist/SharePairAMM.json');
|
|
const KEYS = { b: join(ROOT, '.chipnet-keys-b.json'), c: join(ROOT, '.chipnet-keys-c.json'), e: join(ROOT, '.chipnet-keys-e.json') };
|
|
|
|
const SUPPLY = 1_000_000_000n; // wrap reservoir per IOU
|
|
const MINT_TUSDT = 5_000_000n; // τUSDt minted to LP (5 USDt-equiv, 6 dec)
|
|
const MINT_EUSDC = 6_000_000n; // εUSDC minted to LP (5M seed + 1M for trader)
|
|
const SEED = 5_000_000n; // pool seed per leg (1:1 → start price 1.00)
|
|
const SWAP_IN = 1_000_000n; // trader swaps 1 εUSDC-equiv → τUSDt
|
|
const DUST = 1000n, MF = 2500n;
|
|
|
|
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 txidLE = (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 p2pkh = (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; };
|
|
const u32le = (n) => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, Number(n), true); return b; };
|
|
const u64le = (n) => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(n), true); return b; };
|
|
const oracleSign = (priv, msg) => { const s = secp256k1.signMessageHashSchnorr(priv, laSha256.hash(msg)); if (typeof s === 'string') throw new Error(s); return s; };
|
|
const mintMessage = (lb, amt, resTxid, resVout) => { const m = new Uint8Array(69); m.set(lb, 0); m.set(u64le(amt), 25); m.set(txidLE(resTxid), 33); m.set(u32le(resVout), 65); return m; };
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const divider = (c = '=') => console.log(c.repeat(80));
|
|
const step = (t) => { console.log(''); divider(); console.log(t); divider(); };
|
|
const txidOf = (s) => (typeof s === 'string' ? s : s?.txid ?? s?.txId);
|
|
function pickBch(u, need) { const p = []; let t = 0n; for (const x of u) { p.push(x); t += BigInt(x.satoshis); if (t >= need) break; } return { p, t }; }
|
|
|
|
async function main() {
|
|
if (!process.argv.includes('--yes')) { console.log('Re-run with --yes to broadcast on chipnet.'); return; }
|
|
const wrapArt = JSON.parse(readFileSync(WRAP_ART, 'utf8'));
|
|
const ammArt = JSON.parse(readFileSync(AMM_ART, 'utf8'));
|
|
if (!existsSync(KEYS.e)) { const w = await TestNetWallet.newRandom(); writeFileSync(KEYS.e, JSON.stringify({ wif: w.privateKeyWif }, null, 2)); }
|
|
|
|
const C = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.c, 'utf8')).wif); // LP/funder/operator
|
|
const B = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.b, 'utf8')).wif); // trader
|
|
const E = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.e, 'utf8')).wif); // oracle
|
|
const provider = new ElectrumNetworkProvider('chipnet');
|
|
const sigC = new SignatureTemplate(C.privateKey);
|
|
const sigB = new SignatureTemplate(B.privateKey);
|
|
const wrap = (cat) => new Contract(wrapArt, [txidLE(cat), E.publicKey], { provider });
|
|
|
|
const ftBal = async (w, cat) => (await provider.getUtxos(w.cashaddr)).filter((x) => x.token?.category === cat && !x.token?.nft).reduce((s, x) => s + BigInt(x.token.amount), 0n);
|
|
const ftUtxo = async (w, cat, min) => (await provider.getUtxos(w.cashaddr)).find((x) => x.token?.category === cat && !x.token?.nft && BigInt(x.token?.amount) >= min);
|
|
async function pick(w, need) { const u = (await provider.getUtxos(w.cashaddr)).filter((x) => !x.token).sort((a, b) => Number(b.satoshis) - Number(a.satoshis)); const { p, t } = pickBch(u, need); if (t < need) throw new Error(`low BCH ${t}<${need}`); return { p, t }; }
|
|
async function eligible(w) {
|
|
let u = await w.getUtxos(); let e = u.filter((x) => x.vout === 0 && !x.token); if (e.length) return e[0];
|
|
const bad = u.filter((x) => !(x.vout === 0 && !x.token));
|
|
await w.send([{ cashaddr: w.cashaddr, value: 2000n, unit: 'sat' }], bad.length ? { utxoIds: bad } : undefined); await sleep(12000);
|
|
u = await w.getUtxos(); e = u.filter((x) => x.vout === 0 && !x.token); if (!e.length) throw new Error('splitter failed'); return e[0];
|
|
}
|
|
|
|
// mint a hub IOU via the validated wrap covenant (oracle attests a source-chain lock) → to C (LP)
|
|
async function mintIOU(label, amount) {
|
|
const u0 = await eligible(C); const cat = u0.txid; const w = wrap(cat);
|
|
{ const uSat = BigInt(u0.satoshis); const need = DUST + MF + 1000n;
|
|
const all = (await provider.getUtxos(C.cashaddr)).filter((x) => !x.token && !(x.txid === u0.txid && x.vout === u0.vout)).sort((a, b) => Number(b.satoshis) - Number(a.satoshis));
|
|
const { p, t } = pickBch(all, need > uSat ? need - uSat : 0n); const total = uSat + t;
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(u0, sigC.unlockP2PKH()); for (const x of p) tb.addInput(x, sigC.unlockP2PKH());
|
|
tb.addOutput({ to: w.tokenAddress, amount: DUST, token: { amount: SUPPLY, category: cat, nft: { capability: 'minting', commitment: '' } } });
|
|
tb.addOutput({ to: C.cashaddr, amount: total - DUST - MF });
|
|
await tb.send(); await sleep(8000); }
|
|
const res = (await provider.getUtxos(w.tokenAddress)).find((x) => x.token?.nft?.capability === 'minting' && x.token?.category === cat);
|
|
const msg = mintMessage(p2pkh(C.publicKeyHash), amount, res.txid, res.vout); const oSig = oracleSign(E.privateKey, msg);
|
|
const { p, t } = await pick(C, DUST + MF + 1000n);
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(res, w.unlock.mint(oSig, msg)); for (const x of p) tb.addInput(x, sigC.unlockP2PKH());
|
|
tb.addOutput({ to: w.tokenAddress, amount: BigInt(res.satoshis), token: { amount: SUPPLY - amount, category: cat, nft: { capability: 'minting', commitment: '' } } });
|
|
tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: amount, category: cat } });
|
|
tb.addOutput({ to: C.cashaddr, amount: t - DUST - MF });
|
|
await tb.send(); await sleep(8000);
|
|
console.log(` minted ${amount} ${label} → LP · category ${cat.slice(0, 16)}…`);
|
|
return cat;
|
|
}
|
|
|
|
console.log(`LP/oracle ${C.cashaddr}\ntrader ${B.cashaddr}`);
|
|
|
|
step('1. Mint two hub IOUs to the LP (deposit side — validated lock-and-mint covenant)');
|
|
const tUSDt = await mintIOU('τUSDt', MINT_TUSDT);
|
|
const eUSDC = await mintIOU('εUSDC', MINT_EUSDC);
|
|
|
|
step('2. Genesis the SharePairAMM pool (τUSDt ⇄ εUSDC) + seed it 1:1');
|
|
const amm = new Contract(ammArt, [txidLE(tUSDt), txidLE(eUSDC), C.publicKeyHash], { provider });
|
|
console.log(` pool ${amm.tokenAddress}`);
|
|
const poolA = async () => (await provider.getUtxos(amm.tokenAddress)).find((x) => x.token?.category === tUSDt && !x.token?.nft);
|
|
const poolB = async () => (await provider.getUtxos(amm.tokenAddress)).find((x) => x.token?.category === eUSDC && !x.token?.nft);
|
|
{
|
|
const ta = await ftUtxo(C, tUSDt, SEED), tb_ = await ftUtxo(C, eUSDC, SEED);
|
|
const { p, t } = await pick(C, 3n * DUST + MF + 1000n);
|
|
const tx = new TransactionBuilder({ provider });
|
|
tx.addInput(ta, sigC.unlockP2PKH()); tx.addInput(tb_, sigC.unlockP2PKH()); for (const x of p) tx.addInput(x, sigC.unlockP2PKH());
|
|
tx.addOutput({ to: amm.tokenAddress, amount: DUST, token: { amount: SEED, category: tUSDt } }); // [0] τUSDt reserve
|
|
tx.addOutput({ to: amm.tokenAddress, amount: DUST, token: { amount: SEED, category: eUSDC } }); // [1] εUSDC reserve
|
|
tx.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: BigInt(tb_.token.amount) - SEED, category: eUSDC } }); // εUSDC change (1M)
|
|
tx.addOutput({ to: C.cashaddr, amount: BigInt(ta.satoshis) + BigInt(tb_.satoshis) + t - 3n * DUST - MF });
|
|
console.log(` seeded ${SEED} τUSDt + ${SEED} εUSDC → start price 1.000 εUSDC/τUSDt · ${txidOf(await tx.send())}`); await sleep(9000);
|
|
}
|
|
|
|
step(`3. Fund trader with ${SWAP_IN} εUSDC, then SWAP εUSDC → τUSDt against the curve`);
|
|
{ const eu = await ftUtxo(C, eUSDC, SWAP_IN); const { p, t } = await pick(C, DUST + MF + 1000n);
|
|
const tx = new TransactionBuilder({ provider });
|
|
tx.addInput(eu, sigC.unlockP2PKH()); for (const x of p) tx.addInput(x, sigC.unlockP2PKH());
|
|
tx.addOutput({ to: B.tokenaddr, amount: DUST, token: { amount: SWAP_IN, category: eUSDC } });
|
|
tx.addOutput({ to: C.cashaddr, amount: BigInt(eu.satoshis) + t - DUST - MF });
|
|
console.log(` funded trader ${txidOf(await tx.send())}`); await sleep(9000); }
|
|
|
|
const ra = await poolA(), rb = await poolB();
|
|
const u = BigInt(ra.token.amount), pp = BigInt(rb.token.amount); // u=τUSDt, pp=εUSDC
|
|
const p2 = pp + SWAP_IN; const k = u * pp; let u2 = k / p2; if (u2 * p2 < k) u2 += 1n; const out = u - u2;
|
|
console.log(` curve: τUSDt ${u}→${u2}, εUSDC ${pp}→${p2}; trader receives ${out} τUSDt`);
|
|
let swapTxid;
|
|
{ const beu = await ftUtxo(B, eUSDC, SWAP_IN); const { p, t } = await pick(B, 2n * DUST + MF + 1000n);
|
|
const tx = new TransactionBuilder({ provider });
|
|
tx.addInput(ra, amm.unlock.swap()); tx.addInput(rb, amm.unlock.swap()); tx.addInput(beu, sigB.unlockP2PKH());
|
|
for (const x of p) tx.addInput(x, sigB.unlockP2PKH());
|
|
tx.addOutput({ to: amm.tokenAddress, amount: BigInt(ra.satoshis), token: { amount: u2, category: tUSDt } }); // [0] τUSDt reserve back
|
|
tx.addOutput({ to: amm.tokenAddress, amount: BigInt(rb.satoshis), token: { amount: p2, category: eUSDC } }); // [1] εUSDC reserve back
|
|
tx.addOutput({ to: B.tokenaddr, amount: DUST, token: { amount: out, category: tUSDt } }); // [2] τUSDt to trader
|
|
tx.addOutput({ to: B.cashaddr, amount: BigInt(beu.satoshis) + t - DUST - MF });
|
|
swapTxid = txidOf(await tx.send()); await sleep(9000);
|
|
console.log(` swapped ${swapTxid}`); }
|
|
|
|
// verify
|
|
const ra2 = await poolA(), rb2 = await poolB();
|
|
const got = await ftBal(B, tUSDt);
|
|
const priceBefore = Number(pp) / Number(u);
|
|
const priceAfter = Number(rb2.token.amount) / Number(ra2.token.amount);
|
|
|
|
step('RESULT — hub DEX trading floor (τUSDt ⇄ εUSDC on SharePairAMM)');
|
|
const checks = [
|
|
['two hub IOUs minted (deposit side) and pooled', !!ra && !!rb],
|
|
['trader received τUSDt from the swap (cross-chain asset → cross-chain asset on BCH)', got === out && out > 0n],
|
|
['constant-product invariant held (x·y could only grow)', BigInt(ra2.token.amount) * BigInt(rb2.token.amount) >= k],
|
|
['price moved along the curve (discovery, not a fixed peg)', priceAfter > priceBefore],
|
|
];
|
|
let ok = true; for (const [l, p] of checks) { console.log(` ${p ? 'PASS' : 'FAIL'} ${l}`); if (!p) ok = false; }
|
|
divider();
|
|
console.log(ok
|
|
? `HUB DEX VALIDATED — two cross-chain IOUs (τUSDt from Tron, εUSDC from Ethereum) trade\nagainst each other on BCH via SharePairAMM. Pool ${u}/${pp} → ${ra2.token.amount}/${rb2.token.amount};\nprice ${priceBefore.toFixed(4)} → ${priceAfter.toFixed(4)} εUSDC/τUSDt; trader got ${out} τUSDt for ${SWAP_IN} εUSDC.\nThe loop is closed: DEPOSIT (lock-and-mint) → TRADE (this) → WITHDRAW (redeem). BCH is the\ntrading floor; source chains are vaults. No KYC, on-chain, <$0.001, self-custodial.`
|
|
: 'SOME CHECKS FAILED.');
|
|
console.log(`\npool @ ${amm.tokenAddress} · τUSDt ${tUSDt.slice(0, 12)}… · εUSDC ${eUSDC.slice(0, 12)}…`);
|
|
if (!ok) process.exitCode = 1;
|
|
}
|
|
main().catch((e) => { console.error('FAILED:', e?.stack ?? e?.message ?? e); process.exitCode = 1; });
|