github.com/Synthesix/Sia@v1.3.3-0.20180413141344-f863baeed3ca/modules/wallet/update.go (about)

     1  package wallet
     2  
     3  import (
     4  	"math"
     5  
     6  	"github.com/Synthesix/Sia/modules"
     7  	"github.com/Synthesix/Sia/types"
     8  	"github.com/NebulousLabs/errors"
     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  			return err
   125  		}
   126  	}
   127  	for _, diff := range cc.SiafundOutputDiffs {
   128  		// Verify that the diff is relevant to the wallet.
   129  		if !w.isWalletAddress(diff.SiafundOutput.UnlockHash) {
   130  			continue
   131  		}
   132  
   133  		var err error
   134  		if diff.Direction == modules.DiffApply {
   135  			w.log.Println("Wallet has gained a spendable siafund output:", diff.ID, "::", diff.SiafundOutput.Value)
   136  			err = dbPutSiafundOutput(tx, diff.ID, diff.SiafundOutput)
   137  		} else {
   138  			w.log.Println("Wallet has lost a spendable siafund output:", diff.ID, "::", diff.SiafundOutput.Value)
   139  			err = dbDeleteSiafundOutput(tx, diff.ID)
   140  		}
   141  		if err != nil {
   142  			w.log.Severe("Could not update siafund output:", err)
   143  			return err
   144  		}
   145  	}
   146  	for _, diff := range cc.SiafundPoolDiffs {
   147  		var err error
   148  		if diff.Direction == modules.DiffApply {
   149  			err = dbPutSiafundPool(tx, diff.Adjusted)
   150  		} else {
   151  			err = dbPutSiafundPool(tx, diff.Previous)
   152  		}
   153  		if err != nil {
   154  			w.log.Severe("Could not update siafund pool:", err)
   155  			return err
   156  		}
   157  	}
   158  	return nil
   159  }
   160  
   161  // revertHistory reverts any transaction history that was destroyed by reverted
   162  // blocks in the consensus change.
   163  func (w *Wallet) revertHistory(tx *bolt.Tx, reverted []types.Block) error {
   164  	for _, block := range reverted {
   165  		// Remove any transactions that have been reverted.
   166  		for i := len(block.Transactions) - 1; i >= 0; i-- {
   167  			// If the transaction is relevant to the wallet, it will be the
   168  			// most recent transaction in bucketProcessedTransactions.
   169  			txid := block.Transactions[i].ID()
   170  			pt, err := dbGetLastProcessedTransaction(tx)
   171  			if err != nil {
   172  				break // bucket is empty
   173  			}
   174  			if txid == pt.TransactionID {
   175  				w.log.Println("A wallet transaction has been reverted due to a reorg:", txid)
   176  				if err := dbDeleteLastProcessedTransaction(tx); err != nil {
   177  					w.log.Severe("Could not revert transaction:", err)
   178  					return err
   179  				}
   180  			}
   181  		}
   182  
   183  		// Remove the miner payout transaction if applicable.
   184  		for i, mp := range block.MinerPayouts {
   185  			if w.isWalletAddress(mp.UnlockHash) {
   186  				w.log.Println("Miner payout has been reverted due to a reorg:", block.MinerPayoutID(uint64(i)), "::", mp.Value.HumanString())
   187  				if err := dbDeleteLastProcessedTransaction(tx); err != nil {
   188  					w.log.Severe("Could not revert transaction:", err)
   189  					return err
   190  				}
   191  				break // there will only ever be one miner transaction
   192  			}
   193  		}
   194  
   195  		// decrement the consensus height
   196  		if block.ID() != types.GenesisID {
   197  			consensusHeight, err := dbGetConsensusHeight(tx)
   198  			if err != nil {
   199  				return err
   200  			}
   201  			err = dbPutConsensusHeight(tx, consensusHeight-1)
   202  			if err != nil {
   203  				return err
   204  			}
   205  		}
   206  	}
   207  	return nil
   208  }
   209  
   210  // outputs and collects them in a map of SiacoinOutputID -> SiacoinOutput.
   211  func computeSpentSiacoinOutputSet(diffs []modules.SiacoinOutputDiff) spentSiacoinOutputSet {
   212  	outputs := make(spentSiacoinOutputSet)
   213  	for _, diff := range diffs {
   214  		if diff.Direction == modules.DiffRevert {
   215  			// DiffRevert means spent.
   216  			outputs[diff.ID] = diff.SiacoinOutput
   217  		}
   218  	}
   219  	return outputs
   220  }
   221  
   222  // computeSpentSiafundOutputSet scans a slice of Siafund output diffs for spent
   223  // outputs and collects them in a map of SiafundOutputID -> SiafundOutput.
   224  func computeSpentSiafundOutputSet(diffs []modules.SiafundOutputDiff) spentSiafundOutputSet {
   225  	outputs := make(spentSiafundOutputSet)
   226  	for _, diff := range diffs {
   227  		if diff.Direction == modules.DiffRevert {
   228  			// DiffRevert means spent.
   229  			outputs[diff.ID] = diff.SiafundOutput
   230  		}
   231  	}
   232  	return outputs
   233  }
   234  
   235  // computeProcessedTransactionsFromBlock searches all the miner payouts and
   236  // transactions in a block and computes a ProcessedTransaction slice containing
   237  // all of the transactions processed for the given block.
   238  func (w *Wallet) computeProcessedTransactionsFromBlock(tx *bolt.Tx, block types.Block, spentSiacoinOutputs spentSiacoinOutputSet, spentSiafundOutputs spentSiafundOutputSet, consensusHeight types.BlockHeight) []modules.ProcessedTransaction {
   239  	var pts []modules.ProcessedTransaction
   240  
   241  	// Find ProcessedTransactions from miner payouts.
   242  	relevant := false
   243  	for _, mp := range block.MinerPayouts {
   244  		relevant = relevant || w.isWalletAddress(mp.UnlockHash)
   245  	}
   246  	if relevant {
   247  		w.log.Println("Wallet has received new miner payouts:", block.ID())
   248  		// Apply the miner payout transaction if applicable.
   249  		minerPT := modules.ProcessedTransaction{
   250  			Transaction:           types.Transaction{},
   251  			TransactionID:         types.TransactionID(block.ID()),
   252  			ConfirmationHeight:    consensusHeight,
   253  			ConfirmationTimestamp: block.Timestamp,
   254  		}
   255  		for i, mp := range block.MinerPayouts {
   256  			w.log.Println("\tminer payout:", block.MinerPayoutID(uint64(i)), "::", mp.Value.HumanString())
   257  			minerPT.Outputs = append(minerPT.Outputs, modules.ProcessedOutput{
   258  				ID:             types.OutputID(block.MinerPayoutID(uint64(i))),
   259  				FundType:       types.SpecifierMinerPayout,
   260  				MaturityHeight: consensusHeight + types.MaturityDelay,
   261  				WalletAddress:  w.isWalletAddress(mp.UnlockHash),
   262  				RelatedAddress: mp.UnlockHash,
   263  				Value:          mp.Value,
   264  			})
   265  		}
   266  		pts = append(pts, minerPT)
   267  	}
   268  
   269  	// Find ProcessedTransactions from transactions.
   270  	for _, txn := range block.Transactions {
   271  		// Determine if transaction is relevant.
   272  		relevant := false
   273  		for _, sci := range txn.SiacoinInputs {
   274  			relevant = relevant || w.isWalletAddress(sci.UnlockConditions.UnlockHash())
   275  		}
   276  		for _, sco := range txn.SiacoinOutputs {
   277  			relevant = relevant || w.isWalletAddress(sco.UnlockHash)
   278  		}
   279  		for _, sfi := range txn.SiafundInputs {
   280  			relevant = relevant || w.isWalletAddress(sfi.UnlockConditions.UnlockHash())
   281  		}
   282  		for _, sfo := range txn.SiafundOutputs {
   283  			relevant = relevant || w.isWalletAddress(sfo.UnlockHash)
   284  		}
   285  
   286  		// Only create a ProcessedTransaction if transaction is relevant.
   287  		if !relevant {
   288  			continue
   289  		}
   290  		w.log.Println("A transaction has been confirmed on the blockchain:", txn.ID())
   291  
   292  		pt := modules.ProcessedTransaction{
   293  			Transaction:           txn,
   294  			TransactionID:         txn.ID(),
   295  			ConfirmationHeight:    consensusHeight,
   296  			ConfirmationTimestamp: block.Timestamp,
   297  		}
   298  
   299  		for _, sci := range txn.SiacoinInputs {
   300  			pi := modules.ProcessedInput{
   301  				ParentID:       types.OutputID(sci.ParentID),
   302  				FundType:       types.SpecifierSiacoinInput,
   303  				WalletAddress:  w.isWalletAddress(sci.UnlockConditions.UnlockHash()),
   304  				RelatedAddress: sci.UnlockConditions.UnlockHash(),
   305  				Value:          spentSiacoinOutputs[sci.ParentID].Value,
   306  			}
   307  			pt.Inputs = append(pt.Inputs, pi)
   308  
   309  			// Log any wallet-relevant inputs.
   310  			if pi.WalletAddress {
   311  				w.log.Println("\tSiacoin Input:", pi.ParentID, "::", pi.Value.HumanString())
   312  			}
   313  		}
   314  
   315  		for i, sco := range txn.SiacoinOutputs {
   316  			po := modules.ProcessedOutput{
   317  				ID:             types.OutputID(txn.SiacoinOutputID(uint64(i))),
   318  				FundType:       types.SpecifierSiacoinOutput,
   319  				MaturityHeight: consensusHeight,
   320  				WalletAddress:  w.isWalletAddress(sco.UnlockHash),
   321  				RelatedAddress: sco.UnlockHash,
   322  				Value:          sco.Value,
   323  			}
   324  			pt.Outputs = append(pt.Outputs, po)
   325  
   326  			// Log any wallet-relevant outputs.
   327  			if po.WalletAddress {
   328  				w.log.Println("\tSiacoin Output:", po.ID, "::", po.Value.HumanString())
   329  			}
   330  		}
   331  
   332  		for _, sfi := range txn.SiafundInputs {
   333  			pi := modules.ProcessedInput{
   334  				ParentID:       types.OutputID(sfi.ParentID),
   335  				FundType:       types.SpecifierSiafundInput,
   336  				WalletAddress:  w.isWalletAddress(sfi.UnlockConditions.UnlockHash()),
   337  				RelatedAddress: sfi.UnlockConditions.UnlockHash(),
   338  				Value:          spentSiafundOutputs[sfi.ParentID].Value,
   339  			}
   340  			pt.Inputs = append(pt.Inputs, pi)
   341  			// Log any wallet-relevant inputs.
   342  			if pi.WalletAddress {
   343  				w.log.Println("\tSiafund Input:", pi.ParentID, "::", pi.Value.HumanString())
   344  			}
   345  
   346  			siafundPool, err := dbGetSiafundPool(w.dbTx)
   347  			if err != nil {
   348  				w.log.Println("could not get siafund pool: ", err)
   349  				continue
   350  			}
   351  
   352  			sfo := spentSiafundOutputs[sfi.ParentID]
   353  			po := modules.ProcessedOutput{
   354  				ID:             types.OutputID(sfi.ParentID),
   355  				FundType:       types.SpecifierClaimOutput,
   356  				MaturityHeight: consensusHeight + types.MaturityDelay,
   357  				WalletAddress:  w.isWalletAddress(sfi.UnlockConditions.UnlockHash()),
   358  				RelatedAddress: sfi.ClaimUnlockHash,
   359  				Value:          siafundPool.Sub(sfo.ClaimStart).Mul(sfo.Value),
   360  			}
   361  			pt.Outputs = append(pt.Outputs, po)
   362  			// Log any wallet-relevant outputs.
   363  			if po.WalletAddress {
   364  				w.log.Println("\tClaim Output:", po.ID, "::", po.Value.HumanString())
   365  			}
   366  		}
   367  
   368  		for i, sfo := range txn.SiafundOutputs {
   369  			po := modules.ProcessedOutput{
   370  				ID:             types.OutputID(txn.SiafundOutputID(uint64(i))),
   371  				FundType:       types.SpecifierSiafundOutput,
   372  				MaturityHeight: consensusHeight,
   373  				WalletAddress:  w.isWalletAddress(sfo.UnlockHash),
   374  				RelatedAddress: sfo.UnlockHash,
   375  				Value:          sfo.Value,
   376  			}
   377  			pt.Outputs = append(pt.Outputs, po)
   378  			// Log any wallet-relevant outputs.
   379  			if po.WalletAddress {
   380  				w.log.Println("\tSiafund Output:", po.ID, "::", po.Value.HumanString())
   381  			}
   382  		}
   383  
   384  		for _, fee := range txn.MinerFees {
   385  			pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
   386  				FundType:       types.SpecifierMinerFee,
   387  				MaturityHeight: consensusHeight + types.MaturityDelay,
   388  				Value:          fee,
   389  			})
   390  		}
   391  		pts = append(pts, pt)
   392  	}
   393  	return pts
   394  }
   395  
   396  // applyHistory applies any transaction history that the applied blocks
   397  // introduced.
   398  func (w *Wallet) applyHistory(tx *bolt.Tx, cc modules.ConsensusChange) error {
   399  	spentSiacoinOutputs := computeSpentSiacoinOutputSet(cc.SiacoinOutputDiffs)
   400  	spentSiafundOutputs := computeSpentSiafundOutputSet(cc.SiafundOutputDiffs)
   401  
   402  	for _, block := range cc.AppliedBlocks {
   403  		consensusHeight, err := dbGetConsensusHeight(tx)
   404  		if err != nil {
   405  			return errors.AddContext(err, "failed to consensus height")
   406  		}
   407  		// Increment the consensus height.
   408  		if block.ID() != types.GenesisID {
   409  			consensusHeight++
   410  			err = dbPutConsensusHeight(tx, consensusHeight)
   411  			if err != nil {
   412  				return errors.AddContext(err, "failed to store consensus height in database")
   413  			}
   414  		}
   415  
   416  		pts := w.computeProcessedTransactionsFromBlock(tx, block, spentSiacoinOutputs, spentSiafundOutputs, consensusHeight)
   417  		for _, pt := range pts {
   418  			err := dbAppendProcessedTransaction(tx, pt)
   419  			if err != nil {
   420  				return errors.AddContext(err, "could not put processed transaction")
   421  			}
   422  		}
   423  	}
   424  
   425  	return nil
   426  }
   427  
   428  // ProcessConsensusChange parses a consensus change to update the set of
   429  // confirmed outputs known to the wallet.
   430  func (w *Wallet) ProcessConsensusChange(cc modules.ConsensusChange) {
   431  	if err := w.tg.Add(); err != nil {
   432  		return
   433  	}
   434  	defer w.tg.Done()
   435  
   436  	w.mu.Lock()
   437  	defer w.mu.Unlock()
   438  
   439  	if needRescan, err := w.updateLookahead(w.dbTx, cc); err != nil {
   440  		w.log.Severe("ERROR: failed to update lookahead:", err)
   441  		w.dbRollback = true
   442  	} else if needRescan {
   443  		go w.threadedResetSubscriptions()
   444  	}
   445  	if err := w.updateConfirmedSet(w.dbTx, cc); err != nil {
   446  		w.log.Severe("ERROR: failed to update confirmed set:", err)
   447  		w.dbRollback = true
   448  	}
   449  	if err := w.revertHistory(w.dbTx, cc.RevertedBlocks); err != nil {
   450  		w.log.Severe("ERROR: failed to revert consensus change:", err)
   451  		w.dbRollback = true
   452  	}
   453  	if err := w.applyHistory(w.dbTx, cc); err != nil {
   454  		w.log.Severe("ERROR: failed to apply consensus change:", err)
   455  		w.dbRollback = true
   456  	}
   457  	if err := dbPutConsensusChangeID(w.dbTx, cc.ID); err != nil {
   458  		w.log.Severe("ERROR: failed to update consensus change ID:", err)
   459  		w.dbRollback = true
   460  	}
   461  
   462  	if cc.Synced {
   463  		go w.threadedDefragWallet()
   464  	}
   465  }
   466  
   467  // ReceiveUpdatedUnconfirmedTransactions updates the wallet's unconfirmed
   468  // transaction set.
   469  func (w *Wallet) ReceiveUpdatedUnconfirmedTransactions(diff *modules.TransactionPoolDiff) {
   470  	if err := w.tg.Add(); err != nil {
   471  		return
   472  	}
   473  	defer w.tg.Done()
   474  
   475  	w.mu.Lock()
   476  	defer w.mu.Unlock()
   477  
   478  	// Do the pruning first. If there are any pruned transactions, we will need
   479  	// to re-allocate the whole processed transactions array.
   480  	droppedTransactions := make(map[types.TransactionID]struct{})
   481  	for i := range diff.RevertedTransactions {
   482  		txids := w.unconfirmedSets[diff.RevertedTransactions[i]]
   483  		for i := range txids {
   484  			droppedTransactions[txids[i]] = struct{}{}
   485  		}
   486  		delete(w.unconfirmedSets, diff.RevertedTransactions[i])
   487  	}
   488  
   489  	// Skip the reallocation if we can, otherwise reallocate the
   490  	// unconfirmedProcessedTransactions to no longer have the dropped
   491  	// transactions.
   492  	if len(droppedTransactions) != 0 {
   493  		// Capacity can't be reduced, because we have no way of knowing if the
   494  		// dropped transactions are relevant to the wallet or not, and some will
   495  		// not be relevant to the wallet, meaning they don't have a counterpart
   496  		// in w.unconfirmedProcessedTransactions.
   497  		newUPT := make([]modules.ProcessedTransaction, 0, len(w.unconfirmedProcessedTransactions))
   498  		for _, txn := range w.unconfirmedProcessedTransactions {
   499  			_, exists := droppedTransactions[txn.TransactionID]
   500  			if !exists {
   501  				// Transaction was not dropped, add it to the new unconfirmed
   502  				// transactions.
   503  				newUPT = append(newUPT, txn)
   504  			}
   505  		}
   506  
   507  		// Set the unconfirmed preocessed transactions to the pruned set.
   508  		w.unconfirmedProcessedTransactions = newUPT
   509  	}
   510  
   511  	// Scroll through all of the diffs and add any new transactions.
   512  	for _, unconfirmedTxnSet := range diff.AppliedTransactions {
   513  		// Mark all of the transactions that appeared in this set.
   514  		//
   515  		// TODO: Technically only necessary to mark the ones that are relevant
   516  		// to the wallet, but overhead should be low.
   517  		w.unconfirmedSets[unconfirmedTxnSet.ID] = unconfirmedTxnSet.IDs
   518  
   519  		// Get the values for the spent outputs.
   520  		spentSiacoinOutputs := make(map[types.SiacoinOutputID]types.SiacoinOutput)
   521  		for _, scod := range unconfirmedTxnSet.Change.SiacoinOutputDiffs {
   522  			// Only need to grab the reverted ones, because only reverted ones
   523  			// have the possibility of having been spent.
   524  			if scod.Direction == modules.DiffRevert {
   525  				spentSiacoinOutputs[scod.ID] = scod.SiacoinOutput
   526  			}
   527  		}
   528  
   529  		// Add each transaction to our set of unconfirmed transactions.
   530  		for i, txn := range unconfirmedTxnSet.Transactions {
   531  			// determine whether transaction is relevant to the wallet
   532  			relevant := false
   533  			for _, sci := range txn.SiacoinInputs {
   534  				relevant = relevant || w.isWalletAddress(sci.UnlockConditions.UnlockHash())
   535  			}
   536  			for _, sco := range txn.SiacoinOutputs {
   537  				relevant = relevant || w.isWalletAddress(sco.UnlockHash)
   538  			}
   539  
   540  			// only create a ProcessedTransaction if txn is relevant
   541  			if !relevant {
   542  				continue
   543  			}
   544  
   545  			pt := modules.ProcessedTransaction{
   546  				Transaction:           txn,
   547  				TransactionID:         unconfirmedTxnSet.IDs[i],
   548  				ConfirmationHeight:    types.BlockHeight(math.MaxUint64),
   549  				ConfirmationTimestamp: types.Timestamp(math.MaxUint64),
   550  			}
   551  			for _, sci := range txn.SiacoinInputs {
   552  				pt.Inputs = append(pt.Inputs, modules.ProcessedInput{
   553  					ParentID:       types.OutputID(sci.ParentID),
   554  					FundType:       types.SpecifierSiacoinInput,
   555  					WalletAddress:  w.isWalletAddress(sci.UnlockConditions.UnlockHash()),
   556  					RelatedAddress: sci.UnlockConditions.UnlockHash(),
   557  					Value:          spentSiacoinOutputs[sci.ParentID].Value,
   558  				})
   559  			}
   560  			for i, sco := range txn.SiacoinOutputs {
   561  				pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
   562  					ID:             types.OutputID(txn.SiacoinOutputID(uint64(i))),
   563  					FundType:       types.SpecifierSiacoinOutput,
   564  					MaturityHeight: types.BlockHeight(math.MaxUint64),
   565  					WalletAddress:  w.isWalletAddress(sco.UnlockHash),
   566  					RelatedAddress: sco.UnlockHash,
   567  					Value:          sco.Value,
   568  				})
   569  			}
   570  			for _, fee := range txn.MinerFees {
   571  				pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{
   572  					FundType: types.SpecifierMinerFee,
   573  					Value:    fee,
   574  				})
   575  			}
   576  			w.unconfirmedProcessedTransactions = append(w.unconfirmedProcessedTransactions, pt)
   577  		}
   578  	}
   579  }