UNPKG

sbtc-bridge-lib

Version:

Library for sBTC Bridge web client and API apps

509 lines (508 loc) 20.2 kB
import * as secp from '@noble/secp256k1'; import * as btc from '@scure/btc-signer'; import { hex } from '@scure/base'; import { c32address, c32addressDecode } from 'c32check'; import * as P from 'micro-packed'; import { bitcoinToSats } from './formatting.js'; import { hashMessage } from '@stacks/encryption'; import { sha256 } from '@noble/hashes/sha256'; import { ripemd160 } from '@noble/hashes/ripemd160'; import { recoverSignature } from "micro-stacks/connect"; import { getAddressFromOutScript, getNet } from './wallet_utils.js'; const concat = P.concatBytes; export const MAGIC_BYTES_TESTNET = '5432'; export const MAGIC_BYTES_MAINNET = '5832'; export const PEGIN_OPCODE = '3C'; export const PEGOUT_OPCODE = '3E'; const priv = secp.utils.randomPrivateKey(); const keySetForFeeCalculation = []; keySetForFeeCalculation.push({ priv, ecdsaPub: secp.getPublicKey(priv, true), schnorrPub: secp.getPublicKey(priv, false) }); export function parseDepositPayload(d1) { const magicOp = getMagicAndOpCode(d1); if (magicOp.magic) { return parseDepositPayloadNoMagic(d1.subarray(2)); } return parseDepositPayloadNoMagic(d1); } function parseDepositPayloadNoPrincipal(d1) { const opcode = hex.encode(d1.subarray(0, 1)).toUpperCase(); const addr0 = parseInt(hex.encode(d1.subarray(1, 2)), 16); const addr1 = hex.encode(d1.subarray(2, 22)); const stacksAddress = c32address(addr0, addr1); return { opcode, prinType: 0, stacksAddress, lengthOfCname: 0, cname: undefined, lengthOfMemo: 0, memo: undefined, revealFee: 0, amountSats: 0 }; } function parseDepositPayloadNoMagic(d1) { //console.log('payload rev: ', hex.encode(d1)) const opcode = hex.encode(d1.subarray(0, 1)).toUpperCase(); if (opcode.toUpperCase() !== PEGIN_OPCODE) throw new Error('Wrong OPCODE : expected: ' + PEGIN_OPCODE + ' received: ' + opcode); const prinType = parseInt(hex.encode(d1.subarray(1, 2)), 16); if (prinType === 22 || prinType === 26) return parseDepositPayloadNoPrincipal(d1); const addr0 = parseInt(hex.encode(d1.subarray(2, 3)), 16); const addr1 = hex.encode(d1.subarray(3, 23)); const stacksAddress = c32address(addr0, addr1); const lengthOfCname = parseInt(hex.encode(d1.subarray(23, 24)), 8); let cname; if (lengthOfCname > 0) { cname = new TextDecoder().decode(d1.subarray(24, 24 + lengthOfCname)); } let current = 24 + lengthOfCname; //let memo; //const lengthOfMemo = parseInt(hex.encode(d1.subarray(current, current + 1)), 8); //if (lengthOfMemo > 0) { // memo = new TextDecoder().decode(d1.subarray(current + 1, lengthOfMemo + current + 1)); //} let revealFee = 0; if (d1.length > current + 1) { // + lengthOfMemo) { //current = current + 1 + lengthOfMemo; const rev = d1.subarray(current); console.log('parseDepositPayloadNoMagic: ' + hex.encode(rev)); revealFee = bigUint64ToAmount(rev); console.log('parseDepositPayloadNoMagic:revealFee: ' + revealFee); } return { opcode, prinType, stacksAddress, lengthOfCname, cname, lengthOfMemo: 0, memo: undefined, revealFee, amountSats: 0 }; } export function amountToUint8(amt, size) { const buffer = new ArrayBuffer(size); const view = new DataView(buffer); view.setUint8(0, amt); // Max unsigned 32-bit integer const res = new Uint8Array(view.buffer); return res; } /** export function uint8ToAmount(buf:Uint8Array):number { const hmmm = hex.decode(hex.encode(buf)) // needed to make work ? const view = new DataView(hmmm.buffer); const amt = view.getUint32(0); return amt; } */ export function amountToBigUint64(amt, size) { //P..U64BE(BigInt(amt)) const buffer = new ArrayBuffer(size); const view = new DataView(buffer); view.setBigUint64(0, BigInt(amt)); // Max unsigned 32-bit integer const res = new BigUint64Array(view.buffer); return hex.decode(bufferToHex(res.buffer)); //(amt.toString(16).padStart(16, "0")) } function bufferToHex(buffer) { return [...new Uint8Array(buffer)] .map(b => b.toString(16).padStart(2, "0")) .join(""); } export function bigUint64ToAmount(buf) { // rencode in case it was passed in a string encoded. if (!buf || buf.byteLength === 0) return 0; buf = hex.decode(hex.encode(buf)); const view = new DataView(buf.buffer, 0, 8); const amt = view.getBigUint64(0); return Number(amt); } export function parseWithdrawPayload(network, d0, bitcoinAddress, sigMode) { const d1 = hex.decode(d0); const magicOp = getMagicAndOpCode(d1); if (magicOp.magic) { return parseWithdrawalPayloadNoMagic(network, d1.subarray(2), bitcoinAddress, sigMode); } return parseWithdrawalPayloadNoMagic(network, d1, bitcoinAddress, sigMode); } function parseWithdrawalPayloadNoMagic(network, d1, bitcoinAddress, sigMode) { const opcode = hex.encode(d1.subarray(0, 1)).toUpperCase(); if (opcode !== '3E') throw new Error('Wrong opcode for withdraw: should be 3E was ' + opcode); const amtB = d1.subarray(1, 9); const amountSats = bigUint64ToAmount(amtB); let signature = (hex.encode(d1.subarray(9, 74))); const msgHash = getStacksSimpleHashOfDataToSign(network, amountSats, bitcoinAddress); const pubKey = getPubkeySignature(hex.decode(msgHash), signature, sigMode); console.log('parseWithdrawalPayloadNoMagic:pubKey: ' + hex.encode(pubKey)); const stxAddresses = getStacksAddressFromPubkey(pubKey); const stacksAddress = (network === network) ? stxAddresses.tp2pkh : stxAddresses.mp2pkh; return { opcode, stacksAddress, signature, amountSats }; } export var PrincipalType; (function (PrincipalType) { PrincipalType["STANDARD"] = "05"; PrincipalType["CONTRACT"] = "06"; })(PrincipalType || (PrincipalType = {})); export function buildDepositPayload(network, stacksAddress) { const net = getNet(network); return buildDepositPayloadInternal(net, 0, stacksAddress, false); } export function buildDepositPayloadOpDrop(network, stacksAddress, revealFee) { const net = getNet(network); return buildDepositPayloadInternal(net, revealFee, stacksAddress, true); } function buildDepositPayloadInternal(net, amountSats, address, opDrop) { const magicBuf = (typeof net === 'object' && (net.bech32 === 'tb' || net.bech32 === 'bcrt')) ? hex.decode(MAGIC_BYTES_TESTNET) : hex.decode(MAGIC_BYTES_MAINNET); const opCodeBuf = hex.decode(PEGIN_OPCODE); const addr = c32addressDecode(address.split('.')[0]); //const addr0Buf = hex.encode(amountToUint8(addr[0], 1)); const addr0Buf = (hex.decode(addr[0].toString(16))); const addr1Buf = hex.decode(addr[1]); const cnameLength = new Uint8Array(1); //const memoLength = new Uint8Array(1); const principalType = (address.indexOf('.') > -1) ? hex.decode('06') : hex.decode('05'); let buf1 = concat(opCodeBuf, principalType, addr0Buf, addr1Buf); if (address.indexOf('.') > -1) { const cnameBuf = new TextEncoder().encode(address.split('.')[1]); const cnameBufHex = hex.encode(cnameBuf); let cnameLen; try { cnameLen = cnameLength.fill(cnameBufHex.length); } catch (err) { cnameLen = hex.decode(cnameBuf.length.toString(8)); } buf1 = concat(buf1, cnameLen, cnameBuf); } else { cnameLength.fill(0); buf1 = concat(buf1, cnameLength); } /** if (memo) { const memoBuf = new TextEncoder().encode(memo); const memoLength = hex.decode(memoBuf.length.toString(8)); buf1 = concat(buf1, memoLength, memoBuf); } else { memoLength.fill(0); buf1 = concat(buf1, memoLength); } */ if (opDrop) { const feeBuf = amountToBigUint64(amountSats, 8); buf1 = concat(buf1, feeBuf); } if (!opDrop) return hex.encode(concat(magicBuf, buf1)); return hex.encode(buf1); } /** * @param network (testnet|mainnet) * @param amount * @param signature * @returns */ export function buildWithdrawPayload(network, amount, signature) { const net = getNet(network); return buildWithdrawPayloadInternal(net, amount, signature, false); } /** * Withdrawal using commit reveal (op_drop) pattern * @param network (testnet|mainnet) * @param amount * @param signature * @returns */ export function buildWithdrawPayloadOpDrop(network, amount, signature) { const net = getNet(network); return buildWithdrawPayloadInternal(net, amount, signature, true); } function buildWithdrawPayloadInternal(net, amount, signature, opDrop) { const magicBuf = (typeof net === 'object' && (net.bech32 === 'tb' || net.bech32 === 'bcrt')) ? hex.decode(MAGIC_BYTES_TESTNET) : hex.decode(MAGIC_BYTES_MAINNET); const opCodeBuf = hex.decode(PEGOUT_OPCODE); ///const amountBuf = amountToBigUint64(amount, 8); const amountBytes = P.U64BE.encode(BigInt(amount)); //const amountRev = bigUint64ToAmount(amountBuf); const data = concat(opCodeBuf, amountBytes, hex.decode(signature)); if (!opDrop) return hex.encode(concat(magicBuf, data)); return hex.encode(data); } export function readDepositValue(outputs) { let amountSats = 0; if (outputs[0].scriptPubKey.type.toLowerCase() === 'nulldata') { amountSats = bitcoinToSats(outputs[1].value); } else { amountSats = bitcoinToSats(outputs[0].value); } return amountSats; } /** * * @param network * @param txHex * @returns */ export function parsePayloadFromTransaction(network, txHex) { const tx = btc.Transaction.fromRaw(hex.decode(txHex), { allowUnknowInput: true, allowUnknowOutput: true, allowUnknownOutputs: true, allowUnknownInputs: true }); const out0 = tx.getOutput(0); const script0 = out0.script; const spendScr = btc.OutScript.decode(script0); let payload = {}; if (spendScr.type === 'unknown') { if (!tx.getOutput(1) || !tx.getOutput(1).script) throw new Error('no output 1'); payload = parsePayloadFromOutput(network, tx); if (payload.opcode === '3C') payload.amountSats = Number(tx.getOutput(1).amount); //payload.dust = Number(tx.getOutput(1).amount) } //console.log('parsePayloadFromTransaction: payload: ' + payload); return payload; } export function parsePayloadFromOutput(network, tx) { var _a, _b; const out0 = tx.getOutput(0); let d1 = (_a = out0.script) === null || _a === void 0 ? void 0 : _a.subarray(5); // strip the op type and data length let witnessData = getMagicAndOpCode(d1); if (witnessData.opcode !== '3C' && witnessData.opcode !== '3E') { d1 = (_b = out0.script) === null || _b === void 0 ? void 0 : _b.subarray(2); // strip the op type and data length witnessData = getMagicAndOpCode(d1); } witnessData.txType = btc.OutScript.decode(out0.script).type; let innerPayload = {}; if (witnessData.opcode === '3C') { innerPayload = parseDepositPayload(d1); innerPayload.sbtcWallet = getAddressFromOutScript(network, tx.getOutput(1).script); //const inScript = btc.RawInput.encode({ // index: tx.getInput(0).index || 0, // sequence: tx.getInput(0).sequence || 0, // txid: tx.getInput(0).txid as Uint8Array, // finalScriptSig: tx.getInput(0).finalScriptSig as Uint8Array, //}); if (tx.outputsLength > 2) innerPayload.spendingAddress = getAddressFromOutScript(network, tx.getOutput(2).script); //console.log('parsePayloadFromTransaction:spendingAddress: ' + innerPayload.spendingAddress) return innerPayload; } else if (witnessData.opcode.toUpperCase() === '3E') { const recipient = getAddressFromOutScript(network, tx.getOutput(1).script); try { innerPayload = parseWithdrawPayload(network, hex.encode(d1), recipient, 'vrs'); } catch (err) { innerPayload = parseWithdrawPayload(network, hex.encode(d1), recipient, 'rsv'); } innerPayload.spendingAddress = getAddressFromOutScript(network, tx.getOutput(1).script); if (tx.outputsLength > 2) innerPayload.sbtcWallet = getAddressFromOutScript(network, tx.getOutput(2).script); innerPayload.spendingAddress = recipient; return innerPayload; } else { throw new Error('Wrong opcode : expected: 3E or 3C : recieved: ' + witnessData.opcode); } } /** * * @param network * @param amount * @param bitcoinAddress * @returns */ export function getDataToSign(network, amount, bitcoinAddress) { const net = getNet(network); const tx = new btc.Transaction({ allowUnknowOutput: true, allowUnknownInputs: true, allowUnknownOutputs: true }); tx.addOutputAddress(bitcoinAddress, BigInt(amount), net); const amountBytes = P.U64BE.encode(BigInt(amount)); const data = concat(amountBytes, tx.getOutput(0).script); return `Withdraw request for ${amount} satoshis to the bitcoin address ${bitcoinAddress} (${hex.encode(data)})`; } export function getStacksSimpleHashOfDataToSign(network, amount, bitcoinAddress) { const dataToSign = getDataToSign(network, amount, bitcoinAddress); const msgHash = hashMessage(dataToSign); //console.log('getStacksSimpleHashOfDataToSign:dataToSign: ' + hex.encode(dataToSign)) //console.log('getStacksSimpleHashOfDataToSign:msgHash: ' + hex.encode(msgHash)) return hex.encode(msgHash); } function reverseSigBits(signature) { if (signature.startsWith('00')) { const sig = signature.substring(2); return sig + '00'; //} else { // const sig = signature.substring(0, signature.length - 2) // const sigPre = signature.substring(signature.length - 2) // return sigPre + sig } return signature; } function getPubkeySignature(messageHash, signature, sigMode) { const sigM = recoverSignature({ signature: signature, mode: sigMode }); // vrs to rsv let sig = new secp.Signature(sigM.signature.r, sigM.signature.s); const recBit = parseInt(hex.encode(hex.decode(signature).subarray(0, 1))); //console.log('getPubkeySignature:signature' + signature) //console.log('getPubkeySignature:recBit' + recBit) //console.log('getPubkeySignature:sigMode' + sigMode) sig = sig.addRecoveryBit(recBit); const pubkeyM = sig.recoverPublicKey(messageHash); const pubkey = hex.decode(pubkeyM.toHex()); //console.log(pubkeyM.toHex()) return pubkey; } /** * * @param messageHash * @param signature * @returns */ export function getStacksAddressFromSignature(messageHash, signature) { const pubkey = getPubkeySignature(messageHash, signature, 'vrs'); return getStacksAddressFromPubkey(pubkey); } export function getStacksAddressFromSignatureRsv(messageHash, signature) { const pubkey = getPubkeySignature(messageHash, signature, 'rsv'); return getStacksAddressFromPubkey(pubkey); } export function getStacksAddressFromPubkey(pubkey) { const addresses = { tp2pkh: publicKeyToStxAddress(pubkey, StacksNetworkVersion.testnetP2PKH), tp2sh: publicKeyToStxAddress(pubkey, StacksNetworkVersion.testnetP2SH), mp2pkh: publicKeyToStxAddress(pubkey, StacksNetworkVersion.mainnetP2PKH), mp2sh: publicKeyToStxAddress(pubkey, StacksNetworkVersion.mainnetP2SH), }; //console.log('getStacksAddressFromPubkey: addresses: ', addresses) return addresses; } function publicKeyToStxAddress(publicKey, addressVersion = StacksNetworkVersion.mainnetP2PKH) { return c32address(addressVersion, hex.encode(hash160(publicKey))); } function hash160(input) { const sha = sha256(input); return ripemd160(sha); } export function getMagicAndOpCode(d1) { if (!d1 || d1.length < 2) throw new Error('no magic data passed'); const magic = hex.encode(d1.subarray(0, 2)); if (magic === MAGIC_BYTES_TESTNET || magic === MAGIC_BYTES_MAINNET) { return { magic: magic.toUpperCase(), opcode: hex.encode(d1.subarray(2, 3)).toUpperCase() }; } return { opcode: hex.encode(d1.subarray(0, 1)).toUpperCase() }; } var StacksNetworkVersion; (function (StacksNetworkVersion) { StacksNetworkVersion[StacksNetworkVersion["mainnetP2PKH"] = 22] = "mainnetP2PKH"; StacksNetworkVersion[StacksNetworkVersion["mainnetP2SH"] = 20] = "mainnetP2SH"; StacksNetworkVersion[StacksNetworkVersion["testnetP2PKH"] = 26] = "testnetP2PKH"; StacksNetworkVersion[StacksNetworkVersion["testnetP2SH"] = 21] = "testnetP2SH"; })(StacksNetworkVersion || (StacksNetworkVersion = {})); /** * Ensure we don't overwrite the original object with Uint8Arrays these can't be serialised to local storage. * @param script * @returns */ export function fromStorable(script) { const clone = JSON.parse(JSON.stringify(script)); if (typeof script.tweakedPubkey !== 'string') return clone; return codifyScript(clone, true); } /** * * @param script * @returns */ export function toStorable(script) { //const copied = JSON.parse(JSON.stringify(script)); return codifyScript(script, false); } function codifyScript(script, asString) { return { address: script.address, script: codify(script.script, asString), paymentType: (script.type) ? script.type : script.paymentType, witnessScript: codify(script.witnessScript, asString), redeemScript: codify(script.redeemScript, asString), leaves: (script.leaves) ? codifyLeaves(script.leaves, asString) : undefined, tapInternalKey: codify(script.tapInternalKey, asString), tapLeafScript: (script.tapLeafScript) ? codifyTapLeafScript(script.tapLeafScript, asString) : undefined, tapMerkleRoot: codify(script.tapMerkleRoot, asString), tweakedPubkey: codify(script.tweakedPubkey, asString), }; } function codifyTapLeafScript(tapLeafScript, asString) { if (tapLeafScript[0]) { const level0 = tapLeafScript[0]; if (level0[0]) tapLeafScript[0][0].internalKey = codify(tapLeafScript[0][0].internalKey, asString); if (level0[0]) tapLeafScript[0][0].merklePath[0] = codify(tapLeafScript[0][0].merklePath[0], asString); if (level0[1]) tapLeafScript[0][1] = codify(tapLeafScript[0][1], asString); } if (tapLeafScript[1]) { const level1 = tapLeafScript[1]; if (level1[0]) tapLeafScript[1][0].internalKey = codify(tapLeafScript[1][0].internalKey, asString); if (level1[0]) tapLeafScript[1][0].merklePath[0] = codify(tapLeafScript[1][0].merklePath[0], asString); if (level1[1]) tapLeafScript[1][1] = codify(tapLeafScript[1][1], asString); } return tapLeafScript; } function codify(arg, asString) { if (!arg) return; if (typeof arg === 'string') { return hex.decode(arg); } else { return hex.encode(arg); } } function codifyLeaves(leaves, asString) { if (leaves[0]) { const level1 = leaves[0]; if (level1.controlBlock) leaves[0].controlBlock = codify(leaves[0].controlBlock, asString); if (level1.hash) leaves[0].hash = codify(leaves[0].hash, asString); if (level1.script) leaves[0].script = codify(leaves[0].script, asString); if (level1.path && level1.path[0]) leaves[0].path[0] = codify(leaves[0].path[0], asString); } if (leaves[1]) { const level1 = leaves[1]; if (level1.controlBlock) leaves[1].controlBlock = codify(leaves[1].controlBlock, asString); if (level1.hash) leaves[1].hash = codify(leaves[1].hash, asString); if (level1.script) leaves[1].script = codify(leaves[1].script, asString); if (level1.path && level1.path[0]) leaves[1].path[0] = codify(leaves[1].path[0], asString); } return leaves; }