decred.org/dcrdex@v1.0.5/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 // customWalletConstructors are functions for setting up btc.CustomWallet 174 // implementations used by btc.ExchangeWalletCustom. 175 var customWalletConstructors = map[string]btc.CustomWalletConstructor{} 176 177 // RegisterCustomWallet registers a function that should be used in creating a 178 // btc.CustomWallet implementation for btc.ExchangeWalletCustom. External 179 // consumers can use this function to provide btc.CustomWallet implementation, 180 // and must do so before attempting to create an btc.ExchangeWalletCustom 181 // instance of this type. It'll panic if callers try to register a wallet twice. 182 func RegisterCustomWallet(constructor btc.CustomWalletConstructor, def *asset.WalletDefinition) { 183 for _, availableWallets := range WalletInfo.AvailableWallets { 184 if def.Type == availableWallets.Type { 185 panic(fmt.Sprintf("wallet type (%q) is already registered", def.Type)) 186 } 187 } 188 customWalletConstructors[def.Type] = constructor 189 WalletInfo.AvailableWallets = append(WalletInfo.AvailableWallets, def) 190 } 191 192 // NewWallet is the exported constructor by which the DEX will import the 193 // exchange wallet. 194 func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) { 195 var cloneParams *chaincfg.Params 196 switch network { 197 case dex.Mainnet: 198 cloneParams = dexltc.MainNetParams 199 case dex.Testnet: 200 cloneParams = dexltc.TestNet4Params 201 case dex.Regtest: 202 cloneParams = dexltc.RegressionNetParams 203 default: 204 return nil, fmt.Errorf("unknown network ID %v", network) 205 } 206 207 // Designate the clone ports. These will be overwritten by any explicit 208 // settings in the configuration file. 209 cloneCFG := &btc.BTCCloneCFG{ 210 WalletCFG: cfg, 211 MinNetworkVersion: minNetworkVersion, 212 WalletInfo: WalletInfo, 213 Symbol: "ltc", 214 Logger: logger, 215 Network: network, 216 ChainParams: cloneParams, 217 Ports: NetPorts, 218 DefaultFallbackFee: defaultFee, 219 DefaultFeeRateLimit: defaultFeeRateLimit, 220 LegacyBalance: false, 221 LegacyRawFeeLimit: false, 222 Segwit: true, 223 InitTxSize: dexbtc.InitTxSizeSegwit, 224 InitTxSizeBase: dexbtc.InitTxSizeBaseSegwit, 225 BlockDeserializer: dexltc.DeserializeBlockBytes, 226 ExternalFeeEstimator: externalFeeRate, 227 AssetID: BipID, 228 } 229 230 switch cfg.Type { 231 case walletTypeRPC, walletTypeLegacy: 232 return btc.BTCCloneWallet(cloneCFG) 233 case walletTypeSPV: 234 return btc.OpenSPVWallet(cloneCFG, openSPVWallet) 235 case walletTypeElectrum: 236 cloneCFG.Ports = dexbtc.NetPorts{} // no default ports 237 ver, err := dex.SemverFromString(needElectrumVersion) 238 if err != nil { 239 return nil, err 240 } 241 cloneCFG.MinElectrumVersion = *ver 242 return btc.ElectrumWallet(cloneCFG) 243 default: 244 makeCustomWallet, ok := customWalletConstructors[cfg.Type] 245 if !ok { 246 return nil, fmt.Errorf("unknown wallet type %q", cfg.Type) 247 } 248 return btc.OpenCustomWallet(cloneCFG, makeCustomWallet) 249 } 250 } 251 252 func parseChainParams(net dex.Network) (*ltcchaincfg.Params, error) { 253 switch net { 254 case dex.Mainnet: 255 return <cchaincfg.MainNetParams, nil 256 case dex.Testnet: 257 return <cchaincfg.TestNet4Params, nil 258 case dex.Regtest: 259 return <cchaincfg.RegressionNetParams, nil 260 } 261 return nil, fmt.Errorf("unknown network ID %v", net) 262 } 263 264 func externalFeeRate(ctx context.Context, net dex.Network) (uint64, error) { 265 switch net { 266 case dex.Mainnet, dex.Simnet: 267 return fetchBlockCypherFees(ctx) 268 } 269 return bitcoreFeeRate(ctx, net) 270 } 271 272 var bitcoreFeeRate = btc.BitcoreRateFetcher("LTC") 273 274 func fetchBlockCypherFees(ctx context.Context) (uint64, error) { 275 // Posted rate limits for blockcypher are 3/sec, 100/hr, 1000/day. 1000/day 276 // is once per 86.4 seconds, but I've seen unexpected metering before. 277 // Once per 5 minutes is a good rate, and that's the default. 278 // Can't find a source for testnet. Just using mainnet rate for everything. 279 const uri = "https://api.blockcypher.com/v1/ltc/main" 280 var res struct { 281 Low float64 `json:"low_fee_per_kb"` 282 Medium float64 `json:"medium_fee_per_kb"` 283 High float64 `json:"high_fee_per_kb"` 284 } 285 if err := dexnet.Get(ctx, uri, &res); err != nil { 286 return 0, err 287 } 288 if res.Low == 0 { 289 return 0, errors.New("no fee rate in result") 290 } 291 return uint64(math.Round(res.Low / 1000)), nil 292 }