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 }