Skip to content

Commit 25d118e

Browse files
Merge pull request #862 from rsksmart/feat/fly_2094/integrate_validate_pegout
feat/FLY-2094/Integrate validatePegout
2 parents bb2bd8a + 16a2b2b commit 25d118e

File tree

18 files changed

+3864
-230
lines changed

18 files changed

+3864
-230
lines changed

internal/adapters/dataproviders/bitcoin/derivative_wallet.go

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package bitcoin
22

33
import (
4+
"bytes"
45
"encoding/hex"
56
"errors"
67
"fmt"
@@ -191,26 +192,14 @@ func (wallet *DerivativeWallet) GetBalance() (*entities.Wei, error) {
191192
}
192193

193194
func (wallet *DerivativeWallet) SendWithOpReturn(address string, value *entities.Wei, opReturnContent []byte) (blockchain.BitcoinTransactionResult, error) {
194-
decodedAddress, err := btcutil.DecodeAddress(address, wallet.conn.NetworkParams)
195-
if err != nil {
196-
return blockchain.BitcoinTransactionResult{}, err
197-
}
198-
if err = EnsureLoadedBtcWallet(wallet.conn); err != nil {
199-
return blockchain.BitcoinTransactionResult{}, err
200-
}
201-
202-
satoshis, _ := value.ToSatoshi().Float64()
203-
output := map[btcutil.Address]btcutil.Amount{decodedAddress: btcutil.Amount(satoshis)}
204-
rawTx, err := wallet.conn.client.CreateRawTransaction(nil, output, nil)
205-
if err != nil {
195+
if err := EnsureLoadedBtcWallet(wallet.conn); err != nil {
206196
return blockchain.BitcoinTransactionResult{}, err
207197
}
208198

209-
opReturnScript, err := txscript.NullDataScript(opReturnContent)
199+
rawTx, err := wallet.buildRawTransactionWithOpReturn(address, value, opReturnContent)
210200
if err != nil {
211201
return blockchain.BitcoinTransactionResult{}, err
212202
}
213-
rawTx.AddTxOut(wire.NewTxOut(0, opReturnScript))
214203

215204
opts, err := wallet.buildFundRawTransactionOpts()
216205
if err != nil {
@@ -242,6 +231,21 @@ func (wallet *DerivativeWallet) SendWithOpReturn(address string, value *entities
242231
}, nil
243232
}
244233

234+
func (wallet *DerivativeWallet) CreateUnfundedTransactionWithOpReturn(address string, value *entities.Wei, opReturnContent []byte) ([]byte, error) {
235+
rawTx, err := wallet.buildRawTransactionWithOpReturn(address, value, opReturnContent)
236+
if err != nil {
237+
return nil, err
238+
}
239+
240+
var buf bytes.Buffer
241+
err = rawTx.Serialize(&buf)
242+
if err != nil {
243+
return nil, err
244+
}
245+
246+
return buf.Bytes(), nil
247+
}
248+
245249
func (wallet *DerivativeWallet) ImportAddress(address string) error {
246250
return errors.New("address importing is not supported in this type of wallet")
247251
}
@@ -327,3 +331,25 @@ func (wallet *DerivativeWallet) signFundedTransaction(fundedTx *btcjson.FundRawT
327331
}
328332
return signedTx, nil
329333
}
334+
335+
func (wallet *DerivativeWallet) buildRawTransactionWithOpReturn(address string, value *entities.Wei, opReturnContent []byte) (*wire.MsgTx, error) {
336+
decodedAddress, err := btcutil.DecodeAddress(address, wallet.conn.NetworkParams)
337+
if err != nil {
338+
return nil, err
339+
}
340+
341+
satoshis, _ := value.ToSatoshi().Float64()
342+
output := map[btcutil.Address]btcutil.Amount{decodedAddress: btcutil.Amount(satoshis)}
343+
rawTx, err := wallet.conn.client.CreateRawTransaction(nil, output, nil)
344+
if err != nil {
345+
return nil, err
346+
}
347+
348+
opReturnScript, err := txscript.NullDataScript(opReturnContent)
349+
if err != nil {
350+
return nil, err
351+
}
352+
rawTx.AddTxOut(wire.NewTxOut(0, opReturnScript))
353+
354+
return rawTx, nil
355+
}

internal/adapters/dataproviders/bitcoin/derivative_wallet_test.go

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package bitcoin_test
22

33
import (
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
189193
func 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+
}

internal/adapters/dataproviders/bitcoin/watchonly_wallet.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ func (wallet *WatchOnlyWallet) SendWithOpReturn(address string, value *entities.
6666
return blockchain.BitcoinTransactionResult{}, errors.New("cannot send from a watch-only wallet")
6767
}
6868

69+
func (wallet *WatchOnlyWallet) CreateUnfundedTransactionWithOpReturn(address string, value *entities.Wei, opReturnContent []byte) ([]byte, error) {
70+
return nil, errors.New("cannot create transactions from a watch-only wallet")
71+
}
72+
6973
func (wallet *WatchOnlyWallet) ImportAddress(address string) error {
7074
_, err := btcutil.DecodeAddress(address, wallet.conn.NetworkParams)
7175
if err != nil {

internal/adapters/dataproviders/bitcoin/watchonly_wallet_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,13 @@ func TestWatchOnlyWallet_SendWithOpReturn(t *testing.T) {
306306
require.Empty(t, result.Hash)
307307
require.Nil(t, result.Fee)
308308
}
309+
310+
func TestWatchOnlyWallet_CreateUnfundedTransactionWithOpReturn(t *testing.T) {
311+
client := &mocks.ClientAdapterMock{}
312+
client.On("GetWalletInfo").Return(&btcjson.GetWalletInfoResult{PrivateKeysEnabled: false}, nil).Once()
313+
wallet, err := bitcoin.NewWatchOnlyWallet(bitcoin.NewWalletConnection(&chaincfg.TestNet3Params, client, bitcoin.PeginWalletId))
314+
require.NoError(t, err)
315+
result, err := wallet.CreateUnfundedTransactionWithOpReturn("address", nil, nil)
316+
require.ErrorContains(t, err, "cannot create transactions from a watch-only wallet")
317+
require.Nil(t, result)
318+
}

internal/adapters/dataproviders/rootstock/bindings.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type PegoutBinding interface {
5353
IsQuoteCompleted(opts *bind.CallOpts, quoteHash [32]byte) (bool, error)
5454
RefundUserPegOut(opts *bind.TransactOpts, quoteHash [32]byte) (*types.Transaction, error)
5555
GetFeePercentage(opts *bind.CallOpts) (*big.Int, error)
56+
ValidatePegout(opts *bind.CallOpts, quoteHash [32]byte, btcTx []byte) (bindings.QuotesPegOutQuote, error)
5657
}
5758

5859
type PegoutContractAdapter interface {

0 commit comments

Comments
 (0)