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>
186 lines
6.9 KiB
JavaScript
186 lines
6.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-mint-by-b.mjs — Wallet B mints Token A from Wallet A's contract.
|
|
*
|
|
* Demonstrates the "open" in "open mint": once Wallet A has deployed an
|
|
* open-mint Deviant token, any holder of BCH on chipnet can call mint()
|
|
* on that contract without Wallet A's involvement.
|
|
*
|
|
* Wallet B reads the public token registry (.chipnet-tokens.json — in
|
|
* production this would be served via BCMR or a public token explorer),
|
|
* reconstructs Token A's contract with the public constructor args, and
|
|
* mints to its own token-address. The receipt arrives at Wallet B's
|
|
* tokenaddr, not Wallet A's.
|
|
*
|
|
* Wallet B pays the full BCH cost (tokenAmount * rate) plus miner fee.
|
|
* Wallet A is not a signer, not a payer, not involved in any way except
|
|
* via the deployed contract bytecode that lives on chain.
|
|
*/
|
|
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_B = join(ROOT, '.chipnet-keys-b.json');
|
|
const TOKENS_FILE = join(ROOT, 'ChipnetTests', '.chipnet-tokens.json');
|
|
const ARTIFACT_PATH = join(ROOT, 'contracts/dist/DeviantOpenMintFT.json');
|
|
|
|
// Wallet B mints 30 Token A units = 30,000 sats at rate 1000.
|
|
const MINT_AMOUNT = 30n;
|
|
|
|
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;
|
|
}
|
|
|
|
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_B, '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');
|
|
|
|
const walletB = await TestNetWallet.fromWIF(wif);
|
|
const provider = new ElectrumNetworkProvider('chipnet');
|
|
const sigTemplateB = new SignatureTemplate(walletB.privateKey);
|
|
|
|
// Reconstruct the contract using the PUBLIC constructor args from the
|
|
// registry. Notice that `founderLockingBytecode` is a public parameter —
|
|
// it's already encoded into the P2SH32 contract address. Wallet B
|
|
// reads it without needing any private knowledge of Wallet A.
|
|
const tokenCategory = txidToConsensusBytes(tokenA.categoryId);
|
|
const founderLockingBytecode = hexToBin(tokenA.founderLockingBytecodeHex);
|
|
const contract = new Contract(
|
|
artifact,
|
|
[tokenCategory, BigInt(tokenA.rateSats), BigInt(tokenA.founderFeeBps), founderLockingBytecode],
|
|
{ provider },
|
|
);
|
|
|
|
if (contract.tokenAddress !== tokenA.contractTokenAddress) {
|
|
throw new Error(
|
|
`Contract address mismatch (param drift). Expected ${tokenA.contractTokenAddress}, got ${contract.tokenAddress}.`,
|
|
);
|
|
}
|
|
|
|
// Wallet B receives the new FTs at its OWN token-address — not Wallet A's.
|
|
const recipientLockingBytecode = p2pkhLockingBytecode(walletB.publicKeyHash);
|
|
|
|
const reservoirUtxos = await provider.getUtxos(contract.tokenAddress);
|
|
const reservoir = reservoirUtxos.find(
|
|
(u) => u.token?.nft?.capability === 'minting' && u.token?.category === tokenA.categoryId,
|
|
);
|
|
if (!reservoir) throw new Error('Token A reservoir not found at contract');
|
|
|
|
const walletBUtxos = await provider.getUtxos(walletB.cashaddr);
|
|
const bchUtxos = walletBUtxos.filter((u) => !u.token);
|
|
if (bchUtxos.length === 0) {
|
|
throw new Error('Wallet B has no BCH UTXO to fund the mint. Run npm run chipnet:fund-b first.');
|
|
}
|
|
|
|
const rateSats = BigInt(tokenA.rateSats);
|
|
const grossSats = MINT_AMOUNT * rateSats; // 30,000 sats
|
|
const minerFee = 2500n;
|
|
|
|
// Pick BCH UTXOs from Wallet B until we cover grossSats + minerFee + dust margin.
|
|
const target = grossSats + minerFee + 1_000n;
|
|
let picked = [];
|
|
let pickedTotal = 0n;
|
|
for (const u of bchUtxos) {
|
|
picked.push(u);
|
|
pickedTotal += BigInt(u.satoshis);
|
|
if (pickedTotal >= target) break;
|
|
}
|
|
if (pickedTotal < target) {
|
|
throw new Error(`Wallet B BCH sum ${pickedTotal} sats; need at least ${target}.`);
|
|
}
|
|
|
|
const reservoirRemaining = reservoir.token.amount - MINT_AMOUNT;
|
|
|
|
divider();
|
|
console.log(`Wallet B mints ${MINT_AMOUNT} Token A from Wallet A's open-mint contract`);
|
|
divider();
|
|
console.log(` Wallet B address: ${walletB.cashaddr}`);
|
|
console.log(` Wallet B token addr: ${walletB.tokenaddr}`);
|
|
console.log(` Reservoir input: ${reservoir.txid.slice(0, 16)}:${reservoir.vout} (${reservoir.satoshis} sat, ${reservoir.token.amount} FTs)`);
|
|
console.log(` BCH inputs (B): ${picked.length} totaling ${pickedTotal} sats`);
|
|
console.log(` Backing required: ${grossSats} sats for ${MINT_AMOUNT} Token A units`);
|
|
console.log(` Reservoir after: ${reservoirRemaining} Token A FTs`);
|
|
console.log(` Miner fee: ${minerFee} sats`);
|
|
|
|
const inputTotal = BigInt(reservoir.satoshis) + pickedTotal;
|
|
const change = inputTotal - 1_000n - grossSats - minerFee;
|
|
if (change < 546n) {
|
|
throw new Error(`Change ${change} below dust. inputs=${inputTotal} reservoir=1000 ft=${grossSats} fee=${minerFee}`);
|
|
}
|
|
console.log(` Change to B: ${change} sats`);
|
|
divider();
|
|
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(reservoir, contract.unlock.claim(MINT_AMOUNT, recipientLockingBytecode));
|
|
for (const u of picked) {
|
|
tb.addInput(u, sigTemplateB.unlockP2PKH());
|
|
}
|
|
|
|
tb.addOutput({
|
|
to: contract.tokenAddress,
|
|
amount: 1_000n,
|
|
token: {
|
|
amount: reservoirRemaining,
|
|
category: tokenA.categoryId,
|
|
nft: { capability: 'minting', commitment: '' },
|
|
},
|
|
});
|
|
|
|
tb.addOutput({
|
|
to: walletB.tokenaddr,
|
|
amount: grossSats,
|
|
token: { amount: MINT_AMOUNT, category: tokenA.categoryId },
|
|
});
|
|
|
|
tb.addOutput({ to: walletB.cashaddr, amount: change });
|
|
|
|
const sent = await tb.send();
|
|
const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
|
|
console.log('');
|
|
console.log(`*** Wallet B cross-founder mint TXID: ${txid} ***`);
|
|
divider();
|
|
console.log('Verify: Wallet B should now hold an FT UTXO with ' + MINT_AMOUNT + ' Token A,');
|
|
console.log('Wallet A is unchanged, reservoir decreased by exactly ' + MINT_AMOUNT + '.');
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('FAILED:', err?.message ?? err);
|
|
if (err?.stack) console.error(err.stack);
|
|
process.exit(1);
|
|
});
|