Skip to content

Commit f99c1bb

Browse files
committed
configurable fee rate
1 parent 5a9b8e6 commit f99c1bb

7 files changed

Lines changed: 76 additions & 2 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ Only enable a client-IP header when the reverse proxy overwrites it and the
3636
origin cannot be reached directly. Otherwise a client can spoof the address
3737
used by the payout limits and admin IP allowlist.
3838

39+
### payout fee rate
40+
41+
Automatic faucet payouts use `0.11 sat/vB` by default. Set a different rate
42+
with `-payout-fee-rate-sats-per-vb`, or with the
43+
`FAUCET_PAYOUT_FEE_RATE_SATS_PER_VB` environment variable. The command-line
44+
flag takes precedence over the environment variable. Set the value to `0` to
45+
omit `fee_rate` and let the Bitcoin Core wallet select its default rate.
46+
47+
```sh
48+
-payout-fee-rate-sats-per-vb=0.25
49+
```
3950

4051
### quick run with docker
4152

@@ -65,6 +76,7 @@ docker run \
6576
-turnstile-secret="0x4AAAAAA..." \
6677
-data-dir /data \
6778
-batch-interval=10s \
79+
-payout-fee-rate-sats-per-vb=0.25 \
6880
-consolidation-amount-threshold=2.1 \
6981
-consolidation-min-utxos=10 \
7082
-consolidation-max-utxos=35 \

