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 &ltcchaincfg.MainNetParams, nil
   256  	case dex.Testnet:
   257  		return &ltcchaincfg.TestNet4Params, nil
   258  	case dex.Regtest:
   259  		return &ltcchaincfg.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  }