github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/modules/wallet/database.go (about)

     1  package wallet
     2  
     3  import (
     4  	"encoding/binary"
     5  	"fmt"
     6  	"reflect"
     7  	"time"
     8  
     9  	"SiaPrime/encoding"
    10  	"SiaPrime/modules"
    11  	"SiaPrime/types"
    12  	"gitlab.com/NebulousLabs/errors"
    13  	"gitlab.com/NebulousLabs/fastrand"
    14  
    15  	"gitlab.com/NebulousLabs/bolt"
    16  )
    17  
    18  var (
    19  	// bucketProcessedTransactions stores ProcessedTransactions in
    20  	// chronological order. Only transactions relevant to the wallet are
    21  	// stored. The key of this bucket is an autoincrementing integer.
    22  	bucketProcessedTransactions = []byte("bucketProcessedTransactions")
    23  	// bucketProcessedTxnIndex maps a ProcessedTransactions ID to it's
    24  	// autoincremented index in bucketProcessedTransactions
    25  	bucketProcessedTxnIndex = []byte("bucketProcessedTxnKey")
    26  	// bucketAddrTransactions maps an UnlockHash to the
    27  	// ProcessedTransactions that it appears in.
    28  	bucketAddrTransactions = []byte("bucketAddrTransactions")
    29  	// bucketSiacoinOutputs maps a SiacoinOutputID to its SiacoinOutput. Only
    30  	// outputs that the wallet controls are stored. The wallet uses these
    31  	// outputs to fund transactions.
    32  	bucketSiacoinOutputs = []byte("bucketSiacoinOutputs")
    33  	// bucketSiacoinOutputs maps a SiafundOutputID to its SiafundOutput. Only
    34  	// outputs that the wallet controls are stored. The wallet uses these
    35  	// outputs to fund transactions.
    36  	bucketSiafundOutputs = []byte("bucketSiafundOutputs")
    37  	// bucketSpentOutputs maps an OutputID to the height at which it was
    38  	// spent. Only outputs spent by the wallet are stored. The wallet tracks
    39  	// these outputs so that it can reuse them if they are not confirmed on
    40  	// the blockchain.
    41  	bucketSpentOutputs = []byte("bucketSpentOutputs")
    42  	// bucketUnlockConditions maps an UnlockHash to its UnlockConditions. It
    43  	// is used to track UnlockConditions manually stored by the user,
    44  	// typically with an offline wallet.
    45  	bucketUnlockConditions = []byte("bucketUnlockConditions")
    46  	// bucketWallet contains various fields needed by the wallet, such as its
    47  	// UID, EncryptionVerification, and PrimarySeedFile.
    48  	bucketWallet = []byte("bucketWallet")
    49  
    50  	dbBuckets = [][]byte{
    51  		bucketProcessedTransactions,
    52  		bucketProcessedTxnIndex,
    53  		bucketAddrTransactions,
    54  		bucketSiacoinOutputs,
    55  		bucketSiafundOutputs,
    56  		bucketSpentOutputs,
    57  		bucketUnlockConditions,
    58  		bucketWallet,
    59  	}
    60  
    61  	errNoKey = errors.New("key does not exist")
    62  
    63  	// these keys are used in bucketWallet
    64  	keyAuxiliarySeedFiles     = []byte("keyAuxiliarySeedFiles")
    65  	keyConsensusChange        = []byte("keyConsensusChange")
    66  	keyConsensusHeight        = []byte("keyConsensusHeight")
    67  	keyEncryptionVerification = []byte("keyEncryptionVerification")
    68  	keyPrimarySeedFile        = []byte("keyPrimarySeedFile")
    69  	keyPrimarySeedProgress    = []byte("keyPrimarySeedProgress")
    70  	keySiafundPool            = []byte("keySiafundPool")
    71  	keySpendableKeyFiles      = []byte("keySpendableKeyFiles")
    72  	keyUID                    = []byte("keyUID")
    73  	keyWatchedAddrs           = []byte("keyWatchedAddrs")
    74  )
    75  
    76  // threadedDBUpdate commits the active database transaction and starts a new
    77  // transaction.
    78  func (w *Wallet) threadedDBUpdate() {
    79  	if err := w.tg.Add(); err != nil {
    80  		return
    81  	}
    82  	defer w.tg.Done()
    83  
    84  	for {
    85  		select {
    86  		case <-time.After(2 * time.Minute):
    87  		case <-w.tg.StopChan():
    88  			return
    89  		}
    90  		w.mu.Lock()
    91  		err := w.syncDB()
    92  		w.mu.Unlock()
    93  		if err != nil {
    94  			// If the database is having problems, we need to close it to
    95  			// protect it. This will likely cause a panic somewhere when another
    96  			// caller tries to access dbTx but it is nil.
    97  			w.log.Severe("ERROR: syncDB encountered an error. Closing database to protect wallet. wallet may crash:", err)
    98  			w.db.Close()
    99  			return
   100  		}
   101  	}
   102  }
   103  
   104  // syncDB commits the current global transaction and immediately begins a
   105  // new one. It must be called with a write-lock.
   106  func (w *Wallet) syncDB() error {
   107  	// If the rollback flag is set, it means that somewhere in the middle of an
   108  	// atomic update there  was a failure, and that failure needs to be rolled
   109  	// back. An error will be returned.
   110  	if w.dbRollback {
   111  		err := errors.New("database unable to sync - rollback requested")
   112  		return errors.Compose(err, w.dbTx.Rollback())
   113  	}
   114  
   115  	// commit the current tx
   116  	err := w.dbTx.Commit()
   117  	if err != nil {
   118  		w.log.Severe("ERROR: failed to apply database update:", err)
   119  		err = errors.Compose(err, w.dbTx.Rollback())
   120  		return errors.AddContext(err, "unable to commit dbTx in syncDB")
   121  	}
   122  	// begin a new tx
   123  	w.dbTx, err = w.db.Begin(true)
   124  	if err != nil {
   125  		w.log.Severe("ERROR: failed to start database update:", err)
   126  		return errors.AddContext(err, "unable to begin new dbTx in syncDB")
   127  	}
   128  	return nil
   129  }
   130  
   131  // dbReset wipes and reinitializes a wallet database.
   132  func dbReset(tx *bolt.Tx) error {
   133  	for _, bucket := range dbBuckets {
   134  		err := tx.DeleteBucket(bucket)
   135  		if err != nil {
   136  			return err
   137  		}
   138  		_, err = tx.CreateBucket(bucket)
   139  		if err != nil {
   140  			return err
   141  		}
   142  	}
   143  
   144  	// reinitialize the database with default values
   145  	wb := tx.Bucket(bucketWallet)
   146  	wb.Put(keyUID, fastrand.Bytes(len(uniqueID{})))
   147  	wb.Put(keyConsensusHeight, encoding.Marshal(uint64(0)))
   148  	wb.Put(keyAuxiliarySeedFiles, encoding.Marshal([]seedFile{}))
   149  	wb.Put(keySpendableKeyFiles, encoding.Marshal([]spendableKeyFile{}))
   150  	wb.Put(keyWatchedAddrs, encoding.Marshal([]types.UnlockHash{}))
   151  	dbPutConsensusHeight(tx, 0)
   152  	dbPutConsensusChangeID(tx, modules.ConsensusChangeBeginning)
   153  	dbPutSiafundPool(tx, types.ZeroCurrency)
   154  
   155  	return nil
   156  }
   157  
   158  // dbPut is a helper function for storing a marshalled key/value pair.
   159  func dbPut(b *bolt.Bucket, key, val interface{}) error {
   160  	return b.Put(encoding.Marshal(key), encoding.Marshal(val))
   161  }
   162  
   163  // dbGet is a helper function for retrieving a marshalled key/value pair. val
   164  // must be a pointer.
   165  func dbGet(b *bolt.Bucket, key, val interface{}) error {
   166  	valBytes := b.Get(encoding.Marshal(key))
   167  	if valBytes == nil {
   168  		return errNoKey
   169  	}
   170  	return encoding.Unmarshal(valBytes, val)
   171  }
   172  
   173  // dbDelete is a helper function for deleting a marshalled key/value pair.
   174  func dbDelete(b *bolt.Bucket, key interface{}) error {
   175  	return b.Delete(encoding.Marshal(key))
   176  }
   177  
   178  // dbForEach is a helper function for iterating over a bucket and calling fn
   179  // on each entry. fn must be a function with two parameters. The key/value
   180  // bytes of each bucket entry will be unmarshalled into the types of fn's
   181  // parameters.
   182  func dbForEach(b *bolt.Bucket, fn interface{}) error {
   183  	// check function type
   184  	fnVal, fnTyp := reflect.ValueOf(fn), reflect.TypeOf(fn)
   185  	if fnTyp.Kind() != reflect.Func || fnTyp.NumIn() != 2 {
   186  		panic("bad fn type: needed func(key, val), got " + fnTyp.String())
   187  	}
   188  
   189  	return b.ForEach(func(keyBytes, valBytes []byte) error {
   190  		key, val := reflect.New(fnTyp.In(0)), reflect.New(fnTyp.In(1))
   191  		if err := encoding.Unmarshal(keyBytes, key.Interface()); err != nil {
   192  			return err
   193  		} else if err := encoding.Unmarshal(valBytes, val.Interface()); err != nil {
   194  			return err
   195  		}
   196  		fnVal.Call([]reflect.Value{key.Elem(), val.Elem()})
   197  		return nil
   198  	})
   199  }
   200  
   201  // Type-safe wrappers around the db helpers
   202  
   203  func dbPutSiacoinOutput(tx *bolt.Tx, id types.SiacoinOutputID, output types.SiacoinOutput) error {
   204  	return dbPut(tx.Bucket(bucketSiacoinOutputs), id, output)
   205  }
   206  func dbGetSiacoinOutput(tx *bolt.Tx, id types.SiacoinOutputID) (output types.SiacoinOutput, err error) {
   207  	err = dbGet(tx.Bucket(bucketSiacoinOutputs), id, &output)
   208  	return
   209  }
   210  func dbDeleteSiacoinOutput(tx *bolt.Tx, id types.SiacoinOutputID) error {
   211  	return dbDelete(tx.Bucket(bucketSiacoinOutputs), id)
   212  }
   213  func dbForEachSiacoinOutput(tx *bolt.Tx, fn func(types.SiacoinOutputID, types.SiacoinOutput)) error {
   214  	return dbForEach(tx.Bucket(bucketSiacoinOutputs), fn)
   215  }
   216  
   217  func dbPutSiafundOutput(tx *bolt.Tx, id types.SiafundOutputID, output types.SiafundOutput) error {
   218  	return dbPut(tx.Bucket(bucketSiafundOutputs), id, output)
   219  }
   220  func dbGetSiafundOutput(tx *bolt.Tx, id types.SiafundOutputID) (output types.SiafundOutput, err error) {
   221  	err = dbGet(tx.Bucket(bucketSiafundOutputs), id, &output)
   222  	return
   223  }
   224  func dbDeleteSiafundOutput(tx *bolt.Tx, id types.SiafundOutputID) error {
   225  	return dbDelete(tx.Bucket(bucketSiafundOutputs), id)
   226  }
   227  func dbForEachSiafundOutput(tx *bolt.Tx, fn func(types.SiafundOutputID, types.SiafundOutput)) error {
   228  	return dbForEach(tx.Bucket(bucketSiafundOutputs), fn)
   229  }
   230  
   231  func dbPutSpentOutput(tx *bolt.Tx, id types.OutputID, height types.BlockHeight) error {
   232  	return dbPut(tx.Bucket(bucketSpentOutputs), id, height)
   233  }
   234  func dbGetSpentOutput(tx *bolt.Tx, id types.OutputID) (height types.BlockHeight, err error) {
   235  	err = dbGet(tx.Bucket(bucketSpentOutputs), id, &height)
   236  	return
   237  }
   238  func dbDeleteSpentOutput(tx *bolt.Tx, id types.OutputID) error {
   239  	return dbDelete(tx.Bucket(bucketSpentOutputs), id)
   240  }
   241  
   242  func dbPutAddrTransactions(tx *bolt.Tx, addr types.UnlockHash, txns []uint64) error {
   243  	return dbPut(tx.Bucket(bucketAddrTransactions), addr, txns)
   244  }
   245  func dbGetAddrTransactions(tx *bolt.Tx, addr types.UnlockHash) (txns []uint64, err error) {
   246  	err = dbGet(tx.Bucket(bucketAddrTransactions), addr, &txns)
   247  	return
   248  }
   249  
   250  func dbPutUnlockConditions(tx *bolt.Tx, uc types.UnlockConditions) error {
   251  	return dbPut(tx.Bucket(bucketUnlockConditions), uc.UnlockHash(), uc)
   252  }
   253  func dbGetUnlockConditions(tx *bolt.Tx, addr types.UnlockHash) (uc types.UnlockConditions, err error) {
   254  	err = dbGet(tx.Bucket(bucketUnlockConditions), addr, &uc)
   255  	return
   256  }
   257  
   258  // dbAddAddrTransaction appends a single transaction index to the set of
   259  // transactions associated with addr. If the index is already in the set, it is
   260  // not added again.
   261  func dbAddAddrTransaction(tx *bolt.Tx, addr types.UnlockHash, txn uint64) error {
   262  	txns, err := dbGetAddrTransactions(tx, addr)
   263  	if err != nil && err != errNoKey {
   264  		return err
   265  	}
   266  	for _, i := range txns {
   267  		if i == txn {
   268  			return nil
   269  		}
   270  	}
   271  	return dbPutAddrTransactions(tx, addr, append(txns, txn))
   272  }
   273  
   274  // dbAddProcessedTransactionAddrs updates bucketAddrTransactions to associate
   275  // every address in pt with txn, which is assumed to be pt's index in
   276  // bucketProcessedTransactions.
   277  func dbAddProcessedTransactionAddrs(tx *bolt.Tx, pt modules.ProcessedTransaction, txn uint64) error {
   278  	addrs := make(map[types.UnlockHash]struct{})
   279  	for _, input := range pt.Inputs {
   280  		addrs[input.RelatedAddress] = struct{}{}
   281  	}
   282  	for _, output := range pt.Outputs {
   283  		// miner fees don't have an address, so skip them
   284  		if output.FundType == types.SpecifierMinerFee {
   285  			continue
   286  		}
   287  		addrs[output.RelatedAddress] = struct{}{}
   288  	}
   289  	for addr := range addrs {
   290  		if err := dbAddAddrTransaction(tx, addr, txn); err != nil {
   291  			return errors.AddContext(err, fmt.Sprintf("failed to add txn %v to address %v",
   292  				pt.TransactionID, addr))
   293  		}
   294  	}
   295  	return nil
   296  }
   297  
   298  // bucketProcessedTransactions works a little differently: the key is
   299  // meaningless, only used to order the transactions chronologically.
   300  
   301  // decodeProcessedTransaction decodes a marshalled processedTransaction
   302  func decodeProcessedTransaction(ptBytes []byte, pt *modules.ProcessedTransaction) error {
   303  	err := encoding.Unmarshal(ptBytes, pt)
   304  	if err != nil {
   305  		// COMPATv1.2.1: try decoding into old transaction type
   306  		var oldpt v121ProcessedTransaction
   307  		err = encoding.Unmarshal(ptBytes, &oldpt)
   308  		*pt = convertProcessedTransaction(oldpt)
   309  	}
   310  	return err
   311  }
   312  
   313  func dbDeleteTransactionIndex(tx *bolt.Tx, txid types.TransactionID) error {
   314  	return dbDelete(tx.Bucket(bucketProcessedTxnIndex), txid)
   315  }
   316  func dbPutTransactionIndex(tx *bolt.Tx, txid types.TransactionID, key []byte) error {
   317  	return dbPut(tx.Bucket(bucketProcessedTxnIndex), txid, key)
   318  }
   319  
   320  func dbGetTransactionIndex(tx *bolt.Tx, txid types.TransactionID) (key []byte, err error) {
   321  	key = make([]byte, 8)
   322  	err = dbGet(tx.Bucket(bucketProcessedTxnIndex), txid, &key)
   323  	return
   324  }
   325  
   326  // initProcessedTxnIndex initializes the bucketProcessedTxnIndex with the
   327  // elements from bucketProcessedTransactions
   328  func initProcessedTxnIndex(tx *bolt.Tx) error {
   329  	it := dbProcessedTransactionsIterator(tx)
   330  	indexBytes := make([]byte, 8)
   331  	for it.next() {
   332  		index, pt := it.key(), it.value()
   333  		binary.BigEndian.PutUint64(indexBytes, index)
   334  		if err := dbPutTransactionIndex(tx, pt.TransactionID, indexBytes); err != nil {
   335  			return err
   336  		}
   337  	}
   338  	return nil
   339  }
   340  
   341  func dbAppendProcessedTransaction(tx *bolt.Tx, pt modules.ProcessedTransaction) error {
   342  	b := tx.Bucket(bucketProcessedTransactions)
   343  	key, err := b.NextSequence()
   344  	if err != nil {
   345  		return errors.AddContext(err, "failed to get next sequence from bucket")
   346  	}
   347  	// big-endian is used so that the keys are properly sorted
   348  	keyBytes := make([]byte, 8)
   349  	binary.BigEndian.PutUint64(keyBytes, key)
   350  	if err = b.Put(keyBytes, encoding.Marshal(pt)); err != nil {
   351  		return errors.AddContext(err, "failed to store processed txn in database")
   352  	}
   353  
   354  	// add used index to bucketProcessedTxnIndex
   355  	if err = dbPutTransactionIndex(tx, pt.TransactionID, keyBytes); err != nil {
   356  		return errors.AddContext(err, "failed to store txn index in database")
   357  	}
   358  
   359  	// also add this txid to the bucketAddrTransactions
   360  	if err = dbAddProcessedTransactionAddrs(tx, pt, key); err != nil {
   361  		return errors.AddContext(err, "failed to add processed transaction to addresses in database")
   362  	}
   363  	return nil
   364  }
   365  
   366  func dbGetLastProcessedTransaction(tx *bolt.Tx) (pt modules.ProcessedTransaction, err error) {
   367  	seq := tx.Bucket(bucketProcessedTransactions).Sequence()
   368  	keyBytes := make([]byte, 8)
   369  	binary.BigEndian.PutUint64(keyBytes, seq)
   370  	val := tx.Bucket(bucketProcessedTransactions).Get(keyBytes)
   371  	err = decodeProcessedTransaction(val, &pt)
   372  	return
   373  }
   374  
   375  func dbDeleteLastProcessedTransaction(tx *bolt.Tx) error {
   376  	// Get the last processed txn.
   377  	pt, err := dbGetLastProcessedTransaction(tx)
   378  	if err != nil {
   379  		return errors.New("can't delete from empty bucket")
   380  	}
   381  	// Delete its txid from the index bucket.
   382  	if err := dbDeleteTransactionIndex(tx, pt.TransactionID); err != nil {
   383  		return errors.AddContext(err, "couldn't delete txn index")
   384  	}
   385  	// Delete the last processed txn and decrement the sequence.
   386  	b := tx.Bucket(bucketProcessedTransactions)
   387  	seq := b.Sequence()
   388  	keyBytes := make([]byte, 8)
   389  	binary.BigEndian.PutUint64(keyBytes, seq)
   390  	return errors.Compose(b.SetSequence(seq-1), b.Delete(keyBytes))
   391  }
   392  
   393  func dbGetProcessedTransaction(tx *bolt.Tx, index uint64) (pt modules.ProcessedTransaction, err error) {
   394  	// big-endian is used so that the keys are properly sorted
   395  	indexBytes := make([]byte, 8)
   396  	binary.BigEndian.PutUint64(indexBytes, index)
   397  	val := tx.Bucket(bucketProcessedTransactions).Get(indexBytes)
   398  	err = decodeProcessedTransaction(val, &pt)
   399  	return
   400  }
   401  
   402  // A processedTransactionsIter iterates through the ProcessedTransactions bucket.
   403  type processedTransactionsIter struct {
   404  	c   *bolt.Cursor
   405  	seq uint64
   406  	pt  modules.ProcessedTransaction
   407  }
   408  
   409  // next decodes the next ProcessedTransaction, returning false if the end of
   410  // the bucket has been reached.
   411  func (it *processedTransactionsIter) next() bool {
   412  	var seqBytes, ptBytes []byte
   413  	if it.pt.TransactionID == (types.TransactionID{}) {
   414  		// this is the first time next has been called, so cursor is not
   415  		// initialized yet
   416  		seqBytes, ptBytes = it.c.First()
   417  	} else {
   418  		seqBytes, ptBytes = it.c.Next()
   419  	}
   420  	if seqBytes == nil {
   421  		return false
   422  	}
   423  	it.seq = binary.BigEndian.Uint64(seqBytes)
   424  	return decodeProcessedTransaction(ptBytes, &it.pt) == nil
   425  }
   426  
   427  // key returns the key for the most recently decoded ProcessedTransaction.
   428  func (it *processedTransactionsIter) key() uint64 {
   429  	return it.seq
   430  }
   431  
   432  // value returns the most recently decoded ProcessedTransaction.
   433  func (it *processedTransactionsIter) value() modules.ProcessedTransaction {
   434  	return it.pt
   435  }
   436  
   437  // dbProcessedTransactionsIterator creates a new processedTransactionsIter.
   438  func dbProcessedTransactionsIterator(tx *bolt.Tx) *processedTransactionsIter {
   439  	return &processedTransactionsIter{
   440  		c: tx.Bucket(bucketProcessedTransactions).Cursor(),
   441  	}
   442  }
   443  
   444  // dbGetWalletUID returns the UID assigned to the wallet's primary seed.
   445  func dbGetWalletUID(tx *bolt.Tx) (uid uniqueID) {
   446  	copy(uid[:], tx.Bucket(bucketWallet).Get(keyUID))
   447  	return
   448  }
   449  
   450  // dbGetPrimarySeedProgress returns the number of keys generated from the
   451  // primary seed.
   452  func dbGetPrimarySeedProgress(tx *bolt.Tx) (progress uint64, err error) {
   453  	err = encoding.Unmarshal(tx.Bucket(bucketWallet).Get(keyPrimarySeedProgress), &progress)
   454  	return
   455  }
   456  
   457  // dbPutPrimarySeedProgress sets the primary seed progress counter.
   458  func dbPutPrimarySeedProgress(tx *bolt.Tx, progress uint64) error {
   459  	return tx.Bucket(bucketWallet).Put(keyPrimarySeedProgress, encoding.Marshal(progress))
   460  }
   461  
   462  // dbGetConsensusChangeID returns the ID of the last ConsensusChange processed by the wallet.
   463  func dbGetConsensusChangeID(tx *bolt.Tx) (cc modules.ConsensusChangeID) {
   464  	copy(cc[:], tx.Bucket(bucketWallet).Get(keyConsensusChange))
   465  	return
   466  }
   467  
   468  // dbPutConsensusChangeID stores the ID of the last ConsensusChange processed by the wallet.
   469  func dbPutConsensusChangeID(tx *bolt.Tx, cc modules.ConsensusChangeID) error {
   470  	return tx.Bucket(bucketWallet).Put(keyConsensusChange, cc[:])
   471  }
   472  
   473  // dbGetConsensusHeight returns the height that the wallet has scanned to.
   474  func dbGetConsensusHeight(tx *bolt.Tx) (height types.BlockHeight, err error) {
   475  	err = encoding.Unmarshal(tx.Bucket(bucketWallet).Get(keyConsensusHeight), &height)
   476  	return
   477  }
   478  
   479  // dbPutConsensusHeight stores the height that the wallet has scanned to.
   480  func dbPutConsensusHeight(tx *bolt.Tx, height types.BlockHeight) error {
   481  	return tx.Bucket(bucketWallet).Put(keyConsensusHeight, encoding.Marshal(height))
   482  }
   483  
   484  // dbGetSiafundPool returns the value of the siafund pool.
   485  func dbGetSiafundPool(tx *bolt.Tx) (pool types.Currency, err error) {
   486  	err = encoding.Unmarshal(tx.Bucket(bucketWallet).Get(keySiafundPool), &pool)
   487  	return
   488  }
   489  
   490  // dbPutSiafundPool stores the value of the siafund pool.
   491  func dbPutSiafundPool(tx *bolt.Tx, pool types.Currency) error {
   492  	return tx.Bucket(bucketWallet).Put(keySiafundPool, encoding.Marshal(pool))
   493  }
   494  
   495  // dbPutWatchedAddresses stores the set of watched addresses.
   496  func dbPutWatchedAddresses(tx *bolt.Tx, addrs []types.UnlockHash) error {
   497  	return tx.Bucket(bucketWallet).Put(keyWatchedAddrs, encoding.Marshal(addrs))
   498  }
   499  
   500  // COMPATv121: these types were stored in the db in v1.2.2 and earlier.
   501  type (
   502  	v121ProcessedInput struct {
   503  		FundType       types.Specifier
   504  		WalletAddress  bool
   505  		RelatedAddress types.UnlockHash
   506  		Value          types.Currency
   507  	}
   508  
   509  	v121ProcessedOutput struct {
   510  		FundType       types.Specifier
   511  		MaturityHeight types.BlockHeight
   512  		WalletAddress  bool
   513  		RelatedAddress types.UnlockHash
   514  		Value          types.Currency
   515  	}
   516  
   517  	v121ProcessedTransaction struct {
   518  		Transaction           types.Transaction
   519  		TransactionID         types.TransactionID
   520  		ConfirmationHeight    types.BlockHeight
   521  		ConfirmationTimestamp types.Timestamp
   522  		Inputs                []v121ProcessedInput
   523  		Outputs               []v121ProcessedOutput
   524  	}
   525  )
   526  
   527  func convertProcessedTransaction(oldpt v121ProcessedTransaction) (pt modules.ProcessedTransaction) {
   528  	pt.Transaction = oldpt.Transaction
   529  	pt.TransactionID = oldpt.TransactionID
   530  	pt.ConfirmationHeight = oldpt.ConfirmationHeight
   531  	pt.ConfirmationTimestamp = oldpt.ConfirmationTimestamp
   532  	pt.Inputs = make([]modules.ProcessedInput, len(oldpt.Inputs))
   533  	for i, in := range oldpt.Inputs {
   534  		pt.Inputs[i] = modules.ProcessedInput{
   535  			FundType:       in.FundType,
   536  			WalletAddress:  in.WalletAddress,
   537  			RelatedAddress: in.RelatedAddress,
   538  			Value:          in.Value,
   539  		}
   540  	}
   541  	pt.Outputs = make([]modules.ProcessedOutput, len(oldpt.Outputs))
   542  	for i, out := range oldpt.Outputs {
   543  		pt.Outputs[i] = modules.ProcessedOutput{
   544  			FundType:       out.FundType,
   545  			MaturityHeight: out.MaturityHeight,
   546  			WalletAddress:  out.WalletAddress,
   547  			RelatedAddress: out.RelatedAddress,
   548  			Value:          out.Value,
   549  		}
   550  	}
   551  	return
   552  }