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 }