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>
179 lines
9.7 KiB
JavaScript
179 lines
9.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-unbacked-swap-test.mjs — are UNBACKED (Token-family, dust-only)
|
|
* tokens swappable on the ASM?
|
|
*
|
|
* Prediction (from snapshot 0027): NO, because the ASM moves BCH backing,
|
|
* not token identity. The source side of a swap is a P2PKH spend that
|
|
* releases whatever BCH the source UTXO carries. An unbacked token carries
|
|
* only dust (~675 sats) regardless of how many FTs it holds, so it releases
|
|
* ~675 sats — which buys floor(675 / targetRate) = 0 units of any fixed-rate
|
|
* target at the viable rate floor (≥1000). The swap cannot produce a valid
|
|
* positive-amount target output.
|
|
*
|
|
* Test: take an unbacked FT UTXO Wallet C already holds (1,000,000 tokens
|
|
* in 675 sats — left over from the T-001 / whitelist PoCs), and try to swap
|
|
* it into SWAPSRC (canonical fixed-rate target, rate=1000, from the
|
|
* bonding-swap test). Show the released backing buys 0 target units and the
|
|
* swap cannot be constructed.
|
|
*
|
|
* Contrast: a properly-backed 100-unit DEA source (100,000 sats) mints 100
|
|
* target units (already shown in the bonding-swap test, snapshot 0027).
|
|
*/
|
|
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 SWAP_STATE = join(ROOT, 'ChipnetTests', '.chipnet-bonding-swap-test.json');
|
|
const CANONICAL_ARTIFACT = join(ROOT, 'contracts/dist/DeviantOpenMintFT.json');
|
|
const KEYS_C = join(ROOT, '.chipnet-keys-c.json');
|
|
|
|
const SRC_RATE = 1000n; // SWAPSRC target rate
|
|
const SRC_FEE_BPS = 0n;
|
|
|
|
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 display = hexToBin(txidHex);
|
|
const out = new Uint8Array(display.length);
|
|
for (let i = 0; i < display.length; i++) out[i] = display[display.length - 1 - i];
|
|
return out;
|
|
}
|
|
function p2pkhLockingBytecode(pkh) {
|
|
const out = new Uint8Array(25);
|
|
out[0] = 0x76; out[1] = 0xa9; out[2] = 0x14; out.set(pkh, 3); out[23] = 0x88; out[24] = 0xac;
|
|
return out;
|
|
}
|
|
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(SWAP_STATE)) throw new Error('run chipnet:bonding-swap first (need SWAPSRC target)');
|
|
const swapState = JSON.parse(readFileSync(SWAP_STATE, 'utf8'));
|
|
const target = swapState.src; // SWAPSRC: canonical fixed-rate, rate 1000
|
|
if (!target) throw new Error('SWAPSRC not in bonding-swap state');
|
|
|
|
const canonicalArtifact = JSON.parse(readFileSync(CANONICAL_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 founderLockingBytecode = p2pkhLockingBytecode(wallet.publicKeyHash);
|
|
const recipientLockingBytecode = founderLockingBytecode;
|
|
|
|
const targetContract = new Contract(canonicalArtifact,
|
|
[txidToConsensusBytes(target.categoryId), SRC_RATE, SRC_FEE_BPS, founderLockingBytecode], { provider });
|
|
|
|
// ----------------------------------------------------------------
|
|
step(1, 'Find an UNBACKED FT UTXO in Wallet C (large FT amount, dust BCH)');
|
|
// ----------------------------------------------------------------
|
|
const utxos = await provider.getUtxos(wallet.cashaddr);
|
|
const ftUtxos = utxos.filter((u) => u.token?.category && !u.token?.nft && u.token?.amount > 0n);
|
|
// "Unbacked" = the per-FT backing is far below any viable rate. Pick the
|
|
// UTXO with the highest FT-count-to-sats ratio (most tokens per sat).
|
|
const unbacked = ftUtxos
|
|
.map((u) => ({ u, ratio: Number(u.token.amount) / Number(u.satoshis) }))
|
|
.sort((a, b) => b.ratio - a.ratio)[0]?.u;
|
|
if (!unbacked) throw new Error('no FT UTXO found in C');
|
|
|
|
const srcAmount = unbacked.token.amount;
|
|
const srcSats = BigInt(unbacked.satoshis);
|
|
const perFtBacking = Number(srcSats) / Number(srcAmount);
|
|
console.log(` source FT UTXO: ${unbacked.txid.slice(0, 16)}:${unbacked.vout}`);
|
|
console.log(` category: ${unbacked.token.category}`);
|
|
console.log(` FT amount: ${srcAmount}`);
|
|
console.log(` BCH backing: ${srcSats} sats`);
|
|
console.log(` per-FT backing: ${perFtBacking.toFixed(8)} sats/FT (a viable token needs ≥1000 sats/FT)`);
|
|
console.log(` → This is an UNBACKED token: ${srcAmount} tokens carrying only ${srcSats} sats.`);
|
|
|
|
// ----------------------------------------------------------------
|
|
step(2, 'How much target can the released backing buy?');
|
|
// ----------------------------------------------------------------
|
|
console.log(` Target SWAPSRC rate: ${SRC_RATE} sats/unit`);
|
|
const releasable = srcSats; // P2PKH spend releases the UTXO's BCH
|
|
const targetUnits = releasable / SRC_RATE; // integer floor
|
|
console.log(` ASM source side = P2PKH spend → releases ${releasable} sats (the UTXO's BCH, NOT the token count)`);
|
|
console.log(` target units mintable = floor(${releasable} / ${SRC_RATE}) = ${targetUnits}`);
|
|
console.log('');
|
|
if (targetUnits === 0n) {
|
|
console.log(` RESULT: ${srcAmount} unbacked tokens → ${targetUnits} target units.`);
|
|
console.log(' The released dust cannot buy even ONE unit of a viable fixed-rate target.');
|
|
} else {
|
|
console.log(` (released backing happens to cover ${targetUnits} units — only because target rate is low)`);
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
step(3, 'Attempt to actually construct/broadcast the swap');
|
|
// ----------------------------------------------------------------
|
|
console.log(' Attempting swap: burn the unbacked source, mint target at current backing...');
|
|
const reservoirUtxos = await provider.getUtxos(targetContract.tokenAddress);
|
|
const reservoir = reservoirUtxos.find((u) => u.token?.nft?.capability === 'minting' && u.token?.category === target.categoryId && u.token?.amount > 0n);
|
|
if (!reservoir) throw new Error('SWAPSRC reservoir not found');
|
|
|
|
if (targetUnits === 0n) {
|
|
console.log(' targetUnits = 0 → a CashTokens FT output cannot carry a 0 amount, and');
|
|
console.log(' the contract requires tokenAmount > 0. The swap output is unconstructable.');
|
|
console.log('');
|
|
console.log(' Demonstrating: try contract.unlock.claim(0n, recipient) anyway →');
|
|
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 = srcSats + tot + BigInt(reservoir.satoshis) - 1000n - minerFee;
|
|
const tb = new TransactionBuilder({ provider, allowImplicitFungibleTokenBurn: true });
|
|
tb.addInput(unbacked, sig.unlockP2PKH());
|
|
tb.addInput(reservoir, targetContract.unlock.claim(0n, recipientLockingBytecode));
|
|
for (const u of picked) tb.addInput(u, sig.unlockP2PKH());
|
|
tb.addOutput({ to: targetContract.tokenAddress, amount: 1000n,
|
|
token: { amount: reservoir.token.amount, category: target.categoryId, nft: { capability: 'minting', commitment: '' } } });
|
|
// output[1] would be the target FT to recipient — but amount must be 0 → invalid
|
|
tb.addOutput({ to: wallet.tokenaddr, amount: SRC_RATE * 0n + 546n, token: { amount: 0n, category: target.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 expected): ${err.message?.split('\n')[0]}`);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
step(4, 'Verdict');
|
|
// ----------------------------------------------------------------
|
|
console.log(' UNBACKED tokens are NOT meaningfully swappable on the ASM. Two reasons:');
|
|
console.log('');
|
|
console.log(' (1) AS A SWAP SOURCE: the ASM transfers BCH backing, not token identity.');
|
|
console.log(' The source side is a P2PKH spend releasing the UTXO\'s BCH content.');
|
|
console.log(` An unbacked token carries only dust (${srcSats} sats here, for ${srcAmount} tokens),`);
|
|
console.log(' so it releases dust → buys 0 units of any viable fixed-rate target.');
|
|
console.log(' Holding a million unbacked tokens swaps for nothing.');
|
|
console.log('');
|
|
console.log(' (2) AS A SWAP TARGET: the ASM mint needs a RATE to compute how many target');
|
|
console.log(' units the incoming BCH buys. Token-family (unbacked) contracts have no');
|
|
console.log(' rate and no rate-based mint path, so there is no exchange to compute and');
|
|
console.log(' no contract function the ASM can call. Structurally not an ASM target.');
|
|
console.log('');
|
|
console.log(' (3) BY DESIGN: spec gates ASM-eligibility to permissionless + fixed-rate');
|
|
console.log(' (DEA / PLA). This is not arbitrary — it falls out of (1) and (2): only');
|
|
console.log(' fixed-rate backed assets have BCH backing to transfer AND a rate to');
|
|
console.log(' compute the exchange. Unbacked Token-family assets trade only via');
|
|
console.log(' third-party mechanisms (a buyer, an AMM), never the counterparty-free ASM.');
|
|
divider();
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('FAILED:', err?.message ?? err);
|
|
if (err?.stack) console.error(err.stack);
|
|
process.exit(1);
|
|
});
|