sbtc-bridge-lib
Version:
Library for sBTC Bridge web client and API apps
183 lines (170 loc) • 8.46 kB
text/typescript
import * as btc from '@scure/btc-signer';
import * as secp from '@noble/secp256k1';
import * as P from 'micro-packed';
import { hex } from '@scure/base';
import type { KeySet, BridgeTransactionType, UTXO, WithdrawPayloadUIType } from './types/sbtc_types.js'
import { buildWithdrawPayloadOpDrop, toStorable } from './payload_utils.js'
import { buildWithdrawPayload, amountToBigUint64, bigUint64ToAmount } from './payload_utils.js'
import { addInputs, getNet, getPegWalletAddressFromPublicKey, inputAmt, toXOnly } 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:string, sbtcWalletPublicKey:string, uiPayload:WithdrawPayloadUIType, utxos:Array<UTXO>, btcFeeRates:any) {
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:string, sbtcWalletPublicKey:string, uiPayload:WithdrawPayloadUIType, utxos:Array<UTXO>, btcFeeRates:any, originator:string) {
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:string, opDrop:boolean, utxos:Array<UTXO>, amount:number, feeInfo:{ low_fee_per_kb:number, medium_fee_per_kb:number, high_fee_per_kb:number }, sbtcWalletAddress:string, changeAddress:string, paymentPublicKey:string, data:Uint8Array|undefined) {
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:any) {
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:string, sbtcWalletPublicKey:string, uiPayload:WithdrawPayloadUIType, originator:string):BridgeTransactionType {
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:BridgeTransactionType = {
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:string, uiPayload:WithdrawPayloadUIType, originator:string):BridgeTransactionType {
const req:BridgeTransactionType = {
network,
originator,
uiPayload,
status: 1,
mode: 'op_return',
requestType: 'withdrawal',
created: new Date().getTime(),
updated: new Date().getTime()
}
return req;
}
function buildData(network:string, amount:number, signature:string, opDrop:boolean):string {
if (opDrop) return buildWithdrawPayloadOpDrop(network, amount, signature)
return buildWithdrawPayload(network, amount, signature)
}