Skip to content

Commit

Permalink
Merge pull request #576 from rsksmart/refund_scripts/refund-user-pegout
Browse files Browse the repository at this point in the history
Refund user pegout
  • Loading branch information
Luisfc68 authored Nov 28, 2024
2 parents d7ed917 + f166d53 commit 95e17bd
Show file tree
Hide file tree
Showing 34 changed files with 361 additions and 106 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ utils: download
mkdir -p utils
CGO_ENABLED=0 go build -v -o ./utils/update_provider_url ./cmd/utils/update_provider_url/update_provider_url.go
CGO_ENABLED=0 go build -v -o ./utils/register_pegin ./cmd/utils/register_pegin/register_pegin.go
CGO_ENABLED=0 go build -v -o ./utils/refund_user_pegout ./cmd/utils/refund_user_pegout/refund_user_pegout.go

utils-docker:
rm -rf utils
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ with `go run` or build them with `make utils`. You can run the scripts with the
- **update_provider_url**: updates the URL of a liquidity provider provided when the discovery function of the Liquidity Bridge Contract is executed.
- **register_pegin**: register a PegIn transaction within the Liquidity Bridge Contract. Most times, this script is only required to execute refunds
on special cases. This script requires an input file whose structure can be found [the input-example.json](cmd/utils/register_pegin/input-example.json) file.
- **refund_user_pegout**: executes a refund for a user's peg-out operation through the Liquidity Bridge Contract. This is used when a peg-out operation needs to be refunded back to the user's RSK address. The script requires the quote hash of the operation to refund.

### More information
If you're looking forward to integrate with Flyover Protocol then you can check the [Flyover SDK repository](https://github.com/rsksmart/unified-bridges-sdk/tree/main/packages/flyover-sdk).
Expand Down
87 changes: 14 additions & 73 deletions cmd/utils/refund_user_pegout/refund_user_pegout.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,14 @@ import (
"context"
"flag"
"fmt"
"os"
"syscall"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/go-playground/validator/v10"
"github.com/rsksmart/liquidity-provider-server/cmd/utils/defaults"
"github.com/rsksmart/liquidity-provider-server/internal/adapters/dataproviders/rootstock"
"github.com/rsksmart/liquidity-provider-server/internal/adapters/dataproviders/rootstock/bindings"
"github.com/rsksmart/liquidity-provider-server/cmd/utils/scripts"
"github.com/rsksmart/liquidity-provider-server/internal/configuration/bootstrap"
"github.com/rsksmart/liquidity-provider-server/internal/configuration/bootstrap/wallet"
"github.com/rsksmart/liquidity-provider-server/internal/configuration/environment"
"github.com/rsksmart/liquidity-provider-server/internal/configuration/environment/secrets"
"github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain"
"golang.org/x/term"
)

