UNPKG

sbtc-bridge-lib

Version:

Library for sBTC Bridge web client and API apps

427 lines (426 loc) 18.1 kB
import * as secp from '@noble/secp256k1'; import * as btc from '@scure/btc-signer'; import { hex } from '@scure/base'; import { schnorr } from '@noble/curves/secp256k1'; const privKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101'); const priv = secp.utils.randomPrivateKey(); export const REGTEST_NETWORK = { bech32: 'bcrt', pubKeyHash: 0x6f, scriptHash: 0xc4, wif: 0xc4 }; export function getNet(network) { let net = btc.TEST_NETWORK; if (network === 'devnet') net = REGTEST_NETWORK; else if (network === 'mainnet') net = btc.NETWORK; return net; } const keySetForFeeCalculation = []; keySetForFeeCalculation.push({ priv, ecdsaPub: secp.getPublicKey(priv, true), schnorrPub: secp.getPublicKey(priv, false) }); export const sbtcMiniContracts = { token: 'sbtc-token', controller: 'sbtc-controller', pool: 'sbtc-stacking-pool', registry: 'sbtc-registry', btcTxHelper: 'sbtc-btc-tx-helper', depositProcessor: 'sbtc-deposit-processor', handOff: 'sbtc-hand-off', walletVote: 'sbtc-wallet-vote', withdrawalProcessor: 'sbtc-withdrawal-processor', }; const testWallets = [ { "privateKey": "ad1195070a559967782fb6eaa622a2baeaed9d9d254880059f9fbf781cf7852c", "ecdsaPub": "0235bbcc0b6898fc63d6e856c10b67490b153f8866a88b7e59b2229fb2dc9cf102", "schnorrPub": "0435bbcc0b6898fc63d6e856c10b67490b153f8866a88b7e59b2229fb2dc9cf102369bdef88e0c63b560a7d5295347e6dc6cd9d2158a8edc906ba09ac1019db0f8", }, { "privateKey": "b3fd3a7216621aa796270da8149298a6f1cbf2eba4a4fc3cc21725f289d2551d", "ecdsaPub": "0235bbcc0b6898fc63d6e856c10b67490b153f8866a88b7e59b2229fb2dc9cf102", "schnorrPub": "0435bbcc0b6898fc63d6e856c10b67490b153f8866a88b7e59b2229fb2dc9cf102369bdef88e0c63b560a7d5295347e6dc6cd9d2158a8edc906ba09ac1019db0f8" } ]; export const sbtcWallets = [ { "sbtcAddress": "tb1pf74xr0x574farj55t4hhfvv0vpc9mpgerasawmf5zk9suauckugqdppqe8", "pubKey": "264bd0d3bd80ea2da383b0a2a29f53d258e05904d2279f5f223053b987a3fd56", "desc": "tr([760ce8cf/86'/1'/0'/0/1]264bd0d3bd80ea2da383b0a2a29f53d258e05904d2279f5f223053b987a3fd56)#j4wq04cw", "parent_desc": "tr([760ce8cf/86'/1'/0']tpubDDQtKohNhMryjsYgQu8hsZ1BMXJWb1h4xGDZvsQV5ZK9E5QDNgp3w1h9N2XTyz6GVDmMcbAw5YU67mcGousktHxjVTx6RmqXX6GfJJrkqqh/0/*)#kqt0kevz", "scriptPubKey": "51204faa61bcd4f553d1ca945d6f74b18f60705d85191f61d76d34158b0e7798b710", "witness_program": "4faa61bcd4f553d1ca945d6f74b18f60705d85191f61d76d34158b0e7798b710", }, { "sbtcAddress": "tb1pmmkznvm0pq5unp6geuwryu2f0m8xr6d229yzg2erx78nnk0ms48sk9s6q7", "pubKey": "802fb08c62f33a5e074dae2fc19441e7cef96c6e5a1ffa4065e5f7a8423816a3", "desc": "tr([7e0bf729/86'/1'/0'/0/2]802fb08c62f33a5e074dae2fc19441e7cef96c6e5a1ffa4065e5f7a8423816a3)#d8elhne5", "parent_desc": "tr([7e0bf729/86'/1'/0']tpubDCzcBRDqD1G23fAdF79sTfdECnfRprb5uGKb9vKBxrH4uZbC46ZJmxtSdYHwEJykzuzZV3KUGtFSRaoNAJuZpRSCiKoC1FUxkmRjPjDrbSA/0/*)#a8uhq8yj", "scriptPubKey": "5120deec29b36f0829c98748cf1c3271497ece61e9aa5148242b23378f39d9fb854f", "witness_program": "deec29b36f0829c98748cf1c3271497ece61e9aa5148242b23378f39d9fb854f", } ]; /** * Constructs the script hash with script paths corresponding to two internal * test wallets. */ export function getTestAddresses(network) { const net = getNet(network); return { fromBtcAddress: btc.getAddress('tr', hex.decode(testWallets[0].privateKey), net), sbtcWalletAddress: sbtcWallets[0].sbtcAddress, //reveal: btc.getAddress('tr', hex.decode(testWallets[0].privateKey), net) as string, revealPub: hex.encode(schnorr.getPublicKey(testWallets[0].privateKey)), //revealPrv: testWallets[0].privateKey, //reclaim: btc.getAddress('tr', hex.decode(testWallets[1].privateKey), net) as string, reclaimPub: hex.encode(schnorr.getPublicKey(testWallets[1].privateKey)), //reclaimPrv: testWallets[1].privateKey, stacksAddress: (network === 'testnet') ? 'ST1RBP62PR532FWVP7JRGC9SVFKKHD1JYK23KYNN0' : 'unsupported' }; } // Address from a 33 byte public key (returns the pub key if schnorr pub key passed in) export function addressFromPubkey(network, pubkey) { const net = getNet(network); try { return btc.Address(net).encode(btc.OutScript.decode(pubkey)); } catch (err) { console.error('needs to be a 33 byte public key - doesnot work for schnorr pub keys.'); return hex.encode(pubkey); } } export function checkAddressForNetwork(net, address) { if (!address || typeof address !== 'string') throw new Error('No address passed'); if (address.length < 10) throw new Error('Address is undefined'); if (net === 'devnet') return; if (net === 'testnet') { if (address.startsWith('bc')) throw new Error('Mainnet address passed to testnet app: ' + address); else if (address.startsWith('3')) throw new Error('Mainnet address passed to testnet app: ' + address); else if (address.startsWith('1')) throw new Error('Mainnet address passed to testnet app: ' + address); else if (address.startsWith('SP') || address.startsWith('sp')) throw new Error('Mainnet stacks address passed to testnet app: ' + address); } else { if (address.startsWith('tb')) throw new Error('Testnet address passed to testnet app: ' + address); else if (address.startsWith('2')) throw new Error('Testnet address passed to testnet app: ' + address); else if (address.startsWith('m')) throw new Error('Testnet address passed to testnet app: ' + address); else if (address.startsWith('n')) throw new Error('Testnet address passed to testnet app: ' + address); else if (address.startsWith('ST') || address.startsWith('st')) throw new Error('Testnet stacks address passed to testnet app: ' + address); } } /** * * @param amount - if deposit this is the amount the user is sending. Note: 0 for withdrawals * @param revealPayment - if op drop this is the gas fee for the reveal tx * @param tx - the to add input to * @param feeCalc - true if called for the purposes of calculating the fee (i.e. okay to sign inputs with internal key) * @param utxos - the utxos being spent from * @param paymentPublicKey - pubkey used in script hash payments export function addInputs (network:string, amount:number, revealPayment:number, tx:btc.Transaction, feeCalc:boolean, utxos:Array<UTXO>, paymentPublicKey:string, userSchnorrPubKey:string) { const bar = revealPayment + amount; let amt = 0; for (const utxo of utxos) { const hexy = (utxo.tx.hex) ? utxo.tx.hex : utxo.tx const script = btc.RawTx.decode(hex.decode(hexy)) if (amt < bar && utxo.status.confirmed) { amt += utxo.value; //const pubkey = '0248159447374471c5a6cfa18c296e6e297dbf125a9e6792435a87e80c4f771493' //const script1 = (btc.p2ms(1, [hex.decode(pubkey)])) const txType = utxo.tx.vout[utxo.vout].scriptPubKey.type; if (txType === 'scripthash') { // educated guess at the p2sh wrapping based on the type of the other (non change) output... let wrappedType = '' if (utxo.vout === 1) { wrappedType = utxo.tx.vout[0].scriptPubKey.type } else { wrappedType = utxo.tx.vout[1].scriptPubKey.type } const net = (network === 'testnet') ? btc.TEST_NETWORK : btc.NETWORK; let p2shObj; if (wrappedType === 'witness_v0_keyhash') { p2shObj = btc.p2sh(btc.p2wpkh(hex.decode(paymentPublicKey)), net) } else if (wrappedType === 'witness_v1_taproot') { p2shObj = btc.p2sh(btc.p2tr(hex.decode(userSchnorrPubKey)), net) } else if (wrappedType.indexOf('multi') > -1) { p2shObj = btc.p2sh(btc.p2ms(1, [hex.decode(paymentPublicKey)]), net) } else { p2shObj = btc.p2sh(btc.p2pkh(hex.decode(paymentPublicKey)), net) } const nextI:btc.TransactionInput = { txid: hex.decode(utxo.txid), index: utxo.vout, nonWitnessUtxo: hexy, redeemScript: p2shObj.redeemScript } tx.addInput(nextI); } else { let witnessUtxo = { script: script.outputs[utxo.vout].script, amount: BigInt(utxo.value) } if (feeCalc) { witnessUtxo = { amount: BigInt(utxo.value), script: btc.p2wpkh(secp.getPublicKey(privKey, true)).script, } } const nextI:btc.TransactionInput = { txid: hex.decode(utxo.txid), index: utxo.vout, nonWitnessUtxo: hexy, witnessUtxo } tx.addInput(nextI); } } } } */ export function addInputs(network, amount, revealPayment, transaction, feeCalc, utxos, paymentPublicKey) { const net = getNet(network); const bar = revealPayment + amount; let amt = 0; for (const utxo of utxos) { const hexy = (utxo.tx.hex) ? utxo.tx.hex : utxo.tx; const script = btc.RawTx.decode(hex.decode(hexy)); if (amt < bar && utxo.status.confirmed) { const txFromUtxo = btc.Transaction.fromRaw(hex.decode(hexy), { allowUnknowInput: true, allowUnknowOutput: true, allowUnknownOutputs: true, allowUnknownInputs: true }); const outputToSpend = txFromUtxo.getOutput(utxo.vout); if (!outputToSpend || !outputToSpend.script) throw new Error('no script passed ?'); const spendScr = btc.OutScript.decode(outputToSpend.script); //const addr = getAddressFromOutScript(output.script) if (spendScr.type === 'sh') { let p2shObj; // p2tr cannont be wrapped in p2sh !!! for (let i = 0; i < 10; i++) { try { if (i === 0) { p2shObj = btc.p2sh(btc.p2wpkh(hex.decode(paymentPublicKey)), net); } else if (i === 1) { p2shObj = btc.p2sh(btc.p2wsh(btc.p2wpkh(hex.decode(paymentPublicKey))), net); } else if (i === 2) { p2shObj = btc.p2sh(btc.p2wsh(btc.p2pkh(hex.decode(paymentPublicKey)), net)); } else if (i === 3) { p2shObj = btc.p2sh(btc.p2ms(1, [hex.decode(paymentPublicKey)]), net); } else if (i === 4) { p2shObj = btc.p2sh(btc.p2pkh(hex.decode(paymentPublicKey)), net); } else if (i === 5) { p2shObj = btc.p2sh(btc.p2sh(btc.p2pkh(hex.decode(paymentPublicKey)), net)); } else if (i === 6) { p2shObj = btc.p2sh(btc.p2sh(btc.p2wpkh(hex.decode(paymentPublicKey)), net)); } if (i < 3) { const nextI = redeemAndWitnessScriptAddInput(utxo, p2shObj, hexy); transaction.addInput(nextI); } else { const nextI = redeemScriptAddInput(utxo, p2shObj, hexy); transaction.addInput(nextI); } //('Tx type: ' + i + ' --> input added') break; } catch (err) { console.log('Error: not tx type: ' + i); } } } else if (spendScr.type === 'wpkh') { const spendAddr = getAddressFromOutScript(network, outputToSpend.script); //console.log('spendAddr: ' + spendAddr) const nextI = Object.assign(Object.assign({ txid: hex.decode(utxo.txid), index: utxo.vout }, outputToSpend), { witnessUtxo: { script: outputToSpend.script, amount: BigInt(utxo.value), } }); try { transaction.addInput(nextI); } catch (err) { // try next input console.log(err); } } else if (spendScr.type === 'wsh') { //const p2shObj = btc.p2wsh(btc.p2wpkh(hex.decode(paymentPublicKey), net)) let witnessUtxo = { script: script.outputs[utxo.vout].script, amount: BigInt(utxo.value) }; if (feeCalc) { witnessUtxo = { amount: BigInt(utxo.value), script: btc.p2wpkh(secp.getPublicKey(privKey, true)).script, }; } const nextI = { txid: hex.decode(utxo.txid), index: utxo.vout, nonWitnessUtxo: hexy, witnessUtxo }; try { transaction.addInput(nextI); } catch (err) { // try next input console.log(err); } } else if (spendScr.type === 'pkh') { //const p2shObj = btc.p2pkh(hex.decode(paymentPublicKey), net) const nextI = { txid: hex.decode(utxo.txid), index: utxo.vout, nonWitnessUtxo: hexy, //witnessUtxo }; try { transaction.addInput(nextI); } catch (err) { // try next input console.log(err); } } else { //const p2shObj = btc.p2wpkh(hex.decode(paymentPublicKey), net) const nextI = { txid: hex.decode(utxo.txid), index: utxo.vout, nonWitnessUtxo: hexy, //witnessUtxo }; try { transaction.addInput(nextI); } catch (err) { // try next input console.log(err); } } amt += utxo.value; } } } /** * getAddressFromOutScript converts a script to an address * @param network:string * @param script: Uint8Array * @returns address as string */ export function getAddressFromOutScript(network, script) { const net = getNet(network); const outputScript = btc.OutScript.decode(script); if (outputScript.type === 'pk' || outputScript.type === 'tr') { return btc.Address(net).encode({ type: outputScript.type, pubkey: outputScript.pubkey, }); } if (outputScript.type === 'ms' || outputScript.type === 'tr_ms') { return btc.Address(net).encode({ type: outputScript.type, pubkeys: outputScript.pubkeys, m: outputScript.m, }); } if (outputScript.type === 'tr_ns') { return btc.Address(net).encode({ type: outputScript.type, pubkeys: outputScript.pubkeys, }); } if (outputScript.type === 'unknown') { return btc.Address(net).encode({ type: outputScript.type, script, }); } return btc.Address(net).encode({ type: outputScript.type, hash: outputScript.hash, }); } function redeemScriptAddInput(utxo, p2shObj, hexy) { return { txid: hex.decode(utxo.txid), index: utxo.vout, nonWitnessUtxo: hexy, redeemScript: p2shObj.redeemScript }; } function redeemAndWitnessScriptAddInput(utxo, p2shObj, hexy) { return { txid: hex.decode(utxo.txid), index: utxo.vout, witnessUtxo: { script: p2shObj.script, amount: BigInt(utxo.value), }, redeemScript: p2shObj.redeemScript, }; } function isUTXOConfirmed(utxo) { return utxo.tx.confirmations >= 3; } ; export function inputAmt(tx) { var _a; let amt = 0; for (let idx = 0; idx < tx.inputsLength; idx++) { const inp = tx.getInput(idx); if (inp.witnessUtxo) amt += Number((_a = tx.getInput(idx).witnessUtxo) === null || _a === void 0 ? void 0 : _a.amount); else if (inp.nonWitnessUtxo) amt += Number(inp.nonWitnessUtxo.outputs[inp.index].amount); } return amt; } /** * * @param pubkey * @returns */ export function toXOnly(pubkey) { return hex.encode(hex.decode(pubkey).subarray(1, 33)); } /** * * @param network * @param sbtcWalletPublicKey * @returns */ export function getPegWalletAddressFromPublicKey(network, sbtcWalletPublicKey) { if (!sbtcWalletPublicKey) return; let net = getNet(network); //if (network === 'development' || network === 'simnet') { // net = { bech32: 'bcrt', pubKeyHash: 0x6f, scriptHash: 0xc4, wif: 0 } //} const fullPK = hex.decode(sbtcWalletPublicKey); let xOnlyKey = fullPK; if (fullPK.length === 33) { xOnlyKey = fullPK.subarray(1); } //const addr = btc.Address(net).encode({type: 'tr', pubkey: xOnlyKey}) const trObj = btc.p2tr(xOnlyKey, undefined, net); return trObj.address; }