gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/wallet/money.go (about)

     1  package wallet
     2  
     3  import (
     4  	"errors"
     5  
     6  	"gitlab.com/SiaPrime/SiaPrime/build"
     7  	"gitlab.com/SiaPrime/SiaPrime/modules"
     8  	"gitlab.com/SiaPrime/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  	// Check if consensus is synced
   118  	if !w.cs.Synced() || w.deps.Disrupt("UnsyncedConsensus") {
   119  		return nil, errors.New("cannot send siacoin until fully synced")
   120  	}
   121  
   122  	w.mu.RLock()
   123  	unlocked := w.unlocked
   124  	w.mu.RUnlock()
   125  	if !unlocked {
   126  		w.log.Println("Attempt to send coins has failed - wallet is locked")
   127  		return nil, modules.ErrLockedWallet
   128  	}
   129  
   130  	_, tpoolFee := w.tpool.FeeEstimation()
   131  	tpoolFee = tpoolFee.Mul64(750) // Estimated transaction size in bytes
   132  	output := types.SiacoinOutput{
   133  		Value:      amount,
   134  		UnlockHash: dest,
   135  	}
   136  
   137  	txnBuilder, err := w.StartTransaction()
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	defer func() {
   142  		if err != nil {
   143  			txnBuilder.Drop()
   144  		}
   145  	}()
   146  	err = txnBuilder.FundSiacoinsForOutputs([]types.SiacoinOutput{output}, tpoolFee)
   147  	if err != nil {
   148  		w.log.Println("Attempt to send coins has failed - failed to fund transaction:", err)
   149  		return nil, build.ExtendErr("unable to fund transaction", err)
   150  	}
   151  	txnSet, err := txnBuilder.Sign(true)
   152  	if err != nil {
   153  		w.log.Println("Attempt to send coins has failed - failed to sign transaction:", err)
   154  		return nil, build.ExtendErr("unable to sign transaction", err)
   155  	}
   156  	if w.deps.Disrupt("SendSiacoinsInterrupted") {
   157  		return nil, errors.New("failed to accept transaction set (SendSiacoinsInterrupted)")
   158  	}
   159  	err = w.tpool.AcceptTransactionSet(txnSet)
   160  	if err != nil {
   161  		w.log.Println("Attempt to send coins has failed - transaction pool rejected transaction:", err)
   162  		return nil, build.ExtendErr("unable to get transaction accepted", err)
   163  	}
   164  	w.log.Println("Submitted a siacoin transfer transaction set for value", amount.HumanString(), "with fees", tpoolFee.HumanString(), "IDs:")
   165  	for _, txn := range txnSet {
   166  		w.log.Println("\t", txn.ID())
   167  	}
   168  	return txnSet, nil
   169  }
   170  
   171  // SendSiacoinsMulti creates a transaction that includes the specified
   172  // outputs. The transaction is submitted to the transaction pool and is also
   173  // returned.
   174  func (w *Wallet) SendSiacoinsMulti(outputs []types.SiacoinOutput) (txns []types.Transaction, err error) {
   175  	w.log.Println("Beginning call to SendSiacoinsMulti")
   176  	if err := w.tg.Add(); err != nil {
   177  		err = modules.ErrWalletShutdown
   178  		return nil, err
   179  	}
   180  	defer w.tg.Done()
   181  
   182  	// Check if consensus is synced
   183  	if !w.cs.Synced() || w.deps.Disrupt("UnsyncedConsensus") {
   184  		return nil, errors.New("cannot send siacoin until fully synced")
   185  	}
   186  
   187  	w.mu.RLock()
   188  	unlocked := w.unlocked
   189  	w.mu.RUnlock()
   190  	if !unlocked {
   191  		w.log.Println("Attempt to send coins has failed - wallet is locked")
   192  		return nil, modules.ErrLockedWallet
   193  	}
   194  
   195  	txnBuilder, err := w.StartTransaction()
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  	defer func() {
   200  		if err != nil {
   201  			txnBuilder.Drop()
   202  		}
   203  	}()
   204  
   205  	// Add estimated transaction fee.
   206  	_, tpoolFee := w.tpool.FeeEstimation()
   207  	tpoolFee = tpoolFee.Mul64(2)                              // We don't want send-to-many transactions to fail.
   208  	tpoolFee = tpoolFee.Mul64(1000 + 60*uint64(len(outputs))) // Estimated transaction size in bytes
   209  
   210  	err = txnBuilder.FundSiacoinsForOutputs(outputs, tpoolFee)
   211  	if err != nil {
   212  		return nil, build.ExtendErr("unable to fund transaction", err)
   213  	}
   214  
   215  	txnSet, err := txnBuilder.Sign(true)
   216  	if err != nil {
   217  		w.log.Println("Attempt to send coins has failed - failed to sign transaction:", err)
   218  		return nil, build.ExtendErr("unable to sign transaction", err)
   219  	}
   220  	if w.deps.Disrupt("SendSiacoinsInterrupted") {
   221  		return nil, errors.New("failed to accept transaction set (SendSiacoinsInterrupted)")
   222  	}
   223  	w.log.Println("Attempting to broadcast a multi-send over the network")
   224  	err = w.tpool.AcceptTransactionSet(txnSet)
   225  	if err != nil {
   226  		w.log.Println("Attempt to send coins has failed - transaction pool rejected transaction:", err)
   227  		return nil, build.ExtendErr("unable to get transaction accepted", err)
   228  	}
   229  
   230  	// Log the success.
   231  	var outputList string
   232  	for _, output := range outputs {
   233  		outputList = outputList + "\n\tAddress: " + output.UnlockHash.String() + "\n\tValue: " + output.Value.HumanString() + "\n"
   234  	}
   235  	w.log.Printf("Successfully broadcast transaction with id %v, fee %v, and the following outputs: %v", txnSet[len(txnSet)-1].ID(), tpoolFee.HumanString(), outputList)
   236  	return txnSet, nil
   237  }
   238  
   239  // SendSiafunds creates a transaction sending 'amount' to 'dest'. The transaction
   240  // is submitted to the transaction pool and is also returned.
   241  func (w *Wallet) SendSiafunds(amount types.Currency, dest types.UnlockHash) (txns []types.Transaction, err error) {
   242  	if err := w.tg.Add(); err != nil {
   243  		err = modules.ErrWalletShutdown
   244  		return nil, err
   245  	}
   246  	defer w.tg.Done()
   247  
   248  	// Check if consensus is synced
   249  	if !w.cs.Synced() || w.deps.Disrupt("UnsyncedConsensus") {
   250  		return nil, errors.New("cannot send siafunds until fully synced")
   251  	}
   252  
   253  	w.mu.RLock()
   254  	unlocked := w.unlocked
   255  	w.mu.RUnlock()
   256  	if !unlocked {
   257  		return nil, modules.ErrLockedWallet
   258  	}
   259  
   260  	_, tpoolFee := w.tpool.FeeEstimation()
   261  	tpoolFee = tpoolFee.Mul64(750) // Estimated transaction size in bytes
   262  	tpoolFee = tpoolFee.Mul64(5)   // use large fee to ensure siafund transactions are selected by miners
   263  	output := types.SiafundOutput{
   264  		Value:      amount,
   265  		UnlockHash: dest,
   266  	}
   267  
   268  	txnBuilder, err := w.StartTransaction()
   269  	if err != nil {
   270  		return nil, err
   271  	}
   272  	defer func() {
   273  		if err != nil {
   274  			txnBuilder.Drop()
   275  		}
   276  	}()
   277  	err = txnBuilder.FundSiacoins(tpoolFee)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	err = txnBuilder.FundSiafunds(amount)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	txnBuilder.AddMinerFee(tpoolFee)
   286  	txnBuilder.AddSiafundOutput(output)
   287  	txnSet, err := txnBuilder.Sign(true)
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  	err = w.tpool.AcceptTransactionSet(txnSet)
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  	w.log.Println("Submitted a siafund transfer transaction set for value", amount.HumanString(), "with fees", tpoolFee.HumanString(), "IDs:")
   296  	for _, txn := range txnSet {
   297  		w.log.Println("\t", txn.ID())
   298  	}
   299  	return txnSet, nil
   300  }
   301  
   302  // Len returns the number of elements in the sortedOutputs struct.
   303  func (so sortedOutputs) Len() int {
   304  	if build.DEBUG && len(so.ids) != len(so.outputs) {
   305  		panic("sortedOutputs object is corrupt")
   306  	}
   307  	return len(so.ids)
   308  }
   309  
   310  // Less returns whether element 'i' is less than element 'j'. The currency
   311  // value of each output is used for comparison.
   312  func (so sortedOutputs) Less(i, j int) bool {
   313  	return so.outputs[i].Value.Cmp(so.outputs[j].Value) < 0
   314  }
   315  
   316  // Swap swaps two elements in the sortedOutputs set.
   317  func (so sortedOutputs) Swap(i, j int) {
   318  	so.ids[i], so.ids[j] = so.ids[j], so.ids[i]
   319  	so.outputs[i], so.outputs[j] = so.outputs[j], so.outputs[i]
   320  }