decred.org/dcrdex@v1.0.5/server/db/driver/pg/accounts.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package pg
     5  
     6  import (
     7  	"database/sql"
     8  	"errors"
     9  	"fmt"
    10  	"time"
    11  
    12  	"decred.org/dcrdex/server/account"
    13  	"decred.org/dcrdex/server/db"
    14  	"decred.org/dcrdex/server/db/driver/pg/internal"
    15  	"github.com/decred/dcrd/dcrutil/v4" // TODO: consider a move to "crypto/sha256" instead of dcrutil.Hash160
    16  )
    17  
    18  // Account retrieves the account pubkey, active bonds, and if the account has a
    19  // legacy registration fee address and transaction recorded. If the account does
    20  // not exist or there is in an error retrieving any data, a nil *account.Account
    21  // is returned.
    22  func (a *Archiver) Account(aid account.AccountID, bondExpiry time.Time) (acct *account.Account, bonds []*db.Bond) {
    23  	acct, err := getAccount(a.db, a.tables.accounts, aid)
    24  	switch {
    25  	case errors.Is(err, sql.ErrNoRows):
    26  		return nil, nil
    27  	case err == nil:
    28  	default:
    29  		log.Errorf("getAccount error: %v", err)
    30  		return nil, nil
    31  	}
    32  
    33  	bonds, err = getBondsForAccount(a.db, a.tables.bonds, aid, bondExpiry.Unix())
    34  	switch {
    35  	case errors.Is(err, sql.ErrNoRows):
    36  		bonds = nil
    37  	case err == nil:
    38  	default:
    39  		log.Errorf("getBondsForAccount error: %v", err)
    40  		return nil, nil
    41  	}
    42  
    43  	return acct, bonds
    44  }
    45  
    46  // AccountInfo returns data for an account.
    47  func (a *Archiver) AccountInfo(aid account.AccountID) (*db.Account, error) {
    48  	// bondExpiry time.Time and bonds return needed?
    49  	stmt := fmt.Sprintf(internal.SelectAccountInfo, a.tables.accounts)
    50  	acct := new(db.Account)
    51  	if err := a.db.QueryRow(stmt, aid).Scan(&acct.AccountID, &acct.Pubkey); err != nil {
    52  		if errors.Is(err, sql.ErrNoRows) {
    53  			err = db.ArchiveError{Code: db.ErrAccountUnknown}
    54  		}
    55  		return nil, err
    56  	}
    57  	return acct, nil
    58  }
    59  
    60  // CreateAccountWithBond creates a new account with a fidelity bond.
    61  func (a *Archiver) CreateAccountWithBond(acct *account.Account, bond *db.Bond) error {
    62  	dbTx, err := a.db.BeginTx(a.ctx, nil)
    63  	if err != nil {
    64  		return err
    65  	}
    66  	defer func() {
    67  		if err == nil || errors.Is(err, sql.ErrTxDone) {
    68  			return
    69  		}
    70  		if errR := dbTx.Rollback(); errR != nil {
    71  			log.Errorf("Rollback failed: %v", errR)
    72  		}
    73  	}()
    74  
    75  	err = createAccountForBond(dbTx, a.tables.accounts, acct)
    76  	if err != nil {
    77  		return err
    78  	}
    79  	err = addBond(dbTx, a.tables.bonds, acct.ID, bond)
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	err = dbTx.Commit() // for the defer
    85  	return err
    86  }
    87  
    88  // AddBond stores a new Bond for an existing account.
    89  func (a *Archiver) AddBond(aid account.AccountID, bond *db.Bond) error {
    90  	return addBond(a.db, a.tables.bonds, aid, bond)
    91  }
    92  
    93  func (a *Archiver) DeleteBond(assetID uint32, coinID []byte) error {
    94  	return deleteBond(a.db, a.tables.bonds, assetID, coinID)
    95  }
    96  
    97  func (a *Archiver) FetchPrepaidBond(coinID []byte) (strength uint32, lockTime int64, err error) {
    98  	stmt := fmt.Sprintf(internal.SelectPrepaidBond, prepaidBondsTableName)
    99  	err = a.db.QueryRow(stmt, coinID).Scan(&strength, &lockTime)
   100  	return
   101  }
   102  
   103  func (a *Archiver) DeletePrepaidBond(coinID []byte) (err error) {
   104  	stmt := fmt.Sprintf(internal.DeletePrepaidBond, prepaidBondsTableName)
   105  	_, err = a.db.ExecContext(a.ctx, stmt, coinID)
   106  	return
   107  }
   108  
   109  func (a *Archiver) StorePrepaidBonds(coinIDs [][]byte, strength uint32, lockTime int64) error {
   110  	stmt := fmt.Sprintf(internal.InsertPrepaidBond, prepaidBondsTableName)
   111  	for i := range coinIDs {
   112  		if _, err := a.db.ExecContext(a.ctx, stmt, coinIDs[i], strength, lockTime); err != nil {
   113  			return err
   114  		}
   115  	}
   116  	return nil
   117  }
   118  
   119  // KeyIndex returns the current child index for the an xpub. If it is not
   120  // known, this creates a new entry with index zero.
   121  func (a *Archiver) KeyIndex(xpub string) (uint32, error) {
   122  	keyHash := dcrutil.Hash160([]byte(xpub))
   123  
   124  	var child uint32
   125  	stmt := fmt.Sprintf(internal.CurrentKeyIndex, feeKeysTableName)
   126  	err := a.db.QueryRow(stmt, keyHash).Scan(&child)
   127  	switch {
   128  	case errors.Is(err, sql.ErrNoRows): // continue to create new entry
   129  	case err == nil:
   130  		return child, nil
   131  	default:
   132  		return 0, err
   133  	}
   134  
   135  	log.Debugf("Inserting key entry for xpub %.40s..., hash160 = %x", xpub, keyHash)
   136  	stmt = fmt.Sprintf(internal.InsertKeyIfMissing, feeKeysTableName)
   137  	err = a.db.QueryRow(stmt, keyHash).Scan(&child)
   138  	if err != nil {
   139  		return 0, err
   140  	}
   141  	return child, nil
   142  }
   143  
   144  // SetKeyIndex records the child index for an xpub. An error is returned
   145  // unless exactly 1 row is updated or created.
   146  func (a *Archiver) SetKeyIndex(idx uint32, xpub string) error {
   147  	keyHash := dcrutil.Hash160([]byte(xpub))
   148  	log.Debugf("Recording new index %d for xpub %.40s... (%x)", idx, xpub, keyHash)
   149  	stmt := fmt.Sprintf(internal.UpsertKeyIndex, feeKeysTableName)
   150  	res, err := a.db.Exec(stmt, idx, keyHash)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	N, err := res.RowsAffected()
   155  	if err != nil {
   156  		return err
   157  	}
   158  	if N != 1 {
   159  		return fmt.Errorf("updated %d rows, expected 1", N)
   160  	}
   161  	return nil
   162  }
   163  
   164  // createAccountTables creates the accounts and fee_keys tables.
   165  func createAccountTables(db sqlQueryExecutor) error {
   166  	for _, c := range createAccountTableStatements {
   167  		created, err := createTable(db, publicSchema, c.name)
   168  		if err != nil {
   169  			return err
   170  		}
   171  		if created {
   172  			log.Tracef("Table %s created", c.name)
   173  		}
   174  	}
   175  
   176  	for _, c := range createBondIndexesStatements {
   177  		err := createIndexStmt(db, c.stmt, c.idxName, bondsTableName)
   178  		if err != nil {
   179  			return err
   180  		}
   181  	}
   182  
   183  	return nil
   184  }
   185  
   186  // getAccount gets retrieves the account details, including the pubkey, a flag
   187  // indicating if the account was created with a legacy fee address (not a
   188  // fidelity bond), and a flag indicating if that legacy fee was paid.
   189  func getAccount(dbe sqlQueryer, tableName string, aid account.AccountID) (acct *account.Account, err error) {
   190  	var pubkey []byte
   191  	stmt := fmt.Sprintf(internal.SelectAccount, tableName)
   192  	err = dbe.QueryRow(stmt, aid).Scan(&pubkey)
   193  	if err != nil {
   194  		return
   195  	}
   196  	acct, err = account.NewAccountFromPubKey(pubkey)
   197  	if err != nil {
   198  		return
   199  	}
   200  	return
   201  }
   202  
   203  // createAccountForBond creates an entry for the account in the accounts table.
   204  func createAccountForBond(dbe sqlExecutor, tableName string, acct *account.Account) error {
   205  	stmt := fmt.Sprintf(internal.CreateAccountForBond, tableName)
   206  	_, err := dbe.Exec(stmt, acct.ID, acct.PubKey.SerializeCompressed())
   207  	return err
   208  }
   209  
   210  func addBond(dbe sqlExecutor, tableName string, aid account.AccountID, bond *db.Bond) error {
   211  	stmt := fmt.Sprintf(internal.AddBond, tableName)
   212  	_, err := dbe.Exec(stmt, bond.Version, bond.CoinID, bond.AssetID, aid,
   213  		bond.Amount, bond.Strength, bond.LockTime)
   214  	return err
   215  }
   216  
   217  func deleteBond(dbe sqlExecutor, tableName string, assetID uint32, coinID []byte) error {
   218  	stmt := fmt.Sprintf(internal.DeleteBond, tableName)
   219  	_, err := dbe.Exec(stmt, coinID, assetID)
   220  	return err
   221  }
   222  
   223  func getBondsForAccount(dbe sqlQueryer, tableName string, acct account.AccountID, bondExpiryTime int64) ([]*db.Bond, error) {
   224  	stmt := fmt.Sprintf(internal.SelectActiveBondsForUser, tableName)
   225  	rows, err := dbe.Query(stmt, acct, bondExpiryTime)
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  	defer rows.Close()
   230  
   231  	var bonds []*db.Bond
   232  	for rows.Next() {
   233  		var bond db.Bond
   234  		err = rows.Scan(&bond.Version, &bond.CoinID, &bond.AssetID,
   235  			&bond.Amount, &bond.Strength, &bond.LockTime)
   236  		if err != nil {
   237  			return nil, err
   238  		}
   239  		bonds = append(bonds, &bond)
   240  	}
   241  	if err = rows.Err(); err != nil {
   242  		return nil, err
   243  	}
   244  	return bonds, nil
   245  }