Compare commits

..

No commits in common. "feat/DEZON-80/user" and "main" have entirely different histories.

63 changed files with 16 additions and 3872 deletions

View File

@ -1,51 +0,0 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main cmd/api/main.go "
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

3
.gitignore vendored
View File

@ -1,3 +0,0 @@
config.yaml
bin
tmp

0
cmd/api/.gitkeep Normal file
View File

View File

@ -1,23 +0,0 @@
package main
import (
"backend/config"
"backend/internal/api/http"
"backend/internal/app"
"log"
)
func main() {
cfg, err := config.ReadStandard("config.yaml")
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
appContainer, err := app.NewAppContainer(cfg)
if err != nil {
log.Fatalf("Failed to initialize application: %v", err)
}
defer appContainer.Close()
http.Run(cfg.Server, appContainer)
}

0
cmd/worker/.gitkeep Normal file
View File

0
config/.gitkeep Normal file
View File

View File

@ -1,49 +0,0 @@
package config
type Config struct {
Server Server `mapstructure:"server"`
JWT JWT `mapstructure:"jwt"`
DB DB `mapstructure:"db"`
Redis Redis `mapstructure:"redis"`
KYCProvider KYC `mapstructure:"kyc"`
OTP OTP `mapstructure:"otp"`
Kavenegar SMSProvider `mapstructure:"kavenegar"`
}
type Server struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type DB struct {
User string `mapstructure:"user"`
Pass string `mapstructure:"pass"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
DBName string `mapstructure:"db_name"`
}
type Redis struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Pass string `mapstructure:"pass"`
}
type JWT struct {
TokenExpMinutes uint `mapstructure:"token_exp_minutes"`
RefreshTokenExpMinutes uint `mapstructure:"refresh_token_exp_minutes"`
TokenSecret string `mapstructure:"token_secret"`
}
type KYC struct {
APIKey string `mapstructure:"api_key"`
URL string `mapstructure:"url"`
}
type OTP struct {
CodeExpMinutes uint `mapstructure:"code_exp_minutes"`
}
type SMSProvider struct {
APIKey string `mapstructure:"api_key"`
Template string `mapstructure:"template"`
}

View File

@ -1,51 +0,0 @@
package config
import (
"path/filepath"
"github.com/spf13/viper"
)
func ReadGeneric[T any](cfgPath string) (T, error) {
var cfg T
fullAbsPath, err := absPath(cfgPath)
if err != nil {
return cfg, err
}
// configPath := filepath.Dir(fullAbsPath)
// viper.AddConfigPath(configPath)
// configType := strings.TrimPrefix(filepath.Ext(fullAbsPath), ".")
// viper.SetConfigType(configType)
// configFile := strings.TrimSuffix(filepath.Base(fullAbsPath), filepath.Ext(fullAbsPath))
// viper.SetConfigName(configFile)
viper.SetConfigFile(fullAbsPath)
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
return cfg, err
}
return cfg, viper.Unmarshal(&cfg)
}
func ReadStandard(cfgPath string) (Config, error) {
return ReadGeneric[Config](cfgPath)
}
func absPath(cfgPath string) (string, error) {
if !filepath.IsAbs(cfgPath) {
return filepath.Abs(cfgPath)
}
return cfgPath, nil
}
func MustReadStandard(configPath string) Config {
cfg, err := ReadStandard(configPath)
if err != nil {
panic(err)
}
return cfg
}

View File

@ -1,367 +0,0 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/auth/authenticate": {
"post": {
"description": "Authenticate user with wallet signature",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Authenticate user",
"parameters": [
{
"description": "Authentication Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.AuthenticateRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.AuthenticateResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/challenge": {
"post": {
"description": "Generate a challenge message for wallet authentication",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Generate authentication challenge",
"parameters": [
{
"description": "Challenge Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ChallengeRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.ChallengeResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/kyc": {
"post": {
"description": "Verify user KYC with national ID and birth date",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Verify user KYC",
"parameters": [
{
"description": "KYC Verify Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.KYCVerifyRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/otp": {
"post": {
"description": "Verify the provided OTP code for the phone number",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Verify OTP code",
"parameters": [
{
"description": "OTP Verify Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OTPVerifyRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.OTPVerifyResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
"dto.APIResponse": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"success": {
"type": "boolean"
}
}
},
"dto.AuthenticateRequest": {
"type": "object",
"required": [
"pubKey",
"signature"
],
"properties": {
"pubKey": {
"type": "string"
},
"signature": {
"type": "string"
}
}
},
"dto.AuthenticateResponse": {
"type": "object",
"properties": {
"authorizationToken": {
"type": "string"
},
"expiresAt": {
"type": "integer"
},
"refreshToken": {
"type": "string"
}
}
},
"dto.ChallengeRequest": {
"type": "object",
"required": [
"pubKey"
],
"properties": {
"pubKey": {
"type": "string"
}
}
},
"dto.ChallengeResponse": {
"type": "object",
"properties": {
"expiresAt": {
"type": "string"
},
"message": {
"type": "string"
},
"timeStamp": {
"type": "string"
}
}
},
"dto.KYCVerifyRequest": {
"type": "object",
"required": [
"birthDate",
"nationalId"
],
"properties": {
"birthDate": {
"type": "string"
},
"nationalId": {
"type": "string"
}
}
},
"dto.OTPProviderReq": {
"type": "object",
"properties": {
"receptor": {
"type": "string"
}
}
},
"dto.OTPProviderResponse": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
},
"dto.OTPVerifyRequest": {
"type": "object",
"required": [
"code",
"phone"
],
"properties": {
"code": {
"type": "string"
},
"phone": {
"type": "string"
}
}
},
"dto.OTPVerifyResponse": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "",
Host: "",
BasePath: "",
Schemes: []string{},
Title: "",
Description: "",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

View File

@ -1,338 +0,0 @@
{
"swagger": "2.0",
"info": {
"contact": {}
},
"paths": {
"/auth/authenticate": {
"post": {
"description": "Authenticate user with wallet signature",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Authenticate user",
"parameters": [
{
"description": "Authentication Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.AuthenticateRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.AuthenticateResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/challenge": {
"post": {
"description": "Generate a challenge message for wallet authentication",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Generate authentication challenge",
"parameters": [
{
"description": "Challenge Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ChallengeRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.ChallengeResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/kyc": {
"post": {
"description": "Verify user KYC with national ID and birth date",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Verify user KYC",
"parameters": [
{
"description": "KYC Verify Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.KYCVerifyRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/otp": {
"post": {
"description": "Verify the provided OTP code for the phone number",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Verify OTP code",
"parameters": [
{
"description": "OTP Verify Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OTPVerifyRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.OTPVerifyResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
"dto.APIResponse": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"success": {
"type": "boolean"
}
}
},
"dto.AuthenticateRequest": {
"type": "object",
"required": [
"pubKey",
"signature"
],
"properties": {
"pubKey": {
"type": "string"
},
"signature": {
"type": "string"
}
}
},
"dto.AuthenticateResponse": {
"type": "object",
"properties": {
"authorizationToken": {
"type": "string"
},
"expiresAt": {
"type": "integer"
},
"refreshToken": {
"type": "string"
}
}
},
"dto.ChallengeRequest": {
"type": "object",
"required": [
"pubKey"
],
"properties": {
"pubKey": {
"type": "string"
}
}
},
"dto.ChallengeResponse": {
"type": "object",
"properties": {
"expiresAt": {
"type": "string"
},
"message": {
"type": "string"
},
"timeStamp": {
"type": "string"
}
}
},
"dto.KYCVerifyRequest": {
"type": "object",
"required": [
"birthDate",
"nationalId"
],
"properties": {
"birthDate": {
"type": "string"
},
"nationalId": {
"type": "string"
}
}
},
"dto.OTPProviderReq": {
"type": "object",
"properties": {
"receptor": {
"type": "string"
}
}
},
"dto.OTPProviderResponse": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
},
"dto.OTPVerifyRequest": {
"type": "object",
"required": [
"code",
"phone"
],
"properties": {
"code": {
"type": "string"
},
"phone": {
"type": "string"
}
}
},
"dto.OTPVerifyResponse": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
}
}
}

View File

@ -1,219 +0,0 @@
definitions:
dto.APIResponse:
properties:
data: {}
message:
type: string
success:
type: boolean
type: object
dto.AuthenticateRequest:
properties:
pubKey:
type: string
signature:
type: string
required:
- pubKey
- signature
type: object
dto.AuthenticateResponse:
properties:
authorizationToken:
type: string
expiresAt:
type: integer
refreshToken:
type: string
type: object
dto.ChallengeRequest:
properties:
pubKey:
type: string
required:
- pubKey
type: object
dto.ChallengeResponse:
properties:
expiresAt:
type: string
message:
type: string
timeStamp:
type: string
type: object
dto.KYCVerifyRequest:
properties:
birthDate:
type: string
nationalId:
type: string
required:
- birthDate
- nationalId
type: object
dto.OTPProviderReq:
properties:
receptor:
type: string
type: object
dto.OTPProviderResponse:
properties:
message:
type: string
type: object
dto.OTPVerifyRequest:
properties:
code:
type: string
phone:
type: string
required:
- code
- phone
type: object
dto.OTPVerifyResponse:
properties:
message:
type: string
type: object
info:
contact: {}
paths:
/auth/authenticate:
post:
consumes:
- application/json
description: Authenticate user with wallet signature
parameters:
- description: Authentication Request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.AuthenticateRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.AuthenticateResponse'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
summary: Authenticate user
tags:
- auth
/auth/challenge:
post:
consumes:
- application/json
description: Generate a challenge message for wallet authentication
parameters:
- description: Challenge Request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.ChallengeRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.ChallengeResponse'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Generate authentication challenge
tags:
- auth
/auth/kyc:
post:
consumes:
- application/json
description: Verify user KYC with national ID and birth date
parameters:
- description: KYC Verify Request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.KYCVerifyRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.APIResponse'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Verify user KYC
tags:
- auth
/auth/otp:
post:
consumes:
- application/json
description: Verify the provided OTP code for the phone number
parameters:
- description: OTP Verify Request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.OTPVerifyRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.OTPVerifyResponse'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
summary: Verify OTP code
tags:
- auth
swagger: "2.0"

73
go.mod
View File

@ -1,81 +1,20 @@
module backend module boiler-plate
go 1.25.1 go 1.25.0
require ( require (
github.com/ethereum/go-ethereum v1.16.3
github.com/gofiber/adaptor/v2 v2.2.1
github.com/gofiber/fiber/v2 v2.52.9
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d
github.com/redis/go-redis/v9 v9.13.0
github.com/spf13/viper v1.21.0
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.6
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.5 gorm.io/gorm v1.30.2
) )
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/bits-and-blooms/bitset v1.20.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/consensys/gnark-crypto v0.18.0 // indirect
github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.6 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect golang.org/x/crypto v0.31.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect golang.org/x/sync v0.10.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect golang.org/x/text v0.21.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/supranational/blst v0.3.14 // indirect
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.35.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
) )

310
go.sum
View File

@ -1,138 +1,6 @@
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
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.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
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/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.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0=
github.com/consensys/gnark-crypto v0.18.0/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.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg/0E3OOdI=
github.com/crate-crypto/go-eth-kzg v1.3.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
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.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w=
github.com/ethereum/c-kzg-4844/v2 v2.1.0/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E=
github.com/ethereum/go-ethereum v1.16.3 h1:nDoBSrmsrPbrDIVLTkDQCy1U9KdHN+F2PzvMbDoS42Q=
github.com/ethereum/go-ethereum v1.16.3/go.mod h1:Lrsc6bt9Gm9RyvhfFK53vboCia8kpF9nv+2Ukntnl+8=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
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/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/go-ole/go-ole v1.2.5/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-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofiber/adaptor/v2 v2.2.1 h1:givE7iViQWlsTR4Jh7tB4iXzrlKBgiraB/yTdHs9Lv4=
github.com/gofiber/adaptor/v2 v2.2.1/go.mod h1:AhR16dEqs25W2FY/l8gSj1b51Azg5dtPDmm+pruNOrc=
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
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-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4=
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
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/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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@ -141,186 +9,28 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d h1:5yPyBSS28Nojbr7pAkiXADGj6VpTXx73o6SsprKbSoo=
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d/go.mod h1:CRhvvr4KNAyrg+ewrutOf+/QoHs7lztSoLjp+GqhYlA=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
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.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/redis/go-redis/v9 v9.13.0 h1:PpmlVykE0ODh8P43U0HqC+2NXHXwG+GUtQyz+MPKGRg=
github.com/redis/go-redis/v9 v9.13.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
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/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
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.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
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=
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/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
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/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s= gorm.io/gorm v1.30.2 h1:f7bevlVoVe4Byu3pmbWPVHnPsLoWaMjEb7/clyr9Ivs=
gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.30.2/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=

0
internal/api/.gitkeep Normal file
View File

View File

View File

@ -1,58 +0,0 @@
package dto
type ChallengeRequest struct {
PubKey string `json:"pubKey" validate:"required,eth_pubkey"`
}
type ChallengeResponse struct {
Message string `json:"message"`
TimeStamp string `json:"timeStamp"`
ExpiresAt string `json:"expiresAt"`
}
type AuthenticateRequest struct {
PubKey string `json:"pubKey" validate:"required,eth_pubkey"`
Signature string `json:"signature" validate:"required,eth_signature"`
}
type AuthenticateResponse struct {
AuthorizationToken string `json:"authorizationToken"`
RefreshToken string `json:"refreshToken"`
ExpiresAt int64 `json:"expiresAt"`
}
type RefreshTokenRequest struct {
RefreshToken string `json:"refreshToken" validate:"required,jwt"`
}
type OTPProviderReq struct {
Receptor string `json:"receptor"`
}
type OTPProviderResponse struct {
Message string `json:"message"`
}
type OTPVerifyRequest struct {
Phone string `json:"phone" validate:"required"`
Code string `json:"code" validate:"required"`
}
type OTPVerifyResponse struct {
Message string `json:"message"`
}
type KYCVerifyRequest struct {
NationalID string `json:"nationalId" validate:"required"`
BirthDate string `json:"birthDate" validate:"required"`
}
type KYCVerifyResponse struct {
Message string `json:"message"`
}
type APIResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}

View File

View File

@ -1,193 +0,0 @@
package http
import (
"backend/internal/api/dto"
"backend/internal/api/http/middlewares"
"backend/internal/usecase"
"github.com/gofiber/fiber/v2"
)
type AuthHandler struct {
authService usecase.AuthService
}
func NewAuthHandler(authService *usecase.AuthService) *AuthHandler {
return &AuthHandler{
authService: *authService,
}
}
// GenerateChallenge generates a challenge for authentication
// @Summary Generate authentication challenge
// @Description Generate a challenge message for wallet authentication
// @Tags auth
// @Accept json
// @Produce json
// @Param request body dto.ChallengeRequest true "Challenge Request"
// @Success 200 {object} dto.ChallengeResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/challenge [post]
func (h *AuthHandler) GenerateChallenge(c *fiber.Ctx) error {
var req dto.ChallengeRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "invalid request body",
})
}
challenge, err := h.authService.GenerateChallenge(c.Context(), req.PubKey)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(
dto.ChallengeResponse{
Message: challenge.Message,
TimeStamp: challenge.TimeStamp.String(),
ExpiresAt: challenge.ExpiresAt.String(),
},
)
}
// Authenticate authenticates a user with signed challenge
// @Summary Authenticate user
// @Description Authenticate user with wallet signature
// @Tags auth
// @Accept json
// @Produce json
// @Param request body dto.AuthenticateRequest true "Authentication Request"
// @Success 200 {object} dto.AuthenticateResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /auth/authenticate [post]
func (h *AuthHandler) Authenticate(c *fiber.Ctx) error {
var req dto.AuthenticateRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "invalid request body",
})
}
userToken, err := h.authService.Authenticate(
c.Context(),
req.PubKey,
req.Signature,
//TODO: add chainID to cfg
1,
c.IP(),
c.Get("User-Agent"),
)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(dto.AuthenticateResponse{
AuthorizationToken: userToken.AuthorizationToken,
RefreshToken: userToken.RefreshToken,
ExpiresAt: userToken.ExpiresAt,
})
}
// SendOTP sends OTP code to the provided phone number
// @Summary Send OTP code
// @Description Send OTP code to the provided phone number
// @Tags auth
// @Accept json
// @Produce json
// @Param request body dto.OTPProviderReq true "OTP Request"
// @Success 200 {object} dto.OTPProviderResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/otp [post]
func (h *AuthHandler) SendOTP(c *fiber.Ctx) error {
var req dto.OTPProviderReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "invalid request body",
})
}
_, err := h.authService.SendOTPCode(c.Context(), req.Receptor)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(dto.OTPProviderResponse{
Message: "OTP code sent successfully",
})
}
// VerifyOTP verifies the OTP code
// @Summary Verify OTP code
// @Description Verify the provided OTP code for the phone number
// @Tags auth
// @Accept json
// @Produce json
// @Param request body dto.OTPVerifyRequest true "OTP Verify Request"
// @Success 200 {object} dto.OTPVerifyResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /auth/otp [post]
func (h *AuthHandler) VerifyOTP(c *fiber.Ctx) error {
var req dto.OTPVerifyRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "invalid request body",
})
}
err := h.authService.VerifyOTP(c.Context(), req.Phone, req.Code)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(dto.OTPVerifyResponse{
Message: "OTP verified successfully",
})
}
// VerifyKYC verifies user KYC information
// @Summary Verify user KYC
// @Description Verify user KYC with national ID and birth date
// @Tags auth
// @Accept json
// @Produce json
// @Param request body dto.KYCVerifyRequest true "KYC Verify Request"
// @Success 200 {object} dto.APIResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/kyc [post]
func (h *AuthHandler) VerifyKYC(c *fiber.Ctx) error {
var req dto.KYCVerifyRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "invalid request body",
})
}
claims := middlewares.GetUserClaims(c)
err := h.authService.VerifyKYC(c.Context(), claims.UserID, req.NationalID, req.BirthDate)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(dto.APIResponse{
Success: true,
Message: "KYC verified successfully",
})
}
func (h *AuthHandler) HelloWorld(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"message": "Hello, World!",
})
}

View File

@ -1,37 +0,0 @@
package middlewares
import (
"backend/pkg/jwt"
"strings"
"github.com/gofiber/fiber/v2"
)
const userClaimsKey = "User-Claims"
func JWTAuthMiddleware(secret []byte) fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "missing or invalid Authorization header",
})
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := jwt.ParseToken(tokenString, secret)
if err != nil || claims == nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "invalid or expired token",
})
}
c.Locals(userClaimsKey, claims)
return c.Next()
}
}
func GetUserClaims(c *fiber.Ctx) *jwt.UserClaims {
claims, _ := c.Locals(userClaimsKey).(*jwt.UserClaims)
return claims
}

View File

@ -1,57 +0,0 @@
package http
import (
"backend/config"
"backend/docs"
httpHandlers "backend/internal/api/http/handlers"
"backend/internal/api/http/middlewares"
"backend/internal/app"
"fmt"
"log"
"github.com/gofiber/adaptor/v2"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
httpSwagger "github.com/swaggo/http-swagger"
)
func Run(cfg config.Server, app *app.AppContainer) {
fiberApp := fiber.New()
fiberApp.Use(cors.New())
// Serve static files (HTML, CSS, JS)
fiberApp.Static("/", "./static")
api := fiberApp.Group("/api")
// register routes here
registerPublicRoutes(api, app)
docs.SwaggerInfo.Host = fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
docs.SwaggerInfo.BasePath = "/api"
docs.SwaggerInfo.Schemes = []string{"http", "https"}
api.Get("/swagger/*", adaptor.HTTPHandler(httpSwagger.Handler()))
api.Get("/hello", middlewares.JWTAuthMiddleware([]byte("Secret")), func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
log.Printf("Server starting on %s:%d", cfg.Host, cfg.Port)
log.Fatal(fiberApp.Listen(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)))
}
func registerPublicRoutes(router fiber.Router, app *app.AppContainer) {
authgroup := router.Group("/auth")
authService := app.AuthService()
authHandler := httpHandlers.NewAuthHandler(&authService)
// Register auth routes
authgroup.Post("/challenge", authHandler.GenerateChallenge)
authgroup.Post("/authenticate", authHandler.Authenticate)
authgroup.Post("/otp", authHandler.SendOTP)
authgroup.Post("/verify", authHandler.VerifyOTP)
// add JWT middleware for KYC
authgroup.Post("/kyc", middlewares.JWTAuthMiddleware([]byte("Secret")), authHandler.VerifyKYC)
}

View File

View File

@ -1,89 +0,0 @@
package app
import (
"backend/config"
"backend/internal/repository/cache"
"backend/internal/repository/storage"
"backend/internal/repository/storage/types"
"backend/internal/usecase"
cachePackage "backend/pkg/cache"
"backend/pkg/postgres"
"backend/pkg/sms"
"backend/pkg/zohal"
"fmt"
"log"
"gorm.io/gorm"
)
type AppContainer struct {
cfg config.Config
dbConn *gorm.DB
redis *cachePackage.Redis
authService usecase.AuthService
}
func NewAppContainer(cfg config.Config) (*AppContainer, error) {
dbOptions := postgres.DBConnOptions{
User: cfg.DB.User,
Pass: cfg.DB.Pass,
Host: cfg.DB.Host,
Port: uint(cfg.DB.Port),
DBName: cfg.DB.DBName,
}
dbConn, err := postgres.NewPsqlGormConnection(dbOptions)
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
dbConn.AutoMigrate(&types.User{})
redis, err := cachePackage.NewRedis(cfg.Redis)
if err != nil {
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
}
userRepo := storage.NewUserRepository(dbConn)
sessionRepo := cache.NewSessionRepository(redis)
challengeRepo := cache.NewChallengeRepository(redis)
zohal := zohal.NewZohal(cfg.KYCProvider)
otpRepo := cache.NewOTPRepository(redis)
smsProvider := sms.NewKavenegar(cfg.Kavenegar)
authService := usecase.NewAuthService(
userRepo,
sessionRepo,
challengeRepo,
30,
otpRepo,
zohal,
smsProvider,
cfg,
)
return &AppContainer{
cfg: cfg,
dbConn: dbConn,
redis: redis,
authService: authService,
}, nil
}
func (app *AppContainer) AuthService() usecase.AuthService {
return app.authService
}
func (app *AppContainer) Close() {
if app.redis != nil {
if err := app.redis.Close(); err != nil {
log.Printf("Error closing Redis connection: %v", err)
}
}
if app.dbConn != nil {
if sqlDB, err := app.dbConn.DB(); err == nil {
if err := sqlDB.Close(); err != nil {
log.Printf("Error closing database connection: %v", err)
}
}
}
}

0
internal/domain/.gitkeep Normal file
View File

View File

View File

View File

@ -1,34 +0,0 @@
package domain
import (
"backend/pkg/util"
"context"
"time"
"github.com/google/uuid"
)
type OTPRepo interface {
Create(ctx context.Context, phone string, otp *OTP) error
Get(ctx context.Context, phone string) (*OTP, error)
Delete(ctx context.Context, phone string) error
}
type OTP struct {
ID uuid.UUID
Code string
Phone string
ExpiresAt time.Time
}
func NewOTP(phone string) *OTP {
return &OTP{
ID: uuid.New(),
Code: util.GenOTPCode(),
Phone: phone,
}
}
func (o *OTP) IsExpired() bool {
return time.Now().After(o.ExpiresAt)
}

View File

View File

@ -1,38 +0,0 @@
package domain
import (
"context"
"time"
"github.com/google/uuid"
)
type SessionRepo interface {
Create(ctx context.Context, session *UserSession) error
GetByID(ctx context.Context, id uuid.UUID) (*UserSession, error)
Delete(ctx context.Context, id uuid.UUID) error
GetUserSessions(ctx context.Context, userID uuid.UUID) ([]UserSession, error)
}
// TODO: add IP and UserAgent
type UserSession struct {
ID uuid.UUID
UserID uuid.UUID
User User
WalletID string
ExpiresAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
func NewSession(userID uuid.UUID, walletID string, expiresAt time.Time) *UserSession {
return &UserSession{
ID: uuid.New(),
UserID: userID,
WalletID: walletID,
ExpiresAt: expiresAt,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
}

View File

@ -1,162 +0,0 @@
package domain
import (
"context"
"errors"
"fmt"
"strings"
"time"
"net/mail"
validate "backend/pkg/validate/common"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/google/uuid"
)
var (
ErrUserNotFound = errors.New("user not found")
ErrEmailInvalid = errors.New("email is invalid")
)
type UserRepo interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id uuid.UUID) (*User, error)
GetByPhone(ctx context.Context, phone string) (*User, error)
GetByPubKey(ctx context.Context, pubKey string) (*User, error)
GetOrCreateUserByPubKey(ctx context.Context, pubKey string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id uuid.UUID) error
}
type ChallengeRepo interface {
Create(ctx context.Context, pubKey string, challenge *Challenge) error
GetByPubKey(ctx context.Context, pubKey string) (*Challenge, error)
Delete(ctx context.Context, pubKey string) error
}
type KYCLevel int8
const (
KYCLevel0 KYCLevel = iota
KYCLevel1
KYCLevel2
)
type User struct {
ID uuid.UUID
PubKey string
Name string
KYCLevel KYCLevel
LastName string
PhoneNumber string
Email *string
NationalID string
BirthDate *time.Time
LastLogin *time.Time
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
func NewUser(pubKey, name, lastName, phoneNumber, email, nationalID string) (*User, error) {
_, err := validate.IsValidPhone(phoneNumber)
if err != nil {
return nil, err
}
_, err = validate.IsValidNationalID(nationalID)
if err != nil {
return nil, err
}
_, err = validate.IsValidPublicKey(pubKey)
if err != nil {
return nil, err
}
return &User{
ID: uuid.New(),
PubKey: pubKey,
Name: name,
LastName: lastName,
PhoneNumber: phoneNumber,
NationalID: nationalID,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}, nil
}
func (u *User) UpdateLastLogin() {
now := time.Now().UTC()
u.LastLogin = &now
u.UpdatedAt = now
}
func (u *User) UpdateInfo(email string) {
addr, err := mail.ParseAddress(email)
if err == nil {
u.Email = &addr.Address
}
u.UpdatedAt = time.Now().UTC()
}
func (u *User) UpdateKYCLevel(level KYCLevel) {
u.KYCLevel = level
u.UpdatedAt = time.Now().UTC()
}
type Challenge struct {
Message string
TimeStamp time.Time
ExpiresAt time.Time
}
func (c *Challenge) IsExpired() bool {
return time.Now().After(c.ExpiresAt)
}
func (c *Challenge) Verify(address string, signedMsg string) (bool, error) {
if c.IsExpired() {
return false, errors.New("challenge has expired")
}
signature, err := hexutil.Decode(signedMsg)
if err != nil {
return false, fmt.Errorf("decode signature: %w", err)
}
challengeBytes := []byte(c.Message)
return c.VerifySignedBytes(address, challengeBytes, signature)
}
func (c *Challenge) VerifySignedBytes(expectedAddress string, msg []byte, sig []byte) (bool, error) {
msgHash := accounts.TextHash(msg)
if len(sig) != 65 {
return false, fmt.Errorf("invalid signature length: expected 65 bytes, got %d", len(sig))
}
if sig[crypto.RecoveryIDOffset] == 27 || sig[crypto.RecoveryIDOffset] == 28 {
sig[crypto.RecoveryIDOffset] -= 27
}
recovered, err := crypto.SigToPub(msgHash, sig)
if err != nil {
return false, fmt.Errorf("failed to recover public key: %w", err)
}
recoveredAddr := crypto.PubkeyToAddress(*recovered)
if !common.IsHexAddress(expectedAddress) {
return false, fmt.Errorf("invalid Ethereum address format: %s", expectedAddress)
}
expectedAddr := common.HexToAddress(expectedAddress)
return strings.EqualFold(expectedAddr.Hex(), recoveredAddr.Hex()), nil
}

View File

0
internal/repository/cache/.gitkeep vendored Normal file
View File

View File

@ -1,80 +0,0 @@
package cache
import (
"backend/internal/domain"
"backend/pkg/cache"
"context"
"encoding/json"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type ChallengeRepository struct {
redis *cache.Redis
}
func NewChallengeRepository(redis *cache.Redis) domain.ChallengeRepo {
return &ChallengeRepository{
redis: redis,
}
}
func (r *ChallengeRepository) Create(ctx context.Context, pubKey string, challenge *domain.Challenge) error {
key := r.getChallengeKey(pubKey)
challengeData, err := json.Marshal(challenge)
if err != nil {
return fmt.Errorf("failed to marshal challenge: %w", err)
}
ttl := time.Until(challenge.ExpiresAt)
if ttl <= 0 {
return fmt.Errorf("challenge already expired")
}
if err := r.redis.Client().Set(ctx, key, challengeData, ttl).Err(); err != nil {
return fmt.Errorf("failed to store challenge in Redis: %w", err)
}
return nil
}
func (r *ChallengeRepository) GetByPubKey(ctx context.Context, pubKey string) (*domain.Challenge, error) {
key := r.getChallengeKey(pubKey)
result, err := r.redis.Client().Get(ctx, key).Result()
if err != nil {
if err == redis.Nil {
return nil, fmt.Errorf("challenge not found")
}
return nil, fmt.Errorf("failed to get challenge from Redis: %w", err)
}
var challenge domain.Challenge
if err := json.Unmarshal([]byte(result), &challenge); err != nil {
return nil, fmt.Errorf("failed to unmarshal challenge: %w", err)
}
if challenge.IsExpired() {
r.Delete(ctx, pubKey)
return nil, fmt.Errorf("challenge expired")
}
return &challenge, nil
}
func (r *ChallengeRepository) Delete(ctx context.Context, pubKey string) error {
key := r.getChallengeKey(pubKey)
if err := r.redis.Client().Del(ctx, key).Err(); err != nil {
return fmt.Errorf("failed to delete challenge from Redis: %w", err)
}
return nil
}
func (r *ChallengeRepository) getChallengeKey(pubKey string) string {
return fmt.Sprintf("challenge:%s", pubKey)
}

View File

@ -1,79 +0,0 @@
package cache
import (
"backend/internal/domain"
"backend/pkg/cache"
"context"
"encoding/json"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type OTPRepository struct {
redis *cache.Redis
}
func NewOTPRepository(redis *cache.Redis) domain.OTPRepo {
return &OTPRepository{
redis: redis,
}
}
func (r *OTPRepository) Create(ctx context.Context, phone string, otp *domain.OTP) error {
key := r.getOTPKey(phone)
otpData, err := json.Marshal(otp)
if err != nil {
return fmt.Errorf("failed to marshal otp: %w", err)
}
ttl := time.Until(otp.ExpiresAt)
if ttl <= 0 {
return fmt.Errorf("OTP expired already")
}
if err := r.redis.Client().Set(ctx, key, otpData, ttl).Err(); err != nil {
return fmt.Errorf("failed to store otp in redis: %w", err)
}
return nil
}
func (r *OTPRepository) Delete(ctx context.Context, phone string) error {
key := r.getOTPKey(phone)
if err := r.redis.Client().Del(ctx, key).Err(); err != nil {
return fmt.Errorf("failed to del otp from redis")
}
return nil
}
func (r *OTPRepository) Get(ctx context.Context, phone string) (*domain.OTP, error) {
key := r.getOTPKey(phone)
result, err := r.redis.Client().Get(ctx, key).Result()
if err != nil {
if err == redis.Nil {
return nil, fmt.Errorf("otp code not found")
}
return nil, err
}
var otp domain.OTP
if err := json.Unmarshal([]byte(result), &otp); err != nil {
return nil, err
}
if otp.IsExpired() {
r.Delete(ctx, phone)
return nil, fmt.Errorf("otp is expired")
}
return &otp, nil
}
func (r *OTPRepository) getOTPKey(phone string) string {
return fmt.Sprintf("challenge:%s", phone)
}

View File

@ -1,135 +0,0 @@
package cache
import (
"backend/internal/domain"
"backend/pkg/cache"
"context"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
)
type SessionRepository struct {
redis *cache.Redis
}
func NewSessionRepository(redis *cache.Redis) domain.SessionRepo {
return &SessionRepository{
redis: redis,
}
}
func (r *SessionRepository) Create(ctx context.Context, session *domain.UserSession) error {
key := r.getSessionKey(session.ID)
sessionData, err := json.Marshal(session)
if err != nil {
return fmt.Errorf("failed to marshal session: %w", err)
}
ttl := time.Until(session.ExpiresAt)
if ttl <= 0 {
return fmt.Errorf("session already expired")
}
if err := r.redis.Client().Set(ctx, key, sessionData, ttl).Err(); err != nil {
return fmt.Errorf("failed to store session in Redis: %w", err)
}
userSessionsKey := r.getUserSessionsKey(session.UserID)
if err := r.redis.Client().SAdd(ctx, userSessionsKey, session.ID.String()).Err(); err != nil {
return fmt.Errorf("failed to add session to user sessions set: %w", err)
}
if err := r.redis.Client().Expire(ctx, userSessionsKey, ttl).Err(); err != nil {
return fmt.Errorf("failed to set TTL for user sessions set: %w", err)
}
return nil
}
func (r *SessionRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.UserSession, error) {
key := r.getSessionKey(id)
result, err := r.redis.Client().Get(ctx, key).Result()
if err != nil {
if err == redis.Nil {
return nil, fmt.Errorf("session not found")
}
return nil, fmt.Errorf("failed to get session from Redis: %w", err)
}
var session domain.UserSession
if err := json.Unmarshal([]byte(result), &session); err != nil {
return nil, fmt.Errorf("failed to unmarshal session: %w", err)
}
if time.Now().After(session.ExpiresAt) {
r.Delete(ctx, id)
return nil, fmt.Errorf("session expired")
}
return &session, nil
}
func (r *SessionRepository) Delete(ctx context.Context, id uuid.UUID) error {
key := r.getSessionKey(id)
session, err := r.GetByID(ctx, id)
if err != nil {
return nil
}
if err := r.redis.Client().Del(ctx, key).Err(); err != nil {
return fmt.Errorf("failed to delete session from Redis: %w", err)
}
userSessionsKey := r.getUserSessionsKey(session.UserID)
if err := r.redis.Client().SRem(ctx, userSessionsKey, id.String()).Err(); err != nil {
return fmt.Errorf("failed to remove session from user sessions set: %w", err)
}
return nil
}
func (r *SessionRepository) GetUserSessions(ctx context.Context, userID uuid.UUID) ([]domain.UserSession, error) {
userSessionsKey := r.getUserSessionsKey(userID)
sessionIDs, err := r.redis.Client().SMembers(ctx, userSessionsKey).Result()
if err != nil {
if err == redis.Nil {
return []domain.UserSession{}, nil
}
return nil, fmt.Errorf("failed to get user sessions from Redis: %w", err)
}
var sessions []domain.UserSession
for _, sessionIDStr := range sessionIDs {
sessionID, err := uuid.Parse(sessionIDStr)
if err != nil {
r.redis.Client().SRem(ctx, userSessionsKey, sessionIDStr)
continue
}
session, err := r.GetByID(ctx, sessionID)
if err != nil {
r.redis.Client().SRem(ctx, userSessionsKey, sessionIDStr)
continue
}
sessions = append(sessions, *session)
}
return sessions, nil
}
func (r *SessionRepository) getSessionKey(sessionID uuid.UUID) string {
return fmt.Sprintf("session:%s", sessionID.String())
}
func (r *SessionRepository) getUserSessionsKey(userID uuid.UUID) string {
return fmt.Sprintf("user_sessions:%s", userID.String())
}

0
internal/repository/external/.gitkeep vendored Normal file
View File

View File

View File

@ -1,22 +0,0 @@
package types
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Base struct {
ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
func (b *Base) BeforeCreate(tx *gorm.DB) (err error) {
if b.ID == uuid.Nil {
b.ID = uuid.New()
}
return
}

View File

@ -1,58 +0,0 @@
package types
import (
"backend/internal/domain"
"time"
)
type User struct {
Base
PubKey string
Name string
LastName string
KYCLevel int
//TODO: add NFT Embedded struct
Phone string
NationalID string
LastLogin *time.Time
Email *string
BirthDate *time.Time
}
func CastUserToStorage(u domain.User) *User {
return &User{
Base: Base{
ID: u.ID,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
DeletedAt: u.DeletedAt,
},
PubKey: u.PubKey,
Name: u.Name,
LastName: u.LastName,
KYCLevel: int(u.KYCLevel),
Phone: u.PhoneNumber,
NationalID: u.NationalID,
Email: u.Email,
BirthDate: u.BirthDate,
LastLogin: u.LastLogin,
}
}
func CastUserToDomain(u User) *domain.User {
return &domain.User{
ID: u.ID,
PubKey: u.PubKey,
Name: u.Name,
LastName: u.LastName,
KYCLevel: domain.KYCLevel(u.KYCLevel),
PhoneNumber: u.Phone,
NationalID: u.NationalID,
Email: u.Email,
BirthDate: u.BirthDate,
LastLogin: u.LastLogin,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
DeletedAt: u.DeletedAt,
}
}

View File

@ -1,81 +0,0 @@
package storage
import (
"backend/internal/domain"
"backend/internal/repository/storage/types"
"context"
"github.com/google/uuid"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) domain.UserRepo {
return &UserRepository{
db: db,
}
}
func (r *UserRepository) Create(ctx context.Context, user *domain.User) error {
model := types.CastUserToStorage(*user)
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
return err
}
*user = *types.CastUserToDomain(*model)
return nil
}
func (r *UserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
var user types.User
if err := r.db.WithContext(ctx).First(&user, "id = ?", id).Error; err != nil {
return nil, err
}
return types.CastUserToDomain(user), nil
}
func (r *UserRepository) GetByPhone(ctx context.Context, phone string) (*domain.User, error) {
var user types.User
if err := r.db.WithContext(ctx).First(&user, "phone = ?", phone).Error; err != nil {
return nil, err
}
return types.CastUserToDomain(user), nil
}
func (r *UserRepository) Update(ctx context.Context, user *domain.User) error {
userModel := types.CastUserToStorage(*user)
return r.db.WithContext(ctx).Save(&userModel).Error
}
func (r *UserRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&types.User{}, "id = ?", id).Error
}
func (r *UserRepository) GetByPubKey(ctx context.Context, pubKey string) (*domain.User, error) {
var user types.User
if err := r.db.WithContext(ctx).First(&user, "pub_key = ?", pubKey).Error; err != nil {
return nil, err
}
return types.CastUserToDomain(user), nil
}
func (r *UserRepository) GetOrCreateUserByPubKey(ctx context.Context, pubKey string) (*domain.User, error) {
var user types.User
err := r.db.WithContext(ctx).First(&user, "pub_key = ?", pubKey).Error
if err == nil {
return types.CastUserToDomain(user), nil
}
if err != gorm.ErrRecordNotFound {
return nil, err
}
user = types.User{
PubKey: pubKey,
}
if err := r.db.WithContext(ctx).Create(&user).Error; err != nil {
return nil, err
}
return types.CastUserToDomain(user), nil
}

View File

View File

@ -1,237 +0,0 @@
package usecase
import (
"backend/config"
"backend/internal/domain"
"backend/pkg/jwt"
"backend/pkg/sms"
"backend/pkg/validate/common"
"backend/pkg/zohal"
"context"
"errors"
"fmt"
"time"
jwt2 "github.com/golang-jwt/jwt/v5"
"github.com/kavenegar/kavenegar-go"
"github.com/google/uuid"
)
type AuthService interface {
GenerateChallenge(ctx context.Context, pubKey string) (*domain.Challenge, error)
Authenticate(ctx context.Context, pubKey string, signature string, chainID uint, ipAddress, userAgent string) (*UserToken, error)
SendOTPCode(ctx context.Context, phone string) (string, error)
VerifyOTP(ctx context.Context, phone, code string) error
VerifyKYC(ctx context.Context, userID, nationalID, birthDate string) error
}
type authService struct {
userRepo domain.UserRepo
sessionRepo domain.SessionRepo
challengeRepo domain.ChallengeRepo
otpRepo domain.OTPRepo
zohal *zohal.Zohal
smsProvider *sms.Kavenegar
challengeExp uint
cfg config.Config
}
type UserToken struct {
AuthorizationToken string
RefreshToken string
ExpiresAt int64
}
func NewAuthService(
userRepo domain.UserRepo,
sessionRepo domain.SessionRepo,
challengeRepo domain.ChallengeRepo,
challengeExp uint,
otpRepo domain.OTPRepo,
zohal *zohal.Zohal,
smsProvider *sms.Kavenegar,
cfg config.Config,
) AuthService {
return &authService{
userRepo: userRepo,
sessionRepo: sessionRepo,
challengeRepo: challengeRepo,
otpRepo: otpRepo,
challengeExp: challengeExp,
zohal: zohal,
smsProvider: smsProvider,
cfg: cfg,
}
}
func (s *authService) GenerateChallenge(ctx context.Context, pubKey string) (*domain.Challenge, error) {
_, err := common.IsValidPublicKey(pubKey)
if err != nil {
return nil, err
}
challenge := &domain.Challenge{
Message: uuid.New().String(),
TimeStamp: time.Now().UTC(),
ExpiresAt: time.Now().Add(time.Duration(s.challengeExp) * time.Minute),
}
// Save challenge to Redis
err = s.challengeRepo.Create(ctx, pubKey, challenge)
if err != nil {
return nil, fmt.Errorf("failed to save challenge: %w", err)
}
return challenge, nil
}
func (s *authService) Authenticate(ctx context.Context, pubKey string, signature string, chainID uint, ipAddress, userAgent string) (*UserToken, error) {
_, err := common.IsValidPublicKey(pubKey)
if err != nil {
return nil, fmt.Errorf("invalid public key: %w", err)
}
// Retrieve challenge from Redis
challenge, err := s.challengeRepo.GetByPubKey(ctx, pubKey)
if err != nil {
return nil, fmt.Errorf("failed to retrieve challenge: %w", err)
}
isValid, err := challenge.Verify(pubKey, signature)
if err != nil {
return nil, fmt.Errorf("signature verification failed: %w", err)
}
if !isValid {
return nil, errors.New("invalid signature")
}
// Delete the challenge after successful verification to prevent replay attacks
err = s.challengeRepo.Delete(ctx, pubKey)
if err != nil {
return nil, fmt.Errorf("failed to delete challenge: %w", err)
}
user, err := s.userRepo.GetOrCreateUserByPubKey(ctx, pubKey)
if err != nil {
return nil, fmt.Errorf("failed to get or create user: %w", err)
}
user.UpdateLastLogin()
if err := s.userRepo.Update(ctx, user); err != nil {
return nil, fmt.Errorf("failed to update user: %w", err)
}
expiresAt := time.Now().Add(time.Duration(s.cfg.JWT.TokenExpMinutes) * time.Minute)
session := domain.NewSession(user.ID, user.PubKey, expiresAt)
err = s.sessionRepo.Create(ctx, session)
if err != nil {
return nil, fmt.Errorf("failed to create session: %w", err)
}
claims := &jwt.UserClaims{
UserID: user.ID.String(),
KYCLevel: int(user.KYCLevel),
Sections: []string{},
}
claims.ExpiresAt = jwt2.NewNumericDate(expiresAt)
claims.IssuedAt = jwt2.NewNumericDate(time.Now())
authToken, err := jwt.CreateToken([]byte(s.cfg.JWT.TokenSecret), claims)
if err != nil {
return nil, err
}
refreshExpiresAt := time.Now().Add(time.Duration(s.cfg.JWT.RefreshTokenExpMinutes) * time.Minute)
refreshClaims := &jwt.UserClaims{
UserID: user.ID.String(),
KYCLevel: int(user.KYCLevel),
Sections: []string{"refresh"},
}
refreshClaims.ExpiresAt = jwt2.NewNumericDate(refreshExpiresAt)
refreshClaims.IssuedAt = jwt2.NewNumericDate(time.Now())
refreshToken, err := jwt.CreateToken([]byte(s.cfg.JWT.TokenSecret), refreshClaims)
if err != nil {
return nil, fmt.Errorf("failed to create refresh token: %w", err)
}
return &UserToken{
AuthorizationToken: authToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt.Unix(),
}, nil
}
func (s *authService) VerifyKYC(ctx context.Context, userID, nationalID, birthDate string) error {
uID, err := uuid.Parse(userID)
if err != nil {
return fmt.Errorf("invalid user ID format: %w", err)
}
user, err := s.userRepo.GetByID(ctx, uID)
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
shahkarResp, err := s.zohal.Shahkar(ctx, user.PhoneNumber, user.NationalID)
if err != nil {
return fmt.Errorf("shahkar verification failed: %w", err)
}
identityResp, err := s.zohal.GetPerson(ctx, nationalID, birthDate)
if err != nil {
return fmt.Errorf("identity verification failed: %w", err)
}
if shahkarResp.StatusCode == 200 && identityResp.StatusCode == 200 {
user.UpdateKYCLevel(domain.KYCLevel1)
err = s.userRepo.Update(ctx, user)
if err != nil {
return fmt.Errorf("failed to update user KYC level: %w", err)
}
return nil
}
return fmt.Errorf("KYC verification failed - shahkar status: %d, identity status: %d",
shahkarResp.StatusCode, identityResp.StatusCode)
}
func (s *authService) SendOTPCode(ctx context.Context, phone string) (string, error) {
otp := domain.NewOTP(phone)
if err := s.otpRepo.Create(ctx, phone, otp); err != nil {
return "", err
}
otpMsg := &sms.OTPMsg{
Receptor: otp.Phone,
Token: otp.Code,
Template: s.cfg.Kavenegar.Template,
// TODO: make sure when use VerfiyLookup Params
Params: kavenegar.VerifyLookupParam{},
}
err := s.smsProvider.OTP(otpMsg)
if err != nil {
return "", err
}
return otp.Code, err
}
func (s *authService) VerifyOTP(ctx context.Context, phone, code string) error {
otp, err := s.otpRepo.Get(ctx, phone)
if err != nil {
return err
}
if otp == nil {
return errors.New("otp code not found or expired")
}
if code != otp.Code {
return errors.New("otp code is not valid")
}
return nil
}

36
pkg/cache/redis.go vendored
View File

@ -1,36 +0,0 @@
package cache
import (
"backend/config"
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
type Redis struct {
client *redis.Client
}
func NewRedis(cfg config.Redis) (*Redis, error) {
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
Password: cfg.Pass,
DB: 0,
})
ctx := context.Background()
if err := rdb.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
}
return &Redis{client: rdb}, nil
}
func (r *Redis) Client() *redis.Client {
return r.client
}
func (r *Redis) Close() error {
return r.client.Close()
}

0
pkg/errors/.gitkeep Normal file
View File

View File

@ -1,7 +0,0 @@
package errors
var (
ErrPhoneInvalid = NewAppError(400, "Invalid phone number", ErrorTypeValidation, nil)
ErrNationalIDInvalid = NewAppError(400, "Invalid national ID", ErrorTypeValidation, nil)
ErrWalletInvalid = NewAppError(400, "Invalid wallet address", ErrorTypeValidation, nil)
)

View File

@ -1,51 +0,0 @@
package errors
import (
"errors"
)
type ErrorType string
const (
ErrorTypeValidation ErrorType = "VALIDATION_ERROR"
ErrorTypeAuth ErrorType = "AUTH_ERROR"
)
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
Type ErrorType `json:"type"`
Cause error `json:"_"`
}
func NewAppError(code int, msg string, typ ErrorType, cause error) *AppError {
return &AppError{
Code: code,
Message: msg,
Type: typ,
Cause: cause,
}
}
func (e *AppError) Error() string {
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Cause
}
func (e *AppError) Is(target error) bool {
if t, ok := target.(*AppError); ok {
return e.Type == t.Type
}
return errors.Is(e.Cause, target)
}
func (e *AppError) As(target interface{}) bool {
if t, ok := target.(**AppError); ok {
*t = e
return true
}
return errors.As(e.Cause, target)
}

View File

@ -1,253 +0,0 @@
package ethereum
import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
)
type EthereumClient interface {
GetEthClient() *ethclient.Client
GetRPCClient() *rpc.Client
Close()
IsConnected(ctx context.Context) error
NewBoundContract(address common.Address, abi string) (*bind.BoundContract, error)
// Creates options for sending transactions to the blockchain.
CreateTransactOpts(ctx context.Context, privateKey string, value *big.Int) (*bind.TransactOpts, error)
// Creates options for reading from the blockchain.
CreateCallOpts(ctx context.Context, blockNumber *big.Int) *bind.CallOpts
// Waits for a transaction to be mined and returns the receipt.
WaitForTransaction(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
}
type Geth struct {
ethClient *ethclient.Client
rpcClient *rpc.Client
url string
}
type GethConfig struct {
URL string
RequestTimeout time.Duration
}
func DefaultGethConfig() *GethConfig {
return &GethConfig{
RequestTimeout: 30 * time.Second,
}
}
func NewGeth(clientURL string) (*Geth, error) {
if clientURL == "" {
return nil, fmt.Errorf("client URL cannot be empty")
}
rpcClient, err := rpc.Dial(clientURL)
if err != nil {
return nil, fmt.Errorf("failed to connect to RPC client at %s: %w", clientURL, err)
}
ethClient := ethclient.NewClient(rpcClient)
geth := &Geth{
ethClient: ethClient,
rpcClient: rpcClient,
url: clientURL,
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := geth.IsConnected(ctx); err != nil {
geth.Close()
return nil, fmt.Errorf("connection test failed: %w", err)
}
return geth, nil
}
func NewGethWithConfig(config *GethConfig) (*Geth, error) {
if config == nil {
return nil, fmt.Errorf("config cannot be nil")
}
if config.URL == "" {
return nil, fmt.Errorf("client URL cannot be empty")
}
rpcClient, err := rpc.Dial(config.URL)
if err != nil {
return nil, fmt.Errorf("failed to connect to RPC client at %s: %w", config.URL, err)
}
ethClient := ethclient.NewClient(rpcClient)
geth := &Geth{
ethClient: ethClient,
rpcClient: rpcClient,
url: config.URL,
}
ctx, cancel := context.WithTimeout(context.Background(), config.RequestTimeout)
defer cancel()
if err := geth.IsConnected(ctx); err != nil {
geth.Close()
return nil, fmt.Errorf("connection test failed: %w", err)
}
return geth, nil
}
func (g *Geth) GetEthClient() *ethclient.Client {
return g.ethClient
}
func (g *Geth) GetRPCClient() *rpc.Client {
return g.rpcClient
}
func (g *Geth) Close() {
if g.rpcClient != nil {
g.rpcClient.Close()
}
}
func (g *Geth) IsConnected(ctx context.Context) error {
if g.ethClient == nil {
return fmt.Errorf("ethereum client is not initialized")
}
_, err := g.ethClient.NetworkID(ctx)
if err != nil {
return fmt.Errorf("failed to get network ID: %w", err)
}
return nil
}
func (g *Geth) GetNetworkID(ctx context.Context) (uint64, error) {
if g.ethClient == nil {
return 0, fmt.Errorf("ethereum client is not initialized")
}
networkID, err := g.ethClient.NetworkID(ctx)
if err != nil {
return 0, fmt.Errorf("failed to get network ID: %w", err)
}
return networkID.Uint64(), nil
}
func (g *Geth) GetChainID(ctx context.Context) (uint64, error) {
if g.ethClient == nil {
return 0, fmt.Errorf("ethereum client is not initialized")
}
chainID, err := g.ethClient.ChainID(ctx)
if err != nil {
return 0, fmt.Errorf("failed to get chain ID: %w", err)
}
return chainID.Uint64(), nil
}
func (g *Geth) GetBlockNumber(ctx context.Context) (uint64, error) {
if g.ethClient == nil {
return 0, fmt.Errorf("ethereum client is not initialized")
}
blockNumber, err := g.ethClient.BlockNumber(ctx)
if err != nil {
return 0, fmt.Errorf("failed to get block number: %w", err)
}
return blockNumber, nil
}
func (g *Geth) GetURL() string {
return g.url
}
func (g *Geth) NewBoundContract(address common.Address, abiJSON string) (*bind.BoundContract, error) {
if g.ethClient == nil {
return nil, fmt.Errorf("ethereum client is not initialized")
}
parsedABI, err := parseABI(abiJSON)
if err != nil {
return nil, fmt.Errorf("failed to parse ABI: %w", err)
}
return bind.NewBoundContract(address, parsedABI, g.ethClient, g.ethClient, g.ethClient), nil
}
func (g *Geth) CreateTransactOpts(ctx context.Context, privateKeyHex string, value *big.Int) (*bind.TransactOpts, error) {
if g.ethClient == nil {
return nil, fmt.Errorf("ethereum client is not initialized")
}
privateKey, err := parsePrivateKey(privateKeyHex)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
chainID, err := g.ethClient.ChainID(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get chain ID: %w", err)
}
opts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
if err != nil {
return nil, fmt.Errorf("failed to create transactor: %w", err)
}
opts.Context = ctx
if value != nil {
opts.Value = value
}
return opts, nil
}
func (g *Geth) CreateCallOpts(ctx context.Context, blockNumber *big.Int) *bind.CallOpts {
return &bind.CallOpts{
Context: ctx,
BlockNumber: blockNumber,
}
}
func (g *Geth) WaitForTransaction(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
if g.ethClient == nil {
return nil, fmt.Errorf("ethereum client is not initialized")
}
receipt, err := bind.WaitMined(ctx, g.ethClient, &types.Transaction{})
if err != nil {
return nil, fmt.Errorf("failed to wait for transaction: %w", err)
}
return receipt, nil
}
func parseABI(abiJSON string) (abi.ABI, error) {
return abi.JSON(strings.NewReader(abiJSON))
}
func parsePrivateKey(privateKeyHex string) (*ecdsa.PrivateKey, error) {
privateKeyHex = strings.TrimPrefix(privateKeyHex, "0x")
return crypto.HexToECDSA(privateKeyHex)
}

View File

@ -1,10 +0,0 @@
package jwt
import jwt2 "github.com/golang-jwt/jwt/v5"
type UserClaims struct {
jwt2.RegisteredClaims
UserID string
KYCLevel int
Sections []string
}

View File

@ -1,37 +0,0 @@
package jwt
import (
"errors"
jwt2 "github.com/golang-jwt/jwt/v5"
)
const UserClaimKey = "User-Claims"
func CreateToken(secret []byte, claims *UserClaims) (string, error) {
return jwt2.NewWithClaims(jwt2.SigningMethodHS512, claims).SignedString(secret)
}
func ParseToken(tokenString string, secret []byte) (*UserClaims, error) {
token, err := jwt2.ParseWithClaims(tokenString, &UserClaims{}, func(t *jwt2.Token) (interface{}, error) {
return secret, nil
})
var claim *UserClaims
if token.Claims != nil {
cc, ok := token.Claims.(*UserClaims)
if ok {
claim = cc
}
}
if err != nil {
return claim, err
}
if !token.Valid {
return claim, errors.New("token is not valid")
}
return claim, nil
}

0
pkg/logger/.gitkeep Normal file
View File

View File

@ -1,57 +0,0 @@
package logger
import (
"context"
"log/slog"
"os"
"github.com/google/uuid"
)
type LogLevel string
type contextKey string
const TraceIDKey contextKey = "trace_id"
type Logger struct {
*slog.Logger
}
func NewLogger(level LogLevel) *Logger {
return &Logger{
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo, // default log level
})),
}
}
func GenerateTraceID() string {
return uuid.New().String()
}
func WithTraceID(ctx context.Context) context.Context {
if ctx.Value(TraceIDKey) == nil {
return context.WithValue(ctx, TraceIDKey, GenerateTraceID())
}
return ctx
}
func GetTraceID(ctx context.Context) string {
if traceID, ok := ctx.Value(TraceIDKey).(string); ok {
return traceID
}
return ""
}
func (l *Logger) Info(ctx context.Context, msg string, args ...any) {
traceID := GetTraceID(ctx)
if traceID != "" {
args = append(args, slog.String(string(TraceIDKey), traceID))
}
l.Logger.Info(msg, args...)
}
func (l *Logger) ErrorWithoutContext(msg string, args ...any) {
l.Logger.Error(msg, args...)
}

0
pkg/rabbit/.gitkeep Normal file
View File

View File

@ -1,32 +0,0 @@
package sms
import (
"backend/config"
"github.com/kavenegar/kavenegar-go"
)
type Kavenegar struct {
*kavenegar.Kavenegar
}
type OTPMsg struct {
Receptor string
Token string
Template string
Params kavenegar.VerifyLookupParam
}
func NewKavenegar(cfg config.SMSProvider) *Kavenegar {
instance := kavenegar.New(cfg.APIKey)
return &Kavenegar{
Kavenegar: instance,
}
}
func (k *Kavenegar) OTP(otpMsg *OTPMsg) error {
if _, err := k.Verify.Lookup(otpMsg.Receptor, otpMsg.Template, otpMsg.Token, &otpMsg.Params); err != nil {
return err
}
return nil
}

View File

@ -1,57 +0,0 @@
package util
import (
"bytes"
"context"
"encoding/json"
"net/http"
"sync"
)
type HttpClient struct {
client *http.Client
once sync.Once
}
func NewHttpClient() *HttpClient {
return &HttpClient{
http.DefaultClient,
sync.Once{},
}
}
func (h *HttpClient) getClient() *http.Client {
h.once.Do(func() {
h.client = &http.Client{}
})
return h.client
}
func (h *HttpClient) HttpRequest(ctx context.Context, method, url string, body interface{}, headers map[string]string) (*http.Response, error) {
payload, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(payload))
if err != nil {
return nil, err
}
for headerTitle, headerValue := range headers {
req.Header.Set(headerTitle, headerValue)
}
resp, err := h.getClient().Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return &http.Response{
Status: "200 ok",
StatusCode: 200,
Body: resp.Body,
}, nil
}

View File

@ -1,14 +0,0 @@
package util
import (
"math/rand"
"strconv"
"time"
)
func GenOTPCode() string {
newRand := rand.New(rand.NewSource(time.Now().UnixNano()))
minNum := 100000
maxNum := 999999
return strconv.Itoa(newRand.Intn(maxNum-minNum+1) + minNum)
}

0
pkg/utils/.gitkeep Normal file
View File

View File

@ -1,64 +0,0 @@
package common
import (
"backend/pkg/errors"
"regexp"
"strconv"
"strings"
"github.com/ethereum/go-ethereum/common"
)
func IsValidPhone(value string) (bool, error) {
re := regexp.MustCompile(`^(?:0|\+98|0098)(\d{10})$`)
result := re.FindStringSubmatch(value)
if len(result) > 0 {
return true, nil
}
return false, errors.ErrPhoneInvalid
}
func IsValidNationalID(value string) (bool, error) {
valueCount := len(value)
if valueCount < 8 {
return false, errors.ErrNationalIDInvalid
}
if valueCount >= 8 && valueCount < 10 {
value = strings.Repeat("0", 10-valueCount) + value
}
var valueSlices []uint8
for _, s := range value {
i, err := strconv.ParseInt(string(s), 10, 8)
if err != nil {
return false, errors.ErrNationalIDInvalid
}
valueSlices = append(valueSlices, uint8(i))
}
s := calculateNationalIDNumbers(&valueSlices)
s %= 11
l := valueSlices[len(value)-1]
if (s < 2 && s != int(l)) || (s >= 2 && int(l) != 11-s) {
return false, errors.ErrNationalIDInvalid
}
return true, nil
}
func calculateNationalIDNumbers(valueSlices *[]uint8) (sum int) {
var i uint8
for i = 10; i >= 2; i-- {
sum += int(i * (*valueSlices)[10-i])
}
return sum
}
func IsValidPublicKey(value string) (bool, error) {
if !common.IsHexAddress(value) {
return false, errors.ErrWalletInvalid
}
return true, nil
}

View File

@ -1,50 +0,0 @@
package zohal
type respMatched struct {
Matched bool `json:"matched"`
}
type personIdentity struct {
Matched bool `json:"matched"`
LastName string `json:"last_name"`
FirstName string `json:"first_name"`
FatherName string `json:"father_name"`
NationalCode string `json:"national_code"`
IsDead bool `json:"is_dead"`
Alive bool `json:"alive"`
}
type respBody[T any] struct {
Data T `json:"data"`
Message string `json:"message"`
}
type zohalResp[T any] struct {
StatusCode int `json:"status_code"`
Body respBody[T] `json:"response_body"`
}
type (
// https://service.zohal.io/api/v0/services/inquiry/shahkar
ZohalShahkarReq struct {
Phone string `json:"mobile"`
NationalID string `json:"national_code"`
}
ZohalShahkarResp struct {
zohalResp[respMatched]
}
// https://service.zohal.io/api/v0/services/inquiry/national_identity_inquiry
ZohalIdentityReq struct {
NationalID string `json:"national_code"`
BirthDate string `json:"birth_date"`
}
ZohalIdentityResp struct {
zohalResp[personIdentity]
}
// https://service.zohal.io/api/v0/services/inquiry/check_iban_with_national_code
ZohalIbanReq struct {
NationalID string `json:"national_code"`
IBAN string `json:"IBAN"`
BirthDate string `json:"birth_date"`
}
ZohalIbanResp struct {
zohalResp[respMatched]
}
)

View File

@ -1,105 +0,0 @@
package zohal
import (
"backend/config"
"backend/pkg/util"
"backend/pkg/validate/common"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
)
type Zohal struct {
cfg config.KYC
}
func NewZohal(cfg config.KYC) *Zohal {
return &Zohal{
cfg: cfg,
}
}
// check Phone and NationalID
func (z *Zohal) Shahkar(ctx context.Context, phone, nationalID string) (ZohalShahkarResp, error) {
var req ZohalShahkarReq
var resp ZohalShahkarResp
ok, err := common.IsValidPhone(phone)
if !ok {
return resp, err
}
ok, err = common.IsValidNationalID(nationalID)
if !ok {
return resp, err
}
header := make(map[string]string)
header["Content-Type"] = "application/json"
header["Accept"] = "application/json"
header["Authorization"] = fmt.Sprintf("Bearer %s", z.cfg.APIKey)
req.Phone = phone
req.NationalID = nationalID
u, err := url.Parse(z.cfg.URL)
if err != nil {
return resp, err
}
u = u.JoinPath("inquiry", "shahkar")
client := util.NewHttpClient()
clientResp, err := client.HttpRequest(ctx, http.MethodPost, u.String(), req, header)
if err != nil {
return resp, err
}
bodyBytes, err := io.ReadAll(clientResp.Body)
if err != nil {
return resp, err
}
err = json.Unmarshal(bodyBytes, &resp)
if err != nil {
return resp, err
}
return resp, err
}
func (z *Zohal) GetPerson(ctx context.Context, nationalID, birthDate string) (ZohalIdentityResp, error) {
var req ZohalIdentityReq
var resp ZohalIdentityResp
header := make(map[string]string)
header["Content-Type"] = "application/json"
header["Accept"] = "application/json"
header["Authorization"] = fmt.Sprintf("Bearer %s", z.cfg.APIKey)
req.BirthDate = birthDate
req.NationalID = nationalID
u, err := url.Parse(z.cfg.URL)
if err != nil {
return resp, err
}
u = u.JoinPath("inquiry", "shahkar")
client := util.NewHttpClient()
clientResp, err := client.HttpRequest(ctx, http.MethodPost, u.String(), req, header)
if err != nil {
return resp, err
}
bodyBytes, err := io.ReadAll(clientResp.Body)
if err != nil {
return resp, err
}
err = json.Unmarshal(bodyBytes, &resp)
if err != nil {
return resp, err
}
return resp, err
}

View File

@ -1,26 +0,0 @@
server:
host: "localhost"
port: 8080
db:
user: "postgres"
pass: "postgres"
host: "localhost"
port: 5432
db_name: "dezone"
redis:
host: "localhost"
port: 6379
pass: ""
jwt:
token_exp_minutes: 60
refresh_token_exp_minutes: 10080
token_secret: "Secret"
RPC:
url: "exmaple.com"
otp:
code_exp_minutes: 2

View File

@ -1,245 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web3 Login process with MetaMask</title>
<script src="https://cdn.tailwindcss.com"></script>
<script
src="https://cdn.jsdelivr.net/npm/ethers@5.6.4/dist/ethers.umd.min.js"
type="application/javascript"
></script>
<script>
function web3_check_metamask() {
if (!window.ethereum) {
console.error(
"It seems that the MetaMask extension is not detected. Please install MetaMask first.",
);
alert(
"It seems that the MetaMask extension is not detected. Please install MetaMask first.",
);
return false;
} else {
alert("✅ MetaMask extension has been detected!");
return true;
}
}
async function web3_metamask_hash(pubKey) {
const z = await (
await fetch("http://127.0.0.1:8080/api/auth/challenge", {
body: `{ "pubKey": "${pubKey}"\n}`,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
method: "POST",
})
).json();
return z.message;
}
async function login_api(pubKey, sig) {
res = await (await fetch("http://127.0.0.1:8080/api/auth/authenticate", {
body: JSON.stringify({ pubKey: pubKey, signature: sig }),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
method: "POST",
})).json();
updateStatus(JSON.stringify(res), "text-green-600");
response = await (await fetch("http://127.0.0.1:8080/api/hello", {
headers: {
Authorization: `Bearer ${res.authorizationToken}`,
Accept: "application/json",
"Content-Type": "application/json",
},
method: "GET",
})).json();
console.log(response);
}
async function web3_metamask_login() {
// Check first if the user has the MetaMask installed
if (web3_check_metamask()) {
console.log("Initate Login Process");
updateStatus("Initiating login process...", "text-blue-600");
// Get the Ethereum provider
const provider = new ethers.providers.Web3Provider(window.ethereum);
// Get Ethereum accounts
await provider.send("eth_requestAccounts", []);
console.log("Connected!!");
updateStatus("Connected to MetaMask!", "text-green-600");
// Get the User Ethereum address
const address = await provider.getSigner().getAddress();
console.log(address);
document.getElementById("address").textContent = address;
// Get custom message from input or use hashed string
const customMessage = document
.getElementById("messageInput")
.value.trim();
const messageToSign = customMessage || await web3_metamask_hash(address);
console.log("Message to sign: " + messageToSign);
// Request the user to sign it
updateStatus(
"Please sign the message in MetaMask...",
"text-orange-600",
);
const signature = await provider
.getSigner()
.signMessage(messageToSign);
// Got the signature
console.log("The signature: " + signature);
document.getElementById("signature").textContent = signature;
updateStatus("Message signed successfully!", "text-green-600");
// TODO
// you can then send the signature to the webserver for further processing and verification
console.log({address, signature})
await login_api(address, signature);
}
}
function updateStatus(message, className) {
const statusEl = document.getElementById("status");
statusEl.textContent = message;
statusEl.className = `text-sm font-medium ${className}`;
}
</script>
</head>
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-2xl">
<div class="bg-white rounded-xl shadow-lg p-8">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-800 mb-2">
Web3 MetaMask Login
</h1>
<p class="text-gray-600">
Connect your wallet and sign messages securely
</p>
</div>
<div class="space-y-6">
<!-- MetaMask Detection Section -->
<div class="bg-gray-50 rounded-lg p-6">
<h2 class="text-lg font-semibold text-gray-700 mb-3">
1. MetaMask Detection
</h2>
<button
onclick="web3_check_metamask();"
class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition duration-200 ease-in-out transform hover:scale-105"
>
Detect MetaMask
</button>
</div>
<!-- Message Input Section -->
<div class="bg-gray-50 rounded-lg p-6">
<h2 class="text-lg font-semibold text-gray-700 mb-3">
2. Custom Message (Optional)
</h2>
<div class="mb-4">
<label
for="messageInput"
class="block text-sm font-medium text-gray-600 mb-2"
>
Enter a custom message to sign (leave empty for random hash):
</label>
<input
type="text"
id="messageInput"
placeholder="Enter your custom message here..."
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200"
/>
</div>
</div>
<!-- Login Section -->
<div class="bg-gray-50 rounded-lg p-6">
<h2 class="text-lg font-semibold text-gray-700 mb-3">
3. Wallet Login
</h2>
<button
onclick="web3_metamask_login();"
class="bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white font-medium py-3 px-8 rounded-lg transition duration-200 ease-in-out transform hover:scale-105 shadow-md"
>
🦊 Login with MetaMask
</button>
</div>
<!-- Status Section -->
<div class="bg-white border border-gray-200 rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-700 mb-3">Status</h3>
<div id="status" class="text-sm text-gray-500">
Ready to connect...
</div>
</div>
<!-- Results Section -->
<div class="bg-white border border-gray-200 rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-700 mb-4">
Connection Details
</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1"
>Wallet Address:</label
>
<div
id="address"
class="bg-gray-50 p-3 rounded border text-sm font-mono break-all text-gray-700"
>
Not connected
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1"
>Message Signature:</label
>
<div
id="signature"
class="bg-gray-50 p-3 rounded border text-sm font-mono break-all text-gray-700 max-h-32 overflow-y-auto"
>
No signature yet
</div>
</div>
</div>
</div>
<!-- Info Section -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg
class="h-5 w-5 text-blue-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"
></path>
</svg>
</div>
<div class="ml-3">
<h4 class="text-sm font-medium text-blue-800">How it works</h4>
<p class="text-sm text-blue-700 mt-1">
First detect MetaMask, then optionally enter a custom message,
and finally connect your wallet to sign the message securely.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>