sbtc-bridge-lib
Version:
Library for sBTC Bridge web client and API apps
181 lines (180 loc) • 8.52 kB
JavaScript
import * as btc from '@scure/btc-signer';
import * as P from 'micro-packed';
import { hex } from '@scure/base';
import { buildWithdrawPayloadOpDrop, toStorable } from './payload_utils.js';
import { buildWithdrawPayload } from './payload_utils.js';
import { addInputs, getNet, getPegWalletAddressFromPublicKey, inputAmt } from './wallet_utils.js';
const concat = P.concatBytes;
const privKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
export const fullfillmentFee = 2000;
export const revealPayment = 10001;
export const dust = 500;
/**
*
* @param network
* @param uiPayload
* @param utxos:Array<UTXO>
* @param btcFeeRates
* @returns Transaction from @scure/btc-signer
*/
export function buildWithdrawTransaction(network, sbtcWalletPublicKey, uiPayload, utxos, btcFeeRates) {
if (!uiPayload.signature)
throw new Error('Signature of output 2 scriptPubKey is required');
const net = getNet(network);
const sbtcWalletAddress = getPegWalletAddressFromPublicKey(network, sbtcWalletPublicKey);
const data = buildData(network, uiPayload.amountSats, uiPayload.signature, false);
const txFees = calculateWithdrawFees(network, false, utxos, uiPayload.amountSats, btcFeeRates, sbtcWalletAddress, uiPayload.bitcoinAddress, uiPayload.paymentPublicKey, hex.decode(data));
const tx = new btc.Transaction({ allowUnknowOutput: true, allowUnknownInputs: true, allowUnknownOutputs: true });
addInputs(network, uiPayload.amountSats, 0, tx, false, utxos, uiPayload.paymentPublicKey);
tx.addOutput({ script: btc.Script.encode(['RETURN', hex.decode(data)]), amount: BigInt(0) });
const change = inputAmt(tx) - (fullfillmentFee + dust + txFees[1]);
tx.addOutputAddress(uiPayload.bitcoinAddress, BigInt(dust), net);
tx.addOutputAddress(sbtcWalletAddress, BigInt(fullfillmentFee), net);
if (change > 0)
tx.addOutputAddress(uiPayload.bitcoinAddress, BigInt(change), net);
return tx;
}
/**
*
* @param network
* @param uiPayload
* @param utxos:Array<UTXO>
* @param btcFeeRates
* @param originator
* @returns
*/
export function buildWithdrawTransactionOpDrop(network, sbtcWalletPublicKey, uiPayload, utxos, btcFeeRates, originator) {
if (!uiPayload.signature)
throw new Error('Signature of output 2 scriptPubKey is required');
const net = getNet(network);
const sbtcWalletAddress = getPegWalletAddressFromPublicKey(network, sbtcWalletPublicKey);
const txFees = calculateWithdrawFees(network, true, utxos, uiPayload.amountSats, btcFeeRates, sbtcWalletAddress, uiPayload.bitcoinAddress, uiPayload.paymentPublicKey, undefined);
const tx = new btc.Transaction({ allowUnknowOutput: true, allowUnknownInputs: true, allowUnknownOutputs: true });
addInputs(network, uiPayload.amountSats, revealPayment, tx, false, utxos, uiPayload.paymentPublicKey);
const csvScript = getBridgeWithdrawOpDrop(network, sbtcWalletPublicKey, uiPayload, originator);
//(network, data, sbtcWalletAddress, uiPayload.bitcoinAddress);
if (!csvScript)
throw new Error('script required!');
tx.addOutput({ script: csvScript.commitTxScript.script, amount: BigInt(0) });
tx.addOutputAddress(uiPayload.bitcoinAddress, BigInt(dust), net);
tx.addOutputAddress(sbtcWalletAddress, BigInt(fullfillmentFee), net);
const change = inputAmt(tx) - (fullfillmentFee + dust + txFees[1]);
if (change > 0)
tx.addOutputAddress(uiPayload.bitcoinAddress, BigInt(change), net);
return tx;
}
function calculateWithdrawFees(network, opDrop, utxos, amount, feeInfo, sbtcWalletAddress, changeAddress, paymentPublicKey, data) {
try {
let vsize = 0;
const net = getNet(network);
const tx = new btc.Transaction({ allowUnknowOutput: true, allowUnknownInputs: true, allowUnknownOutputs: true });
addInputs(network, amount, revealPayment, tx, true, utxos, paymentPublicKey);
if (!opDrop) {
if (data)
tx.addOutput({ script: btc.Script.encode(['RETURN', data]), amount: BigInt(0) });
tx.addOutputAddress(sbtcWalletAddress, BigInt(dust), net);
}
else {
tx.addOutput({ script: sbtcWalletAddress, amount: BigInt(dust) });
}
const change = inputAmt(tx) - (dust);
if (change > 0)
tx.addOutputAddress(changeAddress, BigInt(change), net);
//tx.sign(privKey);
//tx.finalize();
vsize = tx.vsize;
const fees = [
Math.floor(vsize * feeInfo['low_fee_per_kb'] / 1024),
Math.floor(vsize * feeInfo['medium_fee_per_kb'] / 1024),
Math.floor(vsize * feeInfo['high_fee_per_kb'] / 1024),
];
return fees;
}
catch (err) {
return [850, 1000, 1150];
}
}
/**
export function getWithdrawScript (network:string, data:Uint8Array, sbtcWalletAddress:string, fromBtcAddress:string):{type:string, script:Uint8Array} {
const net = getNet(network);
const addrScript = btc.Address(net).decode(sbtcWalletAddress)
if (addrScript.type === 'wpkh') {
return {
type: 'wsh',
script: btc.Script.encode([data, 'DROP', btc.p2wpkh(addrScript.hash).script])
}
} else if (addrScript.type === 'tr') {
return {
type: 'tr',
//script: btc.Script.encode([data, 'DROP', btc.OutScript.encode(btc.Address(net).decode(this.fromBtcAddress)), 'CHECKSIG'])
//script: btc.Script.encode([data, 'DROP', 'IF', 144, 'CHECKSEQUENCEVERIFY', 'DROP', btc.OutScript.encode(btc.Address(net).decode(this.fromBtcAddress)), 'CHECKSIG', 'ELSE', 'DUP', 'HASH160', sbtcWalletUint8, 'EQUALVERIFY', 'CHECKSIG', 'ENDIF'])
//script: btc.Script.encode([data, 'DROP', btc.p2tr(hex.decode(pubkey2)).script])
script: btc.Script.encode([data, 'DROP', btc.p2tr(addrScript.pubkey).script])
}
} else {
const asmScript = btc.Script.encode([data, 'DROP',
'IF',
btc.OutScript.encode(btc.Address(net).decode(sbtcWalletAddress)),
'ELSE',
144, 'CHECKSEQUENCEVERIFY', 'DROP',
btc.OutScript.encode(btc.Address(net).decode(fromBtcAddress)),
'CHECKSIG',
'ENDIF'
])
return {
type: 'tr',
//script: btc.Script.encode([data, 'DROP', btc.OutScript.encode(btc.Address(net).decode(this.fromBtcAddress)), 'CHECKSIG'])
//script: btc.Script.encode([data, 'DROP', 'IF', 144, 'CHECKSEQUENCEVERIFY', 'DROP', btc.OutScript.encode(btc.Address(net).decode(this.fromBtcAddress)), 'CHECKSIG', 'ELSE', 'DUP', 'HASH160', sbtcWalletUint8, 'EQUALVERIFY', 'CHECKSIG', 'ENDIF'])
//script: btc.Script.encode([data, 'DROP', btc.p2tr(hex.decode(pubkey2)).script])
script: btc.p2tr(asmScript).script
}
}
}
*/
export function getBridgeWithdrawOpDrop(network, sbtcWalletPublicKey, uiPayload, originator) {
const data = buildData(network, uiPayload.amountSats, uiPayload.signature, true);
const net = getNet(network);
let pk1U = hex.decode(sbtcWalletPublicKey);
let pk2U = hex.decode(uiPayload.reclaimPublicKey);
if (pk1U.length === 33)
pk1U = pk1U.subarray(1);
if (pk2U.length === 33)
pk2U = pk2U.subarray(1);
const scripts = [
{ script: btc.Script.encode([hex.decode(data), 'DROP', pk1U, 'CHECKSIG']) },
{ script: btc.Script.encode(['IF', 144, 'CHECKSEQUENCEVERIFY', 'DROP', pk2U, 'CHECKSIG', 'ENDIF']) }
];
const script = btc.p2tr(btc.TAPROOT_UNSPENDABLE_KEY, scripts, net, true);
// convert unit8 arrays to hex strings for transportation.
const commitTxScript = toStorable(script);
const req = {
network,
originator,
commitTxScript,
uiPayload,
status: 1,
mode: 'op_drop',
requestType: 'withdrawal',
created: new Date().getTime(),
updated: new Date().getTime()
};
return req;
}
export function getBridgeWithdraw(network, uiPayload, originator) {
const req = {
network,
originator,
uiPayload,
status: 1,
mode: 'op_return',
requestType: 'withdrawal',
created: new Date().getTime(),
updated: new Date().getTime()
};
return req;
}
function buildData(network, amount, signature, opDrop) {
if (opDrop)
return buildWithdrawPayloadOpDrop(network, amount, signature);
return buildWithdrawPayload(network, amount, signature);
}