decred.org/dcrdex@v1.0.3/client/core/account.go (about)

     1  package core
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/hex"
     6  	"errors"
     7  	"fmt"
     8  	"math"
     9  
    10  	"decred.org/dcrdex/client/comms"
    11  	"decred.org/dcrdex/client/db"
    12  	"decred.org/dcrdex/dex/encode"
    13  	"decred.org/dcrdex/server/account"
    14  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    15  )
    16  
    17  // stopDEXConnection unsubscribes from the dex's orderbooks and ends the
    18  // connection with the dex. The dexConnection will still remain in c.conns map.
    19  func (c *Core) stopDEXConnection(dc *dexConnection) {
    20  	// Stop dexConnection books.
    21  	dc.cfgMtx.RLock()
    22  	if dc.cfg != nil {
    23  		for _, m := range dc.cfg.Markets {
    24  			// Empty bookie's feeds map, close feeds' channels & stop close timers.
    25  			dc.booksMtx.Lock()
    26  			if b, found := dc.books[m.Name]; found {
    27  				b.closeFeeds()
    28  				if b.closeTimer != nil {
    29  					b.closeTimer.Stop()
    30  				}
    31  			}
    32  			dc.booksMtx.Unlock()
    33  			dc.stopBook(m.Base, m.Quote)
    34  		}
    35  	}
    36  	dc.cfgMtx.RUnlock()
    37  	dc.connMaster.Disconnect() // disconnect
    38  }
    39  
    40  // disconnectDEX disconnects a dex and removes it from the connection map.
    41  func (c *Core) disconnectDEX(dc *dexConnection) {
    42  	// Disconnect and delete connection from map.
    43  	c.stopDEXConnection(dc)
    44  	c.connMtx.Lock()
    45  	delete(c.conns, dc.acct.host)
    46  	c.connMtx.Unlock()
    47  }
    48  
    49  // ToggleAccountStatus is used to disable or enable an account by given host and
    50  // application password.
    51  func (c *Core) ToggleAccountStatus(pw []byte, host string, disable bool) error {
    52  	// Validate password.
    53  	crypter, err := c.encryptionKey(pw)
    54  	if err != nil {
    55  		return codedError(passwordErr, err)
    56  	}
    57  
    58  	// Get dex connection by host. All exchange servers (enabled or not) are loaded as
    59  	// dexConnections but disabled servers are not connected.
    60  	dc, _, err := c.dex(host)
    61  	if err != nil {
    62  		return newError(unknownDEXErr, "error retrieving dex conn: %w", err)
    63  	}
    64  
    65  	if dc.acct.isDisabled() == disable {
    66  		return nil // no-op
    67  	}
    68  
    69  	if disable {
    70  		// Check active orders or bonds.
    71  		if dc.hasActiveOrders() {
    72  			return fmt.Errorf("cannot disable account with active orders")
    73  		}
    74  
    75  		if dc.hasUnspentBond() {
    76  			c.log.Info("Disabling dex server with unspent bonds. Bonds will be refunded when expired.")
    77  		}
    78  	}
    79  
    80  	err = c.db.ToggleAccountStatus(host, disable)
    81  	if err != nil {
    82  		return newError(accountStatusUpdateErr, "error updating account status: %w", err)
    83  	}
    84  
    85  	if disable {
    86  		dc.acct.toggleAccountStatus(true)
    87  		c.stopDEXConnection(dc)
    88  	} else {
    89  		acctInfo, err := c.db.Account(host)
    90  		if err != nil {
    91  			return err
    92  		}
    93  		dc, connected := c.connectAccount(acctInfo)
    94  		if !connected {
    95  			return fmt.Errorf("failed to connected re-enabled account: %w", err)
    96  		}
    97  		c.initializeDEXConnection(dc, crypter)
    98  	}
    99  
   100  	return nil
   101  }
   102  
   103  // AccountExport is used to retrieve account by host for export.
   104  func (c *Core) AccountExport(pw []byte, host string) (*Account, []*db.Bond, error) {
   105  	crypter, err := c.encryptionKey(pw)
   106  	if err != nil {
   107  		return nil, nil, codedError(passwordErr, err)
   108  	}
   109  	defer crypter.Close()
   110  	host, err = addrHost(host)
   111  	if err != nil {
   112  		return nil, nil, newError(addressParseErr, "error parsing address: %w", err)
   113  	}
   114  
   115  	// Load account info, including all bonds, from DB.
   116  	acctInf, err := c.db.Account(host)
   117  	if err != nil {
   118  		return nil, nil, newError(unknownDEXErr, "dex db load error: %w", err)
   119  	}
   120  
   121  	keyB, err := crypter.Decrypt(acctInf.EncKey())
   122  	if err != nil {
   123  		return nil, nil, err
   124  	}
   125  	privKey := secp256k1.PrivKeyFromBytes(keyB)
   126  	pubKey := privKey.PubKey()
   127  	accountID := account.NewID(pubKey.SerializeCompressed())
   128  
   129  	// Account ID is exported for informational purposes only, it is not used during import.
   130  	acct := &Account{
   131  		Host:      host,
   132  		AccountID: accountID.String(),
   133  		// PrivKey: Note that we don't differentiate between legacy and
   134  		// hierarchical private keys here. On import, all keys are treated as
   135  		// legacy keys.
   136  		PrivKey:   hex.EncodeToString(keyB),
   137  		DEXPubKey: hex.EncodeToString(acctInf.DEXPubKey.SerializeCompressed()),
   138  		Cert:      hex.EncodeToString(acctInf.Cert),
   139  	}
   140  	return acct, acctInf.Bonds, nil
   141  }
   142  
   143  // AccountImport is used import an existing account into the db.
   144  func (c *Core) AccountImport(pw []byte, acct *Account, bonds []*db.Bond) error {
   145  	crypter, err := c.encryptionKey(pw)
   146  	if err != nil {
   147  		return codedError(passwordErr, err)
   148  	}
   149  
   150  	host, err := addrHost(acct.Host)
   151  	if err != nil {
   152  		return newError(addressParseErr, "error parsing address: %w", err)
   153  	}
   154  
   155  	// Don't try to create and import an account for a DEX that we already know,
   156  	// but try to import missing bonds.
   157  	if acctInfo, err := c.db.Account(host); err == nil {
   158  		// Before importing bonds, make sure this is the same DEX (by public
   159  		// key) and same account ID, otherwise the bonds do not apply. The user
   160  		// can still refund by manually broadcasting the backup refund tx.
   161  		if acct.DEXPubKey != hex.EncodeToString(acctInfo.DEXPubKey.SerializeCompressed()) {
   162  			return errors.New("known dex host has different public key")
   163  		}
   164  		keyB, err := crypter.Decrypt(acctInfo.EncKey())
   165  		if err != nil {
   166  			return err
   167  		}
   168  		defer encode.ClearBytes(keyB)
   169  		privKey := secp256k1.PrivKeyFromBytes(keyB)
   170  		defer privKey.Zero()
   171  		accountID := account.NewID(privKey.PubKey().SerializeCompressed())
   172  		if acct.AccountID != accountID.String() {
   173  			return errors.New("known dex account has different identity")
   174  		}
   175  
   176  		c.log.Infof("Found existing account for %s. Merging bonds...", host)
   177  		haveBond := func(bond *db.Bond) *db.Bond {
   178  			for _, knownBond := range acctInfo.Bonds {
   179  				if bytes.Equal(knownBond.UniqueID(), bond.UniqueID()) {
   180  					return knownBond
   181  				}
   182  			}
   183  			return nil
   184  		}
   185  		var newLiveBonds int
   186  		for _, bond := range bonds {
   187  			have := haveBond(bond)
   188  			if have != nil && have.KeyIndex != math.MaxUint32 {
   189  				continue // we have this proper (not placeholder) bond already
   190  			}
   191  			if err = c.db.AddBond(host, bond); err != nil { // add OR update
   192  				return fmt.Errorf("importing bond: %v", err)
   193  			}
   194  			if have == nil {
   195  				acctInfo.Bonds = append(acctInfo.Bonds, bond)
   196  			} else { // else this is the placeholder from Unknown active bond reported by server
   197  				*have = *bond // update element in acctInfo.Bonds slice
   198  			}
   199  			if !bond.Refunded {
   200  				newLiveBonds++
   201  			}
   202  		}
   203  		if newLiveBonds == 0 {
   204  			return nil
   205  		}
   206  		c.log.Infof("Imported %d new unspent bonds", newLiveBonds)
   207  		if dc, connected, _ := c.dex(host); connected {
   208  			c.disconnectDEX(dc)
   209  			// TODO: less heavy handed approach to append or update
   210  			// dc.acct.{bonds,pendingBonds,expiredBonds}, using server config...
   211  		}
   212  		dc, err := c.connectDEX(acctInfo)
   213  		if err != nil {
   214  			return err
   215  		}
   216  		c.addDexConnection(dc)
   217  		c.initializeDEXConnection(dc, crypter)
   218  		return nil
   219  	}
   220  
   221  	accountInfo := db.AccountInfo{
   222  		Host:  host,
   223  		Bonds: bonds,
   224  	}
   225  
   226  	DEXpubKey, err := hex.DecodeString(acct.DEXPubKey)
   227  	if err != nil {
   228  		return codedError(decodeErr, err)
   229  	}
   230  	accountInfo.DEXPubKey, err = secp256k1.ParsePubKey(DEXpubKey)
   231  	if err != nil {
   232  		return codedError(parseKeyErr, err)
   233  	}
   234  
   235  	accountInfo.Cert, err = hex.DecodeString(acct.Cert)
   236  	if err != nil {
   237  		return codedError(decodeErr, err)
   238  	}
   239  
   240  	// Before we import the private key as LegacyEncKey, see if the account
   241  	// derives from the app seed. Somewhat inconsequential except for logging
   242  	// and use of the appropriate enc key field.
   243  	privKey, err := hex.DecodeString(acct.PrivKey)
   244  	if err != nil {
   245  		return codedError(decodeErr, err)
   246  	}
   247  	encKey, err := crypter.Encrypt(privKey)
   248  	if err != nil {
   249  		return codedError(encryptionErr, err)
   250  	}
   251  	dcAcct := newDEXAccount(&accountInfo, false)
   252  	creds := c.creds()
   253  	const maxRecoveryIndex = 1000
   254  	for keyIndex := uint32(0); keyIndex < maxRecoveryIndex; keyIndex++ {
   255  		err := dcAcct.setupCryptoV2(creds, crypter, keyIndex)
   256  		if err != nil {
   257  			return newError(acctKeyErr, "setupCryptoV2 error: %w", err)
   258  		}
   259  		if bytes.Equal(privKey, dcAcct.privKey.Serialize()) {
   260  			c.log.Debugf("Account derives from current application seed, with account key index %d", keyIndex)
   261  			accountInfo.EncKeyV2 = encKey
   262  			// Any unspent bonds for this account will refund using KeyIndex.
   263  			break
   264  		}
   265  	}
   266  	if len(accountInfo.EncKeyV2) == 0 {
   267  		c.log.Warnf("Account with foreign key imported. " +
   268  			"Any imported bonds will be refunded to the previous wallet!")
   269  		accountInfo.LegacyEncKey = encKey
   270  		// Any unspent bonds for this account will refund using the backup tx.
   271  	}
   272  	dcAcct.privKey.Zero()
   273  
   274  	err = c.db.CreateAccount(&accountInfo)
   275  	if err != nil {
   276  		return codedError(dbErr, err)
   277  	}
   278  
   279  	dc, err := c.connectDEX(&accountInfo)
   280  	if err != nil {
   281  		return err
   282  	}
   283  	c.addDexConnection(dc)
   284  	c.initializeDEXConnection(dc, crypter)
   285  	return nil
   286  }
   287  
   288  // UpdateCert attempts to connect to a server using a new TLS certificate. If
   289  // the connection is successful, then the cert in the database is updated.
   290  // Updating cert for already connected dex will return an error.
   291  func (c *Core) UpdateCert(host string, cert []byte) error {
   292  	c.connMtx.RLock()
   293  	dc, found := c.conns[host]
   294  	c.connMtx.RUnlock()
   295  	if found && dc.status() == comms.Connected {
   296  		return errors.New("dex is already connected")
   297  	}
   298  
   299  	acct, err := c.db.Account(host)
   300  	if err != nil {
   301  		return err
   302  	}
   303  
   304  	// Ensure user provides a new cert.
   305  	if bytes.Equal(acct.Cert, cert) {
   306  		return errors.New("provided cert is the same with the old cert")
   307  	}
   308  
   309  	// Stop reconnect retry for previous dex connection first but leave it in
   310  	// the map so it remains listed incase we need it in the interim.
   311  	if found {
   312  		dc.connMaster.Disconnect()
   313  		dc.acct.lock()
   314  		dc.booksMtx.Lock()
   315  		for m, b := range dc.books {
   316  			b.closeFeeds()
   317  			if b.closeTimer != nil {
   318  				b.closeTimer.Stop()
   319  			}
   320  			delete(dc.books, m)
   321  		}
   322  		dc.booksMtx.Unlock()
   323  	}
   324  
   325  	acct.Cert = cert
   326  	dc, err = c.connectDEX(acct)
   327  	if err != nil {
   328  		return fmt.Errorf("failed to connect using new cert (will attempt to restore old connection): %v", err)
   329  	}
   330  
   331  	err = c.db.UpdateAccountInfo(acct)
   332  	if err != nil {
   333  		return fmt.Errorf("failed to update account info: %w", err)
   334  	}
   335  
   336  	c.addDexConnection(dc)
   337  
   338  	return nil
   339  }
   340  
   341  // UpdateDEXHost updates the host for a connection to a dex. The dex at oldHost
   342  // and newHost must be the same dex, which means that the dex at both hosts use
   343  // the same public key.
   344  func (c *Core) UpdateDEXHost(oldHost, newHost string, appPW []byte, certI any) (*Exchange, error) {
   345  	if oldHost == newHost {
   346  		return nil, errors.New("old host and new host are the same")
   347  	}
   348  
   349  	crypter, err := c.encryptionKey(appPW)
   350  	if err != nil {
   351  		return nil, codedError(passwordErr, err)
   352  	}
   353  	defer crypter.Close()
   354  
   355  	oldDc, _, err := c.dex(oldHost)
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  
   360  	if oldDc.hasActiveOrders() {
   361  		return nil, fmt.Errorf("cannot update host while dex has active orders")
   362  	}
   363  
   364  	if oldDc.acct.dexPubKey == nil {
   365  		return nil, fmt.Errorf("cannot update host if dex public key is nil")
   366  	}
   367  
   368  	var updatedHost bool
   369  	newDc, err := c.tempDexConnection(newHost, certI)
   370  	if err != nil {
   371  		return nil, err
   372  	}
   373  
   374  	defer func() {
   375  		// Either disconnect or promote this connection.
   376  		if !updatedHost {
   377  			newDc.connMaster.Disconnect()
   378  			return
   379  		}
   380  		c.upgradeConnection(newDc)
   381  	}()
   382  
   383  	if !newDc.acct.dexPubKey.IsEqual(oldDc.acct.dexPubKey) {
   384  		return nil, fmt.Errorf("the dex at %s does not have the same public key as %s",
   385  			oldHost, newHost)
   386  	}
   387  
   388  	c.disconnectDEX(oldDc)
   389  
   390  	if !oldDc.acct.isViewOnly() { // view-only dc should not discoverAcct
   391  		_, err = c.discoverAccount(newDc, crypter)
   392  		if err != nil {
   393  			return nil, err
   394  		}
   395  	}
   396  
   397  	err = c.db.ToggleAccountStatus(oldDc.acct.host, true)
   398  	if err != nil {
   399  		return nil, newError(accountStatusUpdateErr, "error updating account status: %w", err)
   400  	}
   401  
   402  	updatedHost = true
   403  	return c.exchangeInfo(newDc), nil
   404  }