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
This commit is contained in:
Sale Djenic 2024-09-23 09:19:00 +02:00 committed by Anthony Laibe
parent 1128598b03
commit 28506bcd17
17 changed files with 892 additions and 5 deletions

View File

@ -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)

10
services/wallet/errors.go Normal file
View File

@ -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"}
)

View File

@ -0,0 +1,6 @@
package requests
type RouterBuildTransactionsParams struct {
Uuid string `json:"uuid"`
SlippagePercentage float32 `json:"slippagePercentage"`
}

View File

@ -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"`

View File

@ -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"`
}

View File

@ -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,
}
}

View File

@ -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)

View File

@ -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))

View File

@ -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 = &params.Network{}
*newPath.FromChain = *p.FromChain
}
if p.ToChain != nil {
newPath.ToChain = &params.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
}

View File

@ -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: &params.Network{ChainID: 1},
ToChain: &params.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))
}

View File

@ -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)

View File

@ -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"}
)

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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.

View File

@ -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
}