-
Notifications
You must be signed in to change notification settings - Fork 31
feat(faucet-app): migrate faucet-app into nitrolite repo #776
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
Open
nksazonov
wants to merge
5
commits into
main
Choose a base branch
from
feat/faucet-app
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
5aeb728
feat: migrated faucet-app
nksazonov b5f8222
refactor(faucet-app): rename clearnode to nitronode
nksazonov 06dcb25
fix(faucet-app): harden nitronode client
nksazonov f0af8f2
fix(faucet-app): address review feedback
nksazonov 683804a
fix(faucet-app): fix dockerfile context, proxy validation, env exampl…
nksazonov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # ============================================================================= | ||
| # Nitrolite Faucet Server Configuration | ||
| # ============================================================================= | ||
| # Copy this file to .env and update the values according to your setup | ||
| # | ||
| # You can also set these as environment variables instead of using .env file | ||
| # Environment variables take precedence over .env file values | ||
| # ============================================================================= | ||
|
|
||
| # ----------------------------------------------------------------------------- | ||
| # Server Configuration | ||
| # ----------------------------------------------------------------------------- | ||
| # HTTP server port | ||
| SERVER_PORT=8080 | ||
|
|
||
| # ----------------------------------------------------------------------------- | ||
| # Nitronode Configuration | ||
| # ----------------------------------------------------------------------------- | ||
| # Private key for faucet owner wallet (without 0x prefix) | ||
| # REQUIRED: Used for EIP-712 authentication with Nitronode | ||
| OWNER_PRIVATE_KEY=your_owner_private_key_here_without_0x_prefix | ||
|
|
||
| # Cooldown between requests per wallet/IP (Go duration format) | ||
| # Optional: default is 24h | ||
| # COOLDOWN_PERIOD=your_cooldown_period_here (e.g., 24h, 1h, 30m) | ||
|
|
||
| # Nitronode WebSocket URL | ||
| # REQUIRED: The WebSocket endpoint for your Nitronode instance | ||
| NITRONODE_URL=wss://nitronode.example.com/ws | ||
|
|
||
| # Token symbol to distribute | ||
| # REQUIRED: The symbol of the token to distribute (must be supported by Nitronode) | ||
| TOKEN_SYMBOL=usdc | ||
|
|
||
| # Default amount to send per request | ||
| # REQUIRED: Amount in decimal format (e.g., 1.0 = 1 USDC, 0.5 = 0.5 ETH) | ||
| STANDARD_TIP_AMOUNT=10.0 | ||
|
|
||
| # Minimum number of transfers the server should have a balance for to operate | ||
| # REQUIRED: Integer value (e.g., 5) | ||
| MIN_TRANSFER_COUNT=5 | ||
|
|
||
| # ----------------------------------------------------------------------------- | ||
| # Logging Configuration | ||
| # ----------------------------------------------------------------------------- | ||
| # Logging level (debug, info, warn, error) | ||
| # Default: info | ||
| LOG_LEVEL=info |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| .env |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| FROM golang:1.25-alpine AS builder | ||
|
|
||
| WORKDIR /build | ||
|
|
||
| # Copy root module files first (the replace directive in faucet-app/server points here) | ||
| COPY go.mod go.sum ./ | ||
|
|
||
| # Copy faucet-app/server module files and pre-download dependencies | ||
| COPY faucet-app/server/go.mod faucet-app/server/go.sum ./faucet-app/server/ | ||
| RUN cd faucet-app/server && go mod download | ||
|
|
||
| # Copy full source tree so the replace directive can resolve | ||
| COPY . . | ||
|
|
||
| # Build the faucet server binary | ||
| RUN cd faucet-app/server && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /build/bin/faucet-server . | ||
|
|
||
| FROM alpine:3.23.3 | ||
|
|
||
| RUN apk --no-cache add ca-certificates | ||
|
|
||
| RUN addgroup -g 1001 -S faucet | ||
| RUN adduser -S faucet -u 1001 -G faucet | ||
|
|
||
| USER faucet | ||
|
|
||
| COPY --from=builder /build/bin/faucet-server /bin/faucet-server | ||
|
|
||
| EXPOSE 8080 | ||
|
|
||
| CMD ["faucet-server"] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| # Nitrolite Faucet Server | ||
|
|
||
| A Go-based faucet server that distributes tokens through the Nitronode network using WebSocket connections. | ||
|
|
||
| ## Features | ||
|
|
||
| - **Nitrolite SDK Integration**: Uses the local `github.com/layer-3/nitrolite` SDK for Nitronode communication | ||
| - **Ethereum Wallet Integration**: Uses ECDSA private key for signing channel states and transactions | ||
| - **RESTful API**: Simple HTTP endpoints for token requests | ||
| - **Structured Logging**: JSON-formatted logs with configurable levels | ||
| - **Graceful Shutdown**: Proper cleanup of connections and resources | ||
| - **Address Validation**: Validates Ethereum addresses before processing requests | ||
|
|
||
| ## Architecture | ||
|
|
||
| The application is structured into several packages: | ||
|
|
||
| - `internal/config`: Configuration management with environment variables | ||
| - `internal/logger`: Structured logging with logrus | ||
| - `internal/nitronode`: Thin wrapper around the Nitrolite SDK client | ||
| - `internal/server`: HTTP server with Gin framework | ||
|
|
||
| ### Nitronode Client | ||
|
|
||
| The `internal/nitronode` package wraps the Nitrolite SDK's `sdk.Client`. Connection and message signing are handled internally by the SDK — no manual WebSocket management is required. | ||
|
|
||
| ## Quick Start | ||
|
|
||
| 1. **Setup**: | ||
|
|
||
| ```bash | ||
| cd faucet-app/server | ||
| go mod tidy | ||
| ``` | ||
|
|
||
| 2. **Configure environment**: | ||
|
|
||
| ```bash | ||
| cp .env.example .env | ||
| # Edit .env with your configuration | ||
| ``` | ||
|
|
||
| 3. **Run the server**: | ||
|
|
||
| ```bash | ||
| go run main.go | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| The application uses [cleanenv](https://github.com/ilyakaznacheev/cleanenv) for configuration management. Configuration can be provided via: | ||
|
|
||
| 1. **`.env` file** in the current directory | ||
| 2. **Environment variables** (used when `.env` is absent) | ||
|
|
||
| Set the following environment variables: | ||
|
|
||
| | Variable | Required | Default | Description | Example | | ||
| |----------|----------|---------|-------------|---------| | ||
| | `SERVER_PORT` | No | `8080` | HTTP server port | `8080` | | ||
| | `OWNER_PRIVATE_KEY` | **Yes** | - | Owner private key (without 0x prefix) — signs channel states and transfers | `abcdef123...` | | ||
| | `NITRONODE_URL` | **Yes** | - | Nitronode WebSocket URL | `wss://nitronode.example.com/ws` | | ||
| | `TOKEN_SYMBOL` | **Yes** | - | Token symbol to distribute | `usdc` | | ||
| | `STANDARD_TIP_AMOUNT` | **Yes** | - | Amount to send per request (decimal format) | `10.0` | | ||
| | `MIN_TRANSFER_COUNT` | **Yes** | - | Minimum number of transfers the server should have balance for | `5` | | ||
| | `COOLDOWN_PERIOD` | **Yes** | - | Cooldown between requests per wallet/IP (Go duration format) | `24h` | | ||
| | `TRUSTED_PROXIES` | No | `""` | Comma-separated trusted proxy IPs; empty means direct exposure only | `10.0.0.1,10.0.0.2` | | ||
| | `LOG_LEVEL` | No | `info` | Logging level (debug/info/warn/error) | `info` | | ||
|
|
||
| > **Note on `TRUSTED_PROXIES`:** If the faucet is deployed behind an ingress or load balancer, set this to the proxy IP(s). Without it, `c.ClientIP()` returns the proxy address and all requests share one IP rate-limit bucket. | ||
|
|
||
| ## API Endpoints | ||
|
|
||
| ### `POST /requestTokens` | ||
|
|
||
| Request tokens for an Ethereum address. | ||
|
|
||
| **Request body:** | ||
| ```json | ||
| { "userAddress": "0x..." } | ||
| ``` | ||
|
|
||
| **Success response (200):** | ||
| ```json | ||
| { | ||
| "success": true, | ||
| "message": "Tokens sent successfully", | ||
| "txId": "...", | ||
| "amount": "10", | ||
| "asset": "usdc", | ||
| "destination": "0x..." | ||
| } | ||
| ``` | ||
|
|
||
| **Error responses:** | ||
| - `400` — Invalid address or request format | ||
| - `429` — Rate limit exceeded | ||
| - `500` — Transfer failed | ||
| - `503` — Nitronode unavailable or balance insufficient | ||
|
|
||
| ### `GET /info` | ||
|
|
||
| Returns server metadata. | ||
|
|
||
| ## WebSocket Connection Management | ||
|
|
||
| The Nitrolite SDK maintains a persistent WebSocket connection with Nitronode: | ||
|
|
||
| - **Connection**: Established on startup inside `nitronode.NewClient()`; no separate connect/auth step is needed | ||
| - **Authentication**: Handled internally by the SDK | ||
| - **Reconnection**: On each request, `EnsureConnected()` detects a lost connection (via `WaitCh()`) and reconnects with exponential backoff (3 attempts, 300 ms → 600 ms → 2 s) | ||
| - **Post-reconnect ping**: Each reconnect attempt is validated with a `Ping` before the new client is accepted | ||
| - **Message Handling**: Fully managed by the SDK's internal RPC layer | ||
|
|
||
| ## Startup Log Example | ||
|
|
||
| ``` | ||
| {"level":"info","msg":"Starting Nitrolite Faucet Server","time":"..."} | ||
| {"level":"info","msg":"Configuration loaded: Server port=8080, Nitronode URL=wss://nitronode.example.com","time":"..."} | ||
| {"level":"debug","msg":"Token 'usdc' is supported by Nitronode","time":"..."} | ||
| {"level":"info","msg":"✓ Sufficient usdc balance: 50000000","time":"..."} | ||
| {"level":"info","msg":"Successfully connected to Nitronode","time":"..."} | ||
| {"level":"info","msg":"Faucet server is ready to serve requests","time":"..."} | ||
| ``` | ||
|
|
||
| ## Security Features | ||
|
|
||
| - **Address Validation**: Validates Ethereum address format before processing | ||
| - **Private Key Security**: Private key is only used for signing, never exposed | ||
| - **CORS Support**: Configurable CORS headers for web integration | ||
| - **Request Signing**: All Nitronode requests are cryptographically signed by the SDK | ||
| - **Balance Guard**: Refuses to operate below minimum balance threshold | ||
| - **URL Redaction**: `NITRONODE_URL` is never logged in full — only scheme and host are shown | ||
|
|
||
| ## Building for Production | ||
|
|
||
| ```bash | ||
| go build -o faucet-server main.go | ||
| ./faucet-server | ||
| ``` | ||
|
|
||
| ## Development | ||
|
|
||
| ```bash | ||
| # Run tests (from repo root) | ||
| go test ./faucet-app/... | ||
|
|
||
| # Run with hot reload | ||
| go install github.com/cosmtrek/air@latest | ||
| air | ||
| ``` | ||
|
|
||
| ## Error Handling | ||
|
|
||
| - **Connection Errors**: Returns 503 if Nitronode is unavailable or reconnection fails | ||
| - **Validation Errors**: Returns 400 for invalid addresses or request format | ||
| - **Transfer Errors**: Returns 500 for Nitronode transfer failures | ||
| - **Service Unavailable**: Returns 503 if token is unsupported or balance is insufficient | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| **Connection Issues:** | ||
|
|
||
| - Verify `NITRONODE_URL` is correct and accessible | ||
| - Check firewall settings for WebSocket connections | ||
|
|
||
| **Token Not Supported:** | ||
|
|
||
| - Verify `TOKEN_SYMBOL` is supported by the Nitronode instance | ||
|
|
||
| **Insufficient Balance:** | ||
|
|
||
| - Top up the faucet wallet; the server requires at least `MIN_TRANSFER_COUNT × STANDARD_TIP_AMOUNT` available balance |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| package config | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "net" | ||
| "os" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/ilyakaznacheev/cleanenv" | ||
| "github.com/shopspring/decimal" | ||
| ) | ||
|
|
||
| // Config holds all runtime configuration for the faucet server. | ||
| type Config struct { | ||
| ServerPort string `env:"SERVER_PORT" env-default:"8080" env-description:"HTTP server port"` | ||
|
|
||
| OwnerPrivateKey string `env:"OWNER_PRIVATE_KEY" env-required:"true" env-description:"Private key for faucet owner wallet (without 0x prefix)"` | ||
| NitronodeURL string `env:"NITRONODE_URL" env-required:"true" env-description:"Nitronode WebSocket URL"` | ||
| TokenSymbol string `env:"TOKEN_SYMBOL" env-required:"true" env-description:"Token symbol to distribute (e.g., usdc, weth)"` | ||
| StandardTipAmount string `env:"STANDARD_TIP_AMOUNT" env-required:"true" env-description:"Default amount to send per request"` | ||
| MinTransferCount int `env:"MIN_TRANSFER_COUNT" env-required:"true" env-description:"Number of transfers a server should have a balance for to operate"` | ||
| CooldownPeriod string `env:"COOLDOWN_PERIOD" env-required:"true" env-description:"Cooldown between requests per wallet/IP (e.g. 24h, 1h30m)"` | ||
| TrustedProxies string `env:"TRUSTED_PROXIES" env-default:"" env-description:"Comma-separated trusted proxy IPs; empty means direct exposure only"` | ||
|
|
||
| LogLevel string `env:"LOG_LEVEL" env-default:"info" env-description:"Logging level (debug, info, warn, error)"` | ||
|
|
||
| // Parsed values (set after loading) | ||
| StandardTipAmountDecimal decimal.Decimal | ||
| CooldownPeriodDuration time.Duration | ||
| TrustedProxyList []string | ||
| } | ||
|
|
||
| // Load reads configuration from a .env file (if present) or environment variables. | ||
| // A missing .env file is not an error; any other read or validation failure is. | ||
| func Load() (*Config, error) { | ||
| var config Config | ||
|
|
||
| if _, statErr := os.Stat(".env"); statErr == nil { | ||
| if err := cleanenv.ReadConfig(".env", &config); err != nil { | ||
| return nil, fmt.Errorf("failed to load .env file: %w", err) | ||
| } | ||
| } else if !errors.Is(statErr, os.ErrNotExist) { | ||
| return nil, fmt.Errorf("failed to stat .env file: %w", statErr) | ||
| } else { | ||
| if err := cleanenv.ReadEnv(&config); err != nil { | ||
| return nil, fmt.Errorf("failed to load configuration from environment: %w", err) | ||
| } | ||
| } | ||
|
|
||
| if err := config.Validate(); err != nil { | ||
| return nil, fmt.Errorf("config validation failed: %w", err) | ||
| } | ||
|
|
||
| return &config, nil | ||
| } | ||
|
|
||
| // Validate parses and range-checks all fields that require post-load processing. | ||
| func (c *Config) Validate() error { | ||
| amount, err := decimal.NewFromString(c.StandardTipAmount) | ||
| if err != nil { | ||
| return fmt.Errorf("STANDARD_TIP_AMOUNT must be a valid decimal number: %w", err) | ||
| } | ||
| if amount.IsZero() || amount.IsNegative() { | ||
| return fmt.Errorf("STANDARD_TIP_AMOUNT must be a positive number") | ||
| } | ||
| c.StandardTipAmountDecimal = amount | ||
|
|
||
| d, err := time.ParseDuration(c.CooldownPeriod) | ||
| if err != nil { | ||
| return fmt.Errorf("COOLDOWN_PERIOD must be a valid duration (e.g. 24h, 1h30m): %w", err) | ||
| } | ||
| if d <= 0 { | ||
| return fmt.Errorf("COOLDOWN_PERIOD must be positive") | ||
| } | ||
| c.CooldownPeriodDuration = d | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| if c.MinTransferCount <= 0 { | ||
| return fmt.Errorf("MIN_TRANSFER_COUNT must be a positive integer") | ||
| } | ||
|
|
||
| if c.TrustedProxies != "" { | ||
| for _, p := range strings.Split(c.TrustedProxies, ",") { | ||
| trimmed := strings.TrimSpace(p) | ||
| if trimmed == "" { | ||
| continue | ||
| } | ||
| if net.ParseIP(trimmed) == nil { | ||
| if _, _, err := net.ParseCIDR(trimmed); err != nil { | ||
| return fmt.Errorf("TRUSTED_PROXIES contains invalid IP or CIDR %q: %w", trimmed, err) | ||
| } | ||
| } | ||
| c.TrustedProxyList = append(c.TrustedProxyList, trimmed) | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove references to deleted module files.
Lines 8-10 attempt to copy
faucet-app/server/go.modandgo.sum, but per the PR objectives these files were removed when the faucet-app was migrated to use the root module. The Docker build will fail with a "file not found" error.🐛 Proposed fix
Since the faucet-app now uses the root module, remove these lines entirely. Dependencies are already downloaded via the root
go.modcopied at line 6:🤖 Prompt for AI Agents