Expand All @@ -36,46 +31,24 @@ type RefundUserPegOutScriptInput struct {
type PasswordReader = func(int) ([]byte, error)

func main() {
ctx := context.Background()

scriptInput := new(RefundUserPegOutScriptInput)
ReadRefundUserPegOutScriptInput(scriptInput)
env, err := ParseRefundUserPegOutScriptInput(scriptInput, term.ReadPassword)
if err != nil {
ExitWithError(2, "Error reading input", err)
}

rskClient, err := bootstrap.Rootstock(ctx, env.Rsk)
if err != nil {
ExitWithError(2, "Error connecting to RSK node", err)
}
rskWallet, err := GetWallet(ctx, env, rskClient)
env, err := ParseRefundUserPegOutScriptInput(flag.Parse, scriptInput, term.ReadPassword)
if err != nil {
ExitWithError(2, "Error accessing to wallet", err)
scripts.ExitWithError(2, "Error reading input", err)
}

err = ExecuteRefundUserPegOut(ctx, env, rskWallet, rskClient, common.HexToHash(scriptInput.QuoteHashBytes))
ctx := context.Background()
lbc, err := scripts.CreateLiquidityBridgeContract(ctx, bootstrap.Rootstock, env)
if err != nil {
ExitWithError(2, "Error on transaction execution", err)
scripts.ExitWithError(2, "Error accessing the Liquidity Bridge Contract", err)
}
}

func GetWallet(
ctx context.Context,
env environment.Environment,
rskClient *rootstock.RskClient,
) (rootstock.RskSignerWallet, error) {
secretLoader, err := secrets.GetSecretLoader(ctx, env)
txHash, err := ExecuteRefundUserPegOut(lbc, scriptInput.QuoteHashBytes)
if err != nil {
return nil, err
scripts.ExitWithError(2, "Error on transaction execution", err)
}
walletFactory, err := wallet.NewFactory(env, wallet.FactoryCreationArgs{
Ctx: ctx, Env: env, SecretLoader: secretLoader, RskClient: rskClient,
})
if err != nil {
return nil, err
}
return walletFactory.RskWallet()
fmt.Println("Refund user peg out executed successfully. Transaction hash: ", txHash)
}

func ReadRefundUserPegOutScriptInput(scriptInput *RefundUserPegOutScriptInput) {
Expand All @@ -92,9 +65,9 @@ func ReadRefundUserPegOutScriptInput(scriptInput *RefundUserPegOutScriptInput) {
flag.StringVar(&scriptInput.EncryptedJsonPasswordSecret, "password-secret", "", "Name of the secret storing the keystore password. Only required if the secret source is aws")
}

func ParseRefundUserPegOutScriptInput(scriptInput *RefundUserPegOutScriptInput, pwdReader PasswordReader) (environment.Environment, error) {
func ParseRefundUserPegOutScriptInput(parse scripts.ParseFunc, scriptInput *RefundUserPegOutScriptInput, pwdReader PasswordReader) (environment.Environment, error) {
var env environment.Environment
flag.Parse()
parse()
validate := validator.New(validator.WithRequiredStructEnabled())
err := validate.Struct(scriptInput)
if err != nil {
Expand Down Expand Up @@ -142,38 +115,6 @@ func ParseRefundUserPegOutScriptInput(scriptInput *RefundUserPegOutScriptInput,
return env, nil
}

func ExecuteRefundUserPegOut(
ctx context.Context,
env environment.Environment,
rskWallet rootstock.RskSignerWallet,
rskClient *rootstock.RskClient,
quoteHashBytes common.Hash,
) error {
lbc, err := bindings.NewLiquidityBridgeContract(common.HexToAddress(env.Rsk.LbcAddress), rskClient.Rpc())
if err != nil {
return err
}

opts := &bind.TransactOpts{From: rskWallet.Address(), Signer: rskWallet.Sign}
tx, err := lbc.RefundUserPegOut(opts, quoteHashBytes)
if err != nil {
return err
}

receipt, err := bind.WaitMined(ctx, rskClient.Rpc(), tx)
if err != nil {
return err
}

if receipt.Status == 1 {
fmt.Println("Refund user peg out executed successfully. Transaction hash: ", receipt.TxHash.Hex())
return nil
} else {
return fmt.Errorf("transaction %s failed", receipt.TxHash.Hex())
}
}

func ExitWithError(code int, message string, err error) {
fmt.Println(fmt.Sprintf("%s: %s", message, err.Error()))
os.Exit(code)
func ExecuteRefundUserPegOut(lbc blockchain.LiquidityBridgeContract, quoteHash string) (string, error) {
return lbc.RefundUserPegOut(quoteHash)
}
102 changes: 102 additions & 0 deletions cmd/utils/refund_user_pegout/refund_user_pegout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"flag"
"testing"

"github.com/rsksmart/liquidity-provider-server/test"
"github.com/rsksmart/liquidity-provider-server/test/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/term"
)

const (
testRskEndpoint = "http://localhost:4444"
testQuoteHash = "d93f58c82100a6cee4f19ac505c11d51b52cafe220f7f1944b70496f33d277fc"
testAwsLocalEndpoint = "http://localhost:4566"
testNetwork = "regtest"
testKeystoreFile = "./keystore.json"
)

func TestReadRefundUserPegOutScriptInput(t *testing.T) {
t.Run("should set flag values", func(t *testing.T) {
// Reset flags before test
flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)

scriptInput := new(RefundUserPegOutScriptInput)
ReadRefundUserPegOutScriptInput(scriptInput)

// Set test values
err := flag.CommandLine.Parse([]string{
"-network", testNetwork,
"-quote-hash", testQuoteHash,
"-rsk-endpoint", testRskEndpoint,
"-secret-src", "env",
"-keystore-file", testKeystoreFile,
})
require.NoError(t, err)

assert.Equal(t, testNetwork, scriptInput.Network)
assert.Equal(t, testQuoteHash, scriptInput.QuoteHashBytes)
assert.Equal(t, testRskEndpoint, scriptInput.RskEndpoint)
assert.Equal(t, "env", scriptInput.SecretSource)
assert.Equal(t, testKeystoreFile, scriptInput.KeystoreFile)
})
}

func TestParseRefundUserPegOutScriptInput(t *testing.T) {

parse := func() { // parse is a no-op function used as a placeholder in tests since the actual parsing
// functionality is not relevant for these test cases
}

t.Run("should validate required fields", func(t *testing.T) {
scriptInput := &RefundUserPegOutScriptInput{
Network: "",
QuoteHashBytes: "",
RskEndpoint: "",
SecretSource: "",
}

_, err := ParseRefundUserPegOutScriptInput(parse, scriptInput, term.ReadPassword)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid input")
})

t.Run("should parse valid input", func(t *testing.T) {
scriptInput := &RefundUserPegOutScriptInput{
Network: testNetwork,
QuoteHashBytes: testQuoteHash,
RskEndpoint: testRskEndpoint,
SecretSource: "aws",
AwsLocalEndpoint: testAwsLocalEndpoint,
}

env, err := ParseRefundUserPegOutScriptInput(parse, scriptInput, func(fd int) ([]byte, error) {
return []byte("password"), nil
})
require.NoError(t, err)
assert.Equal(t, testNetwork, env.LpsStage)
assert.Equal(t, testRskEndpoint, env.Rsk.Endpoint)
assert.Equal(t, "aws", env.SecretSource)
assert.Equal(t, testAwsLocalEndpoint, env.AwsLocalEndpoint)
})
}

func TestRefundUserPegOut(t *testing.T) {
t.Run("should execute refund user peg out successfully", func(t *testing.T) {
lbc := &mocks.LbcMock{}
expectedTxHash := test.AnyHash

// Setup mock expectations
lbc.On("RefundUserPegOut", testQuoteHash).Return(expectedTxHash, nil)

txHash, err := ExecuteRefundUserPegOut(lbc, testQuoteHash)
require.NoError(t, err)
assert.Equal(t, expectedTxHash, txHash)

// Verify all expectations were met
lbc.AssertExpectations(t)
})
}
4 changes: 3 additions & 1 deletion internal/adapters/dataproviders/rootstock/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package rootstock

import (
"context"
"math/big"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/rsksmart/liquidity-provider-server/internal/adapters/dataproviders/rootstock/bindings"
"math/big"
)

type RpcClientBinding interface {
Expand Down Expand Up @@ -70,6 +71,7 @@ type LbcBinding interface {
ProductFeePercentage(opts *bind.CallOpts) (*big.Int, error)
IsPegOutQuoteCompleted(opts *bind.CallOpts, quoteHash [32]byte) (bool, error)
UpdateProvider(opts *bind.TransactOpts, _name string, _url string) (*types.Transaction, error)
RefundUserPegOut(opts *bind.TransactOpts, quoteHash [32]byte) (*types.Transaction, error)
}

type LbcAdapter interface {
Expand Down
36 changes: 33 additions & 3 deletions internal/adapters/dataproviders/rootstock/lbc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import (
"encoding/hex"
"errors"
"fmt"
"math/big"
"strings"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
geth "github.com/ethereum/go-ethereum/core/types"
Expand All @@ -15,9 +19,6 @@ import (
"github.com/rsksmart/liquidity-provider-server/internal/entities/liquidity_provider"
"github.com/rsksmart/liquidity-provider-server/internal/entities/quote"
log "github.com/sirupsen/logrus"
"math/big"
"strings"
"time"
)

// registerPeginGasLimit Fixed gas limit for registerPegin function, should change only if the function does
Expand Down Expand Up @@ -612,6 +613,35 @@ func (lbc *liquidityBridgeContractImpl) UpdateProvider(name, url string) (string
return receipt.TxHash.String(), nil
}

func (lbc *liquidityBridgeContractImpl) RefundUserPegOut(quoteHash string) (string, error) {
// Validate the hash format
hashBytesSlice, err := hex.DecodeString(quoteHash)
if err != nil {
return "", fmt.Errorf("invalid quote hash format: %w", err)
}
if len(hashBytesSlice) != 32 {
return "", errors.New("quote hash must be 32 bytes long")
}

opts := &bind.TransactOpts{
From: lbc.signer.Address(),
Signer: lbc.signer.Sign,
}
receipt, err := awaitTx(lbc.client, "RefundUserPegOut", func() (*geth.Transaction, error) {
return lbc.contract.RefundUserPegOut(opts, common.HexToHash(quoteHash))
})

if err != nil {
return "", fmt.Errorf("refund user peg out error: %w", err)
} else if receipt == nil {
return "", errors.New("refund user peg out error: incomplete receipt")
} else if receipt.Status == 0 {
txHash := receipt.TxHash.String()
return txHash, fmt.Errorf("refund user peg out error: transaction reverted (%s)", txHash)
}
return receipt.TxHash.String(), nil
}

// parsePeginQuote parses a quote.PeginQuote into a bindings.QuotesPeginQuote. All BTC address fields support all address types
// except for FedBtcAddress which must be a P2SH address.
func parsePeginQuote(peginQuote quote.PeginQuote) (bindings.QuotesPeginQuote, error) {
Expand Down
Loading

0 comments on commit 95e17bd

Please sign in to comment.