[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/bourgeoa/JavaScriptSolidServer/patchAppend/src/token.js [Back]  [Original]

/**
 * MRC20 Token Management
 *
 * Create, manage, and transfer MRC20 tokens anchored to Bitcoin via blocktrails.
 * Uses BIP-341 key chaining to derive unique taproot addresses for each state.
 *
 * Trail state stored at: {DATA_ROOT}/.well-known/token/{ticker}.json
 *
 * References:
 *   - Blocktrails: https://blocktrails.org/
 *   - BIP-341: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
 */

import crypto from 'crypto';
import fs from 'fs-extra';
import path from 'path';
import { secp256k1, schnorr } from '@noble/curves/secp256k1';
import { jcs, sha256Hex, btAddress } from './mrc20.js';

// --- Constants ---
const SECP_N = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');
const MRC20_PROFILE = 'mono.mrc20.v0.1';

// --- Byte helpers ---
function hexToU8(hex) {
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
  return bytes;
}
function bytesToHex(bytes) {
  return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}
function bytesToBigInt(bytes) {
  let r = 0n;
  for (const b of bytes) r = (r  s + a.length, 0);
  const result = new Uint8Array(total);
  let off = 0;
  for (const a of arrays) { result.set(a, off); off += a.length; }
  return result;
}

// --- Serialization helpers ---
function writeU32LE(val) {
  const b = new Uint8Array(4);
  b[0] = val & 0xff; b[1] = (val >> 8) & 0xff; b[2] = (val >> 16) & 0xff; b[3] = (val >> 24) & 0xff;
  return b;
}
function writeU64LE(val) {
  const b = new Uint8Array(8);
  const n = BigInt(val);
  for (let i = 0; i < 8; i++) b[i] = Number((n >> BigInt(i * 8)) & 0xffn);
  return b;
}
function writeVarInt(val) {
  if (val < 0xfd) return new Uint8Array([val]);
  if (val > 8) & 0xff]);
  throw new Error('VarInt too large');
}
function reverseTxid(txidHex) {
  const bytes = hexToU8(txidHex);
  bytes.reverse();
  return bytes;
}

// --- Crypto ---
function sha256Bytes(data) {
  const buf = data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(data, 'utf8');
  return new Uint8Array(crypto.createHash('sha256').update(buf).digest());
}
function taggedHash(tag, ...msgs) {
  const tagHash = sha256Bytes(Buffer.from(tag, 'utf8'));
  return sha256Bytes(concatBytes(tagHash, tagHash, ...msgs));
}

// --- BIP-341 key chaining ---
function btScalar(pubkeyCompressed, state) {
  const xOnly = pubkeyCompressed.slice(1);
  const stateBytes = typeof state === 'string' ? Buffer.from(state, 'utf8') : state;
  const sh = sha256Bytes(stateBytes);
  return bytesToBigInt(taggedHash('TapTweak', xOnly, sh)) % SECP_N;
}

function btDeriveChainedPubkey(pubkeyBase, states) {
  let P = secp256k1.ProjectivePoint.fromHex(bytesToHex(pubkeyBase));
  let cur = pubkeyBase;
  for (const s of states) {
    const t = btScalar(cur, s);
    P = P.add(secp256k1.ProjectivePoint.BASE.multiply(t));
    cur = new Uint8Array(P.toRawBytes(true));
  }
  return cur;
}

export function btDeriveChainedPrivkey(privkeyBytes, states) {
  let d = bytesToBigInt(privkeyBytes);
  let cur = new Uint8Array(secp256k1.getPublicKey(privkeyBytes, true));
  for (const s of states) {
    const t = btScalar(cur, s);
    d = (d + t) % SECP_N;
    cur = new Uint8Array(secp256k1.ProjectivePoint.BASE.multiply(d).toRawBytes(true));
  }
  return bigIntToBytes(d);
}

export function p2trScript(xonly) {
  return concatBytes(new Uint8Array([0x51, 0x20]), xonly);
}

