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 }