deviant/scripts/chipnet-zero-zero-swap-test.mjs
walerikus 599202a721 chore: checkpoint working tree — shell Tier-B retirement + accumulated WIP
Snapshot of accumulated work-in-progress on top of the baseline commit.

This session — retire Tier B from the wallet shell (single-tier model, glossary
"Tiers & trust"):
- SendFlow.tsx: BCH-only; removed the non-functional Tier-B send path, the D-4/D-5
  raw-BCH co-display, and the ADR-0013 friction ladder.
- ArtemisionScreen.tsx: Create wizard collapsed to a single 5-step on-chain genesis
  flow; tier-selection step + Tier-B branch (prepareTierBImport) deleted.
- QuickActions.tsx / SettingsScreen.tsx: receive note reworded; "Send friction
  thresholds" + "Tier explainer" rows removed; registry-trust note reworded.
- Verified: tsc clean, vitest 74/74, full preview walkthrough, no console errors.

Pre-existing uncommitted work also folded in:
- USDm -> FIATm family rename/reorg (Parameters/FIATm/, design records)
- ~130 chipnet + sim-fiatm scripts (catalytic-converter, hyperinflation backtests)
- snapshots 0153-0168; wiki updates (glossary, decisions, index); ADR-0044/0045
- mockups + additional shell screens (Agora, Apps, MainScreen, AssetDetail, ...)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 01:01:37 +02:00

