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