sbtc-bridge-lib
Version:
Library for sBTC Bridge web client and API apps
509 lines (508 loc) • 20.2 kB
JavaScript
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;
}