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 }