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

132 lines
5.3 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-send.mjs — peer-to-peer transfer of ASM between wallets.
*
* Env vars:
* FROM=a|b|c
* TO=a|b|c
* AMOUNT=100 (ASM units to send; minimum 7 to clear FT dust at rate 100)
*
* No contract is involved — this is a normal CashTokens FT transfer
* between two P2PKH addresses. Demonstrates that once a token has been
* claimed into circulation, it can be transferred without touching the
* contract.
*
* The contract's rate peg is implicitly maintained in this script: the
* recipient UTXO carries AMOUNT × rate sats of BCH backing, and the
* change UTXO carries (sourceAmount − AMOUNT) × rate sats. The
* contract has no say in this — it's a sender convention. A malicious
* sender could under-fund the recipient, but the recipient would just
* have less BCH per ASM than the protocol rate implies.
*/
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 FROM = (process.env.FROM ?? 'a').toLowerCase();
const TO = (process.env.TO ?? 'b').toLowerCase();
const AMOUNT = BigInt(process.env.AMOUNT ?? '100');
const KEYS = {
a: join(ROOT, '.chipnet-keys.json'),
b: join(ROOT, '.chipnet-keys-b.json'),
c: join(ROOT, '.chipnet-keys-c.json'),
};
if (!KEYS[FROM]) throw new Error(`FROM must be a/b/c`);
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: wifFrom } = JSON.parse(readFileSync(KEYS[FROM], 'utf8'));
const { tokenaddr: toTokenaddr, cashaddr: toCashaddr } = JSON.parse(readFileSync(KEYS[TO], 'utf8'));
const sender = await TestNetWallet.fromWIF(wifFrom);
const provider = new ElectrumNetworkProvider('chipnet');
const sigTemplate = new SignatureTemplate(sender.privateKey);
const rateSats = BigInt(asm.rateSats);
if (AMOUNT * rateSats < 700n) {
throw new Error(`AMOUNT × rate (${AMOUNT * rateSats} sats) below CashTokens dust ~675. Increase AMOUNT.`);
}
// Find the sender's ASM FT UTXO with enough balance.
const senderUtxos = await provider.getUtxos(sender.cashaddr);
const asmFtUtxos = senderUtxos.filter(
(u) => u.token?.category === asm.categoryId && !u.token?.nft && u.token?.amount > 0n,
);
// Prefer a UTXO that exactly covers AMOUNT; else pick the smallest that does.
asmFtUtxos.sort((a, b) => Number(a.token.amount - b.token.amount));
const sourceFt = asmFtUtxos.find((u) => u.token.amount >= AMOUNT);
if (!sourceFt) throw new Error(`Wallet ${FROM.toUpperCase()} has no ASM UTXO with ≥ ${AMOUNT} ASM`);
const bchUtxos = senderUtxos.filter((u) => !u.token).sort((a, b) => Number(b.satoshis) - Number(a.satoshis));
if (bchUtxos.length === 0) throw new Error(`Wallet ${FROM.toUpperCase()} has no plain BCH UTXO for fee`);
const feeInput = bchUtxos[0];
const sourceAmount = sourceFt.token.amount;
const sendBacking = AMOUNT * rateSats; // BCH to send with the recipient's FT
const changeAmount = sourceAmount - AMOUNT; // ASM remainder back to sender
const changeBacking = changeAmount * rateSats;
const minerFee = 2000n;
// BCH accounting: sourceFt has sourceAmount*rate sats. We split that into
// sendBacking + changeBacking, exact match. Miner fee comes from feeInput.
const bchAvailable = BigInt(sourceFt.satoshis) + BigInt(feeInput.satoshis);
const bchUsed = sendBacking + (changeAmount > 0n ? changeBacking : 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 ${FROM.toUpperCase()} → ${TO.toUpperCase()}: ${AMOUNT} ASM`);
divider();
console.log(` Source FT: ${sourceFt.txid.slice(0, 16)}:${sourceFt.vout} (${sourceAmount} ASM, ${sourceFt.satoshis} sat)`);
console.log(` Fee BCH input: ${feeInput.txid.slice(0, 16)}:${feeInput.vout} (${feeInput.satoshis} sat)`);
console.log(` To recipient: ${AMOUNT} ASM at ${sendBacking} sat backing → ${toTokenaddr.slice(0, 30)}...`);
if (changeAmount > 0n) {
console.log(` FT change back: ${changeAmount} ASM at ${changeBacking} sat backing`);
}
console.log(` BCH change: ${bchChange} sat`);
console.log(` Miner fee: ${minerFee} sat`);
divider();
const tb = new TransactionBuilder({ provider });
tb.addInput(sourceFt, sigTemplate.unlockP2PKH());
tb.addInput(feeInput, sigTemplate.unlockP2PKH());
tb.addOutput({
to: toTokenaddr,
amount: sendBacking,
token: { amount: AMOUNT, category: asm.categoryId },
});
if (changeAmount > 0n) {
tb.addOutput({
to: sender.tokenaddr,
amount: changeBacking,
token: { amount: changeAmount, 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(`Transfer TXID: ${txid}`);
}
main().catch((err) => {
console.error('FAILED:', err?.message ?? err);
if (err?.stack) console.error(err.stack);
process.exit(1);
});