Skip to content

Commit

Permalink
popm/wasm: add automatic fees (#237)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuasing authored Sep 3, 2024
1 parent 6d68681 commit 43c7abf
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 29 deletions.
24 changes: 22 additions & 2 deletions service/popm/popm.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ type Miner struct {
holdoffTimeout time.Duration
requestTimeout time.Duration

cfg *Config
cfg *Config
txFee atomic.Uint32

btcChainParams *btcchaincfg.Params
btcPrivateKey *dcrsecp256k1.PrivateKey
Expand Down Expand Up @@ -146,6 +147,7 @@ func NewMiner(cfg *Config) (*Miner, error) {
mineNowCh: make(chan struct{}, 1),
l2Keystones: make(map[string]L2KeystoneProcessingContainer, l2KeystonesMaxSize),
}
m.SetFee(cfg.StaticFee)

switch strings.ToLower(cfg.BTCChainName) {
case "mainnet":
Expand All @@ -167,6 +169,24 @@ func NewMiner(cfg *Config) (*Miner, error) {
return m, nil
}

// Fee returns the current fee in sats/vB used by the PoP Miner when
// creating PoP transactions.
func (m *Miner) Fee() uint {
return uint(m.txFee.Load())
}

// SetFee sets the fee in sats/vB used by the PoP Miner when creating
// PoP transactions.
func (m *Miner) SetFee(fee uint) {
switch {
case fee < 0:
fee = 1
case fee > 1<<32-1:
fee = 1<<32 - 1
}
m.txFee.Store(uint32(fee))
}

func (m *Miner) bitcoinBalance(ctx context.Context, scriptHash []byte) (uint64, int64, error) {
br := &bfgapi.BitcoinBalanceRequest{
ScriptHash: scriptHash,
Expand Down Expand Up @@ -355,7 +375,7 @@ func (m *Miner) mineKeystone(ctx context.Context, ks *hemi.L2Keystone) error {

// Estimate BTC fees.
txLen := 285 // XXX: for now all transactions are the same size
feePerKB := 1024 * m.cfg.StaticFee
feePerKB := 1024 * m.Fee()
feeAmount := (int64(txLen) * int64(feePerKB)) / 1024

// Retrieve the current balance for the miner.
Expand Down
23 changes: 23 additions & 0 deletions service/popm/popm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,29 @@ func TestNewMiner(t *testing.T) {
}
}

func TestFees(t *testing.T) {
cfg := NewDefaultConfig()
cfg.StaticFee = 5
cfg.BTCPrivateKey = "ebaaedce6af48a03bbfd25e8cd0364140ebaaedce6af48a03bbfd25e8cd03641"

m, err := NewMiner(cfg)
if err != nil {
t.Fatalf("Failed to create new miner: %v", err)
}

// Make sure that the static fee is used.
if m.Fee() != cfg.StaticFee {
t.Errorf("got fee %d, want %d", m.Fee(), cfg.StaticFee)
}

// Make sure the fee can be changed with SetFee.
const newFee = 8
m.SetFee(newFee)
if m.Fee() != newFee {
t.Errorf("got fee %d, want %d", m.Fee(), newFee)
}
}

// TestProcessReceivedKeystones ensures that we store the latest keystone
// correctly as well as data stored in slices within the struct
func TestProcessReceivedKeystones(t *testing.T) {
Expand Down
33 changes: 32 additions & 1 deletion web/packages/pop-miner/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,17 @@ export declare function removeEventListener(
listener: EventListener,
): Promise<void>;

/**
* The type of recommended fee to use when doing automatic fees using
* the mempool.space REST API.
*/
export type RecommendedFeeType =
| 'fastest'
| 'halfHour'
| 'hour'
| 'economy'
| 'minimum';

/**
* @see startPoPMiner
*/
Expand Down Expand Up @@ -359,7 +370,22 @@ export type MinerStartArgs = {
logLevel?: string;

/**
* The number of stats/vB the PoP Miner will pay for fees.
* Whether to do automatic fee estimation using the mempool.space API.
* Defaults to true. If disabled, then only the staticFee will be used.
*
* When the value is true, the 'economy' recommended fee type will be used.
*/
automaticFees?: boolean | RecommendedFeeType;

/**
* The duration between refreshing recommended fee data from the
* mempool.space API, in seconds. Defaults to 300 seconds (5 minutes).
*/
automaticFeeRefreshSeconds?: number;

/**
* The number of sats/vB the PoP Miner will pay for fees. If automaticFees
* is enabled, then this will be used as a fallback fee value.
*/
staticFee: number;
};
Expand Down Expand Up @@ -392,6 +418,11 @@ export type MinerStatusResult = {
* Whether the PoP miner is currently connected to a BFG server.
*/
readonly connected: boolean;

/**
* The current fee used by the PoP miner for PoP transactions, in sats/vB.
*/
readonly fee: number;
};

/**
Expand Down
36 changes: 36 additions & 0 deletions web/popminer/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
package main

import (
"fmt"
"strings"
"syscall/js"

"github.com/hemilabs/heminetwork/service/popm"
Expand Down Expand Up @@ -140,6 +142,36 @@ type BitcoinAddressToScriptHashResult struct {
ScriptHash string `json:"scriptHash"`
}

// RecommendedFeeType is the type of recommended fee to use when doing automatic
// fees using the mempool.space REST API.
type RecommendedFeeType string

const (
RecommendedFeeTypeFastest RecommendedFeeType = "fastest"
RecommendedFeeTypeHalfHour RecommendedFeeType = "halfHour"
RecommendedFeeTypeHour RecommendedFeeType = "hour"
RecommendedFeeTypeEconomy RecommendedFeeType = "economy"
RecommendedFeeTypeMinimum RecommendedFeeType = "minimum"
)

// ParseRecommendedFeeType parses the given string as a RecommendedFeeType.
func ParseRecommendedFeeType(s string) (RecommendedFeeType, error) {
switch strings.ToLower(s) {
case "fastest":
return RecommendedFeeTypeFastest, nil
case "halfHour":
return RecommendedFeeTypeHalfHour, nil
case "hour":
return RecommendedFeeTypeHour, nil
case "economy":
return RecommendedFeeTypeEconomy, nil
case "minimum":
return RecommendedFeeTypeMinimum, nil
default:
return "", fmt.Errorf("unknown recommended fee type: %q", s)
}
}

// MinerStatusResult contains information about the status of the PoP miner.
// Returned by MethodMinerStatus.
type MinerStatusResult struct {
Expand All @@ -149,6 +181,10 @@ type MinerStatusResult struct {
// Connecting is whether the PoP miner is currently connected to a BFG
// server.
Connected bool `json:"connected"`

// Fee is the current fee used by the PoP miner for PoP transactions,
// in sats/vB.
Fee uint `json:"fee"`
}

// PingResult contains information when pinging the BFG server.
Expand Down
77 changes: 57 additions & 20 deletions web/popminer/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"net/http"
"runtime/debug"
"syscall/js"
"time"
Expand Down Expand Up @@ -342,26 +343,13 @@ func startPoPMiner(_ js.Value, args []js.Value) (any, error) {
return nil, errors.New("miner already started")
}

cfg, err := createMinerConfig(args[0])
m, autoFees, err := newMiner(args[0])
if err != nil {
return nil, err
}

miner, err := popm.NewMiner(cfg)
if err != nil {
return nil, fmt.Errorf("create miner: %w", err)
}

// Add WebAssembly miner event handler
miner.RegisterEventHandler(svc.handleMinerEvent)

ctx, cancel := context.WithCancel(context.Background())
m := &Miner{
ctx: ctx,
cancel: cancel,
Miner: miner,
errCh: make(chan error, 1),
}
m.RegisterEventHandler(svc.handleMinerEvent)
svc.miner = m

// run in background
Expand Down Expand Up @@ -390,11 +378,25 @@ func startPoPMiner(_ js.Value, args []js.Value) (any, error) {
svc.dispatchEvent(EventTypeMinerStop, EventMinerStop{})
}()

if autoFees.enabled {
// Automatic fees are enabled, run the goroutine to retrieve the fees
// at the refresh interval.
m.wg.Add(1)
go m.automaticFees(autoFees.feeType, autoFees.refreshInterval)
}

return js.Null(), nil
}

// createMinerConfig creates a [popm.Config] from the given JavaScript object.
func createMinerConfig(config js.Value) (*popm.Config, error) {
type automaticFeeOptions struct {
enabled bool
feeType RecommendedFeeType
refreshInterval time.Duration
}

// newMiner creates a [popm.Miner] using config options from the given
// JavaScript object.
func newMiner(config js.Value) (*Miner, *automaticFeeOptions, error) {
cfg := popm.NewDefaultConfig()
cfg.BTCPrivateKey = config.Get("privateKey").String()
cfg.StaticFee = uint(config.Get("staticFee").Int())
Expand All @@ -405,21 +407,55 @@ func createMinerConfig(config js.Value) (*popm.Config, error) {
cfg.LogLevel = "popm=ERROR"
}
if err := loggo.ConfigureLoggers(cfg.LogLevel); err != nil {
return nil, errorWithCode(ErrorCodeInvalidValue,
return nil, nil, errorWithCode(ErrorCodeInvalidValue,
fmt.Errorf("configure logger: %w", err))
}

// Network
network := config.Get("network").String()
netOpts, ok := networks[network]
if !ok {
return nil, errorWithCode(ErrorCodeInvalidValue,
return nil, nil, errorWithCode(ErrorCodeInvalidValue,
fmt.Errorf("unknown network: %s", network))
}
cfg.BFGWSURL = netOpts.bfgURL
cfg.BTCChainName = netOpts.btcChainName

return cfg, nil
// Automatic fee options
autoFeeConfig := config.Get("automaticFees")
autoFees := &automaticFeeOptions{
enabled: autoFeeConfig.Truthy(),
feeType: RecommendedFeeTypeEconomy,
refreshInterval: 5 * time.Minute,
}
if autoFeeConfig.Type() == js.TypeString {
// automaticFees is a string, parse the selected recommended fee type.
feeType, err := ParseRecommendedFeeType(autoFeeConfig.String())
if err != nil {
return nil, nil, errorWithCode(ErrorCodeInvalidValue, err)
}
autoFees.feeType = feeType
}
if rf := config.Get("automaticFeeRefreshSeconds"); rf.Truthy() {
autoFees.refreshInterval = time.Duration(rf.Int()) * time.Second
}

// Create new miner
miner, err := popm.NewMiner(cfg)
if err != nil {
return nil, nil, fmt.Errorf("create miner: %w", err)
}

m := &Miner{
Miner: miner,
errCh: make(chan error, 1),
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
mempoolSpaceURL: netOpts.mempoolSpaceURL,
}
m.ctx, m.cancel = context.WithCancel(context.Background())
return m, autoFees, nil
}

func stopPopMiner(_ js.Value, _ []js.Value) (any, error) {
Expand Down Expand Up @@ -453,6 +489,7 @@ func minerStatus(_ js.Value, _ []js.Value) (any, error) {
if err == nil {
status.Running = true
status.Connected = miner.Connected()
status.Fee = miner.Fee()
}

return status, nil
Expand Down
Loading

0 comments on commit 43c7abf

Please sign in to comment.