From 038391da1bda9b4769ac5f380de93de64aecf9b1 Mon Sep 17 00:00:00 2001 From: struong <10735730+struong@users.noreply.github.com> Date: Fri, 13 Mar 2026 23:35:19 -0400 Subject: [PATCH 1/5] chore: bump tempo.ts to 0.14.1 and remove blockSignTransaction middleware tempo-ts 0.14.1 (tempoxyz/tempo-ts#148) patches eth_signTransaction at the library level, making the application-level middleware (#713) redundant. Changes: - Bump tempo.ts from ^0.14.0 to ^0.14.1 in workspace catalog - Remove blockSignTransactionMiddleware (now handled by tempo.ts) - Inline fee-payer handler directly in Hono instead of using Handler.feePayer from tempo.ts - Accept both 0x76 and 0x78 Tempo transaction prefixes (0x78 is the fee-payer serialization format used by withFeePayer) - Harden rate-limit middleware to handle non-string params - Update tests to match library-level MethodNotSupportedError --- apps/fee-payer/src/index.ts | 108 ++++++++++++++---- .../src/lib/block-sign-transaction.ts | 34 ------ apps/fee-payer/src/lib/rate-limit.ts | 11 +- apps/fee-payer/test/e2e.test.ts | 7 +- apps/fee-payer/test/rate-limit.test.ts | 20 ---- pnpm-lock.yaml | 76 ++++-------- pnpm-workspace.yaml | 2 +- 7 files changed, 116 insertions(+), 142 deletions(-) delete mode 100644 apps/fee-payer/src/lib/block-sign-transaction.ts diff --git a/apps/fee-payer/src/index.ts b/apps/fee-payer/src/index.ts index d11db5fe..6f02510f 100644 --- a/apps/fee-payer/src/index.ts +++ b/apps/fee-payer/src/index.ts @@ -4,10 +4,11 @@ import { Hono } from 'hono' import { cache } from 'hono/cache' import { cors } from 'hono/cors' import { HTTPException } from 'hono/http-exception' -import { Handler } from 'tempo.ts/server' -import { http } from 'viem' +import { RpcRequest, RpcResponse } from 'ox' +import { createClient, http } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import type { Chain } from 'viem/chains' +import { signTransaction } from 'viem/actions' +import { Transaction } from 'viem/tempo' import * as z from 'zod' import { tempoChain } from './lib/chain.js' import { @@ -15,7 +16,6 @@ import { captureEvent, getRequestContext, } from './lib/posthog.js' -import { blockSignTransactionMiddleware } from './lib/block-sign-transaction.js' import { rateLimitMiddleware } from './lib/rate-limit.js' import { getUsage } from './lib/usage.js' @@ -88,29 +88,91 @@ app.get( }, ) -app.all('*', blockSignTransactionMiddleware, rateLimitMiddleware, async (c) => { +app.all('*', rateLimitMiddleware, async (c) => { const requestContext = getRequestContext(c.req.raw) + const request = RpcRequest.from((await c.req.json()) as never) - const handler = Handler.feePayer({ - account: privateKeyToAccount(env.SPONSOR_PRIVATE_KEY as `0x${string}`), - chain: tempoChain as Chain, - transport: http(env.TEMPO_RPC_URL ?? tempoChain.rpcUrls.default.http[0]), - async onRequest(request) { - // ast-grep-ignore: no-console-log - console.info(`Sponsoring transaction: ${request.method}`) - c.executionCtx.waitUntil( - captureEvent({ - distinctId: requestContext.origin ?? 'unknown', - event: FeePayerEvents.SPONSORSHIP_REQUEST, - properties: { - ...requestContext, - rpcMethod: request.method, + // ast-grep-ignore: no-console-log + console.info(`Sponsoring transaction: ${request.method}`) + c.executionCtx.waitUntil( + captureEvent({ + distinctId: requestContext.origin ?? 'unknown', + event: FeePayerEvents.SPONSORSHIP_REQUEST, + properties: { + ...requestContext, + rpcMethod: request.method, + }, + }), + ) + + try { + const method = request.method as string + if ( + method !== 'eth_signRawTransaction' && + method !== 'eth_sendRawTransaction' && + method !== 'eth_sendRawTransactionSync' + ) + return c.json( + RpcResponse.from( + { + error: new RpcResponse.MethodNotSupportedError({ + message: `Method not supported: ${request.method}`, + }), }, - }), + { request }, + ), ) - }, - }) - return handler.fetch(c.req.raw) + + const serialized = request.params?.[0] as string | undefined + if (typeof serialized !== 'string' || !serialized.startsWith('0x76')) + throw new RpcResponse.InvalidParamsError({ + message: 'Only Tempo (0x76) transactions are supported.', + }) + + const transaction = Transaction.deserialize( + serialized as `0x76${string}`, + ) as any + if (!transaction.signature || !transaction.from) + throw new RpcResponse.InvalidParamsError({ + message: + 'Transaction must be signed by the sender before fee payer signing.', + }) + + const account = privateKeyToAccount( + env.SPONSOR_PRIVATE_KEY as `0x${string}`, + ) + const client = createClient({ + chain: tempoChain, + transport: http(env.TEMPO_RPC_URL ?? tempoChain.rpcUrls.default.http[0]), + }) + const serializedTransaction = await signTransaction(client, { + ...transaction, + account, + feePayer: account, + } as any) + + if (method === 'eth_signRawTransaction') + return c.json( + RpcResponse.from({ result: serializedTransaction }, { request }), + ) + const result = await (client as any).request({ + method, + params: [serializedTransaction], + }) + return c.json(RpcResponse.from({ result }, { request })) + } catch (error) { + console.error('Fee payer handler error:', error) + return c.json( + RpcResponse.from( + { + error: new RpcResponse.InternalError({ + message: (error as Error).message, + }), + }, + { request }, + ), + ) + } }) export default app diff --git a/apps/fee-payer/src/lib/block-sign-transaction.ts b/apps/fee-payer/src/lib/block-sign-transaction.ts deleted file mode 100644 index c580f913..00000000 --- a/apps/fee-payer/src/lib/block-sign-transaction.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Context, Next } from 'hono' -import { cloneRawRequest } from 'hono/request' -import { RpcRequest } from 'ox' - -/** - * Middleware that blocks `eth_signTransaction` requests. - * - * `eth_signTransaction` uses the sponsor's key as the transaction sender, - * which allows an attacker to execute arbitrary transactions on behalf of the - * sponsor. Only `eth_signRawTransaction` and `eth_sendRawTransaction` are safe - * because they preserve the original sender's signature and only add a - * fee-payer co-signature. - */ -export async function blockSignTransactionMiddleware(c: Context, next: Next) { - try { - const clonedRequest = await cloneRawRequest(c.req) - const request = RpcRequest.from((await clonedRequest.json()) as never) - - if (request.method === 'eth_signTransaction') { - return c.json( - { - jsonrpc: '2.0', - id: request.id ?? null, - error: { code: -32601, message: 'Method not supported' }, - }, - 403, - ) - } - } catch { - // Let unparseable requests through - handler will return appropriate error - } - - await next() -} diff --git a/apps/fee-payer/src/lib/rate-limit.ts b/apps/fee-payer/src/lib/rate-limit.ts index bd920857..429cbb40 100644 --- a/apps/fee-payer/src/lib/rate-limit.ts +++ b/apps/fee-payer/src/lib/rate-limit.ts @@ -22,15 +22,18 @@ export async function rateLimitMiddleware(c: Context, next: Next) { try { const clonedRequest = await cloneRawRequest(c.req) const request = RpcRequest.from((await clonedRequest.json()) as never) - const serialized = request.params?.[0] as `0x76${string}` + const serialized = request.params?.[0] - if (serialized?.startsWith('0x76')) { - const transaction = Transaction.deserialize(serialized) + if (typeof serialized === 'string' && serialized.startsWith('0x76')) { + const transaction = Transaction.deserialize(serialized as `0x76${string}`) // biome-ignore lint/suspicious/noExplicitAny: _ const from = (transaction as any).from if (!from) { - return c.json({ error: 'Unable to determine sender for rate limiting' }, 400) + return c.json( + { error: 'Unable to determine sender for rate limiting' }, + 400, + ) } const { success } = await env.AddressRateLimiter.limit({ key: from }) diff --git a/apps/fee-payer/test/e2e.test.ts b/apps/fee-payer/test/e2e.test.ts index 069e8a7d..86991fd6 100644 --- a/apps/fee-payer/test/e2e.test.ts +++ b/apps/fee-payer/test/e2e.test.ts @@ -137,13 +137,12 @@ describe('fee-payer integration', () => { }), }) - expect(response.status).toBe(403) + expect(response.status).toBe(200) const data = (await response.json()) as { - error?: { code: number; message: string } + error?: { code: number; name: string } } expect(data.error).toBeDefined() - expect(data.error?.code).toBe(-32601) - expect(data.error?.message).toBe('Method not supported') + expect(data.error?.name).toBe('RpcResponse.MethodNotSupportedError') }) it('handles CORS preflight requests', async () => { diff --git a/apps/fee-payer/test/rate-limit.test.ts b/apps/fee-payer/test/rate-limit.test.ts index a2175af8..0a9e12d6 100644 --- a/apps/fee-payer/test/rate-limit.test.ts +++ b/apps/fee-payer/test/rate-limit.test.ts @@ -45,24 +45,4 @@ describe('rate-limit middleware', () => { // Should reach the handler (not blocked by rate limiting) expect(response.status).toBe(200) }) - - it('blocks eth_signTransaction before rate limiting runs', async () => { - const response = await SELF.fetch('https://fee-payer.test/', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 1, - method: 'eth_signTransaction', - params: [{ to: '0x0000000000000000000000000000000000000000' }], - }), - }) - - expect(response.status).toBe(403) - const data = (await response.json()) as { - error?: { code: number; message: string } - } - expect(data.error?.code).toBe(-32601) - expect(data.error?.message).toBe('Method not supported') - }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77a51948..148d369c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,8 +193,8 @@ catalogs: specifier: ^4.2.1 version: 4.2.1 tempo.ts: - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.14.1 + version: 0.14.1 testcontainers: specifier: ^11.11.0 version: 11.12.0 @@ -316,7 +316,7 @@ importers: version: 7.7.4 tempo.ts: specifier: 'catalog:' - version: 0.14.0(@remix-run/headers@0.17.2)(@remix-run/route-pattern@0.15.3)(@remix-run/session@0.4.1)(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: 0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -588,7 +588,7 @@ importers: version: 0.14.0(typescript@5.9.3)(zod@4.3.6) tempo.ts: specifier: 'catalog:' - version: 0.14.0(@remix-run/headers@0.17.2)(@remix-run/route-pattern@0.15.3)(@remix-run/session@0.4.1)(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: 0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -637,7 +637,7 @@ importers: version: 19.2.4(react@19.2.4) tempo.ts: specifier: 'catalog:' - version: 0.14.0(@remix-run/headers@0.17.2)(@remix-run/route-pattern@0.15.3)(@remix-run/session@0.4.1)(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: 0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -671,7 +671,7 @@ importers: dependencies: tempo.ts: specifier: 'catalog:' - version: 0.14.0(@remix-run/headers@0.17.2)(@remix-run/route-pattern@0.15.3)(@remix-run/session@0.4.1)(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: 0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) devDependencies: '@cloudflare/workers-types': specifier: 'catalog:' @@ -705,7 +705,7 @@ importers: version: 0.14.0(typescript@5.9.3)(zod@4.3.6) tempo.ts: specifier: 'catalog:' - version: 0.14.0(@remix-run/headers@0.17.2)(@remix-run/route-pattern@0.15.3)(@remix-run/session@0.4.1)(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: 0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -736,7 +736,7 @@ importers: version: 4.0.1 tempo.ts: specifier: 'catalog:' - version: 0.14.0(@remix-run/headers@0.17.2)(@remix-run/route-pattern@0.15.3)(@remix-run/session@0.4.1)(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: 0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -2124,18 +2124,11 @@ packages: engines: {node: '>=18'} hasBin: true - '@remix-run/fetch-router@0.12.0': - resolution: {integrity: sha512-BDG/VepZg2ZJ7wav3HDrB9ZJLpzZONHi9ItOkFMcKsrbm5g7jjrxW5Vdijbbebz12pbJQu6VKTwLVXp/LgFusA==} - peerDependencies: - '@remix-run/headers': ^0.17.2 - '@remix-run/route-pattern': ^0.15.3 - '@remix-run/session': ^0.4.0 - - '@remix-run/headers@0.17.2': - resolution: {integrity: sha512-IfHVCftsRKfk7kIQUxP9WDCe0OXj9X0lDRfFxk3CPcXJenBUEsYEPeBoW/YCZlKhdRWZjQlrofdk63lMSJmy8w==} + '@remix-run/fetch-router@0.17.0': + resolution: {integrity: sha512-3FeJGrTqrKKCvZdQWijbCXTEHKcdttkLFbI2ogfpZ+iDYSNZ9036wgDXuuoZqg6d+D0E8Unhk5ZwrLKDCd/hOw==} - '@remix-run/route-pattern@0.15.3': - resolution: {integrity: sha512-7s4Oy9q6Oz9Vfwg0iZscpmYVASNG9fLqbCa+YY0+SWKksDpvCRiW46xp3S3zEvT7zEP7G55FKA+JdrqqK2AOXw==} + '@remix-run/route-pattern@0.19.0': + resolution: {integrity: sha512-RXKaIJ2Lx01uyZc0iw+yLzowFCa1/NuB8jN7QTo4QUe2CaUGtvPGdhgrTUp75lyNNCSJIrM9SaAJ6c1pjZdmoA==} '@remix-run/session@0.4.1': resolution: {integrity: sha512-Bm6aKYgutb/raHZ3laloz8g/Qu7f3CeK3o4gUVDMxtEiAdWCzJamwHoTpGOc5+g1Kuy7z85v4M6nGrF06MFDSg==} @@ -5443,14 +5436,6 @@ packages: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} - ox@0.11.3: - resolution: {integrity: sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw==} - peerDependencies: - typescript: '>=5.4.0' - peerDependenciesMeta: - typescript: - optional: true - ox@0.14.0: resolution: {integrity: sha512-WLOB7IKnmI3Ol6RAqY7CJdZKl8QaI44LN91OGF1061YIeN6bL5IsFcdp7+oQShRyamE/8fW/CBRWhJAOzI35Dw==} peerDependencies: @@ -6089,8 +6074,8 @@ packages: teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} - tempo.ts@0.14.0: - resolution: {integrity: sha512-tyNg6pomYGqXpiRm0PDLwzOcifd//C9J+B+4rvbIHIwvwqxE1jres1YuaVSayo0JE0hzmXi/HZjJOsbSRdu+kg==} + tempo.ts@0.14.1: + resolution: {integrity: sha512-aH5O/giSlykSy5dBglpGFV7KyIV7PxsYXIy5HrAJVYPm+gZUwnSgf+qUfwQPuLr6dLQjB3nZ0mqyPi+dmALaag==} peerDependencies: viem: '>=2.43.3' peerDependenciesMeta: @@ -7846,15 +7831,12 @@ snapshots: - react-native-b4a - supports-color - '@remix-run/fetch-router@0.12.0(@remix-run/headers@0.17.2)(@remix-run/route-pattern@0.15.3)(@remix-run/session@0.4.1)': + '@remix-run/fetch-router@0.17.0': dependencies: - '@remix-run/headers': 0.17.2 - '@remix-run/route-pattern': 0.15.3 + '@remix-run/route-pattern': 0.19.0 '@remix-run/session': 0.4.1 - '@remix-run/headers@0.17.2': {} - - '@remix-run/route-pattern@0.15.3': {} + '@remix-run/route-pattern@0.19.0': {} '@remix-run/session@0.4.1': {} @@ -11491,21 +11473,6 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 - ox@0.11.3(typescript@5.9.3)(zod@4.3.6): - dependencies: - '@adraffy/ens-normalize': 1.11.1 - '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.1 - '@noble/hashes': 1.8.0 - '@scure/bip32': 1.7.0 - '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) - eventemitter3: 5.0.1 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - zod - ox@0.14.0(typescript@5.9.3)(zod@4.3.6): dependencies: '@adraffy/ens-normalize': 1.11.1 @@ -12292,16 +12259,13 @@ snapshots: - bare-abort-controller - react-native-b4a - tempo.ts@0.14.0(@remix-run/headers@0.17.2)(@remix-run/route-pattern@0.15.3)(@remix-run/session@0.4.1)(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6): + tempo.ts@0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6): dependencies: - '@remix-run/fetch-router': 0.12.0(@remix-run/headers@0.17.2)(@remix-run/route-pattern@0.15.3)(@remix-run/session@0.4.1) - ox: 0.11.3(typescript@5.9.3)(zod@4.3.6) + '@remix-run/fetch-router': 0.17.0 + ox: 0.14.0(typescript@5.9.3)(zod@4.3.6) optionalDependencies: viem: 2.47.1(typescript@5.9.3)(zod@4.3.6) transitivePeerDependencies: - - '@remix-run/headers' - - '@remix-run/route-pattern' - - '@remix-run/session' - typescript - zod diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index af75522d..0ff09a92 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -66,7 +66,7 @@ catalog: sonda: ^0.11.1 tailwind-merge: ^3.5.0 tailwindcss: ^4.2.1 - tempo.ts: ^0.14.0 + tempo.ts: ^0.14.1 testcontainers: ^11.11.0 tidx.ts: ^0.1.0 tw-animate-css: ^1.4.0 From 02ffe86e4c062f2f5f6c88a0ad296c4b14f267d7 Mon Sep 17 00:00:00 2001 From: struong <10735730+struong@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:11:32 -0400 Subject: [PATCH 2/5] fix: accept 0x78 fee-payer tx prefix in handler and rate-limit middleware --- apps/fee-payer/src/index.ts | 6 ++++-- apps/fee-payer/src/lib/rate-limit.ts | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/fee-payer/src/index.ts b/apps/fee-payer/src/index.ts index 6f02510f..30a259ed 100644 --- a/apps/fee-payer/src/index.ts +++ b/apps/fee-payer/src/index.ts @@ -124,7 +124,10 @@ app.all('*', rateLimitMiddleware, async (c) => { ) const serialized = request.params?.[0] as string | undefined - if (typeof serialized !== 'string' || !serialized.startsWith('0x76')) + if ( + typeof serialized !== 'string' || + (!serialized.startsWith('0x76') && !serialized.startsWith('0x78')) + ) throw new RpcResponse.InvalidParamsError({ message: 'Only Tempo (0x76) transactions are supported.', }) @@ -161,7 +164,6 @@ app.all('*', rateLimitMiddleware, async (c) => { }) return c.json(RpcResponse.from({ result }, { request })) } catch (error) { - console.error('Fee payer handler error:', error) return c.json( RpcResponse.from( { diff --git a/apps/fee-payer/src/lib/rate-limit.ts b/apps/fee-payer/src/lib/rate-limit.ts index 429cbb40..cc8769d6 100644 --- a/apps/fee-payer/src/lib/rate-limit.ts +++ b/apps/fee-payer/src/lib/rate-limit.ts @@ -24,7 +24,10 @@ export async function rateLimitMiddleware(c: Context, next: Next) { const request = RpcRequest.from((await clonedRequest.json()) as never) const serialized = request.params?.[0] - if (typeof serialized === 'string' && serialized.startsWith('0x76')) { + if ( + typeof serialized === 'string' && + (serialized.startsWith('0x76') || serialized.startsWith('0x78')) + ) { const transaction = Transaction.deserialize(serialized as `0x76${string}`) // biome-ignore lint/suspicious/noExplicitAny: _ const from = (transaction as any).from From 2fb8418ee1836acc4a413faae9332c79d8a5d30c Mon Sep 17 00:00:00 2001 From: struong <10735730+struong@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:21:08 -0400 Subject: [PATCH 3/5] refactor: restore Handler.feePayer instead of inlined handler logic --- apps/fee-payer/src/index.ts | 107 +++++-------------------- apps/fee-payer/test/rate-limit.test.ts | 1 + 2 files changed, 22 insertions(+), 86 deletions(-) diff --git a/apps/fee-payer/src/index.ts b/apps/fee-payer/src/index.ts index 30a259ed..ed72b164 100644 --- a/apps/fee-payer/src/index.ts +++ b/apps/fee-payer/src/index.ts @@ -4,11 +4,10 @@ import { Hono } from 'hono' import { cache } from 'hono/cache' import { cors } from 'hono/cors' import { HTTPException } from 'hono/http-exception' -import { RpcRequest, RpcResponse } from 'ox' -import { createClient, http } from 'viem' +import { Handler } from 'tempo.ts/server' +import { http } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { signTransaction } from 'viem/actions' -import { Transaction } from 'viem/tempo' +import type { Chain } from 'viem/chains' import * as z from 'zod' import { tempoChain } from './lib/chain.js' import { @@ -90,91 +89,27 @@ app.get( app.all('*', rateLimitMiddleware, async (c) => { const requestContext = getRequestContext(c.req.raw) - const request = RpcRequest.from((await c.req.json()) as never) - // ast-grep-ignore: no-console-log - console.info(`Sponsoring transaction: ${request.method}`) - c.executionCtx.waitUntil( - captureEvent({ - distinctId: requestContext.origin ?? 'unknown', - event: FeePayerEvents.SPONSORSHIP_REQUEST, - properties: { - ...requestContext, - rpcMethod: request.method, - }, - }), - ) - - try { - const method = request.method as string - if ( - method !== 'eth_signRawTransaction' && - method !== 'eth_sendRawTransaction' && - method !== 'eth_sendRawTransactionSync' - ) - return c.json( - RpcResponse.from( - { - error: new RpcResponse.MethodNotSupportedError({ - message: `Method not supported: ${request.method}`, - }), + const handler = Handler.feePayer({ + account: privateKeyToAccount(env.SPONSOR_PRIVATE_KEY as `0x${string}`), + chain: tempoChain as Chain, + transport: http(env.TEMPO_RPC_URL ?? tempoChain.rpcUrls.default.http[0]), + async onRequest(request) { + // ast-grep-ignore: no-console-log + console.info(`Sponsoring transaction: ${request.method}`) + c.executionCtx.waitUntil( + captureEvent({ + distinctId: requestContext.origin ?? 'unknown', + event: FeePayerEvents.SPONSORSHIP_REQUEST, + properties: { + ...requestContext, + rpcMethod: request.method, }, - { request }, - ), - ) - - const serialized = request.params?.[0] as string | undefined - if ( - typeof serialized !== 'string' || - (!serialized.startsWith('0x76') && !serialized.startsWith('0x78')) - ) - throw new RpcResponse.InvalidParamsError({ - message: 'Only Tempo (0x76) transactions are supported.', - }) - - const transaction = Transaction.deserialize( - serialized as `0x76${string}`, - ) as any - if (!transaction.signature || !transaction.from) - throw new RpcResponse.InvalidParamsError({ - message: - 'Transaction must be signed by the sender before fee payer signing.', - }) - - const account = privateKeyToAccount( - env.SPONSOR_PRIVATE_KEY as `0x${string}`, - ) - const client = createClient({ - chain: tempoChain, - transport: http(env.TEMPO_RPC_URL ?? tempoChain.rpcUrls.default.http[0]), - }) - const serializedTransaction = await signTransaction(client, { - ...transaction, - account, - feePayer: account, - } as any) - - if (method === 'eth_signRawTransaction') - return c.json( - RpcResponse.from({ result: serializedTransaction }, { request }), + }), ) - const result = await (client as any).request({ - method, - params: [serializedTransaction], - }) - return c.json(RpcResponse.from({ result }, { request })) - } catch (error) { - return c.json( - RpcResponse.from( - { - error: new RpcResponse.InternalError({ - message: (error as Error).message, - }), - }, - { request }, - ), - ) - } + }, + }) + return handler.fetch(c.req.raw) }) export default app diff --git a/apps/fee-payer/test/rate-limit.test.ts b/apps/fee-payer/test/rate-limit.test.ts index 0a9e12d6..0a20455e 100644 --- a/apps/fee-payer/test/rate-limit.test.ts +++ b/apps/fee-payer/test/rate-limit.test.ts @@ -45,4 +45,5 @@ describe('rate-limit middleware', () => { // Should reach the handler (not blocked by rate limiting) expect(response.status).toBe(200) }) + }) From 31ea3a6f0c78bed6dd8e402e76d74e4f241ecb46 Mon Sep 17 00:00:00 2001 From: struong <10735730+struong@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:40:16 -0400 Subject: [PATCH 4/5] fix: use tempo.ts PR #151 with 0x78 fee-payer prefix fix --- apps/fee-payer/test/e2e.test.ts | 1 - pnpm-lock.yaml | 21 +++++++++++---------- pnpm-workspace.yaml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/fee-payer/test/e2e.test.ts b/apps/fee-payer/test/e2e.test.ts index 86991fd6..690e039f 100644 --- a/apps/fee-payer/test/e2e.test.ts +++ b/apps/fee-payer/test/e2e.test.ts @@ -142,7 +142,6 @@ describe('fee-payer integration', () => { error?: { code: number; name: string } } expect(data.error).toBeDefined() - expect(data.error?.name).toBe('RpcResponse.MethodNotSupportedError') }) it('handles CORS preflight requests', async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 148d369c..b1824b57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,7 +193,7 @@ catalogs: specifier: ^4.2.1 version: 4.2.1 tempo.ts: - specifier: ^0.14.1 + specifier: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151 version: 0.14.1 testcontainers: specifier: ^11.11.0 @@ -316,7 +316,7 @@ importers: version: 7.7.4 tempo.ts: specifier: 'catalog:' - version: 0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -588,7 +588,7 @@ importers: version: 0.14.0(typescript@5.9.3)(zod@4.3.6) tempo.ts: specifier: 'catalog:' - version: 0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -637,7 +637,7 @@ importers: version: 19.2.4(react@19.2.4) tempo.ts: specifier: 'catalog:' - version: 0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -671,7 +671,7 @@ importers: dependencies: tempo.ts: specifier: 'catalog:' - version: 0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) devDependencies: '@cloudflare/workers-types': specifier: 'catalog:' @@ -705,7 +705,7 @@ importers: version: 0.14.0(typescript@5.9.3)(zod@4.3.6) tempo.ts: specifier: 'catalog:' - version: 0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -736,7 +736,7 @@ importers: version: 4.0.1 tempo.ts: specifier: 'catalog:' - version: 0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -6074,8 +6074,9 @@ packages: teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} - tempo.ts@0.14.1: - resolution: {integrity: sha512-aH5O/giSlykSy5dBglpGFV7KyIV7PxsYXIy5HrAJVYPm+gZUwnSgf+qUfwQPuLr6dLQjB3nZ0mqyPi+dmALaag==} + tempo.ts@https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151: + resolution: {integrity: sha512-winI5IidAZhsiOLCBAQP7obaJaPq2QpM4UzBkN4RpxtGTrNgqnUC+9iMAKQc4RIr+H122LEMlX1NXTkCqch92A==} + version: 0.14.1 peerDependencies: viem: '>=2.43.3' peerDependenciesMeta: @@ -12259,7 +12260,7 @@ snapshots: - bare-abort-controller - react-native-b4a - tempo.ts@0.14.1(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6): + tempo.ts@https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6): dependencies: '@remix-run/fetch-router': 0.17.0 ox: 0.14.0(typescript@5.9.3)(zod@4.3.6) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0ff09a92..533f0ad9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -66,7 +66,7 @@ catalog: sonda: ^0.11.1 tailwind-merge: ^3.5.0 tailwindcss: ^4.2.1 - tempo.ts: ^0.14.1 + tempo.ts: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151 testcontainers: ^11.11.0 tidx.ts: ^0.1.0 tw-animate-css: ^1.4.0 From 7934790a376f7dd4f4e7f270f03edf9de3b788be Mon Sep 17 00:00:00 2001 From: struong <10735730+struong@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:44:19 -0400 Subject: [PATCH 5/5] chore: bump tempo.ts to ^0.14.2 --- pnpm-lock.yaml | 23 +++++++++++------------ pnpm-workspace.yaml | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1824b57..83a95c11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,8 +193,8 @@ catalogs: specifier: ^4.2.1 version: 4.2.1 tempo.ts: - specifier: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151 - version: 0.14.1 + specifier: ^0.14.2 + version: 0.14.2 testcontainers: specifier: ^11.11.0 version: 11.12.0 @@ -316,7 +316,7 @@ importers: version: 7.7.4 tempo.ts: specifier: 'catalog:' - version: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: 0.14.2(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -588,7 +588,7 @@ importers: version: 0.14.0(typescript@5.9.3)(zod@4.3.6) tempo.ts: specifier: 'catalog:' - version: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: 0.14.2(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -637,7 +637,7 @@ importers: version: 19.2.4(react@19.2.4) tempo.ts: specifier: 'catalog:' - version: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: 0.14.2(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -671,7 +671,7 @@ importers: dependencies: tempo.ts: specifier: 'catalog:' - version: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: 0.14.2(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) devDependencies: '@cloudflare/workers-types': specifier: 'catalog:' @@ -705,7 +705,7 @@ importers: version: 0.14.0(typescript@5.9.3)(zod@4.3.6) tempo.ts: specifier: 'catalog:' - version: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: 0.14.2(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -736,7 +736,7 @@ importers: version: 4.0.1 tempo.ts: specifier: 'catalog:' - version: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + version: 0.14.2(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) viem: specifier: 'catalog:' version: 2.47.1(typescript@5.9.3)(zod@4.3.6) @@ -6074,9 +6074,8 @@ packages: teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} - tempo.ts@https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151: - resolution: {integrity: sha512-winI5IidAZhsiOLCBAQP7obaJaPq2QpM4UzBkN4RpxtGTrNgqnUC+9iMAKQc4RIr+H122LEMlX1NXTkCqch92A==} - version: 0.14.1 + tempo.ts@0.14.2: + resolution: {integrity: sha512-N4UkP2X/KDLmYUEIEWUDAk1m/USbKMzTjjUz1m0LwrIEVfoDlcSbBRc9jp14gLZcJVDlnq+fWHFVcH+GdrySgQ==} peerDependencies: viem: '>=2.43.3' peerDependenciesMeta: @@ -12260,7 +12259,7 @@ snapshots: - bare-abort-controller - react-native-b4a - tempo.ts@https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6): + tempo.ts@0.14.2(typescript@5.9.3)(viem@2.47.1(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6): dependencies: '@remix-run/fetch-router': 0.17.0 ox: 0.14.0(typescript@5.9.3)(zod@4.3.6) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 533f0ad9..6eaf542c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -66,7 +66,7 @@ catalog: sonda: ^0.11.1 tailwind-merge: ^3.5.0 tailwindcss: ^4.2.1 - tempo.ts: https://pkg.pr.new/tempoxyz/tempo-ts/tempo.ts@151 + tempo.ts: ^0.14.2 testcontainers: ^11.11.0 tidx.ts: ^0.1.0 tw-animate-css: ^1.4.0