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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions backend/musicstrk_collections/bruno/verify-wallet.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
meta {
name: verify-wallet
type: http
seq: 3
}

post {
url: {{DEV_SERVER}}/auth/verify-wallet
body: json
auth: none
}

body:json {
{
"walletAddress": "0x1234567890abcdef1234567890abcdef12345678",
"message": "Sign this message to authenticate with MusicStrk",
"signature": ["0x123456789abcdef", "0xfedcba987654321"]
}
}

tests {
test("should return 200 for valid verification", function() {
expect(res.getStatus()).to.equal(200);
expect(res.getBody().error).to.equal(false);
expect(res.getBody().data.verified).to.equal(true);
});
}
238 changes: 235 additions & 3 deletions backend/src/routes/v1/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Router, Request } from "express";
import { BigNumberish, constants, ec, Provider } from "starknet";

import UserModel, { createUser, findUserByaddress } from "models/UserModel";
import { createUser, findUserByaddress } from "models/UserModel";
import { AUTHENTICATION_SNIP12_MESSAGE } from "constants/index";
import { createJWT } from "utilities/jwt";
import { Role } from "types";
Expand Down Expand Up @@ -121,7 +121,239 @@ AuthRoutes.post(
msg: error.message,
});
}
})

type ReqBody_VerifyWallet = {
walletAddress: string
message: string
signature: string[]
}

/**
* POST /api/v1/auth/verify-wallet
* Verify wallet ownership through message signature
* Reusable for performer registration and voting authentication
*/
AuthRoutes.post("/verify-wallet", async (req: Request<{}, {}, ReqBody_VerifyWallet>, res: any) => {
const startTime = Date.now()
const { walletAddress, message, signature } = req.body

console.log("[/verify-wallet | Request]:", {
walletAddress,
messageLength: message?.length,
signatureLength: signature?.length,
timestamp: new Date().toISOString(),
})

// Input validation
if (!walletAddress || !message || !signature) {
console.log("[/verify-wallet | Validation Error]: Missing required fields")
return res.status(400).json({
error: true,
msg: "Missing required fields: walletAddress, message, and signature are required",
code: "MISSING_FIELDS",
})
}

// Validate wallet address format
if (!walletAddress.startsWith("0x") || walletAddress.length < 10) {
console.log("[/verify-wallet | Validation Error]: Invalid wallet address format")
return res.status(400).json({
error: true,
msg: "Invalid wallet address format",
code: "INVALID_WALLET_FORMAT",
})
}

// Validate signature array
if (!Array.isArray(signature) || signature.length === 0) {
console.log("[/verify-wallet | Validation Error]: Invalid signature format")
return res.status(400).json({
error: true,
msg: "Signature must be a non-empty array",
code: "INVALID_SIGNATURE_FORMAT",
})
}

try {
// Normalize wallet address
const normalizedWalletAddress = walletAddress.toLowerCase()

console.log("[/verify-wallet | Processing]:", {
normalizedWalletAddress,
signatureElements: signature.length,
chainId: await provider.getChainId(),
})

// Create typed data structure for the message
const typedData = {
domain: {
name: "MusicStrk Wallet Verification",
chainId:
process.env.NODE_ENV === "production"
? "0x534e5f4d41494e" // SN_MAIN
: "0x534e5f5345504f4c4941", // SN_SEPOLIA
version: "1.0.0",
revision: "1",
},
message: {
content: message,
},
primaryType: "VerificationMessage",
types: {
VerificationMessage: [
{
name: "content",
type: "shortstring",
},
],
StarknetDomain: [
{
name: "name",
type: "shortstring",
},
{
name: "chainId",
type: "shortstring",
},
{
name: "version",
type: "shortstring",
},
],
},
}

// Handle different signature formats (Argent vs other wallets)
let rIndex: number, sIndex: number

if (signature.length === 2) {
// Standard signature format [r, s]
rIndex = 0
sIndex = 1
} else if (signature.length === 3) {
// Argent signature format [signer_type, r, s]
rIndex = 1
sIndex = 2
} else if (signature.length === 5) {
// Multi-sig Argent format [signer_type, signer_1, r, s, signer_2]
rIndex = 2
sIndex = 3
} else {
throw new Error(`Unsupported signature format: expected 2, 3, or 5 elements, got ${signature.length}`)
}

// Create normalized signature object
const normalizedSignature = new ec.starkCurve.Signature(BigInt(signature[rIndex]), BigInt(signature[sIndex]))

console.log("[/verify-wallet | Signature Analysis]:", {
originalLength: signature.length,
rIndex,
sIndex,
r: signature[rIndex],
s: signature[sIndex],
normalizedR: normalizedSignature.r.toString(16),
normalizedS: normalizedSignature.s.toString(16),
})

// Verify the signature against the typed data
const isSignatureValid = await provider.verifyMessageInStarknet(
typedData,
normalizedSignature,
normalizedWalletAddress,
)

const processingTime = Date.now() - startTime

console.log("[/verify-wallet | Verification Result]:", {
isValid: isSignatureValid,
walletAddress: normalizedWalletAddress,
processingTimeMs: processingTime,
timestamp: new Date().toISOString(),
})

if (!isSignatureValid) {
return res.status(401).json({
error: true,
msg: "Signature verification failed: signature does not match the provided wallet address",
code: "SIGNATURE_MISMATCH",
details: {
walletAddress: normalizedWalletAddress,
verified: false,
},
})
}

// Successful verification
return res.status(200).json({
error: false,
msg: "Wallet verification successful",
data: {
walletAddress: normalizedWalletAddress,
verified: true,
timestamp: new Date().toISOString(),
processingTimeMs: processingTime,
},
})
} catch (error: any) {
const processingTime = Date.now() - startTime

console.error("[/verify-wallet | Error]:", {
error: error.message,
stack: error.stack,
walletAddress,
processingTimeMs: processingTime,
timestamp: new Date().toISOString(),
})

// Handle specific error types
if (error.message.toLowerCase().includes("contract not found")) {
return res.status(400).json({
error: true,
msg: "Wallet contract not found or not deployed on this network",
code: "CONTRACT_NOT_FOUND",
details: {
walletAddress,
network: process.env.NODE_ENV === "production" ? "mainnet" : "sepolia",
},
})
}

if (error.message.toLowerCase().includes("invalid signature")) {
return res.status(400).json({
error: true,
msg: "Invalid signature format or corrupted signature data",
code: "INVALID_SIGNATURE",
details: {
signatureLength: signature?.length,
walletAddress,
},
})
}

if (error.message.toLowerCase().includes("network") || error.message.toLowerCase().includes("rpc")) {
return res.status(503).json({
error: true,
msg: "Network connectivity issue: unable to verify signature",
code: "NETWORK_ERROR",
details: {
retryable: true,
},
})
}

// Generic server error
return res.status(500).json({
error: true,
msg: "Internal server error during wallet verification",
code: "INTERNAL_ERROR",
details: {
timestamp: new Date().toISOString(),
processingTimeMs: processingTime,
},
})
}
);
})

export default AuthRoutes

export default AuthRoutes;
20 changes: 20 additions & 0 deletions backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,23 @@ export interface VotePayload {
score?: number
personalityScale: PersonalityScale
}

export interface WalletVerificationPayload {
walletAddress: string
message: string
signature: string[]
}

export interface WalletVerificationResponse {
error: boolean
msg: string
code?: string
data?: {
walletAddress: string
verified: boolean
timestamp: string
processingTimeMs: number
}
details?: any
}

Loading