decred.org/dcrdex@v1.0.3/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  	// In (*baseWallet).feeRate, use ExchangeWalletElectrum's walletFeeRate
    80  	// override for localFeeRate. No externalFeeRate is required but will be
    81  	// used if eew.walletFeeRate returned an error and an externalFeeRate is
    82  	// enabled.
    83  	btc.localFeeRate = eew.walletFeeRate
    84  
    85  	return eew, nil
    86  }
    87  
    88  // DepositAddress returns an address for depositing funds into the exchange
    89  // wallet. The address will be unused but not necessarily new. Use NewAddress to
    90  // request a new address, but it should be used immediately.
    91  func (btc *ExchangeWalletElectrum) DepositAddress() (string, error) {
    92  	return btc.ew.wallet.GetUnusedAddress(btc.ew.ctx)
    93  }
    94  
    95  // RedemptionAddress gets an address for use in redeeming the counterparty's
    96  // swap. This would be included in their swap initialization. The address will
    97  // be unused but not necessarily new because these addresses often go unused.
    98  func (btc *ExchangeWalletElectrum) RedemptionAddress() (string, error) {
    99  	return btc.ew.wallet.GetUnusedAddress(btc.ew.ctx)
   100  }
   101  
   102  // Connect connects to the Electrum wallet's RPC server and an electrum server
   103  // directly. Goroutines are started to monitor for new blocks and server
   104  // connection changes. Satisfies the dex.Connector interface.
   105  func (btc *ExchangeWalletElectrum) Connect(ctx context.Context) (*sync.WaitGroup, error) {
   106  	wg, err := btc.connect(ctx) // prepares btc.ew.chainV via btc.node.connect()
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	commands, err := btc.ew.wallet.Commands(ctx)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	var hasFreezeUTXO bool
   116  	for i := range commands {
   117  		if commands[i] == "freeze_utxo" {
   118  			hasFreezeUTXO = true
   119  			break
   120  		}
   121  	}
   122  	if !hasFreezeUTXO {
   123  		return nil, errors.New("wallet does not support the freeze_utxo command")
   124  	}
   125  
   126  	serverFeats, err := btc.ew.chain().Features(ctx)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	// TODO: for chainforks with the same genesis hash (BTC -> BCH), compare a
   131  	// block hash at some post-fork height.
   132  	if genesis := btc.chainParams.GenesisHash; genesis != nil && genesis.String() != serverFeats.Genesis {
   133  		return nil, fmt.Errorf("wanted genesis hash %v, got %v (wrong network)",
   134  			genesis.String(), serverFeats.Genesis)
   135  	}
   136  
   137  	verStr, err := btc.ew.wallet.Version(ctx)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	gotVer, err := dex.SemverFromString(verStr)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	if !dex.SemverCompatible(btc.minElectrumVersion, *gotVer) {
   146  		return nil, fmt.Errorf("wanted electrum wallet version %s but got %s", btc.minElectrumVersion, gotVer)
   147  	}
   148  
   149  	if btc.minElectrumVersion.Major >= 4 && btc.minElectrumVersion.Minor >= 5 {
   150  		btc.ew.wallet.SetIncludeIgnoreWarnings(true)
   151  	}
   152  
   153  	// TODO: Firo electrum does not have "onchain_history" method as of firo
   154  	// electrum 4.1.5.3, find an alternative.
   155  	hasOnchainHistory := btc.symbol != "firo"
   156  
   157  	if hasOnchainHistory {
   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  
   170  	wg.Add(1)
   171  	go func() {
   172  		defer wg.Done()
   173  		btc.watchBlocks(ctx) // ExchangeWalletElectrum override
   174  		btc.cancelRedemptionSearches()
   175  	}()
   176  	wg.Add(1)
   177  	go func() {
   178  		defer wg.Done()
   179  		btc.monitorPeers(ctx)
   180  	}()
   181  
   182  	if hasOnchainHistory {
   183  		wg.Add(1)
   184  		go func() {
   185  			defer wg.Done()
   186  			btc.tipMtx.RLock()
   187  			tip := btc.currentTip
   188  			btc.tipMtx.RUnlock()
   189  			go btc.syncTxHistory(uint64(tip.Height))
   190  		}()
   191  	}
   192  
   193  	return wg, nil
   194  }
   195  
   196  func (btc *ExchangeWalletElectrum) cancelRedemptionSearches() {
   197  	// Close all open channels for contract redemption searches
   198  	// to prevent leakages and ensure goroutines that are started
   199  	// to wait on these channels end gracefully.
   200  	btc.findRedemptionMtx.Lock()
   201  	for contractOutpoint, req := range btc.findRedemptionQueue {
   202  		req.fail("shutting down")
   203  		delete(btc.findRedemptionQueue, contractOutpoint)
   204  	}
   205  	btc.findRedemptionMtx.Unlock()
   206  }
   207  
   208  // walletFeeRate satisfies BTCCloneCFG.FeeEstimator.
   209  func (btc *ExchangeWalletElectrum) walletFeeRate(ctx context.Context, _ RawRequester, confTarget uint64) (uint64, error) {
   210  	satPerKB, err := btc.ew.wallet.FeeRate(ctx, int64(confTarget))
   211  	if err != nil {
   212  		return 0, err
   213  	}
   214  	return uint64(dex.IntDivUp(satPerKB, 1000)), nil
   215  }
   216  
   217  // findRedemption will search for the spending transaction of specified
   218  // outpoint. If found, the secret key will be extracted from the input scripts.
   219  // If not found, but otherwise without an error, a nil Hash will be returned
   220  // along with a nil error. Thus, both the error and the Hash should be checked.
   221  // This convention is only used since this is not part of the public API.
   222  func (btc *ExchangeWalletElectrum) findRedemption(ctx context.Context, op OutPoint, contractHash []byte) (*chainhash.Hash, uint32, []byte, error) {
   223  	msgTx, vin, err := btc.ew.findOutputSpender(ctx, &op.TxHash, op.Vout)
   224  	if err != nil {
   225  		return nil, 0, nil, err
   226  	}
   227  	if msgTx == nil {
   228  		return nil, 0, nil, nil
   229  	}
   230  	txHash := msgTx.TxHash()
   231  	txIn := msgTx.TxIn[vin]
   232  	secret, err := dexbtc.FindKeyPush(txIn.Witness, txIn.SignatureScript,
   233  		contractHash, btc.segwit, btc.chainParams)
   234  	if err != nil {
   235  		return nil, 0, nil, fmt.Errorf("failed to extract secret key from tx %v input %d: %w",
   236  			txHash, vin, err) // name the located tx in the error since we found it
   237  	}
   238  	return &txHash, vin, secret, nil
   239  }
   240  
   241  func (btc *ExchangeWalletElectrum) tryRedemptionRequests(ctx context.Context) {
   242  	btc.findRedemptionMtx.RLock()
   243  	reqs := make([]*FindRedemptionReq, 0, len(btc.findRedemptionQueue))
   244  	for _, req := range btc.findRedemptionQueue {
   245  		reqs = append(reqs, req)
   246  	}
   247  	btc.findRedemptionMtx.RUnlock()
   248  
   249  	for _, req := range reqs {
   250  		txHash, vin, secret, err := btc.findRedemption(ctx, req.outPt, req.contractHash)
   251  		if err != nil {
   252  			req.fail("findRedemption: %w", err)
   253  			continue
   254  		}
   255  		if txHash == nil {
   256  			continue // maybe next time
   257  		}
   258  		req.success(&FindRedemptionResult{
   259  			redemptionCoinID: ToCoinID(txHash, vin),
   260  			secret:           secret,
   261  		})
   262  	}
   263  }
   264  
   265  // FindRedemption locates a swap contract output's redemption transaction input
   266  // and the secret key used to spend the output.
   267  func (btc *ExchangeWalletElectrum) FindRedemption(ctx context.Context, coinID, contract dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) {
   268  	txHash, vout, err := decodeCoinID(coinID)
   269  	if err != nil {
   270  		return nil, nil, err
   271  	}
   272  	contractHash := btc.hashContract(contract)
   273  	// We can verify the contract hash via:
   274  	// txRes, _ := btc.ewc.getWalletTransaction(txHash)
   275  	// msgTx, _ := msgTxFromBytes(txRes.Hex)
   276  	// contractHash := dexbtc.ExtractScriptHash(msgTx.TxOut[vout].PkScript)
   277  	// OR
   278  	// txOut, _, _ := btc.ew.getTxOutput(txHash, vout)
   279  	// contractHash := dexbtc.ExtractScriptHash(txOut.PkScript)
   280  
   281  	// Check once before putting this in the queue.
   282  	outPt := NewOutPoint(txHash, vout)
   283  	spendTxID, vin, secret, err := btc.findRedemption(ctx, outPt, contractHash)
   284  	if err != nil {
   285  		return nil, nil, err
   286  	}
   287  	if spendTxID != nil {
   288  		return ToCoinID(spendTxID, vin), secret, nil
   289  	}
   290  
   291  	req := &FindRedemptionReq{
   292  		outPt:        outPt,
   293  		resultChan:   make(chan *FindRedemptionResult, 1),
   294  		contractHash: contractHash,
   295  		// blockHash, blockHeight, and pkScript not used by this impl.
   296  		blockHash: &chainhash.Hash{},
   297  	}
   298  	if err := btc.queueFindRedemptionRequest(req); err != nil {
   299  		return nil, nil, err
   300  	}
   301  
   302  	var result *FindRedemptionResult
   303  	select {
   304  	case result = <-req.resultChan:
   305  		if result == nil {
   306  			err = fmt.Errorf("unexpected nil result for redemption search for %s", outPt)
   307  		}
   308  	case <-ctx.Done():
   309  		err = fmt.Errorf("context cancelled during search for redemption for %s", outPt)
   310  	}
   311  
   312  	// If this contract is still in the findRedemptionQueue, remove from the
   313  	// queue to prevent further redemption search attempts for this contract.
   314  	btc.findRedemptionMtx.Lock()
   315  	delete(btc.findRedemptionQueue, outPt)
   316  	btc.findRedemptionMtx.Unlock()
   317  
   318  	// result would be nil if ctx is canceled or the result channel is closed
   319  	// without data, which would happen if the redemption search is aborted when
   320  	// this ExchangeWallet is shut down.
   321  	if result != nil {
   322  		return result.redemptionCoinID, result.secret, result.err
   323  	}
   324  	return nil, nil, err
   325  }
   326  
   327  func (btc *ExchangeWalletElectrum) queueFindRedemptionRequest(req *FindRedemptionReq) error {
   328  	btc.findRedemptionMtx.Lock()
   329  	defer btc.findRedemptionMtx.Unlock()
   330  	if _, exists := btc.findRedemptionQueue[req.outPt]; exists {
   331  		return fmt.Errorf("duplicate find redemption request for %s", req.outPt)
   332  	}
   333  	btc.findRedemptionQueue[req.outPt] = req
   334  	return nil
   335  }
   336  
   337  // watchBlocks pings for new blocks and runs the tipChange callback function
   338  // when the block changes.
   339  func (btc *ExchangeWalletElectrum) watchBlocks(ctx context.Context) {
   340  	const electrumBlockTick = 5 * time.Second
   341  	ticker := time.NewTicker(electrumBlockTick)
   342  	defer ticker.Stop()
   343  
   344  	bestBlock := func() (*BlockVector, error) {
   345  		hdr, err := btc.node.getBestBlockHeader()
   346  		if err != nil {
   347  			return nil, fmt.Errorf("getBestBlockHeader: %v", err)
   348  		}
   349  		hash, err := chainhash.NewHashFromStr(hdr.Hash)
   350  		if err != nil {
   351  			return nil, fmt.Errorf("invalid best block hash %s: %v", hdr.Hash, err)
   352  		}
   353  		return &BlockVector{hdr.Height, *hash}, nil
   354  	}
   355  
   356  	currentTip, err := bestBlock()
   357  	if err != nil {
   358  		btc.log.Errorf("Failed to get best block: %v", err)
   359  		currentTip = new(BlockVector) // zero height and hash
   360  	}
   361  
   362  	for {
   363  		select {
   364  		case <-ticker.C:
   365  			// Don't make server requests on every tick. Wallet has a headers
   366  			// subscription, so we can just ask wallet the height. That means
   367  			// only comparing heights instead of hashes, which means we might
   368  			// not notice a reorg to a block at the same height, which is
   369  			// unimportant because of how electrum searches for transactions.
   370  			ss, err := btc.node.syncStatus()
   371  			if err != nil {
   372  				btc.log.Errorf("failed to get sync status: %w", err)
   373  				continue
   374  			}
   375  
   376  			sameTip := currentTip.Height == int64(ss.Blocks)
   377  			if sameTip {
   378  				// Could have actually been a reorg to different block at same
   379  				// height. We'll report a new tip block on the next block.
   380  				continue
   381  			}
   382  
   383  			newTip, err := bestBlock()
   384  			if err != nil {
   385  				// NOTE: often says "height X out of range", then succeeds on next tick
   386  				if !strings.Contains(err.Error(), "out of range") {
   387  					btc.log.Errorf("failed to get best block from %s electrum server: %v", btc.symbol, err)
   388  				}
   389  				continue
   390  			}
   391  
   392  			go btc.syncTxHistory(uint64(newTip.Height))
   393  
   394  			btc.log.Tracef("tip change: %d (%s) => %d (%s)", currentTip.Height, currentTip.Hash,
   395  				newTip.Height, newTip.Hash)
   396  			currentTip = newTip
   397  			btc.emit.TipChange(uint64(newTip.Height))
   398  			go btc.tryRedemptionRequests(ctx)
   399  
   400  		case <-ctx.Done():
   401  			return
   402  		}
   403  	}
   404  }
   405  
   406  // syncTxHistory checks to see if there are any transactions which the wallet
   407  // has made or recieved that are not part of the transaction history, then
   408  // identifies and adds them. It also checks all the pending transactions to see
   409  // if they have been mined into a block, and if so, updates the transaction
   410  // history to reflect the block height.
   411  func (btc *ExchangeWalletElectrum) syncTxHistory(tip uint64) {
   412  	if !btc.syncingTxHistory.CompareAndSwap(false, true) {
   413  		return
   414  	}
   415  	defer btc.syncingTxHistory.Store(false)
   416  
   417  	txHistoryDB := btc.txDB()
   418  	if txHistoryDB == nil {
   419  		return
   420  	}
   421  
   422  	ss, err := btc.SyncStatus()
   423  	if err != nil {
   424  		btc.log.Errorf("Error getting sync status: %v", err)
   425  		return
   426  	}
   427  	if !ss.Synced {
   428  		return
   429  	}
   430  
   431  	btc.addUnknownTransactionsToHistory(tip)
   432  
   433  	pendingTxsCopy := make(map[chainhash.Hash]ExtendedWalletTx, len(btc.pendingTxs))
   434  	btc.pendingTxsMtx.RLock()
   435  	for hash, tx := range btc.pendingTxs {
   436  		pendingTxsCopy[hash] = tx
   437  	}
   438  	btc.pendingTxsMtx.RUnlock()
   439  
   440  	handlePendingTx := func(txHash chainhash.Hash, tx *ExtendedWalletTx) {
   441  		if !tx.Submitted {
   442  			return
   443  		}
   444  
   445  		gtr, err := btc.node.getWalletTransaction(&txHash)
   446  		if errors.Is(err, asset.CoinNotFoundError) {
   447  			err = txHistoryDB.RemoveTx(txHash.String())
   448  			if err == nil || errors.Is(err, asset.CoinNotFoundError) {
   449  				btc.pendingTxsMtx.Lock()
   450  				delete(btc.pendingTxs, txHash)
   451  				btc.pendingTxsMtx.Unlock()
   452  			} else {
   453  				// Leave it in the pendingPendingTxs and attempt to remove it
   454  				// again next time.
   455  				btc.log.Errorf("Error removing tx %s from the history store: %v", txHash.String(), err)
   456  			}
   457  			return
   458  		}
   459  		if err != nil {
   460  			btc.log.Errorf("Error getting transaction %s: %v", txHash.String(), err)
   461  			return
   462  		}
   463  
   464  		var updated bool
   465  		if gtr.BlockHash != "" {
   466  			bestHeight, err := btc.node.getBestBlockHeight()
   467  			if err != nil {
   468  				btc.log.Errorf("getBestBlockHeader: %v", err)
   469  				return
   470  			}
   471  			// TODO: Just get the block height with the header.
   472  			blockHeight := bestHeight - int32(gtr.Confirmations) + 1
   473  			i := 0
   474  			for {
   475  				if i > 20 || blockHeight < 0 {
   476  					btc.log.Errorf("Cannot find mined tx block number for %s", gtr.BlockHash)
   477  					return
   478  				}
   479  				bh, err := btc.ew.getBlockHeaderByHeight(btc.ctx, int64(blockHeight))
   480  				if err != nil {
   481  					btc.log.Errorf("Error getting mined tx block number %s: %v", gtr.BlockHash, err)
   482  					return
   483  				}
   484  				if bh.BlockHash().String() == gtr.BlockHash {
   485  					break
   486  				}
   487  				i++
   488  				blockHeight--
   489  			}
   490  			if tx.BlockNumber != uint64(blockHeight) {
   491  				tx.BlockNumber = uint64(blockHeight)
   492  				tx.Timestamp = gtr.BlockTime
   493  				updated = true
   494  			}
   495  		} else if gtr.BlockHash == "" && tx.BlockNumber != 0 {
   496  			tx.BlockNumber = 0
   497  			tx.Timestamp = 0
   498  			updated = true
   499  		}
   500  
   501  		var confs uint64
   502  		if tx.BlockNumber > 0 && tip >= tx.BlockNumber {
   503  			confs = tip - tx.BlockNumber + 1
   504  		}
   505  		if confs >= requiredRedeemConfirms {
   506  			tx.Confirmed = true
   507  			updated = true
   508  		}
   509  
   510  		if updated {
   511  			err = txHistoryDB.StoreTx(tx)
   512  			if err != nil {
   513  				btc.log.Errorf("Error updating tx %s: %v", txHash, err)
   514  				return
   515  			}
   516  
   517  			btc.pendingTxsMtx.Lock()
   518  			if tx.Confirmed {
   519  				delete(btc.pendingTxs, txHash)
   520  			} else {
   521  				btc.pendingTxs[txHash] = *tx
   522  			}
   523  			btc.pendingTxsMtx.Unlock()
   524  
   525  			btc.emit.TransactionNote(tx.WalletTransaction, false)
   526  		}
   527  	}
   528  
   529  	for hash, tx := range pendingTxsCopy {
   530  		if btc.ctx.Err() != nil {
   531  			return
   532  		}
   533  		handlePendingTx(hash, &tx)
   534  	}
   535  }
   536  
   537  // WalletTransaction returns a transaction that either the wallet has made or
   538  // one in which the wallet has received funds. The txID can be either a byte
   539  // reversed tx hash or a hex encoded coin ID.
   540  func (btc *ExchangeWalletElectrum) WalletTransaction(ctx context.Context, txID string) (*asset.WalletTransaction, error) {
   541  	coinID, err := hex.DecodeString(txID)
   542  	if err == nil {
   543  		txHash, _, err := decodeCoinID(coinID)
   544  		if err == nil {
   545  			txID = txHash.String()
   546  		}
   547  	}
   548  
   549  	txHistoryDB := btc.txDB()
   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  }