faucet

package
v0.8.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package faucet is a standalone automatic testnet faucet service. It imports only the gno toolchain, stdlib, and the toolchain-only gno-mcp/gasprice helper (no gno-mcp internals) — so it can be extracted to its own repo later, taking gasprice with it.

Index

Constants

View Source
const (
	OutcomeSuccess        = "success"
	OutcomeBadRequest     = "bad_request"
	OutcomeChainRefused   = "chain_refused"
	OutcomeChainMismatch  = "chain_mismatch"
	OutcomeCooldown       = "cooldown"
	OutcomeRateLimited    = "rate_limited"
	OutcomeDailyCap       = "daily_cap"
	OutcomeDripLimited    = "drip_limited"
	OutcomeFundingLow     = "funding_low"
	OutcomeDispenseFailed = "dispense_failed"
)

Outcome values are bounded, low-cardinality labels for the fund-request counter; they also appear in the per-request access log.

Variables

View Source
var (
	ErrChainRefused  = errors.New("faucet: chain-id is not a testnet")
	ErrChainMismatch = errors.New("faucet: chain-id does not match this faucet")
	ErrBadAddress    = errors.New("faucet: invalid recipient address")
	ErrFundingLow    = errors.New("faucet: funding wallet below minimum balance")
)
View Source
var (
	ErrCooldown    = errors.New("faucet: address in cooldown")
	ErrRateLimited = errors.New("faucet: per-IP rate limit")
	ErrDailyCap    = errors.New("faucet: global daily cap reached")
	ErrDripLimited = errors.New("faucet: global drip rate exceeded")
)

Functions

func IsTestnetChainID

func IsTestnetChainID(id string) bool

IsTestnetChainID reports whether id matches the testnet chain-id pattern (test-?N).

func NewGnoclientBalance added in v0.3.0

func NewGnoclientBalance(cli *gnoclient.Client, addr crypto.Address, ttl time.Duration) func(context.Context) (int64, error)

NewGnoclientBalance returns a funding-balance source for WithBalanceFloor: it queries addr's ugnot balance through cli, caching the result for ttl so a busy faucet does not issue an account query per fund request. The funding key is always a funded account, so a QueryAccount error is treated as a transport failure and propagated (the caller surfaces it) rather than masked as a zero balance.

Types

type Dispenser

type Dispenser interface {
	Send(ctx context.Context, to string, amountUgnot int64) (txHash string, err error)
}

Dispenser sends a ugnot grant to an address and returns the tx hash. This interface is the seam: gnoclientDispenser uses bank.MsgSend today; a realm dispenser (gnoclient.Call) can replace it later without touching the service.

func NewGnoclientDispenser

func NewGnoclientDispenser(cli *gnoclient.Client, from crypto.Address, gasWanted int64) Dispenser

NewGnoclientDispenser builds the production Dispenser: a bank.MsgSend sender signing with the funding key behind cli. gasWanted is the execution ceiling; the GasFee is priced per send from the chain's live gas price.

type Faucet

type Faucet struct {
	// contains filtered or unexported fields
}

Faucet dispenses a fixed ugnot grant to testnet addresses, bounded by a Limiter.

func New

func New(chainID string, grantUgnot int64, d Dispenser, l *Limiter, opts ...Option) *Faucet

func (*Faucet) Fund

func (f *Faucet) Fund(ctx context.Context, address, ip, reqChainID string) (string, error)

Fund validates the chain-id and recipient, applies rate-limits/cap, then dispenses the grant. Check order: chain-id is-testnet -> chain-id matches this faucet -> recipient is valid -> rate-limit -> dispense. The recipient is parsed before the limiter is touched so a garbage address cannot burn the daily cap, and the limiter is keyed on the canonical address so case variants share one bucket. A dispense failure refunds the limiter so a chain hiccup doesn't consume the requester's cooldown or the global budget.

func (*Faucet) Handler

func (f *Faucet) Handler() http.Handler

Handler returns the faucet's HTTP mux (POST /fund, GET /health, GET /limits), wrapped with the access-log middleware.

func (*Faucet) Limits added in v0.7.0

func (f *Faucet) Limits() FaucetLimits

Limits returns the published per-address policy for this faucet.

type FaucetLimits added in v0.7.0

type FaucetLimits struct {
	GrantUgnot int64           `json:"grant_ugnot"`
	PerAddress PerAddressLimit `json:"per_address"`
}

FaucetLimits is the public, policy-only view served at GET /limits. It deliberately omits per-IP, the daily cap, the drip bucket, and all live remaining state — those are anti-abuse internals or metrics-only.

type Limiter

type Limiter struct {
	// contains filtered or unexported fields
}

Limiter is a clock-injectable, mutex-guarded in-memory rate limiter that tracks per-address and per-IP sliding windows plus a hard global daily cap.

func NewLimiter

func NewLimiter(cfg LimiterCfg) *Limiter

NewLimiter constructs a Limiter with the given config, applying defaults for zero-valued duration and clock fields.

func (*Limiter) Allow

