-
Notifications
You must be signed in to change notification settings - Fork 26
/
fees.go
320 lines (296 loc) · 9.17 KB
/
fees.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
package bt
import (
"encoding/json"
"fmt"
"sync"
"time"
)
// FeeType is used to specify which
// type of fee is used depending on
// the type of tx data (eg: standard
// bytes or data bytes).
type FeeType string
const (
// FeeTypeStandard is the fee type for standard tx parts
FeeTypeStandard FeeType = "standard"
// FeeTypeData is the fee type for data tx parts
FeeTypeData FeeType = "data"
)
// FeeQuotes contains a list of miners and the current fees for each miner as well as their expiry.
//
// This can be used when getting fees from multiple miners, and you want to use the cheapest for example.
//
// Usage setup should be calling NewFeeQuotes(minerName).
type FeeQuotes struct {
mu sync.RWMutex
quotes map[string]*FeeQuote
}
// NewFeeQuotes will set up default feeQuotes for the minerName supplied, ie TAAL etc.
func NewFeeQuotes(minerName string) *FeeQuotes {
return &FeeQuotes{
mu: sync.RWMutex{},
quotes: map[string]*FeeQuote{minerName: NewFeeQuote()},
}
}
// AddMinerWithDefault will add a new miner to the quotes map with default fees & immediate expiry.
func (f *FeeQuotes) AddMinerWithDefault(minerName string) *FeeQuotes {
f.mu.Lock()
defer f.mu.Unlock()
f.quotes[minerName] = NewFeeQuote()
return f
}
// AddMiner will add a new miner to the quotes map with the provided feeQuote.
// If you just want to add default fees use the AddMinerWithDefault method.
func (f *FeeQuotes) AddMiner(minerName string, quote *FeeQuote) *FeeQuotes {
f.mu.Lock()
defer f.mu.Unlock()
f.quotes[minerName] = quote
return f
}
// Quote will return all fees for a miner.
// If no fees are found an ErrMinerNoQuotes error is returned.
func (f *FeeQuotes) Quote(minerName string) (*FeeQuote, error) {
if f == nil {
return nil, ErrFeeQuotesNotInit
}
f.mu.RLock()
defer f.mu.RUnlock()
q, ok := f.quotes[minerName]
if !ok {
return nil, ErrMinerNoQuotes
}
return q, nil
}
// Fee is a convenience method for quickly getting a fee by type and miner name.
// If the miner has no fees an ErrMinerNoQuotes error will be returned.
// If the feeType cannot be found an ErrFeeTypeNotFound error will be returned.
func (f *FeeQuotes) Fee(minerName string, feeType FeeType) (*Fee, error) {
if f == nil {
return nil, ErrFeeQuotesNotInit
}
f.mu.RLock()
defer f.mu.RUnlock()
m := f.quotes[minerName]
if m == nil {
return nil, ErrMinerNoQuotes
}
return m.Fee(feeType)
}
// UpdateMinerFees a convenience method to update a fee quote from a FeeQuotes struct directly.
// This will update the miner feeType with the provided fee. Useful after receiving new quotes from mapi.
func (f *FeeQuotes) UpdateMinerFees(minerName string, feeType FeeType, fee *Fee) (*FeeQuote, error) {
f.mu.Lock()
defer f.mu.Unlock()
if minerName == "" || feeType == "" || fee == nil {
return nil, ErrEmptyValues
}
m := f.quotes[minerName]
if m == nil {
return nil, ErrMinerNoQuotes
}
return m.AddQuote(feeType, fee), nil
}
// FeeQuote contains a thread safe map of fees for standard and data
// fees as well as an expiry time for a specific miner.
//
// This can be used if you are only dealing with a single miner and know you
// will always be using a single miner.
// FeeQuote will store the fees for a single miner and can be passed to transactions
// to calculate fees when creating change outputs.
//
// If you are dealing with quotes from multiple miners, use the FeeQuotes structure above.
//
// NewFeeQuote() should be called to get a new instance of a FeeQuote.
//
// When expiry expires ie Expired() == true then you should fetch
// new quotes from a MAPI server and call AddQuote with the fee information.
type FeeQuote struct {
mu sync.RWMutex
fees map[FeeType]*Fee
expiryTime time.Time
}
// NewFeeQuote will set up and return a new FeeQuotes struct which
// contains default fees when initially setup. You would then pass this
// data structure to a singleton struct via injection for reading.
// If you are only getting quotes from one miner you can use this directly
// instead of using the NewFeeQuotes() method which is for storing multiple miner quotes.
//
// fq := NewFeeQuote()
//
// The fees have an expiry time which, when initially setup, has an
// expiry of now.UTC. This allows you to check for fq.Expired() and if true
// fetch a new set of fees from a MAPI server. This means the first check
// will always fetch the latest fees. If you want to just use default fees
// always, you can ignore the expired method and simply call the fq.Fee() method.
// https://github.com/bitcoin-sv-specs/brfc-merchantapi#payload
//
// A basic example of usage is shown below:
//
// func Fee(ft bt.FeeType) *bt.Fee{
// // you would not call this every time - this is just an example
// // you'd call this at app startup and store it / pass to a struct
// fq := NewFeeQuote()
//
// // fq setup with defaultFees
// if !fq.Expired() {
// // not expired, just return fee we have cached
// return fe.Fee(ft)
// }
//
// // cache expired, fetch new quotes
// var stdFee *bt.Fee
// var dataFee *bt.Fee
//
// // fetch quotes from MAPI server
//
// fq.AddQuote(bt.FeeTypeStandard, stdFee)
// fq.AddQuote(bt.FeeTypeData, dataFee)
//
// // MAPI returns a quote expiry
// exp, _ := time.Parse(time.RFC3339, resp.Quote.ExpirationTime)
// fq.UpdateExpiry(exp)
// return fe.Fee(ft)
// }
// It will set the expiry time to now.UTC which when expires
// will indicate that new quotes should be fetched from a MAPI server.
func NewFeeQuote() *FeeQuote {
fq := &FeeQuote{
fees: map[FeeType]*Fee{},
expiryTime: time.Now().UTC(),
mu: sync.RWMutex{},
}
fq.AddQuote(FeeTypeStandard, defaultStandardFee()).
AddQuote(FeeTypeData, defaultDataFee())
return fq
}
// Fee will return a fee by type if found, nil and an error if not.
func (f *FeeQuote) Fee(t FeeType) (*Fee, error) {
if f == nil {
return nil, ErrFeeQuoteNotInit
}
f.mu.RLock()
defer f.mu.RUnlock()
fee, ok := f.fees[t]
if fee == nil || !ok {
return nil, ErrFeeTypeNotFound
}
return fee, nil
}
// AddQuote will add new set of quotes for a feetype or update an existing
// quote if it already exists.
func (f *FeeQuote) AddQuote(ft FeeType, fee *Fee) *FeeQuote {
f.mu.Lock()
defer f.mu.Unlock()
f.fees[ft] = fee
return f
}
// Expiry will return the expiry timestamp for the `bt.FeeQuote` in a threadsafe manner.
func (f *FeeQuote) Expiry() time.Time {
f.mu.RLock()
defer f.mu.RUnlock()
return f.expiryTime
}
// UpdateExpiry will update the expiry time of the quotes, this will be
// used when you fetch a fresh set of quotes from a MAPI server which
// should return an expiration time.
func (f *FeeQuote) UpdateExpiry(exp time.Time) {
f.mu.Lock()
defer f.mu.Unlock()
f.expiryTime = exp
}
// Expired will return true if the expiry time is before UTC now, this
// means we need to fetch fresh quotes from a MAPI server.
func (f *FeeQuote) Expired() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.expiryTime.Before(time.Now().UTC())
}
// MarshalJSON will convert the FeeQuote to a json object
// with the format as shown:
// {
// "data": {
// "miningFee": {
// "satoshis": 5,
// "bytes": 2
// },
// "relayFee": {
// "satoshis": 8,
// "bytes": 4
// }
// },
// "standard": {
// "miningFee": {
// "satoshis": 100,
// "bytes": 10
// },
// "relayFee": {
// "satoshis": 10,
// "bytes": 5
// }
// }
// }
func (f *FeeQuote) MarshalJSON() ([]byte, error) {
return json.Marshal(f.fees)
}
// UnmarshalJSON will convert a json encoded FeeQuote back into a fee quote type, the expected
// JSON format is shown above in the MarshalJSON function.
// If the fee type supplied is unknown an ErrUnknownFeeType will be returned.
func (f *FeeQuote) UnmarshalJSON(body []byte) error {
fees := map[FeeType]*Fee{}
if err := json.Unmarshal(body, &fees); err != nil {
return err
}
for k, v := range fees {
if k != FeeTypeData && k != FeeTypeStandard {
return fmt.Errorf("%w '%s'", ErrUnknownFeeType, k)
}
v.FeeType = k
}
f.fees = fees
return nil
}
// FeeUnit displays the amount of Satoshis needed
// for a specific amount of Bytes in a transaction
// see https://github.com/bitcoin-sv-specs/brfc-misc/tree/master/feespec
type FeeUnit struct {
Satoshis int `json:"satoshis"` // Fee in satoshis of the amount of Bytes
Bytes int `json:"bytes"` // Number of bytes that the Fee covers
}
// Fee displays the MiningFee as well as the RelayFee for a specific
// FeeType, for example 'standard' or 'data'
// see https://github.com/bitcoin-sv-specs/brfc-misc/tree/master/feespec
type Fee struct {
FeeType FeeType `json:"-"` // standard || data
MiningFee FeeUnit `json:"miningFee"`
RelayFee FeeUnit `json:"relayFee"` // Fee for retaining Tx in secondary mempool
}
// defaultStandardFee returns the default
// standard fees offered by most miners.
func defaultStandardFee() *Fee {
return &Fee{
FeeType: FeeTypeStandard,
MiningFee: FeeUnit{
Satoshis: 5,
Bytes: 100,
},
RelayFee: FeeUnit{
Satoshis: 5,
Bytes: 100,
},
}
}
// defaultDataFee returns the default
// data fees offered by most miners.
func defaultDataFee() *Fee {
return &Fee{
FeeType: FeeTypeData,
MiningFee: FeeUnit{
Satoshis: 5,
Bytes: 100,
},
RelayFee: FeeUnit{
Satoshis: 5,
Bytes: 100,
},
}
}