import { Hono, type Context } from 'hono' import type * as z from 'zod/mini' import type * as App from '../../App.js' import * as Db from '../../db/Db.js' import * as Auth from '../../internal/Auth.js' import * as OpenApi from '../../internal/OpenApi.js' import * as Response from '../../internal/Response.js' import * as Timing from '../../internal/Timing.js' import * as Metadata from '../metadata.js' import * as Billing from '../management/Billing.js' import * as Mpp from './Mpp.js' import * as Adapter from './internal/Adapter.js' import * as Idempotency from './internal/Idempotency.js' import * as MppError from './internal/Error.js' import * as Policy from './internal/Policy.js' import * as Sponsorship from './internal/Sponsorship.js' /** Mounts MPP credential validation and broadcast routes. */ export function mpp(options: mpp.Options) { const app = new Hono() .post( '/v1/mpp/validate', Auth.policy({ apiKey: { scopes: ['mpp:write'] }, mpp: false, public: false }), OpenApi.validate('json', Mpp.schema.RelayInput, bodyValidation), OpenApi.describeRoute({ description: 'Checks whether a completed MPP credential can be processed without submitting payment.\n\nTempo validates the challenge and credential, confirms the chain is supported, and screens the payment parties. It does not settle, broadcast, reserve funds, or consume the credential.\n\nRequires `mpp:write`. Well-formed requests return HTTP `200`; inspect `success`. Invalid bodies return `400`; missing or unauthorized API keys return `401` or `403`.', operationId: 'validateMppCredential', responses: responses({ description: 'The MPP credential validation result.', schema: Mpp.schema.VerifyResponse, }), summary: 'Validate MPP credential', tags: ['MPP'], }), async (c) => { if (Auth.narrowAccess) return Auth.accessError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation) const input = c.req.valid('json') const chainId = Policy.chainId(input) ?? c.get('chainId') const response = await Timing.time(c, 'mpp_verify', () => createAdapter(c, { chainId, options }).verify(input), ) c.set('mpp', observability(c, { chainId, feePayer: false, operation: 'verify', response })) return c.json(Response.validated(Mpp.schema.VerifyResponse, response), 200) }, ) .post( '/v1/mpp/broadcast', Auth.policy({ apiKey: { scopes: ['mpp:write'] }, mpp: false, public: false }), OpenApi.validate('json', Mpp.schema.RelayInput, bodyValidation), OpenApi.describeRoute({ description: 'Validates, screens, and submits a completed MPP credential to Tempo.\n\nRequires `mpp:write`. Well-formed requests return HTTP `200`; inspect `success`.\n\nUse `Idempotency-Key` on retries to replay a successful receipt. Reusing a key for a different credential returns `invalid_payment`, and in-flight duplicates return `temporarily_unavailable`.', operationId: 'broadcastMppCredential', parameters: [ { description: 'Optional opaque retry key, scoped to the API key and credential. Reusing it after a successful broadcast returns the original receipt without another submission. Reusing it with a different credential returns `invalid_payment`. While the first request is running, duplicates return `temporarily_unavailable`. Failed attempts are not retained and may be retried with the same key.', in: 'header', name: 'Idempotency-Key', required: false, schema: { example: 'mpp_broadcast_01j3j1k2l3m4n5p6q7r8s9t0u', type: 'string' }, }, ], responses: responses({ description: 'The MPP credential broadcast result.', schema: Mpp.schema.BroadcastResponse, }), summary: 'Broadcast MPP credential', tags: ['MPP'], }), async (c) => { if (Auth.narrowAccess) return Auth.accessError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation) const input = c.req.valid('json') const authorization = await authorizeBroadcast(c, { input, options }) if (authorization.type === 'error') { const response = { error: authorization.error, success: false, } satisfies Mpp.BroadcastResponse c.set( 'mpp', observability(c, { chainId: authorization.chainId, feePayer: false, operation: 'broadcast', response, }), ) return c.json(Response.validated(Mpp.schema.BroadcastResponse, response), 200) } const result = await broadcast(c, { chainId: authorization.chainId, feePayer: authorization.feePayer, input, options, }) c.set( 'mpp', observability(c, { chainId: authorization.chainId, feePayer: Boolean(authorization.feePayer), ...(result.idempotency === undefined ? {} : { idempotency: result.idempotency }), operation: 'broadcast', response: result.response, }), ) return c.json(Response.validated(Mpp.schema.BroadcastResponse, result.response), 200) }, ) return Metadata.attach(app, openapi) } export declare namespace mpp { /** MPP relay capabilities. */ type Options = { /** Account used to sponsor Tempo pull transactions. */ feePayer?: Adapter.create.Options['feePayer'] | undefined /** Reports an unexpected relay failure without exposing credential data. */ reportUnknown?: Adapter.create.Options['reportUnknown'] | undefined /** Atomic durable state for MPPX replay protection and idempotent broadcast responses. */ state: Idempotency.claim.Options['state'] /** Screening-cache duration. @default `Ttl.days(1)` */ screeningTtl?: number | undefined /** Per-transaction fee cap (decimal USD string). @default '1.00' */ txFeeLimit?: string | undefined } } const bodyValidation = { code: 'body_invalid', message: 'Check the MPP credential and challenge, then try again.', } as const function responses(options: { description: string; schema: schema }) { return OpenApi.responses({ errors: { 403: 'The API key does not grant MPP relay access.' }, success: options, }) } /** Resolves whether the caller may broadcast and sponsor one MPP credential. */ async function authorizeBroadcast( c: Context, options: authorizeBroadcast.Options, ) { const { input, options: options_mpp } = options const chainId = Policy.chainId(input) ?? c.get('chainId') try { Policy.assertSupportedChain({ chainId, supportedChainIds: c.get('supportedChainIds') }) return { chainId, feePayer: await Timing.time(c, 'mpp_fee_payer', () => Policy.authorizeFeePayer({ chainId, db: Db.get(c.get('db')), feePayer: options_mpp.feePayer, input, principal: Auth.getPrincipal(c) ?? undefined, }), ), type: 'authorized' as const, } } catch (cause) { return { chainId, error: MppError.map(cause), type: 'error' as const } } } declare namespace authorizeBroadcast { /** Dependencies used to authorize one MPP credential broadcast. */ type Options = { /** Credential to authorize. */ input: Mpp.RelayInput /** MPP relay configuration. */ options: mpp.Options } } /** Broadcasts an MPP credential, replaying a successful request when asked. */ async function broadcast(c: Context, options: broadcast.Options) { const { chainId, feePayer, input, options: options_mpp } = options const create = () => createAdapter(c, { chainId, options: { ...options_mpp, feePayer } }) const idempotencyKey = c.req.header('idempotency-key') || undefined if (!idempotencyKey) return { response: await create().broadcast(input) } const principal = Auth.getPrincipal(c) if (!principal || principal.type !== 'api_key') throw new Error('MPP relay requires an API key.') const identity = { apiKeyId: principal.id, idempotencyKey, inputHash: Idempotency.inputHash(input), } const claim = await Timing.time(c, 'mpp_idempotency_claim', () => Idempotency.claim({ identity, state: options_mpp.state }), ) switch (claim.type) { case 'replay': return { idempotency: 'replay' as const, response: claim.response } case 'pending': return { idempotency: 'pending' as const, response: { error: { code: Mpp.relayErrorCode.temporarilyUnavailable, message: 'MPP credential broadcast is already in progress.', }, success: false, } satisfies Mpp.BroadcastResponse, } case 'mismatch': return { response: { error: { code: Mpp.relayErrorCode.invalidPayment, message: 'Idempotency-Key was already used for a different credential.', }, success: false, } satisfies Mpp.BroadcastResponse, } case 'claimed': { const response = await create().broadcast(input) // Only a successful settlement is replayable. MPPX remains responsible // for payment-proof replay safety when a failed request is retried. if (response.success) await Idempotency.complete({ identity, response, state: options_mpp.state, token: claim.token, }) else await Idempotency.release({ identity, state: options_mpp.state, token: claim.token }) return { idempotency: 'claimed' as const, response } } } } declare namespace broadcast { /** Dependencies used to broadcast one MPP credential. */ type Options = { /** Chain selected by the MPP challenge. */ chainId: number /** Fee payer authorized for this credential. */ feePayer: Adapter.create.Options['feePayer'] /** Credential to broadcast. */ input: Mpp.RelayInput /** MPP relay configuration. */ options: mpp.Options } } function createAdapter(c: Context, options: createAdapter.Options) { const { chainId, options: options_mpp } = options const principal = Auth.getPrincipal(c) const feePayer = options_mpp.feePayer && principal?.type === 'api_key' ? Sponsorship.decorate({ account: options_mpp.feePayer, onSigned: ({ serializedTransaction }) => Sponsorship.record({ db: Db.get(c.get('db')), dbCached: Db.get(c.get('dbCached')), principal, serializedTransaction, txFeeLimit: Billing.toBaseUnits(options_mpp.txFeeLimit ?? '1.00'), }), }) : undefined return Adapter.create({ chainId, ...(feePayer ? { feePayer } : {}), getClient: (chainId) => { const resolved = chainId ?? c.get('chainId') Policy.assertSupportedChain({ chainId: resolved, supportedChainIds: c.get('supportedChainIds'), }) return c.get('getClient')(resolved) }, screeningStore: c.get('store'), ...(options_mpp.screeningTtl === undefined ? {} : { screeningTtl: options_mpp.screeningTtl }), ...(options_mpp.reportUnknown === undefined ? {} : { reportUnknown: options_mpp.reportUnknown }), state: options_mpp.state, time: (name, fn) => Timing.time(c, name, fn), }) } declare namespace createAdapter { /** Dependencies used to create a credential-specific relay adapter. */ type Options = { /** Chain selected by the MPP challenge. */ chainId: number /** MPP relay configuration. */ options: mpp.Options } } function observability( c: Context, options: { chainId: number feePayer: boolean idempotency?: 'claimed' | 'pending' | 'replay' | undefined operation: 'broadcast' | 'verify' response: Mpp.BroadcastResponse | Mpp.VerifyResponse }, ) { return { // Challenge-provided chain ids are untrusted. Keep metrics and analytics // cardinality bounded to the deployment's configured chain set. chainId: c.get('supportedChainIds').has(options.chainId) ? options.chainId : c.get('chainId'), ...(options.response.success ? {} : { errorCode: options.response.error.code }), feePayer: options.feePayer, ...(options.idempotency === undefined ? {} : { idempotency: options.idempotency }), operation: options.operation, outcome: options.response.success ? 'success' : 'failure', } as const } const openapi = { tags: [ { name: 'MPP', description: 'Submit completed MPP credentials to Tempo. Validate checks a credential without settling it; Broadcast validates, screens, and submits it. Both require `mpp:write`.', }, ], } satisfies App.create.Metadata