import { Transaction , Address , bitcoin , BigNumber , SwapParams , SwapProvider } from '@liquality/types' ; import { Provider } from '@liquality/provider' ; import { calculateFee , decodeRawTransaction , normalizeTransactionObject , witnessStackToScriptWitness , getPubKeyHash , validateAddress } from '@liquality/bitcoin-utils' ; import { addressToString , validateValue , validateSecret , validateSecretHash , validateSecretAndHash , validateExpiration } from '@liquality/utils' ; import { BitcoinNetwork } from '@liquality/bitcoin-networks' ; import { Psbt , script as bScript , payments } from 'bitcoinjs-lib' ; interface BitcoinSwapProviderOptions { network : BitcoinNetwork ; mode ? : bitcoin . SwapMode ; } export default class BitcoinSwapProvider extends Provider implements Partial < SwapProvider > { _network : BitcoinNetwork ; _mode : bitcoin . SwapMode ; constructor ( options ) { super ( ) ; const { network , mode = bitcoin . SwapMode . P2WSH } = options ; const swapModes = Object . values ( bitcoin . SwapMode ) ; if ( ! swapModes . includes ( mode ) ) { throw new Error ( `Mode must be one of ${ swapModes . join ( ',' ) }` ) ; } this . _network = network ; this . _mode = mode ; } validateSwapParams ( swapParams ) { validateValue ( swapParams . value ) ; validateAddress ( swapParams . recipientAddress , this . _network ) ; validateAddress ( swapParams . refundAddress , this . _network ) ; validateSecretHash ( swapParams . secretHash ) ; validateExpiration ( swapParams . expiration ) ; } getSwapOutput ( swapParams ) { this . validateSwapParams ( swapParams ) ; const secretHashBuff = Buffer . from ( swapParams . secretHash , 'hex' ) ; const recipientPubKeyHash = getPubKeyHash ( addressToString ( swapParams . recipientAddress ) , this . _network ) ; const refundPubKeyHash = getPubKeyHash ( addressToString ( swapParams . refundAddress ) , this . _network ) ; const OPS = bScript . OPS ; const script = bScript . compile ( [ OPS . OP_IF , OPS . OP_SIZE , bScript . number . encode ( 32 ) , OPS . OP_EQUALVERIFY , OPS . OP_SHA256 , secretHashBuff , OPS . OP_EQUALVERIFY , OPS . OP_DUP , OPS . OP_HASH160 , recipientPubKeyHash , OPS . OP_ELSE , bScript . number . encode ( swapParams . expiration ) , OPS . OP_CHECKLOCKTIMEVERIFY , OPS . OP_DROP , OPS . OP_DUP , OPS . OP_HASH160 , refundPubKeyHash , OPS . OP_ENDIF , OPS . OP_EQUALVERIFY , OPS . OP_CHECKSIG ] ) ; if ( ! [ 97 , 98 ] . includes ( Buffer . byteLength ( script ) ) ) { throw new Error ( 'Invalid swap script' ) ; } return script ; } getSwapInput ( sig , pubKey , isClaim , secret ? ) { const OPS = bScript . OPS ; const redeem = isClaim ? OPS . OP_TRUE : OPS . OP_FALSE ; const secretParams = isClaim ? [ Buffer . from ( secret , 'hex' ) ] : [ ] ; return bScript . compile ( [ sig , pubKey , ... secretParams , redeem ] ) ; } getSwapPaymentVariants ( swapOutput ) { const p2wsh = payments . p2wsh ( { redeem : { output : swapOutput , network : this . _network } , network : this . _network } ) ; const p2shSegwit = payments . p2sh ( { redeem : p2wsh , network : this . _network } ) ; const p2sh = payments . p2sh ( { redeem : { output : swapOutput , network : this . _network } , network : this . _network } ) ; return { [ bitcoin . SwapMode . P2WSH ] : p2wsh , [ bitcoin . SwapMode . P2SH_SEGWIT ] : p2shSegwit , [ bitcoin . SwapMode . P2SH ] : p2sh } ; } async initiateSwap ( swapParams , feePerByte ) { this . validateSwapParams ( swapParams ) ; const swapOutput = this . getSwapOutput ( swapParams ) ; const address = this . getSwapPaymentVariants ( swapOutput ) [ this . _mode ] . address ; return this . client . chain . sendTransaction ( { to : address , value : swapParams . value , fee : feePerByte } ) ; } async fundSwap ( ) { return null ; } async claimSwap ( swapParams , initiationTxHash , secret , feePerByte ) { this . validateSwapParams ( swapParams ) ; validateSecret ( secret ) ; validateSecretAndHash ( secret , swapParams . secretHash ) ; await this . verifyInitiateSwapTransaction ( swapParams , initiationTxHash ) ; return this . _redeemSwap ( swapParams , initiationTxHash , true , secret , feePerByte ) ; } async refundSwap ( swapParams , initiationTxHash , feePerByte ) { this . validateSwapParams ( swapParams ) ; await this . verifyInitiateSwapTransaction ( swapParams , initiationTxHash ) ; return this . _redeemSwap ( swapParams , initiationTxHash , false , undefined , feePerByte ) ; } async _redeemSwap ( swapParams , initiationTxHash , isClaim , secret , feePerByte ) { const address = isClaim ? swapParams . recipientAddress : swapParams . refundAddress ; const swapOutput = this . getSwapOutput ( swapParams ) ; return this . _redeemSwapOutput ( initiationTxHash , swapParams . value , addressToString ( address ) , swapOutput , swapParams . expiration , isClaim , secret , feePerByte ) ; } async _redeemSwapOutput ( initiationTxHash , value , address , swapOutput , expiration , isClaim , secret , _feePerByte ) { const network = this . _network ; const swapPaymentVariants = this . getSwapPaymentVariants ( swapOutput ) ; const initiationTxRaw = await this . getMethod ( 'getRawTransactionByHash' ) ( initiationTxHash ) ; const initiationTx = decodeRawTransaction ( initiationTxRaw , this . _network ) ; let swapVout ; let paymentVariantName ; let paymentVariant ; for ( const vout of initiationTx . vout ) { const paymentVariantEntry = Object . entries ( swapPaymentVariants ) . find ( ( [ , payment ] ) => payment . output . toString ( 'hex' ) === vout . scriptPubKey . hex ) ; const voutValue = new BigNumber ( vout . value ) . times ( 1e8 ) ; if ( paymentVariantEntry && voutValue . eq ( new BigNumber ( value ) ) ) { paymentVariantName = paymentVariantEntry [ 0 ] ; paymentVariant = paymentVariantEntry [ 1 ] ; swapVout = vout ; } } if ( ! swapVout ) { throw new Error ( 'Valid swap output not found' ) ; } const feePerByte = _feePerByte || ( await this . getMethod ( 'getFeePerByte' ) ( ) ) ; const txfee = calculateFee ( 1 , 1 , feePerByte ) ; const swapValue = new BigNumber ( swapVout . value ) . times ( 1e8 ) . toNumber ( ) ; if ( swapValue - txfee < 0 ) { throw new Error ( 'Transaction amount does not cover fee.' ) ; } const psbt = new Psbt ( { network } ) ; if ( ! isClaim ) { psbt . setLocktime ( expiration ) ; } const isSegwit = paymentVariantName === bitcoin . SwapMode . P2WSH || paymentVariantName === bitcoin . SwapMode . P2SH_SEGWIT ; const input = { hash : initiationTxHash , index : swapVout . n , sequence : 0 } ; if ( isSegwit ) { input . witnessUtxo = { script : paymentVariant . output , value : swapValue } ; input . witnessScript = swapPaymentVariants . p2wsh . redeem . output ; } else { input . nonWitnessUtxo = Buffer . from ( initiationTxRaw , 'hex' ) ; input . redeemScript = paymentVariant . redeem . output ; } const output = { address : address , value : swapValue - txfee } ; psbt . addInput ( input ) ; psbt . addOutput ( output ) ; const walletAddress = await this . getMethod ( 'getWalletAddress' ) ( address ) ; const signedPSBTHex = await this . getMethod ( 'signPSBT' ) ( psbt . toBase64 ( ) , [ { index : 0 , derivationPath : walletAddress . derivationPath } ] ) ; const signedPSBT = Psbt . fromBase64 ( signedPSBTHex , { network } ) ; const sig = signedPSBT . data . inputs [ 0 ] . partialSig [ 0 ] . signature ; const swapInput = this . getSwapInput ( sig , Buffer . from ( walletAddress . publicKey , 'hex' ) , isClaim , secret ) ; const paymentParams = { redeem : { output : swapOutput , input : swapInput , network } , network } ; const paymentWithInput = isSegwit ? payments . p2wsh ( paymentParams ) : payments . p2sh ( paymentParams ) ; const getFinalScripts = ( ) => { let finalScriptSig ; let finalScriptWitness ; if ( isSegwit ) { finalScriptWitness = witnessStackToScriptWitness ( paymentWithInput . witness ) ; } if ( paymentVariantName === bitcoin . SwapMode . P2SH_SEGWIT ) { const inputScript = bScript . compile ( [ swapPaymentVariants . p2shSegwit . redeem . output ] ) ; finalScriptSig = inputScript ; } else if ( paymentVariantName === bitcoin . SwapMode . P2SH ) { finalScriptSig = paymentWithInput . input ; } return { finalScriptSig , finalScriptWitness } ; } ; psbt . finalizeInput ( 0 , getFinalScripts ) ; const hex = psbt . extractTransaction ( ) . toHex ( ) ; await this . getMethod ( 'sendRawTransaction' ) ( hex ) ; return normalizeTransactionObject ( decodeRawTransaction ( hex , this . _network ) , txfee ) ; } extractSwapParams ( outputScript ) { const buffer = bScript . decompile ( Buffer . from ( outputScript , 'hex' ) ) as Buffer [ ] ; if ( buffer . length !== 20 ) throw new Error ( 'Invalid swap output script' ) ; const secretHash = buffer [ 5 ] . reverse ( ) . toString ( 'hex' ) ; const recipientPublicKey = buffer [ 9 ] . reverse ( ) . toString ( 'hex' ) ; const expiration = parseInt ( buffer [ 11 ] . reverse ( ) . toString ( 'hex' ) , 16 ) ; const refundPublicKey = buffer [ 16 ] . reverse ( ) . toString ( 'hex' ) ; return { recipientPublicKey , refundPublicKey , secretHash , expiration } ; } /**
     * Only to be used for situations where transaction is trusted. e.g to bump fee
     * DO NOT USE THIS TO VERIFY THE REDEEM
     */ async UNSAFE_isSwapRedeemTransaction ( transaction ) { if ( transaction . _raw . vin . length === 1 && transaction . _raw . vout . length === 1 ) { const swapInput = transaction . _raw . vin [ 0 ] ; const inputScript = this . getInputScript ( swapInput ) ; const initiationTransaction = await this . getMethod ( 'getTransactionByHash' ) ( transaction . _raw . vin [ 0 ] . txid ) ; const scriptType = initiationTransaction . _raw . vout [ transaction . _raw . vin [ 0 ] . vout ] . scriptPubKey . type ; if ( [ 'scripthash' , 'witness_v0_scripthash' ] . includes ( scriptType ) && [ 4 , 5 ] . includes ( inputScript . length ) ) return true ; } return false ; } async updateTransactionFee ( tx , newFeePerByte ) { const txHash = typeof tx === 'string' ? tx : tx . hash ; const transaction = await this . getMethod ( 'getTransactionByHash' ) ( txHash ) ; if ( await this . UNSAFE_isSwapRedeemTransaction ( transaction ) ) { const swapInput = transaction . _raw . vin [ 0 ] ; const inputScript = this . getInputScript ( swapInput ) ; const initiationTxHash = swapInput . txid ; const initiationTx = await this . getMethod ( 'getTransactionByHash' ) ( initiationTxHash ) ; const swapOutput = initiationTx . _raw . vout [ swapInput . vout ] ; const value = new BigNumber ( swapOutput . value ) . times ( 1e8 ) ; const address = transaction . _raw . vout [ 0 ] . scriptPubKey . addresses [ 0 ] ; const isClaim = inputScript . length === 5 ; const secret = isClaim ? inputScript [ 2 ] : undefined ; const outputScript = isClaim ? inputScript [ 4 ] : inputScript [ 3 ] ; const { expiration } = this . extractSwapParams ( outputScript ) ; return this . _redeemSwapOutput ( initiationTxHash , value , address , Buffer . from ( outputScript , 'hex' ) , expiration , isClaim , secret , newFeePerByte ) ; } return this . getMethod ( 'updateTransactionFee' ) ( tx , newFeePerByte ) ; } getInputScript ( vin ) { const inputScript = vin . txinwitness ? vin . txinwitness : bScript . decompile ( Buffer . from ( vin . scriptSig . hex , 'hex' ) ) . map ( ( b ) => ( Buffer . isBuffer ( b ) ? b . toString ( 'hex' ) : b ) ) ; return inputScript as string [ ] ; } doesTransactionMatchRedeem ( initiationTxHash , tx , isRefund ) { const swapInput = tx . _raw . vin . find ( ( vin ) => vin . txid === initiationTxHash ) ; if ( ! swapInput ) return false ; const inputScript = this . getInputScript ( swapInput ) ; if ( ! inputScript ) return false ; if ( isRefund ) { if ( inputScript . length !== 4 ) return false ; } else { if ( inputScript . length !== 5 ) return false ; } return true ; } doesTransactionMatchInitiation ( swapParams , transaction ) { const swapOutput = this . getSwapOutput ( swapParams ) ; const swapPaymentVariants = this . getSwapPaymentVariants ( swapOutput ) ; const vout = transaction . _raw . vout . find ( ( vout ) => Object . values ( swapPaymentVariants ) . find ( ( payment ) => payment . output . toString ( 'hex' ) === vout . scriptPubKey . hex && new BigNumber ( vout . value ) . times ( 1e8 ) . eq ( new BigNumber ( swapParams . value ) ) ) ) ; return Boolean ( vout ) ; } async verifyInitiateSwapTransaction ( swapParams , initiationTxHash ) { this . validateSwapParams ( swapParams ) ; const initiationTransaction = await this . getMethod ( 'getTransactionByHash' ) ( initiationTxHash ) ; return this . doesTransactionMatchInitiation ( swapParams , initiationTransaction ) ; } async findSwapTransaction ( swapParams , blockNumber , predicate ) { const block = await this . getMethod ( 'getBlockByNumber' ) ( blockNumber , true ) ; const swapTransaction = block . transactions . find ( predicate ) ; return swapTransaction ; } async findInitiateSwapTransaction ( swapParams , blockNumber ) { this . validateSwapParams ( swapParams ) ; return this . getMethod ( 'findSwapTransaction' , false ) ( swapParams , blockNumber , ( tx ) => this . doesTransactionMatchInitiation ( swapParams , tx ) ) ; } async findClaimSwapTransaction ( swapParams , initiationTxHash , blockNumber ) { this . validateSwapParams ( swapParams ) ; const claimSwapTransaction = await this . getMethod ( 'findSwapTransaction' , false ) ( swapParams , blockNumber , ( tx ) => this . doesTransactionMatchRedeem ( initiationTxHash , tx , false ) ) ; if ( claimSwapTransaction ) { const swapInput = claimSwapTransaction . _raw . vin . find ( ( vin ) => vin . txid === initiationTxHash ) ; if ( ! swapInput ) { throw new Error ( 'Claim input missing' ) ; } const inputScript = this . getInputScript ( swapInput ) ; const secret = inputScript [ 2 ] as string ; validateSecretAndHash ( secret , swapParams . secretHash ) ; return { ... claimSwapTransaction , secret } ; } } async findRefundSwapTransaction ( swapParams , initiationTxHash , blockNumber ) { this . validateSwapParams ( swapParams ) ; const refundSwapTransaction = await this . getMethod ( 'findSwapTransaction' , false ) ( swapParams , blockNumber , ( tx ) => this . doesTransactionMatchRedeem ( initiationTxHash , tx , true ) ) ; return refundSwapTransaction ; } async findFundSwapTransaction ( ) { return null ; } }