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

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package zcl
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"math"
    10  	"strconv"
    11  
    12  	"decred.org/dcrdex/client/asset"
    13  	"decred.org/dcrdex/client/asset/btc"
    14  	"decred.org/dcrdex/dex"
    15  	dexbtc "decred.org/dcrdex/dex/networks/btc"
    16  	dexzcl "decred.org/dcrdex/dex/networks/zcl"
    17  	dexzec "decred.org/dcrdex/dex/networks/zec"
    18  	"github.com/btcsuite/btcd/btcec/v2"
    19  	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
    20  	"github.com/btcsuite/btcd/btcutil"
    21  	"github.com/btcsuite/btcd/chaincfg"
    22  	"github.com/btcsuite/btcd/chaincfg/chainhash"
    23  	"github.com/btcsuite/btcd/txscript"
    24  	"github.com/btcsuite/btcd/wire"
    25  )
    26  
    27  const (
    28  	version = 0
    29  	BipID   = 147
    30  	// The default fee is passed to the user as part of the asset.WalletInfo
    31  	// structure.
    32  	defaultFee          = 10
    33  	defaultFeeRateLimit = 1000
    34  	minNetworkVersion   = 2010159 // v2.1.1-9-f7bff4ff3-dirty
    35  	walletTypeRPC       = "zclassicdRPC"
    36  
    37  	transparentAddressType = "p2pkh"
    38  	orchardAddressType     = "orchard"
    39  	saplingAddressType     = "sapling"
    40  )
    41  
    42  var (
    43  	configOpts = []*asset.ConfigOption{
    44  		{
    45  			Key:         "rpcuser",
    46  			DisplayName: "JSON-RPC Username",
    47  			Description: "Zclassic's 'rpcuser' setting",
    48  		},
    49  		{
    50  			Key:         "rpcpassword",
    51  			DisplayName: "JSON-RPC Password",
    52  			Description: "Zclassic's 'rpcpassword' setting",
    53  			NoEcho:      true,
    54  		},
    55  		{
    56  			Key:         "rpcbind",
    57  			DisplayName: "JSON-RPC Address",
    58  			Description: "<addr> or <addr>:<port> (default 'localhost')",
    59  		},
    60  		{
    61  			Key:         "rpcport",
    62  			DisplayName: "JSON-RPC Port",
    63  			Description: "Port for RPC connections (if not set in Address)",
    64  		},
    65  		{
    66  			Key:          "fallbackfee",
    67  			DisplayName:  "Fallback fee rate",
    68  			Description:  "Zclassic's 'fallbackfee' rate. Units: ZEC/kB",
    69  			DefaultValue: strconv.FormatFloat(defaultFee*1000/1e8, 'f', -1, 64),
    70  		},
    71  		{
    72  			Key:         "feeratelimit",
    73  			DisplayName: "Highest acceptable fee rate",
    74  			Description: "This is the highest network fee rate you are willing to " +
    75  				"pay on swap transactions. If feeratelimit is lower than a market's " +
    76  				"maxfeerate, you will not be able to trade on that market with this " +
    77  				"wallet.  Units: BTC/kB",
    78  			DefaultValue: strconv.FormatFloat(defaultFeeRateLimit*1000/1e8, 'f', -1, 64),
    79  		},
    80  		{
    81  			Key:         "txsplit",
    82  			DisplayName: "Pre-split funding inputs",
    83  			Description: "When placing an order, create a \"split\" transaction to fund the order without locking more of the wallet balance than " +
    84  				"necessary. Otherwise, excess funds may be reserved to fund the order until the first swap contract is broadcast " +
    85  				"during match settlement, or the order is canceled. This an extra transaction for which network mining fees are paid. " +
    86  				"Used only for standing-type orders, e.g. limit orders without immediate time-in-force.",
    87  			IsBoolean: true,
    88  		},
    89  	}
    90  	// WalletInfo defines some general information about a Zcash wallet.
    91  	WalletInfo = &asset.WalletInfo{
    92  		Name:              "Zclassic",
    93  		SupportedVersions: []uint32{version},
    94  		UnitInfo:          dexzcl.UnitInfo,
    95  		AvailableWallets: []*asset.WalletDefinition{{
    96  			Type:              walletTypeRPC,
    97  			Tab:               "External",
    98  			Description:       "Connect to zclassicd",
    99  			DefaultConfigPath: dexbtc.SystemConfigPath("zclassic"),
   100  			ConfigOpts:        configOpts,
   101  			NoAuth:            true,
   102  		}},
   103  	}
   104  )
   105  
   106  func init() {
   107  	asset.Register(BipID, &Driver{})
   108  }
   109  
   110  // Driver implements asset.Driver.
   111  type Driver struct{}
   112  
   113  // Open creates the ZEC exchange wallet. Start the wallet with its Run method.
   114  func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) {
   115  	return NewWallet(cfg, logger, network)
   116  }
   117  
   118  // DecodeCoinID creates a human-readable representation of a coin ID for
   119  // Zcash.
   120  func (d *Driver) DecodeCoinID(coinID []byte) (string, error) {
   121  	// Zcash shielded transactions don't have transparent outputs, so the coinID
   122  	// will just be the tx hash.
   123  	if len(coinID) == chainhash.HashSize {
   124  		var txHash chainhash.Hash
   125  		copy(txHash[:], coinID)
   126  		return txHash.String(), nil
   127  	}
   128  	// For transparent transactions, Zcash and Bitcoin have the same tx hash
   129  	// and output format.
   130  	return (&btc.Driver{}).DecodeCoinID(coinID)
   131  }
   132  
   133  // Info returns basic information about the wallet and asset.
   134  func (d *Driver) Info() *asset.WalletInfo {
   135  	return WalletInfo
   136  }
   137  
   138  // MinLotSize calculates the minimum bond size for a given fee rate that avoids
   139  // dust outputs on the swap and refund txs, assuming the maxFeeRate doesn't
   140  // change.
   141  func (d *Driver) MinLotSize(maxFeeRate uint64) uint64 {
   142  	return dexbtc.MinLotSize(maxFeeRate, false)
   143  }
   144  
   145  // NewWallet is the exported constructor by which the DEX will import the
   146  // exchange wallet. The wallet will shut down when the provided context is
   147  // canceled. The configPath can be an empty string, in which case the standard
   148  // system location of the zcashd config file is assumed.
   149  func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, net dex.Network) (asset.Wallet, error) {
   150  	var btcParams *chaincfg.Params
   151  	var addrParams *dexzec.AddressParams
   152  	switch net {
   153  	case dex.Mainnet:
   154  		btcParams = dexzcl.MainNetParams
   155  		addrParams = dexzec.MainNetAddressParams
   156  	case dex.Testnet:
   157  		btcParams = dexzcl.TestNet4Params
   158  		addrParams = dexzec.TestNet4AddressParams
   159  	case dex.Regtest:
   160  		btcParams = dexzcl.RegressionNetParams
   161  		addrParams = dexzec.RegressionNetAddressParams
   162  	default:
   163  		return nil, fmt.Errorf("unknown network ID %v", net)
   164  	}
   165  
   166  	// Designate the clone ports. These will be overwritten by any explicit
   167  	// settings in the configuration file.
   168  	ports := dexbtc.NetPorts{
   169  		Mainnet: "8023",
   170  		Testnet: "18023",
   171  		Simnet:  "35768", // zclassic uses 18023 for regtest too. Using our alpha harness port instead.
   172  	}
   173  
   174  	var w *btc.ExchangeWalletNoAuth
   175  	cloneCFG := &btc.BTCCloneCFG{
   176  		WalletCFG:           cfg,
   177  		MinNetworkVersion:   minNetworkVersion,
   178  		WalletInfo:          WalletInfo,
   179  		Symbol:              "zcl",
   180  		Logger:              logger,
   181  		Network:             net,
   182  		ChainParams:         btcParams,
   183  		Ports:               ports,
   184  		DefaultFallbackFee:  defaultFee,
   185  		DefaultFeeRateLimit: defaultFeeRateLimit,
   186  		LegacyRawFeeLimit:   true,
   187  		BalanceFunc: func(ctx context.Context, locked uint64) (*asset.Balance, error) {
   188  			var bal float64
   189  			// args: "(dummy)" minconf includeWatchonly
   190  			if err := w.CallRPC("getbalance", []interface{}{"", 0, false}, &bal); err != nil {
   191  				return nil, err
   192  			}
   193  			return &asset.Balance{
   194  				Available: toSatoshi(bal) - locked,
   195  				Locked:    locked,
   196  				Other:     make(map[asset.BalanceCategory]asset.CustomBalance),
   197  			}, nil
   198  		},
   199  		Segwit: false,
   200  		// InitTxSize from zec still looks right to me for Zclassic.
   201  		InitTxSize:               dexzec.InitTxSize,
   202  		InitTxSizeBase:           dexzec.InitTxSizeBase,
   203  		OmitAddressType:          true,
   204  		LegacySignTxRPC:          true,
   205  		NumericGetRawRPC:         true,
   206  		LegacyValidateAddressRPC: true,
   207  		SingularWallet:           true,
   208  		UnlockSpends:             true,
   209  		FeeEstimator: func(_ context.Context, _ btc.RawRequester, nBlocks uint64) (uint64, error) {
   210  			var r float64
   211  			if err := w.CallRPC("estimatefee", []interface{}{nBlocks}, &r); err != nil {
   212  				return 0, fmt.Errorf("error calling 'estimatefee': %v", err)
   213  			}
   214  			if r < 0 {
   215  				return 0, nil
   216  			}
   217  			return toSatoshi(r), nil
   218  		},
   219  		AddressDecoder: func(addr string, net *chaincfg.Params) (btcutil.Address, error) {
   220  			return dexzec.DecodeAddress(addr, addrParams, btcParams)
   221  		},
   222  		AddressStringer: func(addr btcutil.Address, btcParams *chaincfg.Params) (string, error) {
   223  			return dexzec.EncodeAddress(addr, addrParams)
   224  		},
   225  		TxSizeCalculator: dexzec.CalcTxSize,
   226  		NonSegwitSigner:  signTx,
   227  		TxDeserializer: func(b []byte) (*wire.MsgTx, error) {
   228  			zecTx, err := dexzec.DeserializeTx(b)
   229  			if err != nil {
   230  				return nil, err
   231  			}
   232  			return zecTx.MsgTx, nil
   233  		},
   234  		BlockDeserializer: func(b []byte) (*wire.MsgBlock, error) {
   235  			zecBlock, err := dexzec.DeserializeBlock(b)
   236  			if err != nil {
   237  				return nil, err
   238  			}
   239  			return &zecBlock.MsgBlock, nil
   240  		},
   241  		TxSerializer: func(btcTx *wire.MsgTx) ([]byte, error) {
   242  			return zecTx(btcTx).Bytes()
   243  		},
   244  		TxHasher: func(tx *wire.MsgTx) *chainhash.Hash {
   245  			h := zecTx(tx).TxHash()
   246  			return &h
   247  		},
   248  		TxVersion: func() int32 {
   249  			return dexzec.VersionSapling
   250  		},
   251  		// https://github.com/zcash/zcash/pull/6005
   252  		ManualMedianTime:  true,
   253  		OmitRPCOptionsArg: true,
   254  		AssetID:           BipID,
   255  	}
   256  	w, err := btc.BTCCloneWalletNoAuth(cloneCFG)
   257  	return w, err
   258  }
   259  
   260  // TODO: Implement ShieldedWallet
   261  // type zecWallet struct {
   262  // 	*btc.ExchangeWalletNoAuth
   263  // 	log         dex.Logger
   264  // 	lastAddress atomic.Value // "string"
   265  // }
   266  
   267  // var _ asset.ShieldedWallet = (*zecWallet)(nil)
   268  
   269  func zecTx(tx *wire.MsgTx) *dexzec.Tx {
   270  	return dexzec.NewTxFromMsgTx(tx, dexzec.MaxExpiryHeight)
   271  }
   272  
   273  // signTx signs the transaction input with Zcash's BLAKE-2B sighash digest.
   274  // Won't work with shielded or blended transactions.
   275  func signTx(
   276  	btcTx *wire.MsgTx, idx int, pkScript []byte, hashType txscript.SigHashType,
   277  	key *btcec.PrivateKey, amts []int64, prevScripts [][]byte,
   278  ) ([]byte, error) {
   279  
   280  	tx := zecTx(btcTx)
   281  	sigHash, err := tx.SignatureDigest(idx, hashType, pkScript, amts, prevScripts)
   282  	if err != nil {
   283  		return nil, fmt.Errorf("sighash calculation error: %v", err)
   284  	}
   285  
   286  	return append(ecdsa.Sign(key, sigHash[:]).Serialize(), byte(hashType)), nil
   287  }
   288  
   289  func toSatoshi(v float64) uint64 {
   290  	const conventionalConversionFactor = 1e8
   291  	return uint64(math.Round(v * conventionalConversionFactor))
   292  }