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
85 changes: 85 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Build outputs
dist/
build/
*.tsbuildinfo

# Environment variables
.env.local
.env.development.local
.env.test.local
.env.production.local

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Logs
logs/
*.log

# Runtime data
pids/
*.pid
*.seed
*.pid.lock

# Coverage directory used by tools like istanbul
coverage/
*.lcov

# nyc test coverage
.nyc_output

# Dependency directories
jspm_packages/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt

# Storybook build outputs
.out
.storybook-out

# Temporary folders
tmp/
temp/
2 changes: 2 additions & 0 deletions backend/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DYNAMIC_ENVIRONMENT_ID = 677d52ff-4bd0-4b26-8b6c-b4e2106f2300
DYNAMIC_API_TOKEN = dyn_wfGEbLFkGe9qFqWHOpIsntQjRjS2QuijSHg0kpjLqhP3RGGM4P2tOptj
13 changes: 13 additions & 0 deletions backend/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
],
};
32 changes: 32 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "web3-message-verifier-backend",
"version": "1.0.0",
"description": "Backend API for Web3 message signature verification",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"test": "jest",
"test:watch": "jest --watch"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.0.0",
"ethers": "^6.8.1"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/cors": "^2.8.13",
"@types/node": "^20.5.0",
"@types/jest": "^29.5.4",
"typescript": "^5.1.6",
"ts-node-dev": "^2.0.0",
"jest": "^29.6.2",
"ts-jest": "^29.1.1"
},
"keywords": ["web3", "ethereum", "signature", "verification", "express"],
"author": "",
"license": "MIT"
}
58 changes: 58 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
import signatureRoutes from './routes/signature'

const app = express()
const PORT = process.env.PORT || 4000

// Security middleware
app.use(helmet())

// CORS configuration
app.use(cors({
origin: process.env.NODE_ENV === 'production'
? ['https://your-frontend-domain.com']
: ['http://localhost:5173', 'http://127.0.0.1:5173'],
credentials: true,
}))

// Body parsing middleware
app.use(express.json({ limit: '1mb' }))
app.use(express.urlencoded({ extended: true, limit: '1mb' }))

// Request logging middleware
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`)
next()
})

// Routes
app.use('/', signatureRoutes)

// 404 handler
app.use('*', (req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Route ${req.originalUrl} not found`,
})
})

// Error handling middleware
app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Unhandled error:', error)
res.status(500).json({
error: 'Internal Server Error',
message: 'An unexpected error occurred',
})
})

// Start server
app.listen(PORT, () => {
console.log(`🚀 Web3 Message Verifier API running on port ${PORT}`)
console.log(`📝 POST /verify-signature - Verify message signatures`)
console.log(`❤️ GET /health - Health check`)
console.log(`🌍 Environment: ${process.env.NODE_ENV || 'development'}`)
})

export default app
80 changes: 80 additions & 0 deletions backend/src/middleware/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Request, Response, NextFunction } from 'express'
import { SignatureVerificationRequest } from '../types'

export const validateSignatureRequest = (
req: Request,
res: Response,
next: NextFunction
): void => {
const { message, signature } = req.body as SignatureVerificationRequest

// Check if required fields are present
if (!message || !signature) {
res.status(400).json({
error: 'Bad Request',
message: 'Message and signature are required',
})
return
}

// Check if fields are strings
if (typeof message !== 'string' || typeof signature !== 'string') {
res.status(400).json({
error: 'Bad Request',
message: 'Message and signature must be strings',
})
return
}

// Check if message is not empty
if (message.trim().length === 0) {
res.status(400).json({
error: 'Bad Request',
message: 'Message cannot be empty',
})
return
}

// Check if signature has valid format (basic hex check)
if (!signature.startsWith('0x') || signature.length !== 132) {
res.status(400).json({
error: 'Bad Request',
message: 'Invalid signature format',
})
return
}

next()
}

export const validateContentType = (
req: Request,
res: Response,
next: NextFunction
): void => {
if (req.method === 'POST' && !req.is('application/json')) {
res.status(415).json({
error: 'Unsupported Media Type',
message: 'Content-Type must be application/json',
})
return
}

next()
}

export const validateSignMessageRequest = (
req: Request,
res: Response,
next: NextFunction
): void => {
const { message } = req.body as { message?: unknown }
if (typeof message !== 'string' || message.trim().length === 0) {
res.status(400).json({
error: 'Bad Request',
message: 'Message must be a non-empty string',
})
return
}
next()
}
45 changes: 45 additions & 0 deletions backend/src/routes/signature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Router, Request, Response } from 'express'
import { SignatureVerificationService } from '../services/signatureVerification'
import { validateSignatureRequest, validateContentType, validateSignMessageRequest } from '../middleware/validation'
import { SignatureVerificationRequest } from '../types'
import { signMessageOnServer } from '../services/serverSigner'

const router = Router()

/**
* POST /verify-signature
* Verifies an EIP-191 message signature and returns the recovered signer
*/
router.post(
'/verify-signature',
validateContentType,
validateSignatureRequest,
(req: Request, res: Response) => {
try {
const request: SignatureVerificationRequest = req.body
const result = SignatureVerificationService.verifySignature(request)

res.status(200).json(result)
} catch (error) {
console.error('Signature verification error:', error)
res.status(500).json({
error: 'Internal Server Error',
message: 'Failed to verify signature',
})
}
}
)

/**
* GET /health
* Health check endpoint
*/
router.get('/health', (req: Request, res: Response) => {
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString(),
service: 'Web3 Message Verifier API',
})
})

export default router
72 changes: 72 additions & 0 deletions backend/src/services/signatureVerification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ethers } from 'ethers'
import { SignatureVerificationRequest, SignatureVerificationResponse } from '../types'

export class SignatureVerificationService {
/**
* Verifies an EIP-191 message signature and recovers the signer address
*/
static verifySignature(
request: SignatureVerificationRequest
): SignatureVerificationResponse {
try {
const { message, signature } = request

// Validate input
if (!message || !signature) {
throw new Error('Message and signature are required')
}

if (typeof message !== 'string' || typeof signature !== 'string') {
throw new Error('Message and signature must be strings')
}

// Verify signature format
if (!ethers.isHexString(signature, 65)) {
throw new Error('Invalid signature format')
}

// Recover the signer address
const recoveredAddress = ethers.verifyMessage(message, signature)

// Verify the recovered address is valid
if (!ethers.isAddress(recoveredAddress)) {
throw new Error('Invalid recovered address')
}

return {
isValid: true,
signer: recoveredAddress,
originalMessage: message,
}
} catch (error) {
// If verification fails, return invalid result
return {
isValid: false,
signer: '0x0000000000000000000000000000000000000000',
originalMessage: request.message,
}
}
}

/**
* Validates the signature format without performing verification
*/
static validateSignatureFormat(signature: string): boolean {
try {
return ethers.isHexString(signature, 65)
} catch {
return false
}
}

/**
* Validates Ethereum address format
*/
static validateAddress(address: string): boolean {
try {
return ethers.isAddress(address)
} catch {
return false
}
}
}
Loading