Skip to content

Commit

Permalink
client/{asset,core}: add UnlockCoinsOnLogin option
Browse files Browse the repository at this point in the history
This adds the core.Config.UnlockCoinsOnLogin option that indicates that
on wallet connect during login, or on creation of a new wallet, all
coins with the wallet should be unlocked.

To support this "unlock all" with the wallets, the ReturnCoins method
of the asset.Wallet interface now interprets a nil Coins slice to mean
that all coins should be unlocked. This is similar to the lockunspent
RPC's semantics.

The individual implementations of ReturnCoins are modified to recognize
a nil unspent input slice and accordingly request the wallet backend
unlock all coins.
  • Loading branch information
chappjc committed Dec 15, 2022
1 parent c35709c commit 9870a0c
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 4 deletions.
11 changes: 11 additions & 0 deletions client/asset/btc/btc.go
Expand Up @@ -2144,9 +2144,20 @@ func (btc *baseWallet) splitBaggageFees(maxFeeRate uint64) (swapInputSize, bagga
// ReturnCoins unlocks coins. This would be used in the case of a canceled or
// partially filled order. Part of the asset.Wallet interface.
func (btc *baseWallet) ReturnCoins(unspents asset.Coins) error {
if unspents == nil { // not just empty to make this harder to do accidentally
btc.log.Debugf("Returning all coins.")
btc.fundingMtx.Lock()
defer btc.fundingMtx.Unlock()
if err := btc.node.lockUnspent(true, nil); err != nil {
return err
}
btc.fundingCoins = make(map[outPoint]*utxo)
return nil
}
if len(unspents) == 0 {
return fmt.Errorf("cannot return zero coins")
}

ops := make([]*output, 0, len(unspents))
btc.log.Debugf("returning coins %s", unspents)
btc.fundingMtx.Lock()
Expand Down
10 changes: 10 additions & 0 deletions client/asset/btc/btc_test.go
Expand Up @@ -1149,6 +1149,16 @@ func TestReturnCoins(t *testing.T) {
t.Fatalf("no error for zero coins")
}

// nil unlocks all
wallet.fundingCoins[outPoint{*tTxHash, 0}] = &utxo{}
err = wallet.ReturnCoins(nil)
if err != nil {
t.Fatalf("error for nil coins: %v", err)
}
if len(wallet.fundingCoins) != 0 {
t.Errorf("all funding coins not unlocked")
}

// Have the RPC return negative response.
node.lockUnspentErr = tErr
err = wallet.ReturnCoins(coins)
Expand Down
4 changes: 2 additions & 2 deletions client/asset/btc/rpcclient.go
Expand Up @@ -469,9 +469,9 @@ func (wc *rpcClient) listUnspent() ([]*ListUnspentResult, error) {

// lockUnspent locks and unlocks outputs for spending. An output that is part of
// an order, but not yet spent, should be locked until spent or until the order
// is canceled or fails.
// is canceled or fails.
func (wc *rpcClient) lockUnspent(unlock bool, ops []*output) error {
var rpcops []*RPCOutpoint // To clear all, this must be nil, not empty slice.
var rpcops []*RPCOutpoint // To clear all, this must be nil->null, not empty slice.
for _, op := range ops {
rpcops = append(rpcops, &RPCOutpoint{
TxID: op.txHash().String(),
Expand Down
18 changes: 17 additions & 1 deletion client/asset/dcr/dcr.go
Expand Up @@ -1830,8 +1830,24 @@ func (dcr *ExchangeWallet) lockFundingCoins(fCoins []*fundingCoin) error {
// ReturnCoins unlocks coins. This would be necessary in the case of a canceled
// order. Coins belonging to the tradingAcct, if configured, are transferred to
// the unmixed account with the exception of unspent split tx outputs which are
// kept in the tradingAcct and may later be used to fund future orders.
// kept in the tradingAcct and may later be used to fund future orders. If
// called with a nil slice, all coins are returned and none are moved to the
// unmixed account.
func (dcr *ExchangeWallet) ReturnCoins(unspents asset.Coins) error {
if unspents == nil { // not just empty to make this harder to do accidentally
dcr.log.Debugf("Returning all coins.")
dcr.fundingMtx.Lock()
defer dcr.fundingMtx.Unlock()
if err := dcr.wallet.LockUnspent(dcr.ctx, true, nil); err != nil {
return err
}
dcr.fundingCoins = make(map[outPoint]*fundingCoin)
return nil
}
if len(unspents) == 0 {
return fmt.Errorf("cannot return zero coins")
}

dcr.fundingMtx.Lock()
returnedCoins, err := dcr.returnCoins(unspents)
dcr.fundingMtx.Unlock()
Expand Down
10 changes: 10 additions & 0 deletions client/asset/dcr/dcr_test.go
Expand Up @@ -1004,6 +1004,16 @@ func TestReturnCoins(t *testing.T) {
t.Fatalf("no error for zero coins")
}

// nil unlocks all
wallet.fundingCoins[outPoint{*tTxHash, 0}] = &fundingCoin{}
err = wallet.ReturnCoins(nil)
if err != nil {
t.Fatalf("error for nil coins: %v", err)
}
if len(wallet.fundingCoins) != 0 {
t.Errorf("all funding coins not unlocked")
}

// Have the RPC return negative response.
node.lockUnspentErr = tErr
err = wallet.ReturnCoins(coins)
Expand Down
6 changes: 5 additions & 1 deletion client/asset/interface.go
Expand Up @@ -274,7 +274,11 @@ type Wallet interface {
// PreRedeem gets a pre-redeem estimate for the specified order size.
PreRedeem(*PreRedeemForm) (*PreRedeem, error)
// ReturnCoins unlocks coins. This would be necessary in the case of a
// canceled order.
// canceled order. A nil Coins slice indicates to unlock all coins that the
// wallet may have locked, a syntax that should always be followed by
// FundingCoins for any active orders, and is thus only appropriate at time
// of login. Unlocking all coins is likely only useful for external wallets
// whose lifetime is longer than the asset.Wallet instance.
ReturnCoins(Coins) error
// FundingCoins gets funding coins for the coin IDs. The coins are locked.
// This method might be called to reinitialize an order from data stored
Expand Down
14 changes: 14 additions & 0 deletions client/core/core.go
Expand Up @@ -1183,6 +1183,9 @@ type Config struct {
// on shutdown. This is useful if the consumer is using the BackupDB method,
// or simply creating manual backups of the DB file after shutdown.
NoAutoDBBackup bool // zero value is legacy behavior
// UnlockCoinsOnLogin indicates that on wallet connect during login, or on
// creation of a new wallet, all coins with the wallet should be unlocked.
UnlockCoinsOnLogin bool
}

// Core is the core client application. Core manages DEX connections, wallets,
Expand Down Expand Up @@ -1928,6 +1931,12 @@ func (c *Core) CreateWallet(appPW, walletPW []byte, form *WalletForm) error {
return err
}

if c.cfg.UnlockCoinsOnLogin {
if err = wallet.ReturnCoins(nil); err != nil {
c.log.Errorf("Failed to unlock all %s wallet coins: %v", unbip(wallet.AssetID), err)
}
}

initErr := func(s string, a ...interface{}) error {
_ = wallet.Lock(2 * time.Second) // just try, but don't confuse the user with an error
wallet.Disconnect()
Expand Down Expand Up @@ -3742,6 +3751,11 @@ func (c *Core) Login(pw []byte) (*LoginResult, error) {
db.ErrorLevel, wallet.state()))
return
}
if c.cfg.UnlockCoinsOnLogin {
if err = wallet.ReturnCoins(nil); err != nil {
c.log.Errorf("Failed to unlock all %s wallet coins: %v", unbip(wallet.AssetID), err)
}
}
}
atomic.AddUint32(&connectCount, 1)
}(wallet)
Expand Down

0 comments on commit 9870a0c

Please sign in to comment.