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

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