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 &ltcchaincfg.MainNetParams, nil
   269  	case dex.Testnet:
   270  		return &ltcchaincfg.TestNet4Params, nil
   271  	case dex.Regtest:
   272  		return &ltcchaincfg.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  }