diff --git a/client/asset/bch/bch.go b/client/asset/bch/bch.go new file mode 100644 index 0000000000..69d84d1468 --- /dev/null +++ b/client/asset/bch/bch.go @@ -0,0 +1,265 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package bch + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "math" + + "decred.org/dcrdex/client/asset" + "decred.org/dcrdex/client/asset/btc" + "decred.org/dcrdex/dex" + dexbch "decred.org/dcrdex/dex/networks/bch" + dexbtc "decred.org/dcrdex/dex/networks/btc" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/gcash/bchd/bchec" + bchscript "github.com/gcash/bchd/txscript" + bchwire "github.com/gcash/bchd/wire" +) + +const ( + // BipID is the Bip 44 coin ID for Bitcoin Cash. + BipID = 145 + // The default fee is passed to the user as part of the asset.WalletInfo + // structure. + defaultFee = 100 + minNetworkVersion = 221100 +) + +var ( + fallbackFeeKey = "fallbackfee" + configOpts = []*asset.ConfigOption{ + { + Key: "walletname", + DisplayName: "Wallet Name", + Description: "The wallet name", + }, + { + Key: "rpcuser", + DisplayName: "JSON-RPC Username", + Description: "Bitcoin Cash 'rpcuser' setting", + }, + { + Key: "rpcpassword", + DisplayName: "JSON-RPC Password", + Description: "Bitcoin Cash 'rpcpassword' setting", + NoEcho: true, + }, + { + Key: "rpcbind", + DisplayName: "JSON-RPC Address", + Description: " or : (default 'localhost')", + }, + { + Key: "rpcport", + DisplayName: "JSON-RPC Port", + Description: "Port for RPC connections (if not set in Address)", + }, + { + Key: fallbackFeeKey, + DisplayName: "Fallback fee rate", + Description: "Bitcoin Cash 'fallbackfee' rate. Units: BCH/kB", + DefaultValue: defaultFee * 1000 / 1e8, + }, + { + Key: "txsplit", + DisplayName: "Pre-split funding inputs", + Description: "When placing an order, create a \"split\" transaction to fund the order without locking more of the wallet balance than " + + "necessary. Otherwise, excess funds may be reserved to fund the order until the first swap contract is broadcast " + + "during match settlement, or the order is canceled. This an extra transaction for which network mining fees are paid. " + + "Used only for standing-type orders, e.g. limit orders without immediate time-in-force.", + IsBoolean: true, + }, + } + // WalletInfo defines some general information about a Bitcoin Cash wallet. + WalletInfo = &asset.WalletInfo{ + Name: "Bitcoin Cash", + Units: "Satoshi", + // Same as bitcoin. That's dumb. + DefaultConfigPath: dexbtc.SystemConfigPath("bitcoin"), + ConfigOpts: configOpts, + } +) + +func init() { + asset.Register(BipID, &Driver{}) +} + +// Driver implements asset.Driver. +type Driver struct{} + +// Setup creates the BCH exchange wallet. Start the wallet with its Run method. +func (d *Driver) Setup(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) { + return NewWallet(cfg, logger, network) +} + +// DecodeCoinID creates a human-readable representation of a coin ID for +// Bitcoin Cash. +func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { + // Bitcoin Cash and Bitcoin have the same tx hash and output format. + return (&btc.Driver{}).DecodeCoinID(coinID) +} + +// Info returns basic information about the wallet and asset. +func (d *Driver) Info() *asset.WalletInfo { + return WalletInfo +} + +// NewWallet is the exported constructor by which the DEX will import the +// exchange wallet. The wallet will shut down when the provided context is +// canceled. The configPath can be an empty string, in which case the standard +// system location of the daemon config file is assumed. +func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) { + var params *chaincfg.Params + switch network { + case dex.Mainnet: + params = dexbch.MainNetParams + case dex.Testnet: + params = dexbch.TestNet3Params + case dex.Regtest: + params = dexbch.RegressionNetParams + default: + return nil, fmt.Errorf("unknown network ID %v", network) + } + + // Designate the clone ports. These will be overwritten by any explicit + // settings in the configuration file. Bitcoin Cash uses the same default + // ports as Bitcoin. + ports := dexbtc.NetPorts{ + Mainnet: "8332", + Testnet: "18332", + Simnet: "18443", + } + cloneCFG := &btc.BTCCloneCFG{ + WalletCFG: cfg, + MinNetworkVersion: minNetworkVersion, + WalletInfo: WalletInfo, + Symbol: "bch", + Logger: logger, + Network: network, + ChainParams: params, + Ports: ports, + DefaultFallbackFee: defaultFee, + Segwit: false, + LegacyBalance: true, + // Bitcoin Cash uses the Cash Address encoding, which is Bech32, but + // not indicative of segwit. We provide a custom encoder. + AddressDecoder: dexbch.DecodeCashAddress, + // Bitcoin Cash has a custom signature hash algorithm. Since they don't + // have segwit, Bitcoin Cash implemented a variation of the withdrawn + // BIP0062 that utilizes Shnorr signatures. + // https://gist.github.com/markblundeberg/a3aba3c9d610e59c3c49199f697bc38b#making-unmalleable-smart-contracts + // https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki + NonSegwitSigner: rawTxInSigner, + // The old allowHighFees bool argument to sendrawtransaction. + ArglessChangeAddrRPC: true, + // Bitcoin Cash uses estimatefee instead of estimatesmartfee, and even + // then, they modified it from the old Bitcoin Core estimatefee by + // removing the confirmation target argument. + FeeEstimator: estimateFee, + } + + xcWallet, err := btc.BTCCloneWallet(cloneCFG) + if err != nil { + return nil, err + } + + return &BCHWallet{ + ExchangeWallet: xcWallet, + }, nil +} + +// BCHWallet embeds btc.ExchangeWallet, but re-implements a couple of methods to +// perform on-the-fly address translation. +type BCHWallet struct { + *btc.ExchangeWallet +} + +// Address converts the Bitcoin base58-encoded address returned by the embedded +// ExchangeWallet into a Cash Address. +func (bch *BCHWallet) Address() (string, error) { + btcAddrStr, err := bch.ExchangeWallet.Address() + if err != nil { + return "", err + } + return dexbch.RecodeCashAddress(btcAddrStr, bch.Net()) +} + +// AuditContract modifies the *asset.Contract returned by the ExchangeWallet +// AuditContract method by converting the Recipient to the Cash Address +// encoding. +func (bch *BCHWallet) AuditContract(coinID, contract dex.Bytes) (*asset.AuditInfo, error) { // AuditInfo has address + ai, err := bch.ExchangeWallet.AuditContract(coinID, contract) + if err != nil { + return nil, err + } + ai.Recipient, err = dexbch.RecodeCashAddress(ai.Recipient, bch.Net()) + if err != nil { + return nil, err + } + return ai, nil +} + +// rawTxSigner signs the transaction using Bitcoin Cash's custom signature +// hash and signing algorithm. +func rawTxInSigner(btcTx *wire.MsgTx, idx int, subScript []byte, hashType txscript.SigHashType, btcKey *btcec.PrivateKey, val uint64) ([]byte, error) { + bchTx, err := translateTx(btcTx) + if err != nil { + return nil, fmt.Errorf("btc->bch wire.MsgTx translation error: %v", err) + } + + bchKey, _ := bchec.PrivKeyFromBytes(bchec.S256(), btcKey.Serialize()) + + return bchscript.RawTxInECDSASignature(bchTx, idx, subScript, bchscript.SigHashType(uint32(hashType)), bchKey, int64(val)) +} + +// serializeBtcTx serializes the wire.MsgTx. +func serializeBtcTx(msgTx *wire.MsgTx) ([]byte, error) { + buf := bytes.NewBuffer(make([]byte, 0, msgTx.SerializeSize())) + err := msgTx.Serialize(buf) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// estimateFee uses Bitcoin Cash's estimatefee RPC, since estimatesmartfee +// is not implemented. +func estimateFee(ctx context.Context, node btc.RawRequester, confTarget uint64) (uint64, error) { + resp, err := node.RawRequest(ctx, "estimatefee", nil) + if err != nil { + return 0, err + } + var feeRate float64 + err = json.Unmarshal(resp, &feeRate) + if err != nil { + return 0, err + } + if feeRate <= 0 { + return 0, fmt.Errorf("fee could not be estimated") + } + return uint64(math.Round(feeRate * 1e5)), nil +} + +// translateTx converts the btcd/*wire.MsgTx into a bchd/*wire.MsgTx. +func translateTx(btcTx *wire.MsgTx) (*bchwire.MsgTx, error) { + txB, err := serializeBtcTx(btcTx) + if err != nil { + return nil, err + } + + bchTx := bchwire.NewMsgTx(bchwire.TxVersion) + err = bchTx.Deserialize(bytes.NewBuffer(txB)) + if err != nil { + return nil, err + } + + return bchTx, nil +} diff --git a/client/asset/bch/regnet_test.go b/client/asset/bch/regnet_test.go new file mode 100644 index 0000000000..3191ce45e5 --- /dev/null +++ b/client/asset/bch/regnet_test.go @@ -0,0 +1,40 @@ +// +build harness + +package bch + +// Regnet tests expect the BCH test harness to be running. +// +// Sim harness info: +// The harness has three wallets, alpha, beta, and gamma. +// All three wallets have confirmed UTXOs. +// The beta wallet has only coinbase outputs. +// The alpha wallet has coinbase outputs too, but has sent some to the gamma +// wallet, so also has some change outputs. +// The gamma wallet has regular transaction outputs of varying size and +// confirmation count. Value:Confirmations = +// 10:8, 18:7, 5:6, 7:5, 1:4, 15:3, 3:2, 25:1 + +import ( + "testing" + + "decred.org/dcrdex/client/asset/btc/livetest" + "decred.org/dcrdex/dex" + dexbtc "decred.org/dcrdex/dex/networks/btc" +) + +const alphaAddress = "bchreg:qqnm4z2tftyyeu3kvzzepmlp9mj3g6fvxgft570vll" + +var tBCH = &dex.Asset{ + ID: 2, + Symbol: "bch", + SwapSize: dexbtc.InitTxSize, + SwapSizeBase: dexbtc.InitTxSizeBase, + MaxFeeRate: 10, + LotSize: 1e6, + RateStep: 10, + SwapConf: 1, +} + +func TestWallet(t *testing.T) { + livetest.Run(t, NewWallet, alphaAddress, tBCH, false) +} diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index ede52e5683..7340176e50 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -23,6 +23,7 @@ import ( "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/calc" dexbtc "decred.org/dcrdex/dex/networks/btc" + "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -144,6 +145,9 @@ var ( } ) +// TxInSigner is a transaction input signer. +type TxInSigner func(tx *wire.MsgTx, idx int, subScript []byte, hashType txscript.SigHashType, key *btcec.PrivateKey, val uint64) ([]byte, error) + // BTCCloneCFG holds clone specific parameters. type BTCCloneCFG struct { WalletCFG *asset.WalletConfig @@ -165,6 +169,19 @@ type BTCCloneCFG struct { // LegacyRawFeeLimit can be true if the RPC only supports the boolean // allowHighFees argument to the sendrawtransaction RPC. LegacyRawFeeLimit bool + // AddressDecoder is an optional argument that can decode an address string + // into btcutil.Address. If AddressDecoder is not supplied, + // btcutil.DecodeAddress will be used. + AddressDecoder dexbtc.AddressDecoder + // ArglessChangeAddrRPC can be true if the getrawchangeaddress takes no + // address-type argument. + ArglessChangeAddrRPC bool + // NonSegwitSigner can be true if the transaction signature hash data is not + // the standard for non-segwit Bitcoin. If nil, txscript. + NonSegwitSigner TxInSigner + // FeeEstimator provides a way to get fees given an RawRequest-enabled + // client and a confirmation target. + FeeEstimator func(context.Context, RawRequester, uint64) (uint64, error) } // outPoint is the hash and output index of a transaction output. @@ -232,9 +249,7 @@ func (op *output) wireOutPoint() *wire.OutPoint { return wire.NewOutPoint(op.txHash(), op.vout()) } -// auditInfo is information about a swap contract on that blockchain, not -// necessarily created by this wallet, as would be returned from AuditContract. -// auditInfo satisfies the asset.AuditInfo interface. +// auditInfo is information about a swap contract on that blockchain. type auditInfo struct { output *output recipient btcutil.Address @@ -350,6 +365,9 @@ type ExchangeWallet struct { useLegacyBalance bool segwit bool legacyRawFeeLimit bool + signNonSegwit TxInSigner + estimateFee func(context.Context, RawRequester, uint64) (uint64, error) + decodeAddr dexbtc.AddressDecoder tipMtx sync.RWMutex currentTip *block @@ -486,8 +504,18 @@ func newWallet(requester RawRequester, cfg *BTCCloneCFG, btcCfg *dexbtc.Config) } cfg.Logger.Tracef("Redeem conf target set to %d blocks", redeemConfTarget) - return &ExchangeWallet{ - node: newWalletClient(requester, cfg.Segwit, cfg.ChainParams), + addrDecoder := btcutil.DecodeAddress + if cfg.AddressDecoder != nil { + addrDecoder = cfg.AddressDecoder + } + + nonSegwitSigner := rawTxInSig + if cfg.NonSegwitSigner != nil { + nonSegwitSigner = cfg.NonSegwitSigner + } + + w := &ExchangeWallet{ + node: newWalletClient(requester, cfg.Segwit, addrDecoder, cfg.ArglessChangeAddrRPC, cfg.ChainParams), symbol: cfg.Symbol, chainParams: cfg.ChainParams, log: cfg.Logger, @@ -502,8 +530,17 @@ func newWallet(requester RawRequester, cfg *BTCCloneCFG, btcCfg *dexbtc.Config) useLegacyBalance: cfg.LegacyBalance, segwit: cfg.Segwit, legacyRawFeeLimit: cfg.LegacyRawFeeLimit, + signNonSegwit: nonSegwitSigner, + estimateFee: cfg.FeeEstimator, + decodeAddr: addrDecoder, walletInfo: cfg.WalletInfo, - }, nil + } + + if w.estimateFee == nil { + w.estimateFee = w.feeRate + } + + return w, nil } var _ asset.Wallet = (*ExchangeWallet)(nil) @@ -513,6 +550,12 @@ func (btc *ExchangeWallet) Info() *asset.WalletInfo { return btc.walletInfo } +// Net returns the ExchangeWallet's *chaincfg.Params. This is not part of the +// asset.Wallet interface, but is provided as a convenience for embedding types. +func (btc *ExchangeWallet) Net() *chaincfg.Params { + return btc.chainParams +} + // Connect connects the wallet to the RPC server. Satisfies the dex.Connector // interface. func (btc *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) { @@ -533,6 +576,15 @@ func (btc *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) if err != nil { return nil, fmt.Errorf("error initializing best block for %s: %w", btc.symbol, err) } + // Check for method unkown error for feeRate method. + _, err = btc.estimateFee(ctx, btc.node.requester, 1) + var rpcErr *btcjson.RPCError + if errors.As(err, &rpcErr) && + (rpcErr.Code == btcjson.ErrRPCMethodNotFound.Code || rpcErr.Message == "Method not found") { + + return nil, fmt.Errorf("fee estimation method not found. Are you configured for the correct RPC?") + } + btc.tipMtx.Lock() btc.currentTip, err = btc.blockFromHash(h.String()) btc.tipMtx.Unlock() @@ -651,10 +703,10 @@ func (btc *ExchangeWallet) legacyBalance() (*asset.Balance, error) { }, nil } -// FeeRate returns the current optimal fee rate in sat / byte. -func (btc *ExchangeWallet) feeRate(confTarget uint64) (uint64, error) { - feeResult, err := btc.node.EstimateSmartFee(int64(confTarget), - &btcjson.EstimateModeConservative) +// feeRate returns the current optimal fee rate in sat / byte using the +// estimatesmartfee RPC. +func (btc *ExchangeWallet) feeRate(ctx context.Context, _ RawRequester, confTarget uint64) (uint64, error) { + feeResult, err := btc.node.EstimateSmartFee(int64(confTarget), &btcjson.EstimateModeConservative) if err != nil { return 0, err } @@ -682,7 +734,7 @@ func (a amount) String() string { // feeRateWithFallback attempts to get the optimal fee rate in sat / byte via // FeeRate. If that fails, it will return the configured fallback fee rate. func (btc *ExchangeWallet) feeRateWithFallback(confTarget uint64) uint64 { - feeRate, err := btc.feeRate(confTarget) + feeRate, err := btc.estimateFee(btc.node.ctx, btc.node.requester, confTarget) if err != nil { feeRate = btc.fallbackFeeRate btc.log.Warnf("Unable to get optimal fee rate, using fallback of %d: %v", @@ -705,7 +757,7 @@ func (btc *ExchangeWallet) MaxOrder(lotSize uint64, nfo *dex.Asset) (*asset.Swap // []*compositeUTXO to be used for further order estimation without additional // calls to listunspent. func (btc *ExchangeWallet) maxOrder(lotSize uint64, nfo *dex.Asset) (utxos []*compositeUTXO, feeRate uint64, est *asset.SwapEstimate, err error) { - networkFeeRate, err := btc.feeRate(1) + networkFeeRate, err := btc.estimateFee(btc.node.ctx, btc.node.requester, 1) if err != nil { return nil, 0, nil, fmt.Errorf("error getting network fee estimate: %w", err) } @@ -1061,7 +1113,7 @@ func (btc *ExchangeWallet) split(value uint64, lots uint64, outputs []*output, // This must fund swaps, so don't under-pay. TODO: get and use a fee rate // from server, and have server check fee rate on unconf funding coins. - estFeeRate, err := btc.feeRate(1) + estFeeRate, err := btc.estimateFee(btc.node.ctx, btc.node.requester, 1) if err != nil { // Fallback fee rate is NO GOOD here. return nil, false, fmt.Errorf("unable to get optimal fee rate for pre-split transaction "+ @@ -1293,8 +1345,14 @@ func (btc *ExchangeWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin if err != nil { return nil, nil, 0, fmt.Errorf("error creating revocation address: %w", err) } + + contractAddr, err := btc.decodeAddr(contract.Address, btc.chainParams) + if err != nil { + return nil, nil, 0, fmt.Errorf("address decode error: %v", err) + } + // Create the contract, a P2SH redeem script. - contractScript, err := dexbtc.MakeContract(contract.Address, revokeAddr.String(), + contractScript, err := dexbtc.MakeContract(contractAddr, revokeAddr, contract.SecretHash, int64(contract.LockTime), btc.segwit, btc.chainParams) if err != nil { return nil, nil, 0, fmt.Errorf("unable to create pubkey script for address %s: %w", contract.Address, err) @@ -1393,13 +1451,18 @@ func (btc *ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Co var addresses []btcutil.Address var values []uint64 for _, r := range form.Redemptions { - cinfo, ok := r.Spends.(*auditInfo) - if !ok { - return nil, nil, 0, fmt.Errorf("Redemption contract info of wrong type") + if r.Spends == nil { + return nil, nil, 0, fmt.Errorf("no audit info") + } + + cinfo, err := btc.convertAuditInfo(r.Spends) + if err != nil { + return nil, nil, 0, err } + // Extract the swap contract recipient and secret hash and check the secret // hash against the hash of the provided secret. - contract := r.Spends.Contract() + contract := cinfo.contract _, receiver, _, secretHash, err := dexbtc.ExtractSwapDetails(contract, btc.segwit, btc.chainParams) if err != nil { return nil, nil, 0, fmt.Errorf("error extracting swap addresses: %w", err) @@ -1464,7 +1527,7 @@ func (btc *ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Co } else { for i, r := range form.Redemptions { contract := contracts[i] - redeemSig, redeemPubKey, err := btc.createSig(msgTx, i, contract, addresses[i]) + redeemSig, redeemPubKey, err := btc.createSig(msgTx, i, contract, addresses[i], values[i]) if err != nil { return nil, nil, 0, err } @@ -1493,6 +1556,32 @@ func (btc *ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Co return coinIDs, newOutput(txHash, 0, uint64(txOut.Value)), fee, nil } +// convertAuditInfo converts from the common *asset.AuditInfo type to our +// internal *auditInfo type. +func (btc *ExchangeWallet) convertAuditInfo(ai *asset.AuditInfo) (*auditInfo, error) { + if ai.Coin == nil { + return nil, fmt.Errorf("no coin") + } + + txHash, vout, err := decodeCoinID(ai.Coin.ID()) + if err != nil { + return nil, err + } + + recip, err := btc.decodeAddr(ai.Recipient, btc.chainParams) + if err != nil { + return nil, err + } + + return &auditInfo{ + output: newOutput(txHash, vout, ai.Coin.Value()), // *output + recipient: recip, // btcutil.Address + contract: ai.Contract, // []byte + secretHash: ai.SecretHash, // []byte + expiration: ai.Expiration, // time.Time + }, nil +} + // SignMessage signs the message with the private key associated with the // specified unspent coin. A slice of pubkeys required to spend the coin and a // signature for each pubkey are returned. @@ -1524,7 +1613,7 @@ func (btc *ExchangeWallet) SignMessage(coin asset.Coin, msg dex.Bytes) (pubkeys, // AuditContract retrieves information about a swap contract on the blockchain. // AuditContract would be used to audit the counter-party's contract during a // swap. -func (btc *ExchangeWallet) AuditContract(coinID dex.Bytes, contract dex.Bytes) (asset.AuditInfo, error) { +func (btc *ExchangeWallet) AuditContract(coinID dex.Bytes, contract dex.Bytes) (*asset.AuditInfo, error) { txHash, vout, err := decodeCoinID(coinID) if err != nil { return nil, err @@ -1582,12 +1671,12 @@ func (btc *ExchangeWallet) AuditContract(coinID dex.Bytes, contract dex.Bytes) ( return nil, fmt.Errorf("contract hash doesn't match script address. %x != %x", contractHash, addr.ScriptAddress()) } - return &auditInfo{ - output: newOutput(txHash, vout, toSatoshi(txOut.Value)), - recipient: receiver, - contract: contract, - secretHash: secretHash, - expiration: time.Unix(int64(stamp), 0).UTC(), + return &asset.AuditInfo{ + Coin: newOutput(txHash, vout, toSatoshi(txOut.Value)), + Recipient: receiver.String(), + Contract: contract, + SecretHash: secretHash, + Expiration: time.Unix(int64(stamp), 0).UTC(), }, nil } @@ -2022,7 +2111,7 @@ func (btc *ExchangeWallet) Refund(coinID, contract dex.Bytes) (dex.Bytes, error) txIn.Witness = dexbtc.RefundP2WSHContract(contract, refundSig, refundPubKey) } else { - refundSig, refundPubKey, err := btc.createSig(msgTx, 0, contract, sender) + refundSig, refundPubKey, err := btc.createSig(msgTx, 0, contract, sender, val) if err != nil { return nil, fmt.Errorf("createSig: %w", err) } @@ -2395,12 +2484,12 @@ func (btc *ExchangeWallet) sendWithReturn(baseTx *wire.MsgTx, addr btcutil.Addre // createSig creates and returns the serialized raw signature and compressed // pubkey for a transaction input signature. -func (btc *ExchangeWallet) createSig(tx *wire.MsgTx, idx int, pkScript []byte, addr btcutil.Address) (sig, pubkey []byte, err error) { +func (btc *ExchangeWallet) createSig(tx *wire.MsgTx, idx int, pkScript []byte, addr btcutil.Address, val uint64) (sig, pubkey []byte, err error) { privKey, err := btc.node.PrivKeyForAddress(addr.String()) if err != nil { return nil, nil, err } - sig, err = txscript.RawTxInSignature(tx, idx, pkScript, txscript.SigHashAll, privKey) + sig, err = btc.signNonSegwit(tx, idx, pkScript, txscript.SigHashAll, privKey, val) if err != nil { return nil, nil, err } @@ -2629,7 +2718,6 @@ func (btc *ExchangeWallet) scriptHashAddress(contract []byte) (btcutil.Address, return btcutil.NewAddressWitnessScriptHash(btc.hashContract(contract), btc.chainParams) } return btcutil.NewAddressScriptHash(contract, btc.chainParams) - } // toCoinID converts the tx hash and vout to a coin ID, as a []byte. @@ -2661,3 +2749,9 @@ func isTxNotFoundErr(err error) bool { func toBTC(v uint64) float64 { return btcutil.Amount(v).ToBTC() } + +// rawTxInSig signs the transaction in input using the standard bitcoin +// signature hash and ECDSA algorithm. +func rawTxInSig(tx *wire.MsgTx, idx int, pkScript []byte, hashType txscript.SigHashType, key *btcec.PrivateKey, _ uint64) ([]byte, error) { + return txscript.RawTxInSignature(tx, idx, pkScript, txscript.SigHashAll, key) +} diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index adf20421b9..22cd296f95 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -53,6 +53,16 @@ var ( tP2WPKHAddr = "bc1qq49ypf420s0kh52l9pk7ha8n8nhsugdpculjas" ) +func btcAddr(segwit bool) btcutil.Address { + var addr btcutil.Address + if segwit { + addr, _ = btcutil.DecodeAddress(tP2WPKHAddr, &chaincfg.MainNetParams) + } else { + addr, _ = btcutil.DecodeAddress(tP2PKHAddr, &chaincfg.MainNetParams) + } + return addr +} + func randBytes(l int) []byte { b := make([]byte, l) rand.Read(b) @@ -1411,22 +1421,19 @@ func testRedeem(t *testing.T, segwit bool) { secret := randBytes(32) secretHash := sha256.Sum256(secret) lockTime := time.Now().Add(time.Hour * 12) - addrStr := tP2PKHAddr - if segwit { - addrStr = tP2WPKHAddr - } + addr := btcAddr(segwit) - contract, err := dexbtc.MakeContract(addrStr, addrStr, secretHash[:], lockTime.Unix(), segwit, &chaincfg.MainNetParams) + contract, err := dexbtc.MakeContract(addr, addr, secretHash[:], lockTime.Unix(), segwit, &chaincfg.MainNetParams) if err != nil { t.Fatalf("error making swap contract: %v", err) } - addr, _ := btcutil.DecodeAddress(tP2PKHAddr, &chaincfg.MainNetParams) - ci := &auditInfo{ - output: newOutput(tTxHash, 0, swapVal), - contract: contract, - recipient: addr, - expiration: lockTime, + coin := newOutput(tTxHash, 0, swapVal) + ci := &asset.AuditInfo{ + Coin: coin, + Contract: contract, + Recipient: addr.String(), + Expiration: lockTime, } redemption := &asset.Redemption{ @@ -1441,7 +1448,7 @@ func testRedeem(t *testing.T, segwit bool) { t.Fatalf("error encoding wif: %v", err) } - node.rawRes[methodChangeAddress] = mustMarshal(t, addrStr) + node.rawRes[methodChangeAddress] = mustMarshal(t, addr.String()) node.rawRes[methodPrivKeyForAddress] = mustMarshal(t, wif.String()) redemptions := &asset.RedeemForm{ @@ -1468,7 +1475,7 @@ func testRedeem(t *testing.T, segwit bool) { redemption.Spends = ci // Spoofing AuditInfo is not allowed. - redemption.Spends = &TAuditInfo{} + redemption.Spends = &asset.AuditInfo{} _, _, _, err = wallet.Redeem(redemptions) if err == nil { t.Fatalf("no error for spoofed AuditInfo") @@ -1484,12 +1491,12 @@ func testRedeem(t *testing.T, segwit bool) { redemption.Secret = secret // too low of value - ci.output.value = 200 + coin.value = 200 _, _, _, err = wallet.Redeem(redemptions) if err == nil { t.Fatalf("no error for redemption not worth the fees") } - ci.output.value = swapVal + coin.value = swapVal // Change address error node.rawErr[methodChangeAddress] = tErr @@ -1637,12 +1644,9 @@ func testAuditContract(t *testing.T, segwit bool) { swapVal := toSatoshi(5) secretHash, _ := hex.DecodeString("5124208c80d33507befa517c08ed01aa8d33adbf37ecd70fb5f9352f7a51a88d") lockTime := time.Now().Add(time.Hour * 12) - addrStr := tP2PKHAddr - if segwit { - addrStr = tP2WPKHAddr - } + addr := btcAddr(segwit) - contract, err := dexbtc.MakeContract(addrStr, addrStr, secretHash, lockTime.Unix(), segwit, &chaincfg.MainNetParams) + contract, err := dexbtc.MakeContract(addr, addr, secretHash, lockTime.Unix(), segwit, &chaincfg.MainNetParams) if err != nil { t.Fatalf("error making swap contract: %v", err) } @@ -1668,14 +1672,14 @@ func testAuditContract(t *testing.T, segwit bool) { if err != nil { t.Fatalf("audit error: %v", err) } - if audit.Recipient() != addrStr { - t.Fatalf("wrong recipient. wanted '%s', got '%s'", addrStr, audit.Recipient()) + if audit.Recipient != addr.String() { + t.Fatalf("wrong recipient. wanted '%s', got '%s'", addr, audit.Recipient) } - if !bytes.Equal(audit.Contract(), contract) { + if !bytes.Equal(audit.Contract, contract) { t.Fatalf("contract not set to coin redeem script") } - if audit.Expiration().Equal(lockTime) { - t.Fatalf("wrong lock time. wanted %d, got %d", lockTime.Unix(), audit.Expiration().Unix()) + if audit.Expiration.Equal(lockTime) { + t.Fatalf("wrong lock time. wanted %d, got %d", lockTime.Unix(), audit.Expiration.Unix()) } // Invalid txid @@ -1751,21 +1755,17 @@ func testFindRedemption(t *testing.T, segwit bool) { secret := randBytes(32) secretHash := sha256.Sum256(secret) - addrStr := tP2PKHAddr - if segwit { - addrStr = tP2WPKHAddr - } + addr := btcAddr(segwit) lockTime := time.Now().Add(time.Hour * 12) - contract, err := dexbtc.MakeContract(addrStr, addrStr, secretHash[:], lockTime.Unix(), segwit, &chaincfg.MainNetParams) + contract, err := dexbtc.MakeContract(addr, addr, secretHash[:], lockTime.Unix(), segwit, &chaincfg.MainNetParams) if err != nil { t.Fatalf("error making swap contract: %v", err) } contractAddr, _ := wallet.scriptHashAddress(contract) pkScript, _ := txscript.PayToAddrScript(contractAddr) - otherAddr, _ := btcutil.DecodeAddress(addrStr, &chaincfg.MainNetParams) - otherScript, _ := txscript.PayToAddrScript(otherAddr) + otherScript, _ := txscript.PayToAddrScript(addr) var redemptionWitness, otherWitness [][]byte var redemptionSigScript, otherSigScript []byte @@ -1905,19 +1905,16 @@ func testRefund(t *testing.T, segwit bool) { secretHash := sha256.Sum256(secret) lockTime := time.Now().Add(time.Hour * 12) - addrStr := tP2PKHAddr - if segwit { - addrStr = tP2WPKHAddr - } + addr := btcAddr(segwit) - contract, err := dexbtc.MakeContract(addrStr, addrStr, secretHash[:], lockTime.Unix(), segwit, &chaincfg.MainNetParams) + contract, err := dexbtc.MakeContract(addr, addr, secretHash[:], lockTime.Unix(), segwit, &chaincfg.MainNetParams) if err != nil { t.Fatalf("error making swap contract: %v", err) } bigTxOut := newTxOutResult(nil, 1e8, 2) node.txOutRes = bigTxOut - node.rawRes[methodChangeAddress] = mustMarshal(t, addrStr) + node.rawRes[methodChangeAddress] = mustMarshal(t, addr.String()) privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330") privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), privBytes) @@ -2202,14 +2199,14 @@ func testSendEdges(t *testing.T, segwit bool) { var addr, contractAddr btcutil.Address var dexReqFees, dustCoverage uint64 + addr = btcAddr(segwit) + if segwit { - addr, _ = btcutil.DecodeAddress(tP2WPKHAddr, &chaincfg.MainNetParams) contractAddr, _ = btcutil.NewAddressWitnessScriptHash(randBytes(32), &chaincfg.MainNetParams) // See dexbtc.IsDust for the source of this dustCoverage voodoo. dustCoverage = (dexbtc.P2WPKHOutputSize + 41 + (107 / 4)) * feeRate * 3 dexReqFees = dexbtc.InitTxSizeSegwit * feeRate } else { - addr, _ = btcutil.DecodeAddress(tP2PKHAddr, &chaincfg.MainNetParams) contractAddr, _ = btcutil.NewAddressScriptHash(randBytes(20), &chaincfg.MainNetParams) dustCoverage = (dexbtc.P2PKHOutputSize + 41 + 107) * feeRate * 3 dexReqFees = dexbtc.InitTxSize * feeRate diff --git a/client/asset/btc/livetest/livetest.go b/client/asset/btc/livetest/livetest.go index da7e525008..c256f72910 100644 --- a/client/asset/btc/livetest/livetest.go +++ b/client/asset/btc/livetest/livetest.go @@ -27,7 +27,6 @@ import ( "time" "decred.org/dcrdex/client/asset" - "decred.org/dcrdex/client/asset/btc" "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/config" ) @@ -40,7 +39,7 @@ func toSatoshi(v float64) uint64 { } func tBackend(t *testing.T, ctx context.Context, newWallet WalletConstructor, symbol, node, name string, - logger dex.Logger, blkFunc func(string, error), splitTx bool) (*btc.ExchangeWallet, *dex.ConnectionMaster) { + logger dex.Logger, blkFunc func(string, error), splitTx bool) (asset.Wallet, *dex.ConnectionMaster) { user, err := user.Current() if err != nil { @@ -64,8 +63,7 @@ func tBackend(t *testing.T, ctx context.Context, newWallet WalletConstructor, sy blkFunc(reportName, err) }, } - var backend asset.Wallet - backend, err = newWallet(walletCfg, logger, dex.Regtest) + backend, err := newWallet(walletCfg, logger, dex.Regtest) if err != nil { t.Fatalf("error creating backend: %v", err) } @@ -74,23 +72,23 @@ func tBackend(t *testing.T, ctx context.Context, newWallet WalletConstructor, sy if err != nil { t.Fatalf("error connecting backend: %v", err) } - return backend.(*btc.ExchangeWallet), cm + return backend, cm } type testRig struct { t *testing.T symbol string - backends map[string]*btc.ExchangeWallet + backends map[string]asset.Wallet connectionMasters map[string]*dex.ConnectionMaster } -func (rig *testRig) alpha() *btc.ExchangeWallet { +func (rig *testRig) alpha() asset.Wallet { return rig.backends["alpha"] } -func (rig *testRig) beta() *btc.ExchangeWallet { +func (rig *testRig) beta() asset.Wallet { return rig.backends["beta"] } -func (rig *testRig) gamma() *btc.ExchangeWallet { +func (rig *testRig) gamma() asset.Wallet { return rig.backends["gamma"] } func (rig *testRig) close() { @@ -136,7 +134,7 @@ func Run(t *testing.T, newWallet WalletConstructor, address string, dexAsset *de rig := &testRig{ t: t, symbol: dexAsset.Symbol, - backends: make(map[string]*btc.ExchangeWallet), + backends: make(map[string]asset.Wallet), connectionMasters: make(map[string]*dex.ConnectionMaster, 3), } rig.backends["alpha"], rig.connectionMasters["alpha"] = tBackend(t, tCtx, newWallet, dexAsset.Symbol, "alpha", "", tLogger, blkFunc, splitTx) @@ -274,9 +272,9 @@ func Run(t *testing.T, newWallet WalletConstructor, address string, dexAsset *de if err != nil { t.Fatalf("error auditing contract: %v", err) } - auditCoin := ci.Coin() - if ci.Recipient() != address { - t.Fatalf("wrong address. %s != %s", ci.Recipient(), address) + auditCoin := ci.Coin + if ci.Recipient != address { + t.Fatalf("wrong address. %s != %s", ci.Recipient, address) } if auditCoin.Value() != swapVal { t.Fatalf("wrong contract value. wanted %d, got %d", swapVal, auditCoin.Value()) @@ -291,8 +289,8 @@ func Run(t *testing.T, newWallet WalletConstructor, address string, dexAsset *de if spent { t.Fatalf("makeRedemption: expected unspent, got spent") } - if ci.Expiration().Equal(lockTime) { - t.Fatalf("wrong lock time. wanted %s, got %s", lockTime, ci.Expiration()) + if ci.Expiration.Equal(lockTime) { + t.Fatalf("wrong lock time. wanted %s, got %s", lockTime, ci.Expiration) } return &asset.Redemption{ Spends: ci, diff --git a/client/asset/btc/walletclient.go b/client/asset/btc/walletclient.go index beb4f17396..8907da0d16 100644 --- a/client/asset/btc/walletclient.go +++ b/client/asset/btc/walletclient.go @@ -10,6 +10,7 @@ import ( "encoding/json" "fmt" + dexbtc "decred.org/dcrdex/dex/networks/btc" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" @@ -57,14 +58,24 @@ type rpcClient struct { requester RawRequester chainParams *chaincfg.Params segwit bool + decodeAddr dexbtc.AddressDecoder + // arglessChangeAddrRPC true will pass no arguments to the + // getrawchangeaddress RPC. + arglessChangeAddrRPC bool } -// newWalletClient is the constructor for a rpcClient. -func newWalletClient(requester RawRequester, segwit bool, chainParams *chaincfg.Params) *rpcClient { +// newWalletClient is the constructor for a walletClient. +func newWalletClient(requester RawRequester, segwit bool, addrDecoder dexbtc.AddressDecoder, arglessChangeAddrRPC bool, chainParams *chaincfg.Params) *rpcClient { + if addrDecoder == nil { + addrDecoder = btcutil.DecodeAddress + } + return &rpcClient{ - requester: requester, - chainParams: chainParams, - segwit: segwit, + requester: requester, + chainParams: chainParams, + segwit: segwit, + decodeAddr: addrDecoder, + arglessChangeAddrRPC: arglessChangeAddrRPC, } } @@ -193,15 +204,18 @@ func (wc *rpcClient) ListLockUnspent() ([]*RPCOutpoint, error) { func (wc *rpcClient) ChangeAddress() (btcutil.Address, error) { var addrStr string var err error - if wc.segwit { + switch { + case wc.arglessChangeAddrRPC: + err = wc.call(methodChangeAddress, nil, &addrStr) + case wc.segwit: err = wc.call(methodChangeAddress, anylist{"bech32"}, &addrStr) - } else { + default: err = wc.call(methodChangeAddress, anylist{"legacy"}, &addrStr) } if err != nil { return nil, err } - return btcutil.DecodeAddress(addrStr, wc.chainParams) + return wc.decodeAddr(addrStr, wc.chainParams) } // AddressPKH gets a new base58-encoded (P2PKH) external address from the @@ -224,7 +238,7 @@ func (wc *rpcClient) address(aType string) (btcutil.Address, error) { if err != nil { return nil, err } - return btcutil.DecodeAddress(addrStr, wc.chainParams) + return wc.decodeAddr(addrStr, wc.chainParams) } // SignTx attempts to have the wallet sign the transaction inputs. diff --git a/client/asset/dcr/dcr.go b/client/asset/dcr/dcr.go index 562c85f31e..e66bbc5e3b 100644 --- a/client/asset/dcr/dcr.go +++ b/client/asset/dcr/dcr.go @@ -291,6 +291,32 @@ func (ci *auditInfo) SecretHash() dex.Bytes { return ci.secretHash } +// convertAuditInfo converts from the common *asset.AuditInfo type to our +// internal *auditInfo type. +func convertAuditInfo(ai *asset.AuditInfo, chainParams *chaincfg.Params) (*auditInfo, error) { + if ai.Coin == nil { + return nil, fmt.Errorf("no coin") + } + + op, ok := ai.Coin.(*output) + if !ok { + return nil, fmt.Errorf("unknown coin type %T", ai.Coin) + } + + recip, err := dcrutil.DecodeAddress(ai.Recipient, chainParams) + if err != nil { + return nil, err + } + + return &auditInfo{ + output: op, // *output + recipient: recip, // btcutil.Address + contract: ai.Contract, // []byte + secretHash: ai.SecretHash, // []byte + expiration: ai.Expiration, // time.Time + }, nil +} + // swapReceipt is information about a swap contract that was broadcast by this // wallet. Satisfies the asset.Receipt interface. type swapReceipt struct { @@ -1373,13 +1399,18 @@ func (dcr *ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Co var contracts [][]byte var addresses []dcrutil.Address for _, r := range form.Redemptions { - cinfo, ok := r.Spends.(*auditInfo) - if !ok { - return nil, nil, 0, fmt.Errorf("Redemption contract info of wrong type") + if r.Spends == nil { + return nil, nil, 0, fmt.Errorf("no audit info") + } + + cinfo, err := convertAuditInfo(r.Spends, dcr.chainParams) + if err != nil { + return nil, nil, 0, err } + // Extract the swap contract recipient and secret hash and check the secret // hash against the hash of the provided secret. - contract := r.Spends.Contract() + contract := cinfo.contract _, receiver, _, secretHash, err := dexdcr.ExtractSwapDetails(contract, dcr.chainParams) if err != nil { return nil, nil, 0, fmt.Errorf("error extracting swap addresses: %w", err) @@ -1505,7 +1536,7 @@ func (dcr *ExchangeWallet) SignMessage(coin asset.Coin, msg dex.Bytes) (pubkeys, // AuditContract retrieves information about a swap contract on the // blockchain. This would be used to verify the counter-party's contract // during a swap. -func (dcr *ExchangeWallet) AuditContract(coinID, contract dex.Bytes) (asset.AuditInfo, error) { +func (dcr *ExchangeWallet) AuditContract(coinID, contract dex.Bytes) (*asset.AuditInfo, error) { txHash, vout, err := decodeCoinID(coinID) if err != nil { return nil, err @@ -1549,12 +1580,12 @@ func (dcr *ExchangeWallet) AuditContract(coinID, contract dex.Bytes) (asset.Audi return nil, fmt.Errorf("contract hash doesn't match script address. %x != %x", contractHash, addr.ScriptAddress()) } - return &auditInfo{ - output: newOutput(txHash, vout, toAtoms(txOut.Value), wire.TxTreeRegular), - contract: contract, - secretHash: secretHash, - recipient: receiver, - expiration: time.Unix(int64(stamp), 0).UTC(), + return &asset.AuditInfo{ + Coin: newOutput(txHash, vout, toAtoms(txOut.Value), wire.TxTreeRegular), + Contract: contract, + SecretHash: secretHash, + Recipient: receiver.String(), + Expiration: time.Unix(int64(stamp), 0).UTC(), }, nil } diff --git a/client/asset/dcr/dcr_test.go b/client/asset/dcr/dcr_test.go index 965b5eaf77..97f049d628 100644 --- a/client/asset/dcr/dcr_test.go +++ b/client/asset/dcr/dcr_test.go @@ -1259,11 +1259,13 @@ func TestRedeem(t *testing.T) { t.Fatalf("error making swap contract: %v", err) } - ci := &auditInfo{ - output: newOutput(tTxHash, 0, swapVal, wire.TxTreeRegular), - contract: contract, - recipient: tPKHAddr, - expiration: lockTime, + coin := newOutput(tTxHash, 0, swapVal, wire.TxTreeRegular) + + ci := &asset.AuditInfo{ + Coin: coin, + Contract: contract, + Recipient: tPKHAddr.String(), + Expiration: lockTime, } redemption := &asset.Redemption{ @@ -1303,7 +1305,7 @@ func TestRedeem(t *testing.T) { redemption.Spends = ci // Spoofing AuditInfo is not allowed. - redemption.Spends = &TAuditInfo{} + redemption.Spends = &asset.AuditInfo{} _, _, _, err = wallet.Redeem(redemptions) if err == nil { t.Fatalf("no error for spoofed AuditInfo") @@ -1319,12 +1321,12 @@ func TestRedeem(t *testing.T) { redemption.Secret = secret // too low of value - ci.output.value = 200 + coin.value = 200 _, _, _, err = wallet.Redeem(redemptions) if err == nil { t.Fatalf("no error for redemption not worth the fees") } - ci.output.value = swapVal + coin.value = swapVal // Change address error node.changeAddrErr = tErr @@ -1481,14 +1483,14 @@ func TestAuditContract(t *testing.T) { if err != nil { t.Fatalf("audit error: %v", err) } - if audit.Recipient() != addrStr { - t.Fatalf("wrong recipient. wanted '%s', got '%s'", addrStr, audit.Recipient()) + if audit.Recipient != addrStr { + t.Fatalf("wrong recipient. wanted '%s', got '%s'", addrStr, audit.Recipient) } - if !bytes.Equal(audit.Contract(), contract) { + if !bytes.Equal(audit.Contract, contract) { t.Fatalf("contract not set to coin redeem script") } - if audit.Expiration().Equal(lockTime) { - t.Fatalf("wrong lock time. wanted %d, got %d", lockTime.Unix(), audit.Expiration().Unix()) + if audit.Expiration.Equal(lockTime) { + t.Fatalf("wrong lock time. wanted %d, got %d", lockTime.Unix(), audit.Expiration.Unix()) } // Invalid txid diff --git a/client/asset/dcr/simnet_test.go b/client/asset/dcr/simnet_test.go index 9107d83c1c..b3c04e022c 100644 --- a/client/asset/dcr/simnet_test.go +++ b/client/asset/dcr/simnet_test.go @@ -298,9 +298,9 @@ func runTest(t *testing.T, splitTx bool) { if err != nil { t.Fatalf("error auditing contract: %v", err) } - swapOutput = ci.Coin() - if ci.Recipient() != alphaAddress { - t.Fatalf("wrong address. %s != %s", ci.Recipient(), alphaAddress) + swapOutput = ci.Coin + if ci.Recipient != alphaAddress { + t.Fatalf("wrong address. %s != %s", ci.Recipient, alphaAddress) } if swapOutput.Value() != swapVal { t.Fatalf("wrong contract value. wanted %d, got %d", swapVal, swapOutput.Value()) @@ -312,8 +312,8 @@ func runTest(t *testing.T, splitTx bool) { if confs != 0 { t.Fatalf("unexpected number of confirmations. wanted 0, got %d", confs) } - if ci.Expiration().Equal(lockTime) { - t.Fatalf("wrong lock time. wanted %s, got %s", lockTime, ci.Expiration()) + if ci.Expiration.Equal(lockTime) { + t.Fatalf("wrong lock time. wanted %s, got %s", lockTime, ci.Expiration) } if spent { t.Fatalf("makeRedemption: expected unspent, got spent") diff --git a/client/asset/interface.go b/client/asset/interface.go index f19f6ba5b3..a4e822a5ff 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -119,7 +119,7 @@ type Wallet interface { // during a swap. If the coin cannot be found for the coin ID, the // ExchangeWallet should return CoinNotFoundError. This enables the client // to properly handle network latency. - AuditContract(coinID, contract dex.Bytes) (AuditInfo, error) + AuditContract(coinID, contract dex.Bytes) (*AuditInfo, error) // LocktimeExpired returns true if the specified contract's locktime has // expired, making it possible to issue a Refund. The contract expiry time // is also returned, but reaching this time does not necessarily mean the @@ -216,17 +216,17 @@ type Receipt interface { // AuditInfo is audit information about a swap contract needed to audit the // contract. -type AuditInfo interface { +type AuditInfo struct { // Recipient is the string-encoded recipient address. - Recipient() string + Recipient string // Expiration is the unix timestamp of the contract time lock expiration. - Expiration() time.Time + Expiration time.Time // Coin is the coin that contains the contract. - Coin() Coin + Coin Coin // Contract is the contract script. - Contract() dex.Bytes + Contract dex.Bytes // SecretHash is the contract's secret hash. - SecretHash() dex.Bytes + SecretHash dex.Bytes } // INPUT TYPES @@ -262,7 +262,7 @@ type Contract struct { // contract. type Redemption struct { // Spends is the AuditInfo for the swap output being spent. - Spends AuditInfo + Spends *AuditInfo // Secret is the secret key needed to satisfy the swap contract. Secret dex.Bytes } diff --git a/client/asset/ltc/ltc.go b/client/asset/ltc/ltc.go index b2e6419b4d..5d468cf4d5 100644 --- a/client/asset/ltc/ltc.go +++ b/client/asset/ltc/ltc.go @@ -154,6 +154,7 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) DefaultFallbackFee: defaultFee, DefaultFeeRateLimit: defaultFeeRateLimit, LegacyBalance: true, + LegacyRawFeeLimit: true, Segwit: false, } diff --git a/client/cmd/dexc/main.go b/client/cmd/dexc/main.go index bfb9bb4a96..06394504cc 100644 --- a/client/cmd/dexc/main.go +++ b/client/cmd/dexc/main.go @@ -12,6 +12,7 @@ import ( "sync" "time" + _ "decred.org/dcrdex/client/asset/bch" // register bch asset _ "decred.org/dcrdex/client/asset/btc" // register btc asset _ "decred.org/dcrdex/client/asset/dcr" // register dcr asset _ "decred.org/dcrdex/client/asset/ltc" // register ltc asset diff --git a/client/cmd/dexcctl/simnet-setup.sh b/client/cmd/dexcctl/simnet-setup.sh index c2c8cbd961..940e9c20ba 100755 --- a/client/cmd/dexcctl/simnet-setup.sh +++ b/client/cmd/dexcctl/simnet-setup.sh @@ -2,17 +2,39 @@ # Set up DCR and BTC wallets and register with the DEX. # dcrdex, dexc, and the wallet simnet harnesses should all be running before # calling this script. + +set +e + +~/dextest/ltc/harness-ctl/alpha getblockchaininfo > /dev/null +LTC_ON=$? + +~/dextest/bch/harness-ctl/alpha getblockchaininfo > /dev/null +BCH_ON=$? + set -e + echo initializing ./dexcctl -p abc --simnet init + echo configuring Decred wallet ./dexcctl -p abc -p abc --simnet newwallet 42 ~/dextest/dcr/alpha/alpha.conf '{"account":"default"}' + echo configuring Bitcoin wallet ./dexcctl -p abc -p "" --simnet newwallet 0 ~/dextest/btc/alpha/alpha.conf '{"walletname":"gamma"}' -echo configuring Litecoin wallet -./dexcctl -p abc -p "" --simnet newwallet 2 ~/dextest/ltc/alpha/alpha.conf '{"walletname":"gamma"}' + +if [ $LTC_ON -eq 0 ]; then + echo configuring Litecoin wallet + ./dexcctl -p abc -p "" --simnet newwallet 2 ~/dextest/ltc/alpha/alpha.conf '{"walletname":"gamma"}' +fi + +if [ $BCH_ON -eq 0 ]; then + echo configuring Bitcoin Cash wallet + ./dexcctl -p abc -p "" --simnet newwallet 145 ~/dextest/bch/alpha/alpha.conf '{"walletname":"gamma"}' +fi + echo registering with DEX ./dexcctl -p abc --simnet register 127.0.0.1:17273 100000000 ~/dextest/dcrdex/rpc.cert + echo mining fee confirmation blocks tmux send-keys -t dcr-harness:0 "./mine-alpha 1" C-m sleep 2 diff --git a/client/core/core_test.go b/client/core/core_test.go index da106f7917..e5cac94e00 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -500,34 +500,6 @@ func (r *tReceipt) String() string { return r.coin.String() } -type tAuditInfo struct { - recipient string - expiration time.Time - coin *tCoin - contract []byte - secretHash []byte -} - -func (ai *tAuditInfo) Recipient() string { - return ai.recipient -} - -func (ai *tAuditInfo) Expiration() time.Time { - return ai.expiration -} - -func (ai *tAuditInfo) Coin() asset.Coin { - return ai.coin -} - -func (ai *tAuditInfo) Contract() dex.Bytes { - return ai.contract -} - -func (ai *tAuditInfo) SecretHash() dex.Bytes { - return ai.secretHash -} - type TXCWallet struct { mtx sync.RWMutex payFeeCoin *tCoin @@ -538,7 +510,7 @@ type TXCWallet struct { swapReceipts []asset.Receipt swapCounter int swapErr error - auditInfo asset.AuditInfo + auditInfo *asset.AuditInfo auditErr error auditChan chan struct{} refundCoin dex.Bytes @@ -687,7 +659,7 @@ func (w *TXCWallet) SignMessage(asset.Coin, dex.Bytes) (pubkeys, sigs []dex.Byte return nil, nil, w.signCoinErr } -func (w *TXCWallet) AuditContract(coinID, contract dex.Bytes) (asset.AuditInfo, error) { +func (w *TXCWallet) AuditContract(coinID, contract dex.Bytes) (*asset.AuditInfo, error) { defer func() { if w.auditChan != nil { w.auditChan <- struct{}{} @@ -1768,7 +1740,7 @@ func TestLogin(t *testing.T) { // The extra match is already at MakerSwapCast, and we're the taker, which // will invoke match status conflict resolution and a contract audit. _, auditInfo := tMsgAudit(oid, extraID, addr, qty, encode.RandomBytes(32)) - auditInfo.expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker)) + auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker)) tBtcWallet.auditInfo = auditInfo missedContract := encode.RandomBytes(50) rig.ws.queueResponse(msgjson.MatchStatusRoute, func(msg *msgjson.Message, f msgFunc) error { @@ -2840,7 +2812,7 @@ func TestTradeTracking(t *testing.T) { // Send the counter-party's init info. auditQty := calc.BaseToQuote(rate, matchSize) audit, auditInfo := tMsgAudit(loid, mid, addr, auditQty, proof.SecretHash) - auditInfo.expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeTaker)) + auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeTaker)) tBtcWallet.auditInfo = auditInfo msg, _ = msgjson.NewRequest(1, msgjson.AuditRoute, audit) @@ -2865,33 +2837,33 @@ func TestTradeTracking(t *testing.T) { tBtcWallet.auditErr = nil match.MetaData.Proof.SelfRevoked = false - auditInfo.coin.val = auditQty - 1 + auditInfo.Coin.(*tCoin).val = auditQty - 1 err = tracker.auditContract(match, audit.CoinID, audit.Contract) if err == nil { t.Fatalf("no maker error for low value") } - auditInfo.coin.val = auditQty + auditInfo.Coin.(*tCoin).val = auditQty - auditInfo.secretHash = []byte{0x01} + auditInfo.SecretHash = []byte{0x01} err = tracker.auditContract(match, audit.CoinID, audit.Contract) if err == nil { t.Fatalf("no maker error for wrong secret hash") } - auditInfo.secretHash = proof.SecretHash + auditInfo.SecretHash = proof.SecretHash - auditInfo.recipient = "wrong address" + auditInfo.Recipient = "wrong address" err = tracker.auditContract(match, audit.CoinID, audit.Contract) if err == nil { t.Fatalf("no maker error for wrong address") } - auditInfo.recipient = addr + auditInfo.Recipient = addr - auditInfo.expiration = matchTime.Add(tracker.lockTimeTaker - time.Hour) + auditInfo.Expiration = matchTime.Add(tracker.lockTimeTaker - time.Hour) err = tracker.auditContract(match, audit.CoinID, audit.Contract) if err == nil { t.Fatalf("no maker error for early lock time") } - auditInfo.expiration = matchTime.Add(tracker.lockTimeTaker) + auditInfo.Expiration = matchTime.Add(tracker.lockTimeTaker) // success, full handleAuditRoute>processAuditMsg>auditContract rig.db.setUpdateMatchHook(mid, make(chan order.MatchStatus, 1)) @@ -2922,7 +2894,7 @@ func TestTradeTracking(t *testing.T) { } // Confirming the counter-swap triggers a redemption. - tBtcWallet.setConfs(auditInfo.coin.ID(), tBTC.SwapConf, nil) + tBtcWallet.setConfs(auditInfo.Coin.ID(), tBTC.SwapConf, nil) redeemCoin := encode.RandomBytes(36) //<-tBtcWallet.redeemErrChan tBtcWallet.redeemCoins = []dex.Bytes{redeemCoin} @@ -2998,14 +2970,14 @@ func TestTradeTracking(t *testing.T) { audit, auditInfo = tMsgAudit(loid, mid, addr, matchSize, nil) tBtcWallet.auditInfo = auditInfo // early lock time - auditInfo.expiration = matchTime.Add(tracker.lockTimeMaker - time.Hour) + auditInfo.Expiration = matchTime.Add(tracker.lockTimeMaker - time.Hour) err = tracker.auditContract(match, audit.CoinID, audit.Contract) if err == nil { t.Fatalf("no taker error for early lock time") } // success, full handleAuditRoute>processAuditMsg>auditContract - auditInfo.expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker)) + auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker)) msg, _ = msgjson.NewRequest(1, msgjson.AuditRoute, audit) err = handleAuditRoute(tCore, rig.dc, msg) if err != nil { @@ -3035,7 +3007,7 @@ func TestTradeTracking(t *testing.T) { t.Fatalf("swap broadcast before confirmations") } // confirming maker's swap should trigger taker's swap bcast - tBtcWallet.setConfs(auditInfo.coin.ID(), tBTC.SwapConf, nil) + tBtcWallet.setConfs(auditInfo.Coin.ID(), tBTC.SwapConf, nil) swapID := encode.RandomBytes(36) tDcrWallet.swapReceipts = []asset.Receipt{&tReceipt{coin: &tCoin{id: swapID}}} rig.ws.queueResponse(msgjson.InitRoute, initAcker) @@ -3541,7 +3513,7 @@ func TestRefunds(t *testing.T) { auditQty := calc.BaseToQuote(rate, matchSize) audit, auditInfo := tMsgAudit(loid, mid, addr, auditQty, proof.SecretHash) tBtcWallet.auditInfo = auditInfo - auditInfo.expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker)) + auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker)) // Check audit errors. tBtcWallet.auditErr = tErr @@ -3591,7 +3563,7 @@ func TestRefunds(t *testing.T) { rig.db.setUpdateMatchHook(mid, make(chan order.MatchStatus, 1)) audit, auditInfo = tMsgAudit(loid, mid, addr, matchSize, nil) tBtcWallet.auditInfo = auditInfo - auditInfo.expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker)) + auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker)) tBtcWallet.auditErr = nil msg, _ = msgjson.NewRequest(1, msgjson.AuditRoute, audit) err = handleAuditRoute(tCore, rig.dc, msg) @@ -3610,7 +3582,7 @@ func TestRefunds(t *testing.T) { } tracker.mtx.RUnlock() // maker's swap confirmation should trigger taker's swap bcast - tBtcWallet.setConfs(auditInfo.coin.ID(), tBTC.SwapConf, nil) + tBtcWallet.setConfs(auditInfo.Coin.ID(), tBTC.SwapConf, nil) counterSwapID := encode.RandomBytes(36) counterScript := encode.RandomBytes(36) tDcrWallet.swapReceipts = []asset.Receipt{&tReceipt{coin: &tCoin{id: counterSwapID}, contract: counterScript}} @@ -4137,7 +4109,7 @@ func orderResponse(msgID uint64, msgPrefix msgjson.Stampable, ord order.Order, b return resp } -func tMsgAudit(oid order.OrderID, mid order.MatchID, recipient string, val uint64, secretHash []byte) (*msgjson.Audit, *tAuditInfo) { +func tMsgAudit(oid order.OrderID, mid order.MatchID, recipient string, val uint64, secretHash []byte) (*msgjson.Audit, *asset.AuditInfo) { auditID := encode.RandomBytes(36) auditContract := encode.RandomBytes(75) if secretHash == nil { @@ -4153,11 +4125,11 @@ func tMsgAudit(oid order.OrderID, mid order.MatchID, recipient string, val uint6 } sign(tDexPriv, audit) auditCoin := &tCoin{id: auditID, val: val} - auditInfo := &tAuditInfo{ - recipient: recipient, - coin: auditCoin, - contract: auditContract, - secretHash: secretHash, + auditInfo := &asset.AuditInfo{ + Recipient: recipient, + Coin: auditCoin, + Contract: auditContract, + SecretHash: secretHash, } return audit, auditInfo } @@ -5406,9 +5378,9 @@ func TestMatchStatusResolution(t *testing.T) { proof := &match.MetaData.Proof if isMaker { - auditInfo.expiration = matchTime.Add(trade.lockTimeTaker) + auditInfo.Expiration = matchTime.Add(trade.lockTimeTaker) } else { - auditInfo.expiration = matchTime.Add(trade.lockTimeMaker) + auditInfo.Expiration = matchTime.Add(trade.lockTimeMaker) } if status >= order.MakerSwapCast { @@ -5619,7 +5591,7 @@ func TestMatchStatusResolution(t *testing.T) { servers: order.MakerSwapCast, side: order.Taker, tweaker: func() { - auditInfo.expiration = matchTime + auditInfo.Expiration = matchTime }, countStatusUpdates: 2, // async auditContract -> revoke and db update }, @@ -5654,7 +5626,7 @@ func TestMatchStatusResolution(t *testing.T) { servers: order.TakerSwapCast, side: order.Maker, tweaker: func() { - auditInfo.expiration = matchTime + auditInfo.Expiration = matchTime }, countStatusUpdates: 2, // async auditContract -> revoke and db update }, @@ -5852,10 +5824,10 @@ func TestSuspectTrades(t *testing.T) { // Set counterswaps for both swaps. _, auditInfo := tMsgAudit(oid, swappableMatch2.id, ordertest.RandomAddress(), 1, encode.RandomBytes(32)) - tBtcWallet.setConfs(auditInfo.coin.ID(), tDCR.SwapConf, nil) + tBtcWallet.setConfs(auditInfo.Coin.ID(), tDCR.SwapConf, nil) swappableMatch2.counterSwap = auditInfo _, auditInfo = tMsgAudit(oid, swappableMatch1.id, ordertest.RandomAddress(), 1, encode.RandomBytes(32)) - tBtcWallet.setConfs(auditInfo.coin.ID(), tDCR.SwapConf, nil) + tBtcWallet.setConfs(auditInfo.Coin.ID(), tDCR.SwapConf, nil) swappableMatch1.counterSwap = auditInfo tDcrWallet.swapCounter = 0 @@ -5923,7 +5895,7 @@ func TestSuspectTrades(t *testing.T) { redeemableMatch1 = newMatch(order.Maker, order.TakerSwapCast) redeemableMatch2 = newMatch(order.Taker, order.MakerRedeemed) _, auditInfo := tMsgAudit(oid, redeemableMatch1.id, ordertest.RandomAddress(), 1, encode.RandomBytes(32)) - tBtcWallet.setConfs(auditInfo.coin.ID(), tBTC.SwapConf, nil) + tBtcWallet.setConfs(auditInfo.Coin.ID(), tBTC.SwapConf, nil) redeemableMatch1.counterSwap = auditInfo tBtcWallet.redeemCounter = 0 tracker.matches = map[order.MatchID]*matchTracker{ diff --git a/client/core/trade.go b/client/core/trade.go index ea9b66a2a7..aefb875467 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -74,7 +74,7 @@ type matchTracker struct { refundErr error prefix *order.Prefix trade *order.Trade - counterSwap asset.AuditInfo + counterSwap *asset.AuditInfo // cancelRedemptionSearch should be set when taker starts searching for // maker's redemption. Required to cancel a find redemption attempt if @@ -616,7 +616,7 @@ func (t *trackedTrade) counterPartyConfirms(ctx context.Context, match *matchTra t.dc.log.Warnf("counterPartyConfirms: No AuditInfo available to check!") return } - coin := match.counterSwap.Coin() + coin := match.counterSwap.Coin var err error have, spent, err = t.wallets.toWallet.Confirmations(ctx, coin.ID()) @@ -1969,9 +1969,9 @@ func (t *trackedTrade) processAuditMsg(msgID uint64, audit *msgjson.Audit) error // counterparty contract data again except on reconnect. This may block for a // long time and should be run in a goroutine. The trackedTrade mtx must NOT be // locked. -func (t *trackedTrade) searchAuditInfo(match *matchTracker, coinID []byte, contract []byte) (asset.AuditInfo, error) { +func (t *trackedTrade) searchAuditInfo(match *matchTracker, coinID []byte, contract []byte) (*asset.AuditInfo, error) { errChan := make(chan error, 1) - var auditInfo asset.AuditInfo + var auditInfo *asset.AuditInfo var tries int contractID, contractSymb := coinIDString(t.wallets.toAsset.ID, coinID), t.wallets.toAsset.Symbol t.latencyQ.Wait(&wait.Waiter{ @@ -2040,9 +2040,9 @@ func (t *trackedTrade) auditContract(match *matchTracker, coinID []byte, contrac // 1. Recipient Address // 2. Contract value // 3. Secret hash: maker compares, taker records - if auditInfo.Recipient() != t.Trade().Address { + if auditInfo.Recipient != t.Trade().Address { return fmt.Errorf("swap recipient %s in contract coin %v (%s) is not the order address %s", - auditInfo.Recipient(), contractID, contractSymb, t.Trade().Address) + auditInfo.Recipient, contractID, contractSymb, t.Trade().Address) } dbMatch := match.Match @@ -2050,9 +2050,9 @@ func (t *trackedTrade) auditContract(match *matchTracker, coinID []byte, contrac if t.Trade().Sell { auditQty = calc.BaseToQuote(dbMatch.Rate, auditQty) } - if auditInfo.Coin().Value() < auditQty { + if auditInfo.Coin.Value() < auditQty { return fmt.Errorf("swap contract coin %v (%s) value %d was lower than expected %d", - contractID, contractSymb, auditInfo.Coin().Value(), auditQty) + contractID, contractSymb, auditInfo.Coin.Value(), auditQty) } // TODO: Consider having the server supply the contract txn's fee rate to @@ -2069,8 +2069,8 @@ func (t *trackedTrade) auditContract(match *matchTracker, coinID []byte, contrac if dbMatch.Side == order.Maker { reqLockTime = encode.DropMilliseconds(matchTime.Add(t.lockTimeTaker)) // counterparty == taker } - if auditInfo.Expiration().Before(reqLockTime) { - return fmt.Errorf("lock time too early. Need %s, got %s", reqLockTime, auditInfo.Expiration()) + if auditInfo.Expiration.Before(reqLockTime) { + return fmt.Errorf("lock time too early. Need %s, got %s", reqLockTime, auditInfo.Expiration) } t.mtx.Lock() @@ -2078,15 +2078,15 @@ func (t *trackedTrade) auditContract(match *matchTracker, coinID []byte, contrac proof := &match.MetaData.Proof if dbMatch.Side == order.Maker { // Check that the secret hash is correct. - if !bytes.Equal(proof.SecretHash, auditInfo.SecretHash()) { + if !bytes.Equal(proof.SecretHash, auditInfo.SecretHash) { return fmt.Errorf("secret hash mismatch for contract coin %v (%s), contract %v. expected %x, got %v", - auditInfo.Coin(), t.wallets.toAsset.Symbol, contract, proof.SecretHash, auditInfo.SecretHash()) + auditInfo.Coin, t.wallets.toAsset.Symbol, contract, proof.SecretHash, auditInfo.SecretHash) } // Audit successful. Update status and other match data. match.SetStatus(order.TakerSwapCast) proof.TakerSwap = coinID } else { - proof.SecretHash = auditInfo.SecretHash() + proof.SecretHash = auditInfo.SecretHash match.SetStatus(order.MakerSwapCast) proof.MakerSwap = coinID } @@ -2099,7 +2099,7 @@ func (t *trackedTrade) auditContract(match *matchTracker, coinID []byte, contrac } t.dc.log.Infof("Audited contract (%s: %v) paying to %s for order %s, match %s", - t.wallets.toAsset.Symbol, auditInfo.Coin(), auditInfo.Recipient(), t.ID(), match.id) + t.wallets.toAsset.Symbol, auditInfo.Coin, auditInfo.Recipient, t.ID(), match.id) return nil } diff --git a/client/webserver/site/src/html/bodybuilder.tmpl b/client/webserver/site/src/html/bodybuilder.tmpl index 41a1a8cdef..96ce76d63f 100644 --- a/client/webserver/site/src/html/bodybuilder.tmpl +++ b/client/webserver/site/src/html/bodybuilder.tmpl @@ -85,7 +85,7 @@ {{end}} {{define "bottom"}} - + {{end}} diff --git a/client/webserver/site/src/img/coins/bch.png b/client/webserver/site/src/img/coins/bch.png new file mode 100644 index 0000000000..86d6016d5c Binary files /dev/null and b/client/webserver/site/src/img/coins/bch.png differ diff --git a/client/webserver/site/src/js/doc.js b/client/webserver/site/src/js/doc.js index 64d31e4390..e0c93b63c0 100644 --- a/client/webserver/site/src/js/doc.js +++ b/client/webserver/site/src/js/doc.js @@ -8,7 +8,8 @@ const BipIDs = { 2: 'ltc', 22: 'mona', 28: 'vtc', - 3: 'doge' + 3: 'doge', + 145: 'bch' } const BipSymbols = Object.values(BipIDs) diff --git a/dex/networks/bch/cashaddr.go b/dex/networks/bch/cashaddr.go new file mode 100644 index 0000000000..a1176141fa --- /dev/null +++ b/dex/networks/bch/cashaddr.go @@ -0,0 +1,86 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org + +package bch + +import ( + "fmt" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + bchchaincfg "github.com/gcash/bchd/chaincfg" + "github.com/gcash/bchutil" +) + +// RecodeCashAddress takes a BTC base-58 encoded address and converts it into a +// Cash Address. +func RecodeCashAddress(addr string, net *chaincfg.Params) (string, error) { + btcAddr, err := btcutil.DecodeAddress(addr, net) + if err != nil { + return "", err + } + + var bchAddr bchutil.Address + switch at := btcAddr.(type) { + case *btcutil.AddressPubKeyHash: + bchAddr, err = bchutil.NewAddressPubKeyHash(btcAddr.ScriptAddress(), convertParams(net)) + case *btcutil.AddressScriptHash: + bchAddr, err = bchutil.NewAddressScriptHashFromHash(btcAddr.ScriptAddress(), convertParams(net)) + case *btcutil.AddressPubKey: + bchAddr, err = bchutil.NewAddressPubKey(btcAddr.ScriptAddress(), convertParams(net)) + default: + return "", fmt.Errorf("unsupported address type %T", at) + } + + if err != nil { + return "", err + } + + return withPrefix(bchAddr, net), nil +} + +// DecodeCashAddress decodes a Cash Address string into a btcutil.Address +// that the BTC backend can use internally. +func DecodeCashAddress(addr string, net *chaincfg.Params) (btcutil.Address, error) { + bchAddr, err := bchutil.DecodeAddress(addr, convertParams(net)) + if err != nil { + return nil, fmt.Errorf("error decoding CashAddr address: %v", err) + } + + switch at := bchAddr.(type) { + // From what I can tell, the legacy address formats are probably + // unnecessary, but I'd hate to be wrong. + case *bchutil.AddressPubKeyHash, *bchutil.LegacyAddressPubKeyHash: + return btcutil.NewAddressPubKeyHash(bchAddr.ScriptAddress(), net) + case *bchutil.AddressScriptHash, *bchutil.LegacyAddressScriptHash: + return btcutil.NewAddressScriptHashFromHash(bchAddr.ScriptAddress(), net) + case *bchutil.AddressPubKey: + return btcutil.NewAddressPubKey(bchAddr.ScriptAddress(), net) + default: + return nil, fmt.Errorf("unsupported address type %T", at) + } +} + +// convertParams converts the btcd/*chaincfg.Params to a bchd/*chaincfg.Params. +func convertParams(btcParams *chaincfg.Params) *bchchaincfg.Params { + switch btcParams.Net { + case MainNetParams.Net: + return &bchchaincfg.MainNetParams + case TestNet3Params.Net: + return &bchchaincfg.TestNet3Params + case RegressionNetParams.Net: + return &bchchaincfg.RegressionNetParams + } + panic(fmt.Sprintf("unknown network for %s chain: %v", btcParams.Name, btcParams.Net)) +} + +// withPrefix adds the Bech32 prefix to the bchutil.Address, since the stringers +// don't, for some reason. +func withPrefix(bchAddr bchutil.Address, net *chaincfg.Params) string { + switch bchAddr.(type) { + case *bchutil.AddressPubKeyHash, *bchutil.AddressScriptHash: + return net.Bech32HRPSegwit + ":" + bchAddr.String() + } + // Must be a pubkey address, which gets no prefix. + return bchAddr.String() +} diff --git a/dex/networks/bch/cashaddr_test.go b/dex/networks/bch/cashaddr_test.go new file mode 100644 index 0000000000..e9cc863f32 --- /dev/null +++ b/dex/networks/bch/cashaddr_test.go @@ -0,0 +1,138 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package bch + +import ( + "testing" + + "decred.org/dcrdex/dex/encode" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/gcash/bchutil" +) + +func TestCashAddr(t *testing.T) { + lowB := make([]byte, 20) + highB := make([]byte, 20) + for i := range highB { + highB[i] = 255 + } + + lowPubKey := make([]byte, 33) + lowPubKey[0] = 2 + lowPubKey[32] = 1 + + checkHash := func(net *chaincfg.Params, h []byte) { + t.Helper() + + switch len(h) { + case 20: + var bchAddr bchutil.Address + bchAddr, err := bchutil.NewAddressPubKeyHash(h, convertParams(net)) + if err != nil { + t.Fatalf("bchutil.AddressScriptHash error: %v", err) + } + testRoundTripFromBCH(t, withPrefix(bchAddr, net), net) + + bchAddr, err = bchutil.NewAddressScriptHashFromHash(h, convertParams(net)) + if err != nil { + t.Fatalf("bchutil.AddressScriptHash error: %v", err) + } + testRoundTripFromBCH(t, withPrefix(bchAddr, net), net) + + var btcAddr btcutil.Address + btcAddr, err = btcutil.NewAddressPubKeyHash(h, net) + if err != nil { + t.Fatalf("btcutil.NewAddressPubkeyHash error: %v", err) + } + + testRoundTripFromBTC(t, btcAddr.String(), net) + + btcAddr, err = btcutil.NewAddressScriptHashFromHash(h, net) + if err != nil { + t.Fatalf("btcutil.NewAddressPubkeyHash error: %v", err) + } + testRoundTripFromBTC(t, btcAddr.String(), net) + + case 33, 65: // See btcec.PubKeyBytesLen(Un)Compressed + var bchAddr bchutil.Address + bchAddr, err := bchutil.NewAddressPubKey(h, convertParams(net)) + if err != nil { + t.Fatalf("bchutil.NewAddressPubKey error: %v", err) + } + testRoundTripFromBCH(t, withPrefix(bchAddr, net), net) + + var btcAddr btcutil.Address + btcAddr, err = btcutil.NewAddressPubKey(h, net) + if err != nil { + t.Fatalf("btcutil.NewAddressPubKey error: %v", err) + } + + testRoundTripFromBTC(t, btcAddr.String(), net) + + default: + t.Fatalf("unknown address data length %d", len(h)) + } + + } + + nets := []*chaincfg.Params{MainNetParams, TestNet3Params, RegressionNetParams} + for _, net := range nets { + // Check the lowest and highest possible hashes. + checkHash(net, lowB) + checkHash(net, highB) + // Check a bunch of random addresses. + for i := 0; i < 1000; i++ { + checkHash(net, encode.RandomBytes(20)) + } + // Check Pubkey addresses. These just encode to the hex encoding of the + // serialized pubkey for both bch and btc, so there's little that can go + // wrong. + checkHash(net, lowPubKey) + for i := 0; i < 100; i++ { + _, pubKey := btcec.PrivKeyFromBytes(btcec.S256(), encode.RandomBytes(33)) + checkHash(net, pubKey.SerializeUncompressed()) + checkHash(net, pubKey.SerializeCompressed()) + } + + } +} + +func testRoundTripFromBCH(t *testing.T, bchAddrStr string, net *chaincfg.Params) { + t.Helper() + + btcAddr, err := DecodeCashAddress(bchAddrStr, net) + if err != nil { + t.Fatalf("DecodeCashAddress error: %v", err) + } + + reAddr, err := RecodeCashAddress(btcAddr.String(), net) + if err != nil { + t.Fatalf("RecodeCashAddr error: %v", err) + } + + if reAddr != bchAddrStr { + t.Fatalf("Recoded address mismatch: %s != %s", reAddr, bchAddrStr) + } +} + +func testRoundTripFromBTC(t *testing.T, btcAddrStr string, net *chaincfg.Params) { + t.Helper() + + bchAddrStr, err := RecodeCashAddress(btcAddrStr, net) + if err != nil { + t.Fatalf("RecodeCashAddr error: %v", err) + } + + btcAddr, err := DecodeCashAddress(bchAddrStr, net) + if err != nil { + t.Fatalf("DecodeCashAddress error: %v", err) + } + + reAddr := btcAddr.String() + if reAddr != btcAddrStr { + t.Fatalf("Decoded address mismatch: %s != %s", reAddr, btcAddrStr) + } +} diff --git a/dex/networks/bch/params.go b/dex/networks/bch/params.go new file mode 100644 index 0000000000..c885cb3767 --- /dev/null +++ b/dex/networks/bch/params.go @@ -0,0 +1,50 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org + +package bch + +import ( + "decred.org/dcrdex/dex/networks/btc" + "github.com/btcsuite/btcd/chaincfg" +) + +var ( + // MainNetParams are the clone parameters for mainnet. + MainNetParams = btc.ReadCloneParams(&btc.CloneParams{ + PubKeyHashAddrID: 0x00, + ScriptHashAddrID: 0x05, + Bech32HRPSegwit: "bitcoincash", + CoinbaseMaturity: 100, + Net: 0xe8f3e1e3, + }) + // TestNet3Params are the clone parameters for testnet. + TestNet3Params = btc.ReadCloneParams(&btc.CloneParams{ + PubKeyHashAddrID: 0x6f, + ScriptHashAddrID: 0xc4, + Bech32HRPSegwit: "bchtest", + CoinbaseMaturity: 100, + Net: 0xf4f3e5f4, + }) + // RegressionNetParams are the clone parameters for simnet. + RegressionNetParams = btc.ReadCloneParams(&btc.CloneParams{ + PubKeyHashAddrID: 0x6f, + ScriptHashAddrID: 0xc4, + Bech32HRPSegwit: "bchreg", + CoinbaseMaturity: 100, + // Net is not the standard for BCH simnet, since they never changed it + // from the BTC value. The only place we currently use Net is in + // btcd/chaincfg.Register, where it is checked to prevent duplicate + // registration, so our only requirement is that it is unique. This one + // was just generated with a prng. + Net: 0xee87f733, + }) +) + +func init() { + for _, params := range []*chaincfg.Params{MainNetParams, TestNet3Params, RegressionNetParams} { + err := chaincfg.Register(params) + if err != nil { + panic("failed to register bch parameters: " + err.Error()) + } + } +} diff --git a/dex/networks/btc/clone.go b/dex/networks/btc/clone.go index 283746610e..fc8b949bb9 100644 --- a/dex/networks/btc/clone.go +++ b/dex/networks/btc/clone.go @@ -6,8 +6,12 @@ package btc import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" ) +// AddressDecoder decodes a string address to a btcutil.Address. +type AddressDecoder func(addr string, net *chaincfg.Params) (btcutil.Address, error) + // ReadCloneParams translates a CloneParams into a btcsuite chaincfg.Params. func ReadCloneParams(cloneParams *CloneParams) *chaincfg.Params { return &chaincfg.Params{ diff --git a/dex/networks/btc/script.go b/dex/networks/btc/script.go index 88b329ac60..021e344e8a 100644 --- a/dex/networks/btc/script.go +++ b/dex/networks/btc/script.go @@ -301,32 +301,24 @@ func ParseScriptType(pkScript, redeemScript []byte) BTCScriptType { // MakeContract creates a segwit atomic swap contract. The secretHash MUST // be computed from a secret of length SecretKeySize bytes or the resulting // contract will be invalid. -func MakeContract(recipient, sender string, secretHash []byte, lockTime int64, segwit bool, chainParams *chaincfg.Params) ([]byte, error) { - rAddr, err := btcutil.DecodeAddress(recipient, chainParams) - if err != nil { - return nil, fmt.Errorf("error decoding recipient address %s: %w", recipient, err) - } - sAddr, err := btcutil.DecodeAddress(sender, chainParams) - if err != nil { - return nil, fmt.Errorf("error decoding sender address %s: %w", sender, err) - } +func MakeContract(rAddr, sAddr btcutil.Address, secretHash []byte, lockTime int64, segwit bool, chainParams *chaincfg.Params) ([]byte, error) { if segwit { _, ok := rAddr.(*btcutil.AddressWitnessPubKeyHash) if !ok { - return nil, fmt.Errorf("recipient address %s is not a witness-pubkey-hash address", recipient) + return nil, fmt.Errorf("recipient address %s is not a witness-pubkey-hash address", rAddr.String()) } _, ok = sAddr.(*btcutil.AddressWitnessPubKeyHash) if !ok { - return nil, fmt.Errorf("sender address %s is not a witness-pubkey-hash address", recipient) + return nil, fmt.Errorf("sender address %s is not a witness-pubkey-hash address", sAddr.String()) } } else { _, ok := rAddr.(*btcutil.AddressPubKeyHash) if !ok { - return nil, fmt.Errorf("recipient address %s is not a witness-pubkey-hash address", recipient) + return nil, fmt.Errorf("recipient address %s is not a witness-pubkey-hash address", rAddr.String()) } _, ok = sAddr.(*btcutil.AddressPubKeyHash) if !ok { - return nil, fmt.Errorf("sender address %s is not a witness-pubkey-hash address", recipient) + return nil, fmt.Errorf("sender address %s is not a witness-pubkey-hash address", sAddr.String()) } } if len(secretHash) != SecretHashSize { diff --git a/dex/networks/btc/script_test.go b/dex/networks/btc/script_test.go index fa836b3e9b..4615d4c2c1 100644 --- a/dex/networks/btc/script_test.go +++ b/dex/networks/btc/script_test.go @@ -152,42 +152,26 @@ func testMakeContract(t *testing.T, segwit bool) { p2pkh, _ = btcutil.NewAddressWitnessPubKeyHash(randBytes(20), tParams) } - recipient := ra.String() - sender := sa.String() - badAddr := "notanaddress" - - // Bad recipient - _, err := MakeContract(badAddr, sender, randBytes(32), tStamp, segwit, tParams) - if err == nil { - t.Fatalf("no error for bad recipient") - } // Wrong recipient address type - - _, err = MakeContract(p2sh.String(), sender, randBytes(32), tStamp, segwit, tParams) + _, err := MakeContract(p2sh, sa, randBytes(32), tStamp, segwit, tParams) if err == nil { t.Fatalf("no error for wrong recipient address type") } - // Bad sender - _, err = MakeContract(recipient, badAddr, randBytes(32), tStamp, segwit, tParams) - if err == nil { - t.Fatalf("no error for bad sender") - } // Wrong sender address type. - - _, err = MakeContract(recipient, p2pkh.String(), randBytes(32), tStamp, segwit, tParams) + _, err = MakeContract(ra, p2pkh, randBytes(32), tStamp, segwit, tParams) if err == nil { t.Fatalf("no error for wrong sender address type") } // Bad secret hash - _, err = MakeContract(recipient, sender, randBytes(10), tStamp, segwit, tParams) + _, err = MakeContract(ra, sa, randBytes(10), tStamp, segwit, tParams) if err == nil { t.Fatalf("no error for bad secret hash") } // Good to go - _, err = MakeContract(recipient, sender, randBytes(32), tStamp, segwit, tParams) + _, err = MakeContract(ra, sa, randBytes(32), tStamp, segwit, tParams) if err != nil { t.Fatalf("error for valid contract parameters: %v", err) } @@ -337,10 +321,8 @@ func testExtractSwapDetails(t *testing.T, segwit bool) { sAddr, _ = btcutil.NewAddressPubKeyHash(randBytes(20), tParams) } - recipient := rAddr.String() - sender := sAddr.String() keyHash := randBytes(32) - contract, err := MakeContract(recipient, sender, keyHash, tStamp, segwit, tParams) + contract, err := MakeContract(rAddr, sAddr, keyHash, tStamp, segwit, tParams) if err != nil { t.Fatalf("error creating contract: %v", err) } @@ -349,11 +331,11 @@ func testExtractSwapDetails(t *testing.T, segwit bool) { if err != nil { t.Fatalf("error for valid contract: %v", err) } - if sa.String() != sender { - t.Fatalf("sender address mismatch. wanted %s, got %s", sender, sa.String()) + if sa.String() != sAddr.String() { + t.Fatalf("sender address mismatch. wanted %s, got %s", sAddr.String(), sa.String()) } - if ra.String() != recipient { - t.Fatalf("recipient address mismatch. wanted %s, got %s", recipient, ra.String()) + if ra.String() != rAddr.String() { + t.Fatalf("recipient address mismatch. wanted %s, got %s", rAddr.String(), ra.String()) } if lockTime != uint64(tStamp) { t.Fatalf("incorrect lock time. wanted 5, got %d", lockTime) @@ -458,13 +440,11 @@ func testFindKeyPush(t *testing.T, segwit bool) { rAddr, _ = btcutil.NewAddressPubKeyHash(randBytes(20), tParams) sAddr, _ = btcutil.NewAddressPubKeyHash(randBytes(20), tParams) } - recipient := rAddr.String() - sender := sAddr.String() secret := randBytes(32) secretHash := sha256.Sum256(secret) - contract, _ := MakeContract(recipient, sender, secretHash[:], tStamp, segwit, tParams) - randomContract, _ := MakeContract(recipient, sender, randBytes(32), tStamp, segwit, tParams) + contract, _ := MakeContract(rAddr, sAddr, secretHash[:], tStamp, segwit, tParams) + randomContract, _ := MakeContract(rAddr, sAddr, randBytes(32), tStamp, segwit, tParams) var sigScript, contractHash, randoSigScript []byte var witness, randoWitness [][]byte diff --git a/dex/testing/bch/harness.sh b/dex/testing/bch/harness.sh new file mode 100755 index 0000000000..b90fe2f8d7 --- /dev/null +++ b/dex/testing/bch/harness.sh @@ -0,0 +1,25 @@ +#!/bin/sh +export SYMBOL="bch" +# It is expected that devs rename their Bitcoin Cash binaries, since the +# original name conflicts with Bitcoin. +export DAEMON="bitcoincashd" +export CLI="bitcoincash-cli" +export RPC_USER="user" +export RPC_PASS="pass" +export ALPHA_LISTEN_PORT="21575" +export BETA_LISTEN_PORT="21576" +export ALPHA_RPC_PORT="21556" +export BETA_RPC_PORT="21557" +export ALPHA_WALLET_SEED="cMndqchcXSCUQDDZQSKU2cUHbPb5UfFL9afspxsBELeE6qx6ac9n" +export BETA_WALLET_SEED="cRHosJjgZ2UWsEAeHYYUFa8Z6viHYXm94GguGtpzMo6qwKBC1DSq" +export ALPHA_MINING_ADDR="bchreg:qqnm4z2tftyyeu3kvzzepmlp9mj3g6fvxgft570vll" +export BETA_MINING_ADDR="bchreg:qzr7nnmpnreyhgt9ex3082cg8j0dks8uts9khumg0m" +export WALLET_PASSWORD="abc" +# Gamma is a named wallet in the alpha wallet directory. +export GAMMA_WALLET_SEED="cR6gasj1RtB9Qv9j2kVej2XzQmXPmZBcn8KzUmxSSCQoz3TqTNMg" +export GAMMA_ADDRESS="bchreg:qzltvanmqgnl5gavt85c6gz2upzpu0n3lu95f4p5mv" +# Delta is a named wallet in the beta wallet directory. +export DELTA_WALLET_SEED="cURsyTZ8icuTHwWxSfTC2Geu2F6dMRtnzt1gvSaxHdc9Zf6eviJN" +export DELTA_ADDRESS="bchreg:qzhru360ks09fgzuh0ycpvslslvpj72ulqlw5j6ksy" +# Run the harness +../btc/base-harness.sh diff --git a/dex/testing/dcrdex/harness.sh b/dex/testing/dcrdex/harness.sh index 8d91ccf590..060ffd6b5a 100755 --- a/dex/testing/dcrdex/harness.sh +++ b/dex/testing/dcrdex/harness.sh @@ -29,6 +29,16 @@ fi echo "Writing markets.json and dcrdex.conf" +set +e + +~/dextest/bch/harness-ctl/alpha getblockchaininfo > /dev/null +BCH_ON=$? + +~/dextest/ltc/harness-ctl/alpha getblockchaininfo > /dev/null +LTC_ON=$? + +set -e + # Write markets.json. # The dcr and btc harnesses should be running. The assets config paths # used here are created by the respective harnesses. @@ -40,13 +50,32 @@ cat > "./markets.json" <> "./markets.json" }, { "base": "DCR_simnet", "quote": "LTC_simnet", "epochDuration": ${EPOCH_DURATION}, "marketBuyBuffer": 1.2 - } +EOF +fi + +if [ $BCH_ON -eq 0 ]; then + cat << EOF >> "./markets.json" + }, + { + "base": "DCR_simnet", + "quote": "BCH_simnet", + "epochDuration": ${EPOCH_DURATION}, + "marketBuyBuffer": 1.2 +EOF +fi + +cat << EOF >> "./markets.json" + } ], "assets": { "DCR_simnet": { @@ -66,7 +95,11 @@ cat > "./markets.json" <> "./markets.json" + }, "LTC_simnet": { "bip44symbol": "ltc", "network": "simnet", @@ -75,13 +108,31 @@ cat > "./markets.json" <> "./markets.json" + }, + "BCH_simnet": { + "bip44symbol": "bch", + "network": "simnet", + "lotSize": 1000000, + "rateStep": 1000000, + "maxFeeRate": 20, + "swapConf": 2, + "configPath": "${TEST_ROOT}/bch/alpha/alpha.conf" +EOF +fi + +cat << EOF >> "./markets.json" } } } EOF # Write dcrdex.conf. The regfeexpub comes from the alpha>server_fees account. -cat > "./dcrdex.conf" <> ./dcrdex.conf regfeexpub=spubVWKGn9TGzyo7M4b5xubB5UV4joZ5HBMNBmMyGvYEaoZMkSxVG4opckpmQ26E85iHg8KQxrSVTdex56biddqtXBerG9xMN8Dvb3eNQVFFwpE pgdbname=${TEST_DB} simnet=1 diff --git a/dex/testing/loadbot/go.sum b/dex/testing/loadbot/go.sum index b6165de5e3..1014501785 100644 --- a/dex/testing/loadbot/go.sum +++ b/dex/testing/loadbot/go.sum @@ -3,6 +3,7 @@ decred.org/cspp v0.3.0/go.mod h1:UygjYilC94dER3BEU65Zzyoqy9ngJfWCD2rdJqvUs2A= decred.org/dcrwallet v1.6.0-rc4 h1:5IT6mFa+2YMqenu6aE2LetD0N8QSUVFyAFl205PvIIE= decred.org/dcrwallet v1.6.0-rc4/go.mod h1:lsrNbuKxkPGeHXPufxNTckwQopCEDz0r3t0a8JCKAmU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OpenBazaar/jsonpb v0.0.0-20171123000858-37d32ddf4eef/go.mod h1:55mCznBcN9WQgrtgaAkv+p2LxeW/tQRdidyyE9D0I5k= github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -31,10 +32,13 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4= github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= +github.com/dchest/siphash v1.2.2 h1:9DFz8tQwl9pTVt5iok/9zKyzA1Q6bRGiF3HPiEEVr9I= +github.com/dchest/siphash v1.2.2/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI= github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E= github.com/decred/dcrd/addrmgr v1.2.0/go.mod h1:QlZF9vkzwYh0qs25C76SAFZBRscjETga/K28GEE6qIc= @@ -83,12 +87,22 @@ github.com/decred/go-socks v1.1.0 h1:dnENcc0KIqQo3HSXdgboXAHgqsCIutkqq6ntQjYtm2U github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0= github.com/decred/slog v1.1.0 h1:uz5ZFfmaexj1rEDgZvzQ7wjGkoSPjw2LCh8K+K1VrW4= github.com/decred/slog v1.1.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gcash/bchd v0.14.7/go.mod h1:Gk/O1ktRVW5Kao0RsnVXp3bWxeYQadqawZ1Im9HE78M= +github.com/gcash/bchd v0.15.2/go.mod h1:k9wIjgwnhbrAw+ruIPZ2tHZMzfFNdyUnORZZX7lqXGY= +github.com/gcash/bchd v0.17.2-0.20201218180520-5708823e0e99/go.mod h1:qwEZ/wr6LyUo5IBgAPcAbYHzXrjnr5gc4tj03n1TwKc= +github.com/gcash/bchlog v0.0.0-20180913005452-b4f036f92fa6/go.mod h1:PpfmXTLfjRp7Tf6v/DCGTRXHz+VFbiRcsoUxi7HvwlQ= +github.com/gcash/bchutil v0.0.0-20190625002603-800e62fe9aff/go.mod h1:zXSP0Fg2L52wpSEDApQDQMiSygnQiK5HDquDl0a5BHg= +github.com/gcash/bchutil v0.0.0-20191012211144-98e73ec336ba/go.mod h1:nUIrcbbtEQdCsRwcp+j/CndDKMQE9Fi8p2F8cIZmIqI= +github.com/gcash/bchutil v0.0.0-20200506001747-c2894cd54b33/go.mod h1:wB++2ZcHUvGLN1OgO9swBmJK1vmyshJLW9SNS+apXwc= +github.com/gcash/bchutil v0.0.0-20210113190856-6ea28dff4000/go.mod h1:H2USFGwtiu6CNMxiVQPqZkDzsoVSt9BLNqTfBBqGXRo= github.com/go-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w= github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= @@ -96,15 +110,19 @@ github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -112,14 +130,20 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/improbable-eng/grpc-web v0.9.1/go.mod h1:6hRR09jOEG81ADP5wCQju1z71g6OL4eEvELdran/3cs= +github.com/improbable-eng/grpc-web v0.13.0/go.mod h1:6hRR09jOEG81ADP5wCQju1z71g6OL4eEvELdran/3cs= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v0.0.0-20181221193153-c0795c8afcf4/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.1-0.20200711081900-c17162fe8fd7 h1:Ug59miTxVKVg5Oi2S5uHlKOIV5jBx4Hb2u0jIxxDaSs= @@ -131,38 +155,60 @@ github.com/jrick/wsrpc/v2 v2.3.4/go.mod h1:XPYs8BnRWl99lCvXRM5SLpZmTPqWpSOPkDIqY github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca h1:Ld/zXl5t4+D69SiV4JoN7kkfvJdOWlPpfxrzxpLMoUk= github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM= +github.com/zquestz/grab v0.0.0-20190224022517-abcee96e61b1/go.mod h1:bslhAiUxakrA6z6CHmVyvkfpnxx18RJBwVyx2TluJWw= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -174,12 +220,18 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201022231255-08b38378de70/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201024042810-be3efd7ff127/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= @@ -189,18 +241,27 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5 h1:iCaAy5bMeEvwANu3YnJfWwI0kWAGkEa2RXPdweI/ysk= +golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -215,26 +276,41 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201022181438-0ff5f38871d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/go.mod b/go.mod index 7313dc77c5..b4e4eb7a15 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,8 @@ require ( github.com/decred/dcrd/wire v1.4.0 github.com/decred/go-socks v1.1.0 github.com/decred/slog v1.1.0 + github.com/gcash/bchd v0.17.2-0.20201218180520-5708823e0e99 + github.com/gcash/bchutil v0.0.0-20210113190856-6ea28dff4000 github.com/go-chi/chi v1.5.1 github.com/gorilla/websocket v1.4.2 github.com/jessevdk/go-flags v1.4.1-0.20200711081900-c17162fe8fd7 diff --git a/go.sum b/go.sum index 9c23ac4aba..0b3c5b8138 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,7 @@ decred.org/cspp v0.3.0/go.mod h1:UygjYilC94dER3BEU65Zzyoqy9ngJfWCD2rdJqvUs2A= decred.org/dcrwallet v1.6.0-rc4 h1:5IT6mFa+2YMqenu6aE2LetD0N8QSUVFyAFl205PvIIE= decred.org/dcrwallet v1.6.0-rc4/go.mod h1:lsrNbuKxkPGeHXPufxNTckwQopCEDz0r3t0a8JCKAmU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OpenBazaar/jsonpb v0.0.0-20171123000858-37d32ddf4eef/go.mod h1:55mCznBcN9WQgrtgaAkv+p2LxeW/tQRdidyyE9D0I5k= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= @@ -29,10 +30,12 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4= github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= +github.com/dchest/siphash v1.2.2 h1:9DFz8tQwl9pTVt5iok/9zKyzA1Q6bRGiF3HPiEEVr9I= +github.com/dchest/siphash v1.2.2/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI= github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E= github.com/decred/dcrd/addrmgr v1.2.0/go.mod h1:QlZF9vkzwYh0qs25C76SAFZBRscjETga/K28GEE6qIc= @@ -80,40 +83,63 @@ github.com/decred/go-socks v1.1.0 h1:dnENcc0KIqQo3HSXdgboXAHgqsCIutkqq6ntQjYtm2U github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0= github.com/decred/slog v1.1.0 h1:uz5ZFfmaexj1rEDgZvzQ7wjGkoSPjw2LCh8K+K1VrW4= github.com/decred/slog v1.1.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gcash/bchd v0.14.7/go.mod h1:Gk/O1ktRVW5Kao0RsnVXp3bWxeYQadqawZ1Im9HE78M= +github.com/gcash/bchd v0.15.2/go.mod h1:k9wIjgwnhbrAw+ruIPZ2tHZMzfFNdyUnORZZX7lqXGY= +github.com/gcash/bchd v0.17.2-0.20201218180520-5708823e0e99 h1:Rda+nZnCUvjU8IubEB8f6ZN08gTcIEhHFjnMkg1pxkA= +github.com/gcash/bchd v0.17.2-0.20201218180520-5708823e0e99/go.mod h1:qwEZ/wr6LyUo5IBgAPcAbYHzXrjnr5gc4tj03n1TwKc= +github.com/gcash/bchlog v0.0.0-20180913005452-b4f036f92fa6 h1:3pZvWJ8MSfWstGrb8Hfh4ZpLyZNcXypcGx2Ju4ZibVM= +github.com/gcash/bchlog v0.0.0-20180913005452-b4f036f92fa6/go.mod h1:PpfmXTLfjRp7Tf6v/DCGTRXHz+VFbiRcsoUxi7HvwlQ= +github.com/gcash/bchutil v0.0.0-20190625002603-800e62fe9aff/go.mod h1:zXSP0Fg2L52wpSEDApQDQMiSygnQiK5HDquDl0a5BHg= +github.com/gcash/bchutil v0.0.0-20191012211144-98e73ec336ba/go.mod h1:nUIrcbbtEQdCsRwcp+j/CndDKMQE9Fi8p2F8cIZmIqI= +github.com/gcash/bchutil v0.0.0-20200506001747-c2894cd54b33/go.mod h1:wB++2ZcHUvGLN1OgO9swBmJK1vmyshJLW9SNS+apXwc= +github.com/gcash/bchutil v0.0.0-20210113190856-6ea28dff4000 h1:vVi7Ym3I9T4ZKhQy0/XLKzS3xAqX4K+/cSAmnvMR+HM= +github.com/gcash/bchutil v0.0.0-20210113190856-6ea28dff4000/go.mod h1:H2USFGwtiu6CNMxiVQPqZkDzsoVSt9BLNqTfBBqGXRo= github.com/go-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w= github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/improbable-eng/grpc-web v0.9.1/go.mod h1:6hRR09jOEG81ADP5wCQju1z71g6OL4eEvELdran/3cs= +github.com/improbable-eng/grpc-web v0.13.0/go.mod h1:6hRR09jOEG81ADP5wCQju1z71g6OL4eEvELdran/3cs= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v0.0.0-20181221193153-c0795c8afcf4/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.1-0.20200711081900-c17162fe8fd7 h1:Ug59miTxVKVg5Oi2S5uHlKOIV5jBx4Hb2u0jIxxDaSs= github.com/jessevdk/go-flags v1.4.1-0.20200711081900-c17162fe8fd7/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -125,36 +151,57 @@ github.com/jrick/wsrpc/v2 v2.3.4/go.mod h1:XPYs8BnRWl99lCvXRM5SLpZmTPqWpSOPkDIqY github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca h1:Ld/zXl5t4+D69SiV4JoN7kkfvJdOWlPpfxrzxpLMoUk= github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM= +github.com/zquestz/grab v0.0.0-20190224022517-abcee96e61b1/go.mod h1:bslhAiUxakrA6z6CHmVyvkfpnxx18RJBwVyx2TluJWw= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -168,12 +215,18 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201022231255-08b38378de70/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201024042810-be3efd7ff127 h1:pZPp9+iYUqwYKLjht0SDBbRCRK/9gAXDy7pz5fRDpjo= +golang.org/x/net v0.0.0-20201024042810-be3efd7ff127/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= @@ -183,20 +236,26 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5 h1:iCaAy5bMeEvwANu3YnJfWwI0kWAGkEa2RXPdweI/ysk= +golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -211,26 +270,41 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201022181438-0ff5f38871d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/run_tests.sh b/run_tests.sh index 77d8d5e77f..a068612f54 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -32,10 +32,12 @@ go test $dumptags live ./client/webserver go test $dumptags harness ./client/asset/dcr go test $dumptags harness ./client/asset/btc/livetest go test $dumptags harness ./client/asset/ltc +go test $dumptags harness ./client/asset/bch go test $dumptags harness ./client/core go test $dumptags dcrlive ./server/asset/dcr go test $dumptags btclive ./server/asset/btc go test $dumptags ltclive ./server/asset/ltc +go test $dumptags bchlive ./server/asset/bch go test $dumptags pgonline ./server/db/driver/pg # Return to initial directory. diff --git a/server/asset/bch/bch.go b/server/asset/bch/bch.go new file mode 100644 index 0000000000..9e2f774627 --- /dev/null +++ b/server/asset/bch/bch.go @@ -0,0 +1,124 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package bch + +import ( + "encoding/json" + "fmt" + "math" + + "decred.org/dcrdex/dex" + dexbch "decred.org/dcrdex/dex/networks/bch" + dexbtc "decred.org/dcrdex/dex/networks/btc" + "decred.org/dcrdex/server/asset" + "decred.org/dcrdex/server/asset/btc" + "github.com/btcsuite/btcd/chaincfg" +) + +// Driver implements asset.Driver. +type Driver struct{} + +// Setup creates the BCH backend. Start the backend with its Run method. +func (d *Driver) Setup(configPath string, logger dex.Logger, network dex.Network) (asset.Backend, error) { + return NewBackend(configPath, logger, network) +} + +// DecodeCoinID creates a human-readable representation of a coin ID for +// Bitcoin Cash. +func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { + // Bitcoin Cash and Bitcoin have the same tx hash and output format. + return (&btc.Driver{}).DecodeCoinID(coinID) +} + +func init() { + asset.Register(assetName, &Driver{}) +} + +const assetName = "bch" + +// NewBackend generates the network parameters and creates a bch backend as a +// btc clone using an asset/btc helper function. +func NewBackend(configPath string, logger dex.Logger, network dex.Network) (asset.Backend, error) { + var params *chaincfg.Params + switch network { + case dex.Mainnet: + params = dexbch.MainNetParams + case dex.Testnet: + params = dexbch.TestNet3Params + case dex.Regtest: + params = dexbch.RegressionNetParams + default: + return nil, fmt.Errorf("unknown network ID %v", network) + } + + // Designate the clone ports. These will be overwritten by any explicit + // settings in the configuration file. Bitcoin Cash uses the same default + // ports as Bitcoin. + ports := dexbtc.NetPorts{ + Mainnet: "8332", + Testnet: "18332", + Simnet: "18443", + } + + if configPath == "" { + configPath = dexbtc.SystemConfigPath("bitcoin") // Yes, Bitcoin Cash's default config path is the same as bitcoin. + } + + be, err := btc.NewBTCClone(&btc.BackendCloneConfig{ + Name: assetName, + Segwit: false, + ConfigPath: configPath, + AddressDecoder: dexbch.DecodeCashAddress, + Logger: logger, + Net: network, + ChainParams: params, + Ports: ports, + FeeEstimator: estimateFee, + }) + if err != nil { + return nil, err + } + + return &BCHBackend{ + Backend: be, + }, nil +} + +// BCHBackend embeds *btc.Backend and re-implements the Contract method to deal +// with Cash Address translation. +type BCHBackend struct { + *btc.Backend +} + +// Contract returns the output from embedded Backend's Contract method, but +// with the SwapAddress field converted to Cash Address encoding. +func (bch *BCHBackend) Contract(coinID []byte, redeemScript []byte) (*asset.Contract, error) { // Contract.SwapAddress + contract, err := bch.Backend.Contract(coinID, redeemScript) + if err != nil { + return nil, err + } + contract.SwapAddress, err = dexbch.RecodeCashAddress(contract.SwapAddress, bch.Net()) + if err != nil { + return nil, err + } + return contract, nil +} + +// estimateFee estimates the network transaction fee rate using the estimatefee +// RPC. +func estimateFee(node btc.BTCNode) (uint64, error) { + resp, err := node.RawRequest("estimatefee", nil) + if err != nil { + return 0, err + } + var feeRate float64 + err = json.Unmarshal(resp, &feeRate) + if err != nil { + return 0, err + } + if feeRate <= 0 { + return 0, fmt.Errorf("fee could not be estimated") + } + return uint64(math.Round(feeRate * 1e5)), nil +} diff --git a/server/asset/bch/bch_test.go b/server/asset/bch/bch_test.go new file mode 100644 index 0000000000..8f90cc07a6 --- /dev/null +++ b/server/asset/bch/bch_test.go @@ -0,0 +1,41 @@ +package bch + +import ( + "encoding/hex" + "testing" + + dexbch "decred.org/dcrdex/dex/networks/bch" + "decred.org/dcrdex/server/asset/btc" +) + +func TestCompatibility(t *testing.T) { + fromHex := func(str string) []byte { + b, err := hex.DecodeString(str) + if err != nil { + t.Fatalf("error decoding %s: %v", str, err) + } + return b + } + + // 2b381efec176b72da70e894a6dbba1fc1ba18a1d573af898e6f92915c0ca8209:1 + p2pkhAddr, err := dexbch.DecodeCashAddress("bitcoincash:qznf2drgsapgsejd95yp9nw0qzhw9mrcxsez7d78uv", dexbch.MainNetParams) + if err != nil { + t.Fatalf("error p2pkh decoding CashAddr address: %v", err) + } + + // b63e8090fe7140328d5d6ecdd6045b123e3f05742d9a749f2550fba7d0a6879f:1 + p2shAddr, err := dexbch.DecodeCashAddress("bitcoincash:pqugctqhj096cufywe32rktfu5dpmnnrjgsznuudl2", dexbch.MainNetParams) + if err != nil { + t.Fatalf("error decoding p2sh CashAddr address: %v", err) + } + + // These scripts and addresses are just copy-pasted from random + // getrawtransaction output. + items := &btc.CompatibilityItems{ + P2PKHScript: fromHex("76a914a6953468874288664d2d0812cdcf00aee2ec783488ac"), + PKHAddr: p2pkhAddr.String(), + P2SHScript: fromHex("a914388c2c1793cbac71247662a1d969e51a1dce639287"), + SHAddr: p2shAddr.String(), + } + btc.CompatibilityCheck(items, dexbch.MainNetParams, t) +} diff --git a/server/asset/bch/live_test.go b/server/asset/bch/live_test.go new file mode 100644 index 0000000000..9feb0e248b --- /dev/null +++ b/server/asset/bch/live_test.go @@ -0,0 +1,96 @@ +// +build bchlive +// +// go test -v -tags bchlive -run UTXOStats +// ----------------------------------- +// Grab the most recent block and iterate it's outputs, taking account of +// how many UTXOs are found, how many are of an unknown type, etc. +// +// go test -v -tags bchlive -run P2SHStats +// ----------------------------------------- +// For each output in the last block, check it's previous outpoint to see if +// it's a P2SH or P2WSH. If so, takes statistics on the script types, including +// for the redeem script. +// +// go test -v -tags bchlive -run LiveFees +// ------------------------------------------ +// Test that fees rates are parsed without error and that a few historical fee +// rates are correct + +package bch + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + + "decred.org/dcrdex/dex" + "decred.org/dcrdex/server/asset/btc" + "github.com/btcsuite/btcutil" +) + +var ( + bch *BCHBackend + ctx context.Context +) + +func TestMain(m *testing.M) { + // Wrap everything for defers. + doIt := func() int { + logger := dex.StdOutLogger("BCHTEST", dex.LevelTrace) + + // Since Bitcoin Cash's data dir is the same as Bitcoin, for dev, we'll + // just need to all agree on using ~/.bch instead of ~/.bitcoin. + homeDir := btcutil.AppDataDir("bch", false) + configPath := filepath.Join(homeDir, "bitcoin.conf") + + dexAsset, err := NewBackend(configPath, logger, dex.Mainnet) + if err != nil { + fmt.Printf("NewBackend error: %v\n", err) + return 1 + } + + var ok bool + bch, ok = dexAsset.(*BCHBackend) + if !ok { + fmt.Printf("Could not cast asset.Backend to *BCHBackend") + return 1 + } + + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(context.Background()) + + wg, err := dexAsset.Connect(ctx) + if err != nil { + cancel() + fmt.Printf("Connect failed: %v", err) + return 1 + } + defer func() { + cancel() + wg.Wait() + }() + + return m.Run() + } + + os.Exit(doIt()) +} + +func TestUTXOStats(t *testing.T) { + btc.LiveUTXOStats(bch.Backend, t) +} + +func TestP2SHStats(t *testing.T) { + btc.LiveP2SHStats(bch.Backend, t, 100) +} + +func TestLiveFees(t *testing.T) { + btc.LiveFeeRates(bch.Backend, t, map[string]uint64{ + "bcf7ae875b585e00a61055372c1e99046b20f5fbfcd8659959afb6f428326bfa": 1, + "056762c6df7ae2d80d6ebfcf7d9e3764dc4ca915fc983798ab4999fb3a30538f": 5, + "dc3962fc4d2d7d99646cacc16a23cc49143ea9cfc43128ec986b61e9132b2726": 444, + "0de586d0c74780605c36c0f51dcd850d1772f41a92c549e3aa36f9e78e905284": 2604, + }) +} diff --git a/server/asset/btc/btc.go b/server/asset/btc/btc.go index cefe8c9509..b08c1123a9 100644 --- a/server/asset/btc/btc.go +++ b/server/asset/btc/btc.go @@ -63,10 +63,10 @@ const ( immatureTransactionError = dex.ErrorKind("immature output") ) -// btcNode represents a blockchain information fetcher. In practice, it is +// BTCNode represents a blockchain information fetcher. In practice, it is // satisfied by rpcclient.Client, and all methods are matches for Client // methods. For testing, it can be satisfied by a stub. -type btcNode interface { +type BTCNode interface { EstimateSmartFee(confTarget int64, mode *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) GetTxOut(txHash *chainhash.Hash, index uint32, mempool bool) (*btcjson.GetTxOutResult, error) GetRawTransactionVerbose(txHash *chainhash.Hash) (*btcjson.TxRawResult, error) @@ -92,7 +92,7 @@ type Backend struct { client *rpcclient.Client // node is used throughout for RPC calls, and in typical use will be the same // as client. For testing, it can be set to a stub. - node btcNode + node BTCNode // The block cache stores just enough info about the blocks to shortcut future // calls to GetBlockVerbose. blockCache *blockCache @@ -103,7 +103,9 @@ type Backend struct { chainParams *chaincfg.Params // A logger will be provided by the dex for this backend. All logging should // use the provided logger. - log dex.Logger + log dex.Logger + decodeAddr dexbtc.AddressDecoder + estimateFee func(BTCNode) (uint64, error) } // Check that Backend satisfies the Backend interface. @@ -129,35 +131,70 @@ func NewBackend(configPath string, logger dex.Logger, network dex.Network) (asse configPath = dexbtc.SystemConfigPath("bitcoin") } - return NewBTCClone(assetName, true, configPath, logger, network, params, dexbtc.RPCPorts) + return NewBTCClone(&BackendCloneConfig{ + Name: assetName, + Segwit: true, + ConfigPath: configPath, + Logger: logger, + Net: network, + ChainParams: params, + Ports: dexbtc.RPCPorts, + }) } -func newBTC(name string, segwit bool, chainParams *chaincfg.Params, logger dex.Logger, cfg *dexbtc.Config) *Backend { +func newBTC(cloneCfg *BackendCloneConfig, cfg *dexbtc.Config) *Backend { + + feeEstimator := feeRate + if cloneCfg.FeeEstimator != nil { + feeEstimator = cloneCfg.FeeEstimator + } + + addrDecoder := btcutil.DecodeAddress + if cloneCfg.AddressDecoder != nil { + addrDecoder = cloneCfg.AddressDecoder + } + return &Backend{ cfg: cfg, - name: name, + name: cloneCfg.Name, blockCache: newBlockCache(), blockChans: make(map[chan *asset.BlockUpdate]struct{}), - chainParams: chainParams, - log: logger, - segwit: segwit, + chainParams: cloneCfg.ChainParams, + log: cloneCfg.Logger, + segwit: cloneCfg.Segwit, + decodeAddr: addrDecoder, + estimateFee: feeEstimator, } } +// BackendCloneConfig captures the arguments necessary to configure a BTC clone +// backend. +type BackendCloneConfig struct { + Name string + Segwit bool + ConfigPath string + AddressDecoder dexbtc.AddressDecoder + Logger dex.Logger + Net dex.Network + ChainParams *chaincfg.Params + Ports dexbtc.NetPorts + // FeeEstimator provides a way to get fees given an RawRequest-enabled + // client and a confirmation target. + FeeEstimator func(BTCNode) (uint64, error) +} + // NewBTCClone creates a BTC backend for a set of network parameters and default // network ports. A BTC clone can use this method, possibly in conjunction with // ReadCloneParams, to create a Backend for other assets with minimal coding. // See ReadCloneParams and CompatibilityCheck for more info. -func NewBTCClone(name string, segwit bool, configPath string, logger dex.Logger, network dex.Network, - params *chaincfg.Params, ports dexbtc.NetPorts) (*Backend, error) { - +func NewBTCClone(cloneCfg *BackendCloneConfig) (*Backend, error) { // Read the configuration parameters - cfg, err := dexbtc.LoadConfigFromPath(configPath, name, network, ports) + cfg, err := dexbtc.LoadConfigFromPath(cloneCfg.ConfigPath, cloneCfg.Name, cloneCfg.Net, cloneCfg.Ports) if err != nil { return nil, err } - return newBTC(name, segwit, params, logger, cfg), nil + return newBTC(cloneCfg, cfg), nil } func (btc *Backend) shutdown() { @@ -197,7 +234,7 @@ func (btc *Backend) Connect(ctx context.Context) (*sync.WaitGroup, error) { } } - if _, err = btc.FeeRate(); err != nil { + if _, err = btc.estimateFee(btc.node); err != nil { btc.log.Warnf("%s backend started without fee estimation available: %v", btc.name, err) } @@ -210,13 +247,19 @@ func (btc *Backend) Connect(ctx context.Context) (*sync.WaitGroup, error) { return &wg, nil } +// Net returns the *chaincfg.Params. This is not part of the asset.Backend +// interface, and is exported as a convenience for embedding types. +func (btc *Backend) Net() *chaincfg.Params { + return btc.chainParams +} + // Contract is part of the asset.Backend interface. An asset.Contract is an // output that has been validated as a swap contract for the passed redeem // script. A spendable output is one that can be spent in the next block. Every // output from a non-coinbase transaction is spendable immediately. Coinbase // outputs are only spendable after CoinbaseMaturity confirmations. Pubkey // scripts can be P2PKH or P2SH. Multi-sig P2SH redeem scripts are supported. -func (btc *Backend) Contract(coinID []byte, redeemScript []byte) (asset.Contract, error) { +func (btc *Backend) Contract(coinID []byte, redeemScript []byte) (*asset.Contract, error) { txHash, vout, err := decodeCoinID(coinID) if err != nil { return nil, fmt.Errorf("error decoding coin ID %x: %w", coinID, err) @@ -225,13 +268,8 @@ func (btc *Backend) Contract(coinID []byte, redeemScript []byte) (asset.Contract if err != nil { return nil, err } - contract := &Contract{Output: output} // Verify contract and set refundAddress and swapAddress. - err = btc.auditContract(contract) - if err != nil { - return nil, err - } - return contract, nil + return btc.auditContract(output) } // ValidateSecret checks that the secret satisfies the contract. @@ -356,30 +394,15 @@ func (btc *Backend) InitTxSizeBase() uint32 { // FeeRate returns the current optimal fee rate in sat / byte. func (btc *Backend) FeeRate() (uint64, error) { - feeResult, err := btc.node.EstimateSmartFee(1, &btcjson.EstimateModeConservative) - if err != nil { - return 0, err - } - if len(feeResult.Errors) > 0 { - return 0, fmt.Errorf(strings.Join(feeResult.Errors, "; ")) - } - if feeResult.FeeRate == nil { - return 0, fmt.Errorf("no fee rate available") - } - satPerKB, err := btcutil.NewAmount(*feeResult.FeeRate) - if err != nil { - return 0, err - } - satPerB := uint64(math.Round(float64(satPerKB) / 1000)) - if satPerB == 0 { - satPerB = 1 - } - return satPerB, nil + return btc.estimateFee(btc.node) } // CheckAddress checks that the given address is parseable. func (btc *Backend) CheckAddress(addr string) bool { - _, err := btcutil.DecodeAddress(addr, btc.chainParams) + _, err := btc.decodeAddr(addr, btc.chainParams) + if err != nil { + btc.log.Errorf("CheckAddress error for %s %s: %v", btc.name, addr, err) + } return err == nil } @@ -714,10 +737,19 @@ func (btc *Backend) transaction(txHash *chainhash.Hash, verboseTx *btcjson.TxRaw pkScript: pkScript, }) } + var feeRate uint64 - if verboseTx.Vsize > 0 { - feeRate = (sumIn - sumOut) / uint64(verboseTx.Vsize) + if btc.segwit { + if verboseTx.Vsize > 0 { + feeRate = (sumIn - sumOut) / uint64(verboseTx.Vsize) + } + } else if verboseTx.Size > 0 { + // For non-segwit transactions, Size = Vsize anyway, so use Size to + // cover assets that won't set Vsize in their RPC response. + feeRate = (sumIn - sumOut) / uint64(verboseTx.Size) + } + return newTransaction(btc, txHash, blockHash, lastLookup, blockHeight, isCoinbase, inputs, outputs, feeRate), nil } @@ -772,10 +804,10 @@ func (btc *Backend) getBtcBlock(blockHash *chainhash.Hash) (*cachedBlock, error) // auditContract checks that output is a swap contract and extracts the // receiving address and contract value on success. -func (btc *Backend) auditContract(contract *Contract) error { +func (btc *Backend) auditContract(contract *Output) (*asset.Contract, error) { tx := contract.tx if len(tx.outs) <= int(contract.vout) { - return fmt.Errorf("invalid index %d for transaction %s", contract.vout, tx.hash) + return nil, fmt.Errorf("invalid index %d for transaction %s", contract.vout, tx.hash) } output := tx.outs[int(contract.vout)] @@ -783,38 +815,40 @@ func (btc *Backend) auditContract(contract *Contract) error { // the hash of the user-supplied redeem script. scriptType := dexbtc.ParseScriptType(output.pkScript, contract.redeemScript) if scriptType == dexbtc.ScriptUnsupported { - return fmt.Errorf("specified output %s:%d is not P2SH", tx.hash, contract.vout) + return nil, fmt.Errorf("specified output %s:%d is not P2SH", tx.hash, contract.vout) } var scriptHash, hashed []byte if scriptType.IsP2SH() || scriptType.IsP2WSH() { scriptHash = dexbtc.ExtractScriptHash(output.pkScript) if scriptType.IsSegwit() { if !btc.segwit { - return fmt.Errorf("segwit contract, but %s is not configured for segwit", btc.name) + return nil, fmt.Errorf("segwit contract, but %s is not configured for segwit", btc.name) } shash := sha256.Sum256(contract.redeemScript) hashed = shash[:] } else { if btc.segwit { - return fmt.Errorf("non-segwit contract, but %s is configured for segwit", btc.name) + return nil, fmt.Errorf("non-segwit contract, but %s is configured for segwit", btc.name) } hashed = btcutil.Hash160(contract.redeemScript) } } if scriptHash == nil { - return fmt.Errorf("specified output %s:%d is not P2SH or P2WSH", tx.hash, contract.vout) + return nil, fmt.Errorf("specified output %s:%d is not P2SH or P2WSH", tx.hash, contract.vout) } if !bytes.Equal(hashed, scriptHash) { - return fmt.Errorf("swap contract hash mismatch for %s:%d", tx.hash, contract.vout) + return nil, fmt.Errorf("swap contract hash mismatch for %s:%d", tx.hash, contract.vout) } - refund, receiver, lockTime, _, err := dexbtc.ExtractSwapDetails(contract.redeemScript, contract.btc.segwit, contract.btc.chainParams) + _, receiver, lockTime, _, err := dexbtc.ExtractSwapDetails(contract.redeemScript, contract.btc.segwit, contract.btc.chainParams) if err != nil { - return fmt.Errorf("error parsing swap contract for %s:%d: %w", tx.hash, contract.vout, err) + return nil, fmt.Errorf("error parsing swap contract for %s:%d: %w", tx.hash, contract.vout, err) } - contract.refundAddress = refund.String() - contract.swapAddress = receiver.String() - contract.lockTime = time.Unix(int64(lockTime), 0) - return nil + return &asset.Contract{ + Coin: contract, + SwapAddress: receiver.String(), + RedeemScript: contract.redeemScript, + LockTime: time.Unix(int64(lockTime), 0), + }, nil } // run is responsible for best block polling and checking the application @@ -980,3 +1014,27 @@ func isTxNotFoundErr(err error) bool { var rpcErr *btcjson.RPCError return errors.As(err, &rpcErr) && rpcErr.Code == btcjson.ErrRPCInvalidAddressOrKey } + +// feeRate returns the current optimal fee rate in sat / byte using the +// estimatesmartfee RPC. +func feeRate(node BTCNode) (uint64, error) { + feeResult, err := node.EstimateSmartFee(1, &btcjson.EstimateModeConservative) + if err != nil { + return 0, err + } + if len(feeResult.Errors) > 0 { + return 0, fmt.Errorf(strings.Join(feeResult.Errors, "; ")) + } + if feeResult.FeeRate == nil { + return 0, fmt.Errorf("no fee rate available") + } + satPerKB, err := btcutil.NewAmount(*feeResult.FeeRate) + if err != nil { + return 0, err + } + satPerB := uint64(math.Round(float64(satPerKB) / 1000)) + if satPerB == 0 { + satPerB = 1 + } + return satPerB, nil +} diff --git a/server/asset/btc/btc_test.go b/server/asset/btc/btc_test.go index 4438f34ecb..ed59b29a64 100644 --- a/server/asset/btc/btc_test.go +++ b/server/asset/btc/btc_test.go @@ -741,7 +741,13 @@ func testBackend(segwit bool) (*Backend, func()) { logger := dex.StdOutLogger("TEST", dex.LevelTrace) // skip both loading config file and rpcclient.New in Connect. Manually // create the Backend and set the node to our node stub. - btc := newBTC("btc", segwit, testParams, logger, nil) + btc := newBTC(&BackendCloneConfig{ + Name: "btc", + Segwit: segwit, + AddressDecoder: btcutil.DecodeAddress, + Logger: logger, + ChainParams: testParams, + }, nil) btc.node = &testNode{} ctx, cancel := context.WithCancel(context.Background()) @@ -1050,18 +1056,13 @@ func TestUTXOs(t *testing.T) { t.Fatalf("case 11 - received error for utxo: %v", err) } - contract := &Contract{Output: utxo.Output} - // Now try again with the correct vout. - err = btc.auditContract(contract) // sets refund and swap addresses + contract, err := btc.auditContract(utxo.Output) // sets refund and swap addresses if err != nil { t.Fatalf("case 11 - unexpected error auditing contract: %v", err) } - if contract.SwapAddress() != swap.recipient.String() { - t.Fatalf("case 11 - wrong recipient. wanted '%s' got '%s'", contract.SwapAddress(), swap.recipient.String()) - } - if contract.RefundAddress() != swap.refund.String() { - t.Fatalf("case 11 - wrong recipient. wanted '%s' got '%s'", contract.RefundAddress(), swap.refund.String()) + if contract.SwapAddress != swap.recipient.String() { + t.Fatalf("case 11 - wrong recipient. wanted '%s' got '%s'", contract.SwapAddress, swap.recipient.String()) } if contract.Value() != 5 { t.Fatalf("case 11 - unexpected output value. wanted 5, got %d", contract.Value()) @@ -1373,9 +1374,9 @@ func TestAuxiliary(t *testing.T) { // TestCheckAddress checks that addresses are parsing or not parsing as // expected. func TestCheckAddress(t *testing.T) { - btc := &Backend{ - chainParams: &chaincfg.MainNetParams, - } + btc, shutdown := testBackend(false) + defer shutdown() + type test struct { addr string wantErr bool diff --git a/server/asset/btc/live_test.go b/server/asset/btc/live_test.go index de32624eb9..fb8aa474ad 100644 --- a/server/asset/btc/live_test.go +++ b/server/asset/btc/live_test.go @@ -99,7 +99,7 @@ func TestUTXOStats(t *testing.T) { // enabling use by bitcoin clone backends during compatibility testing. See // LiveP2SHStats in testing.go for an explanation of the test. func TestP2SHStats(t *testing.T) { - LiveP2SHStats(btc, t) + LiveP2SHStats(btc, t, 10000) } // TestBlockMonitor is a live test that connects to bitcoind and listens for diff --git a/server/asset/btc/testing.go b/server/asset/btc/testing.go index 50f6e98476..2bf11d4c48 100644 --- a/server/asset/btc/testing.go +++ b/server/asset/btc/testing.go @@ -19,8 +19,7 @@ import ( // examined to ensure the backend understands what they are and can extract // addresses. Ideally, the stats will show no scripts which were unparseable by // the backend, but the presence of unknowns is not an error. -func LiveP2SHStats(btc *Backend, t *testing.T) { - numToDo := 10000 +func LiveP2SHStats(btc *Backend, t *testing.T, numToDo int) { type scriptStats struct { unknown int p2pk int diff --git a/server/asset/btc/utxo.go b/server/asset/btc/utxo.go index dbc2f1f840..effffbc50d 100644 --- a/server/asset/btc/utxo.go +++ b/server/asset/btc/utxo.go @@ -8,7 +8,6 @@ import ( "context" "errors" "fmt" - "time" "decred.org/dcrdex/dex" dexbtc "decred.org/dcrdex/dex/networks/btc" @@ -177,16 +176,6 @@ type Output struct { spendSize uint32 } -// Contract is a transaction output containing a swap contract. -type Contract struct { - *Output - swapAddress string - refundAddress string - lockTime time.Time -} - -var _ asset.Contract = (*Contract)(nil) - // Confirmations returns the number of confirmations on this output's // transaction. func (output *Output) Confirmations(context.Context) (int64, error) { @@ -340,24 +329,3 @@ func pkMatches(pubkeys [][]byte, addrs []btcutil.Address, hasher func([]byte) [] } return matches } - -// RefundAddress is the refund address of this swap contract. -func (contract *Contract) RefundAddress() string { - return contract.refundAddress -} - -// SwapAddress is the receiving address of this swap contract. -func (contract *Contract) SwapAddress() string { - return contract.swapAddress -} - -// RedeemScript returns the Contract's redeem script. -func (contract *Contract) RedeemScript() []byte { - return contract.redeemScript -} - -// LockTime is a method on the asset.Contract interface for reading the locktime -// in the contract script. -func (contract *Contract) LockTime() time.Time { - return contract.lockTime -} diff --git a/server/asset/common.go b/server/asset/common.go index 9d32208da0..ee974d3506 100644 --- a/server/asset/common.go +++ b/server/asset/common.go @@ -25,7 +25,7 @@ type Backend interface { // Contract returns a Contract only for outputs that would be spendable on // the blockchain immediately. The redeem script is required in order to // calculate sigScript length and verify pubkeys. - Contract(coinID []byte, redeemScript []byte) (Contract, error) + Contract(coinID []byte, redeemScript []byte) (*Contract, error) // ValidateSecret checks that the secret satisfies the contract. ValidateSecret(secret, contract []byte) bool // Redemption returns a Coin for redemptionID, a transaction input, that @@ -100,14 +100,14 @@ type FundingCoin interface { } // Contract is an atomic swap contract. -type Contract interface { +type Contract struct { Coin // SwapAddress is the receiving address of the swap contract. - SwapAddress() string + SwapAddress string // RedeemScript is the contract redeem script. - RedeemScript() []byte + RedeemScript []byte // LockTime is the refund locktime. - LockTime() time.Time + LockTime time.Time } // BlockUpdate is sent over the update channel when a tip change is detected. diff --git a/server/asset/dcr/dcr.go b/server/asset/dcr/dcr.go index 46b16f6bba..8323e72b86 100644 --- a/server/asset/dcr/dcr.go +++ b/server/asset/dcr/dcr.go @@ -275,22 +275,18 @@ func (dcr *Backend) BlockChannel(size int) <-chan *asset.BlockUpdate { // confirmations. Pubkey scripts can be P2PKH or P2SH in either regular- or // stake-tree flavor. P2PKH supports two alternative signatures, Schnorr and // Edwards. Multi-sig P2SH redeem scripts are supported as well. -func (dcr *Backend) Contract(coinID []byte, redeemScript []byte) (asset.Contract, error) { +func (dcr *Backend) Contract(coinID []byte, redeemScript []byte) (*asset.Contract, error) { txHash, vout, err := decodeCoinID(coinID) if err != nil { return nil, fmt.Errorf("error decoding coin ID %x: %w", coinID, err) } - output, err := dcr.output(txHash, vout, redeemScript) - if err != nil { - return nil, err - } - contract := &Contract{Output: output} - // Verify contract and set refundAddress and swapAddress. - err = contract.auditContract() + + op, err := dcr.output(txHash, vout, redeemScript) if err != nil { return nil, err } - return contract, nil + + return auditContract(op) } // ValidateSecret checks that the secret satisfies the contract. @@ -382,6 +378,10 @@ func (dcr *Backend) ValidateContract(contract []byte) error { // CheckAddress checks that the given address is parseable. func (dcr *Backend) CheckAddress(addr string) bool { _, err := dcrutil.DecodeAddress(addr, chainParams) + if err != nil { + dcr.log.Errorf("DecodeAddress error for %s: %v", addr, err) + } + return err == nil } diff --git a/server/asset/dcr/dcr_test.go b/server/asset/dcr/dcr_test.go index 05eb80a455..8f34793232 100644 --- a/server/asset/dcr/dcr_test.go +++ b/server/asset/dcr/dcr_test.go @@ -1090,18 +1090,13 @@ func TestUTXOs(t *testing.T) { t.Fatalf("case 13 - received error for utxo: %v", err) } - contract := &Contract{Output: utxo.Output} - // Now try again with the correct vout. - err = contract.auditContract() // sets refund and swap addresses + contract, err := auditContract(utxo.Output) // sets refund and swap addresses if err != nil { t.Fatalf("case 13 - unexpected error auditing contract: %v", err) } - if contract.SwapAddress() != swap.recipient.String() { - t.Fatalf("case 13 - wrong recipient. wanted '%s' got '%s'", contract.SwapAddress(), swap.recipient.String()) - } - if contract.RefundAddress() != swap.refund.String() { - t.Fatalf("case 13 - wrong recipient. wanted '%s' got '%s'", contract.RefundAddress(), swap.refund.String()) + if contract.SwapAddress != swap.recipient.String() { + t.Fatalf("case 13 - wrong recipient. wanted '%s' got '%s'", contract.SwapAddress, swap.recipient.String()) } if contract.Value() != val { t.Fatalf("case 13 - unexpected output value. wanted 5, got %d", contract.Value()) @@ -1458,7 +1453,9 @@ func TestAuxiliary(t *testing.T) { // TestCheckAddress checks that addresses are parsing or not parsing as // expected. func TestCheckAddress(t *testing.T) { - dcr := &Backend{} + dcr, shutdown := testBackend() + defer shutdown() + type test struct { addr string wantErr bool diff --git a/server/asset/dcr/utxo.go b/server/asset/dcr/utxo.go index e911dd34f3..3ed892d8ae 100644 --- a/server/asset/dcr/utxo.go +++ b/server/asset/dcr/utxo.go @@ -190,16 +190,6 @@ type Output struct { spendSize uint32 } -// Contract is a transaction output containing a swap contract. -type Contract struct { - *Output - swapAddress string - refundAddress string - lockTime time.Time -} - -var _ asset.Contract = (*Contract)(nil) - // Confirmations returns the number of confirmations for a transaction output. // Because it is possible for an output that was once considered valid to later // be considered invalid, this method can return an error to indicate the output @@ -378,48 +368,29 @@ func pkMatches(pubkeys [][]byte, addrs []dcrutil.Address, hasher func([]byte) [] return matches, nil } -// AuditContract checks that the Contract is a swap contract and extracts the +// auditContract checks that the Contract is a swap contract and extracts the // receiving address and contract value on success. -func (contract *Contract) auditContract() error { - tx := contract.tx - if len(tx.outs) <= int(contract.vout) { - return fmt.Errorf("invalid index %d for transaction %s", contract.vout, tx.hash) +func auditContract(op *Output) (*asset.Contract, error) { + tx := op.tx + if len(tx.outs) <= int(op.vout) { + return nil, fmt.Errorf("invalid index %d for transaction %s", op.vout, tx.hash) } - output := tx.outs[int(contract.vout)] + output := tx.outs[int(op.vout)] scriptHash := dexdcr.ExtractScriptHash(output.pkScript) if scriptHash == nil { - return fmt.Errorf("specified output %s:%d is not P2SH", tx.hash, contract.vout) + return nil, fmt.Errorf("specified output %s:%d is not P2SH", tx.hash, op.vout) } - if !bytes.Equal(dcrutil.Hash160(contract.redeemScript), scriptHash) { - return fmt.Errorf("swap contract hash mismatch for %s:%d", tx.hash, contract.vout) + if !bytes.Equal(dcrutil.Hash160(op.redeemScript), scriptHash) { + return nil, fmt.Errorf("swap contract hash mismatch for %s:%d", tx.hash, op.vout) } - refund, receiver, lockTime, _, err := dexdcr.ExtractSwapDetails(contract.redeemScript, chainParams) + _, receiver, lockTime, _, err := dexdcr.ExtractSwapDetails(op.redeemScript, chainParams) if err != nil { - return fmt.Errorf("error parsing swap contract for %s:%d: %w", tx.hash, contract.vout, err) + return nil, fmt.Errorf("error parsing swap contract for %s:%d: %w", tx.hash, op.vout, err) } - contract.refundAddress = refund.String() - contract.swapAddress = receiver.String() - contract.lockTime = time.Unix(int64(lockTime), 0) - return nil -} - -// RefundAddress is the refund address of this swap contract. -func (contract *Contract) RefundAddress() string { - return contract.refundAddress -} - -// SwapAddress is the receiving address of this swap contract. -func (contract *Contract) SwapAddress() string { - return contract.swapAddress -} - -// RedeemScript returns the Contract's redeem script. -func (contract *Contract) RedeemScript() []byte { - return contract.redeemScript -} - -// LockTime is a method on the asset.Contract interface for reading the locktime -// in the contract script. -func (contract *Contract) LockTime() time.Time { - return contract.lockTime + return &asset.Contract{ + Coin: op, + SwapAddress: receiver.String(), + RedeemScript: op.redeemScript, + LockTime: time.Unix(int64(lockTime), 0), + }, nil } diff --git a/server/asset/ltc/live_test.go b/server/asset/ltc/live_test.go index 1a5be94c49..d06a2204a2 100644 --- a/server/asset/ltc/live_test.go +++ b/server/asset/ltc/live_test.go @@ -76,7 +76,7 @@ func TestUTXOStats(t *testing.T) { } func TestP2SHStats(t *testing.T) { - btc.LiveP2SHStats(ltc, t) + btc.LiveP2SHStats(ltc, t, 1000) } func TestLiveFees(t *testing.T) { diff --git a/server/asset/ltc/ltc.go b/server/asset/ltc/ltc.go index e3a24790e1..33cc81c0c7 100644 --- a/server/asset/ltc/ltc.go +++ b/server/asset/ltc/ltc.go @@ -62,5 +62,13 @@ func NewBackend(configPath string, logger dex.Logger, network dex.Network) (asse configPath = dexbtc.SystemConfigPath("litecoin") } - return btc.NewBTCClone(assetName, false, configPath, logger, network, params, ports) + return btc.NewBTCClone(&btc.BackendCloneConfig{ + Name: assetName, + Segwit: false, // TODO: Change to true. + ConfigPath: configPath, + Logger: logger, + Net: network, + ChainParams: params, + Ports: ports, + }) } diff --git a/server/cmd/dcrdex/main.go b/server/cmd/dcrdex/main.go index ebed6269a9..0f1c77f6bb 100644 --- a/server/cmd/dcrdex/main.go +++ b/server/cmd/dcrdex/main.go @@ -18,6 +18,7 @@ import ( "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/encode" "decred.org/dcrdex/server/admin" + _ "decred.org/dcrdex/server/asset/bch" _ "decred.org/dcrdex/server/asset/btc" // register btc asset _ "decred.org/dcrdex/server/asset/dcr" // register dcr asset _ "decred.org/dcrdex/server/asset/ltc" // register ltc asset diff --git a/server/cmd/dexcoin/main.go b/server/cmd/dexcoin/main.go index 0c80c335ba..7d81e79f75 100644 --- a/server/cmd/dexcoin/main.go +++ b/server/cmd/dexcoin/main.go @@ -10,6 +10,7 @@ import ( "os" "decred.org/dcrdex/server/asset" + _ "decred.org/dcrdex/server/asset/bch" _ "decred.org/dcrdex/server/asset/btc" _ "decred.org/dcrdex/server/asset/dcr" _ "decred.org/dcrdex/server/asset/ltc" diff --git a/server/market/routers_test.go b/server/market/routers_test.go index fed347a004..daf3b4d08c 100644 --- a/server/market/routers_test.go +++ b/server/market/routers_test.go @@ -345,8 +345,12 @@ func (b *TBackend) utxo(coinID []byte) (*tUTXO, error) { } return &tUTXO{val: v, decoded: str}, b.utxoErr } -func (b *TBackend) Contract(coinID, redeemScript []byte) (asset.Contract, error) { - return b.utxo(coinID) +func (b *TBackend) Contract(coinID, redeemScript []byte) (*asset.Contract, error) { + c, err := b.utxo(coinID) + if err != nil { + return nil, err + } + return &asset.Contract{Coin: c}, nil } func (b *TBackend) FundingCoin(ctx context.Context, coinID, redeemScript []byte) (asset.FundingCoin, error) { return b.utxo(coinID) diff --git a/server/swap/swap.go b/server/swap/swap.go index bf2fdb3e0f..89bc8c3573 100644 --- a/server/swap/swap.go +++ b/server/swap/swap.go @@ -81,7 +81,7 @@ type swapStatus struct { mtx sync.RWMutex // The time that the swap coordinator sees the transaction. swapTime time.Time - swap asset.Contract + swap *asset.Contract // The time that the transaction receives its SwapConf'th confirmation. swapConfirmed time.Time // The time that the swap coordinator sees the user's redemption @@ -1265,10 +1265,10 @@ func (s *Swapper) processInit(msg *msgjson.Message, params *msgjson.Init, stepIn s.respondError(msg.ID, actor.user, msgjson.ContractError, "low tx fee") return wait.DontTryAgain } - if contract.SwapAddress() != counterParty.order.Trade().SwapAddress() { + if contract.SwapAddress != counterParty.order.Trade().SwapAddress() { s.respondError(msg.ID, actor.user, msgjson.ContractError, fmt.Sprintf("incorrect recipient. expected %s. got %s", - contract.SwapAddress(), counterParty.order.Trade().SwapAddress())) + contract.SwapAddress, counterParty.order.Trade().SwapAddress())) return wait.DontTryAgain } if contract.Value() != stepInfo.checkVal { @@ -1281,9 +1281,9 @@ func (s *Swapper) processInit(msg *msgjson.Message, params *msgjson.Init, stepIn if stepInfo.actor.isMaker { reqLockTime = encode.DropMilliseconds(stepInfo.match.matchTime.Add(s.lockTimeMaker)) } - if contract.LockTime().Before(reqLockTime) { + if contract.LockTime.Before(reqLockTime) { s.respondError(msg.ID, actor.user, msgjson.ContractError, - fmt.Sprintf("contract error. expected lock time >= %s, got %s", reqLockTime, contract.LockTime())) + fmt.Sprintf("contract error. expected lock time >= %s, got %s", reqLockTime, contract.LockTime)) return wait.DontTryAgain } @@ -1407,7 +1407,7 @@ func (s *Swapper) processRedeem(msg *msgjson.Message, params *msgjson.Redeem, st // Make sure that the expected output is being spent. actor, counterParty := stepInfo.actor, stepInfo.counterParty counterParty.status.mtx.RLock() - cpContract := counterParty.status.swap.RedeemScript() + cpContract := counterParty.status.swap.RedeemScript cpSwapCoin := counterParty.status.swap.ID() cpSwapStr := counterParty.status.swap.String() counterParty.status.mtx.RUnlock() diff --git a/server/swap/swap_test.go b/server/swap/swap_test.go index 60b9c93441..2d8a873784 100644 --- a/server/swap/swap_test.go +++ b/server/swap/swap_test.go @@ -343,7 +343,7 @@ type redeemKey struct { // This stub satisfies asset.Backend. type TAsset struct { mtx sync.RWMutex - contracts map[string]asset.Contract + contracts map[string]*asset.Contract contractErr error funds asset.FundingCoin fundsErr error @@ -357,7 +357,7 @@ func newTAsset(lbl string) *TAsset { return &TAsset{ bChan: make(chan *asset.BlockUpdate, 5), lbl: lbl, - contracts: make(map[string]asset.Contract), + contracts: make(map[string]*asset.Contract), redemptions: make(map[redeemKey]asset.Coin), fundsErr: asset.CoinNotFoundError, } @@ -368,7 +368,7 @@ func (a *TAsset) FundingCoin(_ context.Context, coinID, redeemScript []byte) (as defer a.mtx.RUnlock() return a.funds, a.fundsErr } -func (a *TAsset) Contract(coinID, redeemScript []byte) (asset.Contract, error) { +func (a *TAsset) Contract(coinID, redeemScript []byte) (*asset.Contract, error) { a.mtx.RLock() defer a.mtx.RUnlock() if a.contractErr != nil { @@ -378,6 +378,7 @@ func (a *TAsset) Contract(coinID, redeemScript []byte) (asset.Contract, error) { if !found || contract == nil { return nil, asset.CoinNotFoundError } + return contract, nil } func (a *TAsset) Redemption(redemptionID, cpSwapCoinID []byte) (asset.Coin, error) { @@ -413,7 +414,7 @@ func (a *TAsset) setContractErr(err error) { defer a.mtx.Unlock() a.contractErr = err } -func (a *TAsset) setContract(contract asset.Contract, resetErr bool) { +func (a *TAsset) setContract(contract *asset.Contract, resetErr bool) { a.mtx.Lock() a.contracts[string(contract.ID())] = contract if resetErr { @@ -444,7 +445,6 @@ type TCoin struct { confsErr error auditAddr string auditVal uint64 - lockTime time.Time } func (coin *TCoin) Confirmations(context.Context) (int64, error) { @@ -453,18 +453,10 @@ func (coin *TCoin) Confirmations(context.Context) (int64, error) { return coin.confs, coin.confsErr } -func (coin *TCoin) SwapAddress() string { - return coin.auditAddr -} - func (coin *TCoin) Addresses() []string { return []string{coin.auditAddr} } -func (coin *TCoin) LockTime() time.Time { - return coin.lockTime -} - func (coin *TCoin) setConfs(confs int64) { coin.mtx.Lock() defer coin.mtx.Unlock() @@ -482,8 +474,6 @@ func (coin *TCoin) FeeRate() uint64 { return 72 // make sure it's at least the required fee if you want it to pass (test fail TODO) } -func (coin *TCoin) RedeemScript() []byte { return nil } - func TNewAsset(backend asset.Backend) *asset.BackedAsset { return &asset.BackedAsset{ Backend: backend, @@ -1200,7 +1190,7 @@ func tMultiMatchSet(matchQtys, rates []uint64, makerSell bool, isMarket bool) *t // tSwap is the information needed for spoofing a swap transaction. type tSwap struct { - coin *TCoin + coin *asset.Contract req *msgjson.Message contract string } @@ -1215,36 +1205,41 @@ func tNewSwap(matchInfo *tMatch, oid order.OrderID, recipient string, user *tUse auditVal = matcher.BaseToQuote(matchInfo.rate, matchInfo.qty) } coinID := randBytes(36) - swap := &TCoin{ + coin := &TCoin{ confs: 0, auditAddr: recipient + tRecipientSpoofer, auditVal: auditVal * tValSpoofer, id: coinID, } - swap.lockTime = encode.DropMilliseconds(matchInfo.match.Epoch.End().Add(dex.LockTimeTaker(dex.Testnet))) + contract := &asset.Contract{ + Coin: coin, + SwapAddress: recipient + tRecipientSpoofer, + } + + contract.LockTime = encode.DropMilliseconds(matchInfo.match.Epoch.End().Add(dex.LockTimeTaker(dex.Testnet))) if user == matchInfo.maker { - swap.lockTime = encode.DropMilliseconds(matchInfo.match.Epoch.End().Add(dex.LockTimeMaker(dex.Testnet))) + contract.LockTime = encode.DropMilliseconds(matchInfo.match.Epoch.End().Add(dex.LockTimeMaker(dex.Testnet))) } if !tLockTimeSpoofer.IsZero() { - swap.lockTime = tLockTimeSpoofer + contract.LockTime = tLockTimeSpoofer } - contract := "01234567" + user.sigHex + script := "01234567" + user.sigHex req, _ := msgjson.NewRequest(nextID(), msgjson.InitRoute, &msgjson.Init{ OrderID: oid[:], MatchID: matchInfo.matchID[:], // We control what the backend returns, so the txid doesn't matter right now. CoinID: coinID, //Time: encode.UnixMilliU(unixMsNow()), - Contract: dirtyEncode(contract), + Contract: dirtyEncode(script), }) return &tSwap{ - coin: swap, + coin: contract, req: req, - contract: contract, + contract: script, } } @@ -1264,7 +1259,7 @@ func randBytes(len int) []byte { type tRedeem struct { req *msgjson.Message coin *TCoin - cpSwapCoin *TCoin + cpSwapCoin *asset.Contract } func tNewRedeem(matchInfo *tMatch, oid order.OrderID, user *tUser) *tRedeem { @@ -1275,7 +1270,7 @@ func tNewRedeem(matchInfo *tMatch, oid order.OrderID, user *tUser) *tRedeem { CoinID: coinID, //Time: encode.UnixMilliU(unixMsNow()), }) - var cpSwapCoin *TCoin + var cpSwapCoin *asset.Contract switch user.acct { case matchInfo.maker.acct: cpSwapCoin = matchInfo.db.takerSwap.coin @@ -1372,12 +1367,12 @@ func testSwap(t *testing.T, rig *testRig) { ensureNilErr(rig.sendSwap_maker(true)) ensureNilErr(rig.auditSwap_taker()) ensureNilErr(rig.ackAudit_taker(true)) - matchInfo.db.makerSwap.coin.setConfs(int64(makerSwapAsset.SwapConf)) + matchInfo.db.makerSwap.coin.Coin.(*TCoin).setConfs(int64(makerSwapAsset.SwapConf)) sendBlock(makerSwapAsset.Backend.(*TAsset)) ensureNilErr(rig.sendSwap_taker(true)) ensureNilErr(rig.auditSwap_maker()) ensureNilErr(rig.ackAudit_maker(true)) - matchInfo.db.takerSwap.coin.setConfs(int64(takerSwapAsset.SwapConf)) + matchInfo.db.takerSwap.coin.Coin.(*TCoin).setConfs(int64(takerSwapAsset.SwapConf)) sendBlock(takerSwapAsset.Backend.(*TAsset)) ensureNilErr(rig.redeem_maker(true)) ensureNilErr(rig.ackRedemption_taker(true)) @@ -1429,6 +1424,7 @@ func TestSwaps(t *testing.T) { rates := []uint64{uint64(10e8), uint64(11e8), uint64(12e8)} // one taker, 3 makers => 4 'match' requests rig.matches = tMultiMatchSet(matchQtys, rates, makerSell, isMarket) + rig.swapper.Negotiate([]*order.MatchSet{rig.matches.matchSet}) testSwap(t, rig) }) @@ -1492,7 +1488,7 @@ func TestTxWaiters(t *testing.T) { if err := rig.sendSwap_maker(true); err != nil { t.Fatal(err) } - matchInfo.db.makerSwap.coin.setConfs(int64(rig.abc.SwapConf)) + matchInfo.db.makerSwap.coin.Coin.(*TCoin).setConfs(int64(rig.abc.SwapConf)) sendBlock(rig.abc.Backend.(*TAsset)) if err := rig.auditSwap_taker(); err != nil { t.Fatal(err) @@ -1528,7 +1524,7 @@ func TestTxWaiters(t *testing.T) { t.Fatalf("unexpected rpc error for ok taker swap. code: %d, msg: %s", resp.Error.Code, resp.Error.Message) } - matchInfo.db.takerSwap.coin.setConfs(int64(rig.xyz.SwapConf)) + matchInfo.db.takerSwap.coin.Coin.(*TCoin).setConfs(int64(rig.xyz.SwapConf)) sendBlock(rig.xyz.Backend.(*TAsset)) ensureNilErr(rig.auditSwap_maker()) @@ -1681,7 +1677,7 @@ func TestBroadcastTimeouts(t *testing.T) { ensureNilErr(rig.ackAudit_taker(true)) // Maker's swap reaches swapConf. - matchInfo.db.makerSwap.coin.setConfs(int64(rig.abc.SwapConf)) + matchInfo.db.makerSwap.coin.Coin.(*TCoin).setConfs(int64(rig.abc.SwapConf)) sendBlock(rig.abcNode) // tryConfirmSwap // With maker swap confirmed, inaction happens bTimeout after // swapConfirmed time. @@ -1700,7 +1696,7 @@ func TestBroadcastTimeouts(t *testing.T) { ensureNilErr(rig.ackAudit_maker(true)) // Taker's swap reaches swapConf. - matchInfo.db.takerSwap.coin.setConfs(int64(rig.xyz.SwapConf)) + matchInfo.db.takerSwap.coin.Coin.(*TCoin).setConfs(int64(rig.xyz.SwapConf)) sendBlock(rig.xyzNode) // With taker swap confirmed, inaction happens bTimeout after // swapConfirmed time.