gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/wallet/transactions.go (about)

     1  package wallet
     2  
     3  import (
     4  	"encoding/binary"
     5  	"errors"
     6  	"fmt"
     7  	"sort"
     8  
     9  	"gitlab.com/SiaPrime/SiaPrime/build"
    10  	"gitlab.com/SiaPrime/SiaPrime/encoding"
    11  	"gitlab.com/SiaPrime/SiaPrime/modules"
    12  	"gitlab.com/SiaPrime/SiaPrime/types"
    13  )
    14  
    15  var (
    16  	errOutOfBounds = errors.New("requesting transactions at unknown confirmation heights")
    17  )
    18  
    19  // AddressTransactions returns all of the wallet transactions associated with a
    20  // single unlock hash.
    21  func (w *Wallet) AddressTransactions(uh types.UnlockHash) (pts []modules.ProcessedTransaction, err error) {
    22  	if err := w.tg.Add(); err != nil {
    23  		return []modules.ProcessedTransaction{}, err
    24  	}
    25  	defer w.tg.Done()
    26  	// ensure durability of reported transactions
    27  	w.mu.Lock()
    28  	defer w.mu.Unlock()
    29  	if err = w.syncDB(); err != nil {
    30  		return
    31  	}
    32  
    33  	txnIndices, _ := dbGetAddrTransactions(w.dbTx, uh)
    34  	for _, i := range txnIndices {
    35  		pt, err := dbGetProcessedTransaction(w.dbTx, i)
    36  		if err != nil {
    37  			continue
    38  		}
    39  		pts = append(pts, pt)
    40  	}
    41  	return pts, nil
    42  }
    43  
    44  // AddressUnconfirmedTransactions returns all of the unconfirmed wallet transactions
    45  // related to a specific address.
    46  func (w *Wallet) AddressUnconfirmedTransactions(uh types.UnlockHash) (pts []modules.ProcessedTransaction, err error) {
    47  	if err := w.tg.Add(); err != nil {
    48  		return []modules.ProcessedTransaction{}, err
    49  	}
    50  	defer w.tg.Done()
    51  	// ensure durability of reported transactions
    52  	w.mu.Lock()
    53  	defer w.mu.Unlock()
    54  	if err = w.syncDB(); err != nil {
    55  		return
    56  	}
    57  
    58  	// Scan the full list of unconfirmed transactions to see if there are any
    59  	// related transactions.
    60  	for _, pt := range w.unconfirmedProcessedTransactions {
    61  		relevant := false
    62  		for _, input := range pt.Inputs {
    63  			if input.RelatedAddress == uh {
    64  				relevant = true
    65  				break
    66  			}
    67  		}
    68  		for _, output := range pt.Outputs {
    69  			if output.RelatedAddress == uh {
    70  				relevant = true
    71  				break
    72  			}
    73  		}
    74  		if relevant {
    75  			pts = append(pts, pt)
    76  		}
    77  	}
    78  	return pts, err
    79  }
    80  
    81  // Transaction returns the transaction with the given id. 'False' is returned
    82  // if the transaction does not exist.
    83  func (w *Wallet) Transaction(txid types.TransactionID) (pt modules.ProcessedTransaction, found bool, err error) {
    84  	if err := w.tg.Add(); err != nil {
    85  		return modules.ProcessedTransaction{}, false, err
    86  	}
    87  	defer w.tg.Done()
    88  	// ensure durability of reported transaction
    89  	w.mu.Lock()
    90  	defer w.mu.Unlock()
    91  	if err = w.syncDB(); err != nil {
    92  		return
    93  	}
    94  
    95  	// Get the keyBytes for the given txid
    96  	keyBytes, err := dbGetTransactionIndex(w.dbTx, txid)
    97  	if err != nil {
    98  		for _, txn := range w.unconfirmedProcessedTransactions {
    99  			if txn.TransactionID == txid {
   100  				return txn, true, nil
   101  			}
   102  		}
   103  		return modules.ProcessedTransaction{}, false, nil
   104  	}
   105  
   106  	// Retrieve the transaction
   107  	found = encoding.Unmarshal(w.dbTx.Bucket(bucketProcessedTransactions).Get(keyBytes), &pt) == nil
   108  	return
   109  }
   110  
   111  // Transactions returns all transactions relevant to the wallet that were
   112  // confirmed in the range [startHeight, endHeight].
   113  func (w *Wallet) Transactions(startHeight, endHeight types.BlockHeight) (pts []modules.ProcessedTransaction, err error) {
   114  	if err := w.tg.Add(); err != nil {
   115  		return nil, err
   116  	}
   117  	defer w.tg.Done()
   118  
   119  	// There may be transactions which haven't been saved / committed yet. Sync
   120  	// the database to ensure that any information which gets reported to the
   121  	// user will be persisted through a restart.
   122  	w.mu.Lock()
   123  	defer w.mu.Unlock()
   124  	if err = w.syncDB(); err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	height, err := dbGetConsensusHeight(w.dbTx)
   129  	if err != nil {
   130  		return
   131  	} else if startHeight > height || startHeight > endHeight {
   132  		return nil, errOutOfBounds
   133  	}
   134  
   135  	// Get the bucket, the largest key in it and the cursor
   136  	bucket := w.dbTx.Bucket(bucketProcessedTransactions)
   137  	cursor := bucket.Cursor()
   138  	nextKey := bucket.Sequence() + 1
   139  
   140  	// Database is empty
   141  	if nextKey == 1 {
   142  		return
   143  	}
   144  
   145  	var pt modules.ProcessedTransaction
   146  	keyBytes := make([]byte, 8)
   147  	var result int
   148  	func() {
   149  		// Recover from possible panic during binary search
   150  		defer func() {
   151  			r := recover()
   152  			if r != nil {
   153  				err = fmt.Errorf("%v", r)
   154  			}
   155  		}()
   156  
   157  		// Start binary searching
   158  		result = sort.Search(int(nextKey), func(i int) bool {
   159  			// Create the key for the index
   160  			binary.BigEndian.PutUint64(keyBytes, uint64(i))
   161  
   162  			// Retrieve the processed transaction
   163  			key, ptBytes := cursor.Seek(keyBytes)
   164  			if build.DEBUG && key == nil {
   165  				panic("Failed to retrieve processed Transaction by key")
   166  			}
   167  
   168  			// Decode the transaction
   169  			if err = decodeProcessedTransaction(ptBytes, &pt); build.DEBUG && err != nil {
   170  				panic(err)
   171  			}
   172  
   173  			return pt.ConfirmationHeight >= startHeight
   174  		})
   175  	}()
   176  	if err != nil {
   177  		return
   178  	}
   179  
   180  	if uint64(result) == nextKey {
   181  		// No transaction was found
   182  		return
   183  	}
   184  
   185  	// Create the key that corresponds to the result of the search
   186  	binary.BigEndian.PutUint64(keyBytes, uint64(result))
   187  
   188  	// Get the processed transaction and decode it
   189  	key, ptBytes := cursor.Seek(keyBytes)
   190  	if build.DEBUG && key == nil {
   191  		build.Critical("Couldn't find the processed transaction from the search.")
   192  	}
   193  	if err = decodeProcessedTransaction(ptBytes, &pt); build.DEBUG && err != nil {
   194  		build.Critical(err)
   195  	}
   196  
   197  	// Gather all transactions until endHeight is reached
   198  	for pt.ConfirmationHeight <= endHeight {
   199  		if build.DEBUG && pt.ConfirmationHeight < startHeight {
   200  			build.Critical("wallet processed transactions are not sorted")
   201  		}
   202  		pts = append(pts, pt)
   203  
   204  		// Get next processed transaction
   205  		key, ptBytes := cursor.Next()
   206  		if key == nil {
   207  			break
   208  		}
   209  
   210  		// Decode the transaction
   211  		if err := decodeProcessedTransaction(ptBytes, &pt); build.DEBUG && err != nil {
   212  			panic("Failed to decode the processed transaction")
   213  		}
   214  	}
   215  	return
   216  }
   217  
   218  // ComputeValuedTransactions creates ValuedTransaction from a set of
   219  // ProcessedTransactions.
   220  func ComputeValuedTransactions(pts []modules.ProcessedTransaction, blockHeight types.BlockHeight) ([]modules.ValuedTransaction, error) {
   221  	// Loop over all transactions and map the id of each contract to the most
   222  	// recent revision within the set.
   223  	revisionMap := make(map[types.FileContractID]types.FileContractRevision)
   224  	for _, pt := range pts {
   225  		for _, rev := range pt.Transaction.FileContractRevisions {
   226  			revisionMap[rev.ParentID] = rev
   227  		}
   228  	}
   229  	sts := make([]modules.ValuedTransaction, 0, len(pts))
   230  	for _, pt := range pts {
   231  		// Determine the value of the transaction assuming that it's a regular
   232  		// transaction.
   233  		var outgoingSiacoins types.Currency
   234  		for _, input := range pt.Inputs {
   235  			if input.FundType == types.SpecifierSiacoinInput && input.WalletAddress {
   236  				outgoingSiacoins = outgoingSiacoins.Add(input.Value)
   237  			}
   238  		}
   239  		var incomingSiacoins types.Currency
   240  		for _, output := range pt.Outputs {
   241  			if output.FundType == types.SpecifierMinerPayout && output.WalletAddress {
   242  				incomingSiacoins = incomingSiacoins.Add(output.Value)
   243  			}
   244  			if output.FundType == types.SpecifierSiacoinOutput && output.WalletAddress {
   245  				incomingSiacoins = incomingSiacoins.Add(output.Value)
   246  			}
   247  		}
   248  		// Create the txn assuming that it's a regular txn without contracts or
   249  		// revisions.
   250  		st := modules.ValuedTransaction{
   251  			ProcessedTransaction:   pt,
   252  			ConfirmedIncomingValue: incomingSiacoins,
   253  			ConfirmedOutgoingValue: outgoingSiacoins,
   254  		}
   255  		// If the transaction doesn't contain contracts or revisions we are done.
   256  		if len(pt.Transaction.FileContracts) == 0 && len(pt.Transaction.FileContractRevisions) == 0 {
   257  			sts = append(sts, st)
   258  			continue
   259  		}
   260  		// If there are contracts, then there can't be revisions in the
   261  		// transaction.
   262  		if len(pt.Transaction.FileContracts) > 0 {
   263  			// A contract doesn't generate incoming value for the wallet.
   264  			st.ConfirmedIncomingValue = types.ZeroCurrency
   265  			// A contract with a revision doesn't have outgoing value since the
   266  			// outgoing value is determined by the latest revision.
   267  			_, hasRevision := revisionMap[pt.Transaction.FileContractID(0)]
   268  			if hasRevision {
   269  				st.ConfirmedOutgoingValue = types.ZeroCurrency
   270  			}
   271  			sts = append(sts, st)
   272  			continue
   273  		}
   274  		// Else the contract contains a revision.
   275  		rev := pt.Transaction.FileContractRevisions[0]
   276  		latestRev, ok := revisionMap[rev.ParentID]
   277  		if !ok {
   278  			err := errors.New("no revision exists for the provided id which should never happen")
   279  			build.Critical(err)
   280  			return nil, err
   281  		}
   282  		// If the revision isn't the latest one, it has neither incoming nor
   283  		// outgoing value.
   284  		if rev.NewRevisionNumber != latestRev.NewRevisionNumber {
   285  			st.ConfirmedIncomingValue = types.ZeroCurrency
   286  			st.ConfirmedOutgoingValue = types.ZeroCurrency
   287  			sts = append(sts, st)
   288  			continue
   289  		}
   290  		// It is the latest but if it hasn't reached maturiy height yet we
   291  		// don't count the incoming value.
   292  		if blockHeight <= rev.NewWindowEnd+types.MaturityDelay {
   293  			st.ConfirmedIncomingValue = types.ZeroCurrency
   294  			sts = append(sts, st)
   295  			continue
   296  		}
   297  		// Otherwise we leave the incoming and outgoing value fields the way
   298  		// they are.
   299  		sts = append(sts, st)
   300  		continue
   301  	}
   302  	return sts, nil
   303  }
   304  
   305  // UnconfirmedTransactions returns the set of unconfirmed transactions that are
   306  // relevant to the wallet.
   307  func (w *Wallet) UnconfirmedTransactions() ([]modules.ProcessedTransaction, error) {
   308  	if err := w.tg.Add(); err != nil {
   309  		return nil, err
   310  	}
   311  	defer w.tg.Done()
   312  	w.mu.RLock()
   313  	defer w.mu.RUnlock()
   314  	return w.unconfirmedProcessedTransactions, nil
   315  }