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

     1  package wallet
     2  
     3  import (
     4  	"errors"
     5  
     6  	"SiaPrime/build"
     7  	"SiaPrime/modules"
     8  	"SiaPrime/types"
     9  )
    10  
    11  // sortedOutputs is a struct containing a slice of siacoin outputs and their
    12  // corresponding ids. sortedOutputs can be sorted using the sort package.
    13  type sortedOutputs struct {
    14  	ids     []types.SiacoinOutputID
    15  	outputs []types.SiacoinOutput
    16  }
    17  
    18  // DustThreshold returns the quantity per byte below which a Currency is
    19  // considered to be Dust.
    20  func (w *Wallet) DustThreshold() (types.Currency, error) {
    21  	if err := w.tg.Add(); err != nil {
    22  		return types.Currency{}, modules.ErrWalletShutdown
    23  	}
    24  	defer w.tg.Done()
    25  
    26  	minFee, _ := w.tpool.FeeEstimation()
    27  	return minFee.Mul64(3), nil
    28  }
    29  
    30  // ConfirmedBalance returns the balance of the wallet according to all of the
    31  // confirmed transactions.
    32  func (w *Wallet) ConfirmedBalance() (siacoinBalance types.Currency, siafundBalance types.Currency, siafundClaimBalance types.Currency, err error) {
    33  	if err := w.tg.Add(); err != nil {
    34  		return types.ZeroCurrency, types.ZeroCurrency, types.ZeroCurrency, modules.ErrWalletShutdown
    35  	}
    36  	defer w.tg.Done()
    37  
    38  	// dustThreshold has to be obtained separate from the lock
    39  	dustThreshold, err := w.DustThreshold()
    40  	if err != nil {
    41  		return types.ZeroCurrency, types.ZeroCurrency, types.ZeroCurrency, modules.ErrWalletShutdown
    42  	}
    43  
    44  	w.mu.Lock()
    45  	defer w.mu.Unlock()
    46  
    47  	// ensure durability of reported balance
    48  	if err = w.syncDB(); err != nil {
    49  		return
    50  	}
    51  
    52  	dbForEachSiacoinOutput(w.dbTx, func(_ types.SiacoinOutputID, sco types.SiacoinOutput) {
    53  		if sco.Value.Cmp(dustThreshold) > 0 {
    54  			siacoinBalance = siacoinBalance.Add(sco.Value)
    55  		}
    56  	})
    57  
    58  	siafundPool, err := dbGetSiafundPool(w.dbTx)
    59  	if err != nil {
    60  		return
    61  	}
    62  	dbForEachSiafundOutput(w.dbTx, func(_ types.SiafundOutputID, sfo types.SiafundOutput) {
    63  		siafundBalance = siafundBalance.Add(sfo.Value)
    64  		if sfo.ClaimStart.Cmp(siafundPool) > 0 {
    65  			// Skip claims larger than the siafund pool. This should only
    66  			// occur if the siafund pool has not been initialized yet.
    67  			w.log.Debugf("skipping claim with start value %v because siafund pool is only %v", sfo.ClaimStart, siafundPool)
    68  			return
    69  		}
    70  		siafundClaimBalance = siafundClaimBalance.Add(siafundPool.Sub(sfo.ClaimStart).Mul(sfo.Value).Div(types.SiafundCount))
    71  	})
    72  	return
    73  }
    74  
    75  // UnconfirmedBalance returns the number of outgoing and incoming siacoins in
    76  // the unconfirmed transaction set. Refund outputs are included in this
    77  // reporting.
    78  func (w *Wallet) UnconfirmedBalance() (outgoingSiacoins types.Currency, incomingSiacoins types.Currency, err error) {
    79  	if err := w.tg.Add(); err != nil {
    80  		return types.ZeroCurrency, types.ZeroCurrency, modules.ErrWalletShutdown
    81  	}
    82  	defer w.tg.Done()
    83  
    84  	// dustThreshold has to be obtained separate from the lock
    85  	dustThreshold, err := w.DustThreshold()
    86  	if err != nil {
    87  		return types.ZeroCurrency, types.ZeroCurrency, modules.ErrWalletShutdown
    88  	}
    89  
    90  	w.mu.Lock()
    91  	defer w.mu.Unlock()
    92  
    93  	for _, upt := range w.unconfirmedProcessedTransactions {
    94  		for _, input := range upt.Inputs {
    95  			if input.FundType == types.SpecifierSiacoinInput && input.WalletAddress {
    96  				outgoingSiacoins = outgoingSiacoins.Add(input.Value)
    97  			}
    98  		}
    99  		for _, output := range upt.Outputs {
   100  			if output.FundType == types.SpecifierSiacoinOutput && output.WalletAddress && output.Value.Cmp(dustThreshold) > 0 {
   101  				incomingSiacoins = incomingSiacoins.Add(output.Value)
   102  			}
   103  		}
   104  	}
   105  	return
   106  }
   107  
   108  // SendSiacoins creates a transaction sending 'amount' to 'dest'. The transaction
   109  // is submitted to the transaction pool and is also returned.
   110  func (w *Wallet) SendSiacoins(amount types.Currency, dest types.UnlockHash) (txns []types.Transaction, err error) {
   111  	if err := w.tg.Add(); err != nil {
   112  		err = modules.ErrWalletShutdown
   113  		return nil, err
   114  	}
   115  	defer w.tg.Done()
   116  
   117  	w.mu.RLock()
   118  	unlocked := w.unlocked
   119  	w.mu.RUnlock()
   120  	if !unlocked {
   121  		w.log.Println("Attempt to send coins has failed - wallet is locked")
   122  		return nil, modules.ErrLockedWallet
   123  	}
   124  
   125  	_, tpoolFee := w.tpool.FeeEstimation()
   126  	tpoolFee = tpoolFee.Mul64(750) // Estimated transaction size in bytes
   127  	output := types.SiacoinOutput{
   128  		Value:      amount,
   129  		UnlockHash: dest,
   130  	}
   131  
   132  	txnBuilder, err := w.StartTransaction()
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	defer func() {
   137  		if err != nil {
   138  			txnBuilder.Drop()
   139  		}
   140  	}()
   141  	err = txnBuilder.FundSiacoinsForOutputs([]types.SiacoinOutput{output}, tpoolFee)
   142  	if err != nil {
   143  		w.log.Println("Attempt to send coins has failed - failed to fund transaction:", err)
   144  		return nil, build.ExtendErr("unable to fund transaction", err)
   145  	}
   146  	txnSet, err := txnBuilder.Sign(true)
   147  	if err != nil {
   148  		w.log.Println("Attempt to send coins has failed - failed to sign transaction:", err)
   149  		return nil, build.ExtendErr("unable to sign transaction", err)
   150  	}
   151  	if w.deps.Disrupt("SendSiacoinsInterrupted") {
   152  		return nil, errors.New("failed to accept transaction set (SendSiacoinsInterrupted)")
   153  	}
   154  	err = w.tpool.AcceptTransactionSet(txnSet)
   155  	if err != nil {
   156  		w.log.Println("Attempt to send coins has failed - transaction pool rejected transaction:", err)
   157  		return nil, build.ExtendErr("unable to get transaction accepted", err)
   158  	}
   159  	w.log.Println("Submitted a siacoin transfer transaction set for value", amount.HumanString(), "with fees", tpoolFee.HumanString(), "IDs:")
   160  	for _, txn := range txnSet {
   161  		w.log.Println("\t", txn.ID())
   162  	}
   163  	return txnSet, nil
   164  }
   165  
   166  // SendSiacoinsMulti creates a transaction that includes the specified
   167  // outputs. The transaction is submitted to the transaction pool and is also
   168  // returned.
   169  func (w *Wallet) SendSiacoinsMulti(outputs []types.SiacoinOutput) (txns []types.Transaction, err error) {
   170  	w.log.Println("Beginning call to SendSiacoinsMulti")
   171  	if err := w.tg.Add(); err != nil {
   172  		err = modules.ErrWalletShutdown
   173  		return nil, err
   174  	}
   175  	defer w.tg.Done()
   176  	w.mu.RLock()
   177  	unlocked := w.unlocked
   178  	w.mu.RUnlock()
   179  	if !unlocked {
   180  		w.log.Println("Attempt to send coins has failed - wallet is locked")
   181  		return nil, modules.ErrLockedWallet
   182  	}
   183  
   184  	txnBuilder, err := w.StartTransaction()
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	defer func() {
   189  		if err != nil {
   190  			txnBuilder.Drop()
   191  		}
   192  	}()
   193  
   194  	// Add estimated transaction fee.
   195  	_, tpoolFee := w.tpool.FeeEstimation()
   196  	tpoolFee = tpoolFee.Mul64(2)                              // We don't want send-to-many transactions to fail.
   197  	tpoolFee = tpoolFee.Mul64(1000 + 60*uint64(len(outputs))) // Estimated transaction size in bytes
   198  
   199  	err = txnBuilder.FundSiacoinsForOutputs(outputs, tpoolFee)
   200  	if err != nil {
   201  		return nil, build.ExtendErr("unable to fund transaction", err)
   202  	}
   203  
   204  	txnSet, err := txnBuilder.Sign(true)
   205  	if err != nil {
   206  		w.log.Println("Attempt to send coins has failed - failed to sign transaction:", err)
   207  		return nil, build.ExtendErr("unable to sign transaction", err)
   208  	}
   209  	if w.deps.Disrupt("SendSiacoinsInterrupted") {
   210  		return nil, errors.New("failed to accept transaction set (SendSiacoinsInterrupted)")
   211  	}
   212  	w.log.Println("Attempting to broadcast a multi-send over the network")
   213  	err = w.tpool.AcceptTransactionSet(txnSet)
   214  	if err != nil {
   215  		w.log.Println("Attempt to send coins has failed - transaction pool rejected transaction:", err)
   216  		return nil, build.ExtendErr("unable to get transaction accepted", err)
   217  	}
   218  
   219  	// Log the success.
   220  	var outputList string
   221  	for _, output := range outputs {
   222  		outputList = outputList + "\n\tAddress: " + output.UnlockHash.String() + "\n\tValue: " + output.Value.HumanString() + "\n"
   223  	}
   224  	w.log.Printf("Successfully broadcast transaction with id %v, fee %v, and the following outputs: %v", txnSet[len(txnSet)-1].ID(), tpoolFee.HumanString(), outputList)
   225  	return txnSet, nil
   226  }
   227  
   228  // SendSiafunds creates a transaction sending 'amount' to 'dest'. The transaction
   229  // is submitted to the transaction pool and is also returned.
   230  func (w *Wallet) SendSiafunds(amount types.Currency, dest types.UnlockHash) (txns []types.Transaction, err error) {
   231  	if err := w.tg.Add(); err != nil {
   232  		err = modules.ErrWalletShutdown
   233  		return nil, err
   234  	}
   235  	defer w.tg.Done()
   236  	w.mu.RLock()
   237  	unlocked := w.unlocked
   238  	w.mu.RUnlock()
   239  	if !unlocked {
   240  		return nil, modules.ErrLockedWallet
   241  	}
   242  
   243  	_, tpoolFee := w.tpool.FeeEstimation()
   244  	tpoolFee = tpoolFee.Mul64(750) // Estimated transaction size in bytes
   245  	tpoolFee = tpoolFee.Mul64(5)   // use large fee to ensure siafund transactions are selected by miners
   246  	output := types.SiafundOutput{
   247  		Value:      amount,
   248  		UnlockHash: dest,
   249  	}
   250  
   251  	txnBuilder, err := w.StartTransaction()
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	defer func() {
   256  		if err != nil {
   257  			txnBuilder.Drop()
   258  		}
   259  	}()
   260  	err = txnBuilder.FundSiacoins(tpoolFee)
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  	err = txnBuilder.FundSiafunds(amount)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	txnBuilder.AddMinerFee(tpoolFee)
   269  	txnBuilder.AddSiafundOutput(output)
   270  	txnSet, err := txnBuilder.Sign(true)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  	err = w.tpool.AcceptTransactionSet(txnSet)
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	w.log.Println("Submitted a siafund transfer transaction set for value", amount.HumanString(), "with fees", tpoolFee.HumanString(), "IDs:")
   279  	for _, txn := range txnSet {
   280  		w.log.Println("\t", txn.ID())
   281  	}
   282  	return txnSet, nil
   283  }
   284  
   285  // Len returns the number of elements in the sortedOutputs struct.
   286  func (so sortedOutputs) Len() int {
   287  	if build.DEBUG && len(so.ids) != len(so.outputs) {
   288  		panic("sortedOutputs object is corrupt")
   289  	}
   290  	return len(so.ids)
   291  }
   292  
   293  // Less returns whether element 'i' is less than element 'j'. The currency
   294  // value of each output is used for comparison.
   295  func (so sortedOutputs) Less(i, j int) bool {
   296  	return so.outputs[i].Value.Cmp(so.outputs[j].Value) < 0
   297  }
   298  
   299  // Swap swaps two elements in the sortedOutputs set.
   300  func (so sortedOutputs) Swap(i, j int) {
   301  	so.ids[i], so.ids[j] = so.ids[j], so.ids[i]
   302  	so.outputs[i], so.outputs[j] = so.outputs[j], so.outputs[i]
   303  }