This repository has been archived by the owner on May 16, 2019. It is now read-only.
forked from cjepson/dcrticketbuyer
-
Notifications
You must be signed in to change notification settings - Fork 7
/
buyer.go
704 lines (637 loc) · 21.7 KB
/
buyer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
// Copyright (c) 2016 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package main
import (
"encoding/csv"
"fmt"
"io"
"math"
"os"
"path/filepath"
"strconv"
"sync/atomic"
"github.com/decred/dcrrpcclient"
"github.com/decred/dcrutil"
)
var (
// zeroUint32 is the zero value for a uint32.
zeroUint32 = uint32(0)
// useMeanStr is the string indicating that the mean ticket fee
// should be used when determining ticket fee.
useMeanStr = "mean"
// useMedianStr is the string indicating that the median ticket fee
// should be used when determining ticket fee.
useMedianStr = "median"
// useVWAPStr is the string indicating that the volume
// weighted average price should be used as the price target.
useVWAPStr = "vwap"
// usePoolPriceStr is the string indicating that the ticket pool
// price should be used as the price target.
usePoolPriceStr = "pool"
// useDualPriceStr is the string indicating that a combination of the
// ticket pool price and the ticket VWAP should be used as the
// price target.
useDualPriceStr = "dual"
)
// purchaseManager is the main handler of websocket notifications to
// pass to the purchaser and internal quit notifications.
type purchaseManager struct {
purchaser *ticketPurchaser
blockConnectedChan chan int64
quit chan struct{}
}
// newPurchaseManager creates a new purchaseManager.
func newPurchaseManager(purchaser *ticketPurchaser,
blockConnChan chan int64,
quit chan struct{}) *purchaseManager {
return &purchaseManager{
purchaser: purchaser,
blockConnectedChan: blockConnChan,
quit: quit,
}
}
// blockConnectedHandler handles block connected notifications, which trigger
// ticket purchases.
func (p *purchaseManager) blockConnectedHandler() {
out:
for {
select {
case height := <-p.blockConnectedChan:
daemonLog.Infof("Block height %v connected", height)
atomic.StoreInt64(&glChainHeight, height)
err := p.purchaser.purchase(height)
if err != nil {
log.Errorf("Failed to purchase tickets this round: %s",
err.Error())
}
// TODO Poll every couple minute to check if connected;
// if not, try to reconnect.
case <-p.quit:
break out
}
}
}
// ticketPurchaser is the main handler for purchasing tickets. It decides
// whether or not to do so based on information obtained from daemon and
// wallet chain servers.
//
// The variables at the end handle a simple "queue" of tickets to purchase,
// which is equal to the number in toBuyDiffPeriod. toBuyDiffPeriod gets
// reset when we enter a new difficulty period because a new block has been
// connected that is outside the previous difficulty period. The variable
// purchasedDiffPeriod tracks the number purchased in this period.
type ticketPurchaser struct {
cfg *config
dcrdChainSvr *dcrrpcclient.Client
dcrwChainSvr *dcrrpcclient.Client
ticketAddress dcrutil.Address
poolAddress dcrutil.Address
firstStart bool
windowPeriod int // The current window period
idxDiffPeriod int // Relative block index within the difficulty period
toBuyDiffPeriod int // Number to buy in this period
purchasedDiffPeriod int // Number already bought in this period
prevToBuyDiffPeriod int // Number of tickets to buy this period from a previous instance
prevToBuyHeight int // The height from the last time buy diff period was recorded in csv
maintainMaxPrice bool // Flag for maximum price manipulation
maintainMinPrice bool // Flag for minimum price manipulation
useMedian bool // Flag for using median for ticket fees
priceMode avgPriceMode // Price mode to use to calc average price
heightCheck map[int64]struct{}
}
// newTicketPurchaser creates a new ticketPurchaser.
func newTicketPurchaser(cfg *config,
dcrdChainSvr *dcrrpcclient.Client,
dcrwChainSvr *dcrrpcclient.Client) (*ticketPurchaser, error) {
var ticketAddress dcrutil.Address
var err error
if cfg.TicketAddress != "" {
ticketAddress, err = dcrutil.DecodeAddress(cfg.TicketAddress,
activeNet.Params)
if err != nil {
return nil, err
}
}
var poolAddress dcrutil.Address
if cfg.PoolAddress != "" {
poolAddress, err = dcrutil.DecodeNetworkAddress(cfg.PoolAddress)
if err != nil {
return nil, err
}
}
maintainMaxPrice := false
if cfg.MaxPriceScale > 0.0 {
maintainMaxPrice = true
}
maintainMinPrice := false
if cfg.MinPriceScale > 0.0 {
maintainMinPrice = true
}
priceMode := avgPriceMode(AvgPriceVWAPMode)
switch cfg.AvgPriceMode {
case usePoolPriceStr:
priceMode = AvgPricePoolMode
case useDualPriceStr:
priceMode = AvgPriceDualMode
}
// Here we attempt to load purchased.csv from the webui dir. This
// allows us to attempt to see if there have been previous ticketbuyer
// instances during the current stakediff window and reuse the
// previously tracked amount of tickets to purchase during that window.
f, err := os.OpenFile(filepath.Join(csvPath, csvPurchasedFn),
os.O_RDONLY|os.O_CREATE, 0600)
if err != nil {
log.Errorf("Error opening file: %v", err)
return nil, err
}
defer f.Close()
rdr := csv.NewReader(f)
rdr.Comma = ','
prevRecord := []string{}
for {
record, err := rdr.Read()
if err != nil {
if err == io.EOF {
break
}
}
prevRecord = record
}
// Attempt to parse last line in purchased.csv, then set previous amounts.
prevToBuyDiffPeriod := 0
lastHeight := 0
if len(prevRecord) >= 3 {
if prevRecord[1] == "RemainingToBuy" {
lastHeight, err = strconv.Atoi(prevRecord[0])
if err != nil {
log.Errorf("Could not parse last height from "+
"csv: %v", err)
}
prevToBuyDiffPeriod, err = strconv.Atoi(prevRecord[2])
if err != nil {
log.Errorf("Could not parse remaining to buy "+
"from csv %v", err)
}
}
}
return &ticketPurchaser{
cfg: cfg,
dcrdChainSvr: dcrdChainSvr,
dcrwChainSvr: dcrwChainSvr,
firstStart: true,
ticketAddress: ticketAddress,
poolAddress: poolAddress,
maintainMaxPrice: maintainMaxPrice,
maintainMinPrice: maintainMinPrice,
useMedian: cfg.FeeSource == useMedianStr,
priceMode: priceMode,
heightCheck: make(map[int64]struct{}),
prevToBuyDiffPeriod: prevToBuyDiffPeriod,
prevToBuyHeight: lastHeight,
}, nil
}
// purchase is the main handler for purchasing tickets for the user.
// TODO Not make this an inlined pile of crap.
func (t *ticketPurchaser) purchase(height int64) error {
// Check to make sure that the current height has not already been seen
// for a reorg or a fork
if _, exists := t.heightCheck[height]; exists {
log.Tracef("We've already seen this height, "+
"reorg/fork detected at height %v", height)
return nil
}
t.heightCheck[height] = struct{}{}
// Initialize webserver update data. If the webserver is
// enabled, defer a function that writes this data to the
// disk.
var csvData csvUpdateData
csvData.height = height
if t.cfg.HTTPSvrPort != 0 {
// Write ticket fee info for the current block to the
// CSV update data.
oneBlock := uint32(1)
info, err := t.dcrdChainSvr.TicketFeeInfo(&oneBlock, &zeroUint32)
if err != nil {
return err
}
csvData.tfMin = info.FeeInfoBlocks[0].Min
csvData.tfMax = info.FeeInfoBlocks[0].Max
csvData.tfMedian = info.FeeInfoBlocks[0].Median
csvData.tfMean = info.FeeInfoBlocks[0].Mean
// The expensive call to fetch all tickets in the mempool
// is here.
all, err := t.allTicketsInMempool()
if err != nil {
return err
}
csvData.tnAll = all
defer func() {
err := writeToCsvFiles(csvData)
if err != nil {
log.Errorf("Failed to write CSV graph data: %s",
err.Error())
}
}()
}
// Just starting up, initialize our purchaser and start
// buying. Set the start up regular transaction fee here
// too.
winSize := activeNet.StakeDiffWindowSize
fillTicketQueue := false
if t.firstStart {
t.idxDiffPeriod = int(height % winSize)
t.windowPeriod = int(height / winSize)
fillTicketQueue = true
t.firstStart = false
log.Tracef("First run time, initialized idxDiffPeriod to %v",
t.idxDiffPeriod)
txFeeAmt, err := dcrutil.NewAmount(t.cfg.TxFee)
if err != nil {
log.Errorf("Failed to decode tx fee amount %v from config",
t.cfg.TxFee)
} else {
errSet := t.dcrwChainSvr.SetTxFee(txFeeAmt)
if errSet != nil {
log.Errorf("Failed to set tx fee amount %v in wallet",
txFeeAmt)
} else {
log.Tracef("Setting of network regular tx relay fee to %v "+
"was successful", txFeeAmt)
}
}
}
// Move the respective cursors for our positions
// in the blockchain.
t.idxDiffPeriod = int(height % winSize)
t.windowPeriod = int(height / winSize)
// The general case initialization for this function. It
// sets our index in the difficulty period, and then
// decides if it needs to fill the queue with tickets to
// purchase.
// Check to see if we're in a new difficulty period.
// Roll over all of our variables if this is true.
if (height+1)%winSize == 0 {
log.Tracef("Resetting stake window ticket variables "+
"at height %v", height)
t.toBuyDiffPeriod = 0
t.purchasedDiffPeriod = 0
fillTicketQueue = true
}
// We may have disconnected and reconnected in a
// different window period. If this is the case,
// we need reset our variables too.
thisWindowPeriod := int(height / winSize)
if (height+1)%winSize != 0 &&
thisWindowPeriod > t.windowPeriod {
log.Tracef("Detected assymetry in this window period versus "+
"stored window period, resetting purchase orders at "+
"height %v", height)
t.toBuyDiffPeriod = 0
t.purchasedDiffPeriod = 0
fillTicketQueue = true
}
// Parse the ticket purchase frequency. Positive numbers mean
// that many tickets per block. Negative numbers mean to only
// purchase one ticket once every abs(num) blocks.
maxPerBlock := 0
switch {
case t.cfg.MaxPerBlock == 0:
return nil
case t.cfg.MaxPerBlock > 0:
maxPerBlock = t.cfg.MaxPerBlock
case t.cfg.MaxPerBlock < 0:
if int(height)%t.cfg.MaxPerBlock != 0 {
return nil
}
maxPerBlock = 1
}
// Make sure that our wallet is connected to the daemon and the
// wallet is unlocked, otherwise abort.
walletInfo, err := t.dcrwChainSvr.WalletInfo()
if err != nil {
return err
}
if !walletInfo.DaemonConnected {
return fmt.Errorf("Wallet not connected to daemon")
}
if !walletInfo.Unlocked {
return fmt.Errorf("Wallet not unlocked to allow ticket purchases")
}
avgPriceAmt, err := t.calcAverageTicketPrice(height)
if err != nil {
return fmt.Errorf("Failed to calculate average ticket price amount: %s",
err.Error())
}
avgPrice := avgPriceAmt.ToCoin()
log.Tracef("Calculated average ticket price: %v", avgPriceAmt)
csvData.tpAverage = avgPrice
stakeDiffs, err := t.dcrwChainSvr.GetStakeDifficulty()
if err != nil {
return err
}
nextStakeDiff, err := dcrutil.NewAmount(stakeDiffs.NextStakeDifficulty)
if err != nil {
return err
}
csvData.tpCurrent = stakeDiffs.NextStakeDifficulty
atomic.StoreInt64(&glTicketPrice, int64(nextStakeDiff))
sDiffEsts, err := t.dcrdChainSvr.EstimateStakeDiff(nil)
if err != nil {
return err
}
csvData.tpNext = sDiffEsts.Expected
// Scale the average price according to the configuration parameters
// to find minimum and maximum prices for users that are electing to
// attempting to manipulate the stake difficulty.
maxPriceAbsAmt, err := dcrutil.NewAmount(t.cfg.MaxPriceAbsolute)
if err != nil {
return err
}
maxPriceScaledAmt, err := dcrutil.NewAmount(t.cfg.MaxPriceScale * avgPrice)
if err != nil {
return err
}
if t.maintainMaxPrice {
log.Tracef("The maximum price to maintain for this round is set to %v",
maxPriceScaledAmt)
}
csvData.tpMaxScale = maxPriceScaledAmt.ToCoin()
minPriceScaledAmt, err := dcrutil.NewAmount(t.cfg.MinPriceScale * avgPrice)
if err != nil {
return err
}
if t.maintainMinPrice {
log.Tracef("The minimum price to maintain for this round is set to %v",
minPriceScaledAmt)
}
csvData.tpMinScale = minPriceScaledAmt.ToCoin()
balSpendable, err := t.dcrwChainSvr.GetBalanceMinConfType(t.cfg.AccountName,
0, "spendable")
if err != nil {
return err
}
log.Debugf("Current spendable balance at height %v for account '%s': %v",
height, t.cfg.AccountName, balSpendable)
// Checking to see if previous amount buy tickets and height are set,
// then check to make sure that it was from the same current stake
// diff window.
if t.prevToBuyDiffPeriod != 0 && t.prevToBuyHeight != 0 {
prevToBuyWindow := int(t.prevToBuyHeight / int(winSize))
if t.windowPeriod == prevToBuyWindow {
log.Debugf("Previous tickets to buy amount for this "+
"window was found. Using %v for buy ticket amount.",
t.prevToBuyDiffPeriod)
fillTicketQueue = false
t.toBuyDiffPeriod = t.prevToBuyDiffPeriod
t.prevToBuyDiffPeriod = 0
}
}
// Check for new balance credits and update ticket queue if necessary
// TODO: remove atomic on glBalance is possible
balEstimated := atomic.LoadInt64(&glBalance)
if int64(balSpendable)-int64(balEstimated) > atomic.LoadInt64(&glTicketPrice) {
log.Tracef("Current balance %v is greater than estimated "+
"balance: %v", int64(balSpendable), int64(balEstimated))
fillTicketQueue = true
}
// This is the main portion that handles filling up the
// queue of tickets to purchase (t.toBuyDiffPeriod).
if fillTicketQueue {
// Calculate how many tickets we could possibly buy
// at this difficulty. Adjust for already purchased tickets
// in case of new balance credits.
curPrice := nextStakeDiff
balSpent := float64(t.purchasedDiffPeriod) * nextStakeDiff.ToCoin()
couldBuy := math.Floor((balSpendable.ToCoin() + balSpent) / nextStakeDiff.ToCoin())
// Calculate the remaining tickets that could possibly be
// mined in the current window. If couldBuy is greater than
// possible amount than reduce to that amount
diffPeriod := int64(0)
if (height+1)%winSize != 0 {
// In the middle of a window
diffPeriod = int64(t.idxDiffPeriod)
}
ticketsLeftInWindow := (winSize - diffPeriod) *
int64(activeNet.MaxFreshStakePerBlock)
if couldBuy > float64(ticketsLeftInWindow) {
log.Debugf("The total ticket allotment left in this stakediff window is %v. "+
"So this wallet's possible tickets that could be bought is %v so it"+
" has been reduced to %v.",
ticketsLeftInWindow, couldBuy, ticketsLeftInWindow)
couldBuy = float64(ticketsLeftInWindow)
}
// Override the target price being the average price if
// the user has elected to attempt to modify the ticket
// price.
targetPrice := avgPrice
if t.cfg.PriceTarget > 0.0 {
targetPrice = t.cfg.PriceTarget
}
// The target price can not be above the maximum scaled
// price of tickets that the user has elected to maintain.
// If it is, set the target to the scaled maximum instead
// and warn the user.
if t.maintainMaxPrice && targetPrice > maxPriceScaledAmt.ToCoin() {
targetPrice = maxPriceScaledAmt.ToCoin()
log.Warnf("The target price %v that was set to be maintained "+
"was above the allowable scaled maximum of %v, so the "+
"scaled maximum is being used as the target",
t.cfg.PriceTarget, maxPriceScaledAmt)
}
// Decay exponentially if the price is above the ideal or target
// price.
// floor(penalty ^ -(abs(ticket price - average ticket price)))
// Then multiply by the number of tickets we could possibly
// buy.
if curPrice.ToCoin() > targetPrice {
toBuy := math.Floor(math.Pow(t.cfg.HighPricePenalty,
-(math.Abs(curPrice.ToCoin()-targetPrice))) * couldBuy)
t.toBuyDiffPeriod = int(float64(toBuy))
log.Debugf("The current price %v is above the target price %v, "+
"so the number of tickets to buy this window was "+
"scaled from %v to %v", curPrice, targetPrice, couldBuy,
t.toBuyDiffPeriod)
} else {
// Below or equal to the average price. Buy as many
// tickets as possible.
t.toBuyDiffPeriod = int(float64(couldBuy))
log.Debugf("The stake difficulty %v was below the target penalty "+
"cutoff %v; %v many tickets have been queued for purchase",
curPrice, targetPrice, t.toBuyDiffPeriod)
}
}
csvData.leftWindow = t.toBuyDiffPeriod
// Disable purchasing if the ticket price is too high based on
// the absolute cutoff or if the estimated ticket price is above
// our scaled cutoff based on the ideal ticket price.
if nextStakeDiff > maxPriceAbsAmt {
log.Tracef("Aborting ticket purchases because the ticket price %v "+
"is higher than the maximum absolute price %v", nextStakeDiff,
maxPriceAbsAmt)
return nil
}
if t.maintainMaxPrice && (sDiffEsts.Expected > maxPriceScaledAmt.ToCoin()) &&
maxPriceScaledAmt != 0 {
log.Tracef("Aborting ticket purchases because the ticket price "+
"next window estimate %v is higher than the maximum scaled "+
"price %v", sDiffEsts.Expected, maxPriceScaledAmt)
return nil
}
// If we still have tickets in the memory pool, don't try
// to buy even more tickets.
inMP, err := t.ownTicketsInMempool()
if err != nil {
return err
}
csvData.tnOwn = inMP
if !t.cfg.DontWaitForTickets {
if inMP > t.cfg.MaxInMempool {
log.Debugf("Currently waiting for %v tickets to enter the "+
"blockchain before buying more tickets (in mempool: %v,"+
" max allowed in mempool %v)", inMP-t.cfg.MaxInMempool,
inMP, t.cfg.MaxInMempool)
return nil
}
}
// If might be the case that there weren't enough recent
// blocks to average fees from. Use data from the last
// window with the closest difficulty.
chainFee := 0.0
if t.idxDiffPeriod < t.cfg.BlocksToAvg {
chainFee, err = t.findClosestFeeWindows(nextStakeDiff.ToCoin(),
t.useMedian)
if err != nil {
return err
}
} else {
chainFee, err = t.findTicketFeeBlocks(t.useMedian)
if err != nil {
return err
}
}
// Scale the mean fee upwards according to what was asked
// for by the user.
feeToUse := chainFee * t.cfg.FeeTargetScaling
if feeToUse > t.cfg.MaxFee {
log.Tracef("Scaled fee is %v, but max fee is %v; using max",
feeToUse, t.cfg.MaxFee)
feeToUse = t.cfg.MaxFee
}
if feeToUse < t.cfg.MinFee {
log.Tracef("Scaled fee is %v, but min fee is %v; using min",
feeToUse, t.cfg.MinFee)
feeToUse = t.cfg.MinFee
}
feeToUseAmt, err := dcrutil.NewAmount(feeToUse)
if err != nil {
return err
}
err = t.dcrwChainSvr.SetTicketFee(feeToUseAmt)
if err != nil {
return err
}
log.Debugf("Mean fee for the last blocks or window period was %v; "+
"this was scaled to %v", chainFee, feeToUse)
csvData.tfOwn = feeToUse
// Only the maximum number of tickets at each block
// should be purchased, as specified by the user.
toBuyForBlock := t.toBuyDiffPeriod - t.purchasedDiffPeriod
if toBuyForBlock > maxPerBlock {
toBuyForBlock = maxPerBlock
}
// Hijack the number to purchase for this block if we have minimum
// ticket price manipulation enabled.
if t.maintainMinPrice && toBuyForBlock < maxPerBlock {
if sDiffEsts.Expected < minPriceScaledAmt.ToCoin() {
toBuyForBlock = maxPerBlock
log.Debugf("Attempting to manipulate the stake difficulty "+
"so that the price does not fall below the set minimum "+
"%v (current estimate for next stake difficulty: %v) by "+
"purchasing an additional round of tickets",
minPriceScaledAmt, sDiffEsts.Expected)
}
}
// We've already purchased all the tickets we need to.
if toBuyForBlock <= 0 {
log.Tracef("All tickets have been purchased, aborting further " +
"ticket purchases")
return nil
}
// Check our balance versus the amount of tickets we need to buy.
// If there is not enough money, decrement and recheck the balance
// to see if fewer tickets may be purchased. Abort if we don't
// have enough moneys.
notEnough := func(bal dcrutil.Amount, toBuy int, sd dcrutil.Amount) bool {
return (bal.ToCoin() - float64(toBuy)*sd.ToCoin()) <
t.cfg.BalanceToMaintain
}
if notEnough(balSpendable, toBuyForBlock, nextStakeDiff) {
for notEnough(balSpendable, toBuyForBlock, nextStakeDiff) {
if toBuyForBlock == 0 {
break
}
toBuyForBlock--
}
if toBuyForBlock == 0 {
log.Tracef("Aborting purchasing of tickets because our balance "+
"after buying tickets is estimated to be %v but balance "+
"to maintain is set to %v",
(balSpendable.ToCoin() - float64(toBuyForBlock)*
nextStakeDiff.ToCoin()),
t.cfg.BalanceToMaintain)
return nil
}
}
// If an address wasn't passed, create an internal address in
// the wallet for the ticket address.
var ticketAddress dcrutil.Address
if t.ticketAddress != nil {
ticketAddress = t.ticketAddress
} else {
ticketAddress, err =
t.dcrwChainSvr.GetRawChangeAddress(t.cfg.AccountName)
if err != nil {
return err
}
}
// Purchase tickets.
poolFeesAmt, err := dcrutil.NewAmount(t.cfg.PoolFees)
if err != nil {
return err
}
minConf := 0
expiry := int(height) + t.cfg.ExpiryDelta
tickets, err := t.dcrwChainSvr.PurchaseTicket(t.cfg.AccountName,
maxPriceAbsAmt,
&minConf,
ticketAddress,
&toBuyForBlock,
t.poolAddress,
&poolFeesAmt,
&expiry)
if err != nil {
return err
}
t.purchasedDiffPeriod += toBuyForBlock
csvData.purchased = toBuyForBlock
csvData.leftWindow = t.toBuyDiffPeriod - t.purchasedDiffPeriod
for i := range tickets {
log.Infof("Purchased ticket %v at stake difficulty %v (%v "+
"fees per KB used)", tickets[i], nextStakeDiff.ToCoin(),
feeToUseAmt.ToCoin())
}
log.Debugf("Tickets purchased so far in this window: %v",
t.purchasedDiffPeriod)
log.Debugf("Tickets remaining to be purchased in this window: %v",
t.toBuyDiffPeriod-t.purchasedDiffPeriod)
balSpendable, err = t.dcrwChainSvr.GetBalanceMinConfType(t.cfg.AccountName,
0, "spendable")
if err != nil {
return err
}
log.Debugf("Final spendable balance at height %v for account '%s' "+
"after ticket purchases: %v", height, t.cfg.AccountName, balSpendable)
atomic.StoreInt64(&glBalance, int64(balSpendable))
return nil
}