github.com/status-im/status-go@v1.1.0/services/wallet/history/balance_db.go (about)

     1  package history
     2  
     3  import (
     4  	"database/sql"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"math/big"
     8  
     9  	"github.com/ethereum/go-ethereum/common"
    10  	"github.com/ethereum/go-ethereum/log"
    11  	"github.com/status-im/status-go/services/wallet/bigint"
    12  )
    13  
    14  type BalanceDB struct {
    15  	db *sql.DB
    16  }
    17  
    18  func NewBalanceDB(sqlDb *sql.DB) *BalanceDB {
    19  	return &BalanceDB{
    20  		db: sqlDb,
    21  	}
    22  }
    23  
    24  // entry represents a single row in the balance_history table
    25  type entry struct {
    26  	chainID      uint64
    27  	address      common.Address
    28  	tokenSymbol  string
    29  	tokenAddress common.Address
    30  	block        *big.Int
    31  	timestamp    int64
    32  	balance      *big.Int
    33  }
    34  
    35  type assetIdentity struct {
    36  	ChainID     uint64
    37  	Addresses   []common.Address
    38  	TokenSymbol string
    39  }
    40  
    41  func (a *assetIdentity) addressesToString() string {
    42  	var addressesStr string
    43  	for i, address := range a.Addresses {
    44  		addressStr := hex.EncodeToString(address[:])
    45  		if i == 0 {
    46  			addressesStr = "X'" + addressStr + "'"
    47  		} else {
    48  			addressesStr += ", X'" + addressStr + "'"
    49  		}
    50  	}
    51  	return addressesStr
    52  }
    53  
    54  func (e *entry) String() string {
    55  	return fmt.Sprintf("chainID: %v, address: %v, tokenSymbol: %v, tokenAddress: %v, block: %v, timestamp: %v, balance: %v",
    56  		e.chainID, e.address, e.tokenSymbol, e.tokenAddress, e.block, e.timestamp, e.balance)
    57  }
    58  
    59  func (b *BalanceDB) add(entry *entry) error {
    60  	log.Debug("Adding entry to balance_history", "entry", entry)
    61  
    62  	_, err := b.db.Exec("INSERT OR IGNORE INTO balance_history (chain_id, address, currency, block, timestamp, balance) VALUES (?, ?, ?, ?, ?, ?)", entry.chainID, entry.address, entry.tokenSymbol, (*bigint.SQLBigInt)(entry.block), entry.timestamp, (*bigint.SQLBigIntBytes)(entry.balance))
    63  	return err
    64  }
    65  
    66  func (b *BalanceDB) getEntriesWithoutBalances(chainID uint64, address common.Address) (entries []*entry, err error) {
    67  	rows, err := b.db.Query("SELECT blk_number, tr.timestamp, token_address from transfers tr LEFT JOIN balance_history bh ON bh.block = tr.blk_number WHERE tr.network_id = ? AND tr.address = ? AND tr.type != 'erc721' AND tr.type !='erc1155' AND bh.block IS NULL",
    68  		chainID, address)
    69  	if err == sql.ErrNoRows {
    70  		return nil, nil
    71  	}
    72  
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	defer rows.Close()
    77  
    78  	entries = make([]*entry, 0)
    79  	for rows.Next() {
    80  		entry := &entry{
    81  			chainID: chainID,
    82  			address: address,
    83  			block:   new(big.Int),
    84  		}
    85  
    86  		// tokenAddress can be NULL and can not unmarshal to common.Address
    87  		tokenHexAddress := make([]byte, common.AddressLength)
    88  		err := rows.Scan((*bigint.SQLBigInt)(entry.block), &entry.timestamp, &tokenHexAddress)
    89  		if err != nil {
    90  			return nil, err
    91  		}
    92  
    93  		tokenAddress := common.BytesToAddress(tokenHexAddress)
    94  		if tokenAddress != (common.Address{}) {
    95  			entry.tokenAddress = tokenAddress
    96  		}
    97  		entries = append(entries, entry)
    98  	}
    99  	return entries, nil
   100  }
   101  
   102  func (b *BalanceDB) getNewerThan(identity *assetIdentity, timestamp uint64) (entries []*entry, err error) {
   103  	// DISTINCT removes duplicates that can happen when a block has multiple transfers of same token
   104  	rawQueryStr := "SELECT DISTINCT block, timestamp, balance, address FROM balance_history WHERE chain_id = ? AND address IN (%s) AND currency = ? AND timestamp > ? ORDER BY timestamp"
   105  	queryString := fmt.Sprintf(rawQueryStr, identity.addressesToString())
   106  	rows, err := b.db.Query(queryString, identity.ChainID, identity.TokenSymbol, timestamp)
   107  	if err == sql.ErrNoRows {
   108  		return nil, nil
   109  	} else if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	defer rows.Close()
   114  
   115  	result := make([]*entry, 0)
   116  	for rows.Next() {
   117  		entry := &entry{
   118  			chainID:     identity.ChainID,
   119  			tokenSymbol: identity.TokenSymbol,
   120  			block:       new(big.Int),
   121  			balance:     new(big.Int),
   122  		}
   123  		err := rows.Scan((*bigint.SQLBigInt)(entry.block), &entry.timestamp, (*bigint.SQLBigIntBytes)(entry.balance), &entry.address)
   124  		if err != nil {
   125  			return nil, err
   126  		}
   127  		result = append(result, entry)
   128  	}
   129  	return result, nil
   130  }
   131  
   132  func (b *BalanceDB) getEntryPreviousTo(item *entry) (res *entry, err error) {
   133  	res = &entry{
   134  		chainID:     item.chainID,
   135  		address:     item.address,
   136  		block:       new(big.Int),
   137  		balance:     new(big.Int),
   138  		tokenSymbol: item.tokenSymbol,
   139  	}
   140  
   141  	queryStr := "SELECT block, timestamp, balance FROM balance_history WHERE chain_id = ? AND address = ? AND currency = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT 1"
   142  	row := b.db.QueryRow(queryStr, item.chainID, item.address, item.tokenSymbol, item.timestamp)
   143  
   144  	err = row.Scan((*bigint.SQLBigInt)(res.block), &res.timestamp, (*bigint.SQLBigIntBytes)(res.balance))
   145  	if err == sql.ErrNoRows {
   146  		return nil, nil
   147  	} else if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	return res, nil
   152  }
   153  
   154  func (b *BalanceDB) removeBalanceHistory(address common.Address) error {
   155  	_, err := b.db.Exec("DELETE FROM balance_history WHERE address = ?", address)
   156  	return err
   157  }