diff --git a/services/wallet/common/const.go b/services/wallet/common/const.go index 62aa5d9cd..bae001ed4 100644 --- a/services/wallet/common/const.go +++ b/services/wallet/common/const.go @@ -61,6 +61,10 @@ func ZeroBigIntValue() *big.Int { return big.NewInt(0) } +func ZeroHash() ethCommon.Hash { + return ethCommon.Hash{} +} + func (c ChainID) String() string { return strconv.FormatUint(uint64(c), 10) } diff --git a/services/wallet/routeexecution/manager.go b/services/wallet/routeexecution/manager.go index 9d73c5849..6241bec5d 100644 --- a/services/wallet/routeexecution/manager.go +++ b/services/wallet/routeexecution/manager.go @@ -2,11 +2,16 @@ package routeexecution import ( "context" + "database/sql" "time" + "go.uber.org/zap" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/logutils" status_common "github.com/status-im/status-go/common" statusErrors "github.com/status-im/status-go/errors" @@ -23,16 +28,26 @@ type Manager struct { router *router.Router transactionManager *transfer.TransactionManager transferController *transfer.Controller + db *DB + + // Local data used for storage purposes + buildInputParams *requests.RouterBuildTransactionsParams } -func NewManager(router *router.Router, transactionManager *transfer.TransactionManager, transferController *transfer.Controller) *Manager { +func NewManager(walletDB *sql.DB, router *router.Router, transactionManager *transfer.TransactionManager, transferController *transfer.Controller) *Manager { return &Manager{ router: router, transactionManager: transactionManager, transferController: transferController, + db: NewDB(walletDB), } } +func (m *Manager) clearLocalRouteData() { + m.buildInputParams = nil + m.transactionManager.ClearLocalRouterTransactionsData() +} + func (m *Manager) BuildTransactionsFromRoute(ctx context.Context, buildInputParams *requests.RouterBuildTransactionsParams) { go func() { defer status_common.LogOnPanic() @@ -48,7 +63,7 @@ func (m *Manager) BuildTransactionsFromRoute(ctx context.Context, buildInputPara defer func() { if err != nil { - m.transactionManager.ClearLocalRouterTransactionsData() + m.clearLocalRouteData() err = statusErrors.CreateErrorResponseFromError(err) response.SendDetails.ErrorResponse = err.(*statusErrors.ErrorResponse) } @@ -62,6 +77,8 @@ func (m *Manager) BuildTransactionsFromRoute(ctx context.Context, buildInputPara return } + m.buildInputParams = buildInputParams + updateFields(response.SendDetails, routeInputParams) // notify client that sending transactions started (has 3 steps, building txs, signing txs, sending txs) @@ -108,7 +125,7 @@ func (m *Manager) SendRouterTransactionsWithSignatures(ctx context.Context, send } if clearLocalData { - m.transactionManager.ClearLocalRouterTransactionsData() + m.clearLocalRouteData() } if err != nil { @@ -163,6 +180,20 @@ func (m *Manager) SendRouterTransactionsWithSignatures(ctx context.Context, send ////////////////////////////////////////////////////////////////////////////// response.SentTransactions, err = m.transactionManager.SendRouterTransactions(ctx, multiTx) + if err != nil { + log.Error("Error sending router transactions", "error", err) + // TODO #16556: Handle partially successful Tx sends? + // Don't return, store whichever transactions were successfully sent + } + + // don't overwrite err since we want to process it in the deferred function + var tmpErr error + routerTransactions := m.transactionManager.GetRouterTransactions() + routeData := NewRouteData(&routeInputParams, m.buildInputParams, routerTransactions) + tmpErr = m.db.PutRouteData(routeData) + if tmpErr != nil { + log.Error("Error storing route data", "error", tmpErr) + } var ( chainIDs []uint64 @@ -173,13 +204,17 @@ func (m *Manager) SendRouterTransactionsWithSignatures(ctx context.Context, send addresses = append(addresses, common.Address(tx.FromAddress)) go func(chainId uint64, txHash common.Hash) { defer status_common.LogOnPanic() - err = m.transactionManager.WatchTransaction(context.Background(), chainId, txHash) - if err != nil { + tmpErr = m.transactionManager.WatchTransaction(context.Background(), chainId, txHash) + if tmpErr != nil { + logutils.ZapLogger().Error("Error watching transaction", zap.Error(tmpErr)) return } }(tx.FromChain, common.Hash(tx.Hash)) } - err = m.transferController.CheckRecentHistory(chainIDs, addresses) + tmpErr = m.transferController.CheckRecentHistory(chainIDs, addresses) + if tmpErr != nil { + logutils.ZapLogger().Error("Error checking recent history", zap.Error(tmpErr)) + } }() } diff --git a/services/wallet/routeexecution/route_db.go b/services/wallet/routeexecution/route_db.go new file mode 100644 index 000000000..7000cd275 --- /dev/null +++ b/services/wallet/routeexecution/route_db.go @@ -0,0 +1,373 @@ +package routeexecution + +import ( + "database/sql" + + sq "github.com/Masterminds/squirrel" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/services/wallet/requests" + "github.com/status-im/status-go/services/wallet/router/routes" + "github.com/status-im/status-go/sqlite" +) + +type DB struct { + db *sql.DB +} + +func NewDB(db *sql.DB) *DB { + return &DB{db: db} +} + +func (db *DB) PutRouteData(routeData *RouteData) (err error) { + var tx *sql.Tx + tx, err = db.db.Begin() + if err != nil { + return err + } + defer func() { + if err == nil { + err = tx.Commit() + return + } + _ = tx.Rollback() + }() + + if err = putRouteInputParams(tx, routeData.RouteInputParams); err != nil { + return + } + + if err = putBuildTxParams(tx, routeData.BuildInputParams); err != nil { + return + } + + if err = putPathsData(tx, routeData.RouteInputParams.Uuid, routeData.PathsData); err != nil { + return + } + + return +} + +func (db *DB) GetRouteData(uuid string) (*RouteData, error) { + return getRouteData(db.db, uuid) +} + +func putRouteInputParams(creator sqlite.StatementCreator, p *requests.RouteInputParams) error { + q := sq.Replace("route_input_parameters"). + SetMap(sq.Eq{"route_input_params_json": &sqlite.JSONBlob{Data: p}}) + + query, args, err := q.ToSql() + if err != nil { + return err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(args...) + + return err +} + +func putBuildTxParams(creator sqlite.StatementCreator, p *requests.RouterBuildTransactionsParams) error { + q := sq.Replace("route_build_tx_parameters"). + SetMap(sq.Eq{"route_build_tx_params_json": &sqlite.JSONBlob{Data: p}}) + + query, args, err := q.ToSql() + if err != nil { + return err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(args...) + + return err +} + +func putPathsData(creator sqlite.StatementCreator, uuid string, d []*PathData) error { + for i, pathData := range d { + if err := putPathData(creator, uuid, i, pathData); err != nil { + return err + } + } + return nil +} + +func putPathData(creator sqlite.StatementCreator, uuid string, pathIdx int, d *PathData) (err error) { + err = putPath(creator, uuid, pathIdx, d.Path) + if err != nil { + return + } + + for txIdx, txData := range d.TransactionsData { + err = putPathTransaction(creator, uuid, pathIdx, txIdx, txData) + if err != nil { + return + } + err = putSentTransaction(creator, txData) + if err != nil { + return + } + } + + return +} + +func putPath( + creator sqlite.StatementCreator, + uuid string, + pathIdx int, + p *routes.Path) error { + q := sq.Replace("route_paths"). + SetMap(sq.Eq{"uuid": uuid, "path_idx": pathIdx, "path_json": &sqlite.JSONBlob{Data: p}}) + + query, args, err := q.ToSql() + if err != nil { + return err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(args...) + + return err +} + +func putPathTransaction( + creator sqlite.StatementCreator, + uuid string, + pathIdx int, + txIdx int, + txData *TransactionData, +) error { + q := sq.Replace("route_path_transactions"). + SetMap(sq.Eq{ + "uuid": uuid, + "path_idx": pathIdx, + "tx_idx": txIdx, + "is_approval": txData.IsApproval, + "chain_id": txData.ChainID, + "tx_hash": txData.TxHash[:], + "tx_args_json": &sqlite.JSONBlob{Data: txData.TxArgs}, + }) + + query, args, err := q.ToSql() + if err != nil { + return err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(args...) + + return err +} + +func putSentTransaction( + creator sqlite.StatementCreator, + txData *TransactionData, +) error { + q := sq.Replace("sent_transactions"). + SetMap(sq.Eq{ + "chain_id": txData.ChainID, + "tx_hash": txData.TxHash[:], + "tx_json": &sqlite.JSONBlob{Data: txData.Tx}, + }) + + query, args, err := q.ToSql() + if err != nil { + return err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(args...) + + return err +} + +func getRouteData(creator sqlite.StatementCreator, uuid string) (*RouteData, error) { + routeInputParams, err := getRouteInputParams(creator, uuid) + if err != nil { + return nil, err + } + + buildTxParams, err := getBuildTxParams(creator, uuid) + if err != nil { + return nil, err + } + + pathsData, err := getPathsData(creator, uuid) + if err != nil { + return nil, err + } + + return &RouteData{ + RouteInputParams: routeInputParams, + BuildInputParams: buildTxParams, + PathsData: pathsData, + }, nil +} + +func getRouteInputParams(creator sqlite.StatementCreator, uuid string) (*requests.RouteInputParams, error) { + var p requests.RouteInputParams + q := sq.Select("route_input_params_json"). + From("route_input_parameters"). + Where(sq.Eq{"uuid": uuid}) + + query, args, err := q.ToSql() + if err != nil { + return nil, err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return nil, err + } + defer stmt.Close() + + err = stmt.QueryRow(args...).Scan(&sqlite.JSONBlob{Data: &p}) + return &p, err +} + +func getBuildTxParams(creator sqlite.StatementCreator, uuid string) (*requests.RouterBuildTransactionsParams, error) { + var p requests.RouterBuildTransactionsParams + q := sq.Select("route_build_tx_params_json"). + From("route_build_tx_parameters"). + Where(sq.Eq{"uuid": uuid}) + + query, args, err := q.ToSql() + if err != nil { + return nil, err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return nil, err + } + defer stmt.Close() + + err = stmt.QueryRow(args...).Scan(&sqlite.JSONBlob{Data: &p}) + return &p, err +} + +func getPathsData(creator sqlite.StatementCreator, uuid string) ([]*PathData, error) { + var pathsData []*PathData + + paths, err := getPaths(creator, uuid) + if err != nil { + return nil, err + } + + for pathIdx, path := range paths { + pathData := &PathData{Path: path} + txs, err := getPathTransactions(creator, uuid, pathIdx) + if err != nil { + return nil, err + } + pathData.TransactionsData = txs + + pathsData = append(pathsData, pathData) + } + + return pathsData, nil +} + +func getPaths(creator sqlite.StatementCreator, uuid string) ([]*routes.Path, error) { + var paths []*routes.Path + q := sq.Select("path_json"). + From("route_paths"). + Where(sq.Eq{"uuid": uuid}). + OrderBy("path_idx ASC") + + query, args, err := q.ToSql() + if err != nil { + return nil, err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return nil, err + } + defer stmt.Close() + + rows, err := stmt.Query(args...) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var p routes.Path + err = rows.Scan(&sqlite.JSONBlob{Data: &p}) + if err != nil { + return nil, err + } + paths = append(paths, &p) + } + + return paths, nil +} + +func getPathTransactions(creator sqlite.StatementCreator, uuid string, pathIdx int) ([]*TransactionData, error) { + txs := make([]*TransactionData, 0, 2) + q := sq.Select("rpt.is_approval", "rpt.chain_id", "rpt.tx_hash", "rpt.tx_args_json", "st.tx_json"). + From("route_path_transactions rpt"). + LeftJoin(`sent_transactions st ON + rpt.chain_id = st.chain_id AND + rpt.tx_hash = st.tx_hash`). + Where(sq.Eq{"rpt.uuid": uuid, "rpt.path_idx": pathIdx}). + OrderBy("rpt.tx_idx ASC") + + query, args, err := q.ToSql() + if err != nil { + return nil, err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return nil, err + } + defer stmt.Close() + + rows, err := stmt.Query(args...) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var tx TransactionData + var txHash sql.RawBytes + err = rows.Scan(&tx.IsApproval, &tx.ChainID, &txHash, &sqlite.JSONBlob{Data: &tx.TxArgs}, &sqlite.JSONBlob{Data: &tx.Tx}) + if err != nil { + return nil, err + } + if len(txHash) > 0 { + tx.TxHash = types.BytesToHash(txHash) + } + txs = append(txs, &tx) + } + + return txs, nil +} diff --git a/services/wallet/routeexecution/route_db_data_test.go b/services/wallet/routeexecution/route_db_data_test.go new file mode 100644 index 000000000..0ecc04c01 --- /dev/null +++ b/services/wallet/routeexecution/route_db_data_test.go @@ -0,0 +1,95 @@ +package routeexecution_test + +import ( + "encoding/json" + + "github.com/status-im/status-go/services/wallet/requests" + "github.com/status-im/status-go/services/wallet/transfer" +) + +type dbParams struct { + routeInputParams requests.RouteInputParams + buildInputParams *requests.RouterBuildTransactionsParams + transactionDetails []*transfer.RouterTransactionDetails +} + +type dbTestData struct { + name string + insertedParams dbParams + expectedParams dbParams +} + +func createDBParams(routeInputParamsJSON string, buildInputParamsJSON string, transactionDetailsJSON string) dbParams { + var err error + routeInputParams := requests.RouteInputParams{} + err = json.Unmarshal([]byte(routeInputParamsJSON), &routeInputParams) + if err != nil { + panic(err) + } + + buildInputParams := &requests.RouterBuildTransactionsParams{} + err = json.Unmarshal([]byte(buildInputParamsJSON), buildInputParams) + if err != nil { + panic(err) + } + + transactionDetails := make([]*transfer.RouterTransactionDetails, 0) + err = json.Unmarshal([]byte(transactionDetailsJSON), &transactionDetails) + if err != nil { + panic(err) + } + + return dbParams{ + routeInputParams: routeInputParams, + buildInputParams: buildInputParams, + transactionDetails: transactionDetails, + } +} + +func createDBTestData(name string, inserted dbParams, expected dbParams) dbTestData { + return dbTestData{ + name: name, + insertedParams: inserted, + expectedParams: expected, + } +} + +func getUSDTSwapApproveDBTestData() dbParams { + const routeInputParamsJSON = `{"uuid":"m2j0rth4gjvbr","sendType":8,"addrFrom":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","addrTo":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","amountIn":"0xc350","amountOut":"0x0","tokenID":"USDT","tokenIDIsOwnerToken":false,"toTokenID":"DAI","disabledFromChainIDs":[1,42161],"disabledToChainIDs":[1,42161],"gasFeeMode":1,"fromLockedAmount":{},"TestnetMode":false,"username":"","publicKey":"","packID":null,"TestsMode":false,"TestParams":null}` + const buildInputParamsJSON = `{"uuid":"m2j0rth4gjvbr","slippagePercentage":0.0}` + const transactionDetailsJSON = `[{"RouterPath":{"ProcessorName":"Paraswap","FromChain":{"chainId":10,"chainName":"Optimism","defaultRpcUrl":"","defaultFallbackURL":"","defaultFallbackURL2":"","rpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","originalRpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","fallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","originalFallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","blockExplorerUrl":"https://optimistic.etherscan.io","iconUrl":"network/Network=Optimism","nativeCurrencyName":"Ether","nativeCurrencySymbol":"ETH","nativeCurrencyDecimals":18,"isTest":false,"layer":2,"enabled":true,"chainColor":"#E90101","shortName":"oeth","tokenOverrides":null,"relatedChainId":420},"ToChain":{"chainId":10,"chainName":"Optimism","defaultRpcUrl":"","defaultFallbackURL":"","defaultFallbackURL2":"","rpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","originalRpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","fallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","originalFallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","blockExplorerUrl":"https://optimistic.etherscan.io","iconUrl":"network/Network=Optimism","nativeCurrencyName":"Ether","nativeCurrencySymbol":"ETH","nativeCurrencyDecimals":18,"isTest":false,"layer":2,"enabled":true,"chainColor":"#E90101","shortName":"oeth","tokenOverrides":null,"relatedChainId":420},"FromToken":{"address":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","name":"Tether USD","symbol":"USDT","decimals":6,"chainId":10,"pegSymbol":"","verified":true,"tokenListId":"uniswap"},"ToToken":{"address":"0xda10009cbd5d07dd0cecc66161fc93d7c9000da1","name":"Dai Stablecoin","symbol":"DAI","decimals":18,"chainId":10,"pegSymbol":"","verified":true,"tokenListId":"uniswap"},"AmountIn":"0xc350","AmountInLocked":false,"AmountOut":"0xb05244c6f80a5d","SuggestedLevelsForMaxFeesPerGas":{"low":"0xf7bb4","medium":"0xfb528","high":"0xfee9c"},"MaxFeesPerGas":"0xfb528","TxBaseFee":"0x3974","TxPriorityFee":"0xf4240","TxGasAmount":130200,"TxBonderFees":"0x0","TxTokenFees":"0x0","TxFee":"0x1f34ceefc0","TxL1Fee":"0x0","ApprovalRequired":true,"ApprovalAmountRequired":"0xc350","ApprovalContractAddress":"0x6a000f20005980200259b80c5102003040001068","ApprovalBaseFee":"0x3974","ApprovalPriorityFee":"0xf4240","ApprovalGasAmount":46692,"ApprovalFee":"0xb30ed33a0","ApprovalL1Fee":"0x152b221b6e000","TxTotalFee":"0x152dc87730360","EstimatedTime":2,"RequiredTokenBalance":50000,"RequiredNativeBalance":372582095455072,"SubtractFees":false},"TxArgs":null,"Tx":null,"TxHashToSign":"0x0000000000000000000000000000000000000000000000000000000000000000","TxSignature":null,"TxSentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","ApprovalTxArgs":{"version":1,"from":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","to":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","gas":"0xb664","gasPrice":null,"value":"0x0","nonce":null,"maxFeePerGas":"0xfb528","maxPriorityFeePerGas":"0xf4240","input":"0x","data":"0x095ea7b30000000000000000000000006a000f20005980200259b80c5102003040001068000000000000000000000000000000000000000000000000000000000000c350","multiTransactionID":0},"ApprovalTx":{"type":"0x2","nonce":"0x73","gasPrice":"0x0","maxPriorityFeePerGas":"0xf4240","maxFeePerGas":"0xfb528","gas":"0xb664","value":"0x0","input":"0x095ea7b30000000000000000000000006a000f20005980200259b80c5102003040001068000000000000000000000000000000000000000000000000000000000000c350","v":"0x0","r":"0x0","s":"0x0","to":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","chainId":"0x0","accessList":[],"hash":"0xaeac1fd8ef3517e8d42f2ab33680a6c61e2f170060529b21cdadd7201c36a974"},"ApprovalHashToSign":"0x210539bb33c1e2307eed92e1ec56b1e2a94edc4bb091bc8363391166d83577e3","ApprovalSignature":"MRGSrtVUixPRcdpDRvszuZOL+ZjB8dPZVOZDa1EHLHEQvEc+2irHFbwZ4SqHJCjulsT0fPiPUFPS5H16Aah4zAE=","ApprovalTxSentHash":"0xdfdc585b2bb187a715925d3375707b4a2e76ce9d02d074566a480410cd87400f"}]` + + return createDBParams(routeInputParamsJSON, buildInputParamsJSON, transactionDetailsJSON) +} + +func getUSDTSwapTxDBTestData() dbParams { + const routeInputParamsJSON = `{"uuid":"m2j0rth4gjvbr","sendType":8,"addrFrom":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","addrTo":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","amountIn":"0xc350","amountOut":"0x0","tokenID":"USDT","tokenIDIsOwnerToken":false,"toTokenID":"DAI","disabledFromChainIDs":[1,42161],"disabledToChainIDs":[1,42161],"gasFeeMode":1,"fromLockedAmount":{},"TestnetMode":false,"username":"","publicKey":"","packID":null,"TestsMode":false,"TestParams":null}` + const buildInputParamsJSON = `{"uuid":"m2j0rth4gjvbr","slippagePercentage":0.5}` + const transactionDetailsJSON = `[{"RouterPath":{"ProcessorName":"Paraswap","FromChain":{"chainId":10,"chainName":"Optimism","defaultRpcUrl":"","defaultFallbackURL":"","defaultFallbackURL2":"","rpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","originalRpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","fallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","originalFallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","blockExplorerUrl":"https://optimistic.etherscan.io","iconUrl":"network/Network=Optimism","nativeCurrencyName":"Ether","nativeCurrencySymbol":"ETH","nativeCurrencyDecimals":18,"isTest":false,"layer":2,"enabled":true,"chainColor":"#E90101","shortName":"oeth","tokenOverrides":null,"relatedChainId":420},"ToChain":{"chainId":10,"chainName":"Optimism","defaultRpcUrl":"","defaultFallbackURL":"","defaultFallbackURL2":"","rpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","originalRpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","fallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","originalFallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","blockExplorerUrl":"https://optimistic.etherscan.io","iconUrl":"network/Network=Optimism","nativeCurrencyName":"Ether","nativeCurrencySymbol":"ETH","nativeCurrencyDecimals":18,"isTest":false,"layer":2,"enabled":true,"chainColor":"#E90101","shortName":"oeth","tokenOverrides":null,"relatedChainId":420},"FromToken":{"address":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","name":"Tether USD","symbol":"USDT","decimals":6,"chainId":10,"pegSymbol":"","verified":true,"tokenListId":"uniswap"},"ToToken":{"address":"0xda10009cbd5d07dd0cecc66161fc93d7c9000da1","name":"Dai Stablecoin","symbol":"DAI","decimals":18,"chainId":10,"pegSymbol":"","verified":true,"tokenListId":"uniswap"},"AmountIn":"0xc350","AmountInLocked":false,"AmountOut":"0xb05244c6f80a5d","SuggestedLevelsForMaxFeesPerGas":{"low":"0xf7bb4","medium":"0xfb528","high":"0xfee9c"},"MaxFeesPerGas":"0xfb528","TxBaseFee":"0x3974","TxPriorityFee":"0xf4240","TxGasAmount":130200,"TxBonderFees":"0x0","TxTokenFees":"0x0","TxFee":"0x1f34ceefc0","TxL1Fee":"0x0","ApprovalRequired":true,"ApprovalAmountRequired":"0xc350","ApprovalContractAddress":"0x6a000f20005980200259b80c5102003040001068","ApprovalBaseFee":"0x3974","ApprovalPriorityFee":"0xf4240","ApprovalGasAmount":46692,"ApprovalFee":"0xb30ed33a0","ApprovalL1Fee":"0x152b221b6e000","TxTotalFee":"0x152dc87730360","EstimatedTime":2,"RequiredTokenBalance":50000,"RequiredNativeBalance":372582095455072,"SubtractFees":false},"TxArgs":null,"Tx":null,"TxHashToSign":"0x0000000000000000000000000000000000000000000000000000000000000000","TxSignature":null,"TxSentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","ApprovalTxArgs":{"version":1,"from":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","to":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","gas":"0xb664","gasPrice":null,"value":"0x0","nonce":null,"maxFeePerGas":"0xfb528","maxPriorityFeePerGas":"0xf4240","input":"0x","data":"0x095ea7b30000000000000000000000006a000f20005980200259b80c5102003040001068000000000000000000000000000000000000000000000000000000000000c350","multiTransactionID":0},"ApprovalTx":{"type":"0x2","nonce":"0x73","gasPrice":"0x0","maxPriorityFeePerGas":"0xf4240","maxFeePerGas":"0xfb528","gas":"0xb664","value":"0x0","input":"0x095ea7b30000000000000000000000006a000f20005980200259b80c5102003040001068000000000000000000000000000000000000000000000000000000000000c350","v":"0x0","r":"0x0","s":"0x0","to":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","chainId":"0x0","accessList":[],"hash":"0xaeac1fd8ef3517e8d42f2ab33680a6c61e2f170060529b21cdadd7201c36a974"},"ApprovalHashToSign":"0x210539bb33c1e2307eed92e1ec56b1e2a94edc4bb091bc8363391166d83577e3","ApprovalSignature":"MRGSrtVUixPRcdpDRvszuZOL+ZjB8dPZVOZDa1EHLHEQvEc+2irHFbwZ4SqHJCjulsT0fPiPUFPS5H16Aah4zAE=","ApprovalTxSentHash":"0xdfdc585b2bb187a715925d3375707b4a2e76ce9d02d074566a480410cd87400f"},{"RouterPath":{"ProcessorName":"Paraswap","FromChain":{"chainId":10,"chainName":"Optimism","defaultRpcUrl":"","defaultFallbackURL":"","defaultFallbackURL2":"","rpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","originalRpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","fallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","originalFallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","blockExplorerUrl":"https://optimistic.etherscan.io","iconUrl":"network/Network=Optimism","nativeCurrencyName":"Ether","nativeCurrencySymbol":"ETH","nativeCurrencyDecimals":18,"isTest":false,"layer":2,"enabled":true,"chainColor":"#E90101","shortName":"oeth","tokenOverrides":null,"relatedChainId":420},"ToChain":{"chainId":10,"chainName":"Optimism","defaultRpcUrl":"","defaultFallbackURL":"","defaultFallbackURL2":"","rpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","originalRpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","fallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","originalFallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","blockExplorerUrl":"https://optimistic.etherscan.io","iconUrl":"network/Network=Optimism","nativeCurrencyName":"Ether","nativeCurrencySymbol":"ETH","nativeCurrencyDecimals":18,"isTest":false,"layer":2,"enabled":true,"chainColor":"#E90101","shortName":"oeth","tokenOverrides":null,"relatedChainId":420},"FromToken":{"address":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","name":"Tether USD","symbol":"USDT","decimals":6,"chainId":10,"pegSymbol":"","verified":true,"tokenListId":"uniswap"},"ToToken":{"address":"0xda10009cbd5d07dd0cecc66161fc93d7c9000da1","name":"Dai Stablecoin","symbol":"DAI","decimals":18,"chainId":10,"pegSymbol":"","verified":true,"tokenListId":"uniswap"},"AmountIn":"0xc350","AmountInLocked":false,"AmountOut":"0xb05244c6f80a5d","SuggestedLevelsForMaxFeesPerGas":{"low":"0xf7882","medium":"0xfaec4","high":"0xfe506"},"MaxFeesPerGas":"0xfaec4","TxBaseFee":"0x3642","TxPriorityFee":"0xf4240","TxGasAmount":130200,"TxBonderFees":"0x0","TxTokenFees":"0x0","TxFee":"0x1f281cb460","TxL1Fee":"0x0","ApprovalRequired":true,"ApprovalAmountRequired":"0xc350","ApprovalContractAddress":"0x6a000f20005980200259b80c5102003040001068","ApprovalBaseFee":"0x3642","ApprovalPriorityFee":"0xf4240","ApprovalGasAmount":46692,"ApprovalFee":"0xb2c5f9c90","ApprovalL1Fee":"0x152b221b6e000","TxTotalFee":"0x152dc763330f0","EstimatedTime":2,"RequiredTokenBalance":50000,"RequiredNativeBalance":372581806059760,"SubtractFees":false},"TxArgs":{"version":1,"from":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","to":"0x6a000f20005980200259b80c5102003040001068","gas":"0x2e06d","gasPrice":"0x110a6d","value":"0x0","nonce":null,"maxFeePerGas":"0xfaec4","maxPriorityFeePerGas":"0xf4240","input":"0x","data":"0x876a02f60000000000000000000000000000000000000000000000000000000000000060e9b59dc0b30cd4646430c25de0111d651c39577510000000000000000000004600000000000000000000000000000000000000000000000000000000000001e000000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e58000000000000000000000000da10009cbd5d07dd0cecc66161fc93d7c9000da1000000000000000000000000000000000000000000000000000000000000c35000000000000000000000000000000000000000000000000000b0ad2e7c9b36fc00000000000000000000000000000000000000000000000000b19076c2b3268926425462ea4c4b87aab666325ed87fbd00000000000000000000000007913ac400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000006080000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e58000000000000000000000000da10009cbd5d07dd0cecc66161fc93d7c9000da100000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000","multiTransactionID":0},"Tx":{"type":"0x2","nonce":"0x74","gasPrice":"0x0","maxPriorityFeePerGas":"0xf4240","maxFeePerGas":"0xfaec4","gas":"0x2e06d","value":"0x0","input":"0x876a02f60000000000000000000000000000000000000000000000000000000000000060e9b59dc0b30cd4646430c25de0111d651c39577510000000000000000000004600000000000000000000000000000000000000000000000000000000000001e000000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e58000000000000000000000000da10009cbd5d07dd0cecc66161fc93d7c9000da1000000000000000000000000000000000000000000000000000000000000c35000000000000000000000000000000000000000000000000000b0ad2e7c9b36fc00000000000000000000000000000000000000000000000000b19076c2b3268926425462ea4c4b87aab666325ed87fbd00000000000000000000000007913ac400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000006080000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e58000000000000000000000000da10009cbd5d07dd0cecc66161fc93d7c9000da100000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000","v":"0x0","r":"0x0","s":"0x0","to":"0x6a000f20005980200259b80c5102003040001068","chainId":"0x0","accessList":[],"hash":"0xf9c6d9df3745fbf38f1f425aec45e31310992ed24a06323b797fbf11fbedf410"},"TxHashToSign":"0x186fc84a40e3991f602fd77ddf442959782b7005f337127359d90f9d1b9b075a","TxSignature":"r5NomdXT/twEeUnp/VQDqiRv1VSGE2NiZwUuouLhfXFz6QWw7auUlaSRR3oUakkkhDywSxT0u9DQreIrHafiNwA=","TxSentHash":"0x872ae7278767d5fa60adf2f443c89dcafc562825d534dc50503278ac17013b77","ApprovalTxArgs":null,"ApprovalTx":null,"ApprovalHashToSign":"0x0000000000000000000000000000000000000000000000000000000000000000","ApprovalSignature":null,"ApprovalTxSentHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}]` + + return createDBParams(routeInputParamsJSON, buildInputParamsJSON, transactionDetailsJSON) +} + +func getETHSwapTxDBTestData() dbParams { + const routeInputParamsJSON = `{"uuid":"m2j0uqdb367ty","sendType":8,"addrFrom":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","addrTo":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","amountIn":"0x5af3107a4000","amountOut":"0x0","tokenID":"ETH","tokenIDIsOwnerToken":false,"toTokenID":"DAI","disabledFromChainIDs":[1,10],"disabledToChainIDs":[1,10],"gasFeeMode":1,"fromLockedAmount":{},"TestnetMode":false,"username":"","publicKey":"","packID":null,"TestsMode":false,"TestParams":null}` + const buildInputParamsJSON = `{"uuid":"m2j0uqdb367ty","slippagePercentage":0.5}` + const transactionDetailsJSON = `[{"RouterPath":{"ProcessorName":"Paraswap","FromChain":{"chainId":42161,"chainName":"Arbitrum","defaultRpcUrl":"","defaultFallbackURL":"","defaultFallbackURL2":"","rpcUrl":"https://arbitrum-one.rpc.grove.city/v1/39d9bfd5","originalRpcUrl":"https://arbitrum-one.rpc.grove.city/v1/39d9bfd5","fallbackURL":"https://arbitrum-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","originalFallbackURL":"https://arbitrum-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","blockExplorerUrl":"https://arbiscan.io/","iconUrl":"network/Network=Arbitrum","nativeCurrencyName":"Ether","nativeCurrencySymbol":"ETH","nativeCurrencyDecimals":18,"isTest":false,"layer":2,"enabled":true,"chainColor":"#51D0F0","shortName":"arb1","tokenOverrides":null,"relatedChainId":421613},"ToChain":{"chainId":42161,"chainName":"Arbitrum","defaultRpcUrl":"","defaultFallbackURL":"","defaultFallbackURL2":"","rpcUrl":"https://arbitrum-one.rpc.grove.city/v1/39d9bfd5","originalRpcUrl":"https://arbitrum-one.rpc.grove.city/v1/39d9bfd5","fallbackURL":"https://arbitrum-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","originalFallbackURL":"https://arbitrum-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","blockExplorerUrl":"https://arbiscan.io/","iconUrl":"network/Network=Arbitrum","nativeCurrencyName":"Ether","nativeCurrencySymbol":"ETH","nativeCurrencyDecimals":18,"isTest":false,"layer":2,"enabled":true,"chainColor":"#51D0F0","shortName":"arb1","tokenOverrides":null,"relatedChainId":421613},"FromToken":{"address":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","name":"Ether","symbol":"ETH","decimals":18,"chainId":42161,"pegSymbol":"","verified":true,"tokenListId":""},"ToToken":{"address":"0xda10009cbd5d07dd0cecc66161fc93d7c9000da1","name":"DAI Stablecoin","symbol":"DAI","decimals":18,"chainId":42161,"pegSymbol":"","verified":true,"tokenListId":"status"},"AmountIn":"0x5af3107a4000","AmountInLocked":false,"AmountOut":"0x3bb9301e1ee5f34","SuggestedLevelsForMaxFeesPerGas":{"low":"0x8583b1","medium":"0x10b0762","high":"0x1908b13"},"MaxFeesPerGas":"0x10b0762","TxBaseFee":"0x8583b1","TxPriorityFee":"0x0","TxGasAmount":118470,"TxBonderFees":"0x0","TxTokenFees":"0x0","TxFee":"0x1e2b5da91cc","TxL1Fee":"0x0","ApprovalRequired":false,"ApprovalAmountRequired":null,"ApprovalContractAddress":"0x6a000f20005980200259b80c5102003040001068","ApprovalBaseFee":"0x8583b1","ApprovalPriorityFee":"0x0","ApprovalGasAmount":0,"ApprovalFee":"0x0","ApprovalL1Fee":"0x0","TxTotalFee":"0x1e2b5da91cc","EstimatedTime":1,"RequiredTokenBalance":0,"RequiredNativeBalance":102073225236940,"SubtractFees":false},"TxArgs":{"version":1,"from":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","to":"0x6a000f20005980200259b80c5102003040001068","gas":"0x5cc06","gasPrice":"0x6acfc00","value":"0x5af3107a4000","nonce":null,"maxFeePerGas":"0x10b0762","maxPriorityFeePerGas":"0x0","input":"0x","data":"0xe8bb3b6c00000000000000000000000000000000000000000000000000000000000000609a8278e856c0b191b9daa2d7dd1f7b28268e4da210000000000000000000004600000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000da10009cbd5d07dd0cecc66161fc93d7c9000da100000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000003bd7fb5d57ef6fc00000000000000000000000000000000000000000000000003c24f77b668726fcb25403cb7ad4e42999a1446a005fab30000000000000000000000000fdced2d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004082af49447d8a07e3bd95bd0d56f35241523fbab1da10009cbd5d07dd0cecc66161fc93d7c9000da10000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000","multiTransactionID":0},"Tx":{"type":"0x2","nonce":"0x11","gasPrice":"0x0","maxPriorityFeePerGas":"0x0","maxFeePerGas":"0x10b0762","gas":"0x5cc06","value":"0x5af3107a4000","input":"0xe8bb3b6c00000000000000000000000000000000000000000000000000000000000000609a8278e856c0b191b9daa2d7dd1f7b28268e4da210000000000000000000004600000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000da10009cbd5d07dd0cecc66161fc93d7c9000da100000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000003bd7fb5d57ef6fc00000000000000000000000000000000000000000000000003c24f77b668726fcb25403cb7ad4e42999a1446a005fab30000000000000000000000000fdced2d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004082af49447d8a07e3bd95bd0d56f35241523fbab1da10009cbd5d07dd0cecc66161fc93d7c9000da10000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000","v":"0x0","r":"0x0","s":"0x0","to":"0x6a000f20005980200259b80c5102003040001068","chainId":"0x0","accessList":[],"hash":"0x1f096e353a671a7afc82cbad894b76b074b547e165f800a4e31632da7ae0d059"},"TxHashToSign":"0x259bcb36d9267c0f9d2633513024c853162d02d3ba021db2e6b0bde016a7e2a2","TxSignature":"C+EFcJxhD3EqjG3s97PnqX+ilF3E1it/BsRG655KtdJT3VJ13tDJ2RUPlp0/ltrRMbShlzPG05j+YSNSCEQorAA=","TxSentHash":"0x68a1b312c0091c382847e08d91f9c2abcd2e8b7db0f1cc039a935166fc22d92c","ApprovalTxArgs":null,"ApprovalTx":null,"ApprovalHashToSign":"0x0000000000000000000000000000000000000000000000000000000000000000","ApprovalSignature":null,"ApprovalTxSentHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}]` + + return createDBParams(routeInputParamsJSON, buildInputParamsJSON, transactionDetailsJSON) +} + +func getETHBridgeTxDBTestData() dbParams { + const routeInputParamsJSON = `{"uuid":"m2j0w28oxvxth","sendType":5,"addrFrom":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","addrTo":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","amountIn":"0x5af3107a4000","amountOut":"0x0","tokenID":"ETH","tokenIDIsOwnerToken":false,"toTokenID":"","disabledFromChainIDs":[],"disabledToChainIDs":[1,42161],"gasFeeMode":1,"fromLockedAmount":{},"TestnetMode":false,"username":"","publicKey":"","packID":null,"TestsMode":false,"TestParams":null}` + const buildInputParamsJSON = `{"uuid":"m2j0w28oxvxth","slippagePercentage":0}` + const transactionDetailsJSON = `[{"RouterPath":{"ProcessorName":"Hop","FromChain":{"chainId":42161,"chainName":"Arbitrum","defaultRpcUrl":"","defaultFallbackURL":"","defaultFallbackURL2":"","rpcUrl":"https://arbitrum-one.rpc.grove.city/v1/39d9bfd5","originalRpcUrl":"https://arbitrum-one.rpc.grove.city/v1/39d9bfd5","fallbackURL":"https://arbitrum-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","originalFallbackURL":"https://arbitrum-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","blockExplorerUrl":"https://arbiscan.io/","iconUrl":"network/Network=Arbitrum","nativeCurrencyName":"Ether","nativeCurrencySymbol":"ETH","nativeCurrencyDecimals":18,"isTest":false,"layer":2,"enabled":true,"chainColor":"#51D0F0","shortName":"arb1","tokenOverrides":null,"relatedChainId":421613},"ToChain":{"chainId":10,"chainName":"Optimism","defaultRpcUrl":"","defaultFallbackURL":"","defaultFallbackURL2":"","rpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","originalRpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","fallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","originalFallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","blockExplorerUrl":"https://optimistic.etherscan.io","iconUrl":"network/Network=Optimism","nativeCurrencyName":"Ether","nativeCurrencySymbol":"ETH","nativeCurrencyDecimals":18,"isTest":false,"layer":2,"enabled":true,"chainColor":"#E90101","shortName":"oeth","tokenOverrides":null,"relatedChainId":420},"FromToken":{"address":"0x0000000000000000000000000000000000000000","name":"Ether","symbol":"ETH","decimals":18,"chainId":42161,"pegSymbol":"","verified":true,"tokenListId":""},"ToToken":null,"AmountIn":"0x5af3107a4000","AmountInLocked":false,"AmountOut":"0x5a6b7ed63256","SuggestedLevelsForMaxFeesPerGas":{"low":"0x8583b1","medium":"0x10b0762","high":"0x1908b13"},"MaxFeesPerGas":"0x10b0762","TxBaseFee":"0x8583b1","TxPriorityFee":"0x0","TxGasAmount":444980,"TxBonderFees":"0x550e95ee4238","TxTokenFees":"0x552c82d806ad","TxFee":"0x715165cd3e8","TxL1Fee":"0x0","ApprovalRequired":false,"ApprovalAmountRequired":null,"ApprovalContractAddress":"0x33ceb27b39d2bb7d2e61f7564d3df29344020417","ApprovalBaseFee":"0x8583b1","ApprovalPriorityFee":"0x0","ApprovalGasAmount":0,"ApprovalFee":"0x0","ApprovalL1Fee":"0x0","TxTotalFee":"0x715165cd3e8","EstimatedTime":1,"RequiredTokenBalance":0,"RequiredNativeBalance":107787150889960,"SubtractFees":false},"TxArgs":{"version":1,"from":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","to":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","gas":"0x6ca34","gasPrice":null,"value":"0x5af3107a4000","nonce":"0x12","maxFeePerGas":"0x10b0762","maxPriorityFeePerGas":"0x0","input":"0x","data":"0xeea0d7b2000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000a1e277ea6b97effc5b61b3bf5de03f438981247e00000000000000000000000000000000000000000000000000005af3107a40000000000000000000000000000000000000000000000000000000550e95ee423800000000000000000000000000000000000000000000000000005a6b7ed6325600000000000000000000000000000000000000000000000000000000671f8a99000000000000000000000000000000000000000000000000000005bf2915e48e00000000000000000000000000000000000000000000000000000000671f8a99","multiTransactionID":0},"Tx":{"type":"0x2","nonce":"0x12","gasPrice":"0x0","maxPriorityFeePerGas":"0x0","maxFeePerGas":"0x10b0762","gas":"0x6ca34","value":"0x5af3107a4000","input":"0xeea0d7b2000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000a1e277ea6b97effc5b61b3bf5de03f438981247e00000000000000000000000000000000000000000000000000005af3107a40000000000000000000000000000000000000000000000000000000550e95ee423800000000000000000000000000000000000000000000000000005a6b7ed6325600000000000000000000000000000000000000000000000000000000671f8a99000000000000000000000000000000000000000000000000000005bf2915e48e00000000000000000000000000000000000000000000000000000000671f8a99","v":"0x0","r":"0x0","s":"0x0","to":"0x33ceb27b39d2bb7d2e61f7564d3df29344020417","chainId":"0x0","accessList":[],"hash":"0x60b9ea1ffd1927fc0373a34621ce3e161fd2e84ef348c56c6c11fb1e25de1309"},"TxHashToSign":"0xe925b9dabf1a310826ded9034a87e4738d0fa949848d4bd77ec7369b58a7914d","TxSignature":"2NevbNrf5ktHaMzAR2DWZrT6XZGoCpUNfQLUuaaDp9UP2budZYtnEPcLtKxNFXV5cgTxH95TNmDa/kR6AzvRuAE=","TxSentHash":"0xe369ab560ba34612021111e2723f50f5620782e653bfbce69003288cc8625112","ApprovalTxArgs":null,"ApprovalTx":null,"ApprovalHashToSign":"0x0000000000000000000000000000000000000000000000000000000000000000","ApprovalSignature":null,"ApprovalTxSentHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}]` + + return createDBParams(routeInputParamsJSON, buildInputParamsJSON, transactionDetailsJSON) +} + +func getUSDTSendTxDBTestData() dbParams { + const routeInputParamsJSON = `{"uuid":"m2f4gdaecu078","sendType":0,"addrFrom":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","addrTo":"0x0adb6caa256a5375c638c00e2ff80a9ac1b2d3a7","amountIn":"0xc350","amountOut":"0x0","tokenID":"USDT","tokenIDIsOwnerToken":false,"toTokenID":"","disabledFromChainIDs":[1,42161],"disabledToChainIDs":[1,42161],"gasFeeMode":1,"fromLockedAmount":{},"TestnetMode":false,"username":"","publicKey":"","packID":null,"TestsMode":false,"TestParams":null}` + const buildInputParamsJSON = `{"uuid":"m2f4gdaecu078","slippagePercentage":0}` + const transactionDetailsJSON = `[{"RouterPath":{"ProcessorName":"Transfer","FromChain":{"chainId":10,"chainName":"Optimism","defaultRpcUrl":"","defaultFallbackURL":"","defaultFallbackURL2":"","rpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","originalRpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","fallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","originalFallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","blockExplorerUrl":"https://optimistic.etherscan.io","iconUrl":"network/Network=Optimism","nativeCurrencyName":"Ether","nativeCurrencySymbol":"ETH","nativeCurrencyDecimals":18,"isTest":false,"layer":2,"enabled":true,"chainColor":"#E90101","shortName":"oeth","tokenOverrides":null,"relatedChainId":420},"ToChain":{"chainId":10,"chainName":"Optimism","defaultRpcUrl":"","defaultFallbackURL":"","defaultFallbackURL2":"","rpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","originalRpcUrl":"https://optimism-archival.rpc.grove.city/v1/39d9bfd5","fallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","originalFallbackURL":"https://optimism-mainnet.infura.io/v3/36835196264a4ea6bf73a25445cd19e7","blockExplorerUrl":"https://optimistic.etherscan.io","iconUrl":"network/Network=Optimism","nativeCurrencyName":"Ether","nativeCurrencySymbol":"ETH","nativeCurrencyDecimals":18,"isTest":false,"layer":2,"enabled":true,"chainColor":"#E90101","shortName":"oeth","tokenOverrides":null,"relatedChainId":420},"FromToken":{"address":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","name":"Tether USD","symbol":"USDT","decimals":6,"chainId":10,"pegSymbol":"","verified":true,"tokenListId":"uniswap"},"ToToken":null,"AmountIn":"0xc350","AmountInLocked":false,"AmountOut":"0xc350","SuggestedLevelsForMaxFeesPerGas":{"low":"0xb5d34a","medium":"0x15c6454","high":"0x202f55e"},"MaxFeesPerGas":"0x15c6454","TxBaseFee":"0xa6910a","TxPriorityFee":"0xf4240","TxGasAmount":57442,"TxBonderFees":"0x0","TxTokenFees":"0x0","TxFee":"0x1315d27e828","TxL1Fee":"0x0","ApprovalRequired":false,"ApprovalAmountRequired":null,"ApprovalContractAddress":"0x0000000000000000000000000000000000000000","ApprovalBaseFee":"0xa6910a","ApprovalPriorityFee":"0xf4240","ApprovalGasAmount":0,"ApprovalFee":"0x0","ApprovalL1Fee":"0x0","TxTotalFee":"0x1315d27e828","EstimatedTime":1,"RequiredTokenBalance":50000,"RequiredNativeBalance":1311527921704,"SubtractFees":false},"TransactionsData":[{"ChainID":10,"TxHash":"0x1094c3431413f664b756ec87917250f8e3ec0913117003d0a3881236b1e7ec4a","IsApproval":false,"TxArgs":{"version":1,"from":"0xa1e277ea6b97effc5b61b3bf5de03f438981247e","to":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","gas":"0xe062","gasPrice":null,"value":"0x0","nonce":null,"maxFeePerGas":"0x15c6454","maxPriorityFeePerGas":"0xf4240","input":"0x","data":"0xa9059cbb0000000000000000000000000adb6caa256a5375c638c00e2ff80a9ac1b2d3a7000000000000000000000000000000000000000000000000000000000000c350","multiTransactionID":0},"Tx":{"type":"0x2","nonce":"0x6d","gasPrice":"0x0","maxPriorityFeePerGas":"0xf4240","maxFeePerGas":"0x15c6454","gas":"0xe062","value":"0x0","input":"0xa9059cbb0000000000000000000000000adb6caa256a5375c638c00e2ff80a9ac1b2d3a7000000000000000000000000000000000000000000000000000000000000c350","v":"0x0","r":"0x0","s":"0x0","to":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","chainId":"0x0","accessList":[],"hash":"0x0bedf47185c33665be64cbccb7b9fe2a6665f1335e4015f5344015a8d59ca4ef"}}]}]` + + return createDBParams(routeInputParamsJSON, buildInputParamsJSON, transactionDetailsJSON) +} diff --git a/services/wallet/routeexecution/route_db_test.go b/services/wallet/routeexecution/route_db_test.go new file mode 100644 index 000000000..0403e7fb0 --- /dev/null +++ b/services/wallet/routeexecution/route_db_test.go @@ -0,0 +1,48 @@ +package routeexecution_test + +import ( + "testing" + + "github.com/status-im/status-go/services/wallet/routeexecution" + "github.com/status-im/status-go/t/helpers" + "github.com/status-im/status-go/walletdatabase" + + "github.com/stretchr/testify/require" +) + +func Test_PutRouteData(t *testing.T) { + testData := []dbTestData{ + createDBTestData("USDTSwapApprove", getUSDTSwapApproveDBTestData(), getUSDTSwapTxDBTestData()), // After placing the Swap Tx, we expect to get info for both txs + createDBTestData("USDTSwapTx", getUSDTSwapTxDBTestData(), getUSDTSwapTxDBTestData()), + createDBTestData("ETHSwapTx", getETHSwapTxDBTestData(), getETHSwapTxDBTestData()), + createDBTestData("ETHBridgeTx", getETHBridgeTxDBTestData(), getETHBridgeTxDBTestData()), + createDBTestData("USDTSendTx", getUSDTSendTxDBTestData(), getUSDTSendTxDBTestData()), + } + + walletDB, closeFn, err := helpers.SetupTestSQLDB(walletdatabase.DbInitializer{}, "routeexecution-tests") + require.NoError(t, err) + defer func() { + require.NoError(t, closeFn()) + }() + + routeDB := routeexecution.NewDB(walletDB) + + for _, tt := range testData { + t.Run("Put_"+tt.name, func(t *testing.T) { + insertedParams := tt.insertedParams + routeData := routeexecution.NewRouteData(&insertedParams.routeInputParams, insertedParams.buildInputParams, insertedParams.transactionDetails) + err := routeDB.PutRouteData(routeData) + require.NoError(t, err) + }) + } + + for _, tt := range testData { + t.Run("Get_"+tt.name, func(t *testing.T) { + expectedParams := tt.expectedParams + routeData := routeexecution.NewRouteData(&expectedParams.routeInputParams, expectedParams.buildInputParams, expectedParams.transactionDetails) + readRouteData, err := routeDB.GetRouteData(routeData.RouteInputParams.Uuid) + require.NoError(t, err) + require.EqualExportedValues(t, routeData, readRouteData) + }) + } +} diff --git a/services/wallet/routeexecution/route_db_types.go b/services/wallet/routeexecution/route_db_types.go new file mode 100644 index 000000000..e486c3edc --- /dev/null +++ b/services/wallet/routeexecution/route_db_types.go @@ -0,0 +1,78 @@ +package routeexecution + +import ( + ethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/services/wallet/requests" + "github.com/status-im/status-go/services/wallet/router/routes" + "github.com/status-im/status-go/services/wallet/transfer" + "github.com/status-im/status-go/transactions" +) + +// These structs oontain all route execution data +// that's stored to the DB +type RouteData struct { + RouteInputParams *requests.RouteInputParams + BuildInputParams *requests.RouterBuildTransactionsParams + PathsData []*PathData +} + +type PathData struct { + Path *routes.Path + TransactionsData []*TransactionData +} + +type TransactionData struct { + ChainID uint64 + TxHash types.Hash + IsApproval bool + TxArgs *transactions.SendTxArgs + Tx *ethTypes.Transaction +} + +func NewRouteData(routeInputParams *requests.RouteInputParams, + buildInputParams *requests.RouterBuildTransactionsParams, + transactionDetails []*transfer.RouterTransactionDetails) *RouteData { + + pathDataPerProcessorName := make(map[string]*PathData) + pathsData := make([]*PathData, 0, len(transactionDetails)) + for _, td := range transactionDetails { + transactionsData := make([]*TransactionData, 0, 2) + if td.IsApprovalPlaced() { + transactionsData = append(transactionsData, &TransactionData{ + ChainID: td.RouterPath.FromChain.ChainID, + TxHash: td.ApprovalTxSentHash, + IsApproval: true, + TxArgs: td.ApprovalTxArgs, + Tx: td.ApprovalTx, + }) + } + if td.IsTxPlaced() { + transactionsData = append(transactionsData, &TransactionData{ + ChainID: td.RouterPath.FromChain.ChainID, + TxHash: td.TxSentHash, + IsApproval: false, + TxArgs: td.TxArgs, + Tx: td.Tx, + }) + } + + var pathData *PathData + var ok bool + if pathData, ok = pathDataPerProcessorName[td.RouterPath.ProcessorName]; !ok { + pathData = &PathData{ + Path: td.RouterPath, + TransactionsData: make([]*TransactionData, 0, 2), + } + pathsData = append(pathsData, pathData) + pathDataPerProcessorName[td.RouterPath.ProcessorName] = pathData + } + pathData.TransactionsData = append(pathData.TransactionsData, transactionsData...) + } + + return &RouteData{ + RouteInputParams: routeInputParams, + BuildInputParams: buildInputParams, + PathsData: pathsData, + } +} diff --git a/services/wallet/service.go b/services/wallet/service.go index 0148a04f5..18582ac78 100644 --- a/services/wallet/service.go +++ b/services/wallet/service.go @@ -196,7 +196,7 @@ func NewService( router.AddPathProcessor(processor) } - routeExecutionManager := routeexecution.NewManager(router, transactionManager, transferController) + routeExecutionManager := routeexecution.NewManager(db, router, transactionManager, transferController) return &Service{ db: db, diff --git a/services/wallet/transfer/transaction_manager.go b/services/wallet/transfer/transaction_manager.go index 9c77285ed..08fb78ded 100644 --- a/services/wallet/transfer/transaction_manager.go +++ b/services/wallet/transfer/transaction_manager.go @@ -44,17 +44,25 @@ type TransactionDescription struct { } 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 + 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 +} + +func (rtd *RouterTransactionDetails) IsTxPlaced() bool { + return rtd.TxSentHash != types.Hash(wallet_common.ZeroHash()) +} + +func (rtd *RouterTransactionDetails) IsApprovalPlaced() bool { + return rtd.ApprovalTxSentHash != types.Hash(wallet_common.ZeroHash()) } type TransactionManager struct { diff --git a/services/wallet/transfer/transaction_manager_route.go b/services/wallet/transfer/transaction_manager_route.go index d0136ac22..42657f38e 100644 --- a/services/wallet/transfer/transaction_manager_route.go +++ b/services/wallet/transfer/transaction_manager_route.go @@ -34,8 +34,8 @@ func (tm *TransactionManager) ClearLocalRouterTransactionsData() { func (tm *TransactionManager) ApprovalRequiredForPath(pathProcessorName string) bool { for _, desc := range tm.routerTransactions { - if desc.routerPath.ProcessorName == pathProcessorName && - desc.routerPath.ApprovalRequired { + if desc.RouterPath.ProcessorName == pathProcessorName && + desc.RouterPath.ApprovalRequired { return true } } @@ -44,8 +44,7 @@ func (tm *TransactionManager) ApprovalRequiredForPath(pathProcessorName string) func (tm *TransactionManager) ApprovalPlacedForPath(pathProcessorName string) bool { for _, desc := range tm.routerTransactions { - if desc.routerPath.ProcessorName == pathProcessorName && - desc.approvalTxSentHash != (types.Hash{}) { + if desc.RouterPath.ProcessorName == pathProcessorName && desc.IsApprovalPlaced() { return true } } @@ -54,8 +53,7 @@ func (tm *TransactionManager) ApprovalPlacedForPath(pathProcessorName string) bo func (tm *TransactionManager) TxPlacedForPath(pathProcessorName string) bool { for _, desc := range tm.routerTransactions { - if desc.routerPath.ProcessorName == pathProcessorName && - desc.txSentHash != (types.Hash{}) { + if desc.RouterPath.ProcessorName == pathProcessorName && desc.IsTxPlaced() { return true } } @@ -103,10 +101,10 @@ func (tm *TransactionManager) buildApprovalTxForPath(path *routes.Path, addressF usedNonces[path.FromChain.ChainID] = int64(usedNonce) tm.routerTransactions = append(tm.routerTransactions, &RouterTransactionDetails{ - routerPath: path, - approvalTxArgs: approavalSendArgs, - approvalTx: builtApprovalTx, - approvalHashToSign: types.Hash(approvalTxHash), + RouterPath: path, + ApprovalTxArgs: approavalSendArgs, + ApprovalTx: builtApprovalTx, + ApprovalHashToSign: types.Hash(approvalTxHash), }) return types.Hash(approvalTxHash), nil @@ -193,10 +191,10 @@ func (tm *TransactionManager) buildTxForPath(path *routes.Path, pathProcessors m usedNonces[path.FromChain.ChainID] = int64(usedNonce) tm.routerTransactions = append(tm.routerTransactions, &RouterTransactionDetails{ - routerPath: path, - txArgs: sendArgs, - tx: builtTx, - txHashToSign: types.Hash(txHash), + RouterPath: path, + TxArgs: sendArgs, + Tx: builtTx, + TxHashToSign: types.Hash(txHash), }) return types.Hash(txHash), nil @@ -291,20 +289,20 @@ func (tm *TransactionManager) ValidateAndAddSignaturesToRouterTransactions(signa // 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 desc.ApprovalTx != nil && desc.ApprovalTxSentHash == (types.Hash{}) { + sig, err := getSignatureForTxHash(desc.ApprovalHashToSign.String(), signatures) if err != nil { return err } - desc.approvalSignature = sig + desc.ApprovalSignature = sig } - if desc.tx != nil && desc.txSentHash == (types.Hash{}) { - sig, err := getSignatureForTxHash(desc.txHashToSign.String(), signatures) + if desc.Tx != nil && desc.TxSentHash == (types.Hash{}) { + sig, err := getSignatureForTxHash(desc.TxHashToSign.String(), signatures) if err != nil { return err } - desc.txSignature = sig + desc.TxSignature = sig } } @@ -316,41 +314,45 @@ func (tm *TransactionManager) SendRouterTransactions(ctx context.Context, multiT // send transactions for _, desc := range tm.routerTransactions { - if desc.approvalTx != nil && desc.approvalTxSentHash == (types.Hash{}) { + if desc.ApprovalTx != nil && !desc.IsApprovalPlaced() { var approvalTxWithSignature *ethTypes.Transaction - approvalTxWithSignature, err = tm.transactor.AddSignatureToTransaction(desc.approvalTxArgs.FromChainID, desc.approvalTx, desc.approvalSignature) + approvalTxWithSignature, err = tm.transactor.AddSignatureToTransaction(desc.ApprovalTxArgs.FromChainID, desc.ApprovalTx, desc.ApprovalSignature) if err != nil { - return nil, err + return } - desc.approvalTxSentHash, err = tm.transactor.SendTransactionWithSignature(common.Address(desc.approvalTxArgs.From), desc.approvalTxArgs.FromTokenID, multiTx.ID, approvalTxWithSignature) + desc.ApprovalTxSentHash, err = tm.transactor.SendTransactionWithSignature(common.Address(desc.ApprovalTxArgs.From), desc.ApprovalTxArgs.FromTokenID, multiTx.ID, approvalTxWithSignature) if err != nil { - return nil, err + return } - transactions = append(transactions, responses.NewRouterSentTransaction(desc.approvalTxArgs, desc.approvalTxSentHash, true)) + 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 { + if desc.RouterPath.ProcessorName == pathprocessor.ProcessorSwapParaswapName { continue } } - if desc.tx != nil && desc.txSentHash == (types.Hash{}) { + if desc.Tx != nil && !desc.IsTxPlaced() { var txWithSignature *ethTypes.Transaction - txWithSignature, err = tm.transactor.AddSignatureToTransaction(desc.txArgs.FromChainID, desc.tx, desc.txSignature) + txWithSignature, err = tm.transactor.AddSignatureToTransaction(desc.TxArgs.FromChainID, desc.Tx, desc.TxSignature) if err != nil { - return nil, err + return } - desc.txSentHash, err = tm.transactor.SendTransactionWithSignature(common.Address(desc.txArgs.From), desc.txArgs.FromTokenID, multiTx.ID, txWithSignature) + desc.TxSentHash, err = tm.transactor.SendTransactionWithSignature(common.Address(desc.TxArgs.From), desc.TxArgs.FromTokenID, multiTx.ID, txWithSignature) if err != nil { - return nil, err + return } - transactions = append(transactions, responses.NewRouterSentTransaction(desc.txArgs, desc.txSentHash, false)) + transactions = append(transactions, responses.NewRouterSentTransaction(desc.TxArgs, desc.TxSentHash, false)) } } return } + +func (tm *TransactionManager) GetRouterTransactions() []*RouterTransactionDetails { + return tm.routerTransactions +} diff --git a/transactions/pendingtxtracker.go b/transactions/pendingtxtracker.go index 1b988c697..a297f89ed 100644 --- a/transactions/pendingtxtracker.go +++ b/transactions/pendingtxtracker.go @@ -75,8 +75,9 @@ type StatusChangedPayload struct { // PendingTxTracker implements StatusService in common/status_node_service.go type PendingTxTracker struct { - db *sql.DB - rpcClient rpc.ClientInterface + db *sql.DB + trackedTxDB *DB + rpcClient rpc.ClientInterface rpcFilter *rpcfilters.Service eventFeed *event.Feed @@ -87,11 +88,12 @@ type PendingTxTracker struct { func NewPendingTxTracker(db *sql.DB, rpcClient rpc.ClientInterface, rpcFilter *rpcfilters.Service, eventFeed *event.Feed, checkInterval time.Duration) *PendingTxTracker { tm := &PendingTxTracker{ - db: db, - rpcClient: rpcClient, - eventFeed: eventFeed, - rpcFilter: rpcFilter, - logger: logutils.ZapLogger().Named("PendingTxTracker"), + db: db, + trackedTxDB: NewDB(db), + rpcClient: rpcClient, + eventFeed: eventFeed, + rpcFilter: rpcFilter, + logger: logutils.ZapLogger().Named("PendingTxTracker"), } tm.taskRunner = NewConditionalRepeater(checkInterval, func(ctx context.Context) bool { return tm.fetchAndUpdateDB(ctx) @@ -238,6 +240,20 @@ func fetchBatchTxStatus(ctx context.Context, rpcClient rpc.ClientInterface, chai // updateDBStatus returns entries that were updated only func (tm *PendingTxTracker) updateDBStatus(ctx context.Context, chainID common.ChainID, statuses []txStatusRes) ([]txStatusRes, error) { + for _, br := range statuses { + err := tm.trackedTxDB.UpdateTxStatus( + TxIdentity{ + ChainID: chainID, + Hash: br.hash, + }, + br.Status, + ) + if err != nil { + tm.logger.Error("Failed to update trackedTx status", zap.Stringer("hash", br.hash), zap.Error(err)) + continue + } + } + res := make([]txStatusRes, 0, len(statuses)) tx, err := tm.db.BeginTx(ctx, nil) if err != nil { @@ -557,6 +573,18 @@ func (tm *PendingTxTracker) StoreAndTrackPendingTx(transaction *PendingTransacti } func (tm *PendingTxTracker) addPending(transaction *PendingTransaction) error { + err := tm.trackedTxDB.PutTx(TrackedTx{ + ID: TxIdentity{ + ChainID: transaction.ChainID, + Hash: transaction.Hash, + }, + Status: Pending, + Timestamp: transaction.Timestamp, + }) + if err != nil { + return err + } + var notifyFn func() tx, err := tm.db.Begin() if err != nil { @@ -637,6 +665,7 @@ func (tm *PendingTxTracker) addPending(transaction *PendingTransaction) error { transaction.AutoDelete, transaction.Nonce, ) + // Notify listeners of new pending transaction (used in activity history) if err == nil { tm.notifyPendingTransactionListeners(PendingTxUpdatePayload{ diff --git a/transactions/pendingtxtracker_db.go b/transactions/pendingtxtracker_db.go new file mode 100644 index 000000000..f35eca459 --- /dev/null +++ b/transactions/pendingtxtracker_db.go @@ -0,0 +1,115 @@ +package transactions + +import ( + "database/sql" + + sq "github.com/Masterminds/squirrel" + + "github.com/status-im/status-go/sqlite" +) + +type TrackedTx struct { + ID TxIdentity `json:"id"` + Timestamp uint64 `json:"timestamp"` + Status TxStatus `json:"status"` +} + +type DB struct { + db *sql.DB +} + +func NewDB(db *sql.DB) *DB { + return &DB{db: db} +} + +func (db *DB) PutTx(transaction TrackedTx) (err error) { + var tx *sql.Tx + tx, err = db.db.Begin() + if err != nil { + return err + } + defer func() { + if err == nil { + err = tx.Commit() + return + } + _ = tx.Rollback() + }() + + return putTx(tx, transaction) +} + +func (db *DB) GetTx(txID TxIdentity) (tx TrackedTx, err error) { + q := sq.Select("chain_id", "tx_hash", "tx_status", "timestamp"). + From("tracked_transactions"). + Where(sq.Eq{"chain_id": txID.ChainID, "tx_hash": txID.Hash}) + + query, args, err := q.ToSql() + if err != nil { + return + } + + row := db.db.QueryRow(query, args...) + err = row.Scan(&tx.ID.ChainID, &tx.ID.Hash, &tx.Status, &tx.Timestamp) + + return +} + +func putTx(creator sqlite.StatementCreator, tx TrackedTx) error { + q := sq.Replace("tracked_transactions"). + Columns("chain_id", "tx_hash", "tx_status", "timestamp"). + Values(tx.ID.ChainID, tx.ID.Hash, tx.Status, tx.Timestamp) + + query, args, err := q.ToSql() + if err != nil { + return err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(args...) + + return err +} + +func (db *DB) UpdateTxStatus(txID TxIdentity, status TxStatus) (err error) { + var tx *sql.Tx + tx, err = db.db.Begin() + if err != nil { + return err + } + defer func() { + if err == nil { + err = tx.Commit() + return + } + _ = tx.Rollback() + }() + + return updateTxStatus(tx, txID, status) +} + +func updateTxStatus(creator sqlite.StatementCreator, txID TxIdentity, status TxStatus) error { + q := sq.Update("tracked_transactions"). + Set("tx_status", status). + Where(sq.Eq{"chain_id": txID.ChainID, "tx_hash": txID.Hash}) + + query, args, err := q.ToSql() + if err != nil { + return err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(args...) + + return err +} diff --git a/transactions/pendingtxtracker_db_test.go b/transactions/pendingtxtracker_db_test.go new file mode 100644 index 000000000..c9eae729a --- /dev/null +++ b/transactions/pendingtxtracker_db_test.go @@ -0,0 +1,90 @@ +package transactions_test + +import ( + "math/rand" + "strconv" + "testing" + + crypto_rand "crypto/rand" + + eth "github.com/ethereum/go-ethereum/common" + "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/t/helpers" + "github.com/status-im/status-go/transactions" + "github.com/status-im/status-go/walletdatabase" + + "github.com/stretchr/testify/require" +) + +func getRandomStatus() transactions.TxStatus { + switch rand.Intn(3) { // nolint: gosec + case 0: + return transactions.Pending + case 1: + return transactions.Success + case 2: + return transactions.Failed + } + + return transactions.Pending +} + +func getRandomTrackedTx() transactions.TrackedTx { + tx := transactions.TrackedTx{ + ID: transactions.TxIdentity{ + ChainID: common.ChainID(rand.Uint64() % 10), // nolint: gosec + Hash: eth.Hash{}, + }, + Timestamp: 123, + Status: getRandomStatus(), + } + _, _ = crypto_rand.Read(tx.ID.Hash[:]) + + return tx +} + +func getTestData() []struct { + name string + tx transactions.TrackedTx +} { + testData := make([]struct { + name string + tx transactions.TrackedTx + }, 10) + + for i := range testData { + testData[i].name = "test_" + strconv.Itoa(i) + testData[i].tx = getRandomTrackedTx() + } + + return testData +} + +func Test_PuTrackedTx(t *testing.T) { + walletDB, closeFn, err := helpers.SetupTestSQLDB(walletdatabase.DbInitializer{}, "pendingtxtracker-tests") + require.NoError(t, err) + defer func() { + require.NoError(t, closeFn()) + }() + + db := transactions.NewDB(walletDB) + + for _, tt := range getTestData() { + t.Run(tt.name, func(t *testing.T) { + err := db.PutTx(tt.tx) + require.NoError(t, err) + + readTx, err := db.GetTx(tt.tx.ID) + require.NoError(t, err) + require.EqualExportedValues(t, tt.tx, readTx) + + newStatus := getRandomStatus() + err = db.UpdateTxStatus(tt.tx.ID, newStatus) + require.NoError(t, err) + + readTx, err = db.GetTx(tt.tx.ID) + require.NoError(t, err) + require.Equal(t, newStatus, readTx.Status) + }) + } +} diff --git a/walletdatabase/migrations/sql/1728941142_add_route_data.up.sql b/walletdatabase/migrations/sql/1728941142_add_route_data.up.sql new file mode 100644 index 000000000..cc2043e28 --- /dev/null +++ b/walletdatabase/migrations/sql/1728941142_add_route_data.up.sql @@ -0,0 +1,50 @@ +-- store route input parameters +CREATE TABLE IF NOT EXISTS route_input_parameters ( + uuid TEXT NOT NULL AS (json_extract(route_input_params_json, '$.uuid')), + route_input_params_json JSON NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_route_input_parameters_per_uuid ON route_input_parameters (uuid); + +-- store route build tx parameters +CREATE TABLE IF NOT EXISTS route_build_tx_parameters ( + uuid TEXT NOT NULL AS (json_extract(route_build_tx_params_json, '$.uuid')), + route_build_tx_params_json JSON NOT NULL, + FOREIGN KEY(uuid) REFERENCES route_input_parameters(uuid) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_route_build_tx_parameters_per_uuid ON route_build_tx_parameters (uuid); + +-- store route paths +CREATE TABLE IF NOT EXISTS route_paths ( + uuid TEXT NOT NULL, + path_idx INTEGER NOT NULL, + path_json JSON NOT NULL, + FOREIGN KEY(uuid) REFERENCES route_input_parameters(uuid) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_route_path_per_uuid_index ON route_paths (uuid, path_idx); + +-- store route path transactions +CREATE TABLE IF NOT EXISTS route_path_transactions ( + uuid TEXT NOT NULL, + path_idx INTEGER NOT NULL, + tx_idx INTEGER NOT NULL, + is_approval BOOLEAN NOT NULL, + chain_id UNSIGNED BIGINT NOT NULL, + tx_hash BLOB NOT NULL, + tx_args_json JSON NOT NULL, + FOREIGN KEY(uuid, path_idx) REFERENCES route_paths(uuid, path_idx) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_route_path_transaction_per_uuid_path_idx_tx_idx ON route_path_transactions (uuid, path_idx, tx_idx); +CREATE UNIQUE INDEX IF NOT EXISTS idx_route_path_transaction_per_chain_id_tx_hash ON route_path_transactions (chain_id, tx_hash); + +-- store sent transactions +CREATE TABLE IF NOT EXISTS sent_transactions ( + chain_id UNSIGNED BIGINT NOT NULL, + tx_hash BLOB NOT NULL, + tx_json JSON NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_sent_transactions_per_chain_id_tx_hash ON sent_transactions (chain_id, tx_hash); diff --git a/walletdatabase/migrations/sql/1729740219_add_tracked_transactions.up.sql b/walletdatabase/migrations/sql/1729740219_add_tracked_transactions.up.sql new file mode 100644 index 000000000..725ff2159 --- /dev/null +++ b/walletdatabase/migrations/sql/1729740219_add_tracked_transactions.up.sql @@ -0,0 +1,9 @@ +-- store state of tracked transactions +CREATE TABLE IF NOT EXISTS tracked_transactions( + chain_id UNSIGNED BIGINT NOT NULL, + tx_hash BLOB NOT NULL, + tx_status STRING NOT NULL, + timestamp INTEGER NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_tracked_transactions_per_chain_id_tx_hash ON tracked_transactions (chain_id, tx_hash); \ No newline at end of file