11package bitcoin_test
22
33import (
4+ "bytes"
45 "cmp"
6+ "encoding/hex"
57 "encoding/json"
68 "errors"
79 "math/big"
@@ -22,6 +24,7 @@ import (
2224 "github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain"
2325 "github.com/rsksmart/liquidity-provider-server/internal/entities/utils"
2426 "github.com/rsksmart/liquidity-provider-server/test"
27+ "github.com/rsksmart/liquidity-provider-server/test/datasets"
2528 "github.com/rsksmart/liquidity-provider-server/test/mocks"
2629 "github.com/stretchr/testify/assert"
2730 "github.com/stretchr/testify/mock"
@@ -186,6 +189,7 @@ func testImportPubKeyAndRescan(t *testing.T, rskAccount *account.RskAccount, add
186189 client .AssertExpectations (t )
187190}
188191
192+ // nolint:funlen
189193func TestDerivativeWallet (t * testing.T ) {
190194 existingAddressInfo := new (btcjson.GetAddressInfoResult )
191195 nonExistingAddressInfo := new (btcjson.GetAddressInfoResult )
@@ -237,6 +241,21 @@ func TestDerivativeWallet(t *testing.T) {
237241 }
238242 })
239243 })
244+
245+ t .Run ("CreateUnfundedTransactionWithOpReturn" , func (t * testing.T ) {
246+ t .Run ("Success" , func (t * testing.T ) { testCreateUnfundedTransactionWithOpReturn (t , rskAccount , existingAddressInfo ) })
247+ t .Run ("Error handling" , func (t * testing.T ) {
248+ cases := derivativeWalletCreateUnfundedTxErrorSetups (rskAccount )
249+ for _ , testCase := range cases {
250+ client := & mocks.ClientAdapterMock {}
251+ client .On ("GetWalletInfo" ).Return (& btcjson.GetWalletInfoResult {WalletName : bitcoin .DerivativeWalletId , Scanning : btcjson.ScanningOrFalse {Value : false }}, nil ).Twice ()
252+ client .On ("GetAddressInfo" , btcAddress ).Return (existingAddressInfo , nil ).Once ()
253+ t .Run (testCase .description , func (t * testing.T ) {
254+ testCase .setup (t , client )
255+ })
256+ }
257+ })
258+ })
240259 t .Run ("Shutdown" , func (t * testing.T ) { testDerivativeWalletShutdown (t , rskAccount , existingAddressInfo ) })
241260}
242261
@@ -519,9 +538,6 @@ func derivativeWalletSendWithOpReturnErrorSetups(rskAccount *account.RskAccount)
519538 {
520539 description : "error parsing address" ,
521540 setup : func (t * testing.T , client * mocks.ClientAdapterMock ) {
522- // overwrite this expectation because if it fails to parse the address, it will not verify the wallet is loaded
523- client .EXPECT ().GetWalletInfo ().Unset ()
524- client .EXPECT ().GetWalletInfo ().Return (& btcjson.GetWalletInfoResult {WalletName : bitcoin .DerivativeWalletId , Scanning : btcjson.ScanningOrFalse {Value : false }}, nil ).Once ()
525541 wallet , err := bitcoin .NewDerivativeWallet (bitcoin .NewWalletConnection (& chaincfg .TestNet3Params , client , bitcoin .DerivativeWalletId ), rskAccount )
526542 require .NoError (t , err )
527543 result , err := wallet .SendWithOpReturn (test .AnyString , entities .NewWei (1 ), []byte {0xf1 })
@@ -718,3 +734,127 @@ func derivativeWalletEstimateTxFeesErrorSetups(rskAccount *account.RskAccount) [
718734 },
719735 }
720736}
737+
738+ // nolint:funlen
739+ func testCreateUnfundedTransactionWithOpReturn (t * testing.T , rskAccount * account.RskAccount , addressInfo * btcjson.GetAddressInfoResult ) {
740+ for _ , testCase := range datasets .UnfundedTxTestCases {
741+ t .Run (testCase .Description , func (t * testing.T ) {
742+ client := & mocks.ClientAdapterMock {}
743+
744+ // Convert satoshis to Wei
745+ value := entities .SatoshiToWei (uint64 (testCase .ValueSatoshis ))
746+ satoshis , _ := value .ToSatoshi ().Float64 ()
747+
748+ // Decode the payment-only transaction (what CreateRawTransaction returns)
749+ paymentOnlyTxBytes , err := hex .DecodeString (testCase .RawTxPaymentOnly )
750+ require .NoError (t , err )
751+ var paymentOnlyTx wire.MsgTx
752+ err = paymentOnlyTx .DeserializeNoWitness (bytes .NewReader (paymentOnlyTxBytes ))
753+ require .NoError (t , err )
754+
755+ // Decode the expected complete transaction (with OP_RETURN)
756+ expectedTxBytes , err := hex .DecodeString (testCase .ExpectedCompleteTx )
757+ require .NoError (t , err )
758+ var expectedTx wire.MsgTx
759+ err = expectedTx .DeserializeNoWitness (bytes .NewReader (expectedTxBytes ))
760+ require .NoError (t , err )
761+
762+ // Setup mocks
763+ decodedAddress , err := btcutil .DecodeAddress (testCase .Address , & chaincfg .TestNet3Params )
764+ require .NoError (t , err )
765+
766+ client .On ("GetWalletInfo" ).Return (& btcjson.GetWalletInfoResult {WalletName : bitcoin .DerivativeWalletId , Scanning : btcjson.ScanningOrFalse {Value : false }}, nil ).Once ()
767+ client .On ("GetAddressInfo" , btcAddress ).Return (addressInfo , nil ).Once ()
768+ client .On ("CreateRawTransaction" ,
769+ ([]btcjson.TransactionInput )(nil ),
770+ mock .MatchedBy (func (outputs map [btcutil.Address ]btcutil.Amount ) bool {
771+ // Verify the payment output parameters
772+ if len (outputs ) != 1 {
773+ return false
774+ }
775+ for k , v := range outputs {
776+ return k .String () == decodedAddress .String () && v == btcutil .Amount (satoshis )
777+ }
778+ return false
779+ }),
780+ (* int64 )(nil ),
781+ ).Return (& paymentOnlyTx , nil ).Once ()
782+
783+ // Create wallet and call the method
784+ wallet , err := bitcoin .NewDerivativeWallet (bitcoin .NewWalletConnection (& chaincfg .TestNet3Params , client , bitcoin .DerivativeWalletId ), rskAccount )
785+ require .NoError (t , err )
786+
787+ result , err := wallet .CreateUnfundedTransactionWithOpReturn (testCase .Address , value , testCase .OpReturnContent )
788+ require .NoError (t , err )
789+ require .NotEmpty (t , result )
790+
791+ // Decode the actual result
792+ var actualTx wire.MsgTx
793+ err = actualTx .DeserializeNoWitness (bytes .NewReader (result ))
794+ require .NoError (t , err )
795+
796+ // Verify transaction structure matches the expected Bitcoin transaction
797+ assert .Equal (t , expectedTx .Version , actualTx .Version , "Transaction version should match" )
798+ assert .Equal (t , expectedTx .LockTime , actualTx .LockTime , "Lock time should match" )
799+ assert .Empty (t , actualTx .TxIn , "Unfunded transaction should have no inputs" )
800+ assert .Len (t , actualTx .TxOut , 2 , "Transaction should have 2 outputs: payment + OP_RETURN" )
801+
802+ // Verify payment output
803+ assert .Equal (t , expectedTx .TxOut [0 ].Value , actualTx .TxOut [0 ].Value , "Payment output value should match" )
804+ assert .Equal (t , expectedTx .TxOut [0 ].PkScript , actualTx .TxOut [0 ].PkScript , "Payment output script should match" )
805+
806+ // Verify OP_RETURN output
807+ assert .Equal (t , int64 (0 ), actualTx .TxOut [1 ].Value , "OP_RETURN output value should be 0" )
808+ assert .Equal (t , expectedTx .TxOut [1 ].PkScript , actualTx .TxOut [1 ].PkScript , "OP_RETURN script should match" )
809+
810+ // Verify the OP_RETURN contains the expected data
811+ assert .True (t , bytes .Contains (actualTx .TxOut [1 ].PkScript , testCase .OpReturnContent ), "OP_RETURN should contain the expected data" )
812+
813+ // Verify the complete serialized transaction matches
814+ assert .Equal (t , hex .EncodeToString (expectedTxBytes ), hex .EncodeToString (result ), "Complete serialized transaction should match" )
815+
816+ client .AssertExpectations (t )
817+ })
818+ }
819+ }
820+
821+ func derivativeWalletCreateUnfundedTxErrorSetups (rskAccount * account.RskAccount ) []struct {
822+ description string
823+ setup func (t * testing.T , client * mocks.ClientAdapterMock )
824+ } {
825+ return []struct {
826+ description string
827+ setup func (t * testing.T , client * mocks.ClientAdapterMock )
828+ }{
829+ {
830+ description : "invalid address" ,
831+ setup : func (t * testing.T , client * mocks.ClientAdapterMock ) {
832+ // Decoding the address happens before any other calls
833+ // Unset GetWalletInfo since it won't be called twice
834+ client .EXPECT ().GetWalletInfo ().Unset ()
835+ client .EXPECT ().GetWalletInfo ().Return (& btcjson.GetWalletInfoResult {WalletName : bitcoin .DerivativeWalletId , Scanning : btcjson.ScanningOrFalse {Value : false }}, nil ).Once ()
836+ wallet , err := bitcoin .NewDerivativeWallet (bitcoin .NewWalletConnection (& chaincfg .TestNet3Params , client , bitcoin .DerivativeWalletId ), rskAccount )
837+ require .NoError (t , err )
838+ result , err := wallet .CreateUnfundedTransactionWithOpReturn ("invalid-address" , entities .NewWei (1 ), []byte {0x01 })
839+ require .ErrorContains (t , err , "decoded address is of unknown format" )
840+ require .Nil (t , result )
841+ client .AssertExpectations (t )
842+ },
843+ },
844+ {
845+ description : "error on CreateRawTransaction" ,
846+ setup : func (t * testing.T , client * mocks.ClientAdapterMock ) {
847+ // Unset previous expectation since we need different behavior
848+ client .EXPECT ().GetWalletInfo ().Unset ()
849+ client .EXPECT ().GetWalletInfo ().Return (& btcjson.GetWalletInfoResult {WalletName : bitcoin .DerivativeWalletId , Scanning : btcjson.ScanningOrFalse {Value : false }}, nil ).Once ()
850+ client .On ("CreateRawTransaction" , mock .Anything , mock .Anything , mock .Anything ).Return (nil , assert .AnError ).Once ()
851+ wallet , err := bitcoin .NewDerivativeWallet (bitcoin .NewWalletConnection (& chaincfg .TestNet3Params , client , bitcoin .DerivativeWalletId ), rskAccount )
852+ require .NoError (t , err )
853+ result , err := wallet .CreateUnfundedTransactionWithOpReturn (testnetAddress , entities .NewWei (1 ), []byte {0x01 })
854+ require .Error (t , err )
855+ require .Nil (t , result )
856+ client .AssertExpectations (t )
857+ },
858+ },
859+ }
860+ }
0 commit comments