From 25d80b5bda037e705c81b9b18072738b584f76ab Mon Sep 17 00:00:00 2001 From: Philemon Ukane Date: Fri, 22 Dec 2023 10:17:37 +0100 Subject: [PATCH] add support for btc wallet Signed-off-by: Philemon Ukane --- dexc/core.go | 7 +- go.mod | 5 +- go.sum | 8 +- libwallet/assets/btc/dex-wallet.go | 198 ++++++++++++++++++++++++++ libwallet/assets/btc/rescan.go | 4 +- libwallet/assets/btc/sync.go | 6 +- libwallet/assets/btc/wallet.go | 82 +++++++++-- libwallet/assets/dcr/dex_wallet.go | 90 +++++++++++- libwallet/assets_manager.go | 109 ++++++++++++-- ui/page/dcrdex/dcrdex_page.go | 4 +- ui/page/dcrdex/dex_interface.go | 2 +- ui/page/dcrdex/dex_onboarding_page.go | 46 ++++-- 12 files changed, 507 insertions(+), 54 deletions(-) create mode 100644 libwallet/assets/btc/dex-wallet.go diff --git a/dexc/core.go b/dexc/core.go index a24432100..dac856267 100644 --- a/dexc/core.go +++ b/dexc/core.go @@ -18,9 +18,10 @@ const ( // CustomDexWalletType is a keyword that identifies a custom Cryptopower // wallet used by the DEX client. CustomDexWalletType = "cryptopowerwallet" - // DexDcrWalletIDConfigKey is the key that holds the wallet ID value in the - // settings map used to connect an existing dcr wallet to the DEX client. - DexDcrWalletIDConfigKey = "walletid" + // DexWalletIDConfigKey is the key that holds the wallet ID value in the + // settings map used to connect an existing Cryptopower wallet to the DEX + // client. + DexWalletIDConfigKey = "walletid" // DexDcrWalletAccountNameConfigKey is the key that holds the wallet account // values in the settings map used to connect an existing dcr wallet to the // DEX client. diff --git a/go.mod b/go.mod index a3954b006..f03577030 100644 --- a/go.mod +++ b/go.mod @@ -116,7 +116,8 @@ require ( github.com/decred/dcrd/txscript/v3 v3.0.0 // indirect github.com/decred/dcrtime v0.0.0-20191018193024-8d8b4ef0458e // indirect github.com/decred/go-socks v1.1.0 // indirect - github.com/decred/vspd/client/v2 v2.0.0 // indirect + github.com/decred/vspd/client/v2 v2.1.0 // indirect + github.com/decred/vspd/types v1.1.0 // indirect github.com/decred/vspd/types/v2 v2.0.0 // indirect github.com/dgraph-io/ristretto v0.0.2 // indirect github.com/dustin/go-humanize v1.0.1-0.20210705192016-249ff6c91207 // indirect @@ -227,3 +228,5 @@ replace github.com/lib/pq => github.com/lib/pq v1.10.4 // neccessary to work with dcrdex which has latest version of // github.com/btcsuite/btcwallet. replace github.com/btcsuite/btcwallet v0.16.10-0.20230706223227-037580c66b74 => github.com/btcsuite/btcwallet v0.16.9 + +replace decred.org/dcrdex v0.6.3 => ../../decred/dcrdex diff --git a/go.sum b/go.sum index 5497d7554..6af69162b 100644 --- a/go.sum +++ b/go.sum @@ -83,8 +83,6 @@ contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcig decred.org/cspp v0.3.0/go.mod h1:UygjYilC94dER3BEU65Zzyoqy9ngJfWCD2rdJqvUs2A= decred.org/cspp/v2 v2.1.0 h1:HeHb9+BFqrBaAPc6CsPiUpPFmC1uyBM2mJZUAbUXkRw= decred.org/cspp/v2 v2.1.0/go.mod h1:9nO3bfvCheOPIFZw5f6sRQ42CjBFB5RKSaJ9Iq6G4MA= -decred.org/dcrdex v0.6.3 h1:XrqbF5O+CFhQws+eugzqQoPgvjrFKgpYSkfKGAbD3NM= -decred.org/dcrdex v0.6.3/go.mod h1:zzHSNtu94lYxCBUwkuEmWGUL1wlO4pFzMoaPTGoqtBo= decred.org/dcrwallet v1.7.0 h1:U/ew00YBdUlx3rJAynt2OdKDgGzBKK4O89FijBq8iVg= decred.org/dcrwallet v1.7.0/go.mod h1:hNOGyvH53gWdgFB601/ubGRzCPfPtWnEVAi9Grs90y4= decred.org/dcrwallet/v3 v3.0.1 h1:+OLi+u/MvKc3Ubcnf19oyG/a5hJ/qp4OtezdiQZnLIs= @@ -557,8 +555,10 @@ github.com/decred/slog v1.0.0/go.mod h1:zR98rEZHSnbZ4WHZtO0iqmSZjDLKhkXfrPTZQKtA github.com/decred/slog v1.1.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0= github.com/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM= github.com/decred/slog v1.2.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0= -github.com/decred/vspd/client/v2 v2.0.0 h1:gaSF1Bm2/EvoAiSLxNR5fgStrObAO66xmanhedidYIM= -github.com/decred/vspd/client/v2 v2.0.0/go.mod h1:IDDviEe/6CuxxrW0PLOcg448enU3YmeElFHledYHw78= +github.com/decred/vspd/client/v2 v2.1.0 h1:RzwmM/FCvpJDskNMeqeJ8UNnlR7kLCl3JlG8iZiLbG0= +github.com/decred/vspd/client/v2 v2.1.0/go.mod h1:r/CtdQF7TmuoIaFuanHtUMYYlQxWgRBGapdn4b+Bouc= +github.com/decred/vspd/types v1.1.0 h1:hTeqQwgRUN2FGIbuCIdyzBejKV+jxKrmEIcLKxpsB1g= +github.com/decred/vspd/types v1.1.0/go.mod h1:THsO8aBSwWBq6ZsIG25cNqbkNb+EEASXzLhFvODVc0s= github.com/decred/vspd/types/v2 v2.0.0 h1:FaPA+W4OOMRWK+Vk4fyyYdXoVLRMMRQsxzsnSjJjOnI= github.com/decred/vspd/types/v2 v2.0.0/go.mod h1:2xnNqedkt9GuL+pK8uIzDxqYxFlwLRflYFJH64b76n0= github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA= diff --git a/libwallet/assets/btc/dex-wallet.go b/libwallet/assets/btc/dex-wallet.go new file mode 100644 index 000000000..e00b560ec --- /dev/null +++ b/libwallet/assets/btc/dex-wallet.go @@ -0,0 +1,198 @@ +// This code is available on the terms of the project LICENSE.md file, and as +// terms of the BlueOak License. See: https://blueoakcouncil.org/license/1.0.0. + +package btc + +// Note: Most of the code here is a copy-pasta from: +// https://github.com/decred/dcrdex/blob/master/client/asset/btc/spv.go + +import ( + "context" + "errors" + "fmt" + "time" + + "decred.org/dcrdex/client/asset" + dexbtc "decred.org/dcrdex/client/asset/btc" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/wallet/txauthor" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +// DEXWallet wraps *wallet.Wallet and implements dexbtc.BTCWallet. +type DEXWallet struct { + *wallet.Wallet // Implements most of dexbtc.BTCWallet + asset *Asset +} + +var _ dexbtc.BTCWallet = (*DEXWallet)(nil) + +// NewDEXWallet returns a new *DEXWallet. +func NewDEXWallet(asset *Asset) *DEXWallet { + return &DEXWallet{ + Wallet: asset.Internal().BTC, + asset: asset, + } +} + +// The below methods are not implemented by *wallet.Wallet, so must be +// implemented by the BTCWallet implementation. + +func (dw *DEXWallet) WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) { + details, err := wallet.UnstableAPI(dw.Wallet).TxDetails(txHash) + if err != nil { + return nil, err + } + if details == nil { + return nil, dexbtc.WalletTransactionNotFound + } + + return details, nil +} + +func (dw *DEXWallet) SyncedTo() waddrmgr.BlockStamp { + return dw.Wallet.Manager.SyncedTo() +} + +func (dw *DEXWallet) SignTx(tx *wire.MsgTx) error { + var prevPkScripts [][]byte + var inputValues []btcutil.Amount + for _, txIn := range tx.TxIn { + _, txOut, _, _, err := dw.Wallet.FetchInputInfo(&txIn.PreviousOutPoint) + if err != nil { + return err + } + inputValues = append(inputValues, btcutil.Amount(txOut.Value)) + prevPkScripts = append(prevPkScripts, txOut.PkScript) + // Zero the previous witness and signature script or else + // AddAllInputScripts does some weird stuff. + txIn.SignatureScript = nil + txIn.Witness = nil + } + return txauthor.AddAllInputScripts(tx, prevPkScripts, inputValues, &secretSource{dw, dw.ChainParams()}) +} + +func (dw *DEXWallet) BlockNotifications(ctx context.Context) <-chan *dexbtc.BlockNotification { + cl := dw.Wallet.NtfnServer.TransactionNotifications() + ch := make(chan *dexbtc.BlockNotification, 1) + go func() { + defer cl.Done() + for { + select { + case note := <-cl.C: + if len(note.AttachedBlocks) > 0 { + lastBlock := note.AttachedBlocks[len(note.AttachedBlocks)-1] + select { + case ch <- &dexbtc.BlockNotification{ + Hash: *lastBlock.Hash, + Height: lastBlock.Height, + }: + default: + } + } + case <-ctx.Done(): + return + } + } + }() + return ch +} + +func (dw *DEXWallet) RescanAsync() error { + return dw.asset.rescanAsync() +} + +func (dw *DEXWallet) ForceRescan() { + dw.asset.forceRescan() +} + +func (dw *DEXWallet) Start() (dexbtc.SPVService, error) { + return dw.asset.chainClient, nil +} + +func (dw *DEXWallet) Reconfigure(*asset.WalletConfig, string) (bool, error) { + return false, errors.New("Reconfigure not supported for Cyptopower btc wallet") +} + +func (dw *DEXWallet) Birthday() time.Time { + return dw.Manager.Birthday() +} + +func (dw *DEXWallet) Peers() ([]*asset.WalletPeer, error) { + peers := dw.asset.chainClient.CS.Peers() + var walletPeers []*asset.WalletPeer + for i := range peers { + p := peers[i] + walletPeers = append(walletPeers, &asset.WalletPeer{ + Addr: p.Addr(), + Connected: p.Connected(), + Source: asset.WalletDefault, + }) + } + return walletPeers, nil +} + +func (dw *DEXWallet) AddPeer(address string) error { + dw.asset.SetSpecificPeer(address) + return nil +} + +func (dw *DEXWallet) RemovePeer(address string) error { + dw.asset.RemoveSpecificPeer(address) + return nil +} + +// secretSource is used to locate keys and redemption scripts while signing a +// transaction. secretSource satisfies the txauthor.SecretsSource interface. +type secretSource struct { + w *DEXWallet + chainParams *chaincfg.Params +} + +// ChainParams returns the chain parameters. +func (s *secretSource) ChainParams() *chaincfg.Params { + return s.chainParams +} + +// GetKey fetches a private key for the specified address. +func (s *secretSource) GetKey(addr btcutil.Address) (*btcec.PrivateKey, bool, error) { + ma, err := s.w.Wallet.AddressInfo(addr) + if err != nil { + return nil, false, err + } + + mpka, ok := ma.(waddrmgr.ManagedPubKeyAddress) + if !ok { + e := fmt.Errorf("managed address type for %v is `%T` but "+ + "want waddrmgr.ManagedPubKeyAddress", addr, ma) + return nil, false, e + } + + privKey, err := mpka.PrivKey() + if err != nil { + return nil, false, err + } + return privKey, ma.Compressed(), nil +} + +// GetScript fetches the redemption script for the specified p2sh/p2wsh address. +func (s *secretSource) GetScript(addr btcutil.Address) ([]byte, error) { + ma, err := s.w.Wallet.AddressInfo(addr) + if err != nil { + return nil, err + } + + msa, ok := ma.(waddrmgr.ManagedScriptAddress) + if !ok { + e := fmt.Errorf("managed address type for %v is `%T` but "+ + "want waddrmgr.ManagedScriptAddress", addr, ma) + return nil, e + } + return msa.Script() +} diff --git a/libwallet/assets/btc/rescan.go b/libwallet/assets/btc/rescan.go index 0f9d5ea37..a0602f2d1 100644 --- a/libwallet/assets/btc/rescan.go +++ b/libwallet/assets/btc/rescan.go @@ -159,7 +159,7 @@ func (asset *Asset) rescanAsync() error { } log.Infof("Synchronizing wallet (%s) with network...", asset.GetWalletName()) - asset.Internal().BTC.SynchronizeRPC(asset.chainClient) + asset.Internal().BTC.SynchronizeRPC(asset.chainClient.NeutrinoClient) return nil } @@ -379,7 +379,7 @@ func (asset *Asset) getblockStamp(height int32) (*waddrmgr.BlockStamp, error) { return nil, fmt.Errorf("invalid block height provided: Error: %v", err) } - block, err := asset.chainClient.GetBlock(startHash) + block, err := asset.chainClient.NeutrinoClient.GetBlock(startHash) if err != nil { return nil, fmt.Errorf("invalid block hash provided: Error: %v", err) } diff --git a/libwallet/assets/btc/sync.go b/libwallet/assets/btc/sync.go index 763baad85..86d63d526 100644 --- a/libwallet/assets/btc/sync.go +++ b/libwallet/assets/btc/sync.go @@ -343,7 +343,9 @@ func (asset *Asset) prepareChain() error { return err } - asset.chainClient = chain.NewNeutrinoClient(asset.chainParams, chainService) + asset.chainClient = &btcChainService{ + NeutrinoClient: chain.NewNeutrinoClient(asset.chainParams, chainService), + } return nil } @@ -510,7 +512,7 @@ func (asset *Asset) startSync() error { log.Infof("Synchronizing wallet (%s) with network...", asset.GetWalletName()) // Initializes the goroutines handling chain notifications, rescan progress and handlers. - asset.Internal().BTC.SynchronizeRPC(asset.chainClient) + asset.Internal().BTC.SynchronizeRPC(asset.chainClient.NeutrinoClient) select { // Wait for 5 seconds so that all goroutines initialized in SynchronizeRPC() diff --git a/libwallet/assets/btc/wallet.go b/libwallet/assets/btc/wallet.go index f7f0d37fc..c4dbf1ad4 100644 --- a/libwallet/assets/btc/wallet.go +++ b/libwallet/assets/btc/wallet.go @@ -9,6 +9,7 @@ import ( "sync" "time" + dexbtc "decred.org/dcrdex/client/asset/btc" "decred.org/dcrwallet/v3/errors" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcutil" @@ -37,7 +38,7 @@ var _ sharedW.Asset = (*Asset)(nil) type Asset struct { *sharedW.Wallet - chainClient *chain.NeutrinoClient + chainClient *btcChainService chainParams *chaincfg.Params TxAuthoredInfo *TxAuthor @@ -73,19 +74,47 @@ const ( defaultDBTimeout = time.Duration(100) ) -// neutrinoService is satisfied by *neutrino.ChainService. -type neutrinoService interface { - GetBlockHash(int64) (*chainhash.Hash, error) - BestBlock() (*headerfs.BlockStamp, error) - Peers() []*neutrino.ServerPeer - GetBlockHeight(hash *chainhash.Hash) (int32, error) - GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, error) - GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error) - GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error) - Stop() error +// btcChainService wraps *chain.NeutrinoClient in order to translate the +// neutrino.ServerPeer to the SPVPeer interface type as required by the dex btc +// pkg. +type btcChainService struct { + *chain.NeutrinoClient } -var _ neutrinoService = (*neutrino.ChainService)(nil) +func (s *btcChainService) Peers() []dexbtc.SPVPeer { + rawPeers := s.CS.Peers() + peers := make([]dexbtc.SPVPeer, 0, len(rawPeers)) + for _, p := range rawPeers { + peers = append(peers, p) + } + return peers +} + +func (s *btcChainService) AddPeer(addr string) error { + return s.CS.ConnectNode(addr, true) +} + +func (s *btcChainService) RemovePeer(addr string) error { + return s.CS.RemoveNodeByAddr(addr) +} + +func (s *btcChainService) BestBlock() (*headerfs.BlockStamp, error) { + return s.NeutrinoClient.CS.BestBlock() +} + +func (s *btcChainService) GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error) { + return s.CS.GetBlock(blockHash, options...) +} + +func (s *btcChainService) GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error) { + return s.CS.GetCFilter(blockHash, filterType, options...) +} + +func (s *btcChainService) Stop() error { + return s.CS.Stop() +} + +var _ dexbtc.SPVService = (*btcChainService)(nil) // CreateNewWallet creates a new wallet for the BTC asset. func CreateNewWallet(pass *sharedW.AuthInfo, params *sharedW.InitParams) (sharedW.Asset, error) { @@ -142,7 +171,7 @@ func initWalletLoader(chainParams *chaincfg.Params, dbDirPath string) loader.Ass // It validates the network type passed by fetching the chain parameters // associated with it for the BTC asset. It then generates the BTC loader interface // that is passed to be used upstream while creating the watch only wallet in the -// shared wallet implemenation. +// shared wallet implementation. // Immediately a watch only wallet is created, the function to safely cancel network sync // is set. There after returning the watch only wallet's interface. func CreateWatchOnlyWallet(walletName, extendedPublicKey string, params *sharedW.InitParams) (sharedW.Asset, error) { @@ -490,6 +519,33 @@ func (asset *Asset) SetSpecificPeer(address string) { }() } +func (asset *Asset) RemoveSpecificPeer(address string) { + sep := ";" + knownAddr := asset.ReadStringConfigValueForKey(sharedW.SpvPersistentPeerAddressesConfigKey, "") + + addrs1, addrs2, found := strings.Cut(knownAddr, address) + if !found { + return + } + + addrs1 = strings.TrimSuffix(addrs1, sep) + addrs2 = strings.TrimPrefix(addrs2, sep) + addrs := addrs1 + if addrs1 == "" { + addrs = addrs2 + } else if addrs2 != "" { + addrs += sep + addrs2 + } + + asset.SaveUserConfigValue(sharedW.SpvPersistentPeerAddressesConfigKey, knownAddr) + go func() { + err := asset.reloadChainService() + if err != nil { + log.Error(err) + } + }() +} + // GetExtendedPubKey returns the extended public key of the given account, // to do that it calls btcwallet's AccountProperties method, using KeyScopeBIP0084 // and the account number. On failure it returns error. diff --git a/libwallet/assets/dcr/dex_wallet.go b/libwallet/assets/dcr/dex_wallet.go index 379b75abf..92abbcd93 100644 --- a/libwallet/assets/dcr/dex_wallet.go +++ b/libwallet/assets/dcr/dex_wallet.go @@ -21,10 +21,10 @@ import ( walleterrors "decred.org/dcrwallet/v3/errors" walletjson "decred.org/dcrwallet/v3/rpc/jsonrpc/types" dcrwallet "decred.org/dcrwallet/v3/wallet" - sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/decred/dcrd/blockchain/stake/v5" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrutil/v4" chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4" "github.com/decred/dcrd/txscript/v4" "github.com/decred/dcrd/txscript/v4/stdaddr" @@ -33,17 +33,19 @@ import ( // DEXWallet wraps *Asset and implements dexdcr.Wallet. type DEXWallet struct { - *sharedW.Wallet - syncData *SyncData + *Asset + syncData *SyncData + tradingAccountName string } var _ dexdcr.Wallet = (*DEXWallet)(nil) // NewDEXWallet returns a new *DEXWallet. -func NewDEXWallet(w *sharedW.Wallet, syncData *SyncData) *DEXWallet { +func NewDEXWallet(tradingAccountName string, w *Asset, syncData *SyncData) *DEXWallet { return &DEXWallet{ - Wallet: w, - syncData: syncData, + Asset: w, + syncData: syncData, + tradingAccountName: tradingAccountName, } } @@ -64,6 +66,46 @@ func (dw *DEXWallet) SpvMode() bool { return true } +// Accounts returns the names of the accounts for use by the exchange wallet. +func (dw *DEXWallet) Accounts() dexdcr.XCWalletAccounts { + accts := dexdcr.XCWalletAccounts{ + PrimaryAccount: dw.tradingAccountName, + } + + if !dw.IsAccountMixerActive() { + return accts + } + + unMixedAcctNum := dw.UnmixedAccountNumber() + mixedAcctNum := dw.MixedAccountNumber() + accounts, err := dw.GetAccountsRaw() + if err != nil { + log.Errorf("error loading mixer account. %s", err) + return accts + } + + var mixedAccName, unMixedAcctName string + for _, acct := range accounts.Accounts { + if acct.Number == unMixedAcctNum { + unMixedAcctName = acct.Name + } else if acct.Number == mixedAcctNum { + mixedAccName = acct.Name + } + } + + // We only care about the default account. + if mixedAccName == "" { + log.Errorf("Account name not found for mixed account number %d", mixedAcctNum) + return accts + } + + return dexdcr.XCWalletAccounts{ + PrimaryAccount: mixedAccName, + UnmixedAccount: unMixedAcctName, + TradingAccount: dw.tradingAccountName, + } +} + // NotifyOnTipChange is not used, in favor of the tipNotifier pattern. See: // https://github.com/decred/dcrdex/blob/master/client/asset/dcr/spv.go#513. // Part of the Wallet interface. @@ -499,6 +541,40 @@ func (dw *DEXWallet) AddressPrivKey(ctx context.Context, addr stdaddr.Address) ( } // Part of the Wallet interface. -func (dw *DEXWallet) Reconfigure(_ context.Context, _ *dexasset.WalletConfig, _ dex.Network, _, _ string) (restart bool, err error) { +func (dw *DEXWallet) Reconfigure(_ context.Context, _ *dexasset.WalletConfig, _ dex.Network, _ string) (restart bool, err error) { return false, nil } + +// These methods are part of Wallet interface but required only by the +// dexasset.TicketBuyer interface, leave unimplemented. + +// PurchaseTickets purchases n tickets. vspHost and vspPubKey only +// needed for internal wallets. +func (dw *DEXWallet) PurchaseTickets(ctx context.Context, n int, vspHost, vspPubKey string) ([]*dexasset.Ticket, error) { + return nil, nil +} + +// Tickets returns current active ticket hashes up until they are voted +// or revoked. Includes unconfirmed tickets. +func (dw *DEXWallet) Tickets(ctx context.Context) ([]*dexasset.Ticket, error) { + return nil, nil +} + +// VotingPreferences returns current voting preferences. +func (dw *DEXWallet) VotingPreferences(ctx context.Context) ([]*walletjson.VoteChoice, []*dexasset.TBTreasurySpend, []*walletjson.TreasuryPolicyResult, error) { + return []*walletjson.VoteChoice{}, []*dexasset.TBTreasurySpend{}, []*walletjson.TreasuryPolicyResult{}, nil +} + +// SetVotingPreferences sets preferences used when a ticket is chosen to +// be voted on. +func (dw *DEXWallet) SetVotingPreferences(ctx context.Context, choices, tspendPolicy, treasuryPolicy map[string]string) error { + return nil +} + +func (dw *DEXWallet) SetTxFee(ctx context.Context, feePerKB dcrutil.Amount) error { + return nil +} + +func (dw *DEXWallet) StakeInfo(ctx context.Context) (*dcrwallet.StakeInfoData, error) { + return nil, nil +} diff --git a/libwallet/assets_manager.go b/libwallet/assets_manager.go index 55877b0f5..59e21c893 100644 --- a/libwallet/assets_manager.go +++ b/libwallet/assets_manager.go @@ -7,7 +7,6 @@ import ( "path/filepath" "sort" "strconv" - "strings" "sync" "time" @@ -24,7 +23,10 @@ import ( "github.com/crypto-power/cryptopower/ui/values" bolt "go.etcd.io/bbolt" + dexasset "decred.org/dcrdex/client/asset" + dexbtc "decred.org/dcrdex/client/asset/btc" dexDcr "decred.org/dcrdex/client/asset/dcr" + btccfg "github.com/btcsuite/btcd/chaincfg" "github.com/crypto-power/cryptopower/libwallet/assets/btc" "github.com/crypto-power/cryptopower/libwallet/assets/dcr" "github.com/crypto-power/cryptopower/libwallet/assets/ltc" @@ -909,19 +911,27 @@ func (mgr *AssetsManager) DexcReady() bool { // InitializeDEX initializes mgr.dexc. func (mgr *AssetsManager) InitializeDEX(ctx context.Context) { logDir := filepath.Dir(mgr.LogFile()) - dexc, err := dexc.Start(ctx, mgr.RootDir(), mgr.GetLanguagePreference(), logDir, mgr.GetLogLevels(), mgr.NetType(), 0 /* TODO: Make configurable */) + dexcl, err := dexc.Start(ctx, mgr.RootDir(), mgr.GetLanguagePreference(), logDir, mgr.GetLogLevels(), mgr.NetType(), 0 /* TODO: Make configurable */) if err != nil { log.Errorf("Error starting dex client: %v", err) return } mgr.dexcMtx.Lock() - mgr.dexc = dexc + mgr.dexc = dexcl mgr.dexcMtx.Unlock() - err = mgr.PrepareDexSupportForDcrWallet() + // Prepare support for DCR wallet. + err = mgr.PrepareDexSupportForDCRWallet() if err != nil { - log.Errorf("Error preparing wallet support for dex client: %v", err) + log.Errorf("Error preparing dcr wallet support for dex client: %v", err) + return + } + + // Prepare support for BTC wallet. + err = mgr.PrepareDexSupportForBTCWallet() + if err != nil { + log.Errorf("Error preparing btc wallet support for dex client: %v", err) return } @@ -933,14 +943,14 @@ func (mgr *AssetsManager) InitializeDEX(ctx context.Context) { }() } -// PrepareDexSupportForDcrWallet sets up the DEX client to allow using a +// PrepareDexSupportForDCRWallet sets up the DEX client to allow using a // cyptopower dcr wallet with DEX core. -func (mgr *AssetsManager) PrepareDexSupportForDcrWallet() error { +func (mgr *AssetsManager) PrepareDexSupportForDCRWallet() error { // Build a custom wallet definition with custom config options // for use by the dex dcr ExchangeWallet. customWalletConfigOpts := []*asset.ConfigOption{ { - Key: dexc.DexDcrWalletIDConfigKey, + Key: dexc.DexWalletIDConfigKey, DisplayName: "Wallet ID", Description: "ID of existing wallet to use", }, @@ -961,7 +971,7 @@ func (mgr *AssetsManager) PrepareDexSupportForDcrWallet() error { // setup a dcr ExchangeWallet; it allows us to use an existing // wallet instance for wallet operations instead of json-rpc. var walletMaker = func(settings map[string]string, chainParams *dcrcfg.Params, logger dex.Logger) (dexDcr.Wallet, error) { - walletIDStr := settings[dexc.DexDcrWalletIDConfigKey] + walletIDStr := settings[dexc.DexWalletIDConfigKey] walletID, err := strconv.Atoi(walletIDStr) if err != nil || walletID < 0 { return nil, fmt.Errorf("invalid wallet ID %q in settings", walletIDStr) @@ -992,11 +1002,88 @@ func (mgr *AssetsManager) PrepareDexSupportForDcrWallet() error { return nil, fmt.Errorf("DEX wallet not supported for %s", walletParams.Name) } - return dcr.NewDEXWallet(dcrAsset.Wallet, dcrAsset.SyncData()), nil + return dcr.NewDEXWallet(accountName, dcrAsset, dcrAsset.SyncData()), nil } err := dexDcr.RegisterCustomWallet(walletMaker, def) - if err != nil && !strings.Contains(err.Error(), "already support" /* this is part of the error string returned if we've already registered a customer wallet */) { + if err != nil && !errors.Is(err, dexasset.ErrWalletTypeAlreadySupported) { // this is the only error from RegisterCustomWallet, ignore? + return err + } + + return nil +} + +// PrepareDexSupportForBTCWallet sets up the DEX client to allow using a +// cyptopower btc wallet with DEX core. +func (mgr *AssetsManager) PrepareDexSupportForBTCWallet() error { + // Build a custom wallet definition with custom config options for use by + // the dexbtc.ExchangeWalletSPV. + customWalletConfigOpts := []*asset.ConfigOption{ + { + Key: dexc.DexWalletIDConfigKey, + DisplayName: "Wallet ID", + Description: "ID of existing wallet to use", + }, + { + Key: dexbtc.WalletAccountNameConfigKey, + DisplayName: "Wallet Account Name", + Description: "Account name of the selected wallet", + }, + { + Key: dexbtc.WalletAccountNumberConfigKey, + DisplayName: "Wallet Account Number", + Description: "Account number of the selected wallet", + }, + } + + def := &asset.WalletDefinition{ + Type: dexc.CustomDexWalletType, + Description: "Uses an existing cryptopower Wallet instance instead of an rpc connection.", + ConfigOpts: customWalletConfigOpts, + } + + // This function will be invoked when the DEX client needs to setup a + // dexbtc.BTCWallet; it allows us to use an existing wallet instance for + // wallet operations instead of json-rpc. + var btcWalletConstructor = func(settings map[string]string, chainParams *btccfg.Params) (dexbtc.BTCWallet, error) { + walletIDStr := settings[dexc.DexWalletIDConfigKey] + walletID, err := strconv.Atoi(walletIDStr) + if err != nil || walletID < 0 { + return nil, fmt.Errorf("invalid wallet ID %q in settings", walletIDStr) + } + + wallet := mgr.WalletWithID(walletID) + if wallet == nil { + return nil, fmt.Errorf("no wallet exists with ID %q", walletIDStr) + } + + walletParams := wallet.Internal().BTC.ChainParams() + if walletParams.Net != chainParams.Net { + return nil, fmt.Errorf("selected wallet is for %s network, expected %s", walletParams.Name, chainParams.Name) + } + + if wallet.IsWatchingOnlyWallet() { + return nil, fmt.Errorf("cannot use watch only wallet for DEX trade") + } + + // Ensure the wallet account name exists. + accountName := settings[dexbtc.WalletAccountNameConfigKey] + accountNumber, err := wallet.AccountNumber(accountName) + if err != nil { + return nil, fmt.Errorf("error checking selected DEX account: %w", err) + } + + configAcctNumber, accountNumberStr := settings[dexbtc.WalletAccountNumberConfigKey], fmt.Sprint(accountNumber) + if configAcctNumber != accountNumberStr { + return nil, fmt.Errorf("config account number for wallet account(%s) does not match actual account number, expected %s got %s", + accountName, accountNumberStr, configAcctNumber) + } + + return btc.NewDEXWallet(wallet.(*btc.Asset)), nil + } + + err := dexbtc.RegisterCustomSPVWallet(btcWalletConstructor, def) + if err != nil && !errors.Is(err, dexasset.ErrWalletTypeAlreadySupported) { // this is the only error from RegisterCustomSPVWallet, ignore? return err } diff --git a/ui/page/dcrdex/dcrdex_page.go b/ui/page/dcrdex/dcrdex_page.go index 37ff09bc0..b8aec99be 100644 --- a/ui/page/dcrdex/dcrdex_page.go +++ b/ui/page/dcrdex/dcrdex_page.go @@ -172,8 +172,8 @@ func pendingBondConfirmation(am *libwallet.AssetsManager) (string, *core.BondAss xcs := am.DexClient().Exchanges() if len(xcs) == 1 { // first or only exchange for _, xc := range xcs { - if len(xc.PendingBonds) == 1 { - for _, bond := range xc.PendingBonds { + if len(xc.Auth.PendingBonds) == 1 && xc.Auth.LiveStrength == 0 { + for _, bond := range xc.Auth.PendingBonds { bondAsset := xc.BondAssets[bond.Symbol] if bond.Confs < bondAsset.Confs { return xc.Host, bondAsset, bond diff --git a/ui/page/dcrdex/dex_interface.go b/ui/page/dcrdex/dex_interface.go index f783540b7..5271fe464 100644 --- a/ui/page/dcrdex/dex_interface.go +++ b/ui/page/dcrdex/dex_interface.go @@ -15,5 +15,5 @@ type dexClient interface { HasWallet(assetID int32) bool AddWallet(assetID uint32, settings map[string]string, appPW, walletPW []byte) error PostBond(form *core.PostBondForm) (*core.PostBondResult, error) - NotificationFeed() <-chan core.Notification + NotificationFeed() *core.NoteFeed } diff --git a/ui/page/dcrdex/dex_onboarding_page.go b/ui/page/dcrdex/dex_onboarding_page.go index 8c54b8ba6..a3c723717 100644 --- a/ui/page/dcrdex/dex_onboarding_page.go +++ b/ui/page/dcrdex/dex_onboarding_page.go @@ -1,6 +1,7 @@ package dcrdex import ( + "context" "fmt" "image" "image/color" @@ -16,6 +17,7 @@ import ( "gioui.org/widget" "gioui.org/widget/material" + dexbtc "decred.org/dcrdex/client/asset/btc" "github.com/crypto-power/cryptopower/app" "github.com/crypto-power/cryptopower/dexc" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" @@ -95,7 +97,9 @@ type DEXOnboarding struct { scrollContainer *widget.List - dexc dexClient + ctx context.Context + cancelCtx context.CancelFunc + dexc dexClient currentStep onboardingStep onBoardingSteps map[onboardingStep]dexOnboardingStep @@ -214,6 +218,7 @@ func NewDEXOnboarding(l *load.Load) *DEXOnboarding { // the page is displayed. // Part of the load.Page interface. func (pg *DEXOnboarding) OnNavigatedTo() { + pg.ctx, pg.cancelCtx = context.WithCancel(context.Background()) pg.checkForPendingBondPayment() } @@ -225,6 +230,8 @@ func (pg *DEXOnboarding) OnNavigatedTo() { // components unless they'll be recreated in the OnNavigatedTo() method. // Part of the load.Page interface. func (pg *DEXOnboarding) OnNavigatedFrom() { + pg.cancelCtx() + // Empty dex pass for i := range pg.dexPass { pg.dexPass[i] = 0 @@ -1032,6 +1039,7 @@ func (pg *DEXOnboarding) postBond() { Cert: pg.bondServer.cert, FeeBuffer: pg.dexc.BondsFeeBuffer(bondAsset.ID), MaintainTier: &maintainTier, + // LockTime: uint64(time.Now().Add(5 * time.Hour).Unix()), // TODO: For testing and early refund. } // postBondFn sends the actual request to post bond. @@ -1039,8 +1047,17 @@ func (pg *DEXOnboarding) postBond() { // Add bond wallet to core if it does not exist. if !pg.dexc.HasWallet(int32(bondAsset.ID)) { cfg := map[string]string{ - dexc.DexDcrWalletIDConfigKey: fmt.Sprintf("%d", asset.GetWalletID()), - dexc.DexDcrWalletAccountNameConfigKey: pg.bondSourceAccountSelector.SelectedAccount().AccountName, + dexc.DexWalletIDConfigKey: fmt.Sprintf("%d", asset.GetWalletID()), + } + + selectedAcct := pg.bondSourceAccountSelector.SelectedAccount() + if asset.GetAssetType() == libutils.BTCWalletAsset { + cfg[dexbtc.WalletAccountNameConfigKey] = selectedAcct.Name + cfg[dexbtc.WalletAccountNumberConfigKey] = fmt.Sprint(selectedAcct.AccountNumber) + } else if asset.GetAssetType() == libutils.DCRWalletAsset { + // TODO: Remove this config value if using latest dex pkg + // version(i.e > 0.6.3). + cfg[dexc.DexDcrWalletAccountNameConfigKey] = selectedAcct.Name } err := pg.dexc.AddWallet(*postBond.Asset, cfg, pg.dexPass, []byte(walletPass)) @@ -1087,6 +1104,23 @@ func (pg *DEXOnboarding) waitForConfirmationAndListenForBlockNotifications() { pg.currentStep = onBoardingStepWaitForConfirmation pg.scrollContainer.Position.Offset = 0 + noteFeed := pg.dexc.NotificationFeed() + go func() { + for { + select { + case n := <-noteFeed.C: + if n.Topic() == core.TopicBondConfirmed { + noteFeed.ReturnFeed() + pg.ParentNavigator().ClearStackAndDisplay(NewDEXMarketPage(pg.Load)) + return + } + case <-pg.ctx.Done(): + noteFeed.ReturnFeed() + return + } + } + }() + // Listen for new block updates. This listener is removed in // OnNavigateFrom(). asset := pg.bondSourceAccountSelector.SelectedWallet() @@ -1094,11 +1128,7 @@ func (pg *DEXOnboarding) waitForConfirmationAndListenForBlockNotifications() { asset.AddTxAndBlockNotificationListener(&sharedW.TxAndBlockNotificationListener{ OnBlockAttached: func(_ int, _ int32) { pg.bondConfirmationInfo.currentBondConf++ - if pg.bondConfirmationInfo.currentBondConf >= int32(pg.bondConfirmationInfo.requiredBondConf) { - pg.ParentNavigator().ClearStackAndDisplay(NewDEXMarketPage(pg.Load)) - } else { - pg.ParentWindow().Reload() - } + pg.ParentWindow().Reload() }, }, DEXOnboardingPageID) }