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 }