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 }