From 5aeb7281beb36af357e0489f351855b86244856a Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 20 May 2026 16:57:59 +0200 Subject: [PATCH 1/5] feat: migrated faucet-app --- faucet-app/.github/CODEOWNERS | 4 + faucet-app/.github/dependabot.yml | 20 ++ faucet-app/.github/workflows/main-pr.yml | 55 ++++ faucet-app/.github/workflows/main-push.yml | 186 +++++++++++ faucet-app/.github/workflows/stable-tag.yml | 134 ++++++++ faucet-app/LICENSE | 21 ++ faucet-app/chart/.helmignore | 22 ++ faucet-app/chart/Chart.yaml | 5 + faucet-app/chart/README.md | 142 ++++++++ faucet-app/chart/README.md.gotmpl | 92 ++++++ faucet-app/chart/config/sandbox/secrets.yaml | 4 + faucet-app/chart/config/sandbox/values.yaml | 56 ++++ faucet-app/chart/config/uat/secrets.yaml | 4 + faucet-app/chart/config/uat/values.yaml | 56 ++++ faucet-app/chart/templates/deployment.yaml | 52 +++ faucet-app/chart/templates/gateway.yaml | 31 ++ .../chart/templates/helpers/_common.tpl | 116 +++++++ .../chart/templates/helpers/_component.tpl | 78 +++++ faucet-app/chart/templates/helpers/_hpa.tpl | 24 ++ .../chart/templates/helpers/_ingress.tpl | 67 ++++ .../chart/templates/http-route-redirect.yaml | 25 ++ faucet-app/chart/templates/http-route.yaml | 27 ++ faucet-app/chart/templates/ingress.yaml | 17 + faucet-app/chart/templates/podmonitoring.yaml | 16 + faucet-app/chart/templates/service.yaml | 20 ++ faucet-app/chart/values.yaml | 137 ++++++++ faucet-app/scripts/auto_tag.sh | 22 ++ faucet-app/server/.env.example | 48 +++ faucet-app/server/.gitignore | 1 + faucet-app/server/Dockerfile | 33 ++ faucet-app/server/README.md | 252 ++++++++++++++ faucet-app/server/go.mod | 81 +++++ faucet-app/server/go.sum | 309 +++++++++++++++++ .../server/internal/clearnode/client.go | 230 +++++++++++++ .../clearnode/client_validation_test.go | 36 ++ faucet-app/server/internal/config/config.go | 69 ++++ faucet-app/server/internal/logger/logger.go | 68 ++++ .../server/internal/server/ratelimiter.go | 73 ++++ faucet-app/server/internal/server/server.go | 229 +++++++++++++ .../server/internal/server/server_test.go | 311 ++++++++++++++++++ faucet-app/server/main.go | 64 ++++ 41 files changed, 3237 insertions(+) create mode 100644 faucet-app/.github/CODEOWNERS create mode 100644 faucet-app/.github/dependabot.yml create mode 100644 faucet-app/.github/workflows/main-pr.yml create mode 100644 faucet-app/.github/workflows/main-push.yml create mode 100644 faucet-app/.github/workflows/stable-tag.yml create mode 100644 faucet-app/LICENSE create mode 100644 faucet-app/chart/.helmignore create mode 100644 faucet-app/chart/Chart.yaml create mode 100644 faucet-app/chart/README.md create mode 100644 faucet-app/chart/README.md.gotmpl create mode 100644 faucet-app/chart/config/sandbox/secrets.yaml create mode 100644 faucet-app/chart/config/sandbox/values.yaml create mode 100644 faucet-app/chart/config/uat/secrets.yaml create mode 100644 faucet-app/chart/config/uat/values.yaml create mode 100644 faucet-app/chart/templates/deployment.yaml create mode 100644 faucet-app/chart/templates/gateway.yaml create mode 100644 faucet-app/chart/templates/helpers/_common.tpl create mode 100644 faucet-app/chart/templates/helpers/_component.tpl create mode 100644 faucet-app/chart/templates/helpers/_hpa.tpl create mode 100644 faucet-app/chart/templates/helpers/_ingress.tpl create mode 100644 faucet-app/chart/templates/http-route-redirect.yaml create mode 100644 faucet-app/chart/templates/http-route.yaml create mode 100644 faucet-app/chart/templates/ingress.yaml create mode 100644 faucet-app/chart/templates/podmonitoring.yaml create mode 100644 faucet-app/chart/templates/service.yaml create mode 100644 faucet-app/chart/values.yaml create mode 100755 faucet-app/scripts/auto_tag.sh create mode 100644 faucet-app/server/.env.example create mode 100644 faucet-app/server/.gitignore create mode 100644 faucet-app/server/Dockerfile create mode 100644 faucet-app/server/README.md create mode 100644 faucet-app/server/go.mod create mode 100644 faucet-app/server/go.sum create mode 100644 faucet-app/server/internal/clearnode/client.go create mode 100644 faucet-app/server/internal/clearnode/client_validation_test.go create mode 100644 faucet-app/server/internal/config/config.go create mode 100644 faucet-app/server/internal/logger/logger.go create mode 100644 faucet-app/server/internal/server/ratelimiter.go create mode 100644 faucet-app/server/internal/server/server.go create mode 100644 faucet-app/server/internal/server/server_test.go create mode 100644 faucet-app/server/main.go diff --git a/faucet-app/.github/CODEOWNERS b/faucet-app/.github/CODEOWNERS new file mode 100644 index 000000000..4e19d7391 --- /dev/null +++ b/faucet-app/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# CODEOWNERS: https://help.github.com/articles/about-codeowners/ + +# Yellow Network - Research and Development +* @erc7824/yellow-network diff --git a/faucet-app/.github/dependabot.yml b/faucet-app/.github/dependabot.yml new file mode 100644 index 000000000..d5be8b1cc --- /dev/null +++ b/faucet-app/.github/dependabot.yml @@ -0,0 +1,20 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/server/" # Location of package manifests + schedule: + interval: "daily" + commit-message: + prefix: "chore(gomod)" + labels: + - "dependencies" + - "gomod" + groups: + gomod-dependencies: + patterns: + - "*" diff --git a/faucet-app/.github/workflows/main-pr.yml b/faucet-app/.github/workflows/main-pr.yml new file mode 100644 index 000000000..2480d8796 --- /dev/null +++ b/faucet-app/.github/workflows/main-pr.yml @@ -0,0 +1,55 @@ +name: PR on main branch + +on: + pull_request: + branches: [ main ] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'server/go.mod' + cache: true + cache-dependency-path: 'server/go.sum' + + - name: Test + run: go test -v ./... + working-directory: server + + build-and-publish: + name: Build and Publish + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract short SHA + id: sha + run: echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: server + push: true + tags: | + ghcr.io/${{ github.repository }}/server:${{ steps.sha.outputs.short_sha }} diff --git a/faucet-app/.github/workflows/main-push.yml b/faucet-app/.github/workflows/main-push.yml new file mode 100644 index 000000000..fe781c947 --- /dev/null +++ b/faucet-app/.github/workflows/main-push.yml @@ -0,0 +1,186 @@ +name: Push on main branch + +on: + push: + branches: [ main ] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'server/go.mod' + cache: true + cache-dependency-path: 'server/go.sum' + + - name: Test + run: go test -v ./... + working-directory: server + + auto-tag: + name: Auto-Tag + needs: test + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + image-tag: ${{ steps.tagger.outputs.new_tag }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run auto tag script + run: | + git config user.name "GitHub Actions" + git config user.email "github-actions@github.com" + + ./scripts/auto_tag.sh + + - name: Get the new tag without 'v' prefix + id: tagger + run: | + NEW_TAG=$(git describe --tags --abbrev=0) + NEW_TAG_WITHOUT_V=${NEW_TAG#v} + echo "new_tag=$NEW_TAG_WITHOUT_V" >> $GITHUB_OUTPUT + + build-and-publish: + name: Build and Publish + needs: auto-tag + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + image-tag: ${{ steps.tagger.outputs.image_tag }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Save image tag + id: tagger + run: | + echo "image_tag=${{ needs.auto-tag.outputs.image-tag }}" >> $GITHUB_OUTPUT + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: server + push: true + tags: | + ghcr.io/${{ github.repository }}/server:${{ needs.auto-tag.outputs.image-tag }} + ghcr.io/${{ github.repository }}/server:latest-rc + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + name: Deploy UAT + needs: build-and-publish + runs-on: ubuntu-latest + permissions: + contents: read + env: + PROJECT_ID: ${{ secrets.GKE_PROJECT }} + GKE_CLUSTER_NAME: uat + GKE_CLUSTER_ZONE: europe-central2 + GKE_BASTION_NAME: gke-uat-bastion + GKE_BASTION_ZONE: europe-central2-a + HELM_NAMESPACE: clearnet-uat + HELM_RELEASE: faucet-app + HELM_CONFIG_ENV: uat + KUBECONFIG: kubeconfig.conf + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: '${{ secrets.GKE_UAT_SA_KEY }}' + + - name: Set up Google Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + with: + project_id: ${{ secrets.GKE_PROJECT }} + install_components: gke-gcloud-auth-plugin + + - name: Get GKE credentials + run: |- + gcloud container clusters get-credentials ${GKE_CLUSTER_NAME} \ + --project ${PROJECT_ID} \ + --zone ${GKE_CLUSTER_ZONE} \ + --internal-ip + + - name: Port-forward to GKE Bastion + run: |- + gcloud compute ssh ${GKE_BASTION_NAME} \ + --tunnel-through-iap \ + --project ${PROJECT_ID} \ + --zone ${GKE_BASTION_ZONE} \ + --ssh-key-expiration=600s \ + -- -NL8888:127.0.0.1:8888 & + sleep 5 + + - name: Install Helm + uses: azure/setup-helm@v4 + + - name: Release + env: + HTTPS_PROXY: http://localhost:8888 + run: |- + helm upgrade -i ${HELM_RELEASE} chart \ + -n ${HELM_NAMESPACE} \ + --reuse-values \ + -f chart/config/${HELM_CONFIG_ENV}/values.yaml \ + --set image.tag="${{ needs.build-and-publish.outputs.image-tag }}" + + notify-slack: + name: Notify Slack + runs-on: ubuntu-latest + needs: [deploy] + if: always() + + steps: + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_CHANNEL: deployments + SLACK_USERNAME: CI/CD Bot + SLACK_COLOR: ${{ contains(needs.*.result, 'failure') && 'failure' || contains(needs.*.result, 'cancelled') && 'cancelled' || 'success' }} + SLACK_ICON_EMOJI: ${{ contains(needs.*.result, 'failure') && ':x:' || contains(needs.*.result, 'cancelled') && ':warning:' || ':white_check_mark:' }} + SLACK_TITLE: '🚰 🛫 Faucet App Release Candidate 🛬 🚰' + SLACK_MESSAGE_ON_SUCCESS: | + ✅ RC build and deployment completed successfully! + ${{github.event.head_commit.message}} + SLACK_MESSAGE_ON_FAILURE: | + ❌ RC build or deployment failed! + ${{github.event.head_commit.message}} + SLACK_MESSAGE_ON_CANCEL: | + ⚠️ RC build or deployment was cancelled! + ${{github.event.head_commit.message}} + SLACK_FOOTER: 'Faucet App CI/CD Pipeline' diff --git a/faucet-app/.github/workflows/stable-tag.yml b/faucet-app/.github/workflows/stable-tag.yml new file mode 100644 index 000000000..45ee6ce67 --- /dev/null +++ b/faucet-app/.github/workflows/stable-tag.yml @@ -0,0 +1,134 @@ +name: Stable Tag + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + +jobs: + build-and-publish: + name: Build and Publish + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + image-tag: ${{ steps.tagger.outputs.tag }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract tag name without 'v' prefix + id: tagger + run: echo "tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: server + push: true + tags: | + ghcr.io/${{ github.repository }}/server:${{ steps.tagger.outputs.tag }} + ghcr.io/${{ github.repository }}/server:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + name: Deploy Sandbox + needs: build-and-publish + runs-on: ubuntu-latest + env: + PROJECT_ID: ${{ secrets.GKE_PROJECT }} + GKE_CLUSTER_NAME: uat + GKE_CLUSTER_ZONE: europe-central2 + GKE_BASTION_NAME: gke-uat-bastion + GKE_BASTION_ZONE: europe-central2-a + HELM_NAMESPACE: clearnet-sandbox + HELM_RELEASE: faucet-app + HELM_CONFIG_ENV: sandbox + KUBECONFIG: kubeconfig.conf + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: '${{ secrets.GKE_SANDBOX_SA_KEY }}' + + - name: Set up Google Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + with: + project_id: ${{ secrets.GKE_PROJECT }} + install_components: gke-gcloud-auth-plugin + + - name: Get GKE credentials + run: |- + gcloud container clusters get-credentials ${GKE_CLUSTER_NAME} \ + --project ${PROJECT_ID} \ + --zone ${GKE_CLUSTER_ZONE} \ + --internal-ip + + - name: Port-forward to GKE Bastion + run: |- + gcloud compute ssh ${GKE_BASTION_NAME} \ + --tunnel-through-iap \ + --project ${PROJECT_ID} \ + --zone ${GKE_BASTION_ZONE} \ + --ssh-key-expiration=600s \ + -- -NL8888:127.0.0.1:8888 & + sleep 5 + + - name: Install Helm + uses: azure/setup-helm@v4 + + - name: Release + env: + HTTPS_PROXY: http://localhost:8888 + run: |- + helm upgrade -i ${HELM_RELEASE} chart \ + -n ${HELM_NAMESPACE} \ + --reuse-values \ + -f chart/config/${HELM_CONFIG_ENV}/values.yaml \ + --set image.tag="${{ needs.build-and-publish.outputs.image-tag }}" + + notify-slack: + name: Notify Slack + runs-on: ubuntu-latest + needs: [build-and-publish, deploy] + if: always() + + steps: + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_CHANNEL: deployments + SLACK_USERNAME: CI/CD Bot + SLACK_COLOR: ${{ contains(needs.*.result, 'failure') && 'failure' || contains(needs.*.result, 'cancelled') && 'cancelled' || 'success' }} + SLACK_ICON_EMOJI: ${{ contains(needs.*.result, 'failure') && ':x:' || contains(needs.*.result, 'cancelled') && ':warning:' || ':white_check_mark:' }} + SLACK_TITLE: '🚰 🚀 Faucet App Stable Release 🚀 🚰' + SLACK_MESSAGE_ON_SUCCESS: | + ✅ Stable build and deployment completed successfully! + ${{github.event.head_commit.message}} + SLACK_MESSAGE_ON_FAILURE: | + ❌ Stable build or deployment failed! + ${{github.event.head_commit.message}} + SLACK_MESSAGE_ON_CANCEL: | + ⚠️ Stable build or deployment was cancelled! + ${{github.event.head_commit.message}} + SLACK_FOOTER: 'Faucet App CI/CD Pipeline' diff --git a/faucet-app/LICENSE b/faucet-app/LICENSE new file mode 100644 index 000000000..91b2101b4 --- /dev/null +++ b/faucet-app/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ERC-7824 Chain Agnostic State Channel Protocol + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/faucet-app/chart/.helmignore b/faucet-app/chart/.helmignore new file mode 100644 index 000000000..063045509 --- /dev/null +++ b/faucet-app/chart/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +config/ diff --git a/faucet-app/chart/Chart.yaml b/faucet-app/chart/Chart.yaml new file mode 100644 index 000000000..c58c1e2a7 --- /dev/null +++ b/faucet-app/chart/Chart.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v2 +description: Faucet App Helm chart +name: faucet-app +version: 1.0.0 diff --git a/faucet-app/chart/README.md b/faucet-app/chart/README.md new file mode 100644 index 000000000..5e020cb6e --- /dev/null +++ b/faucet-app/chart/README.md @@ -0,0 +1,142 @@ +# faucet-app + +![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) + +Faucet App Helm chart + +## Prerequisites + +- Kubernetes 1.24+ +- Helm 3.0+ +- For TLS: cert-manager installed in the cluster +- For Secrets Management (optional): + - [helm-secrets](https://github.com/jkroepke/helm-secrets/wiki) plugin: `helm plugin install https://github.com/jkroepke/helm-secrets --version v4.6.4` + - [vals](https://github.com/helmfile/vals): `go install github.com/helmfile/vals/cmd/vals@v0.41.0` + +## Installing the Chart + +To install the chart with the release name `my-release`: +```bash +helm install my-release git+https://github.com/erc7824/clearnode@chart?ref=main +``` + +The command deploys Faucet App on the Kubernetes cluster with default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation. + +## Uninstalling the Chart + +To uninstall/delete the `my-release` deployment: +```bash +helm delete my-release +``` + +## Parameters + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | Affinity settings | +| autoscaling.enabled | bool | `false` | Enable autoscaling | +| autoscaling.maxReplicas | int | `100` | Maximum number of replicas | +| autoscaling.minReplicas | int | `2` | Minimum number of replicas | +| autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization | +| autoscaling.targetMemoryUtilizationPercentage | int | `80` | Target memory utilization | +| config.args | list | `["./faucet-server"]` | List of arguments to pass to the container | +| config.clearnodeWsUrl | string | `"wss://clearnode.example.com/ws"` | Clearnode WebSocket URL | +| config.envSecret | string | `""` | Name of the secret containing environment variables | +| config.extraEnvs | object | `{}` | Additional environment variables as key-value pairs | +| config.logLevel | string | `"info"` | Log level (info, debug, warn, error) | +| config.minTransferCount | int | `5` | Minimum number of transfers the server should have a balance for to operate | +| config.secretEnvs | object | `{}` | Additional environment variables to be stored in a secret | +| config.token.symbol | string | `"usdc"` | Token Symbol inside the Clearnode network | +| config.token.tipAmount | int | `10` | The amount of tokens to tip per request | +| extraLabels | object | `{}` | Additional labels to add to all resources | +| fullnameOverride | string | `""` | Override the full name | +| image.repository | string | `"ghcr.io/erc7824/faucet-app/server"` | Docker image repository | +| image.tag | string | `"0.0.1"` | Docker image tag | +| imagePullSecret | string | `""` | Image pull secret name | +| metrics.enabled | bool | `true` | Enable Prometheus metrics | +| metrics.endpoint | string | `"/metrics"` | Metrics endpoint path | +| metrics.podmonitoring.enabled | bool | `false` | Enable PodMonitoring for Managed Prometheus | +| metrics.port | int | `4242` | Metrics port | +| metrics.scrapeInterval | string | `"30s"` | Metrics scrape interval | +| networking.externalHostname | string | `"clearnode.example.com"` | External hostname for the gateway | +| networking.gateway.className | string | `"envoy-gateway"` | Gateway class name | +| networking.gateway.enabled | bool | `true` | Enable API gateway | +| networking.gateway.ipAddressName | string | `""` | GKE static IP address name (GKE only) | +| networking.ingress.annotations | object | `{}` | Ingress annotations | +| networking.ingress.className | string | `"nginx"` | Ingress class name | +| networking.ingress.enabled | bool | `false` | Enable ingress | +| networking.ingress.grpc | bool | `false` | Enable GRPC for ingress | +| networking.ingress.tls.enabled | bool | `false` | Enable TLS for ingress | +| networking.tlsClusterIssuer | string | `"zerossl-prod"` | TLS cluster issuer | +| nodeSelector | object | `{}` | Node selector | +| probes.liveness.enabled | bool | `false` | Enable liveness probe | +| probes.liveness.type | string | `"tcp"` | Liveness probe type (http, tcp) | +| probes.readiness.enabled | bool | `false` | Enable readiness probe | +| probes.readiness.type | string | `"tcp"` | Readiness probe type (http, tcp) | +| replicaCount | int | `1` | Number of replicas | +| resources.limits | object | `{}` | Resource limits | +| resources.requests | object | `{}` | Resource requests | +| service.http.enabled | bool | `true` | Enable HTTP service | +| service.http.path | string | `"/"` | HTTP service path | +| service.http.port | int | `8000` | HTTP service port | +| serviceAccount | string | `""` | Service account name | +| tolerations | list | `[]` | Tolerations | + +## Gateway Configuration + +By default, the chart creates an API Gateway and configures it to use TLS via cert-manager. To use this feature: + +1. Create a cert-manager ClusterIssuer +2. Configure `networking.tlsClusterIssuer` with the issuer name +3. Set `networking.externalHostname` to your domain name + +> **Warning**: The Gateway currently does not support configurations with a static IP address. Ensure that your setup uses a dynamic DNS or hostname for proper functionality. Alternatively, you can configure an ingress resource to use a static IP address if required. + +## Managing Secrets + +For managing sensitive values like API keys and credentials, you can use `helm-secrets` with `vals`: + +1. Set up the required environment variable: + ```bash + export HELM_SECRETS_BACKEND=vals + ``` + +2. Create a values file with your secrets (e.g., `secrets.yaml`) and refer to secrets using the vals syntax: + ```yaml + apiKey: ref+awssecrets://my-secret/api-key + database: + password: ref+vault://secret/data/database?key=password + ``` + +3. When deploying or upgrading, reference your secrets file with the `secrets://` prefix: + ```bash + helm upgrade --install my-release git+https://github.com/erc7824/clearnode@chart?ref=main \ + -f values.yaml \ + -f secrets://secrets.yaml + ``` + +The vals tool supports [multiple backends](https://github.com/helmfile/vals/tree/main?tab=readme-ov-file#supported-backends) including: +- AWS Secrets Manager and SSM Parameter Store +- Google Cloud Secret Manager +- HashiCorp Vault +- Azure Key Vault +- And many more + +For detailed usage, consult the [helm-secrets documentation](https://github.com/jkroepke/helm-secrets/wiki). + +## Troubleshooting + +### Common Issues + +- **Database Connection Issues**: Ensure the database connection URL is correct and the database is accessible from the cluster +- **TLS Certificate Issues**: Check cert-manager logs for problems with certificate issuance +- **Blockchain Connection Issues**: Verify RPC endpoint URLs are correct and accessible + +For more detailed debugging, check the application logs: + +```bash +kubectl logs -l app=clearnode +``` + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/faucet-app/chart/README.md.gotmpl b/faucet-app/chart/README.md.gotmpl new file mode 100644 index 000000000..a42a0dfb8 --- /dev/null +++ b/faucet-app/chart/README.md.gotmpl @@ -0,0 +1,92 @@ +# {{ template "chart.name" . }} + +{{ template "chart.versionBadge" . }} + +{{ template "chart.description" . }} + +## Prerequisites + +- Kubernetes 1.24+ +- Helm 3.0+ +- For TLS: cert-manager installed in the cluster +- For Secrets Management (optional): + - [helm-secrets](https://github.com/jkroepke/helm-secrets/wiki) plugin: `helm plugin install https://github.com/jkroepke/helm-secrets --version v4.6.4` + - [vals](https://github.com/helmfile/vals): `go install github.com/helmfile/vals/cmd/vals@v0.41.0` + +## Installing the Chart + +To install the chart with the release name `my-release`: +```bash +helm install my-release git+https://github.com/erc7824/clearnode@chart?ref=main +``` + +The command deploys Faucet App on the Kubernetes cluster with default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation. + +## Uninstalling the Chart + +To uninstall/delete the `my-release` deployment: +```bash +helm delete my-release +``` + +## Parameters + +{{ template "chart.valuesTable" . }} + +## Gateway Configuration + +By default, the chart creates an API Gateway and configures it to use TLS via cert-manager. To use this feature: + +1. Create a cert-manager ClusterIssuer +2. Configure `networking.tlsClusterIssuer` with the issuer name +3. Set `networking.externalHostname` to your domain name + +> **Warning**: The Gateway currently does not support configurations with a static IP address. Ensure that your setup uses a dynamic DNS or hostname for proper functionality. Alternatively, you can configure an ingress resource to use a static IP address if required. + +## Managing Secrets + +For managing sensitive values like API keys and credentials, you can use `helm-secrets` with `vals`: + +1. Set up the required environment variable: + ```bash + export HELM_SECRETS_BACKEND=vals + ``` + +2. Create a values file with your secrets (e.g., `secrets.yaml`) and refer to secrets using the vals syntax: + ```yaml + apiKey: ref+awssecrets://my-secret/api-key + database: + password: ref+vault://secret/data/database?key=password + ``` + +3. When deploying or upgrading, reference your secrets file with the `secrets://` prefix: + ```bash + helm upgrade --install my-release git+https://github.com/erc7824/clearnode@chart?ref=main \ + -f values.yaml \ + -f secrets://secrets.yaml + ``` + +The vals tool supports [multiple backends](https://github.com/helmfile/vals/tree/main?tab=readme-ov-file#supported-backends) including: +- AWS Secrets Manager and SSM Parameter Store +- Google Cloud Secret Manager +- HashiCorp Vault +- Azure Key Vault +- And many more + +For detailed usage, consult the [helm-secrets documentation](https://github.com/jkroepke/helm-secrets/wiki). + +## Troubleshooting + +### Common Issues + +- **Database Connection Issues**: Ensure the database connection URL is correct and the database is accessible from the cluster +- **TLS Certificate Issues**: Check cert-manager logs for problems with certificate issuance +- **Blockchain Connection Issues**: Verify RPC endpoint URLs are correct and accessible + +For more detailed debugging, check the application logs: + +```bash +kubectl logs -l app=clearnode +``` + +{{ template "helm-docs.versionFooter" . }} diff --git a/faucet-app/chart/config/sandbox/secrets.yaml b/faucet-app/chart/config/sandbox/secrets.yaml new file mode 100644 index 000000000..1b2aeb73c --- /dev/null +++ b/faucet-app/chart/config/sandbox/secrets.yaml @@ -0,0 +1,4 @@ +config: + secretEnvs: + OWNER_PRIVATE_KEY: ref+gcpsecrets://ynet-stage/clearnet-sandbox-faucet-owner-private-key?version=latest + SIGNER_PRIVATE_KEY: ref+gcpsecrets://ynet-stage/clearnet-sandbox-faucet-signer-private-key?version=latest diff --git a/faucet-app/chart/config/sandbox/values.yaml b/faucet-app/chart/config/sandbox/values.yaml new file mode 100644 index 000000000..8315daee2 --- /dev/null +++ b/faucet-app/chart/config/sandbox/values.yaml @@ -0,0 +1,56 @@ +config: + args: ["./faucet-server"] + logLevel: debug + clearnodeWsUrl: wss://clearnet-sandbox.yellow.com/ws + token: + symbol: ytest.usd + tipAmount: 10 + minTransferCount: 5 + envSecret: "" + extraEnvs: {} + +image: + repository: ghcr.io/erc7824/faucet-app/server + tag: 25f6448 + +imagePullSecret: ghcr-pull + +service: + http: + enabled: true + port: 8080 + path: /faucet(/|$)(.*) + +metrics: + enabled: false + podmonitoring: + enabled: false + port: 4242 + endpoint: "/metrics" + +resources: + limits: + cpu: 100m + memory: 256Mi + ephemeral-storage: 100Mi + requests: + cpu: 100m + memory: 256Mi + ephemeral-storage: 100Mi + +autoscaling: + enabled: false + +networking: + externalHostname: clearnet-sandbox.yellow.com + tlsClusterIssuer: zerossl-prod + gateway: + enabled: false + ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 + tls: + enabled: true diff --git a/faucet-app/chart/config/uat/secrets.yaml b/faucet-app/chart/config/uat/secrets.yaml new file mode 100644 index 000000000..35403902f --- /dev/null +++ b/faucet-app/chart/config/uat/secrets.yaml @@ -0,0 +1,4 @@ +config: + secretEnvs: + OWNER_PRIVATE_KEY: ref+gcpsecrets://ynet-stage/clearnet-uat-faucet-owner-private-key?version=latest + SIGNER_PRIVATE_KEY: ref+gcpsecrets://ynet-stage/clearnet-uat-faucet-signer-private-key?version=latest diff --git a/faucet-app/chart/config/uat/values.yaml b/faucet-app/chart/config/uat/values.yaml new file mode 100644 index 000000000..dc4badb95 --- /dev/null +++ b/faucet-app/chart/config/uat/values.yaml @@ -0,0 +1,56 @@ +config: + args: ["./faucet-server"] + logLevel: debug + clearnodeWsUrl: wss://canarynet.yellow.com/ws + token: + symbol: ytest.usd + tipAmount: 0.1 + minTransferCount: 5 + envSecret: "" + extraEnvs: {} + +image: + repository: ghcr.io/erc7824/faucet-app/server + tag: 25f6448 + +imagePullSecret: ghcr-pull + +service: + http: + enabled: true + port: 8080 + path: /faucet(/|$)(.*) + +metrics: + enabled: false + podmonitoring: + enabled: false + port: 4242 + endpoint: "/metrics" + +resources: + limits: + cpu: 100m + memory: 256Mi + ephemeral-storage: 100Mi + requests: + cpu: 100m + memory: 256Mi + ephemeral-storage: 100Mi + +autoscaling: + enabled: false + +networking: + externalHostname: canarynet.yellow.com + tlsClusterIssuer: zerossl-prod + gateway: + enabled: false + ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 + tls: + enabled: true diff --git a/faucet-app/chart/templates/deployment.yaml b/faucet-app/chart/templates/deployment.yaml new file mode 100644 index 000000000..5c48c3004 --- /dev/null +++ b/faucet-app/chart/templates/deployment.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "faucet-app.common.fullname" . }} + labels: + {{- include "faucet-app.common.labels" . | nindent 4 }} +spec: + {{- include "faucet-app.component.replicaCount" . | nindent 2 }} + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + {{- include "faucet-app.common.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + {{- include "faucet-app.component.metricsAnnotations" .Values.metrics | nindent 8 }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + labels: + {{- include "faucet-app.common.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.serviceAccount }} + serviceAccountName: {{ . }} + {{- end }} + containers: + - name: api + args: {{ .Values.config.args | toYaml | nindent 10 }} + image: {{ include "faucet-app.component.image" .Values.image }} + imagePullPolicy: IfNotPresent + env: + {{- include "faucet-app.common.env" . | nindent 12 }} + {{- if or .Values.config.secretEnvs .Values.config.envSecret }} + envFrom: + {{- if .Values.config.envSecret }} + - secretRef: + name: {{ .Values.config.envSecret }} + {{- end }} + {{- if .Values.config.secretEnvs }} + - secretRef: + name: {{ include "faucet-app.common.fullname" . }}-secret-env + {{- end }} + {{- end }} + {{- include "faucet-app.component.ports" .Values.service | nindent 10 }} + {{- include "faucet-app.component.resources" .Values.resources | nindent 10 }} + {{- include "faucet-app.component.probes" . | nindent 10 }} + {{- include "faucet-app.common.imagePullSecrets" . | nindent 6 }} + {{- include "faucet-app.common.nodeSelectorLabels" . | nindent 6 }} + {{- include "faucet-app.common.affinity" . | nindent 6 }} + {{- include "faucet-app.common.tolerations" . | nindent 6 }} diff --git a/faucet-app/chart/templates/gateway.yaml b/faucet-app/chart/templates/gateway.yaml new file mode 100644 index 000000000..48fee0bf6 --- /dev/null +++ b/faucet-app/chart/templates/gateway.yaml @@ -0,0 +1,31 @@ +{{- if .Values.networking.gateway.enabled }} +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: {{ include "faucet-app.common.fullname" . }} + labels: + {{- include "faucet-app.common.labels" . | nindent 4 }} + annotations: + cert-manager.io/cluster-issuer: {{ .Values.networking.tlsClusterIssuer }} + # FIXME: This annotation doesn't work + {{- if .Values.networking.gateway.ipAddressName }} + gateway.envoyproxy.io/service-annotations: | + networking.gke.io/load-balancer-ip-addresses: "{{ .Values.networking.gateway.ipAddressName }}" + {{- end }} +spec: + gatewayClassName: {{ .Values.networking.gateway.className }} + listeners: + - name: http + protocol: HTTP + hostname: {{ .Values.networking.externalHostname }} + port: 80 + - name: https + protocol: HTTPS + hostname: {{ .Values.networking.externalHostname }} + port: 443 + tls: + mode: Terminate + certificateRefs: + - kind: Secret + name: {{ printf "%s-tls" (.Values.networking.externalHostname | replace "." "-") }} +{{- end }} diff --git a/faucet-app/chart/templates/helpers/_common.tpl b/faucet-app/chart/templates/helpers/_common.tpl new file mode 100644 index 000000000..523def25c --- /dev/null +++ b/faucet-app/chart/templates/helpers/_common.tpl @@ -0,0 +1,116 @@ +{{/* vim: set filetype=mustache: */}} + +{{/* +Expand the name of the component. +*/}} +{{- define "faucet-app.common.name" -}} +{{- .Chart.Name | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully common name. +If release name contains chart name it will be used as a full name. +*/}} +{{- define "faucet-app.common.fullname" -}} +{{- if .Values.prefixOverride }} +{{- printf "%s-%s" .Values.prefixOverride .Chart.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- if contains .Chart.Name .Release.Name }} +{{- print .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Common Selector labels +*/}} +{{- define "faucet-app.common.selectorLabels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "faucet-app.common.labels" -}} +helm.sh/chart: {{ include "faucet-app.common.chart" . }} +{{ include "faucet-app.common.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.extraLabels }} +{{ toYaml . }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "faucet-app.common.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Returns common image pull secrets +*/}} +{{- define "faucet-app.common.imagePullSecrets" -}} +{{- with .Values.imagePullSecret }} +imagePullSecrets: +- name: {{ . }} +{{- end }} +{{- end }} + +{{/* +Returns common environment variables +*/}} +{{- define "faucet-app.common.env" -}} +- name: LOG_LEVEL + value: {{ .Values.config.logLevel }} +- name: SERVER_PORT + value: {{ .Values.service.http.internalPort | default "8080" | print | quote }} +- name: CLEARNODE_URL + value: {{ .Values.config.clearnodeWsUrl | print }} +- name: TOKEN_SYMBOL + value: {{ .Values.config.token.symbol | print }} +- name: STANDARD_TIP_AMOUNT + value: {{ .Values.config.token.tipAmount | print | quote }} +- name: MIN_TRANSFER_COUNT + value: {{ .Values.config.minTransferCount | print | quote }} +{{- range $key, $value := .Values.config.extraEnvs }} +- name: {{ $key | upper }} + value: {{ $value | print | quote }} +{{- end }} +{{- end }} + +{{/* +Returns common node selector labels +*/}} +{{- define "faucet-app.common.nodeSelectorLabels" -}} +{{- with .Values.nodeSelector }} +nodeSelector: + {{ toYaml . | nindent 2 }} +{{- end }} +{{- end }} + +{{/* +Returns common tolerations +*/}} +{{- define "faucet-app.common.tolerations" -}} +{{- with .Values.tolerations }} +tolerations: +{{ toYaml . }} +{{- end }} +{{- end }} + +{{/* +Returns common pod's affinity +*/}} +{{- define "faucet-app.common.affinity" -}} +{{- with .Values.affinity }} +affinity: + {{ toYaml . | nindent 2 }} +{{- end }} +{{- end }} diff --git a/faucet-app/chart/templates/helpers/_component.tpl b/faucet-app/chart/templates/helpers/_component.tpl new file mode 100644 index 000000000..ff4b48d8d --- /dev/null +++ b/faucet-app/chart/templates/helpers/_component.tpl @@ -0,0 +1,78 @@ +{{/* vim: set filetype=mustache: */}} + +{{/* +Returns Prometheus metrics' annotations depending on input Values +*/}} +{{- define "faucet-app.component.metricsAnnotations" -}} +prometheus.io/scrape: {{ default false .enabled | print | quote }} +prometheus.io/port: {{ default "4242" .port | print | quote }} +prometheus.io/path: {{ default "/metrics" .endpoint | print | quote }} +{{- end }} + +{{/* +Returns replica count depending on component and HPA settings +*/}} +{{- define "faucet-app.component.replicaCount" -}} +{{- if not (and .autoscaling .autoscaling.enabled) }} +replicas: {{ .replicaCount }} +{{- end }} +{{- end }} + +{{/* +Returns full docker image name +*/}} +{{- define "faucet-app.component.image" -}} +{{ printf "%s:%s" (print .repository) (print .tag) }} +{{- end }} + +{{/* +Returns container ports configuration depending on input service +*/}} +{{- define "faucet-app.component.ports" -}} +{{- if .http.enabled }} +ports: +{{- with .http }} +- name: http + containerPort: {{ default .port .internalPort }} + protocol: TCP +{{- end }} +{{- end }} +{{- end }} + +{{/* +Returns component's resource consumption +*/}} +{{- define "faucet-app.component.resources" -}} +resources: + requests: + cpu: {{ default "100m" .requests.cpu }} + memory: {{ default "128Mi" .requests.memory }} + ephemeral-storage: {{ default "100Mi" .requests.memory }} + limits: + cpu: {{ default "100m" .requests.cpu }} + memory: {{ default "128Mi" .requests.memory }} + ephemeral-storage: {{ default "100Mi" .requests.memory }} +{{- end }} + +{{/* +Returns component's probes +*/}} +{{- define "faucet-app.component.probes" -}} +{{- $port := default .Values.service.http.port .Values.service.http.internalPort }} +{{- range $name, $probe := .Values.probes }} +{{- if $probe.enabled }} +{{ printf "%sProbe" $name }}: + {{- if eq $probe.type "http" }} + httpGet: + port: {{ $port }} + path: {{ default "/health" $probe.endpoint }} + {{- else }} + tcpSocket: + port: {{ $port }} + {{- end }} + initialDelaySeconds: {{ default 5 $probe.initialDelaySeconds }} + timeoutSeconds: {{ default 10 $probe.timeoutSeconds }} + periodSeconds: {{ default 10 $probe.periodSeconds }} +{{- end }} +{{- end }} +{{- end }} diff --git a/faucet-app/chart/templates/helpers/_hpa.tpl b/faucet-app/chart/templates/helpers/_hpa.tpl new file mode 100644 index 000000000..80006892c --- /dev/null +++ b/faucet-app/chart/templates/helpers/_hpa.tpl @@ -0,0 +1,24 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Returns HorizontalPodAutoscaler API version depending on K8s cluster version +*/}} +{{- define "faucet-app.hpa.apiVersion" -}} +{{- if semverCompare ">=1.23-0" .Capabilities.KubeVersion.Version -}} +autoscaling/v2 +{{- else -}} +autoscaling/v2beta2 +{{- end }} +{{- end }} + +{{/* +Returns HorizontalPodAutoscaler resource target utilization depending on K8s cluster version +*/}} +{{- define "faucet-app.hpa.targetUtilization" -}} +{{- if semverCompare ">=1.23-0" .Capabilities.KubeVersion.Version -}} +target: + type: Utilization + averageUtilization: {{ .averageUtilization }} +{{- else -}} +targetAverageUtilization: {{ .averageUtilization }} +{{- end }} +{{- end }} diff --git a/faucet-app/chart/templates/helpers/_ingress.tpl b/faucet-app/chart/templates/helpers/_ingress.tpl new file mode 100644 index 000000000..0bf1ec4fa --- /dev/null +++ b/faucet-app/chart/templates/helpers/_ingress.tpl @@ -0,0 +1,67 @@ +{{/* vim: set filetype=mustache: */}} + +{{/* +Returns Ingress API version depending on K8s cluster version +*/}} +{{- define "faucet-app.ingress.apiVersion" -}} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.Version -}} +networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.Version -}} +networking.k8s.io/v1beta1 +{{- else -}} +extensions/v1beta1 +{{- end }} +{{- end }} + +{{/* +Returns default Ingress annotations +*/}} +{{- define "faucet-app.ingress.annotations" -}} +kubernetes.io/ingress.class: {{ default "nginx" .Values.networking.ingress.className }} +{{- if .Values.networking.ingress.tls.enabled }} +kubernetes.io/tls-acme: "true" +cert-manager.io/cluster-issuer: {{ .Values.networking.tlsClusterIssuer }} +nginx.ingress.kubernetes.io/ssl-redirect: "true" +{{- end }} +{{- if .Values.networking.ingress.grpc }} +nginx.ingress.kubernetes.io/backend-protocol: "GRPC" +{{- end }} +{{- with .Values.networking.ingress.annotations }} +{{ toYaml . }} +{{- end }} +{{- end }} + +{{/* +Returns Ingress TLS configuration +*/}} +{{- define "faucet-app.ingress.tls" -}} +{{- if .Values.networking.ingress.tls.enabled }} +tls: + - secretName: "{{ .Values.networking.externalHostname | replace "." "-" }}-tls" + hosts: + - "{{ .Values.networking.externalHostname }}" +{{- end }} +{{- end }} + +{{/* +Returns Ingress host path configuration +*/}} +{{- define "faucet-app.ingress.httpPath" -}} +{{- $http := .Values.service.http }} +- path: {{ $http.path }} + {{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.Version }} + pathType: ImplementationSpecific + {{- end }} + backend: + {{ $svcName := include "faucet-app.common.fullname" . }} + {{ $svcPort := default $http.port $http.internalPort }} + {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.Version }} + service: + name: {{ $svcName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $svcName }} + servicePort: {{ $svcPort }} + {{- end }} +{{- end }} diff --git a/faucet-app/chart/templates/http-route-redirect.yaml b/faucet-app/chart/templates/http-route-redirect.yaml new file mode 100644 index 000000000..cfe3af2cb --- /dev/null +++ b/faucet-app/chart/templates/http-route-redirect.yaml @@ -0,0 +1,25 @@ +{{- if .Values.networking.gateway.enabled }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ printf "%s-http-redirect" (include "faucet-app.common.fullname" .) }} + labels: + {{- include "faucet-app.common.labels" . | nindent 4 }} +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + namespace: {{ .Release.Namespace }} + name: {{ include "faucet-app.common.fullname" . }} + port: 80 + sectionName: http + hostnames: + - {{ .Values.networking.externalHostname }} + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 + hostname: {{ .Values.networking.externalHostname }} +{{- end }} diff --git a/faucet-app/chart/templates/http-route.yaml b/faucet-app/chart/templates/http-route.yaml new file mode 100644 index 000000000..587eb211b --- /dev/null +++ b/faucet-app/chart/templates/http-route.yaml @@ -0,0 +1,27 @@ +{{- $http := .Values.service.http }} +{{- if and .Values.networking.gateway.enabled $http.enabled }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "faucet-app.common.fullname" . }} + labels: + {{- include "faucet-app.common.labels" . | nindent 4 }} +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + namespace: {{ .Release.Namespace }} + name: {{ include "faucet-app.common.fullname" . }} + port: 443 + sectionName: https + hostnames: + - {{ .Values.networking.externalHostname }} + rules: + - matches: + - path: + type: PathPrefix + value: {{ $http.path }} + backendRefs: + - name: {{ include "faucet-app.common.fullname" . }} + port: {{ default $http.port $http.internalPort }} +{{- end }} diff --git a/faucet-app/chart/templates/ingress.yaml b/faucet-app/chart/templates/ingress.yaml new file mode 100644 index 000000000..7824ff2c9 --- /dev/null +++ b/faucet-app/chart/templates/ingress.yaml @@ -0,0 +1,17 @@ +{{- if and .Values.networking.ingress.enabled .Values.service.http.enabled }} +apiVersion: {{ include "faucet-app.ingress.apiVersion" . }} +kind: Ingress +metadata: + name: {{ include "faucet-app.common.fullname" . }} + labels: + {{- include "faucet-app.common.labels" . | nindent 4 }} + annotations: + {{- include "faucet-app.ingress.annotations" . | nindent 4 }} +spec: + rules: + - host: {{ .Values.networking.externalHostname }} + http: + paths: + {{- include "faucet-app.ingress.httpPath" . | nindent 10 }} + {{- include "faucet-app.ingress.tls" . | nindent 2 }} +{{- end }} diff --git a/faucet-app/chart/templates/podmonitoring.yaml b/faucet-app/chart/templates/podmonitoring.yaml new file mode 100644 index 000000000..31ac7dbdf --- /dev/null +++ b/faucet-app/chart/templates/podmonitoring.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.metrics.enabled .Values.metrics.podmonitoring.enabled }} +apiVersion: monitoring.googleapis.com/v1 +kind: PodMonitoring +metadata: + name: {{ include "faucet-app.common.fullname" . }} + labels: + {{- include "faucet-app.common.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "faucet-app.common.selectorLabels" . | nindent 6 }} + endpoints: + - port: {{ .Values.metrics.port }} + path: {{ .Values.metrics.endpoint }} + interval: {{ .Values.metrics.scrapeInterval }} +{{- end }} diff --git a/faucet-app/chart/templates/service.yaml b/faucet-app/chart/templates/service.yaml new file mode 100644 index 000000000..04854f913 --- /dev/null +++ b/faucet-app/chart/templates/service.yaml @@ -0,0 +1,20 @@ +{{- $svc := .Values.service }} +{{- if $svc.http.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "faucet-app.common.fullname" . }} + labels: + {{- include "faucet-app.common.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + {{- with $svc.http }} + - name: http + port: {{ default .port .externalPort }} + targetPort: {{ default .port .internalPort }} + protocol: TCP + {{- end }} + selector: + {{- include "faucet-app.common.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/faucet-app/chart/values.yaml b/faucet-app/chart/values.yaml new file mode 100644 index 000000000..cf0db080e --- /dev/null +++ b/faucet-app/chart/values.yaml @@ -0,0 +1,137 @@ +# -- Override the full name +fullnameOverride: "" + +config: + # -- List of arguments to pass to the container + args: ["./faucet-server"] + # -- Log level (info, debug, warn, error) + logLevel: info + # -- Clearnode WebSocket URL + clearnodeWsUrl: "wss://clearnode.example.com/ws" + token: + # -- Token Symbol inside the Clearnode network + symbol: usdc + # -- The amount of tokens to tip per request + tipAmount: 10 + # -- Minimum number of transfers the server should have a balance for to operate + minTransferCount: 5 + # -- Additional environment variables as key-value pairs + extraEnvs: {} + # KEY: VALUE + # -- Additional environment variables to be stored in a secret + secretEnvs: {} + # KEY: VALUE + # -- Name of the secret containing environment variables + envSecret: "" + +# -- Number of replicas +replicaCount: 1 + +image: + # -- Docker image repository + repository: ghcr.io/erc7824/faucet-app/server + # -- Docker image tag + tag: 0.0.1 + +service: + http: + # -- Enable HTTP service + enabled: true + # -- HTTP service port + port: 8000 + # -- HTTP service path + path: / + +metrics: + # -- Enable Prometheus metrics + enabled: true + podmonitoring: + # -- Enable PodMonitoring for Managed Prometheus + enabled: false + # -- Metrics port + port: 4242 + # -- Metrics endpoint path + endpoint: "/metrics" + # -- Metrics scrape interval + scrapeInterval: 30s + +probes: + liveness: + # -- Enable liveness probe + enabled: false + # -- Liveness probe type (http, tcp) + type: tcp + readiness: + # -- Enable readiness probe + enabled: false + # -- Readiness probe type (http, tcp) + type: tcp + +resources: + # -- Resource limits + limits: {} + # cpu: 100m + # memory: 256Mi + # ephemeral-storage: 100Mi + # -- Resource requests + requests: {} + # cpu: 100m + # memory: 256Mi + # ephemeral-storage: 100Mi + +# -- Service account name +serviceAccount: "" + +autoscaling: + # -- Enable autoscaling + enabled: false + # -- Minimum number of replicas + minReplicas: 2 + # -- Maximum number of replicas + maxReplicas: 100 + # -- Target CPU utilization + targetCPUUtilizationPercentage: 80 + # -- Target memory utilization + targetMemoryUtilizationPercentage: 80 + +networking: + # -- TLS cluster issuer + tlsClusterIssuer: zerossl-prod + # -- External hostname for the gateway + externalHostname: clearnode.example.com + + gateway: + # -- Enable API gateway + enabled: true + # -- Gateway class name + className: envoy-gateway + # -- GKE static IP address name (GKE only) + ipAddressName: "" + + ingress: + # -- Enable ingress + enabled: false + # -- Ingress class name + className: nginx + # -- Ingress annotations + annotations: {} + # -- Enable GRPC for ingress + grpc: false + tls: + # -- Enable TLS for ingress + enabled: false + +# -- Image pull secret name +imagePullSecret: "" + +# -- Node selector +nodeSelector: {} + +# -- Tolerations +tolerations: [] + +# -- Affinity settings +affinity: {} + +# -- Additional labels to add to all resources +extraLabels: {} diff --git a/faucet-app/scripts/auto_tag.sh b/faucet-app/scripts/auto_tag.sh new file mode 100755 index 000000000..ad36f2452 --- /dev/null +++ b/faucet-app/scripts/auto_tag.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +last_rc_tag=$(git ls-remote -t | grep -Eo 'v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$' | sort -V -r | head -n 1) +last_stable_tag=$(git ls-remote -t | grep -Eo 'v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V -r | head -n 1) + +new_rc_tag="" +if [ -z "$last_rc_tag" ] && [ -z "$last_stable_tag" ]; then + # No tags found + new_rc_tag="v0.0.0-rc.0" +elif [ -z "$last_stable_tag" ]; then + # No stable tags found + new_rc_tag="${last_rc_tag%.*}.$((${last_rc_tag##*.}+1))" +elif [ "${last_rc_tag%-*}" = "${last_stable_tag}" ]; then + # Last RC tag is the latest stable tag + new_rc_tag="${last_stable_tag}-rc.$((${last_rc_tag##*.}+1))" +else + # Last RC tag is not the latest stable tag + new_rc_tag="${last_stable_tag}-rc.0" +fi + +git tag "$new_rc_tag" +git push origin "$new_rc_tag" diff --git a/faucet-app/server/.env.example b/faucet-app/server/.env.example new file mode 100644 index 000000000..3e4f83957 --- /dev/null +++ b/faucet-app/server/.env.example @@ -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 + +# ----------------------------------------------------------------------------- +# Clearnode Configuration +# ----------------------------------------------------------------------------- +# Private key for faucet owner wallet (without 0x prefix) +# REQUIRED: Used for EIP-712 authentication with Clearnode +OWNER_PRIVATE_KEY=your_owner_private_key_here_without_0x_prefix + +# Private key for transaction signing (without 0x prefix) +# REQUIRED: Used for signing transfer transactions - must be different from OWNER_PRIVATE_KEY +SIGNER_PRIVATE_KEY=your_signer_private_key_here_without_0x_prefix + +# Clearnode WebSocket URL +# REQUIRED: The WebSocket endpoint for your Clearnode instance +CLEARNODE_URL=wss://clearnode.example.com/ws + +# Token symbol to distribute +# REQUIRED: The symbol of the token to distribute (must be supported by Clearnode) +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 diff --git a/faucet-app/server/.gitignore b/faucet-app/server/.gitignore new file mode 100644 index 000000000..4c49bd78f --- /dev/null +++ b/faucet-app/server/.gitignore @@ -0,0 +1 @@ +.env diff --git a/faucet-app/server/Dockerfile b/faucet-app/server/Dockerfile new file mode 100644 index 000000000..41e6091cf --- /dev/null +++ b/faucet-app/server/Dockerfile @@ -0,0 +1,33 @@ +# Build stage +FROM golang:1.25-alpine AS builder + +WORKDIR /app + +# Copy go mod and sum files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o faucet-server main.go + +# Production stage +FROM alpine:3.23.3 + +# Install ca-certificates for HTTPS requests +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ + +# Copy the binary from builder stage +COPY --from=builder /app/faucet-server . + +# Expose port +EXPOSE 8080 + +# Run the binary +CMD ["./faucet-server"] diff --git a/faucet-app/server/README.md b/faucet-app/server/README.md new file mode 100644 index 000000000..f8efa5104 --- /dev/null +++ b/faucet-app/server/README.md @@ -0,0 +1,252 @@ +# Nitrolite Faucet Server + +A Go-based faucet server that distributes tokens through the Clearnode network using WebSocket connections. + +## Features + +- **Nitrolite SDK Integration**: Uses the official `github.com/layer-3/nitrolite` SDK for Clearnode 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/clearnode`: Thin wrapper around the Nitrolite SDK client +- `internal/server`: HTTP server with Gin framework + +### Clearnode Client + +The `internal/clearnode` 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. **Clone and setup**: + + ```bash + cd 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. **Environment variables** (highest priority) +2. **`.env` file** in the current directory +3. **Default values** for optional settings + +Set the following environment variables (or create a `.env` file): + +| 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...` | +| `CLEARNODE_URL` | **Yes** | - | Clearnode WebSocket URL | `wss://testnet.clearnode.io/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` | +| `LOG_LEVEL` | No | `info` | Logging level (debug/info/warn/error) | `info` | + +## API Endpoints + +### POST /requestTokens + +Request tokens from the faucet. + +**Request Body:** + +```json +{ + "userAddress": "0x1234567890abcdef1234567890abcdef12345678" +} +``` + +**Success Response:** + +```json +{ + "success": true, + "message": "Tokens sent successfully", + "txId": "abc123", + "amount": "10", + "asset": "usdc", + "destination": "0x1234567890abcdef1234567890abcdef12345678" +} +``` + +**Error Response:** + +```json +{ + "error": "Invalid address format." +} +``` + +### GET /info + +Service information endpoint. + +**Response:** + +```json +{ + "service": "Nitrolite Faucet Server", + "version": "1.0.0", + "faucet_address": "0xabcd...", + "standard_tip_amount": "10", + "token_symbol": "usdc", + "endpoints": ["/requestTokens"] +} +``` + +## Startup Validation + +The server performs validation during startup: + +### Token Support Validation + +- Queries Clearnode using `GetAssets()` to fetch all supported tokens +- Validates that the configured `TOKEN_SYMBOL` exists +- Server refuses to start if the token is not supported + +### Balance Verification + +- Queries the owner balance using `GetBalances()` +- Requires balance ≥ `STANDARD_TIP_AMOUNT × MIN_TRANSFER_COUNT` +- Server refuses to start with insufficient funds + +Example startup output: + +```text +INFO Starting Nitrolite Faucet Server +INFO Faucet owner address: 0xabc... +INFO Successfully connected to Clearnode +INFO Token 'usdc' is supported by Clearnode +INFO ✓ Sufficient usdc balance: 50000000 +INFO Faucet server is ready to serve requests +``` + +## WebSocket Connection Management + +The Nitrolite SDK maintains a persistent WebSocket connection with Clearnode: + +- **Connection**: Established on startup inside `clearnode.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 recreates the SDK client automatically +- **Message Handling**: Fully managed by the SDK's internal RPC layer + +## 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 Clearnode requests are cryptographically signed by the SDK +- **Balance Guard**: Refuses to operate below minimum balance threshold + +## Building for Production + +```bash +# Build binary +go build -o faucet-server main.go + +# Run with environment file +./faucet-server +``` + +## Docker Support + +```dockerfile +FROM golang:1.21-alpine AS builder +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build -o faucet-server main.go + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /root/ +COPY --from=builder /app/faucet-server . +CMD ["./faucet-server"] +``` + +## Development + +```bash +# Install dependencies +go mod tidy + +# Run with hot reload (using air) +go install github.com/cosmtrek/air@latest +air + +# Run tests +go test ./... +``` + +## Logging + +The application uses structured JSON logging: + +```json +{ + "level": "info", + "msg": "Processing faucet request for address: 0x1234...", + "time": "2023-12-01T10:00:00Z" +} +``` + +Log levels: `debug`, `info`, `warn`, `error`, `fatal` + +## Error Handling + +- **Connection Errors**: Returns 503 if Clearnode is unavailable or reconnection fails +- **Validation Errors**: Returns 400 for invalid addresses or request format +- **Transfer Errors**: Returns 500 for Clearnode transfer failures +- **Service Unavailable**: Returns 503 if token is unsupported or balance is insufficient + +## Monitoring + +Key metrics to monitor: + +- Transfer success/failure rates +- Response times +- Server resource usage + +## Troubleshooting + +**Connection Issues:** + +- Verify `CLEARNODE_URL` is correct and accessible +- Check firewall settings for WebSocket connections + +**Authentication Issues:** + +- Verify `OWNER_PRIVATE_KEY` format (no `0x` prefix) +- Review logs for SDK connection errors + +**Transfer Issues:** + +- Verify `TOKEN_SYMBOL` is supported by the Clearnode instance +- Check faucet account balance meets the minimum threshold +- Review Clearnode transfer logs diff --git a/faucet-app/server/go.mod b/faucet-app/server/go.mod new file mode 100644 index 000000000..5bcbd32a5 --- /dev/null +++ b/faucet-app/server/go.mod @@ -0,0 +1,81 @@ +module faucet-server + +go 1.25.9 + +require ( + github.com/ethereum/go-ethereum v1.17.2 + github.com/gin-gonic/gin v1.11.0 + github.com/ilyakaznacheev/cleanenv v1.5.0 + github.com/layer-3/nitrolite v0.0.0 + github.com/shopspring/decimal v1.4.0 + github.com/sirupsen/logrus v1.9.4 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/bits-and-blooms/bitset v1.22.0 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/consensys/gnark-crypto v0.18.1 // indirect + github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set/v2 v2.8.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/goccy/go-json v0.10.4 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/jsternberg/zap-logfmt v1.3.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.57.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/supranational/blst v0.3.16 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect +) + +replace github.com/layer-3/nitrolite => ../../ diff --git a/faucet-app/server/go.sum b/faucet-app/server/go.sum new file mode 100644 index 000000000..0e11d3fd8 --- /dev/null +++ b/faucet-app/server/go.sum @@ -0,0 +1,309 @@ +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= +github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= +github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= +github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.17.2 h1:ag6geu0kn8Hv5FLKTpH+Hm2DHD+iuFtuqKxEuwUsDOI= +github.com/ethereum/go-ethereum v1.17.2/go.mod h1:KHcRXfGOUfUmKg51IhQ0IowiqZ6PqZf08CMtk0g5K1o= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= +github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= +github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= +github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= +github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= diff --git a/faucet-app/server/internal/clearnode/client.go b/faucet-app/server/internal/clearnode/client.go new file mode 100644 index 000000000..f7e14c558 --- /dev/null +++ b/faucet-app/server/internal/clearnode/client.go @@ -0,0 +1,230 @@ +package clearnode + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" + sdk "github.com/layer-3/nitrolite/sdk/go" + "github.com/shopspring/decimal" + + "faucet-server/internal/logger" +) + +// TransferResult holds the result of a token transfer. +type TransferResult struct { + TxID string + Amount string + Asset string +} + +// Client wraps the Nitrolite SDK client for faucet operations. +type Client struct { + mu sync.RWMutex + sdkClient *sdk.Client + newSDKClient func() (*sdk.Client, error) // captures parsed signers; no raw key hex stored + + tokenSymbol string + tipAmount decimal.Decimal + minTransferCount int +} + +func NewClient(privateKeyHex, clearnodeURL, tokenSymbol string, tipAmount decimal.Decimal, minTransferCount int) (*Client, error) { + // Parse signers once — raw key hex is used here and not retained on the struct. + msgSigner, err := sign.NewEthereumMsgSigner(privateKeyHex) + if err != nil { + return nil, fmt.Errorf("failed to create message signer: %w", err) + } + + stateSigner, err := core.NewChannelDefaultSigner(msgSigner) + if err != nil { + return nil, fmt.Errorf("failed to create state signer: %w", err) + } + + txSigner, err := sign.NewEthereumRawSigner(privateKeyHex) + if err != nil { + return nil, fmt.Errorf("failed to create tx signer: %w", err) + } + + // factory captures already-parsed signers so reconnects don't need the raw key. + factory := func() (*sdk.Client, error) { + cl, err := sdk.NewClient(clearnodeURL, stateSigner, txSigner) + if err != nil { + return nil, fmt.Errorf("failed to connect to Clearnode: %w", err) + } + return cl, nil + } + + sdkClient, err := factory() + if err != nil { + return nil, err + } + + return &Client{ + sdkClient: sdkClient, + newSDKClient: factory, + tokenSymbol: tokenSymbol, + tipAmount: tipAmount, + minTransferCount: minTransferCount, + }, nil +} + +// GetOwnerAddress returns the faucet owner's Ethereum address. +func (c *Client) GetOwnerAddress() string { + c.mu.RLock() + defer c.mu.RUnlock() + return c.sdkClient.GetUserAddress() +} + +// EnsureConnected checks the connection and reconnects if necessary. +func (c *Client) EnsureConnected() error { + // Fast path: read WaitCh under read lock. + c.mu.RLock() + waitCh := c.sdkClient.WaitCh() + c.mu.RUnlock() + + select { + case <-waitCh: + // Connection lost; fall through to reconnect. + default: + return nil + } + + // Slow path: write lock with double-check to prevent thundering-herd reconnects. + c.mu.Lock() + + select { + case <-c.sdkClient.WaitCh(): + // Still disconnected; reconnect now. + default: + c.mu.Unlock() + return nil // Another goroutine already reconnected while we waited for the lock. + } + + logger.Info("Connection lost, reconnecting to Clearnode...") + newClient, err := c.newSDKClient() + if err != nil { + c.mu.Unlock() + return fmt.Errorf("failed to reconnect: %w", err) + } + oldClient := c.sdkClient + c.sdkClient = newClient + c.mu.Unlock() // Release before closing old client to avoid holding lock during I/O. + + if err := oldClient.Close(); err != nil { + logger.Errorf("Error closing stale Clearnode client: %v", err) + } + logger.Info("Successfully reconnected to Clearnode") + return nil +} + +// EnsureOperational validates token support and sufficient balance. +func (c *Client) EnsureOperational() error { + if err := c.validateTokenSupport(c.tokenSymbol); err != nil { + return fmt.Errorf("token validation failed: %w", err) + } + + if err := c.validateFaucetBalance(c.tokenSymbol, c.tipAmount, c.minTransferCount); err != nil { + return fmt.Errorf("balance check failed: %w", err) + } + + return nil +} + +const sdkCallTimeout = 30 * time.Second + +func (c *Client) validateTokenSupport(tokenSymbol string) error { + ctx, cancel := context.WithTimeout(context.Background(), sdkCallTimeout) + defer cancel() + + c.mu.RLock() + cl := c.sdkClient + c.mu.RUnlock() + + assets, err := cl.GetAssets(ctx, nil) + if err != nil { + return fmt.Errorf("failed to fetch supported assets: %w", err) + } + + for _, asset := range assets { + if strings.EqualFold(asset.Symbol, tokenSymbol) { + logger.Debugf("Token '%s' is supported by Clearnode", tokenSymbol) + return nil + } + } + + return fmt.Errorf("token '%s' is not supported by Clearnode", tokenSymbol) +} + +func (c *Client) validateFaucetBalance(tokenSymbol string, tipAmount decimal.Decimal, minTransferCount int) error { + ctx, cancel := context.WithTimeout(context.Background(), sdkCallTimeout) + defer cancel() + + c.mu.RLock() + cl := c.sdkClient + c.mu.RUnlock() + + ownerAddress := cl.GetUserAddress() + + balances, err := cl.GetBalances(ctx, ownerAddress) + if err != nil { + return fmt.Errorf("failed to fetch faucet balance: %w", err) + } + + minRequired := tipAmount.Mul(decimal.NewFromInt(int64(minTransferCount))) + + for _, balance := range balances { + if strings.EqualFold(balance.Asset, tokenSymbol) { + if balance.Balance.LessThan(minRequired) { + return fmt.Errorf("insufficient %s balance: %s (required: %s for %d transfers)", + tokenSymbol, balance.Balance.String(), minRequired.String(), minTransferCount) + } + logger.Infof("✓ Sufficient %s balance: %s", tokenSymbol, balance.Balance.String()) + return nil + } + } + + return fmt.Errorf("insufficient %s balance: 0 (required: %s for %d transfers)", + tokenSymbol, minRequired.String(), minTransferCount) +} + +// Transfer sends tokens to the destination address. +func (c *Client) Transfer(destination, asset string, amount decimal.Decimal) (*TransferResult, error) { + ctx, cancel := context.WithTimeout(context.Background(), sdkCallTimeout) + defer cancel() + + c.mu.RLock() + cl := c.sdkClient + c.mu.RUnlock() + + state, err := cl.Transfer(ctx, destination, asset, amount) + if err != nil { + return nil, fmt.Errorf("transfer failed: %w", err) + } + + result := &TransferResult{ + TxID: state.Transition.TxID, + Amount: state.Transition.Amount.String(), + Asset: state.Asset, + } + + if result.Amount == "" || result.Amount == "0" { + result.Amount = amount.String() + } + if result.Asset == "" { + result.Asset = asset + } + + return result, nil +} + +// Close shuts down the Clearnode connection. +func (c *Client) Close() error { + c.mu.RLock() + defer c.mu.RUnlock() + return c.sdkClient.Close() +} diff --git a/faucet-app/server/internal/clearnode/client_validation_test.go b/faucet-app/server/internal/clearnode/client_validation_test.go new file mode 100644 index 000000000..c95a3176c --- /dev/null +++ b/faucet-app/server/internal/clearnode/client_validation_test.go @@ -0,0 +1,36 @@ +package clearnode + +import ( + "testing" + + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewClientValidation(t *testing.T) { + t.Run("should fail with invalid private key", func(t *testing.T) { + client, err := NewClient("not-a-valid-key", "ws://localhost:8080", "usdc", decimal.NewFromInt(10), 1) + + assert.Nil(t, client) + require.Error(t, err) + }) + + t.Run("should fail with invalid URL", func(t *testing.T) { + validKey := "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + + client, err := NewClient(validKey, "ws://localhost:19999", "usdc", decimal.NewFromInt(10), 1) + + assert.Nil(t, client) + require.Error(t, err) + }) + + t.Run("should handle 0x prefixed key", func(t *testing.T) { + // NewEthereumMsgSigner should handle 0x prefix; if it fails, err is non-nil + key := "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + + // We expect either success (SDK handles prefix) or a signer error, + // but never a panic. + _, _ = NewClient(key, "ws://localhost:19999", "usdc", decimal.NewFromInt(10), 1) + }) +} diff --git a/faucet-app/server/internal/config/config.go b/faucet-app/server/internal/config/config.go new file mode 100644 index 000000000..f823954ef --- /dev/null +++ b/faucet-app/server/internal/config/config.go @@ -0,0 +1,69 @@ +package config + +import ( + "fmt" + "time" + + "github.com/ilyakaznacheev/cleanenv" + "github.com/shopspring/decimal" +) + +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)"` + ClearnodeURL string `env:"CLEARNODE_URL" env-required:"true" env-description:"Clearnode 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)"` + + 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 +} + +func Load() (*Config, error) { + var config Config + + // Try to read from .env file first, then from environment variables + if err := cleanenv.ReadConfig(".env", &config); err != nil { + // If .env doesn't exist, try to read from environment variables only + if err := cleanenv.ReadEnv(&config); err != nil { + return nil, fmt.Errorf("failed to load configuration: %w", err) + } + } + + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("config validation failed: %w", err) + } + + return &config, nil +} + +func (c *Config) Validate() error { + // Parse the decimal amount + 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 + + return nil +} diff --git a/faucet-app/server/internal/logger/logger.go b/faucet-app/server/internal/logger/logger.go new file mode 100644 index 000000000..2aa11c5f5 --- /dev/null +++ b/faucet-app/server/internal/logger/logger.go @@ -0,0 +1,68 @@ +package logger + +import ( + "os" + + "github.com/sirupsen/logrus" +) + +var Log *logrus.Logger + +func Initialize(level string) error { + Log = logrus.New() + + Log.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: "2006-01-02T15:04:05.000Z07:00", + }) + + Log.SetOutput(os.Stdout) + + logLevel, err := logrus.ParseLevel(level) + if err != nil { + return err + } + + Log.SetLevel(logLevel) + + return nil +} + +func Info(args ...interface{}) { + Log.Info(args...) +} + +func Infof(format string, args ...interface{}) { + Log.Infof(format, args...) +} + +func Warn(args ...interface{}) { + Log.Warn(args...) +} + +func Warnf(format string, args ...interface{}) { + Log.Warnf(format, args...) +} + +func Error(args ...interface{}) { + Log.Error(args...) +} + +func Errorf(format string, args ...interface{}) { + Log.Errorf(format, args...) +} + +func Debug(args ...interface{}) { + Log.Debug(args...) +} + +func Debugf(format string, args ...interface{}) { + Log.Debugf(format, args...) +} + +func Fatal(args ...interface{}) { + Log.Fatal(args...) +} + +func Fatalf(format string, args ...interface{}) { + Log.Fatalf(format, args...) +} \ No newline at end of file diff --git a/faucet-app/server/internal/server/ratelimiter.go b/faucet-app/server/internal/server/ratelimiter.go new file mode 100644 index 000000000..d9ac4b1c3 --- /dev/null +++ b/faucet-app/server/internal/server/ratelimiter.go @@ -0,0 +1,73 @@ +package server + +import ( + "sync" + "time" +) + +type rateLimiter struct { + mu sync.Mutex + cooldown time.Duration + seen map[string]time.Time + calls uint64 +} + +func newRateLimiter(cooldown time.Duration) *rateLimiter { + return &rateLimiter{ + cooldown: cooldown, + seen: make(map[string]time.Time), + } +} + +// checkAndRecord atomically checks if key is allowed and, if so, records the attempt. +// Returns true if allowed (cooldown slot consumed), false if on cooldown. +func (r *rateLimiter) checkAndRecord(key string) bool { + now := time.Now() + r.mu.Lock() + defer r.mu.Unlock() + last, exists := r.seen[key] + if exists && now.Sub(last) < r.cooldown { + return false + } + r.seen[key] = now + r.calls++ + r.evictExpiredLocked(now) + return true +} + +// checkAndRecordBoth atomically checks both addr and ip. Only records both if +// both pass — prevents a blocked IP from burning the wallet's cooldown slot. +// Returns (false, "address") or (false, "ip") if either is blocked. +func (r *rateLimiter) checkAndRecordBoth(addr, ip string) (bool, string) { + now := time.Now() + r.mu.Lock() + defer r.mu.Unlock() + + if last, ok := r.seen[addr]; ok && now.Sub(last) < r.cooldown { + return false, "address" + } + if last, ok := r.seen[ip]; ok && now.Sub(last) < r.cooldown { + return false, "ip" + } + + r.seen[addr] = now + if ip != addr { + r.seen[ip] = now + } + r.calls++ + r.evictExpiredLocked(now) + return true, "" +} + +// evictExpiredLocked removes entries that are past the cooldown window. +// Must be called with r.mu held. Runs every 1024 calls to amortise cost. +func (r *rateLimiter) evictExpiredLocked(now time.Time) { + if r.calls%1024 != 0 { + return + } + for k, ts := range r.seen { + if now.Sub(ts) >= r.cooldown { + delete(r.seen, k) + } + } +} diff --git a/faucet-app/server/internal/server/server.go b/faucet-app/server/internal/server/server.go new file mode 100644 index 000000000..eb169ee48 --- /dev/null +++ b/faucet-app/server/internal/server/server.go @@ -0,0 +1,229 @@ +package server + +import ( + "net/http" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/gin-gonic/gin" + "github.com/shopspring/decimal" + + "faucet-server/internal/clearnode" + "faucet-server/internal/config" + "faucet-server/internal/logger" +) + +// ClearnodeClient is the interface the server uses to interact with Clearnode. +type ClearnodeClient interface { + GetOwnerAddress() string + EnsureConnected() error + EnsureOperational() error + Transfer(destination, asset string, amount decimal.Decimal) (*clearnode.TransferResult, error) +} + +// Error message constants +const ( + ErrInvalidRequestFormat = "Invalid request format. Expected JSON with 'userAddress' field." + ErrInvalidAddressFormat = "Invalid address format." + ErrClearnodeConnectionFailed = "Failed to connect to Clearnode." + ErrServiceUnavailable = "Faucet service is currently unavailable." + ErrTransferFailed = "Failed to send tokens." + ErrRateLimitExceeded = "Rate limit exceeded. Please try again later." + MsgTokensSentSuccessfully = "Tokens sent successfully" +) + +type Server struct { + config *config.Config + clearnodeClient ClearnodeClient + router *gin.Engine + rateLimiter *rateLimiter +} + +type FaucetRequest struct { + UserAddress string `json:"userAddress" binding:"required"` +} + +type FaucetResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + TxID string `json:"txId,omitempty"` + Amount string `json:"amount,omitempty"` + Asset string `json:"asset,omitempty"` + Destination string `json:"destination,omitempty"` +} + +type ErrorResponse struct { + Error string `json:"error"` +} + +func NewServer(cfg *config.Config, client ClearnodeClient) *Server { + if cfg.LogLevel == "debug" { + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) + } + + router := gin.New() + // Disable X-Forwarded-For trust so c.ClientIP() uses RemoteAddr. + // Configure with actual LB IP(s) if deployed behind a trusted reverse proxy. + router.SetTrustedProxies(nil) + + // Add middleware + router.Use(gin.Recovery()) + router.Use(requestLogger()) + router.Use(corsMiddleware()) + + server := &Server{ + config: cfg, + clearnodeClient: client, + router: router, + rateLimiter: newRateLimiter(cfg.CooldownPeriodDuration), + } + + server.setupRoutes() + return server +} + +func (s *Server) setupRoutes() { + s.router.POST("/requestTokens", s.requestTokens) + s.router.GET("/info", s.getInfo) +} + +func (s *Server) getInfo(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "service": "Nitrolite Faucet Server", + "version": "1.0.0", + "faucet_address": s.clearnodeClient.GetOwnerAddress(), + "standard_tip_amount": s.config.StandardTipAmountDecimal.String(), + "token_symbol": s.config.TokenSymbol, + "endpoints": []string{"/requestTokens"}, + }) +} + +func (s *Server) requestTokens(c *gin.Context) { + var req FaucetRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.Warnf("Invalid request format: %v", err) + c.JSON(http.StatusBadRequest, ErrorResponse{ + Error: ErrInvalidRequestFormat, + }) + return + } + + // Validate the user address + userAddress := strings.TrimSpace(req.UserAddress) + if !common.IsHexAddress(userAddress) { + logger.Warnf("Invalid address format: %s", userAddress) + c.JSON(http.StatusBadRequest, ErrorResponse{ + Error: ErrInvalidAddressFormat, + }) + return + } + + userAddress = common.HexToAddress(userAddress).Hex() + + // Atomically check-and-record both keys under one lock. This prevents a + // blocked IP from burning the wallet's cooldown slot and eliminates TOCTOU. + // Every accepted request (including ones that later fail) consumes a slot, + // preventing unlimited probing via induced failures. + clientIP := c.ClientIP() + if allowed, blocked := s.rateLimiter.checkAndRecordBoth(userAddress, clientIP); !allowed { + if blocked == "address" { + logger.Warnf("Rate limit exceeded for address %s", userAddress) + } else { + logger.Warnf("Rate limit exceeded for IP %s (address: %s)", clientIP, userAddress) + } + c.JSON(http.StatusTooManyRequests, ErrorResponse{Error: ErrRateLimitExceeded}) + return + } + + logger.Infof("Processing faucet request for address: %s", userAddress) + + // Ensure client is connected + if err := s.clearnodeClient.EnsureConnected(); err != nil { + logger.Errorf("Connection failed for %s: %v", userAddress, err) + c.JSON(http.StatusServiceUnavailable, ErrorResponse{ + Error: ErrClearnodeConnectionFailed, + }) + return + } + + // Ensure client is operational + if err := s.clearnodeClient.EnsureOperational(); err != nil { + logger.Errorf("Service not operational for %s: %v", userAddress, err) + c.JSON(http.StatusServiceUnavailable, ErrorResponse{ + Error: ErrServiceUnavailable, + }) + return + } + + // Perform the transfer + result, err := s.clearnodeClient.Transfer( + userAddress, + s.config.TokenSymbol, + s.config.StandardTipAmountDecimal, + ) + if err != nil { + logger.Errorf("Transfer failed for %s: %v", userAddress, err) + c.JSON(http.StatusInternalServerError, ErrorResponse{ + Error: ErrTransferFailed, + }) + return + } + if result == nil { + logger.Errorf("Transfer returned nil result for %s", userAddress) + c.JSON(http.StatusInternalServerError, ErrorResponse{ + Error: ErrTransferFailed, + }) + return + } + + txID := result.TxID + amount := result.Amount + asset := result.Asset + + logger.Infof("Successfully sent %s %s to %s (txID: %s)", + amount, asset, userAddress, txID) + + c.JSON(http.StatusOK, FaucetResponse{ + Success: true, + Message: MsgTokensSentSuccessfully, + TxID: txID, + Amount: amount, + Asset: asset, + Destination: userAddress, + }) +} + +func (s *Server) Start() error { + addr := ":" + s.config.ServerPort + logger.Infof("Starting HTTP server on port %s", s.config.ServerPort) + return s.router.Run(addr) +} + +// Middleware functions + +func requestLogger() gin.HandlerFunc { + return func(c *gin.Context) { + // Log request + logger.Debugf("%s %s from %s", c.Request.Method, c.Request.URL.Path, c.ClientIP()) + c.Next() + // Log response status + logger.Debugf("%s %s - %d", c.Request.Method, c.Request.URL.Path, c.Writer.Status()) + } +} + +func corsMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + return + } + + c.Next() + } +} diff --git a/faucet-app/server/internal/server/server_test.go b/faucet-app/server/internal/server/server_test.go new file mode 100644 index 000000000..ca469091e --- /dev/null +++ b/faucet-app/server/internal/server/server_test.go @@ -0,0 +1,311 @@ +package server + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "faucet-server/internal/clearnode" + "faucet-server/internal/config" + "faucet-server/internal/logger" +) + +// mockClearnodeClient is a simple in-memory mock implementing ClearnodeClient. +type mockClearnodeClient struct { + ownerAddress string + connErr error + operationalErr error + transferResult *clearnode.TransferResult + transferErr error + capturedDest string + capturedAsset string + capturedAmount decimal.Decimal +} + +func (m *mockClearnodeClient) GetOwnerAddress() string { return m.ownerAddress } +func (m *mockClearnodeClient) EnsureConnected() error { return m.connErr } +func (m *mockClearnodeClient) EnsureOperational() error { return m.operationalErr } +func (m *mockClearnodeClient) Transfer(dest, asset string, amount decimal.Decimal) (*clearnode.TransferResult, error) { + m.capturedDest = dest + m.capturedAsset = asset + m.capturedAmount = amount + return m.transferResult, m.transferErr +} + +func defaultConfig() *config.Config { + return &config.Config{ + ServerPort: "0", + OwnerPrivateKey: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ClearnodeURL: "ws://localhost:0", + TokenSymbol: "usdc", + StandardTipAmount: "10", + StandardTipAmountDecimal: decimal.RequireFromString("10"), + CooldownPeriod: "24h", + CooldownPeriodDuration: 24 * time.Hour, + LogLevel: "debug", + } +} + +func defaultMock() *mockClearnodeClient { + return &mockClearnodeClient{ + ownerAddress: "0x9fc51BEE23Fb53569c46CcF013400f0E19524bd2", + transferResult: &clearnode.TransferResult{ + TxID: "tx-abc123", + Amount: "10", + Asset: "usdc", + }, + } +} + +func TestMain(m *testing.M) { + _ = logger.Initialize("debug") + os.Exit(m.Run()) +} + +func TestRequestTokens_Success(t *testing.T) { + mock := defaultMock() + srv := NewServer(defaultConfig(), mock) + + testAddress := common.HexToAddress("0x742D35CC6634c0532925a3B8c17D18fBe3b78890").Hex() + body, _ := json.Marshal(FaucetRequest{UserAddress: testAddress}) + + req := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + srv.router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var resp FaucetResponse + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.True(t, resp.Success) + assert.Equal(t, MsgTokensSentSuccessfully, resp.Message) + assert.Equal(t, "tx-abc123", resp.TxID) + assert.Equal(t, "10", resp.Amount) + assert.Equal(t, "usdc", resp.Asset) + assert.Equal(t, testAddress, resp.Destination) + + assert.Equal(t, testAddress, mock.capturedDest) + assert.Equal(t, "usdc", mock.capturedAsset) + assert.True(t, decimal.RequireFromString("10").Equal(mock.capturedAmount)) +} + +func TestRequestTokens_InvalidAddress(t *testing.T) { + srv := NewServer(defaultConfig(), defaultMock()) + + body, _ := json.Marshal(FaucetRequest{UserAddress: "not-an-address"}) + req := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + srv.router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + var resp ErrorResponse + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, ErrInvalidAddressFormat, resp.Error) +} + +func TestRequestTokens_MissingField(t *testing.T) { + srv := NewServer(defaultConfig(), defaultMock()) + + body, _ := json.Marshal(map[string]string{"wrongField": "0x1234"}) + req := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + srv.router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + var resp ErrorResponse + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, ErrInvalidRequestFormat, resp.Error) +} + +func TestRequestTokens_ConnectionFailure(t *testing.T) { + mock := defaultMock() + mock.connErr = assert.AnError + srv := NewServer(defaultConfig(), mock) + + body, _ := json.Marshal(FaucetRequest{UserAddress: "0x742d35Cc6634C0532925a3b8c17d18fBE3b78890"}) + req := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + srv.router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusServiceUnavailable, w.Code) + var resp ErrorResponse + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, ErrClearnodeConnectionFailed, resp.Error) +} + +func TestRequestTokens_OperationalFailure(t *testing.T) { + mock := defaultMock() + mock.operationalErr = assert.AnError + srv := NewServer(defaultConfig(), mock) + + body, _ := json.Marshal(FaucetRequest{UserAddress: "0x742d35Cc6634C0532925a3b8c17d18fBE3b78890"}) + req := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + srv.router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusServiceUnavailable, w.Code) + var resp ErrorResponse + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, ErrServiceUnavailable, resp.Error) +} + +func TestRequestTokens_TransferFailure(t *testing.T) { + mock := defaultMock() + mock.transferResult = nil + mock.transferErr = assert.AnError + srv := NewServer(defaultConfig(), mock) + + body, _ := json.Marshal(FaucetRequest{UserAddress: "0x742d35Cc6634C0532925a3b8c17d18fBE3b78890"}) + req := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + srv.router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + var resp ErrorResponse + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, ErrTransferFailed, resp.Error) +} + +func TestRateLimiting(t *testing.T) { + cfg := defaultConfig() + cfg.CooldownPeriodDuration = 24 * time.Hour + + t.Run("second request from same wallet is rejected", func(t *testing.T) { + mock := defaultMock() + srv := NewServer(cfg, mock) + + testAddress := common.HexToAddress("0x742D35CC6634c0532925a3B8c17D18fBe3b78890").Hex() + body, _ := json.Marshal(FaucetRequest{UserAddress: testAddress}) + + // First request — should succeed + req1 := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) + req1.Header.Set("Content-Type", "application/json") + w1 := httptest.NewRecorder() + srv.router.ServeHTTP(w1, req1) + assert.Equal(t, http.StatusOK, w1.Code) + + // Second request same wallet — should be rate limited + req2 := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) + req2.Header.Set("Content-Type", "application/json") + w2 := httptest.NewRecorder() + srv.router.ServeHTTP(w2, req2) + assert.Equal(t, http.StatusTooManyRequests, w2.Code) + + var resp ErrorResponse + require.NoError(t, json.Unmarshal(w2.Body.Bytes(), &resp)) + assert.Equal(t, ErrRateLimitExceeded, resp.Error) + }) + + t.Run("failed transfer consumes rate limit slot", func(t *testing.T) { + mock := defaultMock() + mock.transferResult = nil + mock.transferErr = assert.AnError + srv := NewServer(cfg, mock) + + testAddress := common.HexToAddress("0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF").Hex() + body, _ := json.Marshal(FaucetRequest{UserAddress: testAddress}) + + // First request fails at transfer but still consumes the rate-limit slot. + req1 := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) + req1.Header.Set("Content-Type", "application/json") + w1 := httptest.NewRecorder() + srv.router.ServeHTTP(w1, req1) + assert.Equal(t, http.StatusInternalServerError, w1.Code) + + // Second request is rate-limited because the slot was consumed on the first attempt. + req2 := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) + req2.Header.Set("Content-Type", "application/json") + w2 := httptest.NewRecorder() + srv.router.ServeHTTP(w2, req2) + assert.Equal(t, http.StatusTooManyRequests, w2.Code) + }) + + t.Run("different wallets from different IPs are not rate limited by each other", func(t *testing.T) { + mock := defaultMock() + srv := NewServer(cfg, mock) + + addr1 := common.HexToAddress("0x742D35CC6634c0532925a3B8c17D18fBe3b78890").Hex() + addr2 := common.HexToAddress("0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF").Hex() + + body1, _ := json.Marshal(FaucetRequest{UserAddress: addr1}) + body2, _ := json.Marshal(FaucetRequest{UserAddress: addr2}) + + req1 := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body1)) + req1.Header.Set("Content-Type", "application/json") + req1.RemoteAddr = "10.0.0.1:1234" + w1 := httptest.NewRecorder() + srv.router.ServeHTTP(w1, req1) + assert.Equal(t, http.StatusOK, w1.Code) + + req2 := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body2)) + req2.Header.Set("Content-Type", "application/json") + req2.RemoteAddr = "10.0.0.2:1234" + w2 := httptest.NewRecorder() + srv.router.ServeHTTP(w2, req2) + assert.Equal(t, http.StatusOK, w2.Code) + }) + + t.Run("same IP with different wallet is still rate limited", func(t *testing.T) { + mock := defaultMock() + srv := NewServer(cfg, mock) + + addr1 := common.HexToAddress("0x742D35CC6634c0532925a3B8c17D18fBe3b78890").Hex() + addr2 := common.HexToAddress("0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF").Hex() + + body1, _ := json.Marshal(FaucetRequest{UserAddress: addr1}) + body2, _ := json.Marshal(FaucetRequest{UserAddress: addr2}) + + req1 := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body1)) + req1.Header.Set("Content-Type", "application/json") + w1 := httptest.NewRecorder() + srv.router.ServeHTTP(w1, req1) + assert.Equal(t, http.StatusOK, w1.Code) + + // Different wallet, same IP — should be blocked by IP limit + req2 := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body2)) + req2.Header.Set("Content-Type", "application/json") + w2 := httptest.NewRecorder() + srv.router.ServeHTTP(w2, req2) + assert.Equal(t, http.StatusTooManyRequests, w2.Code) + }) +} + +func TestInfoEndpoint(t *testing.T) { + mock := defaultMock() + srv := NewServer(defaultConfig(), mock) + + req := httptest.NewRequest("GET", "/info", nil) + w := httptest.NewRecorder() + + srv.router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var resp map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, "Nitrolite Faucet Server", resp["service"]) + assert.Equal(t, "1.0.0", resp["version"]) + assert.Equal(t, mock.ownerAddress, resp["faucet_address"]) + assert.Equal(t, "10", resp["standard_tip_amount"]) + assert.Equal(t, "usdc", resp["token_symbol"]) +} diff --git a/faucet-app/server/main.go b/faucet-app/server/main.go new file mode 100644 index 000000000..511afeb4c --- /dev/null +++ b/faucet-app/server/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "faucet-server/internal/clearnode" + "faucet-server/internal/config" + "faucet-server/internal/logger" + "faucet-server/internal/server" +) + +func main() { + cfg, err := config.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to load configuration: %v\n", err) + os.Exit(1) + } + + if err := logger.Initialize(cfg.LogLevel); err != nil { + fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err) + os.Exit(1) + } + + logger.Info("Starting Nitrolite Faucet Server") + logger.Infof("Configuration loaded: Server port=%s, Clearnode URL=%s", + cfg.ServerPort, cfg.ClearnodeURL) + + client, err := clearnode.NewClient(cfg.OwnerPrivateKey, cfg.ClearnodeURL, cfg.TokenSymbol, cfg.StandardTipAmountDecimal, cfg.MinTransferCount) + if err != nil { + logger.Fatalf("Failed to create Clearnode client: %v", err) + } + + if err := client.EnsureOperational(); err != nil { + logger.Fatalf("Operational check failed: %v", err) + } + + logger.Infof("Faucet owner address: %s", client.GetOwnerAddress()) + logger.Info("Successfully connected to Clearnode") + + httpServer := server.NewServer(cfg, client) + + go func() { + if err := httpServer.Start(); err != nil { + logger.Fatalf("Failed to start HTTP server: %v", err) + } + }() + + logger.Info("Faucet server is ready to serve requests") + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + logger.Info("Shutting down server...") + + if err := client.Close(); err != nil { + logger.Errorf("Error closing Clearnode connection: %v", err) + } + + logger.Info("Server shutdown complete") +} From b5f822236d10f514f0ab5ae71b2cb8f8e9557d54 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Thu, 21 May 2026 10:03:58 +0200 Subject: [PATCH 2/5] refactor(faucet-app): rename clearnode to nitronode --- faucet-app/chart/config/sandbox/values.yaml | 2 +- faucet-app/chart/config/uat/values.yaml | 2 +- faucet-app/chart/values.yaml | 8 +++--- faucet-app/server/internal/config/config.go | 2 +- .../{clearnode => nitronode}/client.go | 20 +++++++------- .../client_validation_test.go | 2 +- faucet-app/server/internal/server/server.go | 26 +++++++++---------- .../server/internal/server/server_test.go | 26 +++++++++---------- faucet-app/server/main.go | 14 +++++----- 9 files changed, 51 insertions(+), 51 deletions(-) rename faucet-app/server/internal/{clearnode => nitronode}/client.go (90%) rename faucet-app/server/internal/{clearnode => nitronode}/client_validation_test.go (98%) diff --git a/faucet-app/chart/config/sandbox/values.yaml b/faucet-app/chart/config/sandbox/values.yaml index 8315daee2..825839dfd 100644 --- a/faucet-app/chart/config/sandbox/values.yaml +++ b/faucet-app/chart/config/sandbox/values.yaml @@ -1,7 +1,7 @@ config: args: ["./faucet-server"] logLevel: debug - clearnodeWsUrl: wss://clearnet-sandbox.yellow.com/ws + nitronodeWsUrl: wss://clearnet-sandbox.yellow.com/ws token: symbol: ytest.usd tipAmount: 10 diff --git a/faucet-app/chart/config/uat/values.yaml b/faucet-app/chart/config/uat/values.yaml index dc4badb95..f75388c5c 100644 --- a/faucet-app/chart/config/uat/values.yaml +++ b/faucet-app/chart/config/uat/values.yaml @@ -1,7 +1,7 @@ config: args: ["./faucet-server"] logLevel: debug - clearnodeWsUrl: wss://canarynet.yellow.com/ws + nitronodeWsUrl: wss://canarynet.yellow.com/ws token: symbol: ytest.usd tipAmount: 0.1 diff --git a/faucet-app/chart/values.yaml b/faucet-app/chart/values.yaml index cf0db080e..f5c5e4f37 100644 --- a/faucet-app/chart/values.yaml +++ b/faucet-app/chart/values.yaml @@ -6,10 +6,10 @@ config: args: ["./faucet-server"] # -- Log level (info, debug, warn, error) logLevel: info - # -- Clearnode WebSocket URL - clearnodeWsUrl: "wss://clearnode.example.com/ws" + # -- Nitronode WebSocket URL + nitronodeWsUrl: "wss://nitronode.example.com/ws" token: - # -- Token Symbol inside the Clearnode network + # -- Token Symbol inside the Nitronode network symbol: usdc # -- The amount of tokens to tip per request tipAmount: 10 @@ -98,7 +98,7 @@ networking: # -- TLS cluster issuer tlsClusterIssuer: zerossl-prod # -- External hostname for the gateway - externalHostname: clearnode.example.com + externalHostname: nitronode.example.com gateway: # -- Enable API gateway diff --git a/faucet-app/server/internal/config/config.go b/faucet-app/server/internal/config/config.go index f823954ef..5a6604410 100644 --- a/faucet-app/server/internal/config/config.go +++ b/faucet-app/server/internal/config/config.go @@ -12,7 +12,7 @@ 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)"` - ClearnodeURL string `env:"CLEARNODE_URL" env-required:"true" env-description:"Clearnode WebSocket URL"` + 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"` diff --git a/faucet-app/server/internal/clearnode/client.go b/faucet-app/server/internal/nitronode/client.go similarity index 90% rename from faucet-app/server/internal/clearnode/client.go rename to faucet-app/server/internal/nitronode/client.go index f7e14c558..c154adb2f 100644 --- a/faucet-app/server/internal/clearnode/client.go +++ b/faucet-app/server/internal/nitronode/client.go @@ -1,4 +1,4 @@ -package clearnode +package nitronode import ( "context" @@ -33,7 +33,7 @@ type Client struct { minTransferCount int } -func NewClient(privateKeyHex, clearnodeURL, tokenSymbol string, tipAmount decimal.Decimal, minTransferCount int) (*Client, error) { +func NewClient(privateKeyHex, nitronodeURL, tokenSymbol string, tipAmount decimal.Decimal, minTransferCount int) (*Client, error) { // Parse signers once — raw key hex is used here and not retained on the struct. msgSigner, err := sign.NewEthereumMsgSigner(privateKeyHex) if err != nil { @@ -52,9 +52,9 @@ func NewClient(privateKeyHex, clearnodeURL, tokenSymbol string, tipAmount decima // factory captures already-parsed signers so reconnects don't need the raw key. factory := func() (*sdk.Client, error) { - cl, err := sdk.NewClient(clearnodeURL, stateSigner, txSigner) + cl, err := sdk.NewClient(nitronodeURL, stateSigner, txSigner) if err != nil { - return nil, fmt.Errorf("failed to connect to Clearnode: %w", err) + return nil, fmt.Errorf("failed to connect to Nitronode: %w", err) } return cl, nil } @@ -105,7 +105,7 @@ func (c *Client) EnsureConnected() error { return nil // Another goroutine already reconnected while we waited for the lock. } - logger.Info("Connection lost, reconnecting to Clearnode...") + logger.Info("Connection lost, reconnecting to Nitronode...") newClient, err := c.newSDKClient() if err != nil { c.mu.Unlock() @@ -116,9 +116,9 @@ func (c *Client) EnsureConnected() error { c.mu.Unlock() // Release before closing old client to avoid holding lock during I/O. if err := oldClient.Close(); err != nil { - logger.Errorf("Error closing stale Clearnode client: %v", err) + logger.Errorf("Error closing stale Nitronode client: %v", err) } - logger.Info("Successfully reconnected to Clearnode") + logger.Info("Successfully reconnected to Nitronode") return nil } @@ -152,12 +152,12 @@ func (c *Client) validateTokenSupport(tokenSymbol string) error { for _, asset := range assets { if strings.EqualFold(asset.Symbol, tokenSymbol) { - logger.Debugf("Token '%s' is supported by Clearnode", tokenSymbol) + logger.Debugf("Token '%s' is supported by Nitronode", tokenSymbol) return nil } } - return fmt.Errorf("token '%s' is not supported by Clearnode", tokenSymbol) + return fmt.Errorf("token '%s' is not supported by Nitronode", tokenSymbol) } func (c *Client) validateFaucetBalance(tokenSymbol string, tipAmount decimal.Decimal, minTransferCount int) error { @@ -222,7 +222,7 @@ func (c *Client) Transfer(destination, asset string, amount decimal.Decimal) (*T return result, nil } -// Close shuts down the Clearnode connection. +// Close shuts down the Nitronode connection. func (c *Client) Close() error { c.mu.RLock() defer c.mu.RUnlock() diff --git a/faucet-app/server/internal/clearnode/client_validation_test.go b/faucet-app/server/internal/nitronode/client_validation_test.go similarity index 98% rename from faucet-app/server/internal/clearnode/client_validation_test.go rename to faucet-app/server/internal/nitronode/client_validation_test.go index c95a3176c..70f5a6a7a 100644 --- a/faucet-app/server/internal/clearnode/client_validation_test.go +++ b/faucet-app/server/internal/nitronode/client_validation_test.go @@ -1,4 +1,4 @@ -package clearnode +package nitronode import ( "testing" diff --git a/faucet-app/server/internal/server/server.go b/faucet-app/server/internal/server/server.go index eb169ee48..58c254437 100644 --- a/faucet-app/server/internal/server/server.go +++ b/faucet-app/server/internal/server/server.go @@ -8,24 +8,24 @@ import ( "github.com/gin-gonic/gin" "github.com/shopspring/decimal" - "faucet-server/internal/clearnode" + "faucet-server/internal/nitronode" "faucet-server/internal/config" "faucet-server/internal/logger" ) -// ClearnodeClient is the interface the server uses to interact with Clearnode. -type ClearnodeClient interface { +// NitronodeClient is the interface the server uses to interact with Nitronode. +type NitronodeClient interface { GetOwnerAddress() string EnsureConnected() error EnsureOperational() error - Transfer(destination, asset string, amount decimal.Decimal) (*clearnode.TransferResult, error) + Transfer(destination, asset string, amount decimal.Decimal) (*nitronode.TransferResult, error) } // Error message constants const ( ErrInvalidRequestFormat = "Invalid request format. Expected JSON with 'userAddress' field." ErrInvalidAddressFormat = "Invalid address format." - ErrClearnodeConnectionFailed = "Failed to connect to Clearnode." + ErrNitronodeConnectionFailed = "Failed to connect to Nitronode." ErrServiceUnavailable = "Faucet service is currently unavailable." ErrTransferFailed = "Failed to send tokens." ErrRateLimitExceeded = "Rate limit exceeded. Please try again later." @@ -34,7 +34,7 @@ const ( type Server struct { config *config.Config - clearnodeClient ClearnodeClient + nitronodeClient NitronodeClient router *gin.Engine rateLimiter *rateLimiter } @@ -56,7 +56,7 @@ type ErrorResponse struct { Error string `json:"error"` } -func NewServer(cfg *config.Config, client ClearnodeClient) *Server { +func NewServer(cfg *config.Config, client NitronodeClient) *Server { if cfg.LogLevel == "debug" { gin.SetMode(gin.DebugMode) } else { @@ -75,7 +75,7 @@ func NewServer(cfg *config.Config, client ClearnodeClient) *Server { server := &Server{ config: cfg, - clearnodeClient: client, + nitronodeClient: client, router: router, rateLimiter: newRateLimiter(cfg.CooldownPeriodDuration), } @@ -93,7 +93,7 @@ func (s *Server) getInfo(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "service": "Nitrolite Faucet Server", "version": "1.0.0", - "faucet_address": s.clearnodeClient.GetOwnerAddress(), + "faucet_address": s.nitronodeClient.GetOwnerAddress(), "standard_tip_amount": s.config.StandardTipAmountDecimal.String(), "token_symbol": s.config.TokenSymbol, "endpoints": []string{"/requestTokens"}, @@ -140,16 +140,16 @@ func (s *Server) requestTokens(c *gin.Context) { logger.Infof("Processing faucet request for address: %s", userAddress) // Ensure client is connected - if err := s.clearnodeClient.EnsureConnected(); err != nil { + if err := s.nitronodeClient.EnsureConnected(); err != nil { logger.Errorf("Connection failed for %s: %v", userAddress, err) c.JSON(http.StatusServiceUnavailable, ErrorResponse{ - Error: ErrClearnodeConnectionFailed, + Error: ErrNitronodeConnectionFailed, }) return } // Ensure client is operational - if err := s.clearnodeClient.EnsureOperational(); err != nil { + if err := s.nitronodeClient.EnsureOperational(); err != nil { logger.Errorf("Service not operational for %s: %v", userAddress, err) c.JSON(http.StatusServiceUnavailable, ErrorResponse{ Error: ErrServiceUnavailable, @@ -158,7 +158,7 @@ func (s *Server) requestTokens(c *gin.Context) { } // Perform the transfer - result, err := s.clearnodeClient.Transfer( + result, err := s.nitronodeClient.Transfer( userAddress, s.config.TokenSymbol, s.config.StandardTipAmountDecimal, diff --git a/faucet-app/server/internal/server/server_test.go b/faucet-app/server/internal/server/server_test.go index ca469091e..6e2c314de 100644 --- a/faucet-app/server/internal/server/server_test.go +++ b/faucet-app/server/internal/server/server_test.go @@ -14,27 +14,27 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "faucet-server/internal/clearnode" + "faucet-server/internal/nitronode" "faucet-server/internal/config" "faucet-server/internal/logger" ) -// mockClearnodeClient is a simple in-memory mock implementing ClearnodeClient. -type mockClearnodeClient struct { +// mockNitronodeClient is a simple in-memory mock implementing NitronodeClient. +type mockNitronodeClient struct { ownerAddress string connErr error operationalErr error - transferResult *clearnode.TransferResult + transferResult *nitronode.TransferResult transferErr error capturedDest string capturedAsset string capturedAmount decimal.Decimal } -func (m *mockClearnodeClient) GetOwnerAddress() string { return m.ownerAddress } -func (m *mockClearnodeClient) EnsureConnected() error { return m.connErr } -func (m *mockClearnodeClient) EnsureOperational() error { return m.operationalErr } -func (m *mockClearnodeClient) Transfer(dest, asset string, amount decimal.Decimal) (*clearnode.TransferResult, error) { +func (m *mockNitronodeClient) GetOwnerAddress() string { return m.ownerAddress } +func (m *mockNitronodeClient) EnsureConnected() error { return m.connErr } +func (m *mockNitronodeClient) EnsureOperational() error { return m.operationalErr } +func (m *mockNitronodeClient) Transfer(dest, asset string, amount decimal.Decimal) (*nitronode.TransferResult, error) { m.capturedDest = dest m.capturedAsset = asset m.capturedAmount = amount @@ -45,7 +45,7 @@ func defaultConfig() *config.Config { return &config.Config{ ServerPort: "0", OwnerPrivateKey: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - ClearnodeURL: "ws://localhost:0", + NitronodeURL: "ws://localhost:0", TokenSymbol: "usdc", StandardTipAmount: "10", StandardTipAmountDecimal: decimal.RequireFromString("10"), @@ -55,10 +55,10 @@ func defaultConfig() *config.Config { } } -func defaultMock() *mockClearnodeClient { - return &mockClearnodeClient{ +func defaultMock() *mockNitronodeClient { + return &mockNitronodeClient{ ownerAddress: "0x9fc51BEE23Fb53569c46CcF013400f0E19524bd2", - transferResult: &clearnode.TransferResult{ + transferResult: &nitronode.TransferResult{ TxID: "tx-abc123", Amount: "10", Asset: "usdc", @@ -147,7 +147,7 @@ func TestRequestTokens_ConnectionFailure(t *testing.T) { assert.Equal(t, http.StatusServiceUnavailable, w.Code) var resp ErrorResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) - assert.Equal(t, ErrClearnodeConnectionFailed, resp.Error) + assert.Equal(t, ErrNitronodeConnectionFailed, resp.Error) } func TestRequestTokens_OperationalFailure(t *testing.T) { diff --git a/faucet-app/server/main.go b/faucet-app/server/main.go index 511afeb4c..3cfc229a1 100644 --- a/faucet-app/server/main.go +++ b/faucet-app/server/main.go @@ -6,7 +6,7 @@ import ( "os/signal" "syscall" - "faucet-server/internal/clearnode" + "faucet-server/internal/nitronode" "faucet-server/internal/config" "faucet-server/internal/logger" "faucet-server/internal/server" @@ -25,12 +25,12 @@ func main() { } logger.Info("Starting Nitrolite Faucet Server") - logger.Infof("Configuration loaded: Server port=%s, Clearnode URL=%s", - cfg.ServerPort, cfg.ClearnodeURL) + logger.Infof("Configuration loaded: Server port=%s, Nitronode URL=%s", + cfg.ServerPort, cfg.NitronodeURL) - client, err := clearnode.NewClient(cfg.OwnerPrivateKey, cfg.ClearnodeURL, cfg.TokenSymbol, cfg.StandardTipAmountDecimal, cfg.MinTransferCount) + client, err := nitronode.NewClient(cfg.OwnerPrivateKey, cfg.NitronodeURL, cfg.TokenSymbol, cfg.StandardTipAmountDecimal, cfg.MinTransferCount) if err != nil { - logger.Fatalf("Failed to create Clearnode client: %v", err) + logger.Fatalf("Failed to create Nitronode client: %v", err) } if err := client.EnsureOperational(); err != nil { @@ -38,7 +38,7 @@ func main() { } logger.Infof("Faucet owner address: %s", client.GetOwnerAddress()) - logger.Info("Successfully connected to Clearnode") + logger.Info("Successfully connected to Nitronode") httpServer := server.NewServer(cfg, client) @@ -57,7 +57,7 @@ func main() { logger.Info("Shutting down server...") if err := client.Close(); err != nil { - logger.Errorf("Error closing Clearnode connection: %v", err) + logger.Errorf("Error closing Nitronode connection: %v", err) } logger.Info("Server shutdown complete") From 06dcb25ba0479067e41c7c636d60fb416a404bc5 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Thu, 21 May 2026 10:39:29 +0200 Subject: [PATCH 3/5] fix(faucet-app): harden nitronode client --- .../server/internal/nitronode/client.go | 115 +++++++++++++----- 1 file changed, 87 insertions(+), 28 deletions(-) diff --git a/faucet-app/server/internal/nitronode/client.go b/faucet-app/server/internal/nitronode/client.go index c154adb2f..1d283722b 100644 --- a/faucet-app/server/internal/nitronode/client.go +++ b/faucet-app/server/internal/nitronode/client.go @@ -15,6 +15,14 @@ import ( "faucet-server/internal/logger" ) +const ( + sdkCallTimeout = 30 * time.Second + reconnectInitDelay = 300 * time.Millisecond + reconnectMaxDelay = 2 * time.Second + reconnectAttempts = 3 + pingTimeout = 3 * time.Second +) + // TransferResult holds the result of a token transfer. type TransferResult struct { TxID string @@ -28,9 +36,11 @@ type Client struct { sdkClient *sdk.Client newSDKClient func() (*sdk.Client, error) // captures parsed signers; no raw key hex stored + ownerAddress string tokenSymbol string tipAmount decimal.Decimal minTransferCount int + tokenSupported bool // cached after first successful GetAssets; reset on reconnect } func NewClient(privateKeyHex, nitronodeURL, tokenSymbol string, tipAmount decimal.Decimal, minTransferCount int) (*Client, error) { @@ -52,7 +62,12 @@ func NewClient(privateKeyHex, nitronodeURL, tokenSymbol string, tipAmount decima // factory captures already-parsed signers so reconnects don't need the raw key. factory := func() (*sdk.Client, error) { - cl, err := sdk.NewClient(nitronodeURL, stateSigner, txSigner) + cl, err := sdk.NewClient(nitronodeURL, stateSigner, txSigner, + sdk.WithApplicationID("faucet"), + sdk.WithErrorHandler(func(err error) { + logger.Errorf("Nitronode connection error: %v", err) + }), + ) if err != nil { return nil, fmt.Errorf("failed to connect to Nitronode: %w", err) } @@ -67,6 +82,7 @@ func NewClient(privateKeyHex, nitronodeURL, tokenSymbol string, tipAmount decima return &Client{ sdkClient: sdkClient, newSDKClient: factory, + ownerAddress: sdkClient.GetUserAddress(), // immutable: all factory clients share the same signer tokenSymbol: tokenSymbol, tipAmount: tipAmount, minTransferCount: minTransferCount, @@ -74,13 +90,12 @@ func NewClient(privateKeyHex, nitronodeURL, tokenSymbol string, tipAmount decima } // GetOwnerAddress returns the faucet owner's Ethereum address. +// Derived from the immutable raw signer; cached at construction so no lock is needed. func (c *Client) GetOwnerAddress() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.sdkClient.GetUserAddress() + return c.ownerAddress } -// EnsureConnected checks the connection and reconnects if necessary. +// EnsureConnected checks the connection and reconnects with exponential backoff if necessary. func (c *Client) EnsureConnected() error { // Fast path: read WaitCh under read lock. c.mu.RLock() @@ -96,30 +111,62 @@ func (c *Client) EnsureConnected() error { // Slow path: write lock with double-check to prevent thundering-herd reconnects. c.mu.Lock() + defer c.mu.Unlock() select { case <-c.sdkClient.WaitCh(): - // Still disconnected; reconnect now. + // Still disconnected; proceed with reconnect. default: - c.mu.Unlock() return nil // Another goroutine already reconnected while we waited for the lock. } - logger.Info("Connection lost, reconnecting to Nitronode...") - newClient, err := c.newSDKClient() - if err != nil { - c.mu.Unlock() - return fmt.Errorf("failed to reconnect: %w", err) - } - oldClient := c.sdkClient - c.sdkClient = newClient - c.mu.Unlock() // Release before closing old client to avoid holding lock during I/O. + return c.reconnectLocked() +} + +// reconnectLocked tries to establish a new SDK connection with exponential backoff. +// Must be called with c.mu write lock held. +func (c *Client) reconnectLocked() error { + delay := reconnectInitDelay + var lastErr error + + for attempt := 1; attempt <= reconnectAttempts; attempt++ { + logger.Infof("Reconnecting to Nitronode (attempt %d/%d)...", attempt, reconnectAttempts) - if err := oldClient.Close(); err != nil { - logger.Errorf("Error closing stale Nitronode client: %v", err) + newClient, err := c.newSDKClient() + if err != nil { + lastErr = err + logger.Warnf("Reconnect attempt %d/%d failed: %v", attempt, reconnectAttempts, err) + } else { + // Ping to confirm the new connection is alive before accepting it. + ctx, cancel := context.WithTimeout(context.Background(), pingTimeout) + pingErr := newClient.Ping(ctx) + cancel() + + if pingErr == nil { + oldClient := c.sdkClient + c.sdkClient = newClient + c.tokenSupported = false // re-validate token on the new connection + + // doClose is a non-blocking channel close, safe to call under the lock. + if err := oldClient.Close(); err != nil { + logger.Errorf("Error closing stale Nitronode client: %v", err) + } + logger.Infof("Successfully reconnected to Nitronode on attempt %d", attempt) + return nil + } + + lastErr = fmt.Errorf("ping failed: %w", pingErr) + logger.Warnf("Reconnect attempt %d/%d ping failed: %v", attempt, reconnectAttempts, pingErr) + _ = newClient.Close() + } + + if attempt < reconnectAttempts { + time.Sleep(delay) + delay = min(delay*2, reconnectMaxDelay) + } } - logger.Info("Successfully reconnected to Nitronode") - return nil + + return fmt.Errorf("failed to reconnect after %d attempts: %w", reconnectAttempts, lastErr) } // EnsureOperational validates token support and sufficient balance. @@ -135,16 +182,20 @@ func (c *Client) EnsureOperational() error { return nil } -const sdkCallTimeout = 30 * time.Second - func (c *Client) validateTokenSupport(tokenSymbol string) error { - ctx, cancel := context.WithTimeout(context.Background(), sdkCallTimeout) - defer cancel() - + // Fast path: token already confirmed on this connection. c.mu.RLock() + cached := c.tokenSupported cl := c.sdkClient c.mu.RUnlock() + if cached { + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), sdkCallTimeout) + defer cancel() + assets, err := cl.GetAssets(ctx, nil) if err != nil { return fmt.Errorf("failed to fetch supported assets: %w", err) @@ -153,6 +204,9 @@ func (c *Client) validateTokenSupport(tokenSymbol string) error { for _, asset := range assets { if strings.EqualFold(asset.Symbol, tokenSymbol) { logger.Debugf("Token '%s' is supported by Nitronode", tokenSymbol) + c.mu.Lock() + c.tokenSupported = true + c.mu.Unlock() return nil } } @@ -184,6 +238,10 @@ func (c *Client) validateFaucetBalance(tokenSymbol string, tipAmount decimal.Dec tokenSymbol, balance.Balance.String(), minRequired.String(), minTransferCount) } logger.Infof("✓ Sufficient %s balance: %s", tokenSymbol, balance.Balance.String()) + if balance.Enforced.IsPositive() && balance.Enforced.LessThan(balance.Balance) { + logger.Warnf("⚠ %s enforced balance (%s) is below channel balance (%s); consider checkpointing", + tokenSymbol, balance.Enforced.String(), balance.Balance.String()) + } return nil } } @@ -212,7 +270,7 @@ func (c *Client) Transfer(destination, asset string, amount decimal.Decimal) (*T Asset: state.Asset, } - if result.Amount == "" || result.Amount == "0" { + if result.Amount == "" { result.Amount = amount.String() } if result.Asset == "" { @@ -223,8 +281,9 @@ func (c *Client) Transfer(destination, asset string, amount decimal.Decimal) (*T } // Close shuts down the Nitronode connection. +// Uses write lock to serialize with reconnect and prevent closing a freshly installed client. func (c *Client) Close() error { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() return c.sdkClient.Close() } From f0af8f293f0a2275810f28704a40a1aba7945f92 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Thu, 21 May 2026 17:33:42 +0200 Subject: [PATCH 4/5] fix(faucet-app): address review feedback --- faucet-app/.github/CODEOWNERS | 4 - faucet-app/.github/dependabot.yml | 20 -- faucet-app/.github/workflows/main-pr.yml | 55 ---- faucet-app/.github/workflows/main-push.yml | 186 ----------- faucet-app/.github/workflows/stable-tag.yml | 134 -------- faucet-app/LICENSE | 21 -- faucet-app/chart/.helmignore | 22 -- faucet-app/chart/Chart.yaml | 5 - faucet-app/chart/README.md | 142 -------- faucet-app/chart/README.md.gotmpl | 92 ------ faucet-app/chart/config/sandbox/secrets.yaml | 4 - faucet-app/chart/config/sandbox/values.yaml | 56 ---- faucet-app/chart/config/uat/secrets.yaml | 4 - faucet-app/chart/config/uat/values.yaml | 56 ---- faucet-app/chart/templates/deployment.yaml | 52 --- faucet-app/chart/templates/gateway.yaml | 31 -- .../chart/templates/helpers/_common.tpl | 116 ------- .../chart/templates/helpers/_component.tpl | 78 ----- faucet-app/chart/templates/helpers/_hpa.tpl | 24 -- .../chart/templates/helpers/_ingress.tpl | 67 ---- .../chart/templates/http-route-redirect.yaml | 25 -- faucet-app/chart/templates/http-route.yaml | 27 -- faucet-app/chart/templates/ingress.yaml | 17 - faucet-app/chart/templates/podmonitoring.yaml | 16 - faucet-app/chart/templates/service.yaml | 20 -- faucet-app/chart/values.yaml | 137 -------- faucet-app/scripts/auto_tag.sh | 22 -- faucet-app/server/.env.example | 18 +- faucet-app/server/README.md | 189 ++++------- faucet-app/server/go.mod | 81 ----- faucet-app/server/go.sum | 309 ------------------ faucet-app/server/internal/config/config.go | 38 ++- faucet-app/server/internal/logger/logger.go | 67 ++-- .../server/internal/nitronode/client.go | 78 +++-- .../nitronode/client_validation_test.go | 11 +- .../server/internal/server/ratelimiter.go | 9 +- faucet-app/server/internal/server/server.go | 17 +- .../server/internal/server/server_test.go | 34 +- faucet-app/server/main.go | 19 +- go.mod | 32 +- go.sum | 62 +++- 41 files changed, 325 insertions(+), 2072 deletions(-) delete mode 100644 faucet-app/.github/CODEOWNERS delete mode 100644 faucet-app/.github/dependabot.yml delete mode 100644 faucet-app/.github/workflows/main-pr.yml delete mode 100644 faucet-app/.github/workflows/main-push.yml delete mode 100644 faucet-app/.github/workflows/stable-tag.yml delete mode 100644 faucet-app/LICENSE delete mode 100644 faucet-app/chart/.helmignore delete mode 100644 faucet-app/chart/Chart.yaml delete mode 100644 faucet-app/chart/README.md delete mode 100644 faucet-app/chart/README.md.gotmpl delete mode 100644 faucet-app/chart/config/sandbox/secrets.yaml delete mode 100644 faucet-app/chart/config/sandbox/values.yaml delete mode 100644 faucet-app/chart/config/uat/secrets.yaml delete mode 100644 faucet-app/chart/config/uat/values.yaml delete mode 100644 faucet-app/chart/templates/deployment.yaml delete mode 100644 faucet-app/chart/templates/gateway.yaml delete mode 100644 faucet-app/chart/templates/helpers/_common.tpl delete mode 100644 faucet-app/chart/templates/helpers/_component.tpl delete mode 100644 faucet-app/chart/templates/helpers/_hpa.tpl delete mode 100644 faucet-app/chart/templates/helpers/_ingress.tpl delete mode 100644 faucet-app/chart/templates/http-route-redirect.yaml delete mode 100644 faucet-app/chart/templates/http-route.yaml delete mode 100644 faucet-app/chart/templates/ingress.yaml delete mode 100644 faucet-app/chart/templates/podmonitoring.yaml delete mode 100644 faucet-app/chart/templates/service.yaml delete mode 100644 faucet-app/chart/values.yaml delete mode 100755 faucet-app/scripts/auto_tag.sh delete mode 100644 faucet-app/server/go.mod delete mode 100644 faucet-app/server/go.sum diff --git a/faucet-app/.github/CODEOWNERS b/faucet-app/.github/CODEOWNERS deleted file mode 100644 index 4e19d7391..000000000 --- a/faucet-app/.github/CODEOWNERS +++ /dev/null @@ -1,4 +0,0 @@ -# CODEOWNERS: https://help.github.com/articles/about-codeowners/ - -# Yellow Network - Research and Development -* @erc7824/yellow-network diff --git a/faucet-app/.github/dependabot.yml b/faucet-app/.github/dependabot.yml deleted file mode 100644 index d5be8b1cc..000000000 --- a/faucet-app/.github/dependabot.yml +++ /dev/null @@ -1,20 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file - -version: 2 -updates: - - package-ecosystem: "gomod" # See documentation for possible values - directory: "/server/" # Location of package manifests - schedule: - interval: "daily" - commit-message: - prefix: "chore(gomod)" - labels: - - "dependencies" - - "gomod" - groups: - gomod-dependencies: - patterns: - - "*" diff --git a/faucet-app/.github/workflows/main-pr.yml b/faucet-app/.github/workflows/main-pr.yml deleted file mode 100644 index 2480d8796..000000000 --- a/faucet-app/.github/workflows/main-pr.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: PR on main branch - -on: - pull_request: - branches: [ main ] - -jobs: - test: - name: Test - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: 'server/go.mod' - cache: true - cache-dependency-path: 'server/go.sum' - - - name: Test - run: go test -v ./... - working-directory: server - - build-and-publish: - name: Build and Publish - needs: test - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - uses: actions/checkout@v4 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract short SHA - id: sha - run: echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT - - - name: Build and push Docker image - uses: docker/build-push-action@v4 - with: - context: server - push: true - tags: | - ghcr.io/${{ github.repository }}/server:${{ steps.sha.outputs.short_sha }} diff --git a/faucet-app/.github/workflows/main-push.yml b/faucet-app/.github/workflows/main-push.yml deleted file mode 100644 index fe781c947..000000000 --- a/faucet-app/.github/workflows/main-push.yml +++ /dev/null @@ -1,186 +0,0 @@ -name: Push on main branch - -on: - push: - branches: [ main ] - -jobs: - test: - name: Test - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: 'server/go.mod' - cache: true - cache-dependency-path: 'server/go.sum' - - - name: Test - run: go test -v ./... - working-directory: server - - auto-tag: - name: Auto-Tag - needs: test - runs-on: ubuntu-latest - permissions: - contents: write - outputs: - image-tag: ${{ steps.tagger.outputs.new_tag }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Run auto tag script - run: | - git config user.name "GitHub Actions" - git config user.email "github-actions@github.com" - - ./scripts/auto_tag.sh - - - name: Get the new tag without 'v' prefix - id: tagger - run: | - NEW_TAG=$(git describe --tags --abbrev=0) - NEW_TAG_WITHOUT_V=${NEW_TAG#v} - echo "new_tag=$NEW_TAG_WITHOUT_V" >> $GITHUB_OUTPUT - - build-and-publish: - name: Build and Publish - needs: auto-tag - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - outputs: - image-tag: ${{ steps.tagger.outputs.image_tag }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Save image tag - id: tagger - run: | - echo "image_tag=${{ needs.auto-tag.outputs.image-tag }}" >> $GITHUB_OUTPUT - - - name: Build and push Docker image - uses: docker/build-push-action@v4 - with: - context: server - push: true - tags: | - ghcr.io/${{ github.repository }}/server:${{ needs.auto-tag.outputs.image-tag }} - ghcr.io/${{ github.repository }}/server:latest-rc - cache-from: type=gha - cache-to: type=gha,mode=max - - deploy: - name: Deploy UAT - needs: build-and-publish - runs-on: ubuntu-latest - permissions: - contents: read - env: - PROJECT_ID: ${{ secrets.GKE_PROJECT }} - GKE_CLUSTER_NAME: uat - GKE_CLUSTER_ZONE: europe-central2 - GKE_BASTION_NAME: gke-uat-bastion - GKE_BASTION_ZONE: europe-central2-a - HELM_NAMESPACE: clearnet-uat - HELM_RELEASE: faucet-app - HELM_CONFIG_ENV: uat - KUBECONFIG: kubeconfig.conf - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Authenticate to Google Cloud - uses: google-github-actions/auth@v2 - with: - credentials_json: '${{ secrets.GKE_UAT_SA_KEY }}' - - - name: Set up Google Cloud SDK - uses: google-github-actions/setup-gcloud@v2 - with: - project_id: ${{ secrets.GKE_PROJECT }} - install_components: gke-gcloud-auth-plugin - - - name: Get GKE credentials - run: |- - gcloud container clusters get-credentials ${GKE_CLUSTER_NAME} \ - --project ${PROJECT_ID} \ - --zone ${GKE_CLUSTER_ZONE} \ - --internal-ip - - - name: Port-forward to GKE Bastion - run: |- - gcloud compute ssh ${GKE_BASTION_NAME} \ - --tunnel-through-iap \ - --project ${PROJECT_ID} \ - --zone ${GKE_BASTION_ZONE} \ - --ssh-key-expiration=600s \ - -- -NL8888:127.0.0.1:8888 & - sleep 5 - - - name: Install Helm - uses: azure/setup-helm@v4 - - - name: Release - env: - HTTPS_PROXY: http://localhost:8888 - run: |- - helm upgrade -i ${HELM_RELEASE} chart \ - -n ${HELM_NAMESPACE} \ - --reuse-values \ - -f chart/config/${HELM_CONFIG_ENV}/values.yaml \ - --set image.tag="${{ needs.build-and-publish.outputs.image-tag }}" - - notify-slack: - name: Notify Slack - runs-on: ubuntu-latest - needs: [deploy] - if: always() - - steps: - - name: Slack Notification - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} - SLACK_CHANNEL: deployments - SLACK_USERNAME: CI/CD Bot - SLACK_COLOR: ${{ contains(needs.*.result, 'failure') && 'failure' || contains(needs.*.result, 'cancelled') && 'cancelled' || 'success' }} - SLACK_ICON_EMOJI: ${{ contains(needs.*.result, 'failure') && ':x:' || contains(needs.*.result, 'cancelled') && ':warning:' || ':white_check_mark:' }} - SLACK_TITLE: '🚰 🛫 Faucet App Release Candidate 🛬 🚰' - SLACK_MESSAGE_ON_SUCCESS: | - ✅ RC build and deployment completed successfully! - ${{github.event.head_commit.message}} - SLACK_MESSAGE_ON_FAILURE: | - ❌ RC build or deployment failed! - ${{github.event.head_commit.message}} - SLACK_MESSAGE_ON_CANCEL: | - ⚠️ RC build or deployment was cancelled! - ${{github.event.head_commit.message}} - SLACK_FOOTER: 'Faucet App CI/CD Pipeline' diff --git a/faucet-app/.github/workflows/stable-tag.yml b/faucet-app/.github/workflows/stable-tag.yml deleted file mode 100644 index 45ee6ce67..000000000 --- a/faucet-app/.github/workflows/stable-tag.yml +++ /dev/null @@ -1,134 +0,0 @@ -name: Stable Tag - -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - -jobs: - build-and-publish: - name: Build and Publish - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - outputs: - image-tag: ${{ steps.tagger.outputs.tag }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract tag name without 'v' prefix - id: tagger - run: echo "tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - - - name: Build and push Docker image - uses: docker/build-push-action@v4 - with: - context: server - push: true - tags: | - ghcr.io/${{ github.repository }}/server:${{ steps.tagger.outputs.tag }} - ghcr.io/${{ github.repository }}/server:latest - cache-from: type=gha - cache-to: type=gha,mode=max - - deploy: - name: Deploy Sandbox - needs: build-and-publish - runs-on: ubuntu-latest - env: - PROJECT_ID: ${{ secrets.GKE_PROJECT }} - GKE_CLUSTER_NAME: uat - GKE_CLUSTER_ZONE: europe-central2 - GKE_BASTION_NAME: gke-uat-bastion - GKE_BASTION_ZONE: europe-central2-a - HELM_NAMESPACE: clearnet-sandbox - HELM_RELEASE: faucet-app - HELM_CONFIG_ENV: sandbox - KUBECONFIG: kubeconfig.conf - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Authenticate to Google Cloud - uses: google-github-actions/auth@v2 - with: - credentials_json: '${{ secrets.GKE_SANDBOX_SA_KEY }}' - - - name: Set up Google Cloud SDK - uses: google-github-actions/setup-gcloud@v2 - with: - project_id: ${{ secrets.GKE_PROJECT }} - install_components: gke-gcloud-auth-plugin - - - name: Get GKE credentials - run: |- - gcloud container clusters get-credentials ${GKE_CLUSTER_NAME} \ - --project ${PROJECT_ID} \ - --zone ${GKE_CLUSTER_ZONE} \ - --internal-ip - - - name: Port-forward to GKE Bastion - run: |- - gcloud compute ssh ${GKE_BASTION_NAME} \ - --tunnel-through-iap \ - --project ${PROJECT_ID} \ - --zone ${GKE_BASTION_ZONE} \ - --ssh-key-expiration=600s \ - -- -NL8888:127.0.0.1:8888 & - sleep 5 - - - name: Install Helm - uses: azure/setup-helm@v4 - - - name: Release - env: - HTTPS_PROXY: http://localhost:8888 - run: |- - helm upgrade -i ${HELM_RELEASE} chart \ - -n ${HELM_NAMESPACE} \ - --reuse-values \ - -f chart/config/${HELM_CONFIG_ENV}/values.yaml \ - --set image.tag="${{ needs.build-and-publish.outputs.image-tag }}" - - notify-slack: - name: Notify Slack - runs-on: ubuntu-latest - needs: [build-and-publish, deploy] - if: always() - - steps: - - name: Slack Notification - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} - SLACK_CHANNEL: deployments - SLACK_USERNAME: CI/CD Bot - SLACK_COLOR: ${{ contains(needs.*.result, 'failure') && 'failure' || contains(needs.*.result, 'cancelled') && 'cancelled' || 'success' }} - SLACK_ICON_EMOJI: ${{ contains(needs.*.result, 'failure') && ':x:' || contains(needs.*.result, 'cancelled') && ':warning:' || ':white_check_mark:' }} - SLACK_TITLE: '🚰 🚀 Faucet App Stable Release 🚀 🚰' - SLACK_MESSAGE_ON_SUCCESS: | - ✅ Stable build and deployment completed successfully! - ${{github.event.head_commit.message}} - SLACK_MESSAGE_ON_FAILURE: | - ❌ Stable build or deployment failed! - ${{github.event.head_commit.message}} - SLACK_MESSAGE_ON_CANCEL: | - ⚠️ Stable build or deployment was cancelled! - ${{github.event.head_commit.message}} - SLACK_FOOTER: 'Faucet App CI/CD Pipeline' diff --git a/faucet-app/LICENSE b/faucet-app/LICENSE deleted file mode 100644 index 91b2101b4..000000000 --- a/faucet-app/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 ERC-7824 Chain Agnostic State Channel Protocol - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/faucet-app/chart/.helmignore b/faucet-app/chart/.helmignore deleted file mode 100644 index 063045509..000000000 --- a/faucet-app/chart/.helmignore +++ /dev/null @@ -1,22 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj -config/ diff --git a/faucet-app/chart/Chart.yaml b/faucet-app/chart/Chart.yaml deleted file mode 100644 index c58c1e2a7..000000000 --- a/faucet-app/chart/Chart.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -apiVersion: v2 -description: Faucet App Helm chart -name: faucet-app -version: 1.0.0 diff --git a/faucet-app/chart/README.md b/faucet-app/chart/README.md deleted file mode 100644 index 5e020cb6e..000000000 --- a/faucet-app/chart/README.md +++ /dev/null @@ -1,142 +0,0 @@ -# faucet-app - -![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) - -Faucet App Helm chart - -## Prerequisites - -- Kubernetes 1.24+ -- Helm 3.0+ -- For TLS: cert-manager installed in the cluster -- For Secrets Management (optional): - - [helm-secrets](https://github.com/jkroepke/helm-secrets/wiki) plugin: `helm plugin install https://github.com/jkroepke/helm-secrets --version v4.6.4` - - [vals](https://github.com/helmfile/vals): `go install github.com/helmfile/vals/cmd/vals@v0.41.0` - -## Installing the Chart - -To install the chart with the release name `my-release`: -```bash -helm install my-release git+https://github.com/erc7824/clearnode@chart?ref=main -``` - -The command deploys Faucet App on the Kubernetes cluster with default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation. - -## Uninstalling the Chart - -To uninstall/delete the `my-release` deployment: -```bash -helm delete my-release -``` - -## Parameters - -| Key | Type | Default | Description | -|-----|------|---------|-------------| -| affinity | object | `{}` | Affinity settings | -| autoscaling.enabled | bool | `false` | Enable autoscaling | -| autoscaling.maxReplicas | int | `100` | Maximum number of replicas | -| autoscaling.minReplicas | int | `2` | Minimum number of replicas | -| autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization | -| autoscaling.targetMemoryUtilizationPercentage | int | `80` | Target memory utilization | -| config.args | list | `["./faucet-server"]` | List of arguments to pass to the container | -| config.clearnodeWsUrl | string | `"wss://clearnode.example.com/ws"` | Clearnode WebSocket URL | -| config.envSecret | string | `""` | Name of the secret containing environment variables | -| config.extraEnvs | object | `{}` | Additional environment variables as key-value pairs | -| config.logLevel | string | `"info"` | Log level (info, debug, warn, error) | -| config.minTransferCount | int | `5` | Minimum number of transfers the server should have a balance for to operate | -| config.secretEnvs | object | `{}` | Additional environment variables to be stored in a secret | -| config.token.symbol | string | `"usdc"` | Token Symbol inside the Clearnode network | -| config.token.tipAmount | int | `10` | The amount of tokens to tip per request | -| extraLabels | object | `{}` | Additional labels to add to all resources | -| fullnameOverride | string | `""` | Override the full name | -| image.repository | string | `"ghcr.io/erc7824/faucet-app/server"` | Docker image repository | -| image.tag | string | `"0.0.1"` | Docker image tag | -| imagePullSecret | string | `""` | Image pull secret name | -| metrics.enabled | bool | `true` | Enable Prometheus metrics | -| metrics.endpoint | string | `"/metrics"` | Metrics endpoint path | -| metrics.podmonitoring.enabled | bool | `false` | Enable PodMonitoring for Managed Prometheus | -| metrics.port | int | `4242` | Metrics port | -| metrics.scrapeInterval | string | `"30s"` | Metrics scrape interval | -| networking.externalHostname | string | `"clearnode.example.com"` | External hostname for the gateway | -| networking.gateway.className | string | `"envoy-gateway"` | Gateway class name | -| networking.gateway.enabled | bool | `true` | Enable API gateway | -| networking.gateway.ipAddressName | string | `""` | GKE static IP address name (GKE only) | -| networking.ingress.annotations | object | `{}` | Ingress annotations | -| networking.ingress.className | string | `"nginx"` | Ingress class name | -| networking.ingress.enabled | bool | `false` | Enable ingress | -| networking.ingress.grpc | bool | `false` | Enable GRPC for ingress | -| networking.ingress.tls.enabled | bool | `false` | Enable TLS for ingress | -| networking.tlsClusterIssuer | string | `"zerossl-prod"` | TLS cluster issuer | -| nodeSelector | object | `{}` | Node selector | -| probes.liveness.enabled | bool | `false` | Enable liveness probe | -| probes.liveness.type | string | `"tcp"` | Liveness probe type (http, tcp) | -| probes.readiness.enabled | bool | `false` | Enable readiness probe | -| probes.readiness.type | string | `"tcp"` | Readiness probe type (http, tcp) | -| replicaCount | int | `1` | Number of replicas | -| resources.limits | object | `{}` | Resource limits | -| resources.requests | object | `{}` | Resource requests | -| service.http.enabled | bool | `true` | Enable HTTP service | -| service.http.path | string | `"/"` | HTTP service path | -| service.http.port | int | `8000` | HTTP service port | -| serviceAccount | string | `""` | Service account name | -| tolerations | list | `[]` | Tolerations | - -## Gateway Configuration - -By default, the chart creates an API Gateway and configures it to use TLS via cert-manager. To use this feature: - -1. Create a cert-manager ClusterIssuer -2. Configure `networking.tlsClusterIssuer` with the issuer name -3. Set `networking.externalHostname` to your domain name - -> **Warning**: The Gateway currently does not support configurations with a static IP address. Ensure that your setup uses a dynamic DNS or hostname for proper functionality. Alternatively, you can configure an ingress resource to use a static IP address if required. - -## Managing Secrets - -For managing sensitive values like API keys and credentials, you can use `helm-secrets` with `vals`: - -1. Set up the required environment variable: - ```bash - export HELM_SECRETS_BACKEND=vals - ``` - -2. Create a values file with your secrets (e.g., `secrets.yaml`) and refer to secrets using the vals syntax: - ```yaml - apiKey: ref+awssecrets://my-secret/api-key - database: - password: ref+vault://secret/data/database?key=password - ``` - -3. When deploying or upgrading, reference your secrets file with the `secrets://` prefix: - ```bash - helm upgrade --install my-release git+https://github.com/erc7824/clearnode@chart?ref=main \ - -f values.yaml \ - -f secrets://secrets.yaml - ``` - -The vals tool supports [multiple backends](https://github.com/helmfile/vals/tree/main?tab=readme-ov-file#supported-backends) including: -- AWS Secrets Manager and SSM Parameter Store -- Google Cloud Secret Manager -- HashiCorp Vault -- Azure Key Vault -- And many more - -For detailed usage, consult the [helm-secrets documentation](https://github.com/jkroepke/helm-secrets/wiki). - -## Troubleshooting - -### Common Issues - -- **Database Connection Issues**: Ensure the database connection URL is correct and the database is accessible from the cluster -- **TLS Certificate Issues**: Check cert-manager logs for problems with certificate issuance -- **Blockchain Connection Issues**: Verify RPC endpoint URLs are correct and accessible - -For more detailed debugging, check the application logs: - -```bash -kubectl logs -l app=clearnode -``` - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/faucet-app/chart/README.md.gotmpl b/faucet-app/chart/README.md.gotmpl deleted file mode 100644 index a42a0dfb8..000000000 --- a/faucet-app/chart/README.md.gotmpl +++ /dev/null @@ -1,92 +0,0 @@ -# {{ template "chart.name" . }} - -{{ template "chart.versionBadge" . }} - -{{ template "chart.description" . }} - -## Prerequisites - -- Kubernetes 1.24+ -- Helm 3.0+ -- For TLS: cert-manager installed in the cluster -- For Secrets Management (optional): - - [helm-secrets](https://github.com/jkroepke/helm-secrets/wiki) plugin: `helm plugin install https://github.com/jkroepke/helm-secrets --version v4.6.4` - - [vals](https://github.com/helmfile/vals): `go install github.com/helmfile/vals/cmd/vals@v0.41.0` - -## Installing the Chart - -To install the chart with the release name `my-release`: -```bash -helm install my-release git+https://github.com/erc7824/clearnode@chart?ref=main -``` - -The command deploys Faucet App on the Kubernetes cluster with default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation. - -## Uninstalling the Chart - -To uninstall/delete the `my-release` deployment: -```bash -helm delete my-release -``` - -## Parameters - -{{ template "chart.valuesTable" . }} - -## Gateway Configuration - -By default, the chart creates an API Gateway and configures it to use TLS via cert-manager. To use this feature: - -1. Create a cert-manager ClusterIssuer -2. Configure `networking.tlsClusterIssuer` with the issuer name -3. Set `networking.externalHostname` to your domain name - -> **Warning**: The Gateway currently does not support configurations with a static IP address. Ensure that your setup uses a dynamic DNS or hostname for proper functionality. Alternatively, you can configure an ingress resource to use a static IP address if required. - -## Managing Secrets - -For managing sensitive values like API keys and credentials, you can use `helm-secrets` with `vals`: - -1. Set up the required environment variable: - ```bash - export HELM_SECRETS_BACKEND=vals - ``` - -2. Create a values file with your secrets (e.g., `secrets.yaml`) and refer to secrets using the vals syntax: - ```yaml - apiKey: ref+awssecrets://my-secret/api-key - database: - password: ref+vault://secret/data/database?key=password - ``` - -3. When deploying or upgrading, reference your secrets file with the `secrets://` prefix: - ```bash - helm upgrade --install my-release git+https://github.com/erc7824/clearnode@chart?ref=main \ - -f values.yaml \ - -f secrets://secrets.yaml - ``` - -The vals tool supports [multiple backends](https://github.com/helmfile/vals/tree/main?tab=readme-ov-file#supported-backends) including: -- AWS Secrets Manager and SSM Parameter Store -- Google Cloud Secret Manager -- HashiCorp Vault -- Azure Key Vault -- And many more - -For detailed usage, consult the [helm-secrets documentation](https://github.com/jkroepke/helm-secrets/wiki). - -## Troubleshooting - -### Common Issues - -- **Database Connection Issues**: Ensure the database connection URL is correct and the database is accessible from the cluster -- **TLS Certificate Issues**: Check cert-manager logs for problems with certificate issuance -- **Blockchain Connection Issues**: Verify RPC endpoint URLs are correct and accessible - -For more detailed debugging, check the application logs: - -```bash -kubectl logs -l app=clearnode -``` - -{{ template "helm-docs.versionFooter" . }} diff --git a/faucet-app/chart/config/sandbox/secrets.yaml b/faucet-app/chart/config/sandbox/secrets.yaml deleted file mode 100644 index 1b2aeb73c..000000000 --- a/faucet-app/chart/config/sandbox/secrets.yaml +++ /dev/null @@ -1,4 +0,0 @@ -config: - secretEnvs: - OWNER_PRIVATE_KEY: ref+gcpsecrets://ynet-stage/clearnet-sandbox-faucet-owner-private-key?version=latest - SIGNER_PRIVATE_KEY: ref+gcpsecrets://ynet-stage/clearnet-sandbox-faucet-signer-private-key?version=latest diff --git a/faucet-app/chart/config/sandbox/values.yaml b/faucet-app/chart/config/sandbox/values.yaml deleted file mode 100644 index 825839dfd..000000000 --- a/faucet-app/chart/config/sandbox/values.yaml +++ /dev/null @@ -1,56 +0,0 @@ -config: - args: ["./faucet-server"] - logLevel: debug - nitronodeWsUrl: wss://clearnet-sandbox.yellow.com/ws - token: - symbol: ytest.usd - tipAmount: 10 - minTransferCount: 5 - envSecret: "" - extraEnvs: {} - -image: - repository: ghcr.io/erc7824/faucet-app/server - tag: 25f6448 - -imagePullSecret: ghcr-pull - -service: - http: - enabled: true - port: 8080 - path: /faucet(/|$)(.*) - -metrics: - enabled: false - podmonitoring: - enabled: false - port: 4242 - endpoint: "/metrics" - -resources: - limits: - cpu: 100m - memory: 256Mi - ephemeral-storage: 100Mi - requests: - cpu: 100m - memory: 256Mi - ephemeral-storage: 100Mi - -autoscaling: - enabled: false - -networking: - externalHostname: clearnet-sandbox.yellow.com - tlsClusterIssuer: zerossl-prod - gateway: - enabled: false - ingress: - enabled: true - className: nginx - annotations: - nginx.ingress.kubernetes.io/use-regex: "true" - nginx.ingress.kubernetes.io/rewrite-target: /$2 - tls: - enabled: true diff --git a/faucet-app/chart/config/uat/secrets.yaml b/faucet-app/chart/config/uat/secrets.yaml deleted file mode 100644 index 35403902f..000000000 --- a/faucet-app/chart/config/uat/secrets.yaml +++ /dev/null @@ -1,4 +0,0 @@ -config: - secretEnvs: - OWNER_PRIVATE_KEY: ref+gcpsecrets://ynet-stage/clearnet-uat-faucet-owner-private-key?version=latest - SIGNER_PRIVATE_KEY: ref+gcpsecrets://ynet-stage/clearnet-uat-faucet-signer-private-key?version=latest diff --git a/faucet-app/chart/config/uat/values.yaml b/faucet-app/chart/config/uat/values.yaml deleted file mode 100644 index f75388c5c..000000000 --- a/faucet-app/chart/config/uat/values.yaml +++ /dev/null @@ -1,56 +0,0 @@ -config: - args: ["./faucet-server"] - logLevel: debug - nitronodeWsUrl: wss://canarynet.yellow.com/ws - token: - symbol: ytest.usd - tipAmount: 0.1 - minTransferCount: 5 - envSecret: "" - extraEnvs: {} - -image: - repository: ghcr.io/erc7824/faucet-app/server - tag: 25f6448 - -imagePullSecret: ghcr-pull - -service: - http: - enabled: true - port: 8080 - path: /faucet(/|$)(.*) - -metrics: - enabled: false - podmonitoring: - enabled: false - port: 4242 - endpoint: "/metrics" - -resources: - limits: - cpu: 100m - memory: 256Mi - ephemeral-storage: 100Mi - requests: - cpu: 100m - memory: 256Mi - ephemeral-storage: 100Mi - -autoscaling: - enabled: false - -networking: - externalHostname: canarynet.yellow.com - tlsClusterIssuer: zerossl-prod - gateway: - enabled: false - ingress: - enabled: true - className: nginx - annotations: - nginx.ingress.kubernetes.io/use-regex: "true" - nginx.ingress.kubernetes.io/rewrite-target: /$2 - tls: - enabled: true diff --git a/faucet-app/chart/templates/deployment.yaml b/faucet-app/chart/templates/deployment.yaml deleted file mode 100644 index 5c48c3004..000000000 --- a/faucet-app/chart/templates/deployment.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "faucet-app.common.fullname" . }} - labels: - {{- include "faucet-app.common.labels" . | nindent 4 }} -spec: - {{- include "faucet-app.component.replicaCount" . | nindent 2 }} - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 - selector: - matchLabels: - {{- include "faucet-app.common.selectorLabels" . | nindent 6 }} - template: - metadata: - annotations: - {{- include "faucet-app.component.metricsAnnotations" .Values.metrics | nindent 8 }} - checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} - labels: - {{- include "faucet-app.common.selectorLabels" . | nindent 8 }} - spec: - {{- with .Values.serviceAccount }} - serviceAccountName: {{ . }} - {{- end }} - containers: - - name: api - args: {{ .Values.config.args | toYaml | nindent 10 }} - image: {{ include "faucet-app.component.image" .Values.image }} - imagePullPolicy: IfNotPresent - env: - {{- include "faucet-app.common.env" . | nindent 12 }} - {{- if or .Values.config.secretEnvs .Values.config.envSecret }} - envFrom: - {{- if .Values.config.envSecret }} - - secretRef: - name: {{ .Values.config.envSecret }} - {{- end }} - {{- if .Values.config.secretEnvs }} - - secretRef: - name: {{ include "faucet-app.common.fullname" . }}-secret-env - {{- end }} - {{- end }} - {{- include "faucet-app.component.ports" .Values.service | nindent 10 }} - {{- include "faucet-app.component.resources" .Values.resources | nindent 10 }} - {{- include "faucet-app.component.probes" . | nindent 10 }} - {{- include "faucet-app.common.imagePullSecrets" . | nindent 6 }} - {{- include "faucet-app.common.nodeSelectorLabels" . | nindent 6 }} - {{- include "faucet-app.common.affinity" . | nindent 6 }} - {{- include "faucet-app.common.tolerations" . | nindent 6 }} diff --git a/faucet-app/chart/templates/gateway.yaml b/faucet-app/chart/templates/gateway.yaml deleted file mode 100644 index 48fee0bf6..000000000 --- a/faucet-app/chart/templates/gateway.yaml +++ /dev/null @@ -1,31 +0,0 @@ -{{- if .Values.networking.gateway.enabled }} -apiVersion: gateway.networking.k8s.io/v1 -kind: Gateway -metadata: - name: {{ include "faucet-app.common.fullname" . }} - labels: - {{- include "faucet-app.common.labels" . | nindent 4 }} - annotations: - cert-manager.io/cluster-issuer: {{ .Values.networking.tlsClusterIssuer }} - # FIXME: This annotation doesn't work - {{- if .Values.networking.gateway.ipAddressName }} - gateway.envoyproxy.io/service-annotations: | - networking.gke.io/load-balancer-ip-addresses: "{{ .Values.networking.gateway.ipAddressName }}" - {{- end }} -spec: - gatewayClassName: {{ .Values.networking.gateway.className }} - listeners: - - name: http - protocol: HTTP - hostname: {{ .Values.networking.externalHostname }} - port: 80 - - name: https - protocol: HTTPS - hostname: {{ .Values.networking.externalHostname }} - port: 443 - tls: - mode: Terminate - certificateRefs: - - kind: Secret - name: {{ printf "%s-tls" (.Values.networking.externalHostname | replace "." "-") }} -{{- end }} diff --git a/faucet-app/chart/templates/helpers/_common.tpl b/faucet-app/chart/templates/helpers/_common.tpl deleted file mode 100644 index 523def25c..000000000 --- a/faucet-app/chart/templates/helpers/_common.tpl +++ /dev/null @@ -1,116 +0,0 @@ -{{/* vim: set filetype=mustache: */}} - -{{/* -Expand the name of the component. -*/}} -{{- define "faucet-app.common.name" -}} -{{- .Chart.Name | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully common name. -If release name contains chart name it will be used as a full name. -*/}} -{{- define "faucet-app.common.fullname" -}} -{{- if .Values.prefixOverride }} -{{- printf "%s-%s" .Values.prefixOverride .Chart.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- if contains .Chart.Name .Release.Name }} -{{- print .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Common Selector labels -*/}} -{{- define "faucet-app.common.selectorLabels" -}} -app.kubernetes.io/name: {{ .Chart.Name }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "faucet-app.common.labels" -}} -helm.sh/chart: {{ include "faucet-app.common.chart" . }} -{{ include "faucet-app.common.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- with .Values.extraLabels }} -{{ toYaml . }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "faucet-app.common.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Returns common image pull secrets -*/}} -{{- define "faucet-app.common.imagePullSecrets" -}} -{{- with .Values.imagePullSecret }} -imagePullSecrets: -- name: {{ . }} -{{- end }} -{{- end }} - -{{/* -Returns common environment variables -*/}} -{{- define "faucet-app.common.env" -}} -- name: LOG_LEVEL - value: {{ .Values.config.logLevel }} -- name: SERVER_PORT - value: {{ .Values.service.http.internalPort | default "8080" | print | quote }} -- name: CLEARNODE_URL - value: {{ .Values.config.clearnodeWsUrl | print }} -- name: TOKEN_SYMBOL - value: {{ .Values.config.token.symbol | print }} -- name: STANDARD_TIP_AMOUNT - value: {{ .Values.config.token.tipAmount | print | quote }} -- name: MIN_TRANSFER_COUNT - value: {{ .Values.config.minTransferCount | print | quote }} -{{- range $key, $value := .Values.config.extraEnvs }} -- name: {{ $key | upper }} - value: {{ $value | print | quote }} -{{- end }} -{{- end }} - -{{/* -Returns common node selector labels -*/}} -{{- define "faucet-app.common.nodeSelectorLabels" -}} -{{- with .Values.nodeSelector }} -nodeSelector: - {{ toYaml . | nindent 2 }} -{{- end }} -{{- end }} - -{{/* -Returns common tolerations -*/}} -{{- define "faucet-app.common.tolerations" -}} -{{- with .Values.tolerations }} -tolerations: -{{ toYaml . }} -{{- end }} -{{- end }} - -{{/* -Returns common pod's affinity -*/}} -{{- define "faucet-app.common.affinity" -}} -{{- with .Values.affinity }} -affinity: - {{ toYaml . | nindent 2 }} -{{- end }} -{{- end }} diff --git a/faucet-app/chart/templates/helpers/_component.tpl b/faucet-app/chart/templates/helpers/_component.tpl deleted file mode 100644 index ff4b48d8d..000000000 --- a/faucet-app/chart/templates/helpers/_component.tpl +++ /dev/null @@ -1,78 +0,0 @@ -{{/* vim: set filetype=mustache: */}} - -{{/* -Returns Prometheus metrics' annotations depending on input Values -*/}} -{{- define "faucet-app.component.metricsAnnotations" -}} -prometheus.io/scrape: {{ default false .enabled | print | quote }} -prometheus.io/port: {{ default "4242" .port | print | quote }} -prometheus.io/path: {{ default "/metrics" .endpoint | print | quote }} -{{- end }} - -{{/* -Returns replica count depending on component and HPA settings -*/}} -{{- define "faucet-app.component.replicaCount" -}} -{{- if not (and .autoscaling .autoscaling.enabled) }} -replicas: {{ .replicaCount }} -{{- end }} -{{- end }} - -{{/* -Returns full docker image name -*/}} -{{- define "faucet-app.component.image" -}} -{{ printf "%s:%s" (print .repository) (print .tag) }} -{{- end }} - -{{/* -Returns container ports configuration depending on input service -*/}} -{{- define "faucet-app.component.ports" -}} -{{- if .http.enabled }} -ports: -{{- with .http }} -- name: http - containerPort: {{ default .port .internalPort }} - protocol: TCP -{{- end }} -{{- end }} -{{- end }} - -{{/* -Returns component's resource consumption -*/}} -{{- define "faucet-app.component.resources" -}} -resources: - requests: - cpu: {{ default "100m" .requests.cpu }} - memory: {{ default "128Mi" .requests.memory }} - ephemeral-storage: {{ default "100Mi" .requests.memory }} - limits: - cpu: {{ default "100m" .requests.cpu }} - memory: {{ default "128Mi" .requests.memory }} - ephemeral-storage: {{ default "100Mi" .requests.memory }} -{{- end }} - -{{/* -Returns component's probes -*/}} -{{- define "faucet-app.component.probes" -}} -{{- $port := default .Values.service.http.port .Values.service.http.internalPort }} -{{- range $name, $probe := .Values.probes }} -{{- if $probe.enabled }} -{{ printf "%sProbe" $name }}: - {{- if eq $probe.type "http" }} - httpGet: - port: {{ $port }} - path: {{ default "/health" $probe.endpoint }} - {{- else }} - tcpSocket: - port: {{ $port }} - {{- end }} - initialDelaySeconds: {{ default 5 $probe.initialDelaySeconds }} - timeoutSeconds: {{ default 10 $probe.timeoutSeconds }} - periodSeconds: {{ default 10 $probe.periodSeconds }} -{{- end }} -{{- end }} -{{- end }} diff --git a/faucet-app/chart/templates/helpers/_hpa.tpl b/faucet-app/chart/templates/helpers/_hpa.tpl deleted file mode 100644 index 80006892c..000000000 --- a/faucet-app/chart/templates/helpers/_hpa.tpl +++ /dev/null @@ -1,24 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Returns HorizontalPodAutoscaler API version depending on K8s cluster version -*/}} -{{- define "faucet-app.hpa.apiVersion" -}} -{{- if semverCompare ">=1.23-0" .Capabilities.KubeVersion.Version -}} -autoscaling/v2 -{{- else -}} -autoscaling/v2beta2 -{{- end }} -{{- end }} - -{{/* -Returns HorizontalPodAutoscaler resource target utilization depending on K8s cluster version -*/}} -{{- define "faucet-app.hpa.targetUtilization" -}} -{{- if semverCompare ">=1.23-0" .Capabilities.KubeVersion.Version -}} -target: - type: Utilization - averageUtilization: {{ .averageUtilization }} -{{- else -}} -targetAverageUtilization: {{ .averageUtilization }} -{{- end }} -{{- end }} diff --git a/faucet-app/chart/templates/helpers/_ingress.tpl b/faucet-app/chart/templates/helpers/_ingress.tpl deleted file mode 100644 index 0bf1ec4fa..000000000 --- a/faucet-app/chart/templates/helpers/_ingress.tpl +++ /dev/null @@ -1,67 +0,0 @@ -{{/* vim: set filetype=mustache: */}} - -{{/* -Returns Ingress API version depending on K8s cluster version -*/}} -{{- define "faucet-app.ingress.apiVersion" -}} -{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.Version -}} -networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.Version -}} -networking.k8s.io/v1beta1 -{{- else -}} -extensions/v1beta1 -{{- end }} -{{- end }} - -{{/* -Returns default Ingress annotations -*/}} -{{- define "faucet-app.ingress.annotations" -}} -kubernetes.io/ingress.class: {{ default "nginx" .Values.networking.ingress.className }} -{{- if .Values.networking.ingress.tls.enabled }} -kubernetes.io/tls-acme: "true" -cert-manager.io/cluster-issuer: {{ .Values.networking.tlsClusterIssuer }} -nginx.ingress.kubernetes.io/ssl-redirect: "true" -{{- end }} -{{- if .Values.networking.ingress.grpc }} -nginx.ingress.kubernetes.io/backend-protocol: "GRPC" -{{- end }} -{{- with .Values.networking.ingress.annotations }} -{{ toYaml . }} -{{- end }} -{{- end }} - -{{/* -Returns Ingress TLS configuration -*/}} -{{- define "faucet-app.ingress.tls" -}} -{{- if .Values.networking.ingress.tls.enabled }} -tls: - - secretName: "{{ .Values.networking.externalHostname | replace "." "-" }}-tls" - hosts: - - "{{ .Values.networking.externalHostname }}" -{{- end }} -{{- end }} - -{{/* -Returns Ingress host path configuration -*/}} -{{- define "faucet-app.ingress.httpPath" -}} -{{- $http := .Values.service.http }} -- path: {{ $http.path }} - {{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.Version }} - pathType: ImplementationSpecific - {{- end }} - backend: - {{ $svcName := include "faucet-app.common.fullname" . }} - {{ $svcPort := default $http.port $http.internalPort }} - {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.Version }} - service: - name: {{ $svcName }} - port: - number: {{ $svcPort }} - {{- else }} - serviceName: {{ $svcName }} - servicePort: {{ $svcPort }} - {{- end }} -{{- end }} diff --git a/faucet-app/chart/templates/http-route-redirect.yaml b/faucet-app/chart/templates/http-route-redirect.yaml deleted file mode 100644 index cfe3af2cb..000000000 --- a/faucet-app/chart/templates/http-route-redirect.yaml +++ /dev/null @@ -1,25 +0,0 @@ -{{- if .Values.networking.gateway.enabled }} -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: {{ printf "%s-http-redirect" (include "faucet-app.common.fullname" .) }} - labels: - {{- include "faucet-app.common.labels" . | nindent 4 }} -spec: - parentRefs: - - group: gateway.networking.k8s.io - kind: Gateway - namespace: {{ .Release.Namespace }} - name: {{ include "faucet-app.common.fullname" . }} - port: 80 - sectionName: http - hostnames: - - {{ .Values.networking.externalHostname }} - rules: - - filters: - - type: RequestRedirect - requestRedirect: - scheme: https - statusCode: 301 - hostname: {{ .Values.networking.externalHostname }} -{{- end }} diff --git a/faucet-app/chart/templates/http-route.yaml b/faucet-app/chart/templates/http-route.yaml deleted file mode 100644 index 587eb211b..000000000 --- a/faucet-app/chart/templates/http-route.yaml +++ /dev/null @@ -1,27 +0,0 @@ -{{- $http := .Values.service.http }} -{{- if and .Values.networking.gateway.enabled $http.enabled }} -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: {{ include "faucet-app.common.fullname" . }} - labels: - {{- include "faucet-app.common.labels" . | nindent 4 }} -spec: - parentRefs: - - group: gateway.networking.k8s.io - kind: Gateway - namespace: {{ .Release.Namespace }} - name: {{ include "faucet-app.common.fullname" . }} - port: 443 - sectionName: https - hostnames: - - {{ .Values.networking.externalHostname }} - rules: - - matches: - - path: - type: PathPrefix - value: {{ $http.path }} - backendRefs: - - name: {{ include "faucet-app.common.fullname" . }} - port: {{ default $http.port $http.internalPort }} -{{- end }} diff --git a/faucet-app/chart/templates/ingress.yaml b/faucet-app/chart/templates/ingress.yaml deleted file mode 100644 index 7824ff2c9..000000000 --- a/faucet-app/chart/templates/ingress.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if and .Values.networking.ingress.enabled .Values.service.http.enabled }} -apiVersion: {{ include "faucet-app.ingress.apiVersion" . }} -kind: Ingress -metadata: - name: {{ include "faucet-app.common.fullname" . }} - labels: - {{- include "faucet-app.common.labels" . | nindent 4 }} - annotations: - {{- include "faucet-app.ingress.annotations" . | nindent 4 }} -spec: - rules: - - host: {{ .Values.networking.externalHostname }} - http: - paths: - {{- include "faucet-app.ingress.httpPath" . | nindent 10 }} - {{- include "faucet-app.ingress.tls" . | nindent 2 }} -{{- end }} diff --git a/faucet-app/chart/templates/podmonitoring.yaml b/faucet-app/chart/templates/podmonitoring.yaml deleted file mode 100644 index 31ac7dbdf..000000000 --- a/faucet-app/chart/templates/podmonitoring.yaml +++ /dev/null @@ -1,16 +0,0 @@ -{{- if and .Values.metrics.enabled .Values.metrics.podmonitoring.enabled }} -apiVersion: monitoring.googleapis.com/v1 -kind: PodMonitoring -metadata: - name: {{ include "faucet-app.common.fullname" . }} - labels: - {{- include "faucet-app.common.labels" . | nindent 4 }} -spec: - selector: - matchLabels: - {{- include "faucet-app.common.selectorLabels" . | nindent 6 }} - endpoints: - - port: {{ .Values.metrics.port }} - path: {{ .Values.metrics.endpoint }} - interval: {{ .Values.metrics.scrapeInterval }} -{{- end }} diff --git a/faucet-app/chart/templates/service.yaml b/faucet-app/chart/templates/service.yaml deleted file mode 100644 index 04854f913..000000000 --- a/faucet-app/chart/templates/service.yaml +++ /dev/null @@ -1,20 +0,0 @@ -{{- $svc := .Values.service }} -{{- if $svc.http.enabled }} -apiVersion: v1 -kind: Service -metadata: - name: {{ include "faucet-app.common.fullname" . }} - labels: - {{- include "faucet-app.common.labels" . | nindent 4 }} -spec: - type: ClusterIP - ports: - {{- with $svc.http }} - - name: http - port: {{ default .port .externalPort }} - targetPort: {{ default .port .internalPort }} - protocol: TCP - {{- end }} - selector: - {{- include "faucet-app.common.selectorLabels" . | nindent 4 }} -{{- end }} diff --git a/faucet-app/chart/values.yaml b/faucet-app/chart/values.yaml deleted file mode 100644 index f5c5e4f37..000000000 --- a/faucet-app/chart/values.yaml +++ /dev/null @@ -1,137 +0,0 @@ -# -- Override the full name -fullnameOverride: "" - -config: - # -- List of arguments to pass to the container - args: ["./faucet-server"] - # -- Log level (info, debug, warn, error) - logLevel: info - # -- Nitronode WebSocket URL - nitronodeWsUrl: "wss://nitronode.example.com/ws" - token: - # -- Token Symbol inside the Nitronode network - symbol: usdc - # -- The amount of tokens to tip per request - tipAmount: 10 - # -- Minimum number of transfers the server should have a balance for to operate - minTransferCount: 5 - # -- Additional environment variables as key-value pairs - extraEnvs: {} - # KEY: VALUE - # -- Additional environment variables to be stored in a secret - secretEnvs: {} - # KEY: VALUE - # -- Name of the secret containing environment variables - envSecret: "" - -# -- Number of replicas -replicaCount: 1 - -image: - # -- Docker image repository - repository: ghcr.io/erc7824/faucet-app/server - # -- Docker image tag - tag: 0.0.1 - -service: - http: - # -- Enable HTTP service - enabled: true - # -- HTTP service port - port: 8000 - # -- HTTP service path - path: / - -metrics: - # -- Enable Prometheus metrics - enabled: true - podmonitoring: - # -- Enable PodMonitoring for Managed Prometheus - enabled: false - # -- Metrics port - port: 4242 - # -- Metrics endpoint path - endpoint: "/metrics" - # -- Metrics scrape interval - scrapeInterval: 30s - -probes: - liveness: - # -- Enable liveness probe - enabled: false - # -- Liveness probe type (http, tcp) - type: tcp - readiness: - # -- Enable readiness probe - enabled: false - # -- Readiness probe type (http, tcp) - type: tcp - -resources: - # -- Resource limits - limits: {} - # cpu: 100m - # memory: 256Mi - # ephemeral-storage: 100Mi - # -- Resource requests - requests: {} - # cpu: 100m - # memory: 256Mi - # ephemeral-storage: 100Mi - -# -- Service account name -serviceAccount: "" - -autoscaling: - # -- Enable autoscaling - enabled: false - # -- Minimum number of replicas - minReplicas: 2 - # -- Maximum number of replicas - maxReplicas: 100 - # -- Target CPU utilization - targetCPUUtilizationPercentage: 80 - # -- Target memory utilization - targetMemoryUtilizationPercentage: 80 - -networking: - # -- TLS cluster issuer - tlsClusterIssuer: zerossl-prod - # -- External hostname for the gateway - externalHostname: nitronode.example.com - - gateway: - # -- Enable API gateway - enabled: true - # -- Gateway class name - className: envoy-gateway - # -- GKE static IP address name (GKE only) - ipAddressName: "" - - ingress: - # -- Enable ingress - enabled: false - # -- Ingress class name - className: nginx - # -- Ingress annotations - annotations: {} - # -- Enable GRPC for ingress - grpc: false - tls: - # -- Enable TLS for ingress - enabled: false - -# -- Image pull secret name -imagePullSecret: "" - -# -- Node selector -nodeSelector: {} - -# -- Tolerations -tolerations: [] - -# -- Affinity settings -affinity: {} - -# -- Additional labels to add to all resources -extraLabels: {} diff --git a/faucet-app/scripts/auto_tag.sh b/faucet-app/scripts/auto_tag.sh deleted file mode 100755 index ad36f2452..000000000 --- a/faucet-app/scripts/auto_tag.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -last_rc_tag=$(git ls-remote -t | grep -Eo 'v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$' | sort -V -r | head -n 1) -last_stable_tag=$(git ls-remote -t | grep -Eo 'v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V -r | head -n 1) - -new_rc_tag="" -if [ -z "$last_rc_tag" ] && [ -z "$last_stable_tag" ]; then - # No tags found - new_rc_tag="v0.0.0-rc.0" -elif [ -z "$last_stable_tag" ]; then - # No stable tags found - new_rc_tag="${last_rc_tag%.*}.$((${last_rc_tag##*.}+1))" -elif [ "${last_rc_tag%-*}" = "${last_stable_tag}" ]; then - # Last RC tag is the latest stable tag - new_rc_tag="${last_stable_tag}-rc.$((${last_rc_tag##*.}+1))" -else - # Last RC tag is not the latest stable tag - new_rc_tag="${last_stable_tag}-rc.0" -fi - -git tag "$new_rc_tag" -git push origin "$new_rc_tag" diff --git a/faucet-app/server/.env.example b/faucet-app/server/.env.example index 3e4f83957..a12ef1ba7 100644 --- a/faucet-app/server/.env.example +++ b/faucet-app/server/.env.example @@ -14,22 +14,22 @@ SERVER_PORT=8080 # ----------------------------------------------------------------------------- -# Clearnode Configuration +# Nitronode Configuration # ----------------------------------------------------------------------------- # Private key for faucet owner wallet (without 0x prefix) -# REQUIRED: Used for EIP-712 authentication with Clearnode +# REQUIRED: Used for EIP-712 authentication with Nitronode OWNER_PRIVATE_KEY=your_owner_private_key_here_without_0x_prefix -# Private key for transaction signing (without 0x prefix) -# REQUIRED: Used for signing transfer transactions - must be different from OWNER_PRIVATE_KEY -SIGNER_PRIVATE_KEY=your_signer_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) -# Clearnode WebSocket URL -# REQUIRED: The WebSocket endpoint for your Clearnode instance -CLEARNODE_URL=wss://clearnode.example.com/ws +# 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 Clearnode) +# REQUIRED: The symbol of the token to distribute (must be supported by Nitronode) TOKEN_SYMBOL=usdc # Default amount to send per request diff --git a/faucet-app/server/README.md b/faucet-app/server/README.md index f8efa5104..1540c441e 100644 --- a/faucet-app/server/README.md +++ b/faucet-app/server/README.md @@ -1,10 +1,10 @@ # Nitrolite Faucet Server -A Go-based faucet server that distributes tokens through the Clearnode network using WebSocket connections. +A Go-based faucet server that distributes tokens through the Nitronode network using WebSocket connections. ## Features -- **Nitrolite SDK Integration**: Uses the official `github.com/layer-3/nitrolite` SDK for Clearnode communication +- **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 @@ -17,19 +17,19 @@ The application is structured into several packages: - `internal/config`: Configuration management with environment variables - `internal/logger`: Structured logging with logrus -- `internal/clearnode`: Thin wrapper around the Nitrolite SDK client +- `internal/nitronode`: Thin wrapper around the Nitrolite SDK client - `internal/server`: HTTP server with Gin framework -### Clearnode Client +### Nitronode Client -The `internal/clearnode` package wraps the Nitrolite SDK's `sdk.Client`. Connection and message signing are handled internally by the SDK — no manual WebSocket management is required. +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. **Clone and setup**: +1. **Setup**: ```bash - cd server + cd faucet-app/server go mod tidy ``` @@ -50,203 +50,124 @@ The `internal/clearnode` package wraps the Nitrolite SDK's `sdk.Client`. Connect The application uses [cleanenv](https://github.com/ilyakaznacheev/cleanenv) for configuration management. Configuration can be provided via: -1. **Environment variables** (highest priority) -2. **`.env` file** in the current directory -3. **Default values** for optional settings +1. **`.env` file** in the current directory +2. **Environment variables** (used when `.env` is absent) -Set the following environment variables (or create a `.env` file): +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...` | -| `CLEARNODE_URL` | **Yes** | - | Clearnode WebSocket URL | `wss://testnet.clearnode.io/ws` | +| `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` | -## API Endpoints +> **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. -### POST /requestTokens +## API Endpoints -Request tokens from the faucet. +### `POST /requestTokens` -**Request Body:** +Request tokens for an Ethereum address. +**Request body:** ```json -{ - "userAddress": "0x1234567890abcdef1234567890abcdef12345678" -} +{ "userAddress": "0x..." } ``` -**Success Response:** - +**Success response (200):** ```json { "success": true, "message": "Tokens sent successfully", - "txId": "abc123", + "txId": "...", "amount": "10", "asset": "usdc", - "destination": "0x1234567890abcdef1234567890abcdef12345678" -} -``` - -**Error Response:** - -```json -{ - "error": "Invalid address format." + "destination": "0x..." } ``` -### GET /info - -Service information endpoint. - -**Response:** - -```json -{ - "service": "Nitrolite Faucet Server", - "version": "1.0.0", - "faucet_address": "0xabcd...", - "standard_tip_amount": "10", - "token_symbol": "usdc", - "endpoints": ["/requestTokens"] -} -``` - -## Startup Validation - -The server performs validation during startup: - -### Token Support Validation - -- Queries Clearnode using `GetAssets()` to fetch all supported tokens -- Validates that the configured `TOKEN_SYMBOL` exists -- Server refuses to start if the token is not supported +**Error responses:** +- `400` — Invalid address or request format +- `429` — Rate limit exceeded +- `500` — Transfer failed +- `503` — Nitronode unavailable or balance insufficient -### Balance Verification +### `GET /info` -- Queries the owner balance using `GetBalances()` -- Requires balance ≥ `STANDARD_TIP_AMOUNT × MIN_TRANSFER_COUNT` -- Server refuses to start with insufficient funds - -Example startup output: - -```text -INFO Starting Nitrolite Faucet Server -INFO Faucet owner address: 0xabc... -INFO Successfully connected to Clearnode -INFO Token 'usdc' is supported by Clearnode -INFO ✓ Sufficient usdc balance: 50000000 -INFO Faucet server is ready to serve requests -``` +Returns server metadata. ## WebSocket Connection Management -The Nitrolite SDK maintains a persistent WebSocket connection with Clearnode: +The Nitrolite SDK maintains a persistent WebSocket connection with Nitronode: -- **Connection**: Established on startup inside `clearnode.NewClient()`; no separate connect/auth step is needed +- **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 recreates the SDK client automatically +- **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 Clearnode requests are cryptographically signed by the SDK +- **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 -# Build binary go build -o faucet-server main.go - -# Run with environment file ./faucet-server ``` -## Docker Support - -```dockerfile -FROM golang:1.21-alpine AS builder -WORKDIR /app -COPY go.mod go.sum ./ -RUN go mod download -COPY . . -RUN go build -o faucet-server main.go - -FROM alpine:latest -RUN apk --no-cache add ca-certificates -WORKDIR /root/ -COPY --from=builder /app/faucet-server . -CMD ["./faucet-server"] -``` - ## Development ```bash -# Install dependencies -go mod tidy +# Run tests (from repo root) +go test ./faucet-app/... -# Run with hot reload (using air) +# Run with hot reload go install github.com/cosmtrek/air@latest air - -# Run tests -go test ./... ``` -## Logging - -The application uses structured JSON logging: - -```json -{ - "level": "info", - "msg": "Processing faucet request for address: 0x1234...", - "time": "2023-12-01T10:00:00Z" -} -``` - -Log levels: `debug`, `info`, `warn`, `error`, `fatal` - ## Error Handling -- **Connection Errors**: Returns 503 if Clearnode is unavailable or reconnection fails +- **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 Clearnode transfer failures +- **Transfer Errors**: Returns 500 for Nitronode transfer failures - **Service Unavailable**: Returns 503 if token is unsupported or balance is insufficient -## Monitoring - -Key metrics to monitor: - -- Transfer success/failure rates -- Response times -- Server resource usage - ## Troubleshooting **Connection Issues:** -- Verify `CLEARNODE_URL` is correct and accessible +- Verify `NITRONODE_URL` is correct and accessible - Check firewall settings for WebSocket connections -**Authentication Issues:** +**Token Not Supported:** -- Verify `OWNER_PRIVATE_KEY` format (no `0x` prefix) -- Review logs for SDK connection errors +- Verify `TOKEN_SYMBOL` is supported by the Nitronode instance -**Transfer Issues:** +**Insufficient Balance:** -- Verify `TOKEN_SYMBOL` is supported by the Clearnode instance -- Check faucet account balance meets the minimum threshold -- Review Clearnode transfer logs +- Top up the faucet wallet; the server requires at least `MIN_TRANSFER_COUNT × STANDARD_TIP_AMOUNT` available balance diff --git a/faucet-app/server/go.mod b/faucet-app/server/go.mod deleted file mode 100644 index 5bcbd32a5..000000000 --- a/faucet-app/server/go.mod +++ /dev/null @@ -1,81 +0,0 @@ -module faucet-server - -go 1.25.9 - -require ( - github.com/ethereum/go-ethereum v1.17.2 - github.com/gin-gonic/gin v1.11.0 - github.com/ilyakaznacheev/cleanenv v1.5.0 - github.com/layer-3/nitrolite v0.0.0 - github.com/shopspring/decimal v1.4.0 - github.com/sirupsen/logrus v1.9.4 - github.com/stretchr/testify v1.11.1 -) - -require ( - github.com/BurntSushi/toml v1.5.0 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect - github.com/bits-and-blooms/bitset v1.22.0 // indirect - github.com/bytedance/sonic v1.14.0 // indirect - github.com/bytedance/sonic/loader v0.3.0 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudwego/base64x v0.1.6 // indirect - github.com/consensys/gnark-crypto v0.18.1 // indirect - github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/deckarep/golang-set/v2 v2.8.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.28.0 // indirect - github.com/goccy/go-json v0.10.4 // indirect - github.com/goccy/go-yaml v1.18.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - github.com/holiman/uint256 v1.3.2 // indirect - github.com/joho/godotenv v1.5.1 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/jsternberg/zap-logfmt v1.3.0 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.21 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.57.0 // indirect - github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/supranational/blst v0.3.16 // indirect - github.com/tklauser/go-sysconf v0.3.16 // indirect - github.com/tklauser/numcpus v0.11.0 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.0 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.28.0 // indirect - golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.50.0 // indirect - golang.org/x/net v0.53.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.36.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect -) - -replace github.com/layer-3/nitrolite => ../../ diff --git a/faucet-app/server/go.sum b/faucet-app/server/go.sum deleted file mode 100644 index 0e11d3fd8..000000000 --- a/faucet-app/server/go.sum +++ /dev/null @@ -1,309 +0,0 @@ -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= -github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= -github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= -github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= -github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= -github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= -github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= -github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= -github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= -github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= -github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= -github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= -github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= -github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= -github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= -github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= -github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= -github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= -github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= -github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= -github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= -github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= -github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= -github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= -github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= -github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= -github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= -github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= -github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= -github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= -github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= -github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= -github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= -github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= -github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= -github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= -github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= -github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= -github.com/ethereum/go-ethereum v1.17.2 h1:ag6geu0kn8Hv5FLKTpH+Hm2DHD+iuFtuqKxEuwUsDOI= -github.com/ethereum/go-ethereum v1.17.2/go.mod h1:KHcRXfGOUfUmKg51IhQ0IowiqZ6PqZf08CMtk0g5K1o= -github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= -github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= -github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= -github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= -github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= -github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= -github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= -github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= -github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= -github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= -github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= -github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= -github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= -github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= -github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= -github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= -github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= -github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= -github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= -github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= -github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= -github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= -github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= -github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= -github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= -github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= -github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= -github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= -github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= -github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= -github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= -github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= -github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= -github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= -github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= -github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= -github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= -github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= -github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= -github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= -github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= -github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= -github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= -github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= -github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= -github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= -github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= -github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= -github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= -github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= -github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= -github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= -github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= -github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= -github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= -go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= -go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= -go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= -golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= -golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= -olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= diff --git a/faucet-app/server/internal/config/config.go b/faucet-app/server/internal/config/config.go index 5a6604410..c49ae2a85 100644 --- a/faucet-app/server/internal/config/config.go +++ b/faucet-app/server/internal/config/config.go @@ -1,38 +1,50 @@ package config import ( + "errors" "fmt" + "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)"` + 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 - // Try to read from .env file first, then from environment variables - if err := cleanenv.ReadConfig(".env", &config); err != nil { - // If .env doesn't exist, try to read from environment variables only + 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: %w", err) + return nil, fmt.Errorf("failed to load configuration from environment: %w", err) } } @@ -43,17 +55,15 @@ func Load() (*Config, error) { return &config, nil } +// Validate parses and range-checks all fields that require post-load processing. func (c *Config) Validate() error { - // Parse the decimal amount 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) @@ -65,5 +75,17 @@ func (c *Config) Validate() error { } c.CooldownPeriodDuration = d + 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, ",") { + if trimmed := strings.TrimSpace(p); trimmed != "" { + c.TrustedProxyList = append(c.TrustedProxyList, trimmed) + } + } + } + return nil } diff --git a/faucet-app/server/internal/logger/logger.go b/faucet-app/server/internal/logger/logger.go index 2aa11c5f5..867993cbe 100644 --- a/faucet-app/server/internal/logger/logger.go +++ b/faucet-app/server/internal/logger/logger.go @@ -1,3 +1,4 @@ +// Package logger provides a thin structured-logging wrapper around logrus. package logger import ( @@ -6,63 +7,55 @@ import ( "github.com/sirupsen/logrus" ) -var Log *logrus.Logger - -func Initialize(level string) error { - Log = logrus.New() +// Log is the package-level logger. It is initialised with sane defaults at +// package init time so callers never encounter a nil dereference, even if +// Initialize has not yet been called. +var Log = logrus.New() +func init() { Log.SetFormatter(&logrus.JSONFormatter{ TimestampFormat: "2006-01-02T15:04:05.000Z07:00", }) - Log.SetOutput(os.Stdout) + Log.SetLevel(logrus.InfoLevel) +} +// Initialize reconfigures the logger with the requested level. +func Initialize(level string) error { logLevel, err := logrus.ParseLevel(level) if err != nil { return err } - Log.SetLevel(logLevel) - return nil } -func Info(args ...interface{}) { - Log.Info(args...) -} +// Info logs an info-level message. +func Info(args ...interface{}) { Log.Info(args...) } -func Infof(format string, args ...interface{}) { - Log.Infof(format, args...) -} +// Infof logs a formatted info-level message. +func Infof(format string, args ...interface{}) { Log.Infof(format, args...) } -func Warn(args ...interface{}) { - Log.Warn(args...) -} +// Warn logs a warn-level message. +func Warn(args ...interface{}) { Log.Warn(args...) } -func Warnf(format string, args ...interface{}) { - Log.Warnf(format, args...) -} +// Warnf logs a formatted warn-level message. +func Warnf(format string, args ...interface{}) { Log.Warnf(format, args...) } -func Error(args ...interface{}) { - Log.Error(args...) -} +// Error logs an error-level message. +func Error(args ...interface{}) { Log.Error(args...) } -func Errorf(format string, args ...interface{}) { - Log.Errorf(format, args...) -} +// Errorf logs a formatted error-level message. +func Errorf(format string, args ...interface{}) { Log.Errorf(format, args...) } -func Debug(args ...interface{}) { - Log.Debug(args...) -} +// Debug logs a debug-level message. +func Debug(args ...interface{}) { Log.Debug(args...) } -func Debugf(format string, args ...interface{}) { - Log.Debugf(format, args...) -} +// Debugf logs a formatted debug-level message. +func Debugf(format string, args ...interface{}) { Log.Debugf(format, args...) } -func Fatal(args ...interface{}) { - Log.Fatal(args...) -} +// Fatal logs a fatal-level message and exits. +func Fatal(args ...interface{}) { Log.Fatal(args...) } -func Fatalf(format string, args ...interface{}) { - Log.Fatalf(format, args...) -} \ No newline at end of file +// Fatalf logs a formatted fatal-level message and exits. +func Fatalf(format string, args ...interface{}) { Log.Fatalf(format, args...) } diff --git a/faucet-app/server/internal/nitronode/client.go b/faucet-app/server/internal/nitronode/client.go index 1d283722b..93232e9b8 100644 --- a/faucet-app/server/internal/nitronode/client.go +++ b/faucet-app/server/internal/nitronode/client.go @@ -12,7 +12,7 @@ import ( sdk "github.com/layer-3/nitrolite/sdk/go" "github.com/shopspring/decimal" - "faucet-server/internal/logger" + "github.com/layer-3/nitrolite/faucet-app/server/internal/logger" ) const ( @@ -36,13 +36,19 @@ type Client struct { sdkClient *sdk.Client newSDKClient func() (*sdk.Client, error) // captures parsed signers; no raw key hex stored + reconnectMu sync.Mutex // serialises reconnect attempts; not held during I/O + tokenMu sync.Mutex // serialises GetAssets; prevents N goroutines racing to validate + ownerAddress string tokenSymbol string tipAmount decimal.Decimal minTransferCount int - tokenSupported bool // cached after first successful GetAssets; reset on reconnect + tokenSupported bool // cached per connection; reset in reconnect } +// NewClient creates a Client that wraps the Nitrolite SDK for faucet operations. +// privateKeyHex drives both message signing and tx signing. nitronodeURL is the +// WebSocket endpoint. The client is immediately connected and ready to use. func NewClient(privateKeyHex, nitronodeURL, tokenSymbol string, tipAmount decimal.Decimal, minTransferCount int) (*Client, error) { // Parse signers once — raw key hex is used here and not retained on the struct. msgSigner, err := sign.NewEthereumMsgSigner(privateKeyHex) @@ -97,35 +103,42 @@ func (c *Client) GetOwnerAddress() string { // EnsureConnected checks the connection and reconnects with exponential backoff if necessary. func (c *Client) EnsureConnected() error { - // Fast path: read WaitCh under read lock. + // Fast path: check WaitCh under read lock. c.mu.RLock() waitCh := c.sdkClient.WaitCh() c.mu.RUnlock() select { case <-waitCh: - // Connection lost; fall through to reconnect. + // Connection lost; fall through. default: return nil } - // Slow path: write lock with double-check to prevent thundering-herd reconnects. - c.mu.Lock() - defer c.mu.Unlock() + // Serialise reconnect attempts; only one goroutine does the work at a time. + c.reconnectMu.Lock() + defer c.reconnectMu.Unlock() + + // Double-check under read lock — another goroutine may have reconnected while + // we waited for reconnectMu. + c.mu.RLock() + waitCh = c.sdkClient.WaitCh() + c.mu.RUnlock() select { - case <-c.sdkClient.WaitCh(): - // Still disconnected; proceed with reconnect. + case <-waitCh: + // Still disconnected; proceed. default: - return nil // Another goroutine already reconnected while we waited for the lock. + return nil } - return c.reconnectLocked() + return c.reconnect() } -// reconnectLocked tries to establish a new SDK connection with exponential backoff. -// Must be called with c.mu write lock held. -func (c *Client) reconnectLocked() error { +// reconnect retries SDK connection with exponential backoff. +// reconnectMu must be held by the caller; c.mu is NOT held here so I/O +// (dial + ping) does not stall readers or writers. +func (c *Client) reconnect() error { delay := reconnectInitDelay var lastErr error @@ -137,18 +150,21 @@ func (c *Client) reconnectLocked() error { lastErr = err logger.Warnf("Reconnect attempt %d/%d failed: %v", attempt, reconnectAttempts, err) } else { - // Ping to confirm the new connection is alive before accepting it. + // Ping without holding any lock. ctx, cancel := context.WithTimeout(context.Background(), pingTimeout) pingErr := newClient.Ping(ctx) cancel() if pingErr == nil { - oldClient := c.sdkClient + // Swap under write lock — fast, no I/O. + c.mu.Lock() + old := c.sdkClient c.sdkClient = newClient - c.tokenSupported = false // re-validate token on the new connection + c.tokenSupported = false // re-validate on new connection + c.mu.Unlock() - // doClose is a non-blocking channel close, safe to call under the lock. - if err := oldClient.Close(); err != nil { + // Close old client outside the lock. + if err := old.Close(); err != nil { logger.Errorf("Error closing stale Nitronode client: %v", err) } logger.Infof("Successfully reconnected to Nitronode on attempt %d", attempt) @@ -157,7 +173,9 @@ func (c *Client) reconnectLocked() error { lastErr = fmt.Errorf("ping failed: %w", pingErr) logger.Warnf("Reconnect attempt %d/%d ping failed: %v", attempt, reconnectAttempts, pingErr) - _ = newClient.Close() + if err := newClient.Close(); err != nil { + logger.Warnf("Error closing failed reconnect client: %v", err) + } } if attempt < reconnectAttempts { @@ -183,7 +201,19 @@ func (c *Client) EnsureOperational() error { } func (c *Client) validateTokenSupport(tokenSymbol string) error { - // Fast path: token already confirmed on this connection. + // Fast path: already confirmed for this connection. + c.mu.RLock() + if c.tokenSupported { + c.mu.RUnlock() + return nil + } + c.mu.RUnlock() + + // Serialise the GetAssets call so only one goroutine fetches at a time. + c.tokenMu.Lock() + defer c.tokenMu.Unlock() + + // Double-check after acquiring tokenMu. c.mu.RLock() cached := c.tokenSupported cl := c.sdkClient @@ -205,7 +235,9 @@ func (c *Client) validateTokenSupport(tokenSymbol string) error { if strings.EqualFold(asset.Symbol, tokenSymbol) { logger.Debugf("Token '%s' is supported by Nitronode", tokenSymbol) c.mu.Lock() - c.tokenSupported = true + if c.sdkClient == cl { // guard against reconnect between fetch and write + c.tokenSupported = true + } c.mu.Unlock() return nil } @@ -281,7 +313,7 @@ func (c *Client) Transfer(destination, asset string, amount decimal.Decimal) (*T } // Close shuts down the Nitronode connection. -// Uses write lock to serialize with reconnect and prevent closing a freshly installed client. +// Uses write lock to serialise with reconnect and prevent closing a freshly installed client. func (c *Client) Close() error { c.mu.Lock() defer c.mu.Unlock() diff --git a/faucet-app/server/internal/nitronode/client_validation_test.go b/faucet-app/server/internal/nitronode/client_validation_test.go index 70f5a6a7a..6bd8a5ea0 100644 --- a/faucet-app/server/internal/nitronode/client_validation_test.go +++ b/faucet-app/server/internal/nitronode/client_validation_test.go @@ -26,11 +26,14 @@ func TestNewClientValidation(t *testing.T) { }) t.Run("should handle 0x prefixed key", func(t *testing.T) { - // NewEthereumMsgSigner should handle 0x prefix; if it fails, err is non-nil + // NewEthereumMsgSigner should handle 0x prefix; if it fails, err is non-nil. + // Either outcome is acceptable — the test just asserts no panic and consistency. key := "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" - // We expect either success (SDK handles prefix) or a signer error, - // but never a panic. - _, _ = NewClient(key, "ws://localhost:19999", "usdc", decimal.NewFromInt(10), 1) + client, err := NewClient(key, "ws://localhost:19999", "usdc", decimal.NewFromInt(10), 1) + if err == nil { + require.NotNil(t, client) + require.NoError(t, client.Close()) + } }) } diff --git a/faucet-app/server/internal/server/ratelimiter.go b/faucet-app/server/internal/server/ratelimiter.go index d9ac4b1c3..854544635 100644 --- a/faucet-app/server/internal/server/ratelimiter.go +++ b/faucet-app/server/internal/server/ratelimiter.go @@ -25,13 +25,13 @@ func (r *rateLimiter) checkAndRecord(key string) bool { now := time.Now() r.mu.Lock() defer r.mu.Unlock() + r.calls++ + r.evictExpiredLocked(now) last, exists := r.seen[key] if exists && now.Sub(last) < r.cooldown { return false } r.seen[key] = now - r.calls++ - r.evictExpiredLocked(now) return true } @@ -43,6 +43,9 @@ func (r *rateLimiter) checkAndRecordBoth(addr, ip string) (bool, string) { r.mu.Lock() defer r.mu.Unlock() + r.calls++ + r.evictExpiredLocked(now) + if last, ok := r.seen[addr]; ok && now.Sub(last) < r.cooldown { return false, "address" } @@ -54,8 +57,6 @@ func (r *rateLimiter) checkAndRecordBoth(addr, ip string) (bool, string) { if ip != addr { r.seen[ip] = now } - r.calls++ - r.evictExpiredLocked(now) return true, "" } diff --git a/faucet-app/server/internal/server/server.go b/faucet-app/server/internal/server/server.go index 58c254437..8214ac6f2 100644 --- a/faucet-app/server/internal/server/server.go +++ b/faucet-app/server/internal/server/server.go @@ -8,9 +8,9 @@ import ( "github.com/gin-gonic/gin" "github.com/shopspring/decimal" - "faucet-server/internal/nitronode" - "faucet-server/internal/config" - "faucet-server/internal/logger" + "github.com/layer-3/nitrolite/faucet-app/server/internal/nitronode" + "github.com/layer-3/nitrolite/faucet-app/server/internal/config" + "github.com/layer-3/nitrolite/faucet-app/server/internal/logger" ) // NitronodeClient is the interface the server uses to interact with Nitronode. @@ -64,9 +64,14 @@ func NewServer(cfg *config.Config, client NitronodeClient) *Server { } router := gin.New() - // Disable X-Forwarded-For trust so c.ClientIP() uses RemoteAddr. - // Configure with actual LB IP(s) if deployed behind a trusted reverse proxy. - router.SetTrustedProxies(nil) + if len(cfg.TrustedProxyList) > 0 { + router.SetTrustedProxies(cfg.TrustedProxyList) + } else { + // No proxies configured: c.ClientIP() uses RemoteAddr directly. + // Set TRUSTED_PROXIES if the faucet is behind an ingress or load balancer, + // otherwise IP-based rate limiting will collapse to one bucket per proxy. + router.SetTrustedProxies(nil) + } // Add middleware router.Use(gin.Recovery()) diff --git a/faucet-app/server/internal/server/server_test.go b/faucet-app/server/internal/server/server_test.go index 6e2c314de..524f49a0f 100644 --- a/faucet-app/server/internal/server/server_test.go +++ b/faucet-app/server/internal/server/server_test.go @@ -14,9 +14,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "faucet-server/internal/nitronode" - "faucet-server/internal/config" - "faucet-server/internal/logger" + "github.com/layer-3/nitrolite/faucet-app/server/internal/nitronode" + "github.com/layer-3/nitrolite/faucet-app/server/internal/config" + "github.com/layer-3/nitrolite/faucet-app/server/internal/logger" ) // mockNitronodeClient is a simple in-memory mock implementing NitronodeClient. @@ -67,7 +67,9 @@ func defaultMock() *mockNitronodeClient { } func TestMain(m *testing.M) { - _ = logger.Initialize("debug") + if err := logger.Initialize("debug"); err != nil { + panic(err) + } os.Exit(m.Run()) } @@ -76,7 +78,8 @@ func TestRequestTokens_Success(t *testing.T) { srv := NewServer(defaultConfig(), mock) testAddress := common.HexToAddress("0x742D35CC6634c0532925a3B8c17D18fBe3b78890").Hex() - body, _ := json.Marshal(FaucetRequest{UserAddress: testAddress}) + body, err := json.Marshal(FaucetRequest{UserAddress: testAddress}) + require.NoError(t, err) req := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") @@ -103,7 +106,8 @@ func TestRequestTokens_Success(t *testing.T) { func TestRequestTokens_InvalidAddress(t *testing.T) { srv := NewServer(defaultConfig(), defaultMock()) - body, _ := json.Marshal(FaucetRequest{UserAddress: "not-an-address"}) + body, err := json.Marshal(FaucetRequest{UserAddress: "not-an-address"}) + require.NoError(t, err) req := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() @@ -119,7 +123,8 @@ func TestRequestTokens_InvalidAddress(t *testing.T) { func TestRequestTokens_MissingField(t *testing.T) { srv := NewServer(defaultConfig(), defaultMock()) - body, _ := json.Marshal(map[string]string{"wrongField": "0x1234"}) + body, err := json.Marshal(map[string]string{"wrongField": "0x1234"}) + require.NoError(t, err) req := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() @@ -137,7 +142,8 @@ func TestRequestTokens_ConnectionFailure(t *testing.T) { mock.connErr = assert.AnError srv := NewServer(defaultConfig(), mock) - body, _ := json.Marshal(FaucetRequest{UserAddress: "0x742d35Cc6634C0532925a3b8c17d18fBE3b78890"}) + body, err := json.Marshal(FaucetRequest{UserAddress: "0x742d35Cc6634C0532925a3b8c17d18fBE3b78890"}) + require.NoError(t, err) req := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() @@ -155,7 +161,8 @@ func TestRequestTokens_OperationalFailure(t *testing.T) { mock.operationalErr = assert.AnError srv := NewServer(defaultConfig(), mock) - body, _ := json.Marshal(FaucetRequest{UserAddress: "0x742d35Cc6634C0532925a3b8c17d18fBE3b78890"}) + body, err := json.Marshal(FaucetRequest{UserAddress: "0x742d35Cc6634C0532925a3b8c17d18fBE3b78890"}) + require.NoError(t, err) req := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() @@ -174,7 +181,8 @@ func TestRequestTokens_TransferFailure(t *testing.T) { mock.transferErr = assert.AnError srv := NewServer(defaultConfig(), mock) - body, _ := json.Marshal(FaucetRequest{UserAddress: "0x742d35Cc6634C0532925a3b8c17d18fBE3b78890"}) + body, err := json.Marshal(FaucetRequest{UserAddress: "0x742d35Cc6634C0532925a3b8c17d18fBE3b78890"}) + require.NoError(t, err) req := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() @@ -196,7 +204,8 @@ func TestRateLimiting(t *testing.T) { srv := NewServer(cfg, mock) testAddress := common.HexToAddress("0x742D35CC6634c0532925a3B8c17D18fBe3b78890").Hex() - body, _ := json.Marshal(FaucetRequest{UserAddress: testAddress}) + body, err := json.Marshal(FaucetRequest{UserAddress: testAddress}) + require.NoError(t, err) // First request — should succeed req1 := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) @@ -224,7 +233,8 @@ func TestRateLimiting(t *testing.T) { srv := NewServer(cfg, mock) testAddress := common.HexToAddress("0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF").Hex() - body, _ := json.Marshal(FaucetRequest{UserAddress: testAddress}) + body, err := json.Marshal(FaucetRequest{UserAddress: testAddress}) + require.NoError(t, err) // First request fails at transfer but still consumes the rate-limit slot. req1 := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) diff --git a/faucet-app/server/main.go b/faucet-app/server/main.go index 3cfc229a1..7402244fe 100644 --- a/faucet-app/server/main.go +++ b/faucet-app/server/main.go @@ -2,16 +2,25 @@ package main import ( "fmt" + "net/url" "os" "os/signal" "syscall" - "faucet-server/internal/nitronode" - "faucet-server/internal/config" - "faucet-server/internal/logger" - "faucet-server/internal/server" + "github.com/layer-3/nitrolite/faucet-app/server/internal/config" + "github.com/layer-3/nitrolite/faucet-app/server/internal/logger" + "github.com/layer-3/nitrolite/faucet-app/server/internal/nitronode" + "github.com/layer-3/nitrolite/faucet-app/server/internal/server" ) +func redactURL(raw string) string { + u, err := url.Parse(raw) + if err != nil || u.Host == "" { + return "[invalid URL]" + } + return fmt.Sprintf("%s://%s", u.Scheme, u.Host) +} + func main() { cfg, err := config.Load() if err != nil { @@ -26,7 +35,7 @@ func main() { logger.Info("Starting Nitrolite Faucet Server") logger.Infof("Configuration loaded: Server port=%s, Nitronode URL=%s", - cfg.ServerPort, cfg.NitronodeURL) + cfg.ServerPort, redactURL(cfg.NitronodeURL)) client, err := nitronode.NewClient(cfg.OwnerPrivateKey, cfg.NitronodeURL, cfg.TokenSymbol, cfg.StandardTipAmountDecimal, cfg.MinTransferCount) if err != nil { diff --git a/go.mod b/go.mod index 2350609e0..7e1771147 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,15 @@ require ( cloud.google.com/go/kms v1.30.0 github.com/c-bata/go-prompt v0.2.6 github.com/ethereum/go-ethereum v1.17.2 + github.com/gin-gonic/gin v1.11.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 + github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/joho/godotenv v1.5.1 github.com/jsternberg/zap-logfmt v1.3.0 github.com/prometheus/client_golang v1.23.2 + github.com/shopspring/decimal v1.4.0 + github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.42.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 @@ -30,20 +34,43 @@ require ( cloud.google.com/go/iam v1.7.0 // indirect cloud.google.com/go/longrunning v0.9.0 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.4 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect github.com/googleapis/gax-go/v2 v2.22.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-tty v0.0.3 // indirect github.com/moby/moby/api v1.54.2 // indirect github.com/moby/moby/client v0.4.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/term v1.2.0-beta.2 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect github.com/stretchr/objx v0.5.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/mod v0.34.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.43.0 // indirect google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect @@ -81,7 +108,6 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect - github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.9.2 // indirect @@ -90,7 +116,7 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/jmoiron/sqlx v1.4.0 github.com/klauspost/compress v1.18.5 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -120,8 +146,6 @@ require ( github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/shirou/gopsutil/v4 v4.26.3 // indirect - github.com/shopspring/decimal v1.4.0 - github.com/sirupsen/logrus v1.9.4 // indirect github.com/supranational/blst v0.3.16 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect diff --git a/go.sum b/go.sum index 6b994e395..c0082ae6a 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -44,6 +48,8 @@ github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= @@ -118,10 +124,16 @@ github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeD github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -130,10 +142,22 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -150,6 +174,7 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -204,12 +229,14 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -218,6 +245,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= @@ -273,6 +302,11 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= @@ -283,6 +317,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= @@ -315,6 +351,10 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -338,10 +378,15 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= @@ -356,6 +401,10 @@ github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYI github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= @@ -380,6 +429,8 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= @@ -388,10 +439,14 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= @@ -409,7 +464,6 @@ golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= @@ -420,6 +474,8 @@ golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk= From 683804af96701fd8cebbe88f2a7f2a7d1b4fac35 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Fri, 22 May 2026 10:43:46 +0200 Subject: [PATCH 5/5] fix(faucet-app): fix dockerfile context, proxy validation, env example, gofmt --- faucet-app/server/.env.example | 4 +-- faucet-app/server/Dockerfile | 30 +++++++++---------- faucet-app/server/internal/config/config.go | 18 +++++++---- faucet-app/server/internal/server/server.go | 2 +- .../server/internal/server/server_test.go | 26 ++++++++-------- 5 files changed, 43 insertions(+), 37 deletions(-) diff --git a/faucet-app/server/.env.example b/faucet-app/server/.env.example index a12ef1ba7..7b33510b7 100644 --- a/faucet-app/server/.env.example +++ b/faucet-app/server/.env.example @@ -20,9 +20,9 @@ SERVER_PORT=8080 # 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) +# Cooldown between requests per wallet/IP (Go duration format) # Optional: default is 24h -COOLDOWN_PERIOD=your_cooldown_period_here (e.g., 24h, 1h, 30m) +# COOLDOWN_PERIOD=your_cooldown_period_here (e.g., 24h, 1h, 30m) # Nitronode WebSocket URL # REQUIRED: The WebSocket endpoint for your Nitronode instance diff --git a/faucet-app/server/Dockerfile b/faucet-app/server/Dockerfile index 41e6091cf..7209e87f2 100644 --- a/faucet-app/server/Dockerfile +++ b/faucet-app/server/Dockerfile @@ -1,33 +1,31 @@ -# Build stage FROM golang:1.25-alpine AS builder -WORKDIR /app +WORKDIR /build -# Copy go mod and sum files +# Copy root module files first (the replace directive in faucet-app/server points here) COPY go.mod go.sum ./ -# Download dependencies -RUN go mod download +# 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 source code +# Copy full source tree so the replace directive can resolve COPY . . -# Build the application -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o faucet-server main.go +# 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 . -# Production stage FROM alpine:3.23.3 -# Install ca-certificates for HTTPS requests RUN apk --no-cache add ca-certificates -WORKDIR /root/ +RUN addgroup -g 1001 -S faucet +RUN adduser -S faucet -u 1001 -G faucet -# Copy the binary from builder stage -COPY --from=builder /app/faucet-server . +USER faucet + +COPY --from=builder /build/bin/faucet-server /bin/faucet-server -# Expose port EXPOSE 8080 -# Run the binary -CMD ["./faucet-server"] +CMD ["faucet-server"] diff --git a/faucet-app/server/internal/config/config.go b/faucet-app/server/internal/config/config.go index c49ae2a85..f67ed7ab7 100644 --- a/faucet-app/server/internal/config/config.go +++ b/faucet-app/server/internal/config/config.go @@ -3,6 +3,7 @@ package config import ( "errors" "fmt" + "net" "os" "strings" "time" @@ -15,9 +16,9 @@ import ( 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)"` + 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)"` @@ -81,9 +82,16 @@ func (c *Config) Validate() error { if c.TrustedProxies != "" { for _, p := range strings.Split(c.TrustedProxies, ",") { - if trimmed := strings.TrimSpace(p); trimmed != "" { - c.TrustedProxyList = append(c.TrustedProxyList, trimmed) + 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) } } diff --git a/faucet-app/server/internal/server/server.go b/faucet-app/server/internal/server/server.go index 8214ac6f2..a56a07728 100644 --- a/faucet-app/server/internal/server/server.go +++ b/faucet-app/server/internal/server/server.go @@ -8,9 +8,9 @@ import ( "github.com/gin-gonic/gin" "github.com/shopspring/decimal" - "github.com/layer-3/nitrolite/faucet-app/server/internal/nitronode" "github.com/layer-3/nitrolite/faucet-app/server/internal/config" "github.com/layer-3/nitrolite/faucet-app/server/internal/logger" + "github.com/layer-3/nitrolite/faucet-app/server/internal/nitronode" ) // NitronodeClient is the interface the server uses to interact with Nitronode. diff --git a/faucet-app/server/internal/server/server_test.go b/faucet-app/server/internal/server/server_test.go index 524f49a0f..7a400b4e9 100644 --- a/faucet-app/server/internal/server/server_test.go +++ b/faucet-app/server/internal/server/server_test.go @@ -14,25 +14,25 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/layer-3/nitrolite/faucet-app/server/internal/nitronode" "github.com/layer-3/nitrolite/faucet-app/server/internal/config" "github.com/layer-3/nitrolite/faucet-app/server/internal/logger" + "github.com/layer-3/nitrolite/faucet-app/server/internal/nitronode" ) // mockNitronodeClient is a simple in-memory mock implementing NitronodeClient. type mockNitronodeClient struct { - ownerAddress string - connErr error - operationalErr error - transferResult *nitronode.TransferResult - transferErr error - capturedDest string - capturedAsset string - capturedAmount decimal.Decimal + ownerAddress string + connErr error + operationalErr error + transferResult *nitronode.TransferResult + transferErr error + capturedDest string + capturedAsset string + capturedAmount decimal.Decimal } -func (m *mockNitronodeClient) GetOwnerAddress() string { return m.ownerAddress } -func (m *mockNitronodeClient) EnsureConnected() error { return m.connErr } +func (m *mockNitronodeClient) GetOwnerAddress() string { return m.ownerAddress } +func (m *mockNitronodeClient) EnsureConnected() error { return m.connErr } func (m *mockNitronodeClient) EnsureOperational() error { return m.operationalErr } func (m *mockNitronodeClient) Transfer(dest, asset string, amount decimal.Decimal) (*nitronode.TransferResult, error) { m.capturedDest = dest @@ -205,7 +205,7 @@ func TestRateLimiting(t *testing.T) { testAddress := common.HexToAddress("0x742D35CC6634c0532925a3B8c17D18fBe3b78890").Hex() body, err := json.Marshal(FaucetRequest{UserAddress: testAddress}) - require.NoError(t, err) + require.NoError(t, err) // First request — should succeed req1 := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body)) @@ -234,7 +234,7 @@ func TestRateLimiting(t *testing.T) { testAddress := common.HexToAddress("0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF").Hex() body, err := json.Marshal(FaucetRequest{UserAddress: testAddress}) - require.NoError(t, err) + require.NoError(t, err) // First request fails at transfer but still consumes the rate-limit slot. req1 := httptest.NewRequest("POST", "/requestTokens", bytes.NewReader(body))