diff --git a/client/core/bond.go b/client/core/bond.go index c47c174fca..e5f1b90a97 100644 --- a/client/core/bond.go +++ b/client/core/bond.go @@ -159,9 +159,19 @@ func (c *Core) rotateBonds(ctx context.Context) { coinID []byte } - bondKeysReady := c.bondKeysReady() // nextBondKey requires login to decrypt bond xpriv + if !c.bondKeysReady() { // not logged in, and nextBondKey requires login to decrypt bond xpriv + return // nothing to do until wallets are connected on login + } for _, dc := range c.dexConnections() { + initialized, unlocked, _ := dc.acct.status() + if !initialized { + continue // view-only or temporary connection + } + // Account unlocked is generally implied by bondKeysReady, but we will + // check per-account before post since accounts can be individually + // locked. However, we must refund bonds regardless. + lockTimeThresh := now // in case dex is down, expire (to refund when lock time is passed) var bondExpiry int64 bondAssets := make(map[uint32]*msgjson.BondAsset) @@ -204,7 +214,6 @@ func (c *Core) rotateBonds(ctx context.Context) { } dc.acct.authMtx.Lock() - authed := dc.acct.isAuthed // can't post bonds if we aren't logged in tier, targetTier := dc.acct.tier, dc.acct.targetTier bondAssetID, maxBondedAmt := dc.acct.bondAsset, dc.acct.maxBondedAmt // Screen the unexpired bonds slices. @@ -229,6 +238,12 @@ func (c *Core) rotateBonds(ctx context.Context) { dc.acct.authMtx.Unlock() for _, bond := range repost { // outside of authMtx lock + if !unlocked { // can't sign the postbond msg + c.log.Warnf("Cannot post pending bond for %v until account is unlocked.", dc.acct.host) + continue + } + // Not dependent on authed - this may be the first bond + // (registering) where bondConfirmed does authDEX if needed. if bondAsset, ok := bondAssets[bond.AssetID]; ok { c.monitorBondConfs(dc, bond, bondAsset.Confs) } else { @@ -336,10 +351,10 @@ func (c *Core) rotateBonds(ctx context.Context) { expiredStrength := sumBondStrengths(dc.acct.expiredBonds) dc.acct.authMtx.Unlock() - if authed && mustPost > 0 && targetTier > 0 && bondExpiry > 0 { + if mustPost > 0 && targetTier > 0 && bondExpiry > 0 { c.log.Infof("Gotta post %d bond increments now. Target tier %d, current tier %d (%d weak, %d pending)", mustPost, targetTier, tier, weakStrength, pendingStrength) - if !bondKeysReady || dc.status() != comms.Connected { + if !unlocked || dc.status() != comms.Connected { c.log.Warnf("Unable to post the required bond while disconnected or logged out.") continue } @@ -722,6 +737,12 @@ func (c *Core) PostBond(form *PostBondForm) (*PostBondResult, error) { return nil, fmt.Errorf("app not initialized") } + // Check that the bond amount is non-zero before we touch wallets and make + // connections to the DEX host. + if form.Bond == 0 { + return nil, newError(bondAmtErr, "zero registration fees not allowed") + } + // Get the wallet to author the transaction. Default to DCR. bondAssetID := uint32(42) if form.Asset != nil { @@ -732,10 +753,13 @@ func (c *Core) PostBond(form *PostBondForm) (*PostBondResult, error) { if err != nil { return nil, fmt.Errorf("cannot connect to %s wallet to pay fee: %w", bondAssetSymbol, err) } - if _, ok := wallet.Wallet.(asset.Bonder); !ok { // will fail in MakeBondTx, but assert early return nil, fmt.Errorf("wallet %v is not an asset.Bonder", bondAssetSymbol) } + _, err = wallet.refreshUnlock() + if err != nil { + return nil, fmt.Errorf("bond asset wallet %v is locked", unbip(bondAssetID)) + } // Check the app password. crypter, err := c.encryptionKey(form.AppPass) @@ -771,6 +795,14 @@ func (c *Core) PostBond(form *PostBondForm) (*PostBondResult, error) { "(use UpdateBondOptions to change bond maintenance settings)") } } else { + // Before connecting to the DEX host, do a quick balance check to ensure + // we at least have the nominal bond amount available. + if bal, err := wallet.Balance(); err != nil { + return nil, newError(bondAssetErr, "unable to check wallet balance: %w", err) + } else if bal.Available < form.Bond { + return nil, newError(bondAssetErr, "insufficient available balance") + } + maxBondedAmt := 4 * form.Bond // default if form.MaxBondedAmt != nil { maxBondedAmt = *form.MaxBondedAmt @@ -841,10 +873,6 @@ func (c *Core) PostBond(form *PostBondForm) (*PostBondResult, error) { return nil, newError(bondTimeErr, "lock time of %d in the past", form.LockTime) } - // Check that the bond amount is non-zero. - if form.Bond == 0 { - return nil, newError(bondAmtErr, "zero registration fees not allowed") - } // Check that the bond amount matches the caller's expectations. if form.Bond < bondAsset.Amt { return nil, newError(bondAmtErr, "specified bond amount is less than the DEX-provided amount. %d < %d", @@ -956,6 +984,7 @@ func (c *Core) makeAndPostBond(dc *dexConnection, acctExists bool, wallet *xcWal c.connMtx.Lock() c.conns[dc.acct.host] = dc c.connMtx.Unlock() + // NOTE: it's still not authed if this was the first bond } // Broadcast the bond and start waiting for confs. diff --git a/client/core/types.go b/client/core/types.go index b5dde34270..ee8c413f20 100644 --- a/client/core/types.go +++ b/client/core/types.go @@ -841,7 +841,14 @@ func (a *dexAccount) lock() { a.keyMtx.Unlock() } -// locked will be true if the account private key is currently decrypted. +func (a *dexAccount) status() (initialized, unlocked, authed bool) { + a.keyMtx.RLock() + defer a.keyMtx.RUnlock() + return len(a.encKey) > 0, a.privKey != nil, a.isAuthed +} + +// locked will be true if the account private key is currently decrypted, or +// there are no account keys generated yet. func (a *dexAccount) locked() bool { a.keyMtx.RLock() defer a.keyMtx.RUnlock()