github.com/status-im/status-go@v1.1.0/services/wallet/transfer/controller.go (about) 1 package transfer 2 3 import ( 4 "context" 5 "database/sql" 6 "fmt" 7 "math/big" 8 9 "golang.org/x/exp/slices" // since 1.21, this is in the standard library 10 11 "github.com/ethereum/go-ethereum/common" 12 "github.com/ethereum/go-ethereum/core/types" 13 "github.com/ethereum/go-ethereum/event" 14 "github.com/ethereum/go-ethereum/log" 15 statusaccounts "github.com/status-im/status-go/multiaccounts/accounts" 16 "github.com/status-im/status-go/rpc" 17 "github.com/status-im/status-go/rpc/chain" 18 "github.com/status-im/status-go/services/accounts/accountsevent" 19 "github.com/status-im/status-go/services/wallet/balance" 20 "github.com/status-im/status-go/services/wallet/blockchainstate" 21 "github.com/status-im/status-go/services/wallet/token" 22 "github.com/status-im/status-go/transactions" 23 ) 24 25 type Controller struct { 26 db *Database 27 accountsDB *statusaccounts.Database 28 rpcClient *rpc.Client 29 blockDAO *BlockDAO 30 blockRangesSeqDAO *BlockRangeSequentialDAO 31 reactor *Reactor 32 accountFeed *event.Feed 33 TransferFeed *event.Feed 34 accWatcher *accountsevent.Watcher 35 transactionManager *TransactionManager 36 pendingTxManager *transactions.PendingTxTracker 37 tokenManager *token.Manager 38 balanceCacher balance.Cacher 39 blockChainState *blockchainstate.BlockChainState 40 } 41 42 func NewTransferController(db *sql.DB, accountsDB *statusaccounts.Database, rpcClient *rpc.Client, accountFeed *event.Feed, transferFeed *event.Feed, 43 transactionManager *TransactionManager, pendingTxManager *transactions.PendingTxTracker, tokenManager *token.Manager, 44 balanceCacher balance.Cacher, blockChainState *blockchainstate.BlockChainState) *Controller { 45 46 blockDAO := &BlockDAO{db} 47 return &Controller{ 48 db: NewDB(db), 49 accountsDB: accountsDB, 50 blockDAO: blockDAO, 51 blockRangesSeqDAO: &BlockRangeSequentialDAO{db}, 52 rpcClient: rpcClient, 53 accountFeed: accountFeed, 54 TransferFeed: transferFeed, 55 transactionManager: transactionManager, 56 pendingTxManager: pendingTxManager, 57 tokenManager: tokenManager, 58 balanceCacher: balanceCacher, 59 blockChainState: blockChainState, 60 } 61 } 62 63 func (c *Controller) Start() { 64 go func() { _ = c.cleanupAccountsLeftovers() }() 65 } 66 67 func (c *Controller) Stop() { 68 if c.reactor != nil { 69 c.reactor.stop() 70 } 71 72 if c.accWatcher != nil { 73 c.accWatcher.Stop() 74 c.accWatcher = nil 75 } 76 } 77 78 func sameChains(chainIDs1 []uint64, chainIDs2 []uint64) bool { 79 if len(chainIDs1) != len(chainIDs2) { 80 return false 81 } 82 83 for _, chainID := range chainIDs1 { 84 if !slices.Contains(chainIDs2, chainID) { 85 return false 86 } 87 } 88 89 return true 90 } 91 92 func (c *Controller) CheckRecentHistory(chainIDs []uint64, accounts []common.Address) error { 93 if len(accounts) == 0 { 94 return nil 95 } 96 97 if len(chainIDs) == 0 { 98 return nil 99 } 100 101 err := c.blockDAO.mergeBlocksRanges(chainIDs, accounts) 102 if err != nil { 103 return err 104 } 105 106 chainClients, err := c.rpcClient.EthClients(chainIDs) 107 if err != nil { 108 return err 109 } 110 111 if c.reactor != nil { 112 if !sameChains(chainIDs, c.reactor.chainIDs) { 113 err := c.reactor.restart(chainClients, accounts) 114 if err != nil { 115 return err 116 } 117 } 118 119 return nil 120 } 121 122 const omitHistory = true 123 c.reactor = NewReactor(c.db, c.blockDAO, c.blockRangesSeqDAO, c.accountsDB, c.TransferFeed, c.transactionManager, 124 c.pendingTxManager, c.tokenManager, c.balanceCacher, omitHistory, c.blockChainState) 125 126 err = c.reactor.start(chainClients, accounts) 127 if err != nil { 128 return err 129 } 130 131 c.startAccountWatcher(chainIDs) 132 133 return nil 134 } 135 136 func (c *Controller) startAccountWatcher(chainIDs []uint64) { 137 if c.accWatcher == nil { 138 c.accWatcher = accountsevent.NewWatcher(c.accountsDB, c.accountFeed, func(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address) { 139 c.onAccountsChanged(changedAddresses, eventType, currentAddresses, chainIDs) 140 }) 141 } 142 c.accWatcher.Start() 143 } 144 145 func (c *Controller) onAccountsChanged(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address, chainIDs []uint64) { 146 if eventType == accountsevent.EventTypeRemoved { 147 for _, address := range changedAddresses { 148 c.cleanUpRemovedAccount(address) 149 } 150 } 151 152 if c.reactor == nil { 153 log.Warn("reactor is not initialized") 154 return 155 } 156 157 if eventType == accountsevent.EventTypeAdded || eventType == accountsevent.EventTypeRemoved { 158 log.Debug("list of accounts was changed from a previous version. reactor will be restarted", "new", currentAddresses) 159 160 chainClients, err := c.rpcClient.EthClients(chainIDs) 161 if err != nil { 162 return 163 } 164 165 err = c.reactor.restart(chainClients, currentAddresses) 166 if err != nil { 167 log.Error("failed to restart reactor with new accounts", "error", err) 168 } 169 } 170 } 171 172 // Only used by status-mobile 173 func (c *Controller) LoadTransferByHash(ctx context.Context, rpcClient *rpc.Client, address common.Address, hash common.Hash) error { 174 chainClient, err := rpcClient.EthClient(rpcClient.UpstreamChainID) 175 if err != nil { 176 return err 177 } 178 179 signer := types.LatestSignerForChainID(chainClient.ToBigInt()) 180 181 transfer, err := getTransferByHash(ctx, chainClient, signer, address, hash) 182 if err != nil { 183 return err 184 } 185 186 transfers := []Transfer{*transfer} 187 188 err = c.db.InsertBlock(rpcClient.UpstreamChainID, address, transfer.BlockNumber, transfer.BlockHash) 189 if err != nil { 190 return err 191 } 192 193 tx, err := c.db.client.BeginTx(ctx, nil) 194 if err != nil { 195 return err 196 } 197 198 blocks := []*big.Int{transfer.BlockNumber} 199 err = saveTransfersMarkBlocksLoaded(tx, rpcClient.UpstreamChainID, address, transfers, blocks) 200 if err != nil { 201 rollErr := tx.Rollback() 202 if rollErr != nil { 203 return fmt.Errorf("failed to rollback transaction due to error: %v", err) 204 } 205 return err 206 } 207 208 return nil 209 } 210 211 func (c *Controller) GetTransfersByAddress(ctx context.Context, chainID uint64, address common.Address, toBlock *big.Int, 212 limit int64, fetchMore bool) ([]View, error) { 213 214 rst, err := c.reactor.getTransfersByAddress(ctx, chainID, address, toBlock, limit) 215 if err != nil { 216 log.Error("[WalletAPI:: GetTransfersByAddress] can't fetch transfers", "err", err) 217 return nil, err 218 } 219 220 return castToTransferViews(rst), nil 221 } 222 223 func (c *Controller) GetTransfersForIdentities(ctx context.Context, identities []TransactionIdentity) ([]View, error) { 224 rst, err := c.db.GetTransfersForIdentities(ctx, identities) 225 if err != nil { 226 log.Error("[transfer.Controller.GetTransfersForIdentities] DB err", err) 227 return nil, err 228 } 229 230 return castToTransferViews(rst), nil 231 } 232 233 func (c *Controller) GetCachedBalances(ctx context.Context, chainID uint64, addresses []common.Address) ([]BlockView, error) { 234 result, error := c.blockDAO.getLastKnownBlocks(chainID, addresses) 235 if error != nil { 236 return nil, error 237 } 238 239 return blocksToViews(result), nil 240 } 241 242 func (c *Controller) cleanUpRemovedAccount(address common.Address) { 243 // Transfers will be deleted by foreign key constraint by cascade 244 err := deleteBlocks(c.db.client, address) 245 if err != nil { 246 log.Error("Failed to delete blocks", "error", err) 247 } 248 err = deleteAllRanges(c.db.client, address) 249 if err != nil { 250 log.Error("Failed to delete old blocks ranges", "error", err) 251 } 252 253 err = c.blockRangesSeqDAO.deleteRange(address) 254 if err != nil { 255 log.Error("Failed to delete blocks ranges sequential", "error", err) 256 } 257 258 err = c.transactionManager.removeMultiTransactionByAddress(address) 259 if err != nil { 260 log.Error("Failed to delete multitransactions", "error", err) 261 } 262 263 rpcLimitsStorage := chain.NewLimitsDBStorage(c.db.client) 264 err = rpcLimitsStorage.Delete(accountLimiterTag(address)) 265 if err != nil { 266 log.Error("Failed to delete limits", "error", err) 267 } 268 } 269 270 func (c *Controller) cleanupAccountsLeftovers() error { 271 // We clean up accounts that were deleted and soft removed 272 accounts, err := c.accountsDB.GetWalletAddresses() 273 if err != nil { 274 log.Error("Failed to get accounts", "error", err) 275 return err 276 } 277 278 existingAddresses := make([]common.Address, len(accounts)) 279 for i, account := range accounts { 280 existingAddresses[i] = (common.Address)(account) 281 } 282 283 addressesInWalletDB, err := getAddresses(c.db.client) 284 if err != nil { 285 log.Error("Failed to get addresses from wallet db", "error", err) 286 return err 287 } 288 289 missing := findMissingItems(addressesInWalletDB, existingAddresses) 290 for _, address := range missing { 291 c.cleanUpRemovedAccount(address) 292 } 293 294 return nil 295 } 296 297 // find items from one slice that are not in another 298 func findMissingItems(slice1 []common.Address, slice2 []common.Address) []common.Address { 299 var missing []common.Address 300 for _, item := range slice1 { 301 if !slices.Contains(slice2, item) { 302 missing = append(missing, item) 303 } 304 } 305 return missing 306 }