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 }