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:
defiQUG
2026-04-07 23:22:12 -07:00
parent d931be8e19
commit 6eef6b07f6
224 changed files with 19671 additions and 3291 deletions

View File

@@ -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{

View 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)
}