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

     1  package wallet
     2  
     3  import (
     4  	"database/sql"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"time"
     9  
    10  	"github.com/ethereum/go-ethereum/common"
    11  	"github.com/status-im/status-go/constants"
    12  	multiAccCommon "github.com/status-im/status-go/multiaccounts/common"
    13  )
    14  
    15  type savedAddressMeta struct {
    16  	UpdateClock uint64 // wall clock used to deconflict concurrent updates
    17  }
    18  
    19  type SavedAddress struct {
    20  	Address common.Address `json:"address"`
    21  	// TODO: Add Emoji
    22  	// Emoji	string		   `json:"emoji"`
    23  	Name            string                            `json:"name"`
    24  	ChainShortNames string                            `json:"chainShortNames"` // used with address only, not with ENSName
    25  	ENSName         string                            `json:"ens"`
    26  	ColorID         multiAccCommon.CustomizationColor `json:"colorId"`
    27  	IsTest          bool                              `json:"isTest"`
    28  	CreatedAt       int64                             `json:"createdAt"`
    29  	Removed         bool                              `json:"removed"`
    30  	savedAddressMeta
    31  }
    32  
    33  func (s *SavedAddress) ID() string {
    34  	return fmt.Sprintf("%s-%t", s.Address.Hex(), s.IsTest)
    35  }
    36  
    37  func (s *SavedAddress) MarshalJSON() ([]byte, error) {
    38  	item := struct {
    39  		Address          common.Address                    `json:"address"`
    40  		MixedcaseAddress string                            `json:"mixedcaseAddress"`
    41  		Name             string                            `json:"name"`
    42  		ChainShortNames  string                            `json:"chainShortNames"`
    43  		ENSName          string                            `json:"ens"`
    44  		ColorID          multiAccCommon.CustomizationColor `json:"colorId"`
    45  		IsTest           bool                              `json:"isTest"`
    46  		CreatedAt        int64                             `json:"createdAt"`
    47  		Removed          bool                              `json:"removed"`
    48  	}{
    49  		Address:          s.Address,
    50  		MixedcaseAddress: s.Address.Hex(),
    51  		Name:             s.Name,
    52  		ChainShortNames:  s.ChainShortNames,
    53  		ENSName:          s.ENSName,
    54  		ColorID:          s.ColorID,
    55  		IsTest:           s.IsTest,
    56  		CreatedAt:        s.CreatedAt,
    57  		Removed:          s.Removed,
    58  	}
    59  
    60  	return json.Marshal(item)
    61  }
    62  
    63  type SavedAddressesManager struct {
    64  	db *sql.DB
    65  }
    66  
    67  func NewSavedAddressesManager(db *sql.DB) *SavedAddressesManager {
    68  	return &SavedAddressesManager{db: db}
    69  }
    70  
    71  const rawQueryColumnsOrder = "address, name, removed, update_clock, chain_short_names, ens_name, is_test, created_at, color"
    72  
    73  // getSavedAddressesFromDBRows retrieves all data based on SELECT Query using rawQueryColumnsOrder
    74  func getSavedAddressesFromDBRows(rows *sql.Rows) ([]*SavedAddress, error) {
    75  	var addresses []*SavedAddress
    76  	for rows.Next() {
    77  		sa := &SavedAddress{}
    78  		// based on rawQueryColumnsOrder
    79  		err := rows.Scan(
    80  			&sa.Address,
    81  			&sa.Name,
    82  			&sa.Removed,
    83  			&sa.UpdateClock,
    84  			&sa.ChainShortNames,
    85  			&sa.ENSName,
    86  			&sa.IsTest,
    87  			&sa.CreatedAt,
    88  			&sa.ColorID,
    89  		)
    90  		if err != nil {
    91  			return nil, err
    92  		}
    93  
    94  		addresses = append(addresses, sa)
    95  	}
    96  
    97  	return addresses, nil
    98  }
    99  
   100  func (sam *SavedAddressesManager) getSavedAddresses(condition string) ([]*SavedAddress, error) {
   101  	var whereCondition string
   102  	if condition != "" {
   103  		whereCondition = fmt.Sprintf("WHERE %s", condition)
   104  	}
   105  
   106  	rows, err := sam.db.Query(fmt.Sprintf("SELECT %s FROM saved_addresses %s", rawQueryColumnsOrder, whereCondition))
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	defer rows.Close()
   111  
   112  	addresses, err := getSavedAddressesFromDBRows(rows)
   113  	return addresses, err
   114  }
   115  
   116  func (sam *SavedAddressesManager) GetSavedAddresses() ([]*SavedAddress, error) {
   117  	return sam.getSavedAddresses("removed != 1")
   118  }
   119  
   120  func (sam *SavedAddressesManager) GetSavedAddressesPerMode(testnetMode bool) ([]*SavedAddress, error) {
   121  	return sam.getSavedAddresses(fmt.Sprintf("is_test = %t AND removed != 1", testnetMode))
   122  }
   123  
   124  // GetRawSavedAddresses provides access to the soft-delete and sync metadata
   125  func (sam *SavedAddressesManager) GetRawSavedAddresses() ([]*SavedAddress, error) {
   126  	return sam.getSavedAddresses("")
   127  }
   128  
   129  func (sam *SavedAddressesManager) RemainingCapacityForSavedAddresses(testnetMode bool) (int, error) {
   130  	savedAddress, err := sam.GetSavedAddressesPerMode(testnetMode)
   131  	if err != nil {
   132  		return 0, err
   133  	}
   134  	remainingCapacity := constants.MaxNumberOfSavedAddresses - len(savedAddress)
   135  	if remainingCapacity <= 0 {
   136  		return 0, errors.New("no more save addresses can be added")
   137  	}
   138  
   139  	return remainingCapacity, nil
   140  }
   141  
   142  func (sam *SavedAddressesManager) upsertSavedAddress(sa SavedAddress, tx *sql.Tx) (err error) {
   143  	if tx == nil {
   144  		tx, err = sam.db.Begin()
   145  		if err != nil {
   146  			return err
   147  		}
   148  		defer func() {
   149  			if err == nil {
   150  				err = tx.Commit()
   151  				return
   152  			}
   153  			_ = tx.Rollback()
   154  		}()
   155  	}
   156  	rows, err := tx.Query(
   157  		fmt.Sprintf("SELECT %s FROM saved_addresses WHERE address = ? AND is_test = ?", rawQueryColumnsOrder),
   158  		sa.Address, sa.IsTest,
   159  	)
   160  	if err != nil {
   161  		return err
   162  	}
   163  	defer rows.Close()
   164  	savedAddresses, err := getSavedAddressesFromDBRows(rows)
   165  	if err != nil {
   166  		return err
   167  	}
   168  	sa.CreatedAt = time.Now().Unix()
   169  	for _, savedAddress := range savedAddresses {
   170  		if savedAddress.Address == sa.Address && savedAddress.IsTest == sa.IsTest {
   171  			sa.CreatedAt = savedAddress.CreatedAt
   172  			break
   173  		}
   174  	}
   175  	sqlStatement := `
   176  	INSERT OR REPLACE
   177  	INTO
   178  		saved_addresses (
   179  			address,
   180  			name,
   181  			removed,
   182  			update_clock,
   183  			chain_short_names,
   184  			ens_name,
   185  			is_test,
   186  			created_at,
   187  			color
   188  		)
   189  	VALUES
   190  		(?, ?, ?, ?, ?, ?, ?, ?, ?)
   191  	`
   192  	insert, err := tx.Prepare(sqlStatement)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	defer insert.Close()
   197  	_, err = insert.Exec(sa.Address, sa.Name, sa.Removed, sa.UpdateClock, sa.ChainShortNames, sa.ENSName,
   198  		sa.IsTest, sa.CreatedAt, sa.ColorID)
   199  	return err
   200  }
   201  
   202  func (sam *SavedAddressesManager) UpdateMetadataAndUpsertSavedAddress(sa SavedAddress) error {
   203  	return sam.upsertSavedAddress(sa, nil)
   204  }
   205  
   206  func (sam *SavedAddressesManager) startTransactionAndCheckIfNewerChange(address common.Address, isTest bool, updateClock uint64) (newer bool, tx *sql.Tx, err error) {
   207  	tx, err = sam.db.Begin()
   208  	if err != nil {
   209  		return false, nil, err
   210  	}
   211  	row := tx.QueryRow("SELECT update_clock FROM saved_addresses WHERE address = ? AND is_test = ?", address, isTest)
   212  	if err != nil {
   213  		return false, tx, err
   214  	}
   215  
   216  	var dbUpdateClock uint64
   217  	err = row.Scan(&dbUpdateClock)
   218  	if err != nil {
   219  		return err == sql.ErrNoRows, tx, err
   220  	}
   221  	return dbUpdateClock < updateClock, tx, nil
   222  }
   223  
   224  func (sam *SavedAddressesManager) AddSavedAddressIfNewerUpdate(sa SavedAddress) (insertedOrUpdated bool, err error) {
   225  	newer, tx, err := sam.startTransactionAndCheckIfNewerChange(sa.Address, sa.IsTest, sa.UpdateClock)
   226  	defer func() {
   227  		if err == nil {
   228  			err = tx.Commit()
   229  			return
   230  		}
   231  		_ = tx.Rollback()
   232  	}()
   233  	if !newer {
   234  		return false, err
   235  	}
   236  
   237  	err = sam.upsertSavedAddress(sa, tx)
   238  	if err != nil {
   239  		return false, err
   240  	}
   241  
   242  	return true, err
   243  }
   244  
   245  func (sam *SavedAddressesManager) DeleteSavedAddress(address common.Address, isTest bool, updateClock uint64) (deleted bool, err error) {
   246  	if err != nil {
   247  		return false, err
   248  	}
   249  	newer, tx, err := sam.startTransactionAndCheckIfNewerChange(address, isTest, updateClock)
   250  	defer func() {
   251  		if err == nil {
   252  			err = tx.Commit()
   253  			return
   254  		}
   255  		_ = tx.Rollback()
   256  	}()
   257  	if !newer {
   258  		return false, err
   259  	}
   260  
   261  	update, err := tx.Prepare(`UPDATE saved_addresses SET removed = 1, update_clock = ? WHERE address = ? AND is_test = ?`)
   262  	if err != nil {
   263  		return false, err
   264  	}
   265  	defer update.Close()
   266  	res, err := update.Exec(updateClock, address, isTest)
   267  	if err != nil {
   268  		return false, err
   269  	}
   270  
   271  	nRows, err := res.RowsAffected()
   272  	if err != nil {
   273  		return false, err
   274  	}
   275  
   276  	return nRows > 0, nil
   277  }
   278  
   279  func (sam *SavedAddressesManager) DeleteSoftRemovedSavedAddresses(threshold uint64) error {
   280  	_, err := sam.db.Exec(`DELETE FROM saved_addresses WHERE removed = 1 AND update_clock < ?`, threshold)
   281  	return err
   282  }