decred.org/dcrdex@v1.0.3/client/asset/ltc/ltc.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 ltc 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "math" 11 "path/filepath" 12 "time" 13 14 "decred.org/dcrdex/client/asset" 15 "decred.org/dcrdex/client/asset/btc" 16 "decred.org/dcrdex/dex" 17 "decred.org/dcrdex/dex/config" 18 "decred.org/dcrdex/dex/dexnet" 19 dexbtc "decred.org/dcrdex/dex/networks/btc" 20 dexltc "decred.org/dcrdex/dex/networks/ltc" 21 "github.com/btcsuite/btcd/chaincfg" 22 "github.com/dcrlabs/ltcwallet/wallet" 23 ltcchaincfg "github.com/ltcsuite/ltcd/chaincfg" 24 ) 25 26 const ( 27 version = 1 28 // BipID is the BIP-0044 asset ID. 29 BipID = 2 30 // defaultFee is the default value for the fallbackfee. 31 defaultFee = 10 32 // defaultFeeRateLimit is the default value for the feeratelimit. 33 defaultFeeRateLimit = 100 34 minNetworkVersion = 210201 35 walletTypeRPC = "litecoindRPC" 36 walletTypeSPV = "SPV" 37 walletTypeLegacy = "" 38 walletTypeElectrum = "electrumRPC" 39 needElectrumVersion = "4.2.2" 40 ) 41 42 var ( 43 NetPorts = dexbtc.NetPorts{ 44 Mainnet: "9332", 45 Testnet: "19332", 46 Simnet: "19443", 47 } 48 rpcWalletDefinition = &asset.WalletDefinition{ 49 Type: walletTypeRPC, 50 Tab: "Litecoin Core (external)", 51 Description: "Connect to litecoind", 52 DefaultConfigPath: dexbtc.SystemConfigPath("litecoin"), 53 ConfigOpts: append(btc.RPCConfigOpts("Litecoin", "9332"), btc.CommonConfigOpts("LTC", true)...), 54 MultiFundingOpts: btc.MultiFundingOpts, 55 } 56 electrumWalletDefinition = &asset.WalletDefinition{ 57 Type: walletTypeElectrum, 58 Tab: "Electrum-LTC (external)", 59 Description: "Use an external Electrum-LTC Wallet", 60 // json: DefaultConfigPath: filepath.Join(btcutil.AppDataDir("electrum-ltc", false), "config"), // e.g. ~/.electrum-ltc/config ConfigOpts: append(rpcOpts, commonOpts...), 61 ConfigOpts: append(btc.ElectrumConfigOpts, btc.CommonConfigOpts("LTC", true)...), 62 MultiFundingOpts: btc.MultiFundingOpts, 63 } 64 spvWalletDefinition = &asset.WalletDefinition{ 65 Type: walletTypeSPV, 66 Tab: "Native", 67 Description: "Use the built-in SPV wallet", 68 ConfigOpts: btc.CommonConfigOpts("LTC", true), 69 Seeded: true, 70 MultiFundingOpts: btc.MultiFundingOpts, 71 } 72 // WalletInfo defines some general information about a Litecoin wallet. 73 WalletInfo = &asset.WalletInfo{ 74 Name: "Litecoin", 75 SupportedVersions: []uint32{version}, 76 UnitInfo: dexltc.UnitInfo, 77 AvailableWallets: []*asset.WalletDefinition{ 78 spvWalletDefinition, 79 rpcWalletDefinition, 80 electrumWalletDefinition, 81 }, 82 } 83 ) 84 85 func init() { 86 asset.Register(BipID, &Driver{}) 87 } 88 89 // Driver implements asset.Driver. 90 type Driver struct{} 91 92 // Check that Driver implements asset.Driver. 93 var _ asset.Driver = (*Driver)(nil) 94 95 // Open creates the LTC exchange wallet. Start the wallet with its Run method. 96 func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) { 97 return NewWallet(cfg, logger, network) 98 } 99 100 // DecodeCoinID creates a human-readable representation of a coin ID for 101 // Litecoin. 102 func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { 103 // Litecoin and Bitcoin have the same tx hash and output format. 104 return (&btc.Driver{}).DecodeCoinID(coinID) 105 } 106 107 // Info returns basic information about the wallet and asset. 108 func (d *Driver) Info() *asset.WalletInfo { 109 return WalletInfo 110 } 111 112 // MinLotSize calculates the minimum bond size for a given fee rate that avoids 113 // dust outputs on the swap and refund txs, assuming the maxFeeRate doesn't 114 // change. 115 func (d *Driver) MinLotSize(maxFeeRate uint64) uint64 { 116 return dexbtc.MinLotSize(maxFeeRate, true) 117 } 118 119 // Exists checks the existence of the wallet. Part of the Creator interface, so 120 // only used for wallets with WalletDefinition.Seeded = true. 121 func (d *Driver) Exists(walletType, dataDir string, settings map[string]string, net dex.Network) (bool, error) { 122 if walletType != walletTypeSPV { 123 return false, fmt.Errorf("no Bitcoin wallet of type %q available", walletType) 124 } 125 126 chainParams, err := parseChainParams(net) 127 if err != nil { 128 return false, err 129 } 130 walletDir := filepath.Join(dataDir, chainParams.Name) 131 loader := wallet.NewLoader(chainParams, walletDir, true, dbTimeout, 250) 132 return loader.WalletExists() 133 } 134 135 // Create creates a new SPV wallet. 136 func (d *Driver) Create(params *asset.CreateWalletParams) error { 137 if params.Type != walletTypeSPV { 138 return fmt.Errorf("SPV is the only seeded wallet type. required = %q, requested = %q", walletTypeSPV, params.Type) 139 } 140 if len(params.Seed) == 0 { 141 return errors.New("wallet seed cannot be empty") 142 } 143 if len(params.DataDir) == 0 { 144 return errors.New("must specify wallet data directory") 145 } 146 chainParams, err := parseChainParams(params.Net) 147 if err != nil { 148 return fmt.Errorf("error parsing chain: %w", err) 149 } 150 151 walletCfg := new(btc.WalletConfig) 152 err = config.Unmapify(params.Settings, walletCfg) 153 if err != nil { 154 return err 155 } 156 157 recoveryCfg := new(btc.RecoveryCfg) 158 err = config.Unmapify(params.Settings, recoveryCfg) 159 if err != nil { 160 return err 161 } 162 163 bday := btc.DefaultWalletBirthday 164 if params.Birthday != 0 { 165 bday = time.Unix(int64(params.Birthday), 0) 166 } 167 168 walletDir := filepath.Join(params.DataDir, chainParams.Name) 169 return createSPVWallet(params.Pass, params.Seed, bday, walletDir, 170 params.Logger, recoveryCfg.NumExternalAddresses, recoveryCfg.NumInternalAddresses, chainParams) 171 } 172 173 // customSPVWalletConstructors are functions for setting up custom 174 // implementations of the btc.BTCWallet interface that may be used by the 175 // ExchangeWalletSPV instead of the default spv implementation. 176 var customSPVWalletConstructors = map[string]btc.CustomSPVWalletConstructor{} 177 178 // RegisterCustomSPVWallet registers a function that should be used in creating 179 // a btc.BTCWallet implementation that the ExchangeWalletSPV will use in place 180 // of the default spv wallet implementation. External consumers can use this 181 // function to provide alternative btc.BTCWallet implementations, and must do so 182 // before attempting to create an ExchangeWalletSPV instance of this type. It'll 183 // panic if callers try to register a wallet twice. 184 func RegisterCustomSPVWallet(constructor btc.CustomSPVWalletConstructor, def *asset.WalletDefinition) { 185 for _, availableWallets := range WalletInfo.AvailableWallets { 186 if def.Type == availableWallets.Type { 187 panic(fmt.Sprintf("wallet type (%q) is already registered", def.Type)) 188 } 189 } 190 customSPVWalletConstructors[def.Type] = constructor 191 WalletInfo.AvailableWallets = append(WalletInfo.AvailableWallets, def) 192 } 193 194 // NewWallet is the exported constructor by which the DEX will import the 195 // exchange wallet. 196 func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) { 197 var cloneParams *chaincfg.Params 198 switch network { 199 case dex.Mainnet: 200 cloneParams = dexltc.MainNetParams 201 case dex.Testnet: 202 cloneParams = dexltc.TestNet4Params 203 case dex.Regtest: 204 cloneParams = dexltc.RegressionNetParams 205 default: 206 return nil, fmt.Errorf("unknown network ID %v", network) 207 } 208 209 // Designate the clone ports. These will be overwritten by any explicit 210 // settings in the configuration file. 211 cloneCFG := &btc.BTCCloneCFG{ 212 WalletCFG: cfg, 213 MinNetworkVersion: minNetworkVersion, 214 WalletInfo: WalletInfo, 215 Symbol: "ltc", 216 Logger: logger, 217 Network: network, 218 ChainParams: cloneParams, 219 Ports: NetPorts, 220 DefaultFallbackFee: defaultFee, 221 DefaultFeeRateLimit: defaultFeeRateLimit, 222 LegacyBalance: false, 223 LegacyRawFeeLimit: false, 224 Segwit: true, 225 InitTxSize: dexbtc.InitTxSizeSegwit, 226 InitTxSizeBase: dexbtc.InitTxSizeBaseSegwit, 227 BlockDeserializer: dexltc.DeserializeBlockBytes, 228 ExternalFeeEstimator: externalFeeRate, 229 AssetID: BipID, 230 } 231 232 switch cfg.Type { 233 case walletTypeRPC, walletTypeLegacy: 234 return btc.BTCCloneWallet(cloneCFG) 235 case walletTypeSPV: 236 return btc.OpenSPVWallet(cloneCFG, openSPVWallet) 237 case walletTypeElectrum: 238 cloneCFG.Ports = dexbtc.NetPorts{} // no default ports 239 ver, err := dex.SemverFromString(needElectrumVersion) 240 if err != nil { 241 return nil, err 242 } 243 cloneCFG.MinElectrumVersion = *ver 244 return btc.ElectrumWallet(cloneCFG) 245 default: 246 makeCustomWallet, ok := customSPVWalletConstructors[cfg.Type] 247 if !ok { 248 return nil, fmt.Errorf("unknown wallet type %q", cfg.Type) 249 } 250 251 // Create custom wallet first and return early if we encounter any 252 // error. 253 ltcWallet, err := makeCustomWallet(cfg.Settings, cloneCFG.ChainParams) 254 if err != nil { 255 return nil, fmt.Errorf("custom wallet setup error: %v", err) 256 } 257 258 walletConstructor := func(_ string, _ *btc.WalletConfig, _ *chaincfg.Params, _ dex.Logger) btc.BTCWallet { 259 return ltcWallet 260 } 261 return btc.OpenSPVWallet(cloneCFG, walletConstructor) 262 } 263 } 264 265 func parseChainParams(net dex.Network) (*ltcchaincfg.Params, error) { 266 switch net { 267 case dex.Mainnet: 268 return <cchaincfg.MainNetParams, nil 269 case dex.Testnet: 270 return <cchaincfg.TestNet4Params, nil 271 case dex.Regtest: 272 return <cchaincfg.RegressionNetParams, nil 273 } 274 return nil, fmt.Errorf("unknown network ID %v", net) 275 } 276 277 func externalFeeRate(ctx context.Context, net dex.Network) (uint64, error) { 278 switch net { 279 case dex.Mainnet, dex.Simnet: 280 return fetchBlockCypherFees(ctx) 281 } 282 return bitcoreFeeRate(ctx, net) 283 } 284 285 var bitcoreFeeRate = btc.BitcoreRateFetcher("LTC") 286 287 func fetchBlockCypherFees(ctx context.Context) (uint64, error) { 288 // Posted rate limits for blockcypher are 3/sec, 100/hr, 1000/day. 1000/day 289 // is once per 86.4 seconds, but I've seen unexpected metering before. 290 // Once per 5 minutes is a good rate, and that's the default. 291 // Can't find a source for testnet. Just using mainnet rate for everything. 292 const uri = "https://api.blockcypher.com/v1/ltc/main" 293 var res struct { 294 Low float64 `json:"low_fee_per_kb"` 295 Medium float64 `json:"medium_fee_per_kb"` 296 High float64 `json:"high_fee_per_kb"` 297 } 298 if err := dexnet.Get(ctx, uri, &res); err != nil { 299 return 0, err 300 } 301 if res.Low == 0 { 302 return 0, errors.New("no fee rate in result") 303 } 304 return uint64(math.Round(res.Low / 1000)), nil 305 }