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
- Variables
- func IsTestnetChainID(id string) bool
- func NewGnoclientBalance(cli *gnoclient.Client, addr crypto.Address, ttl time.Duration) func(context.Context) (int64, error)
- type Dispenser
- type Faucet
- type FaucetLimits
- type Limiter
- type LimiterCfg
- type LimiterPolicy
- type LimiterSnapshot
- type Metrics
- type Option
- type PerAddressLimit
Constants ¶
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 ¶
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") )
Functions ¶
func IsTestnetChainID ¶
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 ¶
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 (*Faucet) Fund ¶
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 ¶
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 ¶
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 ¶
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
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
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
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
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
WithMetrics sets the outcome metrics recorder. Defaults to a no-op.
func WithTrustedProxies ¶ added in v0.5.0
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
PerAddressLimit is the only rate limit safe to disclose: a fresh key bypasses it, so naming it defeats no anti-abuse control.