145 lines
7.6 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-zero-zero-swap-test.mjs — can TWO zero-value (unbacked) tokens
* swap into each other on the ASM?
*
* Distinct from snapshot 0028 (which swapped an unbacked token into a
* *backed* fixed-rate target). Here BOTH sides are zero-value:
* SOURCE: RUG — 1,000,000 FT carrying 675 sats (T-001 victim coin)
* TARGET: whitelist token — also a coin whose circulating UTXOs are
* dust-backed (snapshot 0024). Its contract nominally has
* rate=1000, but the swapper's source provides no backing to
* mint with.
*
* Prediction: the swap mints 0 target units, because the source releases
* only 675 sats of dust → floor(675 / 1000) = 0 → claim(0) reverts. Two
* zero-value tokens have nothing for the ASM to redistribute.
*/
import { existsSync, 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 WL_STATE = join(ROOT, 'ChipnetTests', '.chipnet-whitelist-test.json');
const WL_ARTIFACT = join(ROOT, 'contracts/dist/DeviantOpenMintFT_Whitelist.json');
const KEYS_C = join(ROOT, '.chipnet-keys-c.json');
const RUG_CATEGORY = 'e9d5434a6008f3bbd3f3b7c6cdf0300eef159ba06c5fccb86ebdca0b8383957e';
function hexToBin(hex) {
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
return out;
}
function txidToConsensusBytes(txidHex) {
const d = hexToBin(txidHex); const o = new Uint8Array(d.length);
for (let i = 0; i < d.length; i++) o[i] = d[d.length - 1 - i];
return o;
}
function p2pkhLockingBytecode(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;
}
function divider(ch = '=') { console.log(ch.repeat(80)); }
function step(n, t) { console.log(''); divider(); console.log(`STEP ${n} — ${t}`); divider(); }
async function main() {
if (!existsSync(WL_STATE)) throw new Error('need .chipnet-whitelist-test.json (the zero-value TARGET contract)');
const wl = JSON.parse(readFileSync(WL_STATE, 'utf8')).contract;
const wlArtifact = JSON.parse(readFileSync(WL_ARTIFACT, 'utf8'));
const { wif } = JSON.parse(readFileSync(KEYS_C, 'utf8'));
const wallet = await TestNetWallet.fromWIF(wif);
const provider = new ElectrumNetworkProvider('chipnet');
const sig = new SignatureTemplate(wallet.privateKey);
const recipientLockingBytecode = p2pkhLockingBytecode(wallet.publicKeyHash);
// Reconstruct the whitelist (zero-value) target contract
const targetContract = new Contract(wlArtifact, [
txidToConsensusBytes(wl.categoryId),
BigInt(wl.rateSats),
BigInt(wl.feeBps),
hexToBin(wl.founderLockingBytecodeHex),
hexToBin(wl.founderPkHex),
hexToBin(wl.privilegedPkHex),
], { provider });
if (targetContract.tokenAddress !== wl.contractTokenAddress) {
throw new Error(`target reconstruct mismatch: ${targetContract.tokenAddress} vs ${wl.contractTokenAddress}`);
}
// ----------------------------------------------------------------
step(1, 'Both sides are zero-value tokens');
// ----------------------------------------------------------------
const utxos = await provider.getUtxos(wallet.cashaddr);
const rug = utxos.find((u) => u.token?.category === RUG_CATEGORY && !u.token?.nft && u.token?.amount > 0n);
if (!rug) throw new Error('no RUG UTXO in C — expected the T-001 victim coin');
console.log(` SOURCE — RUG:`);
console.log(` ${rug.txid.slice(0, 16)}:${rug.vout} — ${rug.token.amount} FT, ${rug.satoshis} sats (${(Number(rug.satoshis)/Number(rug.token.amount)).toFixed(8)} sats/FT)`);
console.log(` TARGET — whitelist token (category ${wl.categoryId.slice(0,16)}…):`);
console.log(` contract nominal rate ${wl.rateSats}; its own circulating UTXOs are dust-backed too (snapshot 0024)`);
console.log(` Both carry ~0 BCH value.`);
// ----------------------------------------------------------------
step(2, 'What can RUG\'s released backing mint of the target?');
// ----------------------------------------------------------------
const released = BigInt(rug.satoshis); // 675
const targetRate = BigInt(wl.rateSats); // 1000
const targetUnits = released / targetRate; // floor → 0
console.log(` ASM source side (P2PKH spend) releases: ${released} sats`);
console.log(` target units = floor(${released} / ${targetRate}) = ${targetUnits}`);
console.log(` → ${rug.token.amount} RUG → ${targetUnits} whitelist tokens`);
// ----------------------------------------------------------------
step(3, 'Attempt the swap on-chain (expect revert)');
// ----------------------------------------------------------------
const reservoirUtxos = await provider.getUtxos(targetContract.tokenAddress);
const reservoir = reservoirUtxos.find((u) => u.token?.nft?.capability === 'minting' && u.token?.category === wl.categoryId && u.token?.amount > 0n);
if (!reservoir) throw new Error('whitelist reservoir not found');
console.log(' Building: burn RUG source, claim(0) on the whitelist target...');
try {
const minerFee = 6000n;
const bchOnly = utxos.filter((u) => !u.token).sort((a, b) => Number(b.satoshis) - Number(a.satoshis));
const picked = []; let tot = 0n;
for (const u of bchOnly) { picked.push(u); tot += BigInt(u.satoshis); if (tot >= minerFee + 1000n) break; }
const change = released + tot + BigInt(reservoir.satoshis) - 1000n - minerFee;
const tb = new TransactionBuilder({ provider, allowImplicitFungibleTokenBurn: true });
tb.addInput(rug, sig.unlockP2PKH()); // burn zero-value source
tb.addInput(reservoir, targetContract.unlock.claim(targetUnits, recipientLockingBytecode)); // mint 0 target
for (const u of picked) tb.addInput(u, sig.unlockP2PKH());
tb.addOutput({ to: targetContract.tokenAddress, amount: 1000n,
token: { amount: reservoir.token.amount - targetUnits, category: wl.categoryId, nft: { capability: 'minting', commitment: '' } } });
tb.addOutput({ to: wallet.tokenaddr, amount: 546n, token: { amount: targetUnits, category: wl.categoryId } });
tb.addOutput({ to: wallet.cashaddr, amount: change });
const sent = await tb.send();
const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
console.log(` UNEXPECTED: broadcast accepted (TXID ${txid})`);
} catch (err) {
console.log(` REJECTED (as predicted): ${err.message?.split('\n')[0]}`);
}
// ----------------------------------------------------------------
step(4, 'Verdict');
// ----------------------------------------------------------------
console.log(' TWO zero-value tokens cannot swap on the ASM.');
console.log(` RUG (${rug.token.amount} FT) released ${released} sats → minted ${targetUnits} whitelist tokens.`);
console.log(' The ASM redistributes BCH backing; with ~0 backing on both sides there is');
console.log(' nothing to redistribute, and claim(0) reverts (require(tokenAmount > 0)).');
console.log('');
console.log(' "0-for-0" is a market/barter concept, not an ASM operation. Two willing');
console.log(' parties could still trade these via a counterparty atomic swap or an AMM at a');
console.log(' market-agreed ratio — but that needs a counterparty, which is exactly what the');
console.log(' ASM removes via backing. No backing → no counterparty-free swap.');
divider();
}
main().catch((err) => {
console.error('FAILED:', err?.message ?? err);
if (err?.stack) console.error(err.stack);
process.exit(1);
});