feat: explorer API, wallet, CCIP scripts, and config refresh
- Backend REST/gateway/track routes, analytics, Blockscout proxy paths. - Frontend wallet and liquidity surfaces; MetaMask token list alignment. - Deployment docs, verification scripts, address inventory updates. Check: go build ./... under backend/ (pass). Made-with: Cursor
This commit is contained in:
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
@@ -14,6 +16,13 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWalletAuthStorageNotInitialized = errors.New("wallet authentication storage is not initialized; run migration 0010_track_schema")
|
||||
ErrWalletNonceNotFoundOrExpired = errors.New("nonce not found or expired")
|
||||
ErrWalletNonceExpired = errors.New("nonce expired")
|
||||
ErrWalletNonceInvalid = errors.New("invalid nonce")
|
||||
)
|
||||
|
||||
// WalletAuth handles wallet-based authentication
|
||||
type WalletAuth struct {
|
||||
db *pgxpool.Pool
|
||||
@@ -28,6 +37,10 @@ func NewWalletAuth(db *pgxpool.Pool, jwtSecret []byte) *WalletAuth {
|
||||
}
|
||||
}
|
||||
|
||||
func isMissingWalletNonceTableError(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), `relation "wallet_nonces" does not exist`)
|
||||
}
|
||||
|
||||
// NonceRequest represents a nonce request
|
||||
type NonceRequest struct {
|
||||
Address string `json:"address"`
|
||||
@@ -84,6 +97,9 @@ func (w *WalletAuth) GenerateNonce(ctx context.Context, address string) (*NonceR
|
||||
`
|
||||
_, err := w.db.Exec(ctx, query, normalizedAddr, nonce, expiresAt)
|
||||
if err != nil {
|
||||
if isMissingWalletNonceTableError(err) {
|
||||
return nil, ErrWalletAuthStorageNotInitialized
|
||||
}
|
||||
return nil, fmt.Errorf("failed to store nonce: %w", err)
|
||||
}
|
||||
|
||||
@@ -110,22 +126,25 @@ func (w *WalletAuth) AuthenticateWallet(ctx context.Context, req *WalletAuthRequ
|
||||
query := `SELECT nonce, expires_at FROM wallet_nonces WHERE address = $1`
|
||||
err := w.db.QueryRow(ctx, query, normalizedAddr).Scan(&storedNonce, &expiresAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("nonce not found or expired")
|
||||
if isMissingWalletNonceTableError(err) {
|
||||
return nil, ErrWalletAuthStorageNotInitialized
|
||||
}
|
||||
return nil, ErrWalletNonceNotFoundOrExpired
|
||||
}
|
||||
|
||||
if time.Now().After(expiresAt) {
|
||||
return nil, fmt.Errorf("nonce expired")
|
||||
return nil, ErrWalletNonceExpired
|
||||
}
|
||||
|
||||
if storedNonce != req.Nonce {
|
||||
return nil, fmt.Errorf("invalid nonce")
|
||||
return nil, ErrWalletNonceInvalid
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
message := fmt.Sprintf("Sign this message to authenticate with SolaceScanScout Explorer.\n\nNonce: %s", req.Nonce)
|
||||
messageHash := accounts.TextHash([]byte(message))
|
||||
|
||||
sigBytes, err := hex.DecodeString(req.Signature[2:]) // Remove 0x prefix
|
||||
sigBytes, err := decodeWalletSignature(req.Signature)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid signature format: %w", err)
|
||||
}
|
||||
@@ -241,9 +260,45 @@ func (w *WalletAuth) ValidateJWT(tokenString string) (string, int, error) {
|
||||
}
|
||||
|
||||
track := int(trackFloat)
|
||||
if w.db == nil {
|
||||
return address, track, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
currentTrack, err := w.getUserTrack(ctx, address)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("failed to resolve current track: %w", err)
|
||||
}
|
||||
if currentTrack < track {
|
||||
track = currentTrack
|
||||
}
|
||||
|
||||
return address, track, nil
|
||||
}
|
||||
|
||||
func decodeWalletSignature(signature string) ([]byte, error) {
|
||||
if len(signature) < 2 || !strings.EqualFold(signature[:2], "0x") {
|
||||
return nil, fmt.Errorf("signature must start with 0x")
|
||||
}
|
||||
|
||||
raw := signature[2:]
|
||||
if len(raw) != 130 {
|
||||
return nil, fmt.Errorf("invalid signature length")
|
||||
}
|
||||
|
||||
sigBytes, err := hex.DecodeString(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(sigBytes) != 65 {
|
||||
return nil, fmt.Errorf("invalid signature length")
|
||||
}
|
||||
|
||||
return sigBytes, nil
|
||||
}
|
||||
|
||||
// getPermissionsForTrack returns permissions for a track level
|
||||
func getPermissionsForTrack(track int) []string {
|
||||
permissions := []string{
|
||||
|
||||
28
backend/auth/wallet_auth_test.go
Normal file
28
backend/auth/wallet_auth_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDecodeWalletSignatureRejectsMalformedValues(t *testing.T) {
|
||||
_, err := decodeWalletSignature("deadbeef")
|
||||
require.ErrorContains(t, err, "signature must start with 0x")
|
||||
|
||||
_, err = decodeWalletSignature("0x1234")
|
||||
require.ErrorContains(t, err, "invalid signature length")
|
||||
}
|
||||
|
||||
func TestValidateJWTReturnsClaimsWhenDBUnavailable(t *testing.T) {
|
||||
secret := []byte("test-secret")
|
||||
auth := NewWalletAuth(nil, secret)
|
||||
|
||||
token, _, err := auth.generateJWT("0x4A666F96fC8764181194447A7dFdb7d471b301C8", 4)
|
||||
require.NoError(t, err)
|
||||
|
||||
address, track, err := auth.ValidateJWT(token)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "0x4A666F96fC8764181194447A7dFdb7d471b301C8", address)
|
||||
require.Equal(t, 4, track)
|
||||
}
|
||||
Reference in New Issue
Block a user