main.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"flag"
66
"log"
7+
"math"
78
"net"
89
"net/http"
910
"os"
@@ -45,6 +46,14 @@ func main() {
4546
var batchIntervalStr string
4647
var autoConsolidationIntervalStr string
4748
clientIPHeaderDefault := os.Getenv("FAUCET_CLIENT_IP_HEADER")
49+
payoutFeeRateDefault := service.DefaultPayoutFeeRateSatsPerVB
50+
if value := strings.TrimSpace(os.Getenv("FAUCET_PAYOUT_FEE_RATE_SATS_PER_VB")); value != "" {
51+
parsed, err := strconv.ParseFloat(value, 64)
52+
if err != nil {
53+
log.Fatalf("Error: invalid FAUCET_PAYOUT_FEE_RATE_SATS_PER_VB value %q: %v", value, err)
54+
}
55+
payoutFeeRateDefault = parsed
56+
}
4857

4958
flag.StringVar(&cfg.ListenAddr, "listen", ":8080", "HTTP server listen address")
5059
flag.StringVar(&cfg.MetricsAddr, "metrics-addr", "0.0.0.0:9222", "Metrics server listen address")
@@ -56,6 +65,7 @@ func main() {
5665
flag.StringVar(&cfg.BitcoinCoreWalletName, "bitcoin-wallet-name", "faucet", "Bitcoin wallet name, will be loaded at start")
5766

5867
flag.StringVar(&batchIntervalStr, "batch-interval", "1m", "Batch processing interval (e.g., 1m, 5m, 30s)")
68+
flag.Float64Var(&cfg.PayoutFeeRateSatsPerVB, "payout-fee-rate-sats-per-vb", payoutFeeRateDefault, "Fee rate for automatic faucet payouts in sat/vB (0 uses the Bitcoin Core wallet default)")
5969
flag.StringVar(&enabledAmountRangesStr, "enabled-amount-ranges", "1,2,3", "Comma-separated amount ranges to enable (1=0.001-0.009, 2=0.01-0.09, 3=0.1-0.9, 4=1.0-2.0)")
6070
flag.IntVar(&cfg.DefaultAmountRange, "default-amount-range", 2, "Default selected amount range (1-4)")
6171
flag.Float64Var(&cfg.MinBalance, "min-balance", 0.1, "Minimum wallet balance threshold (BTC)")
@@ -98,6 +108,9 @@ func main() {
98108
if cfg.MinConsolidationUTXOs > cfg.MaxConsolidationUTXOs {
99109
log.Fatalf("invalid consolidation cfg, min: %d > max: %d", cfg.MinConsolidationUTXOs, cfg.MaxConsolidationUTXOs)
100110
}
111+
if cfg.PayoutFeeRateSatsPerVB < 0 || math.IsNaN(cfg.PayoutFeeRateSatsPerVB) || math.IsInf(cfg.PayoutFeeRateSatsPerVB, 0) {
112+
log.Fatal("Error: payout fee rate must be a finite number greater than or equal to 0")
113+
}
101114

102115
if len(adminAllowlistIP) == 0 && len(adminAllowlistCIDR) == 0 {
103116
adminAllowlistIP = []string{"127.0.0.1"}
@@ -173,6 +186,11 @@ func main() {
173186
log.Printf("Metrics address: %s", cfg.MetricsAddr)
174187
log.Printf("Data directory: %s", cfg.DataDir)
175188
log.Printf("Batch interval: %s", cfg.BatchInterval)
189+
if cfg.PayoutFeeRateSatsPerVB == 0 {
190+
log.Printf("Payout fee rate: Bitcoin Core wallet default")
191+
} else {
192+
log.Printf("Payout fee rate: %.8f sat/vB", cfg.PayoutFeeRateSatsPerVB)
193+
}
176194
log.Printf("Enabled amount ranges: %v (default: %d)", cfg.EnabledAmountRanges, cfg.DefaultAmountRange)
177195
if cfg.ClientIPHeader == "" {
178196
log.Printf("Client IP source: direct peer address")

service/admin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ func (svc *Service) adminDashboardHandler(w http.ResponseWriter, r *http.Request
174174
"ClientIPHeader": svc.cfg.ClientIPHeader,
175175
"AdminAllowlist": formatCIDRs(svc.cfg.AdminAllowlist),
176176
"BatchInterval": svc.cfg.BatchInterval,
177+
"PayoutFeeRateSatsPerVB": svc.cfg.PayoutFeeRateSatsPerVB,
177178
}
178179

179180
if err := svc.renderTemplate(w, "admin_dashboard.html", data); err != nil {

service/processor.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,10 @@ func (svc *Service) processBatch() {
6969
continue
7070
}
7171

72-
fees := btc.FeeSatsPerVBLowerLimit * 1.15
7372
txid, err := svc.rpcClient.SendToAddressWithOpReturn(
7473
tx.Address,
7574
tx.AmountBTC,
76-
fees,
75+
svc.cfg.PayoutFeeRateSatsPerVB,
7776
defaultOpReturn,
7877
)
7978

service/service.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,16 @@ var AllAmountRanges = []AmountRange{
3636
{ID: 4, MinBTC: 1.0, MaxBTC: 2.0, Label: "1.0 - 2.0"},
3737
}
3838

39+
const DefaultPayoutFeeRateSatsPerVB = 0.11
40+
3941
type Config struct {
4042
ListenAddr string
4143
MetricsAddr string
4244
DataDir string
4345
BitcoinRPC btc.BitcoinRPCConfig
4446
BitcoinCoreWalletName string
4547
BatchInterval time.Duration
48+
PayoutFeeRateSatsPerVB float64
4649
MinBalance float64
4750
TurnstileSecret string
4851
TurnstileSiteKey string

service/service_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ func testConfig() *Config {
136136
DataDir: "/tmp/test",
137137
BitcoinCoreWalletName: "faucet",
138138
BatchInterval: time.Minute,
139+
PayoutFeeRateSatsPerVB: DefaultPayoutFeeRateSatsPerVB,
139140
MinBalance: 0.1,
140141
SubmitTokenSecret: "submit-token-secret-for-tests-32-chars",
141142
AdminPassword: "testpass123",
@@ -1773,6 +1774,45 @@ func TestProcessBatch_SendsTransactions(t *testing.T) {
17731774
}
17741775
}
17751776

1777+
func TestProcessBatch_UsesConfiguredPayoutFeeRate(t *testing.T) {
1778+
mock := newMockRPC()
1779+
var feeRate string
1780+
mock.handlers["fundrawtransaction"] = func(params json.RawMessage) (any, *rpcErr) {
1781+
var decoded []json.RawMessage
1782+
if err := json.Unmarshal(params, &decoded); err != nil {
1783+
t.Fatalf("decode fundrawtransaction params: %v", err)
1784+
}
1785+
if len(decoded) != 2 {
1786+
t.Fatalf("expected fee options, got %d params", len(decoded))
1787+
}
1788+
1789+
var options map[string]string
1790+
if err := json.Unmarshal(decoded[1], &options); err != nil {
1791+
t.Fatalf("decode fundrawtransaction options: %v", err)
1792+
}
1793+
feeRate = options["fee_rate"]
1794+
1795+
return map[string]any{"hex": "fundedhex000", "fee": 0.00001}, nil
1796+
}
1797+
1798+
rpcServer := httptest.NewServer(mock)
1799+
t.Cleanup(rpcServer.Close)
1800+
svc := testService(t, rpcServer)
1801+
svc.cfg.PayoutFeeRateSatsPerVB = 0.42
1802+
svc.db.Create(&db.Transaction{
1803+
Address: "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx",
1804+
IPAddress: "1.2.3.4",
1805+
AmountBTC: 0.05,
1806+
Status: db.TxnStatusPending,
1807+
})
1808+
1809+
svc.processBatch()
1810+
1811+
if feeRate != "0.42000000" {
1812+
t.Fatalf("expected configured fee rate 0.42000000, got %q", feeRate)
1813+
}
1814+
}
1815+
17761816
func TestProcessBatch_InsufficientBalance(t *testing.T) {
17771817
mock := newMockRPC()
17781818
mock.handlers["getbalances"] = func(_ json.RawMessage) (any, *rpcErr) {

templates/admin_dashboard.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ <h2>Configuration</h2>
461461
<tr><td style="color: #999;">Max Consolidation UTXOs</td><td>{{.MaxConsolidationUTXOs}}</td></tr>
462462
<tr><td style="color: #999;">Auto Consolidation Interval</td><td>{{if gt .AutoConsolidationInterval 0}}{{.AutoConsolidationInterval}}{{else}}disabled{{end}}</td></tr>
463463
<tr><td style="color: #999;">Batch Interval</td><td>{{.BatchInterval}}</td></tr>
464+
<tr><td style="color: #999;">Payout Fee Rate</td><td>{{if eq .PayoutFeeRateSatsPerVB 0.0}}Bitcoin Core wallet default{{else}}{{printf "%.8f" .PayoutFeeRateSatsPerVB}} sat/vB{{end}}</td></tr>
464465
<tr><td style="color: #999;">Max Withdrawals per IP (24h)</td><td>{{.MaxWithdrawalsPerIP24h}}</td></tr>
465466
<tr><td style="color: #999;">Max Deposits per Address</td><td>{{.MaxDepositsPerAddress}}</td></tr>
466467
<tr><td style="color: #999;">Client IP Source</td><td>{{if .ClientIPHeader}}{{.ClientIPHeader}} header{{else}}direct peer address{{end}}</td></tr>

0 commit comments

Comments
 (0)