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