From 28506bcd177b63cf9ef2e7564cc2a268dfcab746 Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Mon, 23 Sep 2024 09:19:00 +0200 Subject: [PATCH] chore_: improvements of the sending route generated by the router process This commit simplifies the sending process of the best route suggested by the router. It also makes the sending process the same for accounts (key pairs) migrated to a keycard and those stored locally in local keystore files. Deprecated endpoints: - `CreateMultiTransaction` - `ProceedWithTransactionsSignatures` Deprecated signal: - `wallet.sign.transactions` New endpoints: - `BuildTransactionsFromRoute` - `SendRouterTransactionsWithSignatures` The flow for sending the best router suggested by the router: - call `BuildTransactionsFromRoute` - wait for the `wallet.router.sign-transactions` signal - sign received hashes using `SignMessage` call or sign on keycard - call `SendRouterTransactionsWithSignatures` with the signatures of signed hashes from the previous step - `wallet.router.transactions-sent` signal will be sent after transactions are sent or if an error occurs New signals: - `wallet.router.sending-transactions-started` // notifies client that the sending transactions process started - `wallet.router.sign-transactions` // notifies client about the list of transactions that need to be signed - `wallet.router.transactions-sent` // notifies client about transactions that are sent - `wallet.transaction.status-changed` // notifies about status of sent transactions --- services/wallet/api.go | 190 ++++++++++ services/wallet/errors.go | 10 + .../router_build_transactions_params.go | 6 + .../wallet/requests/router_input_params.go | 1 + .../router_send_transactions_params.go | 8 + .../wallet/responses/router_transactions.go | 72 ++++ services/wallet/router/router.go | 14 + services/wallet/router/routes/route.go | 8 + services/wallet/router/routes/router_path.go | 115 ++++++ .../wallet/router/routes/router_path_test.go | 59 +++ .../transfer/commands_sequential_test.go | 2 +- services/wallet/transfer/errors.go | 13 + .../wallet/transfer/transaction_manager.go | 28 ++ .../transaction_manager_multitransaction.go | 1 + .../transfer/transaction_manager_route.go | 353 ++++++++++++++++++ signal/events_wallet.go | 10 +- transactions/transactor.go | 7 +- 17 files changed, 892 insertions(+), 5 deletions(-) create mode 100644 services/wallet/errors.go create mode 100644 services/wallet/requests/router_build_transactions_params.go create mode 100644 services/wallet/requests/router_send_transactions_params.go create mode 100644 services/wallet/responses/router_transactions.go create mode 100644 services/wallet/router/routes/router_path_test.go create mode 100644 services/wallet/transfer/errors.go create mode 100644 services/wallet/transfer/transaction_manager_route.go diff --git a/services/wallet/api.go b/services/wallet/api.go index eae8c7abe..934023873 100644 --- a/services/wallet/api.go +++ b/services/wallet/api.go @@ -18,6 +18,7 @@ import ( signercore "github.com/ethereum/go-ethereum/signer/core/apitypes" abi_spec "github.com/status-im/status-go/abi-spec" "github.com/status-im/status-go/account" + statusErrors "github.com/status-im/status-go/errors" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/params" @@ -30,13 +31,16 @@ import ( "github.com/status-im/status-go/services/wallet/history" "github.com/status-im/status-go/services/wallet/onramp" "github.com/status-im/status-go/services/wallet/requests" + "github.com/status-im/status-go/services/wallet/responses" "github.com/status-im/status-go/services/wallet/router" "github.com/status-im/status-go/services/wallet/router/fees" "github.com/status-im/status-go/services/wallet/router/pathprocessor" + "github.com/status-im/status-go/services/wallet/router/sendtype" "github.com/status-im/status-go/services/wallet/thirdparty" "github.com/status-im/status-go/services/wallet/token" "github.com/status-im/status-go/services/wallet/transfer" "github.com/status-im/status-go/services/wallet/walletconnect" + "github.com/status-im/status-go/signal" "github.com/status-im/status-go/transactions" ) @@ -712,6 +716,15 @@ func (api *API) SendTransactionWithSignature(ctx context.Context, chainID uint64 return api.s.transactionManager.SendTransactionWithSignature(chainID, params, sig) } +// Deprecated: `CreateMultiTransaction` is the old way of sending transactions and should not be used anymore. +// +// The flow that should be used instead: +// - call `BuildTransactionsFromRoute` +// - wait for the `wallet.router.sign-transactions` signal +// - sign received hashes using `SignMessage` call or sign on keycard +// - call `SendRouterTransactionsWithSignatures` with the signatures of signed hashes from the previous step +// +// TODO: remove this struct once mobile switches to the new approach func (api *API) CreateMultiTransaction(ctx context.Context, multiTransactionCommand *transfer.MultiTransactionCommand, data []*pathprocessor.MultipathProcessorTxArgs, password string) (*transfer.MultiTransactionCommandResult, error) { log.Debug("[WalletAPI:: CreateMultiTransaction] create multi transaction") @@ -742,11 +755,188 @@ func (api *API) CreateMultiTransaction(ctx context.Context, multiTransactionComm return nil, api.s.transactionManager.SendTransactionForSigningToKeycard(ctx, cmd, data, api.router.GetPathProcessors()) } +func updateFields(sd *responses.SendDetails, inputParams requests.RouteInputParams) { + sd.SendType = int(inputParams.SendType) + sd.FromAddress = types.Address(inputParams.AddrFrom) + sd.ToAddress = types.Address(inputParams.AddrTo) + sd.FromToken = inputParams.TokenID + sd.ToToken = inputParams.ToTokenID + if inputParams.AmountIn != nil { + sd.FromAmount = inputParams.AmountIn.String() + } + if inputParams.AmountOut != nil { + sd.ToAmount = inputParams.AmountOut.String() + } + sd.OwnerTokenBeingSent = inputParams.TokenIDIsOwnerToken + sd.Username = inputParams.Username + sd.PublicKey = inputParams.PublicKey + if inputParams.PackID != nil { + sd.PackID = inputParams.PackID.String() + } +} + +func (api *API) BuildTransactionsFromRoute(ctx context.Context, buildInputParams *requests.RouterBuildTransactionsParams) { + log.Debug("[WalletAPI::BuildTransactionsFromRoute] builds transactions from the generated best route", "uuid", buildInputParams.Uuid) + + go func() { + api.router.StopSuggestedRoutesAsyncCalculation() + + var err error + response := &responses.RouterTransactionsForSigning{ + SendDetails: &responses.SendDetails{ + Uuid: buildInputParams.Uuid, + }, + } + + defer func() { + if err != nil { + api.s.transactionManager.ClearLocalRouterTransactionsData() + err = statusErrors.CreateErrorResponseFromError(err) + response.SendDetails.ErrorResponse = err.(*statusErrors.ErrorResponse) + } + signal.SendWalletEvent(signal.SignRouterTransactions, response) + }() + + route, routeInputParams := api.router.GetBestRouteAndAssociatedInputParams() + if routeInputParams.Uuid != buildInputParams.Uuid { + // should never be here + err = ErrCannotResolveRouteId + return + } + + updateFields(response.SendDetails, routeInputParams) + + // notify client that sending transactions started (has 3 steps, building txs, signing txs, sending txs) + signal.SendWalletEvent(signal.RouterSendingTransactionsStarted, response.SendDetails) + + response.SigningDetails, err = api.s.transactionManager.BuildTransactionsFromRoute( + route, + api.router.GetPathProcessors(), + transfer.BuildRouteExtraParams{ + AddressFrom: routeInputParams.AddrFrom, + AddressTo: routeInputParams.AddrTo, + Username: routeInputParams.Username, + PublicKey: routeInputParams.PublicKey, + PackID: routeInputParams.PackID.ToInt(), + SlippagePercentage: buildInputParams.SlippagePercentage, + }, + ) + }() +} + +// Deprecated: `ProceedWithTransactionsSignatures` is the endpoint used in the old way of sending transactions and should not be used anymore. +// +// The flow that should be used instead: +// - call `BuildTransactionsFromRoute` +// - wait for the `wallet.router.sign-transactions` signal +// - sign received hashes using `SignMessage` call or sign on keycard +// - call `SendRouterTransactionsWithSignatures` with the signatures of signed hashes from the previous step +// +// TODO: remove this struct once mobile switches to the new approach func (api *API) ProceedWithTransactionsSignatures(ctx context.Context, signatures map[string]transfer.SignatureDetails) (*transfer.MultiTransactionCommandResult, error) { log.Debug("[WalletAPI:: ProceedWithTransactionsSignatures] sign with signatures and send multi transaction") return api.s.transactionManager.ProceedWithTransactionsSignatures(ctx, signatures) } +func (api *API) SendRouterTransactionsWithSignatures(ctx context.Context, sendInputParams *requests.RouterSendTransactionsParams) { + log.Debug("[WalletAPI:: SendRouterTransactionsWithSignatures] sign with signatures and send") + go func() { + + var ( + err error + routeInputParams requests.RouteInputParams + ) + response := &responses.RouterSentTransactions{ + SendDetails: &responses.SendDetails{ + Uuid: sendInputParams.Uuid, + }, + } + + defer func() { + clearLocalData := true + if routeInputParams.SendType == sendtype.Swap { + // in case of swap don't clear local data if an approval is placed, but swap tx is not sent yet + if api.s.transactionManager.ApprovalRequiredForPath(pathprocessor.ProcessorSwapParaswapName) && + api.s.transactionManager.ApprovalPlacedForPath(pathprocessor.ProcessorSwapParaswapName) && + !api.s.transactionManager.TxPlacedForPath(pathprocessor.ProcessorSwapParaswapName) { + clearLocalData = false + } + } + + if clearLocalData { + api.s.transactionManager.ClearLocalRouterTransactionsData() + } + + if err != nil { + err = statusErrors.CreateErrorResponseFromError(err) + response.SendDetails.ErrorResponse = err.(*statusErrors.ErrorResponse) + } + signal.SendWalletEvent(signal.RouterTransactionsSent, response) + }() + + _, routeInputParams = api.router.GetBestRouteAndAssociatedInputParams() + if routeInputParams.Uuid != sendInputParams.Uuid { + err = ErrCannotResolveRouteId + return + } + + updateFields(response.SendDetails, routeInputParams) + + err = api.s.transactionManager.ValidateAndAddSignaturesToRouterTransactions(sendInputParams.Signatures) + if err != nil { + return + } + + ////////////////////////////////////////////////////////////////////////////// + // prepare multitx + var mtType transfer.MultiTransactionType = transfer.MultiTransactionSend + if routeInputParams.SendType == sendtype.Bridge { + mtType = transfer.MultiTransactionBridge + } else if routeInputParams.SendType == sendtype.Swap { + mtType = transfer.MultiTransactionSwap + } + + multiTx := transfer.NewMultiTransaction( + /* Timestamp: */ uint64(time.Now().Unix()), + /* FromNetworkID: */ 0, + /* ToNetworkID: */ 0, + /* FromTxHash: */ common.Hash{}, + /* ToTxHash: */ common.Hash{}, + /* FromAddress: */ routeInputParams.AddrFrom, + /* ToAddress: */ routeInputParams.AddrTo, + /* FromAsset: */ routeInputParams.TokenID, + /* ToAsset: */ routeInputParams.ToTokenID, + /* FromAmount: */ routeInputParams.AmountIn, + /* ToAmount: */ routeInputParams.AmountOut, + /* Type: */ mtType, + /* CrossTxID: */ "", + ) + + _, err = api.s.transactionManager.InsertMultiTransaction(multiTx) + if err != nil { + return + } + ////////////////////////////////////////////////////////////////////////////// + + response.SentTransactions, err = api.s.transactionManager.SendRouterTransactions(ctx, multiTx) + var ( + chainIDs []uint64 + addresses []common.Address + ) + for _, tx := range response.SentTransactions { + chainIDs = append(chainIDs, tx.FromChain) + addresses = append(addresses, common.Address(tx.FromAddress)) + go func(chainId uint64, txHash common.Hash) { + err = api.s.transactionManager.WatchTransaction(context.Background(), chainId, txHash) + if err != nil { + return + } + }(tx.FromChain, common.Hash(tx.Hash)) + } + err = api.s.transferController.CheckRecentHistory(chainIDs, addresses) + }() +} + func (api *API) GetMultiTransactions(ctx context.Context, transactionIDs []wcommon.MultiTransactionIDType) ([]*transfer.MultiTransaction, error) { log.Debug("wallet.api.GetMultiTransactions", "IDs.len", len(transactionIDs)) return api.s.transactionManager.GetMultiTransactions(ctx, transactionIDs) diff --git a/services/wallet/errors.go b/services/wallet/errors.go new file mode 100644 index 000000000..77cd8bbe1 --- /dev/null +++ b/services/wallet/errors.go @@ -0,0 +1,10 @@ +package wallet + +import ( + "github.com/status-im/status-go/errors" +) + +// Abbreviation `W` for the error code stands for Wallet +var ( + ErrCannotResolveRouteId = &errors.ErrorResponse{Code: errors.ErrorCode("W-001"), Details: "cannot resolve route id"} +) diff --git a/services/wallet/requests/router_build_transactions_params.go b/services/wallet/requests/router_build_transactions_params.go new file mode 100644 index 000000000..1090d9f7c --- /dev/null +++ b/services/wallet/requests/router_build_transactions_params.go @@ -0,0 +1,6 @@ +package requests + +type RouterBuildTransactionsParams struct { + Uuid string `json:"uuid"` + SlippagePercentage float32 `json:"slippagePercentage"` +} diff --git a/services/wallet/requests/router_input_params.go b/services/wallet/requests/router_input_params.go index 1ada0b5a5..1130fdf33 100644 --- a/services/wallet/requests/router_input_params.go +++ b/services/wallet/requests/router_input_params.go @@ -44,6 +44,7 @@ type RouteInputParams struct { AmountIn *hexutil.Big `json:"amountIn" validate:"required"` AmountOut *hexutil.Big `json:"amountOut"` TokenID string `json:"tokenID" validate:"required"` + TokenIDIsOwnerToken bool `json:"tokenIDIsOwnerToken"` ToTokenID string `json:"toTokenID"` DisabledFromChainIDs []uint64 `json:"disabledFromChainIDs"` DisabledToChainIDs []uint64 `json:"disabledToChainIDs"` diff --git a/services/wallet/requests/router_send_transactions_params.go b/services/wallet/requests/router_send_transactions_params.go new file mode 100644 index 000000000..bc0de61c0 --- /dev/null +++ b/services/wallet/requests/router_send_transactions_params.go @@ -0,0 +1,8 @@ +package requests + +import "github.com/status-im/status-go/services/wallet/transfer" + +type RouterSendTransactionsParams struct { + Uuid string `json:"uuid"` + Signatures map[string]transfer.SignatureDetails `json:"signatures"` +} diff --git a/services/wallet/responses/router_transactions.go b/services/wallet/responses/router_transactions.go new file mode 100644 index 000000000..1f277e428 --- /dev/null +++ b/services/wallet/responses/router_transactions.go @@ -0,0 +1,72 @@ +package responses + +import ( + "github.com/status-im/status-go/errors" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/transactions" +) + +type SendDetails struct { + Uuid string `json:"uuid"` + SendType int `json:"sendType"` + FromAddress types.Address `json:"fromAddress"` + ToAddress types.Address `json:"toAddress"` + FromToken string `json:"fromToken"` + ToToken string `json:"toToken"` + FromAmount string `json:"fromAmount"` // total amount + ToAmount string `json:"toAmount"` + OwnerTokenBeingSent bool `json:"ownerTokenBeingSent"` + ErrorResponse *errors.ErrorResponse `json:"errorResponse,omitempty"` + + Username string `json:"username"` + PublicKey string `json:"publicKey"` + PackID string `json:"packId"` +} + +type SigningDetails struct { + Address types.Address `json:"address"` + AddressPath string `json:"addressPath"` + KeyUid string `json:"keyUid"` + SignOnKeycard bool `json:"signOnKeycard"` + Hashes []types.Hash `json:"hashes"` +} + +type RouterTransactionsForSigning struct { + SendDetails *SendDetails `json:"sendDetails"` + SigningDetails *SigningDetails `json:"signingDetails"` +} + +type RouterSentTransaction struct { + FromAddress types.Address `json:"fromAddress"` + ToAddress types.Address `json:"toAddress"` + FromChain uint64 `json:"fromChain"` + ToChain uint64 `json:"toChain"` + FromToken string `json:"fromToken"` + ToToken string `json:"toToken"` + Amount string `json:"amount"` // amount of the transaction + Hash types.Hash `json:"hash"` + ApprovalTx bool `json:"approvalTx"` +} + +type RouterSentTransactions struct { + SendDetails *SendDetails `json:"sendDetails"` + SentTransactions []*RouterSentTransaction `json:"sentTransactions"` +} + +func NewRouterSentTransaction(sendArgs *transactions.SendTxArgs, hash types.Hash, approvalTx bool) *RouterSentTransaction { + addr := types.Address{} + if sendArgs.To != nil { + addr = *sendArgs.To + } + return &RouterSentTransaction{ + FromAddress: sendArgs.From, + ToAddress: addr, + FromChain: sendArgs.FromChainID, + ToChain: sendArgs.ToChainID, + FromToken: sendArgs.FromTokenID, + ToToken: sendArgs.ToTokenID, + Amount: sendArgs.Value.String(), + Hash: hash, + ApprovalTx: approvalTx, + } +} diff --git a/services/wallet/router/router.go b/services/wallet/router/router.go index 86dc58684..6b66c25e2 100644 --- a/services/wallet/router/router.go +++ b/services/wallet/router/router.go @@ -120,6 +120,20 @@ func (r *Router) GetPathProcessors() map[string]pathprocessor.PathProcessor { return r.pathProcessors } +func (r *Router) GetBestRouteAndAssociatedInputParams() (routes.Route, requests.RouteInputParams) { + r.activeRoutesMutex.Lock() + defer r.activeRoutesMutex.Unlock() + if r.activeRoutes == nil { + return nil, requests.RouteInputParams{} + } + + r.lastInputParamsMutex.Lock() + defer r.lastInputParamsMutex.Unlock() + ip := *r.lastInputParams + + return r.activeRoutes.Best.Copy(), ip +} + func (r *Router) SetTestBalanceMap(balanceMap map[string]*big.Int) { for k, v := range balanceMap { r.activeBalanceMap.Store(k, v) diff --git a/services/wallet/router/routes/route.go b/services/wallet/router/routes/route.go index aa00ae002..56057f066 100644 --- a/services/wallet/router/routes/route.go +++ b/services/wallet/router/routes/route.go @@ -9,6 +9,14 @@ import ( type Route []*Path +func (r Route) Copy() Route { + newRoute := make(Route, len(r)) + for i, path := range r { + newRoute[i] = path.Copy() + } + return newRoute +} + func FindBestRoute(routes []Route, tokenPrice float64, nativeTokenPrice float64) Route { var best Route bestCost := big.NewFloat(math.Inf(1)) diff --git a/services/wallet/router/routes/router_path.go b/services/wallet/router/routes/router_path.go index 1aa8ee421..87b57bb34 100644 --- a/services/wallet/router/routes/router_path.go +++ b/services/wallet/router/routes/router_path.go @@ -54,3 +54,118 @@ type Path struct { func (p *Path) Equal(o *Path) bool { return p.FromChain.ChainID == o.FromChain.ChainID && p.ToChain.ChainID == o.ToChain.ChainID } + +func (p *Path) Copy() *Path { + newPath := &Path{ + ProcessorName: p.ProcessorName, + AmountInLocked: p.AmountInLocked, + TxGasAmount: p.TxGasAmount, + ApprovalRequired: p.ApprovalRequired, + ApprovalGasAmount: p.ApprovalGasAmount, + EstimatedTime: p.EstimatedTime, + SubtractFees: p.SubtractFees, + } + + if p.FromChain != nil { + newPath.FromChain = ¶ms.Network{} + *newPath.FromChain = *p.FromChain + } + + if p.ToChain != nil { + newPath.ToChain = ¶ms.Network{} + *newPath.ToChain = *p.ToChain + } + + if p.FromToken != nil { + newPath.FromToken = &walletToken.Token{} + *newPath.FromToken = *p.FromToken + } + + if p.ToToken != nil { + newPath.ToToken = &walletToken.Token{} + *newPath.ToToken = *p.ToToken + } + + if p.AmountIn != nil { + newPath.AmountIn = (*hexutil.Big)(big.NewInt(0).Set(p.AmountIn.ToInt())) + } + + if p.AmountOut != nil { + newPath.AmountOut = (*hexutil.Big)(big.NewInt(0).Set(p.AmountOut.ToInt())) + } + + if p.SuggestedLevelsForMaxFeesPerGas != nil { + newPath.SuggestedLevelsForMaxFeesPerGas = &fees.MaxFeesLevels{ + Low: (*hexutil.Big)(big.NewInt(0).Set(p.SuggestedLevelsForMaxFeesPerGas.Low.ToInt())), + Medium: (*hexutil.Big)(big.NewInt(0).Set(p.SuggestedLevelsForMaxFeesPerGas.Medium.ToInt())), + High: (*hexutil.Big)(big.NewInt(0).Set(p.SuggestedLevelsForMaxFeesPerGas.High.ToInt())), + } + } + + if p.MaxFeesPerGas != nil { + newPath.MaxFeesPerGas = (*hexutil.Big)(big.NewInt(0).Set(p.MaxFeesPerGas.ToInt())) + } + + if p.TxBaseFee != nil { + newPath.TxBaseFee = (*hexutil.Big)(big.NewInt(0).Set(p.TxBaseFee.ToInt())) + } + + if p.TxPriorityFee != nil { + newPath.TxPriorityFee = (*hexutil.Big)(big.NewInt(0).Set(p.TxPriorityFee.ToInt())) + } + + if p.TxBonderFees != nil { + newPath.TxBonderFees = (*hexutil.Big)(big.NewInt(0).Set(p.TxBonderFees.ToInt())) + } + + if p.TxTokenFees != nil { + newPath.TxTokenFees = (*hexutil.Big)(big.NewInt(0).Set(p.TxTokenFees.ToInt())) + } + + if p.TxFee != nil { + newPath.TxFee = (*hexutil.Big)(big.NewInt(0).Set(p.TxFee.ToInt())) + } + + if p.TxL1Fee != nil { + newPath.TxL1Fee = (*hexutil.Big)(big.NewInt(0).Set(p.TxL1Fee.ToInt())) + } + + if p.ApprovalAmountRequired != nil { + newPath.ApprovalAmountRequired = (*hexutil.Big)(big.NewInt(0).Set(p.ApprovalAmountRequired.ToInt())) + } + + if p.ApprovalContractAddress != nil { + addr := common.HexToAddress(p.ApprovalContractAddress.Hex()) + newPath.ApprovalContractAddress = &addr + } + + if p.ApprovalBaseFee != nil { + newPath.ApprovalBaseFee = (*hexutil.Big)(big.NewInt(0).Set(p.ApprovalBaseFee.ToInt())) + } + + if p.ApprovalPriorityFee != nil { + newPath.ApprovalPriorityFee = (*hexutil.Big)(big.NewInt(0).Set(p.ApprovalPriorityFee.ToInt())) + } + + if p.ApprovalFee != nil { + newPath.ApprovalFee = (*hexutil.Big)(big.NewInt(0).Set(p.ApprovalFee.ToInt())) + } + + if p.ApprovalL1Fee != nil { + newPath.ApprovalL1Fee = (*hexutil.Big)(big.NewInt(0).Set(p.ApprovalL1Fee.ToInt())) + } + + if p.TxTotalFee != nil { + newPath.TxTotalFee = (*hexutil.Big)(big.NewInt(0).Set(p.TxTotalFee.ToInt())) + } + + if p.RequiredTokenBalance != nil { + newPath.RequiredTokenBalance = big.NewInt(0).Set(p.RequiredTokenBalance) + } + + if p.RequiredNativeBalance != nil { + newPath.RequiredNativeBalance = big.NewInt(0).Set(p.RequiredNativeBalance) + } + + return newPath +} diff --git a/services/wallet/router/routes/router_path_test.go b/services/wallet/router/routes/router_path_test.go new file mode 100644 index 000000000..79cbe17e7 --- /dev/null +++ b/services/wallet/router/routes/router_path_test.go @@ -0,0 +1,59 @@ +package routes + +import ( + "math/big" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/services/wallet/router/fees" + "github.com/status-im/status-go/services/wallet/token" +) + +func TestCopyPath(t *testing.T) { + addr := common.HexToAddress("0x123") + path := &Path{ + ProcessorName: "test", + FromChain: ¶ms.Network{ChainID: 1}, + ToChain: ¶ms.Network{ChainID: 2}, + FromToken: &token.Token{Symbol: "symbol1"}, + ToToken: &token.Token{Symbol: "symbol2"}, + AmountIn: (*hexutil.Big)(big.NewInt(100)), + AmountInLocked: true, + AmountOut: (*hexutil.Big)(big.NewInt(200)), + SuggestedLevelsForMaxFeesPerGas: &fees.MaxFeesLevels{ + Low: (*hexutil.Big)(big.NewInt(100)), + Medium: (*hexutil.Big)(big.NewInt(200)), + High: (*hexutil.Big)(big.NewInt(300)), + }, + MaxFeesPerGas: (*hexutil.Big)(big.NewInt(100)), + TxBaseFee: (*hexutil.Big)(big.NewInt(100)), + TxPriorityFee: (*hexutil.Big)(big.NewInt(100)), + TxGasAmount: 100, + TxBonderFees: (*hexutil.Big)(big.NewInt(100)), + TxTokenFees: (*hexutil.Big)(big.NewInt(100)), + TxFee: (*hexutil.Big)(big.NewInt(100)), + TxL1Fee: (*hexutil.Big)(big.NewInt(100)), + ApprovalRequired: true, + ApprovalAmountRequired: (*hexutil.Big)(big.NewInt(100)), + ApprovalContractAddress: &addr, + ApprovalBaseFee: (*hexutil.Big)(big.NewInt(100)), + ApprovalPriorityFee: (*hexutil.Big)(big.NewInt(100)), + ApprovalGasAmount: 100, + ApprovalFee: (*hexutil.Big)(big.NewInt(100)), + ApprovalL1Fee: (*hexutil.Big)(big.NewInt(100)), + TxTotalFee: (*hexutil.Big)(big.NewInt(100)), + EstimatedTime: fees.TransactionEstimation(100), + RequiredTokenBalance: big.NewInt(100), + RequiredNativeBalance: big.NewInt(100), + SubtractFees: true, + } + + newPath := path.Copy() + + assert.True(t, reflect.DeepEqual(path, newPath)) +} diff --git a/services/wallet/transfer/commands_sequential_test.go b/services/wallet/transfer/commands_sequential_test.go index 6f23346df..b463ada8d 100644 --- a/services/wallet/transfer/commands_sequential_test.go +++ b/services/wallet/transfer/commands_sequential_test.go @@ -1325,7 +1325,7 @@ func TestFetchTransfersForLoadedBlocks(t *testing.T) { db, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{}) require.NoError(t, err) - tm := &TransactionManager{NewMultiTransactionDB(db), nil, nil, nil, nil, nil, nil, nil, nil, nil} + tm := &TransactionManager{NewMultiTransactionDB(db), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil} mediaServer, err := server.NewMediaServer(appdb, nil, nil, db) require.NoError(t, err) diff --git a/services/wallet/transfer/errors.go b/services/wallet/transfer/errors.go new file mode 100644 index 000000000..383f47fde --- /dev/null +++ b/services/wallet/transfer/errors.go @@ -0,0 +1,13 @@ +package transfer + +import ( + "github.com/status-im/status-go/errors" +) + +// Abbreviation `WT` for the error code stands for Wallet Transfer +var ( + ErrNoRoute = &errors.ErrorResponse{Code: errors.ErrorCode("WT-001"), Details: "no generated route"} + ErrNoTrsansactionsBeingBuilt = &errors.ErrorResponse{Code: errors.ErrorCode("WT-002"), Details: "no transactions being built"} + ErrMissingSignatureForTx = &errors.ErrorResponse{Code: errors.ErrorCode("WT-003"), Details: "missing signature for transaction %s"} + ErrInvalidSignatureDetails = &errors.ErrorResponse{Code: errors.ErrorCode("WT-004"), Details: "invalid signature details"} +) diff --git a/services/wallet/transfer/transaction_manager.go b/services/wallet/transfer/transaction_manager.go index c839d4f6c..9c77285ed 100644 --- a/services/wallet/transfer/transaction_manager.go +++ b/services/wallet/transfer/transaction_manager.go @@ -17,6 +17,7 @@ import ( "github.com/status-im/status-go/params" wallet_common "github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/services/wallet/router/pathprocessor" + "github.com/status-im/status-go/services/wallet/router/routes" "github.com/status-im/status-go/transactions" ) @@ -26,6 +27,15 @@ type SignatureDetails struct { V string `json:"v"` } +func (sd *SignatureDetails) Validate() error { + if len(sd.R) != 64 || len(sd.S) != 64 || len(sd.V) != 2 { + return ErrInvalidSignatureDetails + } + + return nil +} + +// TODO: remove this struct once mobile switches to the new approach type TransactionDescription struct { chainID uint64 from common.Address @@ -33,6 +43,20 @@ type TransactionDescription struct { signature []byte } +type RouterTransactionDetails struct { + routerPath *routes.Path + txArgs *transactions.SendTxArgs + tx *ethTypes.Transaction + txHashToSign types.Hash + txSignature []byte + txSentHash types.Hash + approvalTxArgs *transactions.SendTxArgs + approvalTx *ethTypes.Transaction + approvalHashToSign types.Hash + approvalSignature []byte + approvalTxSentHash types.Hash +} + type TransactionManager struct { storage MultiTransactionStorage gethManager *account.GethManager @@ -42,9 +66,13 @@ type TransactionManager struct { pendingTracker *transactions.PendingTxTracker eventFeed *event.Feed + // TODO: remove this struct once mobile switches to the new approach multiTransactionForKeycardSigning *MultiTransaction multipathTransactionsData []*pathprocessor.MultipathProcessorTxArgs transactionsForKeycardSigning map[common.Hash]*TransactionDescription + + // used in a new approach + routerTransactions []*RouterTransactionDetails } type MultiTransactionStorage interface { diff --git a/services/wallet/transfer/transaction_manager_multitransaction.go b/services/wallet/transfer/transaction_manager_multitransaction.go index f2685bd36..a678d590a 100644 --- a/services/wallet/transfer/transaction_manager_multitransaction.go +++ b/services/wallet/transfer/transaction_manager_multitransaction.go @@ -198,6 +198,7 @@ func (tm *TransactionManager) WatchTransaction(ctx context.Context, chainID uint return err } if p.ChainID == wallet_common.ChainID(chainID) && p.Hash == transactionHash { + signal.SendWalletEvent(signal.TransactionStatusChanged, p) return nil } } diff --git a/services/wallet/transfer/transaction_manager_route.go b/services/wallet/transfer/transaction_manager_route.go new file mode 100644 index 000000000..f885d7374 --- /dev/null +++ b/services/wallet/transfer/transaction_manager_route.go @@ -0,0 +1,353 @@ +package transfer + +import ( + "context" + "encoding/hex" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/status-im/status-go/errors" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + walletCommon "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/services/wallet/responses" + "github.com/status-im/status-go/services/wallet/router/pathprocessor" + "github.com/status-im/status-go/services/wallet/router/routes" + "github.com/status-im/status-go/transactions" +) + +type BuildRouteExtraParams struct { + AddressFrom common.Address + AddressTo common.Address + Username string + PublicKey string + PackID *big.Int + SlippagePercentage float32 +} + +func (tm *TransactionManager) ClearLocalRouterTransactionsData() { + tm.routerTransactions = nil +} + +func (tm *TransactionManager) ApprovalRequiredForPath(pathProcessorName string) bool { + for _, desc := range tm.routerTransactions { + if desc.routerPath.ProcessorName == pathProcessorName && + desc.routerPath.ApprovalRequired { + return true + } + } + return false +} + +func (tm *TransactionManager) ApprovalPlacedForPath(pathProcessorName string) bool { + for _, desc := range tm.routerTransactions { + if desc.routerPath.ProcessorName == pathProcessorName && + desc.approvalTxSentHash != (types.Hash{}) { + return true + } + } + return false +} + +func (tm *TransactionManager) TxPlacedForPath(pathProcessorName string) bool { + for _, desc := range tm.routerTransactions { + if desc.routerPath.ProcessorName == pathProcessorName && + desc.txSentHash != (types.Hash{}) { + return true + } + } + return false +} + +func (tm *TransactionManager) buildApprovalTxForPath(path *routes.Path, addressFrom common.Address, + usedNonces map[uint64]int64, signer ethTypes.Signer) (types.Hash, error) { + lastUsedNonce := int64(-1) + if nonce, ok := usedNonces[path.FromChain.ChainID]; ok { + lastUsedNonce = nonce + } + + data, err := walletCommon.PackApprovalInputData(path.AmountIn.ToInt(), path.ApprovalContractAddress) + if err != nil { + return types.Hash{}, err + } + + addrTo := types.Address(path.FromToken.Address) + approavalSendArgs := &transactions.SendTxArgs{ + Version: transactions.SendTxArgsVersion1, + + // tx fields + From: types.Address(addressFrom), + To: &addrTo, + Value: (*hexutil.Big)(big.NewInt(0)), + Data: data, + Gas: (*hexutil.Uint64)(&path.ApprovalGasAmount), + MaxFeePerGas: path.MaxFeesPerGas, + MaxPriorityFeePerGas: path.ApprovalPriorityFee, + + // additional fields version 1 + FromChainID: path.FromChain.ChainID, + } + if path.FromToken != nil { + approavalSendArgs.FromTokenID = path.FromToken.Symbol + } + + builtApprovalTx, usedNonce, err := tm.transactor.ValidateAndBuildTransaction(approavalSendArgs.FromChainID, *approavalSendArgs, lastUsedNonce) + if err != nil { + return types.Hash{}, err + } + approvalTxHash := signer.Hash(builtApprovalTx) + usedNonces[path.FromChain.ChainID] = int64(usedNonce) + + tm.routerTransactions = append(tm.routerTransactions, &RouterTransactionDetails{ + routerPath: path, + approvalTxArgs: approavalSendArgs, + approvalTx: builtApprovalTx, + approvalHashToSign: types.Hash(approvalTxHash), + }) + + return types.Hash(approvalTxHash), nil +} + +func (tm *TransactionManager) buildTxForPath(path *routes.Path, pathProcessors map[string]pathprocessor.PathProcessor, + usedNonces map[uint64]int64, signer ethTypes.Signer, params BuildRouteExtraParams) (types.Hash, error) { + lastUsedNonce := int64(-1) + if nonce, ok := usedNonces[path.FromChain.ChainID]; ok { + lastUsedNonce = nonce + } + + processorInputParams := pathprocessor.ProcessorInputParams{ + FromAddr: params.AddressFrom, + ToAddr: params.AddressTo, + FromChain: path.FromChain, + ToChain: path.ToChain, + FromToken: path.FromToken, + ToToken: path.ToToken, + AmountIn: path.AmountIn.ToInt(), + AmountOut: path.AmountOut.ToInt(), + + Username: params.Username, + PublicKey: params.PublicKey, + PackID: params.PackID, + } + + data, err := pathProcessors[path.ProcessorName].PackTxInputData(processorInputParams) + if err != nil { + return types.Hash{}, err + } + + addrTo := types.Address(params.AddressTo) + sendArgs := &transactions.SendTxArgs{ + Version: transactions.SendTxArgsVersion1, + + // tx fields + From: types.Address(params.AddressFrom), + To: &addrTo, + Value: path.AmountIn, + Data: data, + Gas: (*hexutil.Uint64)(&path.TxGasAmount), + MaxFeePerGas: path.MaxFeesPerGas, + MaxPriorityFeePerGas: path.TxPriorityFee, + + // additional fields version 1 + ValueOut: path.AmountOut, + FromChainID: path.FromChain.ChainID, + ToChainID: path.ToChain.ChainID, + SlippagePercentage: params.SlippagePercentage, + } + if path.FromToken != nil { + sendArgs.FromTokenID = path.FromToken.Symbol + sendArgs.ToContractAddress = types.Address(path.FromToken.Address) + + // special handling for transfer tx if selected token is not ETH + // TODO: we should fix that in the trasactor, but till then, the best place to handle it is here + if !path.FromToken.IsNative() { + sendArgs.Value = (*hexutil.Big)(big.NewInt(0)) + + if path.ProcessorName == pathprocessor.ProcessorTransferName || + path.ProcessorName == pathprocessor.ProcessorStickersBuyName || + path.ProcessorName == pathprocessor.ProcessorENSRegisterName || + path.ProcessorName == pathprocessor.ProcessorENSReleaseName || + path.ProcessorName == pathprocessor.ProcessorENSPublicKeyName { + // TODO: update functions from `TransactorIface` to use `ToContractAddress` (as an address of the contract a transaction should be sent to) + // and `To` (as the destination address, recipient) of `SendTxArgs` struct appropriately + toContractAddr := types.Address(path.FromToken.Address) + sendArgs.To = &toContractAddr + } + } + } + if path.ToToken != nil { + sendArgs.ToTokenID = path.ToToken.Symbol + } + + builtTx, usedNonce, err := pathProcessors[path.ProcessorName].BuildTransactionV2(sendArgs, lastUsedNonce) + if err != nil { + return types.Hash{}, err + } + txHash := signer.Hash(builtTx) + usedNonces[path.FromChain.ChainID] = int64(usedNonce) + + tm.routerTransactions = append(tm.routerTransactions, &RouterTransactionDetails{ + routerPath: path, + txArgs: sendArgs, + tx: builtTx, + txHashToSign: types.Hash(txHash), + }) + + return types.Hash(txHash), nil +} + +func (tm *TransactionManager) BuildTransactionsFromRoute(route routes.Route, pathProcessors map[string]pathprocessor.PathProcessor, + params BuildRouteExtraParams) (*responses.SigningDetails, error) { + if len(route) == 0 { + return nil, ErrNoRoute + } + + accFrom, err := tm.accountsDB.GetAccountByAddress(types.Address(params.AddressFrom)) + if err != nil { + return nil, err + } + + keypair, err := tm.accountsDB.GetKeypairByKeyUID(accFrom.KeyUID) + if err != nil { + return nil, err + } + + response := &responses.SigningDetails{ + Address: accFrom.Address, + AddressPath: accFrom.Path, + KeyUid: accFrom.KeyUID, + SignOnKeycard: keypair.MigratedToKeycard(), + } + + usedNonces := make(map[uint64]int64) + for _, path := range route { + signer := ethTypes.NewLondonSigner(big.NewInt(int64(path.FromChain.ChainID))) + + // always check for approval tx first for the path and build it if needed + if path.ApprovalRequired && !tm.ApprovalPlacedForPath(path.ProcessorName) { + approvalTxHash, err := tm.buildApprovalTxForPath(path, params.AddressFrom, usedNonces, signer) + if err != nil { + return nil, err + } + response.Hashes = append(response.Hashes, approvalTxHash) + + // if approval is needed for swap, we cannot build the swap tx before the approval tx is mined + if path.ProcessorName == pathprocessor.ProcessorSwapParaswapName { + continue + } + } + + // build tx for the path + txHash, err := tm.buildTxForPath(path, pathProcessors, usedNonces, signer, params) + if err != nil { + return nil, err + } + response.Hashes = append(response.Hashes, txHash) + } + + return response, nil +} + +func getSignatureForTxHash(txHash string, signatures map[string]SignatureDetails) ([]byte, error) { + sigDetails, ok := signatures[txHash] + if !ok { + err := &errors.ErrorResponse{ + Code: ErrMissingSignatureForTx.Code, + Details: fmt.Sprintf(ErrMissingSignatureForTx.Details, txHash), + } + return nil, err + } + + err := sigDetails.Validate() + if err != nil { + return nil, err + } + + rBytes, _ := hex.DecodeString(sigDetails.R) + sBytes, _ := hex.DecodeString(sigDetails.S) + vByte := byte(0) + if sigDetails.V == "01" { + vByte = 1 + } + + signature := make([]byte, crypto.SignatureLength) + copy(signature[32-len(rBytes):32], rBytes) + copy(signature[64-len(rBytes):64], sBytes) + signature[64] = vByte + + return signature, nil +} + +func (tm *TransactionManager) ValidateAndAddSignaturesToRouterTransactions(signatures map[string]SignatureDetails) error { + if len(tm.routerTransactions) == 0 { + return ErrNoTrsansactionsBeingBuilt + } + + // check if all transactions have been signed + for _, desc := range tm.routerTransactions { + if desc.approvalTx != nil && desc.approvalTxSentHash == (types.Hash{}) { + sig, err := getSignatureForTxHash(desc.approvalHashToSign.String(), signatures) + if err != nil { + return err + } + desc.approvalSignature = sig + } + + if desc.tx != nil && desc.txSentHash == (types.Hash{}) { + sig, err := getSignatureForTxHash(desc.txHashToSign.String(), signatures) + if err != nil { + return err + } + desc.txSignature = sig + } + } + + return nil +} + +func (tm *TransactionManager) SendRouterTransactions(ctx context.Context, multiTx *MultiTransaction) (transactions []*responses.RouterSentTransaction, err error) { + transactions = make([]*responses.RouterSentTransaction, 0) + + // send transactions + for _, desc := range tm.routerTransactions { + if desc.approvalTx != nil && desc.approvalTxSentHash == (types.Hash{}) { + var approvalTxWithSignature *ethTypes.Transaction + approvalTxWithSignature, err = tm.transactor.AddSignatureToTransaction(desc.approvalTxArgs.FromChainID, desc.approvalTx, desc.approvalSignature) + if err != nil { + return nil, err + } + + desc.approvalTxSentHash, err = tm.transactor.SendTransactionWithSignature(common.Address(desc.approvalTxArgs.From), desc.approvalTxArgs.FromTokenID, multiTx.ID, approvalTxWithSignature) + if err != nil { + return nil, err + } + + transactions = append(transactions, responses.NewRouterSentTransaction(desc.approvalTxArgs, desc.approvalTxSentHash, true)) + + // if approval is needed for swap, then we need to wait for the approval tx to be mined before sending the swap tx + if desc.routerPath.ProcessorName == pathprocessor.ProcessorSwapParaswapName { + continue + } + } + + if desc.tx != nil && desc.txSentHash == (types.Hash{}) { + var txWithSignature *ethTypes.Transaction + txWithSignature, err = tm.transactor.AddSignatureToTransaction(desc.txArgs.FromChainID, desc.tx, desc.txSignature) + if err != nil { + return nil, err + } + + desc.txSentHash, err = tm.transactor.SendTransactionWithSignature(common.Address(desc.txArgs.From), desc.txArgs.FromTokenID, multiTx.ID, txWithSignature) + if err != nil { + return nil, err + } + + transactions = append(transactions, responses.NewRouterSentTransaction(desc.txArgs, desc.txSentHash, false)) + } + } + + return +} diff --git a/signal/events_wallet.go b/signal/events_wallet.go index 925958958..02456aca8 100644 --- a/signal/events_wallet.go +++ b/signal/events_wallet.go @@ -3,9 +3,13 @@ package signal type SignalType string const ( - Wallet = SignalType("wallet") - SignTransactions = SignalType("wallet.sign.transactions") - SuggestedRoutes = SignalType("wallet.suggested.routes") + Wallet = SignalType("wallet") + SignTransactions = SignalType("wallet.sign.transactions") + RouterSendingTransactionsStarted = SignalType("wallet.router.sending-transactions-started") + SignRouterTransactions = SignalType("wallet.router.sign-transactions") + RouterTransactionsSent = SignalType("wallet.router.transactions-sent") + TransactionStatusChanged = SignalType("wallet.transaction.status-changed") + SuggestedRoutes = SignalType("wallet.suggested.routes") ) // SendWalletEvent sends event from services/wallet/events. diff --git a/transactions/transactor.go b/transactions/transactor.go index 766e9a217..bf6193e50 100644 --- a/transactions/transactor.go +++ b/transactions/transactor.go @@ -443,6 +443,11 @@ func (t *Transactor) validateAndBuildTransaction(rpcWrapper *rpcWrapper, args Se } func (t *Transactor) validateAndPropagate(rpcWrapper *rpcWrapper, selectedAccount *account.SelectedExtKey, args SendTxArgs, lastUsedNonce int64) (hash types.Hash, nonce uint64, err error) { + symbol := args.Symbol + if args.Version == SendTxArgsVersion1 { + symbol = args.FromTokenID + } + if err = t.validateAccount(args, selectedAccount); err != nil { return hash, nonce, err } @@ -458,7 +463,7 @@ func (t *Transactor) validateAndPropagate(rpcWrapper *rpcWrapper, selectedAccoun return hash, nonce, err } - hash, err = t.sendTransaction(rpcWrapper, common.Address(args.From), args.Symbol, args.MultiTransactionID, signedTx) + hash, err = t.sendTransaction(rpcWrapper, common.Address(args.From), symbol, args.MultiTransactionID, signedTx) return hash, tx.Nonce(), err }