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>
229 lines
8.8 KiB
JavaScript
229 lines
8.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-mint-A.mjs — step 5 of the Q4 validation plan (snapshot 0005).
|
|
*
|
|
* Calls the DeviantOpenMintFT contract's mint() function for Token A,
|
|
* producing a self-backed FT UTXO that the wallet holds. This is the
|
|
* material the Q4 swap test will spend on the source side.
|
|
*
|
|
* Transaction shape:
|
|
*
|
|
* Inputs:
|
|
* [0] minting NFT UTXO at Token A's contract — unlocks via contract.unlock.mint()
|
|
* [1+] BCH from wallet (P2PKH) — covers tokenAmount * rate + miner fee
|
|
*
|
|
* Outputs:
|
|
* [0] minting NFT back to contract (category bytes preserved, dust value)
|
|
* [1] FT to wallet token-address — tokenAmount FTs, tokenAmount * rate sats
|
|
* [2] BCH change back to wallet (optional)
|
|
*
|
|
* No founder fee output because the token was created with feeBps=0.
|
|
*
|
|
* After this script runs, the wallet should hold:
|
|
* - A new FT UTXO with `tokenAmount` Token A FTs and `tokenAmount * rate`
|
|
* sats inside it — self-backed.
|
|
*
|
|
* Idempotency note: this script always mints fresh tokens. Re-running it
|
|
* produces another FT UTXO at the wallet, doesn't disturb the existing
|
|
* one. That's fine for our test.
|
|
*/
|
|
import { 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 KEYS_FILE = join(ROOT, '.chipnet-keys.json');
|
|
const TOKENS_FILE = join(ROOT, 'ChipnetTests', '.chipnet-tokens.json');
|
|
const ARTIFACT_PATH = join(ROOT, 'contracts/dist/DeviantOpenMintFT.json');
|
|
|
|
// How many Token A units to mint. With rate=1000 sats/unit, 100 units =
|
|
// 100,000 sats locked inside the new FT UTXO. Comfortable for swap math.
|
|
const MINT_AMOUNT = 100n;
|
|
|
|
function hexToBin(hex) {
|
|
if (hex.length % 2 !== 0) throw new Error(`odd hex length: ${hex.length}`);
|
|
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;
|
|
}
|
|
|
|
// See chipnet-genesis.mjs for the byte-order rationale. Must match the
|
|
// helper used at genesis exactly — same input → same constructor bytes →
|
|
// same contract address.
|
|
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() {
|
|
console.log('='.repeat(72));
|
|
}
|
|
|
|
async function main() {
|
|
const { wif } = JSON.parse(readFileSync(KEYS_FILE, 'utf8'));
|
|
const tokensFile = JSON.parse(readFileSync(TOKENS_FILE, 'utf8'));
|
|
const artifact = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8'));
|
|
|
|
const tokenA = tokensFile.tokens.find((t) => t.name === 'TokenA');
|
|
if (!tokenA) throw new Error('TokenA missing from .chipnet-tokens.json — run chipnet:genesis first');
|
|
|
|
const wallet = await TestNetWallet.fromWIF(wif);
|
|
const provider = new ElectrumNetworkProvider('chipnet');
|
|
const sigTemplate = new SignatureTemplate(wallet.privateKey);
|
|
|
|
const rateSats = BigInt(tokenA.rateSats);
|
|
const founderFeeBps = BigInt(tokenA.founderFeeBps);
|
|
const founderLockingBytecode = hexToBin(tokenA.founderLockingBytecodeHex);
|
|
const tokenCategory = txidToConsensusBytes(tokenA.categoryId);
|
|
const recipientLockingBytecode = p2pkhLockingBytecode(wallet.publicKeyHash);
|
|
|
|
// Reconstruct the contract with the same constructor args used at genesis.
|
|
// If any byte differs, the resulting P2SH32 address won't match and
|
|
// `getUtxos` will return nothing.
|
|
const contract = new Contract(
|
|
artifact,
|
|
[tokenCategory, rateSats, founderFeeBps, founderLockingBytecode],
|
|
{ provider },
|
|
);
|
|
|
|
if (contract.tokenAddress !== tokenA.contractTokenAddress) {
|
|
throw new Error(
|
|
`Contract address mismatch — params differ from genesis. ` +
|
|
`Expected ${tokenA.contractTokenAddress}, got ${contract.tokenAddress}.`,
|
|
);
|
|
}
|
|
|
|
// Fetch the minting NFT UTXO from the contract.
|
|
const contractUtxos = await provider.getUtxos(contract.tokenAddress);
|
|
const mintingNft = contractUtxos.find(
|
|
(u) => u.token?.nft?.capability === 'minting' && u.token?.category === tokenA.categoryId,
|
|
);
|
|
if (!mintingNft) throw new Error('Minting NFT not found at contract address');
|
|
|
|
// Fetch wallet BCH UTXOs (no tokens) for fee + payment funding.
|
|
const walletUtxos = await provider.getUtxos(wallet.cashaddr);
|
|
const bchUtxos = walletUtxos.filter((u) => !u.token);
|
|
if (bchUtxos.length === 0) throw new Error('No BCH UTXOs in wallet to fund the mint');
|
|
|
|
const grossSats = MINT_AMOUNT * rateSats; // sats inside the FT output
|
|
// The mint tx needs:
|
|
// - NFT output back to contract: 1000 sats (carry it forward)
|
|
// - FT output to wallet: grossSats
|
|
// - Change to wallet (optional): rest
|
|
// - Miner fee: ~700 sats (will refine)
|
|
// The NFT input contributes its 1000 sats. So the wallet must supply
|
|
// approximately: grossSats + ~700 (miner fee).
|
|
// We greedily pick BCH UTXOs until we cover ~grossSats + 2000 sats slack.
|
|
const targetCover = grossSats + 2_000n; // 2000 sats slack for fees & dust safety
|
|
let picked = [];
|
|
let picketTotal = 0n;
|
|
for (const u of bchUtxos) {
|
|
picked.push(u);
|
|
picketTotal += BigInt(u.satoshis);
|
|
if (picketTotal >= targetCover) break;
|
|
}
|
|
if (picketTotal < targetCover) {
|
|
throw new Error(`BCH UTXOs sum ${picketTotal} sats; need at least ${targetCover}`);
|
|
}
|
|
|
|
divider();
|
|
console.log(`mint Token A: ${MINT_AMOUNT} units at rate ${rateSats} sats/unit`);
|
|
divider();
|
|
console.log(` Reservoir input: ${mintingNft.txid.slice(0, 16)}:${mintingNft.vout} (${mintingNft.satoshis} sats, ${mintingNft.token.amount} FTs, minting NFT)`);
|
|
console.log(` BCH inputs picked: ${picked.length} totaling ${picketTotal} sats`);
|
|
console.log(` FT output target: ${grossSats} sats backing ${MINT_AMOUNT} FT units`);
|
|
console.log(` Recipient (token): ${wallet.tokenaddr}`);
|
|
|
|
// Build the transaction. Cashscript's TransactionBuilder composes a tx
|
|
// with mixed inputs (covenant + P2PKH) — exactly the pattern we need
|
|
// for the swap test too.
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(mintingNft, contract.unlock.claim(MINT_AMOUNT, recipientLockingBytecode));
|
|
for (const u of picked) {
|
|
tb.addInput(u, sigTemplate.unlockP2PKH());
|
|
}
|
|
|
|
// Output [0]: reservoir UTXO back to the contract. The minting NFT is
|
|
// preserved AND the FT supply decreases by exactly MINT_AMOUNT. The
|
|
// contract enforces this; libauth's token-validation also enforces FT
|
|
// conservation across the tx.
|
|
const reservoirRemaining = mintingNft.token.amount - MINT_AMOUNT;
|
|
if (reservoirRemaining < 0n) {
|
|
throw new Error(
|
|
`Reservoir would underflow: have ${mintingNft.token.amount} FTs, trying to mint ${MINT_AMOUNT}`,
|
|
);
|
|
}
|
|
tb.addOutput({
|
|
to: contract.tokenAddress,
|
|
amount: 1000n,
|
|
token: {
|
|
amount: reservoirRemaining,
|
|
category: tokenA.categoryId,
|
|
nft: { capability: 'minting', commitment: '' },
|
|
},
|
|
});
|
|
|
|
// Output [1]: FT to wallet, self-backed at MINT_AMOUNT * rate sats.
|
|
tb.addOutput({
|
|
to: wallet.tokenaddr,
|
|
amount: grossSats,
|
|
token: {
|
|
amount: MINT_AMOUNT,
|
|
category: tokenA.categoryId,
|
|
},
|
|
});
|
|
|
|
// Output [2]: BCH change back to wallet. The TransactionBuilder doesn't
|
|
// automatically compute change — we compute it explicitly to keep the
|
|
// tx valid. Inputs - outputs - miner fee.
|
|
const inputTotal = BigInt(mintingNft.satoshis) + picketTotal;
|
|
// Miner fee. BCH min relay is 1 sat/byte. A CashTokens covenant tx
|
|
// with 1 covenant input + 3 P2PKH inputs + 3 outputs (one of them a
|
|
// reservoir UTXO with both NFT and FT data) lands around 1800-2200
|
|
// bytes once the redeem script is included. Allocating 2500 sats
|
|
// gives a healthy safety margin without burning much.
|
|
const minerFee = 2500n;
|
|
const change = inputTotal - 1000n - grossSats - minerFee;
|
|
if (change < 546n) {
|
|
throw new Error(
|
|
`Change ${change} below dust. inputs=${inputTotal} nft=1000 ft=${grossSats} fee=${minerFee}`,
|
|
);
|
|
}
|
|
tb.addOutput({ to: wallet.cashaddr, amount: change });
|
|
|
|
console.log(` Change output: ${change} sats back to wallet`);
|
|
console.log(` Implied miner fee: ${minerFee} sats`);
|
|
|
|
// Broadcast.
|
|
const sent = await tb.send();
|
|
const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
|
|
console.log('');
|
|
console.log(`Mint TXID: ${txid}`);
|
|
divider();
|
|
console.log('Ready for step 6 (chipnet-swap.mjs) — the Q4 test itself.');
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('FAILED:', err?.message ?? err);
|
|
if (err?.stack) console.error(err.stack);
|
|
process.exit(1);
|
|
});
|