sbtc-bridge-lib
Version:
Library for sBTC Bridge web client and API apps
526 lines (479 loc) • 19.3 kB
text/typescript
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 type { PayloadType } from './types/sbtc_types.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()
type KeySet = {
priv: Uint8Array,
ecdsaPub: Uint8Array,
schnorrPub: Uint8Array
}
const keySetForFeeCalculation: KeySet[] = []
keySetForFeeCalculation.push({
priv,
ecdsaPub: secp.getPublicKey(priv, true),
schnorrPub: secp.getPublicKey(priv, false)
})
export function parseDepositPayload(d1:Uint8Array):PayloadType {
const magicOp = getMagicAndOpCode(d1);
if (magicOp.magic) {
return parseDepositPayloadNoMagic(d1.subarray(2));
}
return parseDepositPayloadNoMagic(d1);
}
function parseDepositPayloadNoPrincipal(d1:Uint8Array):PayloadType {
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:Uint8Array):PayloadType {
//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:number, size:number):Uint8Array {
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:number, size:number) {
//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:ArrayBuffer) {
return [...new Uint8Array (buffer)]
.map (b => b.toString (16).padStart (2, "0"))
.join ("");
}
export function bigUint64ToAmount(buf:Uint8Array):number {
// 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:string, d0:string, bitcoinAddress:string, sigMode: 'rsv'|'vrs'):PayloadType {
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:string, d1:Uint8Array, bitcoinAddress:string, sigMode: 'rsv'|'vrs'):PayloadType {
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 enum PrincipalType {
STANDARD = '05',
CONTRACT = '06'
}
export function buildDepositPayload(network:string, stacksAddress:string):string {
const net = getNet(network);
return buildDepositPayloadInternal(net, 0, stacksAddress, false)
}
export function buildDepositPayloadOpDrop(network:string, stacksAddress:string, revealFee:number):string {
const net = getNet(network);
return buildDepositPayloadInternal(net, revealFee, stacksAddress, true)
}
function buildDepositPayloadInternal(net:any, amountSats:number, address:string, opDrop:boolean):string {
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:any;
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:string, amount:number, signature:string):string {
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:string, amount:number, signature:string):string {
const net = getNet(network);
return buildWithdrawPayloadInternal(net, amount, signature, true)
}
function buildWithdrawPayloadInternal(net:any, amount:number, signature:string, opDrop:boolean):string {
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:Array<any>) {
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:string, txHex:string):PayloadType {
const tx:btc.Transaction = btc.Transaction.fromRaw(hex.decode(txHex), {allowUnknowInput:true, allowUnknowOutput: true, allowUnknownOutputs: true, allowUnknownInputs: true})
const out0 = tx.getOutput(0);
const script0 = out0.script as Uint8Array
const spendScr = btc.OutScript.decode(script0);
let payload = {} as PayloadType;
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:string, tx:btc.Transaction):PayloadType {
const out0 = tx.getOutput(0)
let d1 = out0.script?.subarray(5) as Uint8Array // strip the op type and data length
let witnessData = getMagicAndOpCode(d1);
if (witnessData.opcode !== '3C' && witnessData.opcode !== '3E') {
d1 = out0.script?.subarray(2) as Uint8Array // strip the op type and data length
witnessData = getMagicAndOpCode(d1);
}
witnessData.txType = btc.OutScript.decode(out0.script as Uint8Array).type;
let innerPayload:PayloadType = {} as PayloadType;
if (witnessData.opcode === '3C') {
innerPayload = parseDepositPayload(d1);
innerPayload.sbtcWallet = getAddressFromOutScript(network, tx.getOutput(1).script as Uint8Array)
//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 as Uint8Array)
try {
innerPayload = parseWithdrawPayload(network, hex.encode(d1), recipient, 'vrs')
} catch (err:any) {
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 as Uint8Array)
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:string, amount:number, bitcoinAddress:string):string {
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:string, amount:number, bitcoinAddress:string):string {
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:string) {
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:Uint8Array, signature:string, sigMode:'vrs'|'rsv'|undefined) {
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:Uint8Array, signature:string) {
const pubkey = getPubkeySignature(messageHash, signature, 'vrs')
return getStacksAddressFromPubkey(pubkey);
}
export function getStacksAddressFromSignatureRsv(messageHash:Uint8Array, signature:string) {
const pubkey = getPubkeySignature(messageHash, signature, 'rsv')
return getStacksAddressFromPubkey(pubkey);
}
export function getStacksAddressFromPubkey(pubkey:Uint8Array) {
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: Uint8Array,
addressVersion: StacksNetworkVersion = StacksNetworkVersion.mainnetP2PKH): string {
return c32address(addressVersion, hex.encode(hash160(publicKey)));
}
function hash160(input: Uint8Array): Uint8Array {
const sha = sha256(input);
return ripemd160(sha);
}
export function getMagicAndOpCode(d1: Uint8Array): {magic?:string; opcode:string; txType? :string; } {
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()
}
}
enum StacksNetworkVersion {
mainnetP2PKH = 22, // 'P' MainnetSingleSig
mainnetP2SH = 20, // 'M' MainnetMultiSig
testnetP2PKH = 26, // 'T' TestnetSingleSig
testnetP2SH = 21, // 'N' TestnetMultiSig
}
/**
* 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:any) {
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:any) {
//const copied = JSON.parse(JSON.stringify(script));
return codifyScript(script, false)
}
function codifyScript(script:any, asString:boolean) {
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:any, asString:boolean) {
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:unknown, asString:boolean) {
if (!arg) return;
if (typeof arg === 'string') {
return hex.decode(arg)
} else {
return hex.encode(arg as Uint8Array)
}
}
function codifyLeaves(leaves:any, asString:boolean) {
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;
}