From 9f5bcfc3ac758fa91c5b6a0ae0ff68ae70135012 Mon Sep 17 00:00:00 2001 From: Julius VPS Date: Sun, 28 Jun 2026 05:40:31 +0100 Subject: [PATCH 1/8] db: add wallet_auth_challenge table --- db_migrations.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/db_migrations.go b/db_migrations.go index 23f3281a..05b1768b 100644 --- a/db_migrations.go +++ b/db_migrations.go @@ -2803,4 +2803,23 @@ var migrations = []any{ CREATE INDEX IF NOT EXISTS transfer_contract_dispute_create_time ON transfer_contract (dispute, outcome, create_time) `), + + newSqlMigration(` + CREATE TABLE wallet_auth_challenge ( + challenge_id uuid NOT NULL, + challenge_value varchar(128) NOT NULL, + wallet_address text NULL, + blockchain varchar(32) NULL, + create_time timestamp NOT NULL DEFAULT now(), + expire_time timestamp NOT NULL, + used bool NOT NULL DEFAULT false, + + PRIMARY KEY (challenge_id), + UNIQUE (challenge_value) + ) + `), + newSqlMigration(` + CREATE INDEX wallet_auth_challenge_expire_time_used + ON wallet_auth_challenge (expire_time, used) + `), } From c52a34fff32d20d5b2d52e3ba21470b199d82fa0 Mon Sep 17 00:00:00 2001 From: Julius VPS Date: Sun, 28 Jun 2026 05:45:09 +0100 Subject: [PATCH 2/8] model: add wallet auth challenge lifecycle --- model/wallet_auth_challenge_model.go | 254 +++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 model/wallet_auth_challenge_model.go diff --git a/model/wallet_auth_challenge_model.go b/model/wallet_auth_challenge_model.go new file mode 100644 index 00000000..c9e69312 --- /dev/null +++ b/model/wallet_auth_challenge_model.go @@ -0,0 +1,254 @@ +package model + +import ( + "context" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/urnetwork/glog" + "github.com/urnetwork/server" +) + +const ( + WalletAuthChallengeLifetime = 5 * time.Minute + WalletAuthChallengeSkewPast = 1 * time.Minute + // Use the same lifetime for max future clock skew so a challenge + // cannot be hoarded beyond its own expiry. + WalletAuthChallengeSkewFuture = 5 * time.Minute + WalletAuthChallengeValueBytes = 32 +) + +type WalletAuthChallengeArgs struct { + WalletAddress *string `json:"wallet_address,omitempty"` + Blockchain *string `json:"blockchain,omitempty"` +} + +type WalletAuthChallengeResult struct { + Challenge string `json:"challenge"` + Timestamp int64 `json:"timestamp"` + ExpiresIn int64 `json:"expires_in"` + MessageTemplate string `json:"message_template"` + Error *WalletAuthChallengeResultError `json:"error,omitempty"` +} + +type WalletAuthChallengeResultError struct { + Message string `json:"message"` +} + +func CreateWalletAuthChallenge( + args WalletAuthChallengeArgs, + ctx context.Context, +) *WalletAuthChallengeResult { + now := server.NowUtc() + expire := now.Add(WalletAuthChallengeLifetime) + + challengeBytes := make([]byte, WalletAuthChallengeValueBytes) + if _, err := rand.Read(challengeBytes); err != nil { + glog.Errorf("Failed to generate wallet auth challenge: %v", err) + return &WalletAuthChallengeResult{ + Error: &WalletAuthChallengeResultError{ + Message: "failed to generate challenge", + }, + } + } + challengeValue := base64.URLEncoding.EncodeToString(challengeBytes) + + blockchain := "" + if args.Blockchain != nil { + blockchain = strings.ToLower(strings.TrimSpace(*args.Blockchain)) + } + // default empty/missing blockchain to solana to match existing behavior + if blockchain == "" { + blockchain = SOL.String() + } + + var walletAddress *string + if args.WalletAddress != nil { + w := strings.TrimSpace(*args.WalletAddress) + walletAddress = &w + } + + server.Tx(ctx, func(tx server.PgTx) { + server.RaisePgResult(tx.Exec( + ctx, + ` + INSERT INTO wallet_auth_challenge ( + challenge_id, + challenge_value, + wallet_address, + blockchain, + create_time, + expire_time, + used + ) + VALUES ($1, $2, $3, $4, $5, $6, false) + `, + server.NewId(), + challengeValue, + walletAddress, + blockchain, + now, + expire, + )) + }) + + message := FormatWalletAuthChallengeMessage(challengeValue, now.Unix()) + + return &WalletAuthChallengeResult{ + Challenge: challengeValue, + Timestamp: now.Unix(), + ExpiresIn: int64(WalletAuthChallengeLifetime / time.Second), + MessageTemplate: message, + } +} + +// FormatWalletAuthChallengeMessage must match the client-side construction +// exactly. Any change here must be reflected in the dashboard hook. +func FormatWalletAuthChallengeMessage(challenge string, timestamp int64) string { + return fmt.Sprintf("Sign in to URnetwork\nChallenge: %s\nTimestamp: %d", challenge, timestamp) +} + +// parseWalletAuthChallengeMessage is the inverse of FormatWalletAuthChallengeMessage. +// It returns the embedded challenge and timestamp so the server can look up the row. +func parseWalletAuthChallengeMessage(message string) (challenge string, timestamp int64, err error) { + parts := strings.SplitN(message, "\n", 3) + if len(parts) != 3 || + parts[0] != "Sign in to URnetwork" || + !strings.HasPrefix(parts[1], "Challenge: ") || + !strings.HasPrefix(parts[2], "Timestamp: ") { + return "", 0, errors.New("invalid message format") + } + challenge = strings.TrimPrefix(parts[1], "Challenge: ") + tsStr := strings.TrimPrefix(parts[2], "Timestamp: ") + timestamp, err = strconv.ParseInt(tsStr, 10, 64) + if err != nil { + return "", 0, errors.New("invalid timestamp in message") + } + return challenge, timestamp, nil +} + +type UseWalletAuthChallengeArgs struct { + Blockchain string + PublicKey string + Message string + Signature string +} + +type UseWalletAuthChallengeResult struct { + Valid bool + Error *WalletAuthChallengeResultError +} + +func UseWalletAuthChallenge( + args *UseWalletAuthChallengeArgs, + ctx context.Context, +) (*UseWalletAuthChallengeResult, error) { + challengeValue, timestamp, err := parseWalletAuthChallengeMessage(args.Message) + if err != nil { + return &UseWalletAuthChallengeResult{ + Valid: false, + Error: &WalletAuthChallengeResultError{Message: "400 invalid message format"}, + }, nil + } + + now := server.NowUtc() + messageTime := time.Unix(timestamp, 0).UTC() + + if messageTime.Before(now.Add(-WalletAuthChallengeSkewPast)) { + return &UseWalletAuthChallengeResult{ + Valid: false, + Error: &WalletAuthChallengeResultError{Message: "400 challenge timestamp too old"}, + }, nil + } + if messageTime.After(now.Add(WalletAuthChallengeSkewFuture)) { + return &UseWalletAuthChallengeResult{ + Valid: false, + Error: &WalletAuthChallengeResultError{Message: "400 challenge timestamp too far in the future"}, + }, nil + } + + blockchain := args.Blockchain + if blockchain == "" { + blockchain = SOL.String() + } + + isValid, err := VerifySignature(blockchain, args.PublicKey, args.Message, args.Signature) + if err != nil { + return nil, err + } + if !isValid { + return &UseWalletAuthChallengeResult{ + Valid: false, + Error: &WalletAuthChallengeResultError{Message: "401 invalid signature"}, + }, nil + } + + var used bool + var expireTime time.Time + server.Tx(ctx, func(tx server.PgTx) { + result, dbErr := tx.Query( + ctx, + ` + SELECT + used, + expire_time + FROM wallet_auth_challenge + WHERE challenge_value = $1 + FOR UPDATE + `, + challengeValue, + ) + if dbErr != nil { + server.Raise(dbErr) + } + server.WithPgResult(result, dbErr, func() { + if !result.Next() { + err = errors.New("challenge not found") + return + } + server.Raise(result.Scan(&used, &expireTime)) + }) + + if err != nil { + return + } + + if used { + err = errors.New("challenge already used") + return + } + + if server.NowUtc().After(expireTime) { + err = errors.New("challenge expired") + return + } + + server.RaisePgResult(tx.Exec( + ctx, + ` + UPDATE wallet_auth_challenge + SET used = true, + wallet_address = $2, + blockchain = $3 + WHERE challenge_value = $1 + `, + challengeValue, + args.PublicKey, + blockchain, + )) + }) + + if err != nil { + return &UseWalletAuthChallengeResult{ + Valid: false, + Error: &WalletAuthChallengeResultError{Message: fmt.Sprintf("401 %s", err.Error())}, + }, nil + } + + return &UseWalletAuthChallengeResult{Valid: true}, nil +} From 14a7304bfee7cbbf3b1ef1b2c1d5cb923978d584 Mon Sep 17 00:00:00 2001 From: Julius VPS Date: Sun, 28 Jun 2026 05:53:53 +0100 Subject: [PATCH 3/8] api: add POST /auth/wallet-challenge endpoint --- api/api.go | 1 + api/handlers/wallet_auth_challenge_handlers.go | 12 ++++++++++++ controller/auth_controller.go | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 api/handlers/wallet_auth_challenge_handlers.go diff --git a/api/api.go b/api/api.go index 63f69199..44274df7 100644 --- a/api/api.go +++ b/api/api.go @@ -24,6 +24,7 @@ func Routes() []*router.Route { router.NewRoute("POST", "/auth/login", handlers.AuthLogin), router.NewRoute("POST", "/auth/login-with-password", handlers.AuthLoginWithPassword), router.NewRoute("POST", "/auth/verify", handlers.AuthVerify), + router.NewRoute("POST", "/auth/wallet-challenge", handlers.AuthWalletChallenge), router.NewRoute("GET", "/auth/refresh", handlers.AuthRefreshToken), router.NewRoute("POST", "/auth/verify-send", handlers.AuthVerifySend), router.NewRoute("POST", "/auth/password-reset", handlers.AuthPasswordReset), diff --git a/api/handlers/wallet_auth_challenge_handlers.go b/api/handlers/wallet_auth_challenge_handlers.go new file mode 100644 index 00000000..60297d2c --- /dev/null +++ b/api/handlers/wallet_auth_challenge_handlers.go @@ -0,0 +1,12 @@ +package handlers + +import ( + "net/http" + + "github.com/urnetwork/server/controller" + "github.com/urnetwork/server/router" +) + +func AuthWalletChallenge(w http.ResponseWriter, r *http.Request) { + router.WrapWithInputNoAuth(controller.AuthWalletChallenge, w, r) +} diff --git a/controller/auth_controller.go b/controller/auth_controller.go index 4c874d5c..cbd38f7b 100644 --- a/controller/auth_controller.go +++ b/controller/auth_controller.go @@ -18,6 +18,24 @@ var SsoRedirectUrl = sync.OnceValue(func() string { return c["web_connect"].(map[string]any)["redirect_url"].(string) }) +type AuthWalletChallengeArgs struct { + WalletAddress *string `json:"wallet_address,omitempty"` + Blockchain *string `json:"blockchain,omitempty"` +} + +type AuthWalletChallengeResult = model.WalletAuthChallengeResult + +func AuthWalletChallenge( + args AuthWalletChallengeArgs, + session *session.ClientSession, +) (*AuthWalletChallengeResult, error) { + result := model.CreateWalletAuthChallenge(model.WalletAuthChallengeArgs{ + WalletAddress: args.WalletAddress, + Blockchain: args.Blockchain, + }, session.Ctx) + return result, nil +} + func AuthLogin( login model.AuthLoginArgs, session *session.ClientSession, From e87ec0b6f99a37c3a6f6ffb42a4a753d4eec794d Mon Sep 17 00:00:00 2001 From: Julius VPS Date: Sun, 28 Jun 2026 05:54:14 +0100 Subject: [PATCH 4/8] auth: add challenge fields to WalletAuthArgs --- model/auth_model.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/model/auth_model.go b/model/auth_model.go index 37a69aa3..f7973759 100644 --- a/model/auth_model.go +++ b/model/auth_model.go @@ -45,6 +45,9 @@ type WalletAuthArgs struct { Signature string `json:"wallet_signature,omitempty"` Message string `json:"wallet_message,omitempty"` Blockchain string `json:"blockchain,omitempty"` + // new fields; kept optional for backwards compat during deploy window + Challenge string `json:"challenge,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` } type AuthLoginArgs struct { From a3a5d368ae80cf705bcc217db23db3492417918a Mon Sep 17 00:00:00 2001 From: Julius VPS Date: Sun, 28 Jun 2026 05:59:47 +0100 Subject: [PATCH 5/8] test: wallet auth challenge replay/expiry/timestamp --- model/wallet_auth_challenge_model_test.go | 80 +++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 model/wallet_auth_challenge_model_test.go diff --git a/model/wallet_auth_challenge_model_test.go b/model/wallet_auth_challenge_model_test.go new file mode 100644 index 00000000..49b3212d --- /dev/null +++ b/model/wallet_auth_challenge_model_test.go @@ -0,0 +1,80 @@ +package model + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/go-playground/assert/v2" + "github.com/urnetwork/server" +) + +func TestWalletAuthChallengeCreateAndUse(t *testing.T) { + server.DefaultTestEnv().Run(t, func(t testing.TB) { + ctx := context.Background() + + result := CreateWalletAuthChallenge(WalletAuthChallengeArgs{}, ctx) + assert.Equal(t, result.Error, nil) + assert.Equal(t, strings.Contains(result.MessageTemplate, result.Challenge), true) + + useResult, err := UseWalletAuthChallenge(&UseWalletAuthChallengeArgs{ + Blockchain: "solana", + PublicKey: "6UJtwDRMv2CCfVCKm6hgMDAGrFzv7z8WKEHut2u8dV8s", + Message: result.MessageTemplate, + Signature: "KEpagxVwv1FmPt3KIMdVZz4YsDxgD7J23+f6aafejwdnBy3WJgkE4qteYMwucNoH+9RaPU70YV2Bf+xI+Nd7Cw==", + }, ctx) + assert.Equal(t, err, nil) + assert.Equal(t, useResult.Valid, true) + + // replay must fail + useResult2, err := UseWalletAuthChallenge(&UseWalletAuthChallengeArgs{ + Blockchain: "solana", + PublicKey: "6UJtwDRMv2CCfVCKm6hgMDAGrFzv7z8WKEHut2u8dV8s", + Message: result.MessageTemplate, + Signature: "KEpagxVwv1FmPt3KIMdVZz4YsDxgD7J23+f6aafejwdnBy3WJgkE4qteYMwucNoH+9RaPU70YV2Bf+xI+Nd7Cw==", + }, ctx) + assert.Equal(t, err, nil) + assert.Equal(t, useResult2.Valid, false) + assert.Equal(t, useResult2.Error != nil, true) + }) +} + +func TestWalletAuthChallengeExpired(t *testing.T) { + server.DefaultTestEnv().Run(t, func(t testing.TB) { + ctx := context.Background() + + result := CreateWalletAuthChallenge(WalletAuthChallengeArgs{}, ctx) + assert.Equal(t, result.Error, nil) + + // mutate the message to an old timestamp + oldMessage := FormatWalletAuthChallengeMessage(result.Challenge, result.Timestamp-int64((6*time.Minute)/time.Second)) + useResult, err := UseWalletAuthChallenge(&UseWalletAuthChallengeArgs{ + Blockchain: "solana", + PublicKey: "6UJtwDRMv2CCfVCKm6hgMDAGrFzv7z8WKEHut2u8dV8s", + Message: oldMessage, + Signature: "KEpagxVwv1FmPt3KIMdVZz4YsDxgD7J23+f6aafejwdnBy3WJgkE4qteYMwucNoH+9RaPU70YV2Bf+xI+Nd7Cw==", + }, ctx) + assert.Equal(t, err, nil) + assert.Equal(t, useResult.Valid, false) + }) +} + +func TestWalletAuthChallengeFutureTimestamp(t *testing.T) { + server.DefaultTestEnv().Run(t, func(t testing.TB) { + ctx := context.Background() + + result := CreateWalletAuthChallenge(WalletAuthChallengeArgs{}, ctx) + assert.Equal(t, result.Error, nil) + + futureMessage := FormatWalletAuthChallengeMessage(result.Challenge, result.Timestamp+int64((6*time.Minute)/time.Second)) + useResult, err := UseWalletAuthChallenge(&UseWalletAuthChallengeArgs{ + Blockchain: "solana", + PublicKey: "6UJtwDRMv2CCfVCKm6hgMDAGrFzv7z8WKEHut2u8dV8s", + Message: futureMessage, + Signature: "KEpagxVwv1FmPt3KIMdVZz4YsDxgD7J23+f6aafejwdnBy3WJgkE4qteYMwucNoH+9RaPU70YV2Bf+xI+Nd7Cw==", + }, ctx) + assert.Equal(t, err, nil) + assert.Equal(t, useResult.Valid, false) + }) +} From 6be0998369c9542788149ccb7fcd4bbb2e74f7b1 Mon Sep 17 00:00:00 2001 From: Julius VPS Date: Sun, 28 Jun 2026 05:59:55 +0100 Subject: [PATCH 6/8] auth,network: enforce server wallet challenge before wallet login/create --- model/auth_model.go | 27 +++++++++++++++------------ model/network_model.go | 25 +++++++++++++------------ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/model/auth_model.go b/model/auth_model.go index f7973759..e73203b2 100644 --- a/model/auth_model.go +++ b/model/auth_model.go @@ -435,23 +435,26 @@ func handleLoginWallet( ctx context.Context, ) (result *AuthLoginResult, returnErr error) { /** - * Handle wallet login + * Handle wallet login by validating the server-issued challenge. + * UseWalletAuthChallenge verifies the signature, checks the timestamp, + * and marks the challenge as used atomically. */ - - isValid, err := VerifySignature( - walletAuth.Blockchain, - walletAuth.PublicKey, - walletAuth.Message, - walletAuth.Signature, - ) - + useResult, err := UseWalletAuthChallenge(&UseWalletAuthChallengeArgs{ + Blockchain: walletAuth.Blockchain, + PublicKey: walletAuth.PublicKey, + Message: walletAuth.Message, + Signature: walletAuth.Signature, + }, ctx) if err != nil { returnErr = err return } - - if !isValid { - returnErr = errors.New("invalid signature") + if !useResult.Valid { + msg := "invalid wallet challenge" + if useResult.Error != nil { + msg = useResult.Error.Message + } + returnErr = errors.New(msg) return } diff --git a/model/network_model.go b/model/network_model.go index f0ec8d77..f2a35e00 100644 --- a/model/network_model.go +++ b/model/network_model.go @@ -356,27 +356,28 @@ func NetworkCreate( }, }, nil } - networkCreate.WalletAuth.Blockchain = parsedBlockchain.String() /** - * verify the wallet signature + * validate the wallet challenge */ - isValid, err := VerifySignature( - networkCreate.WalletAuth.Blockchain, - networkCreate.WalletAuth.PublicKey, - networkCreate.WalletAuth.Message, - networkCreate.WalletAuth.Signature, - ) - + useResult, err := UseWalletAuthChallenge(&UseWalletAuthChallengeArgs{ + Blockchain: networkCreate.WalletAuth.Blockchain, + PublicKey: networkCreate.WalletAuth.PublicKey, + Message: networkCreate.WalletAuth.Message, + Signature: networkCreate.WalletAuth.Signature, + }, session.Ctx) if err != nil { return nil, err } - - if !isValid { + if !useResult.Valid { + msg := "invalid wallet challenge" + if useResult.Error != nil { + msg = useResult.Error.Message + } return &NetworkCreateResult{ Error: &NetworkCreateResultError{ - Message: "invalid wallet signature", + Message: msg, }, }, nil } From 80c5cb5a368ed12d8e43abb2d7134940de0c099f Mon Sep 17 00:00:00 2001 From: Julius VPS Date: Sun, 28 Jun 2026 07:20:02 +0100 Subject: [PATCH 7/8] test(wallet-auth-challenge): use real Solana keypair for signatures Replace hardcoded signature placeholders with locally generated Solana keypairs so the happy path and replay/timestamp tests exercise real signature verification against the server-issued challenge. --- model/wallet_auth_challenge_model_test.go | 43 ++++++++++++++++++----- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/model/wallet_auth_challenge_model_test.go b/model/wallet_auth_challenge_model_test.go index 49b3212d..024beb18 100644 --- a/model/wallet_auth_challenge_model_test.go +++ b/model/wallet_auth_challenge_model_test.go @@ -2,11 +2,13 @@ package model import ( "context" + "encoding/base64" "strings" "testing" "time" "github.com/go-playground/assert/v2" + "github.com/gagliardetto/solana-go" "github.com/urnetwork/server" ) @@ -14,15 +16,24 @@ func TestWalletAuthChallengeCreateAndUse(t *testing.T) { server.DefaultTestEnv().Run(t, func(t testing.TB) { ctx := context.Background() + // Generate a fresh Solana keypair for a real end-to-end signature test. + privateKey, err := solana.NewRandomPrivateKey() + assert.Equal(t, err, nil) + publicKey := privateKey.PublicKey() + result := CreateWalletAuthChallenge(WalletAuthChallengeArgs{}, ctx) assert.Equal(t, result.Error, nil) assert.Equal(t, strings.Contains(result.MessageTemplate, result.Challenge), true) + signature, err := privateKey.Sign([]byte(result.MessageTemplate)) + assert.Equal(t, err, nil) + signatureB64 := base64.StdEncoding.EncodeToString(signature[:]) + useResult, err := UseWalletAuthChallenge(&UseWalletAuthChallengeArgs{ Blockchain: "solana", - PublicKey: "6UJtwDRMv2CCfVCKm6hgMDAGrFzv7z8WKEHut2u8dV8s", + PublicKey: publicKey.String(), Message: result.MessageTemplate, - Signature: "KEpagxVwv1FmPt3KIMdVZz4YsDxgD7J23+f6aafejwdnBy3WJgkE4qteYMwucNoH+9RaPU70YV2Bf+xI+Nd7Cw==", + Signature: signatureB64, }, ctx) assert.Equal(t, err, nil) assert.Equal(t, useResult.Valid, true) @@ -30,9 +41,9 @@ func TestWalletAuthChallengeCreateAndUse(t *testing.T) { // replay must fail useResult2, err := UseWalletAuthChallenge(&UseWalletAuthChallengeArgs{ Blockchain: "solana", - PublicKey: "6UJtwDRMv2CCfVCKm6hgMDAGrFzv7z8WKEHut2u8dV8s", + PublicKey: publicKey.String(), Message: result.MessageTemplate, - Signature: "KEpagxVwv1FmPt3KIMdVZz4YsDxgD7J23+f6aafejwdnBy3WJgkE4qteYMwucNoH+9RaPU70YV2Bf+xI+Nd7Cw==", + Signature: signatureB64, }, ctx) assert.Equal(t, err, nil) assert.Equal(t, useResult2.Valid, false) @@ -44,16 +55,24 @@ func TestWalletAuthChallengeExpired(t *testing.T) { server.DefaultTestEnv().Run(t, func(t testing.TB) { ctx := context.Background() + privateKey, err := solana.NewRandomPrivateKey() + assert.Equal(t, err, nil) + publicKey := privateKey.PublicKey() + result := CreateWalletAuthChallenge(WalletAuthChallengeArgs{}, ctx) assert.Equal(t, result.Error, nil) // mutate the message to an old timestamp oldMessage := FormatWalletAuthChallengeMessage(result.Challenge, result.Timestamp-int64((6*time.Minute)/time.Second)) + signature, err := privateKey.Sign([]byte(oldMessage)) + assert.Equal(t, err, nil) + signatureB64 := base64.StdEncoding.EncodeToString(signature[:]) + useResult, err := UseWalletAuthChallenge(&UseWalletAuthChallengeArgs{ Blockchain: "solana", - PublicKey: "6UJtwDRMv2CCfVCKm6hgMDAGrFzv7z8WKEHut2u8dV8s", + PublicKey: publicKey.String(), Message: oldMessage, - Signature: "KEpagxVwv1FmPt3KIMdVZz4YsDxgD7J23+f6aafejwdnBy3WJgkE4qteYMwucNoH+9RaPU70YV2Bf+xI+Nd7Cw==", + Signature: signatureB64, }, ctx) assert.Equal(t, err, nil) assert.Equal(t, useResult.Valid, false) @@ -64,15 +83,23 @@ func TestWalletAuthChallengeFutureTimestamp(t *testing.T) { server.DefaultTestEnv().Run(t, func(t testing.TB) { ctx := context.Background() + privateKey, err := solana.NewRandomPrivateKey() + assert.Equal(t, err, nil) + publicKey := privateKey.PublicKey() + result := CreateWalletAuthChallenge(WalletAuthChallengeArgs{}, ctx) assert.Equal(t, result.Error, nil) futureMessage := FormatWalletAuthChallengeMessage(result.Challenge, result.Timestamp+int64((6*time.Minute)/time.Second)) + signature, err := privateKey.Sign([]byte(futureMessage)) + assert.Equal(t, err, nil) + signatureB64 := base64.StdEncoding.EncodeToString(signature[:]) + useResult, err := UseWalletAuthChallenge(&UseWalletAuthChallengeArgs{ Blockchain: "solana", - PublicKey: "6UJtwDRMv2CCfVCKm6hgMDAGrFzv7z8WKEHut2u8dV8s", + PublicKey: publicKey.String(), Message: futureMessage, - Signature: "KEpagxVwv1FmPt3KIMdVZz4YsDxgD7J23+f6aafejwdnBy3WJgkE4qteYMwucNoH+9RaPU70YV2Bf+xI+Nd7Cw==", + Signature: signatureB64, }, ctx) assert.Equal(t, err, nil) assert.Equal(t, useResult.Valid, false) From 15bc702021f9d30cf5d24ff50e440b87589c1f9b Mon Sep 17 00:00:00 2001 From: ryanmello07 Date: Sun, 28 Jun 2026 07:49:31 +0100 Subject: [PATCH 8/8] test: fix test database locale for systems without en_US.UTF-8 template --- test_util.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test_util.go b/test_util.go index d4f79912..a6e9bf9f 100644 --- a/test_util.go +++ b/test_util.go @@ -217,9 +217,11 @@ func (self *TestEnv) setup() func() { ` CREATE DATABASE %s WITH - OWNER=%s + OWNER=%s ENCODING=UTF8 - LOCALE='en_US.UTF-8' + LC_COLLATE='en_US.UTF-8' + LC_CTYPE='en_US.UTF-8' + TEMPLATE=template_utf8 `, testPgDbName, pg["user"],