Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement/fee quotes #18

Merged
merged 17 commits into from
Aug 18, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions bscript/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,7 @@ func NewAddressFromPublicKeyHash(hash []byte, mainnet bool) (*Address, error) {
if !mainnet {
bb[0] = 111
}

// nolint: makezero // this needs init this way
// nolint: makezero // we need to setup the array with 1
bb = append(bb, hash...)

return &Address{
Expand All @@ -106,7 +105,7 @@ func NewAddressFromPublicKey(pubKey *bec.PublicKey, mainnet bool) (*Address, err
if !mainnet {
bb[0] = 111
}
// nolint: makezero // this needs init this way
// nolint: makezero // we need to setup the array with 1
bb = append(bb, hash...)

return &Address{
Expand Down
243 changes: 211 additions & 32 deletions fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ package bt

import (
"errors"
"sync"
"time"
)

// Sentinel errors reported by the fees.
var (
ErrFeeQuotesNotInit = errors.New("feeQuotes have not been setup, call NewFeeQuotes")
ErrMinerNoQuotes = errors.New("miner has no quotes stored")
ErrFeeTypeNotFound = errors.New("feetype not found")
ErrFeeQuoteNotInit = errors.New("feeQuote has not been initialised, call NewFeeQuote()")
ErrEmptyValues = errors.New("empty value or values passed, all arguments are required and cannot be empty")
)

// FeeType is used to specify which
Expand All @@ -18,6 +29,202 @@ const (
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 setup 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, errors.New("empty value or values passed, all arguments are required and cannot be empty")
}
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 setup 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
}

// 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())
}

// 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
Expand All @@ -35,9 +242,9 @@ type Fee struct {
RelayFee FeeUnit `json:"relayFee"` // Fee for retaining Tx in secondary mempool
}

// DefaultStandardFee returns the default
// defaultStandardFee returns the default
// standard fees offered by most miners.
func DefaultStandardFee() *Fee {
func defaultStandardFee() *Fee {
return &Fee{
FeeType: FeeTypeStandard,
MiningFee: FeeUnit{
Expand All @@ -51,9 +258,9 @@ func DefaultStandardFee() *Fee {
}
}

// DefaultDataFee returns the default
// defaultDataFee returns the default
// data fees offered by most miners.
func DefaultDataFee() *Fee {
func defaultDataFee() *Fee {
return &Fee{
FeeType: FeeTypeData,
MiningFee: FeeUnit{
Expand All @@ -66,31 +273,3 @@ func DefaultDataFee() *Fee {
},
}
}

// DefaultFees returns an array of the default
// standard and data fees offered by most miners.
func DefaultFees() (f []*Fee) {
f = append(f, DefaultStandardFee())
f = append(f, DefaultDataFee())
return
}

// ExtractStandardFee returns the standard fee in the fees array supplied.
func ExtractStandardFee(fees []*Fee) (*Fee, error) {
return extractFeeType(FeeTypeStandard, fees)
}

// ExtractDataFee returns the data fee in the fees array supplied.
func ExtractDataFee(fees []*Fee) (*Fee, error) {
return extractFeeType(FeeTypeData, fees)
}

func extractFeeType(ft FeeType, fees []*Fee) (*Fee, error) {
for _, f := range fees {
if f.FeeType == ft {
return f, nil
}
}

return nil, errors.New("no " + string(ft) + " fee supplied")
}
Loading