@bsv/payment-express-middleware
Version:
BSV Blockchain service monetization express middleware
134 lines (120 loc) • 4.53 kB
text/typescript
import { Request, Response, NextFunction } from 'express'
import { AtomicBEEF, createNonce, Utils, verifyNonce } from '@bsv/sdk'
import { BSVPayment, PaymentMiddlewareOptions, PaymentResult } from './types.js'
const PAYMENT_VERSION = '1.0'
/**
* Creates middleware that enforces BSV payment for HTTP requests.
*
* NOTE: This middleware should run after the authentication middleware so that `req.auth` is available.
*
* @param options - Configuration for the payment middleware
* @param options.wallet - A wallet instance capable of submitting direct transactions.
* @param options.calculateRequestPrice - A function returning the price for the request in satoshis.
*
* @returns Express middleware that requires payment if `calculateRequestPrice` > 0.
*/
export function createPaymentMiddleware(options: PaymentMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => Promise<void> {
const {
calculateRequestPrice = () => 100, // Default to 100 satoshis if no price calculator is provided
wallet
} = options
if (typeof calculateRequestPrice !== 'function') {
throw new Error('The calculateRequestPrice option must be a function.')
}
if (wallet === undefined || typeof wallet !== 'object') {
throw new Error('A valid wallet instance must be supplied to the payment middleware.')
}
return async (req: Request, res: Response, next: NextFunction) => {
if (req.auth === undefined || typeof req.auth.identityKey !== 'string') {
return res.status(500).json({
status: 'error',
code: 'ERR_SERVER_MISCONFIGURED',
description: 'The payment middleware must be executed after the Auth middleware.'
})
}
let requestPrice: number
try {
requestPrice = await calculateRequestPrice(req)
} catch (err) {
console.error(err)
return res.status(500).json({
status: 'error',
code: 'ERR_PAYMENT_INTERNAL',
description: 'An internal error occurred while determining the payment required for this request.'
})
}
// If no payment is required, proceed immediately.
if (requestPrice === 0) {
req.payment = { satoshisPaid: 0 }
return next()
}
const bsvPaymentHeader = req.headers['x-bsv-payment']
if (bsvPaymentHeader === undefined) {
const derivationPrefix = await createNonce(wallet)
return res.status(402)
.set({
'x-bsv-payment-version': PAYMENT_VERSION,
'x-bsv-payment-satoshis-required': String(requestPrice),
'x-bsv-payment-derivation-prefix': derivationPrefix
})
.json({
status: 'error',
code: 'ERR_PAYMENT_REQUIRED',
satoshisRequired: requestPrice,
description: 'A BSV payment is required to complete this request. Provide the X-BSV-Payment header.'
})
}
let paymentData: BSVPayment
try {
paymentData = JSON.parse(String(bsvPaymentHeader))
try {
const valid = await verifyNonce(paymentData.derivationPrefix, wallet);
if (!valid) {
throw new Error('ERR_INVALID_DERIVATION_PREFIX');
}
} catch {
return res.status(400).json({
status: 'error',
code: 'ERR_INVALID_DERIVATION_PREFIX',
description: 'The X-BSV-Payment-Derivation-Prefix header is not valid.',
})
}
} catch (err) {
return res.status(400).json({
status: 'error',
code: 'ERR_MALFORMED_PAYMENT',
description: 'The X-BSV-Payment header is not valid JSON.'
})
}
try {
const { accepted }: PaymentResult = await wallet.internalizeAction({
tx: Utils.toArray(paymentData.transaction, 'base64') as AtomicBEEF,
outputs: [{
paymentRemittance: {
derivationPrefix: paymentData.derivationPrefix,
derivationSuffix: paymentData.derivationSuffix,
senderIdentityKey: req.auth.identityKey
},
outputIndex: 0,
protocol: 'wallet payment'
}],
description: 'Payment for request'
})
req.payment = {
satoshisPaid: requestPrice,
accepted,
tx: paymentData.transaction
}
res.set({
'x-bsv-payment-satoshis-paid': String(requestPrice)
})
next()
} catch (err: any) {
return res.status(400).json({
status: 'error',
code: err.code ?? 'ERR_PAYMENT_FAILED',
description: err.message ?? 'Payment failed.'
})
}
}
}