decred.org/dcrdex@v1.0.5/client/asset/btc/electrum.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 btc
     5  
     6  import (
     7  	"context"
     8  	"encoding/hex"
     9  	"errors"
    10  	"fmt"
    11  	"strings"
    12  	"sync"
    13  	"sync/atomic"
    14  	"time"
    15  
    16  	"decred.org/dcrdex/client/asset"
    17  	"decred.org/dcrdex/client/asset/btc/electrum"
    18  	"decred.org/dcrdex/dex"
    19  	"decred.org/dcrdex/dex/config"
    20  	dexbtc "decred.org/dcrdex/dex/networks/btc"
    21  	"github.com/btcsuite/btcd/chaincfg/chainhash"
    22  )
    23  
    24  const needElectrumVersion = "4.5.5"
    25  
    26  // ExchangeWalletElectrum is the asset.Wallet for an external Electrum wallet.
    27  type ExchangeWalletElectrum struct {
    28  	*baseWallet
    29  	*authAddOn
    30  	ew                 *electrumWallet
    31  	minElectrumVersion dex.Semver
    32  
    33  	findRedemptionMtx   sync.RWMutex
    34  	findRedemptionQueue map[OutPoint]*FindRedemptionReq
    35  
    36  	syncingTxHistory atomic.Bool
    37  }
    38  
    39  var _ asset.Wallet = (*ExchangeWalletElectrum)(nil)
    40  var _ asset.Authenticator = (*ExchangeWalletElectrum)(nil)
    41  var _ asset.WalletHistorian = (*ExchangeWalletElectrum)(nil)
    42  
    43  // ElectrumWallet creates a new ExchangeWalletElectrum for the provided
    44  // configuration, which must contain the necessary details for accessing the
    45  // Electrum wallet's RPC server in the WalletCFG.Settings map.
    46  func ElectrumWallet(cfg *BTCCloneCFG) (*ExchangeWalletElectrum, error) {
    47  	clientCfg := new(RPCWalletConfig)
    48  	err := config.Unmapify(cfg.WalletCFG.Settings, clientCfg)
    49  	if err != nil {
    50  		return nil, fmt.Errorf("error parsing rpc wallet config: %w", err)
    51  	}
    52  
    53  	btc, err := newUnconnectedWallet(cfg, &clientCfg.WalletConfig)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  
    58  	rpcCfg := &clientCfg.RPCConfig
    59  	dexbtc.StandardizeRPCConf(&rpcCfg.RPCConfig, "")
    60  	ewc := electrum.NewWalletClient(rpcCfg.RPCUser, rpcCfg.RPCPass,
    61  		"http://"+rpcCfg.RPCBind, rpcCfg.WalletName)
    62  	ew := newElectrumWallet(ewc, &electrumWalletConfig{
    63  		params:       cfg.ChainParams,
    64  		log:          cfg.Logger.SubLogger("ELECTRUM"),
    65  		addrDecoder:  cfg.AddressDecoder,
    66  		addrStringer: cfg.AddressStringer,
    67  		segwit:       cfg.Segwit,
    68  		rpcCfg:       rpcCfg,
    69  	})
    70  	btc.setNode(ew)
    71  
    72  	eew := &ExchangeWalletElectrum{
    73  		baseWallet:          btc,
    74  		authAddOn:           &authAddOn{btc.node},
    75  		ew:                  ew,
    76  		findRedemptionQueue: make(map[OutPoint]*FindRedemptionReq),
    77  		minElectrumVersion:  cfg.MinElectrumVersion,
    78  	}
    79  
    80  	// In (*baseWallet).feeRate, use ExchangeWalletElectrum's walletFeeRate
    81  	// override for localFeeRate. No externalFeeRate is required but will be
    82  	// used if eew.walletFeeRate returned an error and an externalFeeRate is
    83  	// enabled.
    84  	btc.localFeeRate = eew.walletFeeRate
    85  
    86  	// Firo electrum does not have "onchain_history" method as of firo
    87  	// electrum 4.1.5.3, find an alternative.
    88  	btc.noListTxHistory = cfg.Symbol == "firo"
    89  
    90  	return eew, nil
    91  }
    92  
    93  // DepositAddress returns an address for depositing funds into the exchange
    94  // wallet. The address will be unused but not necessarily new. Use NewAddress to
    95  // request a new address, but it should be used immediately.
    96  func (btc *ExchangeWalletElectrum) DepositAddress() (string, error) {
    97  	return btc.ew.wallet.GetUnusedAddress(btc.ew.ctx)
    98  }
    99  
   100  // RedemptionAddress gets an address for use in redeeming the counterparty's
   101  // swap. This would be included in their swap initialization. The address will
   102  // be unused but not necessarily new because these addresses often go unused.
   103  func (btc *ExchangeWalletElectrum) RedemptionAddress() (string, error) {
   104  	return btc.ew.wallet.GetUnusedAddress(btc.ew.ctx)
   105  }
   106  
   107  // Connect connects to the Electrum wallet's RPC server and an electrum server
   108  // directly. Goroutines are started to monitor for new blocks and server
   109  // connection changes. Satisfies the dex.Connector interface.
   110  func (btc *ExchangeWalletElectrum) Connect(ctx context.Context) (*sync.WaitGroup, error) {
   111  	wg, err := btc.connect(ctx) // prepares btc.ew.chainV via btc.node.connect()
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	commands, err := btc.ew.wallet.Commands(ctx)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	var hasFreezeUTXO bool
   121  	for i := range commands {
   122  		if commands[i] == "freeze_utxo" {
   123  			hasFreezeUTXO = true
   124  			break
   125  		}
   126  	}
   127  	if !hasFreezeUTXO {
   128  		return nil, errors.New("wallet does not support the freeze_utxo command")
   129  	}
   130  
   131  	serverFeats, err := btc.ew.chain().Features(ctx)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	// TODO: for chainforks with the same genesis hash (BTC -> BCH), compare a
   136  	// block hash at some post-fork height.
   137  	if genesis := btc.chainParams.GenesisHash; genesis != nil && genesis.String() != serverFeats.Genesis {
   138  		return nil, fmt.Errorf("wanted genesis hash %v, got %v (wrong network)",
   139  			genesis.String(), serverFeats.Genesis)
   140  	}
   141  
   142  	verStr, err := btc.ew.wallet.Version(ctx)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  	gotVer, err := dex.SemverFromString(verStr)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	if !dex.SemverCompatible(btc.minElectrumVersion, *gotVer) {
   151  		return nil, fmt.Errorf("wanted electrum wallet version %s but got %s", btc.minElectrumVersion, gotVer)
   152  	}
   153  
   154  	if btc.minElectrumVersion.Major >= 4 && btc.minElectrumVersion.Minor >= 5 {
   155  		btc.ew.wallet.SetIncludeIgnoreWarnings(true)
   156  	}
   157  
   158  	dbWG, err := btc.startTxHistoryDB(ctx)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	wg.Add(1)
   164  	go func() {
   165  		defer wg.Done()
   166  		dbWG.Wait()
   167  	}()
   168  
   169  	wg.Add(1)
   170  	go func() {
   171  		defer wg.Done()
   172  		btc.watchBlocks(ctx) // ExchangeWalletElectrum override
   173  		btc.cancelRedemptionSearches()
   174  	}()
   175  	wg.Add(1)
   176  	go func() {
   177  		defer wg.Done()
   178  		btc.monitorPeers(ctx)
   179  	}()
   180  
   181  	wg.Add(1)
   182  	go func() {
   183  		defer wg.Done()
   184  		btc.tipMtx.RLock()
   185  		tip := btc.currentTip
   186  		btc.tipMtx.RUnlock()
   187  		go btc.syncTxHistory(uint64(tip.Height))
   188  	}()
   189  
   190  	return wg, nil
   191  }
   192  
   193  func (btc *ExchangeWalletElectrum) cancelRedemptionSearches() {
   194  	// Close all open channels for contract redemption searches
   195  	// to prevent leakages and ensure goroutines that are started
   196  	// to wait on these channels end gracefully.
   197  	btc.findRedemptionMtx.Lock()
   198  	for contractOutpoint, req := range btc.findRedemptionQueue {
   199  		req.fail("shutting down")
   200  		delete(btc.findRedemptionQueue, contractOutpoint)
   201  	}
   202  	btc.findRedemptionMtx.Unlock()
   203  }
   204  
   205  // walletFeeRate satisfies BTCCloneCFG.FeeEstimator.
   206  func (btc *ExchangeWalletElectrum) walletFeeRate(ctx context.Context, _ RawRequester, confTarget uint64) (uint64, error) {
   207  	satPerKB, err := btc.ew.wallet.FeeRate(ctx, int64(confTarget))
   208  	if err != nil {
   209  		return 0, err
   210  	}
   211  	return uint64(dex.IntDivUp(satPerKB, 1000)), nil
   212  }
   213  
   214  // findRedemption will search for the spending transaction of specified
   215  // outpoint. If found, the secret key will be extracted from the input scripts.
   216  // If not found, but otherwise without an error, a nil Hash will be returned
   217  // along with a nil error. Thus, both the error and the Hash should be checked.
   218  // This convention is only used since this is not part of the public API.
   219  func (btc *ExchangeWalletElectrum) findRedemption(ctx context.Context, op OutPoint, contractHash []byte) (*chainhash.Hash, uint32, []byte, error) {
   220  	msgTx, vin, err := btc.ew.findOutputSpender(ctx, &op.TxHash, op.Vout)
   221  	if err != nil {
   222  		return nil, 0, nil, err
   223  	}
   224  	if msgTx == nil {
   225  		return nil, 0, nil, nil
   226  	}
   227  	txHash := msgTx.TxHash()
   228  	txIn := msgTx.TxIn[vin]
   229  	secret, err := dexbtc.FindKeyPush(txIn.Witness, txIn.SignatureScript,
   230  		contractHash, btc.segwit, btc.chainParams)
   231  	if err != nil {
   232  		return nil, 0, nil, fmt.Errorf("failed to extract secret key from tx %v input %d: %w",
   233  			txHash, vin, err) // name the located tx in the error since we found it
   234  	}
   235  	return &txHash, vin, secret, nil
   236  }
   237  
   238  func (btc *ExchangeWalletElectrum) tryRedemptionRequests(ctx context.Context) {
   239  	btc.findRedemptionMtx.RLock()
   240  	reqs := make([]*FindRedemptionReq, 0, len(btc.findRedemptionQueue))
   241  	for _, req := range btc.findRedemptionQueue {
   242  		reqs = append(reqs, req)
   243  	}
   244  	btc.findRedemptionMtx.RUnlock()
   245  
   246  	for _, req := range reqs {
   247  		txHash, vin, secret, err := btc.findRedemption(ctx, req.outPt, req.contractHash)
   248  		if err != nil {
   249  			req.fail("findRedemption: %w", err)
   250  			continue
   251  		}
   252  		if txHash == nil {
   253  			continue // maybe next time
   254  		}
   255  		req.success(&FindRedemptionResult{
   256  			redemptionCoinID: ToCoinID(txHash, vin),
   257  			secret:           secret,
   258  		})
   259  	}
   260  }
   261  
   262  // FindRedemption locates a swap contract output's redemption transaction input
   263  // and the secret key used to spend the output.
   264  func (btc *ExchangeWalletElectrum) FindRedemption(ctx context.Context, coinID, contract dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) {
   265  	txHash, vout, err := decodeCoinID(coinID)
   266  	if err != nil {
   267  		return nil, nil, err
   268  	}
   269  	contractHash := btc.hashContract(contract)
   270  	// We can verify the contract hash via:
   271  	// txRes, _ := btc.ewc.getWalletTransaction(txHash)
   272  	// msgTx, _ := msgTxFromBytes(txRes.Hex)
   273  	// contractHash := dexbtc.ExtractScriptHash(msgTx.TxOut[vout].PkScript)
   274  	// OR
   275  	// txOut, _, _ := btc.ew.getTxOutput(txHash, vout)
   276  	// contractHash := dexbtc.ExtractScriptHash(txOut.PkScript)
   277  
   278  	// Check once before putting this in the queue.
   279  	outPt := NewOutPoint(txHash, vout)
   280  	spendTxID, vin, secret, err := btc.findRedemption(ctx, outPt, contractHash)
   281  	if err != nil {
   282  		return nil, nil, err
   283  	}
   284  	if spendTxID != nil {
   285  		return ToCoinID(spendTxID, vin), secret, nil
   286  	}
   287  
   288  	req := &FindRedemptionReq{
   289  		outPt:        outPt,
   290  		resultChan:   make(chan *FindRedemptionResult, 1),
   291  		contractHash: contractHash,
   292  		// blockHash, blockHeight, and pkScript not used by this impl.
   293  		blockHash: &chainhash.Hash{},
   294  	}
   295  	if err := btc.queueFindRedemptionRequest(req); err != nil {
   296  		return nil, nil, err
   297  	}
   298  
   299  	var result *FindRedemptionResult
   300  	select {
   301  	case result = <-req.resultChan:
   302  		if result == nil {
   303  			err = fmt.Errorf("unexpected nil result for redemption search for %s", outPt)
   304  		}
   305  	case <-ctx.Done():
   306  		err = fmt.Errorf("context cancelled during search for redemption for %s", outPt)
   307  	}
   308  
   309  	// If this contract is still in the findRedemptionQueue, remove from the
   310  	// queue to prevent further redemption search attempts for this contract.
   311  	btc.findRedemptionMtx.Lock()
   312  	delete(btc.findRedemptionQueue, outPt)
   313  	btc.findRedemptionMtx.Unlock()
   314  
   315  	// result would be nil if ctx is canceled or the result channel is closed
   316  	// without data, which would happen if the redemption search is aborted when
   317  	// this ExchangeWallet is shut down.
   318  	if result != nil {
   319  		return result.redemptionCoinID, result.secret, result.err
   320  	}
   321  	return nil, nil, err
   322  }
   323  
   324  func (btc *ExchangeWalletElectrum) queueFindRedemptionRequest(req *FindRedemptionReq) error {
   325  	btc.findRedemptionMtx.Lock()
   326  	defer btc.findRedemptionMtx.Unlock()
   327  	if _, exists := btc.findRedemptionQueue[req.outPt]; exists {
   328  		return fmt.Errorf("duplicate find redemption request for %s", req.outPt)
   329  	}
   330  	btc.findRedemptionQueue[req.outPt] = req
   331  	return nil
   332  }
   333  
   334  // watchBlocks pings for new blocks and runs the tipChange callback function
   335  // when the block changes.
   336  func (btc *ExchangeWalletElectrum) watchBlocks(ctx context.Context) {
   337  	const electrumBlockTick = 5 * time.Second
   338  	ticker := time.NewTicker(electrumBlockTick)
   339  	defer ticker.Stop()
   340  
   341  	bestBlock := func() (*BlockVector, error) {
   342  		hdr, err := btc.node.GetBestBlockHeader()
   343  		if err != nil {
   344  			return nil, fmt.Errorf("getBestBlockHeader: %v", err)
   345  		}
   346  		hash, err := chainhash.NewHashFromStr(hdr.Hash)
   347  		if err != nil {
   348  			return nil, fmt.Errorf("invalid best block hash %s: %v", hdr.Hash, err)
   349  		}
   350  		return &BlockVector{hdr.Height, *hash}, nil
   351  	}
   352  
   353  	currentTip, err := bestBlock()
   354  	if err != nil {
   355  		btc.log.Errorf("Failed to get best block: %v", err)
   356  		currentTip = new(BlockVector) // zero height and hash
   357  	}
   358  
   359  	for {
   360  		select {
   361  		case <-ticker.C:
   362  			// Don't make server requests on every tick. Wallet has a headers
   363  			// subscription, so we can just ask wallet the height. That means
   364  			// only comparing heights instead of hashes, which means we might
   365  			// not notice a reorg to a block at the same height, which is
   366  			// unimportant because of how electrum searches for transactions.
   367  			ss, err := btc.node.SyncStatus()
   368  			if err != nil {
   369  				btc.log.Errorf("failed to get sync status: %w", err)
   370  				continue
   371  			}
   372  
   373  			sameTip := currentTip.Height == int64(ss.Blocks)
   374  			if sameTip {
   375  				// Could have actually been a reorg to different block at same
   376  				// height. We'll report a new tip block on the next block.
   377  				continue
   378  			}
   379  
   380  			newTip, err := bestBlock()
   381  			if err != nil {
   382  				// NOTE: often says "height X out of range", then succeeds on next tick
   383  				if !strings.Contains(err.Error(), "out of range") {
   384  					btc.log.Errorf("failed to get best block from %s electrum server: %v", btc.symbol, err)
   385  				}
   386  				continue
   387  			}
   388  
   389  			go btc.syncTxHistory(uint64(newTip.Height))
   390  
   391  			btc.log.Tracef("tip change: %d (%s) => %d (%s)", currentTip.Height, currentTip.Hash,
   392  				newTip.Height, newTip.Hash)
   393  			currentTip = newTip
   394  			btc.emit.TipChange(uint64(newTip.Height))
   395  			go btc.tryRedemptionRequests(ctx)
   396  
   397  		case <-ctx.Done():
   398  			return
   399  		}
   400  	}
   401  }
   402  
   403  // syncTxHistory checks to see if there are any transactions which the wallet
   404  // has made or recieved that are not part of the transaction history, then
   405  // identifies and adds them. It also checks all the pending transactions to see
   406  // if they have been mined into a block, and if so, updates the transaction
   407  // history to reflect the block height.
   408  func (btc *ExchangeWalletElectrum) syncTxHistory(tip uint64) {
   409  	if !btc.syncingTxHistory.CompareAndSwap(false, true) {
   410  		return
   411  	}
   412  	defer btc.syncingTxHistory.Store(false)
   413  
   414  	txHistoryDB := btc.txDB()
   415  	if txHistoryDB == nil {
   416  		return
   417  	}
   418  
   419  	ss, err := btc.SyncStatus()
   420  	if err != nil {
   421  		btc.log.Errorf("Error getting sync status: %v", err)
   422  		return
   423  	}
   424  	if !ss.Synced {
   425  		return
   426  	}
   427  
   428  	btc.addUnknownTransactionsToHistory(tip)
   429  
   430  	pendingTxsCopy := make(map[chainhash.Hash]ExtendedWalletTx, len(btc.pendingTxs))
   431  	btc.pendingTxsMtx.RLock()
   432  	for hash, tx := range btc.pendingTxs {
   433  		pendingTxsCopy[hash] = tx
   434  	}
   435  	btc.pendingTxsMtx.RUnlock()
   436  
   437  	handlePendingTx := func(txHash chainhash.Hash, tx *ExtendedWalletTx) {
   438  		if !tx.Submitted {
   439  			return
   440  		}
   441  
   442  		gtr, err := btc.node.GetWalletTransaction(&txHash)
   443  		if errors.Is(err, asset.CoinNotFoundError) {
   444  			err = txHistoryDB.RemoveTx(txHash.String())
   445  			if err == nil || errors.Is(err, asset.CoinNotFoundError) {
   446  				btc.pendingTxsMtx.Lock()
   447  				delete(btc.pendingTxs, txHash)
   448  				btc.pendingTxsMtx.Unlock()
   449  			} else {
   450  				// Leave it in the pendingPendingTxs and attempt to remove it
   451  				// again next time.
   452  				btc.log.Errorf("Error removing tx %s from the history store: %v", txHash.String(), err)
   453  			}
   454  			return
   455  		}
   456  		if err != nil {
   457  			btc.log.Errorf("Error getting transaction %s: %v", txHash.String(), err)
   458  			return
   459  		}
   460  
   461  		var updated bool
   462  		if gtr.BlockHash != "" {
   463  			bestHeight, err := btc.node.GetBestBlockHeight()
   464  			if err != nil {
   465  				btc.log.Errorf("GetBestBlockHeader: %v", err)
   466  				return
   467  			}
   468  			// TODO: Just get the block height with the header.
   469  			blockHeight := bestHeight - int32(gtr.Confirmations) + 1
   470  			i := 0
   471  			for {
   472  				if i > 20 || blockHeight < 0 {
   473  					btc.log.Errorf("Cannot find mined tx block number for %s", gtr.BlockHash)
   474  					return
   475  				}
   476  				bh, err := btc.ew.GetBlockHash(int64(blockHeight))
   477  				if err != nil {
   478  					btc.log.Errorf("Error getting mined tx block number %s: %v", gtr.BlockHash, err)
   479  					return
   480  				}
   481  				if bh.String() == gtr.BlockHash {
   482  					break
   483  				}
   484  				i++
   485  				blockHeight--
   486  			}
   487  			if tx.BlockNumber != uint64(blockHeight) {
   488  				tx.BlockNumber = uint64(blockHeight)
   489  				tx.Timestamp = gtr.BlockTime
   490  				updated = true
   491  			}
   492  		} else if gtr.BlockHash == "" && tx.BlockNumber != 0 {
   493  			tx.BlockNumber = 0
   494  			tx.Timestamp = 0
   495  			updated = true
   496  		}
   497  
   498  		var confs uint64
   499  		if tx.BlockNumber > 0 && tip >= tx.BlockNumber {
   500  			confs = tip - tx.BlockNumber + 1
   501  		}
   502  		if confs >= requiredRedeemConfirms {
   503  			tx.Confirmed = true
   504  			updated = true
   505  		}
   506  
   507  		if updated {
   508  			err = txHistoryDB.StoreTx(tx)
   509  			if err != nil {
   510  				btc.log.Errorf("Error updating tx %s: %v", txHash, err)
   511  				return
   512  			}
   513  
   514  			btc.pendingTxsMtx.Lock()
   515  			if tx.Confirmed {
   516  				delete(btc.pendingTxs, txHash)
   517  			} else {
   518  				btc.pendingTxs[txHash] = *tx
   519  			}
   520  			btc.pendingTxsMtx.Unlock()
   521  
   522  			btc.emit.TransactionNote(tx.WalletTransaction, false)
   523  		}
   524  	}
   525  
   526  	for hash, tx := range pendingTxsCopy {
   527  		if btc.ctx.Err() != nil {
   528  			return
   529  		}
   530  		handlePendingTx(hash, &tx)
   531  	}
   532  }
   533  
   534  // WalletTransaction returns a transaction that either the wallet has made or
   535  // one in which the wallet has received funds. The txID can be either a byte
   536  // reversed tx hash or a hex encoded coin ID.
   537  func (btc *ExchangeWalletElectrum) WalletTransaction(ctx context.Context, txID string) (*asset.WalletTransaction, error) {
   538  	coinID, err := hex.DecodeString(txID)
   539  	if err == nil {
   540  		txHash, _, err := decodeCoinID(coinID)
   541  		if err == nil {
   542  			txID = txHash.String()
   543  		}
   544  	}
   545  
   546  	txHistoryDB := btc.txDB()
   547  	if txHistoryDB == nil {
   548  		return nil, fmt.Errorf("tx database not initialized")
   549  	}
   550  	tx, err := txHistoryDB.GetTx(txID)
   551  	if err != nil && !errors.Is(err, asset.CoinNotFoundError) {
   552  		return nil, err
   553  	}
   554  
   555  	if tx == nil {
   556  		txHash, err := chainhash.NewHashFromStr(txID)
   557  		if err != nil {
   558  			return nil, fmt.Errorf("error decoding txid %s: %w", txID, err)
   559  		}
   560  
   561  		gtr, err := btc.node.GetWalletTransaction(txHash)
   562  		if err != nil {
   563  			return nil, fmt.Errorf("error getting transaction %s: %w", txID, err)
   564  		}
   565  
   566  		var blockHeight uint32
   567  		if gtr.BlockHash != "" {
   568  			bestHeight, err := btc.node.GetBestBlockHeight()
   569  			if err != nil {
   570  				return nil, fmt.Errorf("GetBestBlockHeader: %v", err)
   571  			}
   572  			// TODO: Just get the block height with the header.
   573  			blockHeight := bestHeight - int32(gtr.Confirmations) + 1
   574  			i := 0
   575  			for {
   576  				if i > 20 || blockHeight < 0 {
   577  					return nil, fmt.Errorf("Cannot find mined tx block number for %s", gtr.BlockHash)
   578  				}
   579  				bh, err := btc.ew.getBlockHeaderByHeight(btc.ctx, int64(blockHeight))
   580  				if err != nil {
   581  					return nil, fmt.Errorf("Error getting mined tx block number %s: %v", gtr.BlockHash, err)
   582  				}
   583  				if bh.BlockHash().String() == gtr.BlockHash {
   584  					break
   585  				}
   586  				i++
   587  				blockHeight--
   588  			}
   589  		}
   590  
   591  		tx, err = btc.idUnknownTx(&ListTransactionsResult{
   592  			BlockHeight: blockHeight,
   593  			BlockTime:   gtr.BlockTime,
   594  			TxID:        txID,
   595  		})
   596  		if err != nil {
   597  			return nil, fmt.Errorf("error identifying transaction: %v", err)
   598  		}
   599  
   600  		tx.BlockNumber = uint64(blockHeight)
   601  		tx.Timestamp = gtr.BlockTime
   602  		tx.Confirmed = blockHeight > 0
   603  		btc.addTxToHistory(tx, txHash, true, false)
   604  	}
   605  
   606  	return tx, nil
   607  }
   608  
   609  // TxHistory returns all the transactions the wallet has made. If refID is nil,
   610  // then transactions starting from the most recent are returned (past is ignored).
   611  // If past is true, the transactions prior to the refID are returned, otherwise
   612  // the transactions after the refID are returned. n is the number of
   613  // transactions to return. If n is <= 0, all the transactions will be returned.
   614  func (btc *ExchangeWalletElectrum) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) {
   615  	txHistoryDB := btc.txDB()
   616  	if txHistoryDB == nil {
   617  		return nil, fmt.Errorf("tx database not initialized")
   618  	}
   619  	return txHistoryDB.GetTxs(n, refID, past)
   620  }