deviant/scripts/chipnet-asm-batch.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

146 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* chipnet-asm-batch.mjs — Wallet C batches N small ASM transfers in one tx.
*
* Env vars:
* BATCH=100 (number of output transfers; default 100)
* PER=7 (ASM per output; default 7 — the dust minimum at rate 100)
* TO=a|b|c (recipient for all outputs; default 'a')
*
* Demonstrates the "100 transactions in one tx fee" property. Each of
* the BATCH outputs is a complete CashTokens FT transfer that, if sent
* individually, would consume one tx fee each (BATCH × ~2K = ~200K sats
* for 100). Bundled into a single transaction, the entire batch shares
* one miner fee (~8K sats for a 100-output tx, plus the per-output dust
* which is the same in either case).
*
* Net saving for 100 outputs: ~190K sats in miner fees not paid.
*/
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { TestNetWallet } from 'mainnet-js';
import {
ElectrumNetworkProvider,
TransactionBuilder,
SignatureTemplate,
} from 'cashscript';
const ROOT = join(import.meta.dirname, '..');
const ASM_FILE = join(ROOT, 'ChipnetTests', '.chipnet-asm-token.json');
const KEYS_C = join(ROOT, '.chipnet-keys-c.json');
const BATCH = parseInt(process.env.BATCH ?? '100', 10);
const PER = BigInt(process.env.PER ?? '7');
const TO = (process.env.TO ?? 'a').toLowerCase();
const KEYS = {
a: join(ROOT, '.chipnet-keys.json'),
b: join(ROOT, '.chipnet-keys-b.json'),
c: join(ROOT, '.chipnet-keys-c.json'),
};
if (!KEYS[TO]) throw new Error(`TO must be a/b/c`);
function divider() { console.log('='.repeat(72)); }
async function main() {
const asm = JSON.parse(readFileSync(ASM_FILE, 'utf8'));
const { wif } = JSON.parse(readFileSync(KEYS_C, 'utf8'));
const { tokenaddr: toTokenaddr } = JSON.parse(readFileSync(KEYS[TO], 'utf8'));
const sender = await TestNetWallet.fromWIF(wif);
const provider = new ElectrumNetworkProvider('chipnet');
const sigTemplate = new SignatureTemplate(sender.privateKey);
const rateSats = BigInt(asm.rateSats);
const perOutputBacking = PER * rateSats;
if (perOutputBacking < 700n) {
throw new Error(`PER × rate (${perOutputBacking} sats) below CashTokens dust ~675`);
}
const totalAsmSent = BigInt(BATCH) * PER;
const totalSendBacking = BigInt(BATCH) * perOutputBacking;
// Source FT UTXO must have enough ASM.
const senderUtxos = await provider.getUtxos(sender.cashaddr);
const ftUtxos = senderUtxos.filter(
(u) => u.token?.category === asm.categoryId && !u.token?.nft && u.token?.amount > 0n,
);
ftUtxos.sort((a, b) => Number(b.token.amount - a.token.amount));
const sourceFt = ftUtxos.find((u) => u.token.amount >= totalAsmSent);
if (!sourceFt) {
throw new Error(`Wallet C has no ASM UTXO with ≥ ${totalAsmSent} ASM. Claim more first.`);
}
const bchUtxos = senderUtxos.filter((u) => !u.token).sort((a, b) => Number(b.satoshis) - Number(a.satoshis));
if (bchUtxos.length === 0) throw new Error('Wallet C has no plain BCH UTXO for fee');
const feeInput = bchUtxos[0];
// Miner fee estimate: each output ~40 bytes (P2PKH locking + token bitfield + amount).
// Each P2PKH input ~150 bytes. Tx header ~10. So for BATCH+1 outputs + 1 change FT
// output + 1 BCH change output + 2 inputs:
// ~(BATCH + 3) * 40 + 2 * 150 + 10 ≈ BATCH*40 + 430
// For BATCH=100: ~4430 bytes. At 1.5 sat/byte safety margin: ~6700 sats.
const txByteEstimate = BigInt(BATCH) * 40n + 600n;
const minerFee = txByteEstimate * 2n; // 2 sats/byte safety
const sourceAmount = sourceFt.token.amount;
const ftChangeAmount = sourceAmount - totalAsmSent;
const ftChangeBacking = ftChangeAmount * rateSats;
const bchAvailable = BigInt(sourceFt.satoshis) + BigInt(feeInput.satoshis);
const bchUsed = totalSendBacking + (ftChangeAmount > 0n ? ftChangeBacking : 0n) + minerFee;
const bchChange = bchAvailable - bchUsed;
if (bchChange < 546n) {
throw new Error(
`BCH change ${bchChange} below dust. avail=${bchAvailable} used=${bchUsed}`,
);
}
divider();
console.log(`Wallet C batches ${BATCH} ASM transfers in ONE tx`);
divider();
console.log(` Source FT: ${sourceFt.txid.slice(0, 16)}:${sourceFt.vout} (${sourceAmount} ASM)`);
console.log(` Fee BCH input: ${feeInput.txid.slice(0, 16)}:${feeInput.vout} (${feeInput.satoshis} sat)`);
console.log(` Per-output payload: ${PER} ASM + ${perOutputBacking} sat backing`);
console.log(` Outputs in batch: ${BATCH}`);
console.log(` Total ASM sent: ${totalAsmSent}`);
console.log(` All to recipient: ${toTokenaddr.slice(0, 30)}...`);
console.log(` Estimated tx bytes: ${txByteEstimate}`);
console.log(` Miner fee: ${minerFee} sat (single fee for ${BATCH} transfers)`);
console.log(` Equivalent N-tx fee: ${BigInt(BATCH) * 2_000n} sat (if sent individually)`);
console.log(` Saving: ~${BigInt(BATCH) * 2_000n - minerFee} sat`);
console.log(` FT change to C: ${ftChangeAmount} ASM`);
console.log(` BCH change to C: ${bchChange} sat`);
divider();
const tb = new TransactionBuilder({ provider });
tb.addInput(sourceFt, sigTemplate.unlockP2PKH());
tb.addInput(feeInput, sigTemplate.unlockP2PKH());
for (let i = 0; i < BATCH; i++) {
tb.addOutput({
to: toTokenaddr,
amount: perOutputBacking,
token: { amount: PER, category: asm.categoryId },
});
}
if (ftChangeAmount > 0n) {
tb.addOutput({
to: sender.tokenaddr,
amount: ftChangeBacking,
token: { amount: ftChangeAmount, category: asm.categoryId },
});
}
tb.addOutput({ to: sender.cashaddr, amount: bchChange });
const sent = await tb.send();
const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
console.log('');
console.log(`Batch TXID: ${txid}`);
console.log(`Verify on chipnet: each of ${BATCH} recipients now holds one ${PER}-ASM UTXO.`);
}
main().catch((err) => {
console.error('FAILED:', err?.message ?? err);
if (err?.stack) console.error(err.stack);
process.exit(1);
});