gitlab.com/jokerrs1/Sia@v1.3.2/modules/wallet/update.go (about)

     1  package wallet
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  
     7  	"github.com/NebulousLabs/Sia/modules"
     8  	"github.com/NebulousLabs/Sia/types"
     9  
    10  	"github.com/coreos/bbolt"
    11  )
    12  
    13  type (
    14  	spentSiacoinOutputSet map[types.SiacoinOutputID]types.SiacoinOutput
    15  	spentSiafundOutputSet map[types.SiafundOutputID]types.SiafundOutput
    16  )
    17  
    18  // threadedResetSubscriptions unsubscribes the wallet from the consensus set and transaction pool
    19  // and subscribes again.
    20  func (w *Wallet) threadedResetSubscriptions() error {
    21  	if !w.scanLock.TryLock() {
    22  		return errScanInProgress
    23  	}
    24  	defer w.scanLock.Unlock()
    25  
    26  	w.cs.Unsubscribe(w)
    27  	w.tpool.Unsubscribe(w)
    28  
    29  	err := w.cs.ConsensusSetSubscribe(w, modules.ConsensusChangeBeginning, w.tg.StopChan())
    30  	if err != nil {
    31  		return err
    32  	}
    33  	w.tpool.TransactionPoolSubscribe(w)
    34  	return nil
    35  }
    36  
    37  // advanceSeedLookahead generates all keys from the current primary seed progress up to index
    38  // and adds them to the set of spendable keys.  Therefore the new primary seed progress will
    39  // be index+1 and new lookahead keys will be generated starting from index+1
    40  // Returns true if a blockchain rescan is required
    41  func (w *Wallet) advanceSeedLookahead(index uint64) (bool, error) {
    42  	progress, err := dbGetPrimarySeedProgress(w.dbTx)
    43  	if err != nil {
    44  		return false, err
    45  	}
    46  	newProgress := index + 1
    47  
    48  	// Add spendable keys and remove them from lookahead
    49  	spendableKeys := generateKeys(w.primarySeed, progress, newProgress-progress)
    50  	for _, key := range spendableKeys {
    51  		w.keys[key.UnlockConditions.UnlockHash()] = key
    52  		delete(w.lookahead, key.UnlockConditions.UnlockHash())
    53  	}
    54  
    55  	// Update the primarySeedProgress
    56  	dbPutPrimarySeedProgress(w.dbTx, newProgress)
    57  	if err != nil {
    58  		return false, err
    59  	}
    60  
    61  	// Regenerate lookahead
    62  	w.regenerateLookahead(newProgress)
    63  
    64  	// If more than lookaheadRescanThreshold keys were generated
    65  	// also initialize a rescan just to be safe.
    66  	if uint64(len(spendableKeys)) > lookaheadRescanThreshold {
    67  		return true, nil
    68  	}
    69  
    70  	return false, nil
    71  }
    72  
    73  // isWalletAddress is a helper function that checks if an UnlockHash is
    74  // derived from one of the wallet's spendable keys or future keys.
    75  func (w *Wallet) isWalletAddress(uh types.UnlockHash) bool {
    76  	_, exists := w.keys[uh]
    77  	return exists
    78  }
    79  
    80  // updateLookahead uses a consensus change to update the seed progress if one of the outputs
    81  // contains an unlock hash of the lookahead set. Returns true if a blockchain rescan is required
    82  func (w *Wallet) updateLookahead(tx *bolt.Tx, cc modules.ConsensusChange) (bool, error) {
    83  	var largestIndex uint64
    84  	for _, diff := range cc.SiacoinOutputDiffs {
    85  		if index, ok := w.lookahead[diff.SiacoinOutput.UnlockHash]; ok {
    86  			if index > largestIndex {
    87  				largestIndex = index
    88  			}
    89  		}
    90  	}
    91  	for _, diff := range cc.SiafundOutputDiffs {
    92  		if index, ok := w.lookahead[diff.SiafundOutput.UnlockHash]; ok {
    93  			if index > largestIndex {
    94  				largestIndex = index
    95  			}
    96  		}
    97  	}
    98  	if largestIndex > 0 {
    99  		return w.advanceSeedLookahead(largestIndex)
   100  	}
   101  
   102  	return false, nil
   103  }
   104  
   105  // updateConfirmedSet uses a consensus change to update the confirmed set of
   106  // outputs as understood by the wallet.
   107  func (w *Wallet) updateConfirmedSet(tx *bolt.Tx, cc modules.ConsensusChange) error {
   108  	for _, diff := range cc.SiacoinOutputDiffs {
   109  		// Verify that the diff is relevant to the wallet.
   110  		if !w.isWalletAddress(diff.SiacoinOutput.UnlockHash) {
   111  			continue
   112  		}
   113  
   114  		var err error
   115  		if diff.Direction == modules.DiffApply {
   116  			w.log.Println("Wallet has gained a spendable siacoin output:", diff.ID, "::", diff.SiacoinOutput.Value.HumanString())
   117  			err = dbPutSiacoinOutput(tx, diff.ID, diff.SiacoinOutput)
   118  		} else {
   119  			w.log.Println("Wallet has lost a spendable siacoin output:", diff.ID, "::", diff.SiacoinOutput.Value.HumanString())
   120  			err = dbDeleteSiacoinOutput(tx, diff.ID)
   121  		}
   122  		if err != nil {
   123  			w.log.Severe("Could not update siacoin output:", err)
   124  		}
   125  	}
   126  	for _, diff := range cc.SiafundOutputDiffs {
   127  		// Verify that the diff is relevant to the wallet.
   128  		if !w.isWalletAddress(diff.SiafundOutput.UnlockHash) {
   129  			continue
   130  		}
   131  
   132  		var err error
   133  		if diff.Direction == modules.DiffApply {
   134  			w.log.Println("Wallet has gained a spendable siafund output:", diff.ID, "::", diff.SiafundOutput.Value)
   135  			err = dbPutSiafundOutput(tx, diff.ID, diff.SiafundOutput)
   136  		} else {
   137  			w.log.Println("Wallet has lost a spendable siafund output:", diff.ID, "::", diff.SiafundOutput.Value)
   138  			err = dbDeleteSiafundOutput(tx, diff.ID)
   139  		}
   140  		if err != nil {
   141  			w.log.Severe("Could not update siafund output:", err)
   142  		}
   143  	}
   144  	for _, diff := range cc.SiafundPoolDiffs {
   145  		var err error
   146  		if diff.Direction == modules.DiffApply {
   147  			err = dbPutSiafundPool(tx, diff.Adjusted)
   148  		} else {
   149  			err = dbPutSiafundPool(tx, diff.Previous)
   150  		}
   151  		if err != nil {
   152  			w.log.Severe("Could not update siafund pool:", err)
   153  		}
   154  	}
   155  	return nil
   156  }
   157  
   158  // revertHistory reverts any transaction history that was destroyed by reverted
   159  // blocks in the consensus change.
   160  func (w *Wallet) revertHistory(tx *bolt.Tx, reverted []types.Block) error {
   161  	for _, block := range reverted {
   162  		// Remove any transactions that have been reverted.
   163  		for i := len(block.Transactions) - 1; i >= 0; i-- {
   164  			// If the transaction is relevant to the wallet, it will be the
   165  			// most recent transaction in bucketProcessedTransactions.
   166  			txid := block.Transactions[i].ID()
   167  			pt, err := dbGetLastProcessedTransaction(tx)
   168  			if err != nil {
   169  				break // bucket is empty
   170  			}
   171  			if txid == pt.TransactionID {
   172  				w.log.Println("A wallet transaction has been reverted due to a reorg:", txid)
   173  				if err := dbDeleteLastProcessedTransaction(tx); err != nil {
   174  					w.log.Severe("Could not revert transaction:", err)
   175  				}
   176  			}
   177  		}
   178  
   179  		// Remove the miner payout transaction if applicable.
   180  		for i, mp := range block.MinerPayouts {
   181  			if w.isWalletAddress(mp.UnlockHash) {
   182  				w.log.Println("Miner payout has been reverted due to a reorg:", block.MinerPayoutID(uint64(i)), "::", mp.Value.HumanString())
   183  				if err := dbDeleteLastProcessedTransaction(tx); err != nil {
   184  					w.log.Severe("Could not revert transaction:", err)
   185  				}
   186  				break // there will only ever be one miner transaction
   187  			}
   188  		}
   189  
   190  		// decrement the consensus height
   191  		if block.ID() != types.GenesisID {
   192  			consensusHeight, err := dbGetConsensusHeight(tx)
   193  			if err != nil {
   194  				return err
   195  			}
   196  			err = dbPutConsensusHeight(tx, consensusHeight-1)
   197  			if err != nil {
   198  				return err
   199  			}
   200  		}
   201  	}
   202  	return nil
   203  }
   204  
   205  // outputs and collects them in a map of SiacoinOutputID -> SiacoinOutput.
   206  func computeSpentSiacoinOutputSet(diffs []modules.SiacoinOutputDiff) spentSiacoinOutputSet {
   207  	outputs := make(spentSiacoinOutputSet)
   208  	for _, diff := range diffs {
   209  		if diff.Direction == modules.DiffRevert {
   210  			// DiffRevert means spent.
   211  			outputs[diff.ID] = diff.SiacoinOutput
   212  		}
   213  	}
   214  	return outputs
   215  }
   216  
   217  // computeSpentSiafundOutputSet scans a slice of Siafund output diffs for spent
   218  // outputs and collects them in a map of SiafundOutputID -> SiafundOutput.
   219  func computeSpentSiafundOutputSet(diffs []modules.SiafundOutputDiff) spentSiafundOutputSet {
   220  	outputs := make(spentSiafundOutputSet)
   221  	for _, diff := range diffs {
   222  		if diff.Direction == modules.DiffRevert {
   223  			// DiffRevert means spent.
   224  			outputs[diff.ID] = diff.SiafundOutput
   225  		}
   226  	}
   227  	return outputs
   228  }
   229  
   230  // computeProcessedTransactionsFromBlock searches all the miner payouts and
   231  // transactions in a block and computes a ProcessedTransaction slice containing
   232  // all of the transactions processed for the given block.
   233  func (w *Wallet) computeProcessedTransactionsFromBlock(tx *bolt.Tx, block types.Block, spentSiacoinOutputs spentSiacoinOutputSet, spentSiafundOutputs spentSiafundOutputSet, consensusHeight types.BlockHeight) []modules.ProcessedTransaction {
   234  	var pts []modules.ProcessedTransaction
   235  
   236  	// Find ProcessedTransactions from miner payouts.
   237  	relevant := false
   238  	for _, mp := range block.MinerPayouts {
   239  		relevant = relevant || w.isWalletAddress(mp.UnlockHash)
   240  	}
   241  	if relevant {
   242  		w.log.Println("Wallet has received new miner payouts:", block.ID())
   243  		// Apply the miner payout transaction if applicable.
   244  		minerPT := modules.ProcessedTransaction{
   245  			Transaction:           types.Transaction{},
   246  			TransactionID:         types.TransactionID(block.ID()),
   247  			ConfirmationHeight:    consensusHeight,
   248  			ConfirmationTimestamp: block.Timestamp,
   249  		}
   250  		for i, mp := range block.MinerPayouts {
   251  			w.log.Println("\tminer payout:", block.MinerPayoutID(uint64(i)), "::", mp.Value.HumanString())
   252  			minerPT.Outputs = append(minerPT.Outputs, modules.ProcessedOutput{
   253  				ID:             types.OutputID(block.MinerPayoutID(uint64(i))),
   254  				FundType:       types.SpecifierMinerPayout,
   255  				MaturityHeight: consensusHeight + types.MaturityDelay,
   256  				WalletAddress:  w.isWalletAddress(mp.UnlockHash),
   257  				RelatedAddress: mp.UnlockHash,
   258  				Value:          mp.Value,
   259  			})
   260  		}
   261  		pts = append(pts, minerPT)
   262  	}
   263  
   264  	// Find ProcessedTransactions from transactions.
   265  	for _, txn := range block.Transactions {
   266  		// Determine if transaction is relevant.
   267  		relevant := false
   268  		for _, sci := range txn.SiacoinInputs {
   269  			relevant = relevant || w.isWalletAddress(sci.UnlockConditions.UnlockHash())
   270  		}
   271  		for _, sco := range txn.SiacoinOutputs {
   272  			relevant = relevant || w.isWalletAddress(sco.UnlockHash)
   273  		}
   274  		for _, sfi := range txn.SiafundInputs {
   275  			relevant = relevant || w.isWalletAddress(sfi.UnlockConditions.UnlockHash())
   276  		}
   277  		for _, sfo := range txn.SiafundOutputs {
   278  			relevant = relevant || w.isWalletAddress(sfo.UnlockHash)
   279  		}
   280  
   281  		// Only create a ProcessedTransaction if transaction is relevant.
   282  		if !relevant {
   283  			continue
   284  		}
   285  		w.log.Println("A transaction has been confirmed on the blockchain:", txn.ID())
   286  
   287  		pt := modules.ProcessedTransaction{
   288  			Transaction:           txn,
   289  			TransactionID:         txn.ID(),
   290  			ConfirmationHeight:    consensusHeight,
   291  			ConfirmationTimestamp: block.Timestamp,
   292  		}
   293  
   294  		for _, sci := range txn.SiacoinInputs {
   295  			pi := modules.ProcessedInput{
   296  				ParentID:       types.OutputID(sci.ParentID),
   297  				FundType:       types.SpecifierSiacoinInput,
   298  				WalletAddress:  w.isWalletAddress(sci.UnlockConditions.UnlockHash()),
   299  				RelatedAddress: sci.UnlockConditions.UnlockHash(),
   300  				Value:          spentSiacoinOutputs[sci.ParentID].Value,
   301  			}
   302  			pt.Inputs = append(pt.Inputs, pi)
   303  
   304  			// Log any wallet-relevant inputs.
   305  			if pi.WalletAddress {
   306  				w.log.Println("\tSiacoin Input:", pi.ParentID, "::", pi.Value.HumanString())
   307  			}
   308  		}
   309  
   310  		for i, sco := range txn.SiacoinOutputs {
   311  			po := modules.ProcessedOutput{
   312  				ID:             types.OutputID(txn.SiacoinOutputID(uint64(i))),
   313  				FundType:       types.SpecifierSiacoinOutput,
   314  				MaturityHeight: consensusHeight,
   315  				WalletAddress:  w.isWalletAddress(sco.UnlockHash),
   316  				RelatedAddress: sco.UnlockHash,
   317  				Value:          sco.Value,
   318  			}
   319  			pt.Outputs = append(pt.Outputs, po)
   320  
   321  			// Log any wallet-relevant outputs.
   322  			if po.WalletAddress {
   323  				w.log.Println("\tSiacoin Output:", po.ID, "::", po.Value.HumanString())
   324  			}
   325  		}
   326  
   327  		for _, sfi := range txn.SiafundInputs {
   328  			pi := modules.ProcessedInput{
   329  				ParentID:       types.OutputID(sfi.ParentID),
   330  				FundType:       types.SpecifierSiafundInput,
   331  				WalletAddress:  w.isWalletAddress(sfi.UnlockConditions.UnlockHash()),
   332  				RelatedAddress: sfi.UnlockConditions.UnlockHash(),
   333  				Value:          spentSiafundOutputs[sfi.ParentID].Value,
   334  			}
   335  			pt.Inputs = append(pt.Inputs, pi)
   336  			// Log any wallet-relevant inputs.
   337  			if pi.WalletAddress {
   338  				w.log.Println("\tSiafund Input:", pi.ParentID, "::", pi.Value.HumanString())
   339  			}
   340  
   341  			siafundPool, err := dbGetSiafundPool(w.dbTx)
   342  			if err != nil {
   343  				w.log.Println("could not get siafund pool: ", err)
   344  				continue
   345  			}
   346  
   347  			sfo := spentSiafundOutputs[sfi.ParentID]
   348  			po := modules.ProcessedOutput{
   349  				ID:             types.OutputID(sfi.ParentID),
   350  				FundType:       types.SpecifierClaimOutput,
   351  				MaturityHeight: consensusHeight + types.MaturityDelay,
   352  				WalletAddress:  w.isWalletAddress(sfi.UnlockConditions.UnlockHash()),
   353  				RelatedAddress: sfi.ClaimUnlockHash,
   354  				Value:          siafundPool.Sub(sfo.ClaimStart).Mul(sfo.Value),
   355  			}
   356  			pt.Outputs = append(pt.Outputs, po)
   357  			// Log any wallet-relevant outputs.
   358  			if po.WalletAddress {
   359  				w.log.Println("\tClaim Output:", po.ID, "::", po.Value.HumanString())
   360  			}
   361  		}
   362  
   363  		for i, sfo := range txn.SiafundOutputs {
   364  			po := modules.ProcessedOutput{
   365  				ID:             types.OutputID(txn.SiafundOutputID(uint64(i))),
   366  				FundType:       types.SpecifierSiafundOutput,
   367  				MaturityHeight: consensusHeight,
   368  				WalletAddress:  w.isWalletAddress(sfo.UnlockHash),
   369  				RelatedAddress: sfo.UnlockHash,
   370  				Value:          sfo.Value,
   371  			}
   372  			pt.Outputs = append(pt.Outputs, po)
   373  			// Log any wallet-relevant outputs.
   374  			if po.WalletAddress {
   375  				w.log.Println("\tSiafund Output:", po.ID, "::", po.Value.HumanString())
   376  			}
   377  		}
   378  
   379  		for _, fee := range txn.MinerFees {
   380  			pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
   381  				FundType: types.SpecifierMinerFee,
   382  				Value:    fee,
   383  			})
   384  		}
   385  		pts = append(pts, pt)
   386  	}
   387  	return pts
   388  }
   389  
   390  // applyHistory applies any transaction history that the applied blocks
   391  // introduced.
   392  func (w *Wallet) applyHistory(tx *bolt.Tx, cc modules.ConsensusChange) error {
   393  	spentSiacoinOutputs := computeSpentSiacoinOutputSet(cc.SiacoinOutputDiffs)
   394  	spentSiafundOutputs := computeSpentSiafundOutputSet(cc.SiafundOutputDiffs)
   395  
   396  	for _, block := range cc.AppliedBlocks {
   397  		consensusHeight, err := dbGetConsensusHeight(tx)
   398  		if err != nil {
   399  			return err
   400  		}
   401  		// Increment the consensus height.
   402  		if block.ID() != types.GenesisID {
   403  			consensusHeight++
   404  			err = dbPutConsensusHeight(tx, consensusHeight)
   405  			if err != nil {
   406  				return err
   407  			}
   408  		}
   409  
   410  		pts := w.computeProcessedTransactionsFromBlock(tx, block, spentSiacoinOutputs, spentSiafundOutputs, consensusHeight)
   411  		for _, pt := range pts {
   412  			err := dbAppendProcessedTransaction(tx, pt)
   413  			if err != nil {
   414  				return fmt.Errorf("could not put processed transaction: %v", err)
   415  			}
   416  		}
   417  	}
   418  
   419  	return nil
   420  }
   421  
   422  // ProcessConsensusChange parses a consensus change to update the set of
   423  // confirmed outputs known to the wallet.
   424  func (w *Wallet) ProcessConsensusChange(cc modules.ConsensusChange) {
   425  	if err := w.tg.Add(); err != nil {
   426  		return
   427  	}
   428  	defer w.tg.Done()
   429  
   430  	w.mu.Lock()
   431  	defer w.mu.Unlock()
   432  
   433  	if needRescan, err := w.updateLookahead(w.dbTx, cc); err != nil {
   434  		w.log.Println("ERROR: failed to update lookahead:", err)
   435  	} else if needRescan {
   436  		go w.threadedResetSubscriptions()
   437  	}
   438  	if err := w.updateConfirmedSet(w.dbTx, cc); err != nil {
   439  		w.log.Println("ERROR: failed to update confirmed set:", err)
   440  	}
   441  	if err := w.revertHistory(w.dbTx, cc.RevertedBlocks); err != nil {
   442  		w.log.Println("ERROR: failed to revert consensus change:", err)
   443  	}
   444  	if err := w.applyHistory(w.dbTx, cc); err != nil {
   445  		w.log.Println("ERROR: failed to apply consensus change:", err)
   446  	}
   447  	if err := dbPutConsensusChangeID(w.dbTx, cc.ID); err != nil {
   448  		w.log.Println("ERROR: failed to update consensus change ID:", err)
   449  	}
   450  
   451  	if cc.Synced {
   452  		go w.threadedDefragWallet()
   453  	}
   454  }
   455  
   456  // ReceiveUpdatedUnconfirmedTransactions updates the wallet's unconfirmed
   457  // transaction set.
   458  func (w *Wallet) ReceiveUpdatedUnconfirmedTransactions(diff *modules.TransactionPoolDiff) {
   459  	if err := w.tg.Add(); err != nil {
   460  		return
   461  	}
   462  	defer w.tg.Done()
   463  
   464  	w.mu.Lock()
   465  	defer w.mu.Unlock()
   466  
   467  	// Do the pruning first. If there are any pruned transactions, we will need
   468  	// to re-allocate the whole processed transactions array.
   469  	droppedTransactions := make(map[types.TransactionID]struct{})
   470  	for i := range diff.RevertedTransactions {
   471  		txids := w.unconfirmedSets[diff.RevertedTransactions[i]]
   472  		for i := range txids {
   473  			droppedTransactions[txids[i]] = struct{}{}
   474  		}
   475  		delete(w.unconfirmedSets, diff.RevertedTransactions[i])
   476  	}
   477  
   478  	// Skip the reallocation if we can, otherwise reallocate the
   479  	// unconfirmedProcessedTransactions to no longer have the dropped
   480  	// transactions.
   481  	if len(droppedTransactions) != 0 {
   482  		// Capacity can't be reduced, because we have no way of knowing if the
   483  		// dropped transactions are relevant to the wallet or not, and some will
   484  		// not be relevant to the wallet, meaning they don't have a counterpart
   485  		// in w.unconfirmedProcessedTransactions.
   486  		newUPT := make([]modules.ProcessedTransaction, 0, len(w.unconfirmedProcessedTransactions))
   487  		for _, txn := range w.unconfirmedProcessedTransactions {
   488  			_, exists := droppedTransactions[txn.TransactionID]
   489  			if !exists {
   490  				// Transaction was not dropped, add it to the new unconfirmed
   491  				// transactions.
   492  				newUPT = append(newUPT, txn)
   493  			}
   494  		}
   495  
   496  		// Set the unconfirmed preocessed transactions to the pruned set.
   497  		w.unconfirmedProcessedTransactions = newUPT
   498  	}
   499  
   500  	// Scroll through all of the diffs and add any new transactions.
   501  	for _, unconfirmedTxnSet := range diff.AppliedTransactions {
   502  		// Mark all of the transactions that appeared in this set.
   503  		//
   504  		// TODO: Technically only necessary to mark the ones that are relevant
   505  		// to the wallet, but overhead should be low.
   506  		w.unconfirmedSets[unconfirmedTxnSet.ID] = unconfirmedTxnSet.IDs
   507  
   508  		// Get the values for the spent outputs.
   509  		spentSiacoinOutputs := make(map[types.SiacoinOutputID]types.SiacoinOutput)
   510  		for _, scod := range unconfirmedTxnSet.Change.SiacoinOutputDiffs {
   511  			// Only need to grab the reverted ones, because only reverted ones
   512  			// have the possibility of having been spent.
   513  			if scod.Direction == modules.DiffRevert {
   514  				spentSiacoinOutputs[scod.ID] = scod.SiacoinOutput
   515  			}
   516  		}
   517  
   518  		// Add each transaction to our set of unconfirmed transactions.
   519  		for i, txn := range unconfirmedTxnSet.Transactions {
   520  			// determine whether transaction is relevant to the wallet
   521  			relevant := false
   522  			for _, sci := range txn.SiacoinInputs {
   523  				relevant = relevant || w.isWalletAddress(sci.UnlockConditions.UnlockHash())
   524  			}
   525  			for _, sco := range txn.SiacoinOutputs {
   526  				relevant = relevant || w.isWalletAddress(sco.UnlockHash)
   527  			}
   528  
   529  			// only create a ProcessedTransaction if txn is relevant
   530  			if !relevant {
   531  				continue
   532  			}
   533  
   534  			pt := modules.ProcessedTransaction{
   535  				Transaction:           txn,
   536  				TransactionID:         unconfirmedTxnSet.IDs[i],
   537  				ConfirmationHeight:    types.BlockHeight(math.MaxUint64),
   538  				ConfirmationTimestamp: types.Timestamp(math.MaxUint64),
   539  			}
   540  			for _, sci := range txn.SiacoinInputs {
   541  				pt.Inputs = append(pt.Inputs, modules.ProcessedInput{
   542  					ParentID:       types.OutputID(sci.ParentID),
   543  					FundType:       types.SpecifierSiacoinInput,
   544  					WalletAddress:  w.isWalletAddress(sci.UnlockConditions.UnlockHash()),
   545  					RelatedAddress: sci.UnlockConditions.UnlockHash(),
   546  					Value:          spentSiacoinOutputs[sci.ParentID].Value,
   547  				})
   548  			}
   549  			for i, sco := range txn.SiacoinOutputs {
   550  				pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
   551  					ID:             types.OutputID(txn.SiacoinOutputID(uint64(i))),
   552  					FundType:       types.SpecifierSiacoinOutput,
   553  					MaturityHeight: types.BlockHeight(math.MaxUint64),
   554  					WalletAddress:  w.isWalletAddress(sco.UnlockHash),
   555  					RelatedAddress: sco.UnlockHash,
   556  					Value:          sco.Value,
   557  				})
   558  			}
   559  			for _, fee := range txn.MinerFees {
   560  				pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
   561  					FundType: types.SpecifierMinerFee,
   562  					Value:    fee,
   563  				})
   564  			}
   565  			w.unconfirmedProcessedTransactions = append(w.unconfirmedProcessedTransactions, pt)
   566  		}
   567  	}
   568  }