func (l *Limiter) Allow(addr, ip string) (time.Time, error)

Allow records a grant for addr/ip and returns the time it was recorded, or a non-nil error (ErrCooldown, ErrRateLimited, ErrDailyCap, ErrDripLimited) if blocked. Checks run in order: addr → IP → daily cap → global drip. The returned time is passed to Refund so a refund knows which UTC day the grant counted against.

func (*Limiter) Policy added in v0.7.0

func (l *Limiter) Policy() LimiterPolicy

Policy returns the static per-address configuration. These fields are set once in NewLimiter and never mutated, so no lock is taken.

func (*Limiter) Refund

func (l *Limiter) Refund(addr, ip string, grantedAt time.Time)

Refund reverses the accounting of the Allow at grantedAt for addr/ip: it drops their newest recorded hit and credits the grant back to the daily counter. Called when a dispense fails after Allow succeeded so a chain error doesn't consume the requester's cooldown or the global daily budget. The daily credit is skipped if a day-rollover has reset daySpent since the grant — that reset already discarded the grant's contribution, so crediting again would under-count the new day and let the cap be exceeded.

func (*Limiter) Snapshot added in v0.5.0

func (l *Limiter) Snapshot() LimiterSnapshot

Snapshot returns the current global budget state without mutating the limiter: it projects accrued drip tokens up to now (capped at burst) without advancing the bucket clock, and reports zero daily spend once the UTC day has rolled over. Safe to call on every scrape.

type LimiterCfg

type LimiterCfg struct {
	Now           func() time.Time // nil -> time.Now
	PerAddrWindow time.Duration    // 0 -> 24h
	PerAddrMax    int              // max grants per address per window
	PerIPWindow   time.Duration    // 0 -> 1h
	PerIPMax      int              // max grants per IP per window
	DailyCapUgnot int64            // hard ceiling on total daily outflow
	GrantUgnot    int64            // amount counted per successful Allow

	// Global drip: a token bucket over total ugnot outflow, independent of
	// address or IP. DripBurstUgnot is the bucket capacity (the largest spike
	// tolerated); DripRateUgnotPerSec is the sustained refill. A zero
	// DripBurstUgnot disables the drip control (fail-open for this one check —
	// the daily cap still bounds total outflow).
	DripBurstUgnot      int64
	DripRateUgnotPerSec int64
}

LimiterCfg configures the rate limiter. Defaults: Now→time.Now, PerAddrWindow→24h, PerIPWindow→1h when zero. PerAddrMax, PerIPMax, and DailyCapUgnot are REQUIRED — a zero value blocks all grants (fail-closed). Callers must set them explicitly.

type LimiterPolicy added in v0.7.0

type LimiterPolicy struct {
	PerAddrMax    int
	PerAddrWindow time.Duration
}

LimiterPolicy is the limiter's static, non-sensitive configuration: safe to publish. Unlike LimiterSnapshot it carries no live counters.

type LimiterSnapshot added in v0.5.0

type LimiterSnapshot struct {
	DripEnabled   bool
	DripTokens    float64 // projected tokens available now (0 when disabled)
	DripBurst     float64 // bucket capacity
	DailyCapUgnot int64
	DaySpentUgnot int64 // 0 once the UTC day has rolled over
}

LimiterSnapshot is a point-in-time view of the limiter's global budget state, for metrics. It carries no per-address or per-IP data.

type Metrics added in v0.5.0

type Metrics interface {
	RecordOutcome(ctx context.Context, outcome string)
}

Metrics records faucet business outcomes. Implementations must be safe for concurrent use. The default is a no-op; cmd/agentfaucet supplies an OTel-backed implementation. Defined with stdlib-only signatures so the faucet package stays free of any telemetry dependency.

type Option added in v0.3.0

type Option func(*Faucet)

Option configures optional Faucet behavior.

func WithBalanceFloor added in v0.3.0

func WithBalanceFloor(minUgnot int64, balance func(context.Context) (int64, error)) Option

WithBalanceFloor refuses grants (ErrFundingLow) while the funding wallet's balance is below minUgnot. balance reports the funding wallet's current ugnot; it should cache internally to avoid an RPC per request.

func WithLogger added in v0.5.0

func WithLogger(l *slog.Logger) Option

WithLogger sets the structured logger used for the per-request access log and internal-error logging. Defaults to slog.Default().

func WithMetrics added in v0.5.0

func WithMetrics(m Metrics) Option

WithMetrics sets the outcome metrics recorder. Defaults to a no-op.

func WithTrustedProxies added in v0.5.0

func WithTrustedProxies(n int) Option

WithTrustedProxies sets how many reverse-proxy hops in front of the faucet are trusted to append honest X-Forwarded-For entries (e.g. 1 for a single ALB). 0 means use the direct peer address. See clientIP.

type PerAddressLimit added in v0.7.0

type PerAddressLimit struct {
	Max           int `json:"max"`
	WindowSeconds int `json:"window_seconds"`
}

PerAddressLimit is the only rate limit safe to disclose: a fresh key bypasses it, so naming it defeats no anti-abuse control.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL