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 }