Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 50 additions & 25 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,36 +1,61 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# Global ignores
.DS_Store
*.pem
*.log
*.swp

# testing
/coverage
# Node.js (React - Next.js)
client/node_modules
client/.pnp
client.pnp.js
client/.yarn/install-state.gz

# next.js
/.next/
/out/
# Testing
client/coverage

# production
/build
# Next.js
client/.next/
client/out/

# misc
.DS_Store
*.pem
# Production build
client/build

# debug
# Debugging logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local
# Local environment variables
client/.env*
client/.env*.local

# Vercel deployment
client/.vercel

# TypeScript
client/*.tsbuildinfo
client/next-env.d.ts

# Golang
api/*.exe
api/*.exe~
api/*.dll
api/*.so
api/*.dylib

# Test binaries
api/*.test

# Coverage output
api/*.out

# Go workspace files
api/go.work
api/go.work.sum

# vercel
.vercel
# Environment variables
api/.env
api/.env*.local

# typescript
*.tsbuildinfo
next-env.d.ts
# Vendor directory (optional, remove the comment below to include it)
# api/vendor/
14 changes: 14 additions & 0 deletions api/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.git
.env
*.md
Dockerfile
docker-compose.yml
# Except what's needed:
!cmd/
!pkg/
!internal/
!migrations/
!*.go
!go.mod
!go.sum
.DS_Store
34 changes: 34 additions & 0 deletions api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
FROM golang:1.24.1-alpine AS builder

# Install build dependencies
WORKDIR /app

# Copy go mod and sum files
COPY go.mod go.sum ./

# Download dependencies
RUN go mod tidy && go mod download

# Copy source code
COPY . .

# Build the API server binary
RUN go build -v -o /solana-faucet-api ./cmd/main.go

# Build the migration tool binary
RUN go build -v -o /migrate-tool ./cmd/migrate/main.go

# Final Image
FROM alpine:latest

WORKDIR /app

# Copy the compiled binaries from build
COPY --from=builder /solana-faucet-api /solana-faucet-api
COPY --from=builder /migrate-tool /migrate-tool

# Copy migration files (IMPORTANT!)
COPY --from=builder /app/cmd/migrate/migrations /app/cmd/migrate/migrations

# Run command
CMD ["/solana-faucet-api"]
23 changes: 23 additions & 0 deletions api/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
build:
@go build -o bin/solana-faucet-api cmd/main.go

test:
@go test -v ./...

run: build
@GIN_MODE=release ./bin/solana-faucet-api

migration:
@migrate create -ext sql -dir db/migrations $(filter-out $@,$(MAKECMDGOALS))

migrate-up:
@go run db/migrate.go up || (echo "Migration failed. Check if the database is dirty."; exit 1)

migrate-force:
@go run db/migrate.go force $V || (echo "Failed to force migration. Ensure the version is correct."; exit 1)

migrate-down:
@go run db/migrate.go down

migrate-status:
@go run db/migrate.go version
Binary file added api/bin/solana-faucet-api
Binary file not shown.
45 changes: 45 additions & 0 deletions api/cmd/api/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package api

import (
"database/sql"
"log"
"solana-faucet-api/services/faucet"

// "solana-faucet-api/service/faucet"

"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)

type APIServer struct {
addr string
db *sql.DB
}

func NewAPIServer(addr string, db *sql.DB) *APIServer {
return &APIServer{
addr: addr,
db: db,
}
}

func (s *APIServer) Run() error {
r := gin.Default()

// CORS Middleware (Replaces handlers.CORS)
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"X-Requested-With", "Content-Type", "Authorization"},
AllowCredentials: true,
}))

// Register routes
api := r.Group("/api/v1")
faucetStore := faucet.NewStore(s.db)
faucetHandler := faucet.NewHandler(faucetStore)
faucetHandler.RegisterRoutes(api)

log.Println("Listening on", s.addr)
return r.Run(s.addr)
}
42 changes: 42 additions & 0 deletions api/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"database/sql"
"log"
"solana-faucet-api/cmd/api"
"solana-faucet-api/configs"
"solana-faucet-api/db"

"github.com/go-sql-driver/mysql"
)

func main() {
// Initialize MySQL Storage
database, err := db.NewMySQLStorage(mysql.Config{
User: configs.Envs.DBUser,
Passwd: configs.Envs.DBPassword,
Addr: configs.Envs.DBAddress,
DBName: configs.Envs.DBName,
Net: "tcp",
AllowNativePasswords: true,
ParseTime: true,
})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}

initStorage(database.DB)

// Initialize and start the API server
server := api.NewAPIServer(":8080", database.DB)
if err := server.Run(); err != nil {
log.Fatal("Server error:", err)
}
}

func initStorage(db *sql.DB) {
if err := db.Ping(); err != nil {
log.Fatal("Database connection failed:", err)
}
log.Println("DB-server: Successfully connected!")
}
54 changes: 54 additions & 0 deletions api/cmd/migrate/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"log"
"os"
"solana-faucet-api/configs"
"solana-faucet-api/db"

mysqlCfg "github.com/go-sql-driver/mysql"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/mysql"
_ "github.com/golang-migrate/migrate/v4/source/file"
)

func main() {
database, err := db.NewMySQLStorage(mysqlCfg.Config{
User: configs.Envs.DBUser,
Passwd: configs.Envs.DBPassword,
Addr: configs.Envs.DBAddress,
DBName: configs.Envs.DBName,
Net: "tcp",
AllowNativePasswords: true,
ParseTime: true,
})
if err != nil {
log.Fatal(err)
}

driver, err := mysql.WithInstance(database.DB, &mysql.Config{})
if err != nil {
log.Fatal(err)
}

m, err := migrate.NewWithDatabaseInstance(
"file://cmd/migrate/migrations",
"mysql",
driver,
)
if err != nil {
log.Fatal(err)
}

cmd := os.Args[len(os.Args)-1]
if cmd == "up" {
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
log.Fatal(err)
}
}
if cmd == "down" {
if err := m.Down(); err != nil && err != migrate.ErrNoChange {
log.Fatal(err)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS faucet_requests;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE faucet_requests (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
ip_address VARCHAR(45) NOT NULL,
wallet_address VARCHAR(64) NOT NULL,
lamports BIGINT UNSIGNED NOT NULL,
tx_hash VARCHAR(88),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
77 changes: 77 additions & 0 deletions api/configs/envs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package configs

import (
"fmt"
"os"
"strconv"

"github.com/lpernett/godotenv"
)

type Config struct {
PublicHost string
Port string
DBUser string
DBPassword string
DBAddress string
DBName string
JWTSecret string
JWTExpirationInSeconds int64
FundingWalletPrivateKey string
FundingWalletPublicKey string
SOLTransactionLamports uint64
RecaptchaSecretKey string
}

var Envs = initConfig()

func initConfig() Config {
godotenv.Load()

return Config{
PublicHost: getEnv("PUBLIC_HOST", "http:"),
Port: getEnv("PORT", "8080"),
DBUser: getEnv("DB_USER", "root"),
DBPassword: getEnv("DB_PASSWORD", ""),
DBAddress: fmt.Sprintf("%s:%s", getEnv("DB_HOST", "127.0.0.1"), getEnv("DB_PORT", "3306")),
DBName: getEnv("DB_NAME", "solana-faucet-api"),
JWTSecret: getEnv("JWT_SECRET", "not-secret-anymore?"),
JWTExpirationInSeconds: getEnvAsInt("JWT_EXP", 3600*24*7),
FundingWalletPrivateKey: getEnv("FUNDING_WALLET_PRIVATE_KEY", "............."),
FundingWalletPublicKey: getEnv("FUNDING_WALLET_PUBLIC_KEY", "............."),
SOLTransactionLamports: getEnvAsUint64("SOL_TRANSACTION_LAMPORTS", 100000), // Default to 100,000 lamports (0.0001 SOL)
RecaptchaSecretKey: getEnv("RECAPTCHA_SECRET_KEY", "not-secret-anymore?"),
}
}

func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}

return fallback
}

func getEnvAsInt(key string, fallback int64) int64 {
if value, ok := os.LookupEnv(key); ok {
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return fallback
}

return i
}

return fallback
}

func getEnvAsUint64(key string, fallback uint64) uint64 {
if value, ok := os.LookupEnv(key); ok {
u, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return fallback
}
return u
}
return fallback
}
Loading