Skip to content
Merged
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
3 changes: 3 additions & 0 deletions services/stellar-wallet/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ ENCRYPTION_KEY=
# Rate limiting settings
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100

STELLAR_SECRET_KEY=SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
SOROBAN_CONTRACT_ID=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
8 changes: 8 additions & 0 deletions services/stellar-wallet/src/config/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ const envSchema = z.object({
PORT: z.coerce.number().default(3000), // Parses PORT to number and defaults to 3000
HORIZON_URL: z.url().default('https://horizon-testnet.stellar.org'), // Must be a valid URL
SOROBAN_RPC_URL: z.url().default('https://soroban-testnet.stellar.org'), // Must be a valid URL
STELLAR_SECRET_KEY:
process.env.NODE_ENV === 'test'
? z.string().default('STEST_MOCK_KEY_FOR_TESTING')
: z.string().min(1, 'STELLAR_SECRET_KEY is required'),
SOROBAN_CONTRACT_ID:
process.env.NODE_ENV === 'test'
? z.string().default('CTEST_MOCK_CONTRACT_FOR_TESTING')
: z.string().min(1, 'SOROBAN_CONTRACT_ID is required'),
})

// Validate and parse environment variables
Expand Down
2 changes: 2 additions & 0 deletions services/stellar-wallet/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import express, { type NextFunction, type Request, type Response } from 'express
import envs from './config/envs'
import { authLimiter, kycLimiter, walletLimiter } from './middlewares/rate-limit'
import { kycRouter } from './routes/kyc'
import { kycVerifyRouter } from './routes/kyc-verify'
import { walletRouter } from './routes/wallet'

export const app = express()
Expand All @@ -21,6 +22,7 @@ app.post('/auth', authLimiter, (_req: Request, res: Response) => {
})

app.use('/kyc', kycLimiter, kycRouter)
app.use('/kyc', kycLimiter, kycVerifyRouter)

app.use('/wallet', walletLimiter, walletRouter)

Expand Down
111 changes: 111 additions & 0 deletions services/stellar-wallet/src/routes/kyc-verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { createHash } from 'node:crypto'
import { Router, type Request, type Response } from 'express'
import * as StellarSdk from '@stellar/stellar-sdk'
import { connectDB, findKycById, run } from '../db/kyc'
import { validateKycData } from '../kyc/validate'
import { connectSoroban } from '../soroban/client'
import envs from '../config/envs'

export const kycVerifyRouter = Router()

interface VerifyKycRequest {
kyc_id: string
name: string
document: string
}

interface VerifyKycResponse {
kyc_id: string
data_hash: string
status: string
}

/**
* POST /kyc/verify - Register KYC data in Soroban contract and update database
*/
kycVerifyRouter.post('/verify', async (req: Request, res: Response) => {
try {
const { kyc_id, name, document } = req.body as VerifyKycRequest

// Validate required fields
if (!kyc_id || typeof kyc_id !== 'string') {
return res.status(400).json({ error: 'kyc_id is required and must be a string' })
}

// Validate KYC data using existing validator
const validation = validateKycData({ name, document })
if (!validation.isValid) {
return res.status(400).json({ error: validation.errors.join(', ') })
}

// Connect to database and verify kyc_id exists
const db = await connectDB()
const kycRecord = await findKycById(db, parseInt(kyc_id))
if (!kycRecord) {
return res.status(400).json({ error: 'Invalid kyc_id' })
}

// Generate hash of KYC data
const kycDataString = JSON.stringify({
name: validation.data!.name,
document: validation.data!.document,
})
const dataHash = createHash('sha256').update(kycDataString).digest('hex')

// Connect to Soroban and prepare contract call
const sorobanServer = connectSoroban()
const sourceKeypair = StellarSdk.Keypair.fromSecret(envs.STELLAR_SECRET_KEY)
const sourceAccount = await sorobanServer.getAccount(sourceKeypair.publicKey())

// Build contract call transaction
const contract = new StellarSdk.Contract(envs.SOROBAN_CONTRACT_ID)
const operation = contract.call(
'register_kyc',
StellarSdk.nativeToScVal(kyc_id, { type: 'string' }),
StellarSdk.nativeToScVal(dataHash, { type: 'string' }),
StellarSdk.nativeToScVal('approved', { type: 'string' }),
)

const transaction = new StellarSdk.TransactionBuilder(sourceAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(operation)
.setTimeout(30)
.build()

// Sign and submit transaction
transaction.sign(sourceKeypair)
const response = await sorobanServer.sendTransaction(transaction)

if (response.status === 'ERROR') {
throw new Error(`Contract call failed: ${response.errorResult}`)
}

// Wait for transaction confirmation
let txResponse = await sorobanServer.getTransaction(response.hash)
while (txResponse.status === 'NOT_FOUND') {
await new Promise((resolve) => setTimeout(resolve, 1000))
txResponse = await sorobanServer.getTransaction(response.hash)
}

if (txResponse.status !== 'SUCCESS') {
throw new Error(`Transaction failed: ${txResponse.status}`)
}

// Update database status to approved
await run(db, 'UPDATE kyc SET status = ? WHERE id = ?', ['approved', parseInt(kyc_id)])

// Return success response
const verifyResponse: VerifyKycResponse = {
kyc_id,
data_hash: dataHash,
status: 'approved',
}

return res.status(201).json(verifyResponse)
} catch (error) {
console.error('KYC verification error:', error)
return res.status(500).json({ error: 'Failed to register KYC' })
}
})
Loading