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>
146 lines
5.9 KiB
JavaScript
146 lines
5.9 KiB
JavaScript
#!/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);
|
||
});
|