decred.org/dcrdex@v1.0.5/client/asset/bch/bch.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package bch
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"math"
    13  	"path/filepath"
    14  	"time"
    15  
    16  	"decred.org/dcrdex/client/asset"
    17  	"decred.org/dcrdex/client/asset/btc"
    18  	"decred.org/dcrdex/dex"
    19  	"decred.org/dcrdex/dex/config"
    20  	dexbch "decred.org/dcrdex/dex/networks/bch"
    21  	dexbtc "decred.org/dcrdex/dex/networks/btc"
    22  	"github.com/btcsuite/btcd/btcec/v2"
    23  	"github.com/btcsuite/btcd/chaincfg"
    24  	"github.com/btcsuite/btcd/txscript"
    25  	"github.com/btcsuite/btcd/wire"
    26  	"github.com/dcrlabs/bchwallet/wallet"
    27  	"github.com/gcash/bchd/bchec"
    28  	bchchaincfg "github.com/gcash/bchd/chaincfg"
    29  	bchtxscript "github.com/gcash/bchd/txscript"
    30  	bchwire "github.com/gcash/bchd/wire"
    31  )
    32  
    33  const (
    34  	version = 0
    35  
    36  	// BipID is the Bip 44 coin ID for Bitcoin Cash.
    37  	BipID = 145
    38  	// The default fee is passed to the user as part of the asset.WalletInfo
    39  	// structure.
    40  	defaultFee         = 100
    41  	minNetworkVersion  = 270000 // v27.0.0-49ad6a9a9
    42  	walletTypeRPC      = "bitcoindRPC"
    43  	walletTypeSPV      = "SPV"
    44  	walletTypeLegacy   = ""
    45  	walletTypeElectrum = "electrumRPC"
    46  )
    47  
    48  var (
    49  	netPorts = dexbtc.NetPorts{
    50  		Mainnet: "8332",
    51  		Testnet: "28332",
    52  		Simnet:  "18443",
    53  	}
    54  
    55  	rpcWalletDefinition = &asset.WalletDefinition{
    56  		Type:              walletTypeRPC,
    57  		Tab:               "External",
    58  		Description:       "Connect to bitcoind",
    59  		DefaultConfigPath: dexbtc.SystemConfigPath("bitcoin"), // Same as bitcoin. That's dumb.
    60  		ConfigOpts:        append(btc.RPCConfigOpts("Bitcoin Cash", ""), btc.CommonConfigOpts("BCH", true)...),
    61  		MultiFundingOpts:  btc.MultiFundingOpts,
    62  	}
    63  	spvWalletDefinition = &asset.WalletDefinition{
    64  		Type:             walletTypeSPV,
    65  		Tab:              "Native",
    66  		Description:      "Use the built-in SPV wallet",
    67  		ConfigOpts:       btc.CommonConfigOpts("BCH", true),
    68  		Seeded:           true,
    69  		MultiFundingOpts: btc.MultiFundingOpts,
    70  	}
    71  
    72  	electrumWalletDefinition = &asset.WalletDefinition{
    73  		Type:        walletTypeElectrum,
    74  		Tab:         "Electron Cash  (external)",
    75  		Description: "Use an external Electron Cash (BCH Electrum fork) Wallet",
    76  		// json: DefaultConfigPath: filepath.Join(btcutil.AppDataDir("electrom-cash", false), "config"), // maybe?
    77  		ConfigOpts:       btc.CommonConfigOpts("BCH", true),
    78  		MultiFundingOpts: btc.MultiFundingOpts,
    79  	}
    80  
    81  	// WalletInfo defines some general information about a Bitcoin Cash wallet.
    82  	WalletInfo = &asset.WalletInfo{
    83  		Name:              "Bitcoin Cash",
    84  		SupportedVersions: []uint32{version},
    85  		// Same as bitcoin. That's dumb.
    86  		UnitInfo: dexbch.UnitInfo,
    87  		AvailableWallets: []*asset.WalletDefinition{
    88  			// spvWalletDefinition,
    89  			rpcWalletDefinition,
    90  			// electrumWalletDefinition, // getinfo RPC needs backport: https://github.com/Electron-Cash/Electron-Cash/pull/2399
    91  		},
    92  	}
    93  
    94  	externalFeeRate = btc.BitcoreRateFetcher("BCH")
    95  )
    96  
    97  func init() {
    98  	asset.Register(BipID, &Driver{})
    99  	asset.RegisterSPVWithdrawFunc(BipID, WithdrawSPVFunds)
   100  }
   101  
   102  // Driver implements asset.Driver.
   103  type Driver struct{}
   104  
   105  // Check that Driver implements asset.Driver.
   106  var _ asset.Driver = (*Driver)(nil)
   107  
   108  // Open creates the BCH exchange wallet. Start the wallet with its Run method.
   109  func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) {
   110  	if cfg.Type == walletTypeSPV {
   111  		return nil, asset.ErrWalletTypeDisabled
   112  	}
   113  	return NewWallet(cfg, logger, network)
   114  }
   115  
   116  // DecodeCoinID creates a human-readable representation of a coin ID for
   117  // Bitcoin Cash.
   118  func (d *Driver) DecodeCoinID(coinID []byte) (string, error) {
   119  	// Bitcoin Cash and Bitcoin have the same tx hash and output format.
   120  	return (&btc.Driver{}).DecodeCoinID(coinID)
   121  }
   122  
   123  // Info returns basic information about the wallet and asset.
   124  func (d *Driver) Info() *asset.WalletInfo {
   125  	return WalletInfo
   126  }
   127  
   128  // Exists checks the existence of the wallet. Part of the Creator interface, so
   129  // only used for wallets with WalletDefinition.Seeded = true.
   130  func (d *Driver) Exists(walletType, dataDir string, settings map[string]string, net dex.Network) (bool, error) {
   131  	if walletType != walletTypeSPV {
   132  		return false, fmt.Errorf("no Bitcoin Cash wallet of type %q available", walletType)
   133  	}
   134  
   135  	chainParams, err := parseChainParams(net)
   136  	if err != nil {
   137  		return false, err
   138  	}
   139  	walletDir := filepath.Join(dataDir, chainParams.Name)
   140  	// recoverWindow argument borrowed from bchwallet directly.
   141  	loader := wallet.NewLoader(chainParams, walletDir, true, 250)
   142  	return loader.WalletExists()
   143  }
   144  
   145  // Create creates a new SPV wallet.
   146  func (d *Driver) Create(params *asset.CreateWalletParams) error {
   147  	if params.Type != walletTypeSPV {
   148  		return fmt.Errorf("SPV is the only seeded wallet type. required = %q, requested = %q", walletTypeSPV, params.Type)
   149  	}
   150  	if len(params.Seed) == 0 {
   151  		return errors.New("wallet seed cannot be empty")
   152  	}
   153  	if len(params.DataDir) == 0 {
   154  		return errors.New("must specify wallet data directory")
   155  	}
   156  	chainParams, err := parseChainParams(params.Net)
   157  	if err != nil {
   158  		return fmt.Errorf("error parsing chain: %w", err)
   159  	}
   160  
   161  	walletCfg := new(btc.WalletConfig)
   162  	err = config.Unmapify(params.Settings, walletCfg)
   163  	if err != nil {
   164  		return err
   165  	}
   166  
   167  	recoveryCfg := new(btc.RecoveryCfg)
   168  	err = config.Unmapify(params.Settings, recoveryCfg)
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	bday := btc.DefaultWalletBirthday
   174  	if params.Birthday != 0 {
   175  		bday = time.Unix(int64(params.Birthday), 0)
   176  	}
   177  
   178  	walletDir := filepath.Join(params.DataDir, chainParams.Name)
   179  	return createSPVWallet(params.Pass, params.Seed, bday, walletDir,
   180  		params.Logger, recoveryCfg.NumExternalAddresses, recoveryCfg.NumInternalAddresses, chainParams)
   181  }
   182  
   183  // MinLotSize calculates the minimum bond size for a given fee rate that avoids
   184  // dust outputs on the swap and refund txs, assuming the maxFeeRate doesn't
   185  // change.
   186  func (d *Driver) MinLotSize(maxFeeRate uint64) uint64 {
   187  	return dexbtc.MinLotSize(maxFeeRate, false)
   188  }
   189  
   190  // NewWallet is the exported constructor by which the DEX will import the
   191  // exchange wallet.
   192  func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) {
   193  	cloneParams := parseCloneParams(network)
   194  	if cloneParams == nil {
   195  		return nil, fmt.Errorf("unknown network ID %v", network)
   196  	}
   197  
   198  	// Designate the clone ports. These will be overwritten by any explicit
   199  	// settings in the configuration file. Bitcoin Cash uses the same default
   200  	// ports as Bitcoin.
   201  	cloneCFG := &btc.BTCCloneCFG{
   202  		WalletCFG:            cfg,
   203  		MinNetworkVersion:    minNetworkVersion,
   204  		WalletInfo:           WalletInfo,
   205  		Symbol:               "bch",
   206  		Logger:               logger,
   207  		Network:              network,
   208  		ChainParams:          cloneParams,
   209  		Ports:                netPorts,
   210  		DefaultFallbackFee:   defaultFee,
   211  		Segwit:               false,
   212  		InitTxSizeBase:       dexbtc.InitTxSizeBase,
   213  		InitTxSize:           dexbtc.InitTxSize,
   214  		ExternalFeeEstimator: externalFeeRate,
   215  		LegacyBalance:        cfg.Type != walletTypeSPV,
   216  		// Bitcoin Cash uses the Cash Address encoding, which is Bech32, but not
   217  		// indicative of segwit. We provide a custom encoder and decode to go
   218  		// to/from a btcutil.Address and a string.
   219  		AddressDecoder:  dexbch.DecodeCashAddress,
   220  		AddressStringer: dexbch.EncodeCashAddress,
   221  		// Bitcoin Cash has a custom signature hash algorithm. Since they don't
   222  		// have segwit, Bitcoin Cash implemented a variation of the withdrawn
   223  		// BIP0062 that utilizes Schnorr signatures.
   224  		// https://gist.github.com/markblundeberg/a3aba3c9d610e59c3c49199f697bc38b#making-unmalleable-smart-contracts
   225  		// https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki
   226  		NonSegwitSigner: rawTxInSigner,
   227  		// Bitcoin Cash don't take a change_type argument in their options
   228  		// unlike Bitcoin Core.
   229  		OmitAddressType: true,
   230  		AssetID:         BipID,
   231  	}
   232  
   233  	switch cfg.Type {
   234  	case walletTypeRPC, walletTypeLegacy:
   235  		// Bitcoin Cash uses estimatefee instead of estimatesmartfee, and even
   236  		// then, they modified it from the old Bitcoin Core estimatefee by
   237  		// removing the confirmation target argument.
   238  		cloneCFG.FeeEstimator = estimateFee
   239  		return btc.BTCCloneWallet(cloneCFG)
   240  	// case walletTypeElectrum:
   241  	// 	logger.Warnf("\n\nUNTESTED Bitcoin Cash ELECTRUM WALLET IMPLEMENTATION! DO NOT USE ON mainnet!\n\n")
   242  	// 	cloneCFG.FeeEstimator = nil        // Electrum can do it, use the feeRate method
   243  	// 	cloneCFG.LegacyBalance = false
   244  	// 	cloneCFG.Ports = dexbtc.NetPorts{} // no default ports for Electrum wallet
   245  	// 	return btc.ElectrumWallet(cloneCFG)
   246  	case walletTypeSPV:
   247  		return btc.OpenSPVWallet(cloneCFG, openSPVWallet)
   248  	}
   249  	return nil, fmt.Errorf("wallet type %q not known", cfg.Type)
   250  }
   251  
   252  // rawTxInSigner signs the transaction using Bitcoin Cash's custom signature
   253  // hash and signing algorithm.
   254  func rawTxInSigner(btcTx *wire.MsgTx, idx int, subScript []byte, hashType txscript.SigHashType,
   255  	btcKey *btcec.PrivateKey, vals []int64, _ [][]byte) ([]byte, error) {
   256  
   257  	bchTx, err := translateTx(btcTx)
   258  	if err != nil {
   259  		return nil, fmt.Errorf("btc->bch wire.MsgTx translation error: %v", err)
   260  	}
   261  
   262  	bchKey, _ := bchec.PrivKeyFromBytes(bchec.S256(), btcKey.Serialize())
   263  
   264  	return bchtxscript.RawTxInECDSASignature(bchTx, idx, subScript, bchtxscript.SigHashType(uint32(hashType)), bchKey, vals[idx])
   265  }
   266  
   267  // serializeBtcTx serializes the wire.MsgTx.
   268  func serializeBtcTx(msgTx *wire.MsgTx) ([]byte, error) {
   269  	buf := bytes.NewBuffer(make([]byte, 0, msgTx.SerializeSize()))
   270  	err := msgTx.Serialize(buf)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  	return buf.Bytes(), nil
   275  }
   276  
   277  // estimateFee uses Bitcoin Cash's estimatefee RPC, since estimatesmartfee
   278  // is not implemented.
   279  func estimateFee(ctx context.Context, node btc.RawRequester, confTarget uint64) (uint64, error) {
   280  	resp, err := node.RawRequest(ctx, "estimatefee", nil)
   281  	if err != nil {
   282  		return 0, err
   283  	}
   284  	var feeRate float64
   285  	err = json.Unmarshal(resp, &feeRate)
   286  	if err != nil {
   287  		return 0, err
   288  	}
   289  	if feeRate <= 0 {
   290  		return 0, fmt.Errorf("fee could not be estimated")
   291  	}
   292  	return uint64(math.Round(feeRate * 1e5)), nil
   293  }
   294  
   295  // translateTx converts the btcd/*wire.MsgTx into a bchd/*wire.MsgTx.
   296  func translateTx(btcTx *wire.MsgTx) (*bchwire.MsgTx, error) {
   297  	txB, err := serializeBtcTx(btcTx)
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  
   302  	bchTx := new(bchwire.MsgTx)
   303  	err = bchTx.Deserialize(bytes.NewBuffer(txB))
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  
   308  	return bchTx, nil
   309  }
   310  
   311  func parseCloneParams(net dex.Network) *chaincfg.Params {
   312  	switch net {
   313  	case dex.Mainnet:
   314  		return dexbch.MainNetParams
   315  	case dex.Testnet:
   316  		return dexbch.TestNet4Params
   317  	case dex.Regtest:
   318  		return dexbch.RegressionNetParams
   319  	}
   320  	return nil
   321  }
   322  
   323  func parseChainParams(net dex.Network) (*bchchaincfg.Params, error) {
   324  	switch net {
   325  	case dex.Mainnet:
   326  		return &bchchaincfg.MainNetParams, nil
   327  	case dex.Testnet:
   328  		return &bchchaincfg.TestNet4Params, nil
   329  	case dex.Regtest:
   330  		return &bchchaincfg.RegressionNetParams, nil
   331  	}
   332  	return nil, fmt.Errorf("unknown network ID %v", net)
   333  }
   334  
   335  // WithdrawSPVFunds is a function to generate a tx that spends all funds from a
   336  // deprecated SPV wallet.
   337  func WithdrawSPVFunds(ctx context.Context, walletPW []byte, recipient, dataDir string, net dex.Network, log dex.Logger) ([]byte, error) {
   338  	cloneParams := parseCloneParams(net)
   339  	if cloneParams == nil {
   340  		return nil, fmt.Errorf("unknown net %v", net)
   341  	}
   342  	addr, err := dexbch.DecodeCashAddress(recipient, cloneParams)
   343  	if err != nil {
   344  		return nil, fmt.Errorf("error decoding address %q: %w", recipient, err)
   345  	}
   346  	c := make(chan asset.WalletNotification, 16)
   347  	cfg := &asset.WalletConfig{
   348  		Type:        walletTypeSPV,
   349  		Emit:        asset.NewWalletEmitter(c, BipID, log),
   350  		PeersChange: func(u uint32, err error) {},
   351  		DataDir:     dataDir,
   352  		Settings: map[string]string{
   353  			"apifeefallback": "true",
   354  			"fallbackfee":    "0.001", // = defaultFee in BCH/kB
   355  		},
   356  	}
   357  	wi, err := NewWallet(cfg, log, net)
   358  	if err != nil {
   359  		return nil, fmt.Errorf("error constructing wallet: %w", err)
   360  	}
   361  	w := wi.(*btc.ExchangeWalletSPV)
   362  
   363  	btcTx, err := w.WithdrawTx(ctx, walletPW, addr)
   364  	if err != nil {
   365  		return nil, fmt.Errorf("error generating withdraw tx: %w", err)
   366  	}
   367  
   368  	bchTx, err := translateTx(btcTx)
   369  	if err != nil {
   370  		return nil, fmt.Errorf("btc->bch wire.MsgTx translation error: %v", err)
   371  	}
   372  
   373  	buf := bytes.NewBuffer(make([]byte, 0, bchTx.SerializeSize()))
   374  	if err = bchTx.Serialize(buf); err != nil {
   375  		return nil, fmt.Errorf("error serializing tx: %w", err)
   376  	}
   377  	return buf.Bytes(), nil
   378  }