// --- Bitcoin transaction building ---
export function buildTransaction(inputs, outputs, privkeyBytes) {
  const internalXOnly = new Uint8Array(secp256k1.getPublicKey(privkeyBytes, true)).slice(1);
  const untweakedHex = '5120' + bytesToHex(internalXOnly);
  const needsTweak = bytesToHex(inputs[0].scriptPubKey) !== untweakedHex;
  let signingKey = privkeyBytes;
  if (needsTweak) {
    const tweak = taggedHash('TapTweak', internalXOnly);
    const t = bytesToBigInt(tweak);
    let d = bytesToBigInt(privkeyBytes);
    const fullPub = secp256k1.getPublicKey(privkeyBytes, false);
    if (fullPub[64] & 1) d = SECP_N - d;
    signingKey = bigIntToBytes((d + t) % SECP_N);
  }

  const version = 2, locktime = 0, sequence = 0xfffffffd;
  const serOutputs = outputs.map(o =>
    concatBytes(writeU64LE(o.amount), writeVarInt(o.scriptPubKey.length), o.scriptPubKey)
  );

  const shaPrevouts = sha256Bytes(concatBytes(...inputs.map(i =>
    concatBytes(reverseTxid(i.txid), writeU32LE(i.vout))
  )));
  const shaAmounts = sha256Bytes(concatBytes(...inputs.map(i => writeU64LE(i.amount))));
  const shaScriptPubKeys = sha256Bytes(concatBytes(...inputs.map(i =>
    concatBytes(writeVarInt(i.scriptPubKey.length), i.scriptPubKey)
  )));
  const shaSequences = sha256Bytes(concatBytes(...inputs.map(() => writeU32LE(sequence))));
  const shaOutputs = sha256Bytes(concatBytes(...serOutputs));

  const sigs = [];
  for (let i = 0; i < inputs.length; i++) {
    const sigMsg = concatBytes(
      new Uint8Array([0x00, 0x00]),
      writeU32LE(version), writeU32LE(locktime),
      shaPrevouts, shaAmounts, shaScriptPubKeys, shaSequences, shaOutputs,
      new Uint8Array([0x00]),
      writeU32LE(i)
    );
    const sighash = taggedHash('TapSighash', sigMsg);
    sigs.push(schnorr.sign(sighash, signingKey));
  }

  const parts = [
    writeU32LE(version),
    new Uint8Array([0x00, 0x01]),
    writeVarInt(inputs.length)
  ];
  for (const inp of inputs) {
    parts.push(reverseTxid(inp.txid), writeU32LE(inp.vout), new Uint8Array([0x00]), writeU32LE(sequence));
  }
  parts.push(writeVarInt(outputs.length));
  for (const so of serOutputs) parts.push(so);
  for (const sig of sigs) {
    parts.push(new Uint8Array([0x01]), writeVarInt(sig.length), sig);
  }
  parts.push(writeU32LE(locktime));
  return bytesToHex(concatBytes(...parts));
}

export async function broadcastTx(rawTxHex, mempoolUrl) {
  const res = await fetch(`${mempoolUrl}/api/tx`, {
    method: 'POST',
    headers: { 'Content-Type': 'text/plain' },
    body: rawTxHex
  });
  if (!res.ok) {
    const err = await res.text();
    throw new Error(`Broadcast failed: ${err}`);
  }
  return res.text();
}

// --- Trail persistence ---
function trailDir(root) {
  return path.join(root || process.env.DATA_ROOT || './data', '.well-known', 'token');
}

function trailPath(ticker, root) {
  return path.join(trailDir(root), `${ticker.toLowerCase()}.json`);
}

export async function loadTrail(ticker, root) {
  try {
    const data = await fs.readFile(trailPath(ticker, root), 'utf8');
    return JSON.parse(data);
  } catch { return null; }
}

async function saveTrail(trail, root) {
  await fs.ensureDir(trailDir(root));
  await fs.writeFile(trailPath(trail.ticker, root), JSON.stringify(trail, null, 2));
}

export async function listTrails(root) {
  try {
    const dir = trailDir(root);
    const files = await fs.readdir(dir);
    const trails = [];
    for (const f of files) {
      if (f.endsWith('.json')) {
        const data = await fs.readFile(path.join(dir, f), 'utf8');
        trails.push(JSON.parse(data));
      }
    }
    return trails;
  } catch { return []; }
}

// --- TXO URI parsing ---
export function parseTxoUri(uri) {
  // txo:btc:txid:vout?amount=N&key=hex
  const match = uri.match(/(?:txo:btc:)?([0-9a-f]{64}):(\d+)\?amount=(\d+)&key=([0-9a-f]{64})/i);
  if (!match) throw new Error('Invalid TXO URI format');
  return {
    txid: match[1],
    vout: parseInt(match[2], 10),
    amount: parseInt(match[3], 10),
    privkey: match[4]
  };
}

// --- Mint: create genesis MRC20 token ---
export async function mintToken({ ticker, name, supply, voucher, mempoolUrl = 'https://mempool.space/testnet4', network = 'testnet4', root }) {
  const txo = parseTxoUri(voucher);
  const privkeyBytes = hexToU8(txo.privkey);
  const pubkeyBase = new Uint8Array(secp256k1.getPublicKey(privkeyBytes, true));
  const pubkeyBaseHex = bytesToHex(pubkeyBase);

  // Check if token already exists
  const existing = await loadTrail(ticker, root);
  if (existing) throw new Error(`Token ${ticker} already exists`);

  // Create genesis MRC20 state
  const genesisState = {
    profile: MRC20_PROFILE,
    prev: '0'.repeat(64),
    seq: 0,
    ticker,
    name: name || ticker,
    decimals: 0,
    supply,
    balances: { [pubkeyBaseHex]: supply },
    ops: []
  };
  const genesisJcs = jcs(genesisState);

  // Derive genesis taproot address
  const genesisP = btDeriveChainedPubkey(pubkeyBase, [genesisJcs]);
  const genesisXonly = genesisP.slice(1);
  const genesisScript = p2trScript(genesisXonly);
  const genesisAddr = btAddress(pubkeyBaseHex, [genesisJcs], network);

  // Fetch voucher scriptPubKey
  const txResp = await fetch(`${mempoolUrl}/api/tx/${txo.txid}`);
  if (!txResp.ok) throw new Error('Could not fetch voucher transaction');
  const txData = await txResp.json();
  const prevOut = txData.vout?.[txo.vout];
  if (!prevOut) throw new Error(`Voucher output ${txo.vout} not found`);
  const scriptPubKey = hexToU8(prevOut.scriptpubkey);

  // Build and broadcast genesis tx
  const fee = 300;
  const outputAmount = txo.amount - fee;
  if (outputAmount 

Web Proxy Viewer  |  New URL  |  Original Page