Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/remote-fee-payer-transport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'viem': patch
---

Added `withRemoteFeePayer` to route sponsored transaction fills to a remote fee-payer service while broadcasting through the default transport.
2 changes: 2 additions & 0 deletions site/pages/tempo/transports/withFeePayer.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# `withFeePayer`

Deprecated alias for [`withRelay`](https://docs.tempo.xyz/tempo/transports/withRelay).
Use [`withRemoteFeePayer`](https://docs.tempo.xyz/tempo/transports/withRemoteFeePayer)
for a fill-only remote fee-payer service.

Creates a transport that routes transactions to a relay service when a `feePayer` is requested on an action.

Expand Down
59 changes: 59 additions & 0 deletions site/pages/tempo/transports/withRemoteFeePayer.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# `withRemoteFeePayer`

Creates a transport that sends sponsored transaction fills to a remote fee-payer service while keeping transaction broadcast on the default transport.

- [View Guide](https://docs.tempo.xyz/guide/payments/sponsor-user-fees)
- [View Specification](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction)

Only `eth_fillTransaction` requests with `feePayer: true` are sent to the remote fee payer. Every other request, including `eth_sendRawTransaction` and `eth_sendRawTransactionSync`, uses the default transport.

The remote fee payer must return a sponsored transaction from `eth_fillTransaction`. Use [`withRelay`](/tempo/transports/withRelay) when the service signs or broadcasts through separate RPC methods.

## Usage

:::code-group

```ts twoslash [example.ts]
import { privateKeyToAccount } from 'viem/accounts'
import { createClient, http, withRemoteFeePayer } from 'viem/tempo'

const client = createClient({
account: privateKeyToAccount('0x...'),
testnet: true,
transport: withRemoteFeePayer(
http(), // ← Default Transport
http('https://fee-payer.example.com'), // ← Remote Fee Payer Transport // [!code hl]
),
})

const receipt = await client.sendTransactionSync({
feePayer: true, // [!code hl]
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb',
})
```

```ts twoslash [viem.config.ts] filename="viem.config.ts"
// [!include ~/snippets/tempo/viem.config.ts:setup]
```

:::

## Return Type

```ts
type ReturnType = Transport<'remoteFeePayer'>
```

## Parameters

### defaultTransport

- **Type:** `Transport`

The transport used for unsponsored fills and every other RPC method, including transaction broadcast.

### remoteFeePayerTransport

- **Type:** `Transport`

The remote fee-payer transport used only for `eth_fillTransaction` requests with `feePayer: true`.
4 changes: 4 additions & 0 deletions site/vocs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3324,6 +3324,10 @@ export default defineConfig({
{
text: 'Transports',
items: [
{
text: 'withRemoteFeePayer',
link: '/tempo/transports/withRemoteFeePayer',
},
{
text: 'withRelay',
link: '/tempo/transports/withRelay',
Expand Down
48 changes: 46 additions & 2 deletions src/tempo/Transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ import {
getClient,
http,
} from '~test/tempo/config.js'
import { walletNamespaceCompat, withFeePayer, withRelay } from './Transport.js'
import {
walletNamespaceCompat,
withFeePayer,
withRelay,
withRemoteFeePayer,
} from './Transport.js'

describe('withRelay', () => {
describe('relay transports', () => {
let server: Http.Server
let overrideSponsorFields = false
let overrideSponsorNonce = false
Expand Down Expand Up @@ -638,6 +643,45 @@ describe('withRelay', () => {
})
})
})

describe('withRemoteFeePayer', () => {
const client = getClient({
transport: withRemoteFeePayer(http(), http('http://localhost:3051')),
})

test('behavior: routes sponsored fill only and broadcasts with default transport', async () => {
sponsorFills = true
const account = privateKeyToAccount(
'0xecc3fe55647412647e5c6b657c496803b08ef956f927b7a821da298cfbdd9666',
)

const receipt = await sendTransactionSync(client, {
account,
feePayer: true,
to: '0x0000000000000000000000000000000000000004',
})

expect(receipt.status).toBe('success')
expect(receipt.feePayer).toBe(accounts[0].address.toLowerCase())
expect(relayRequests.map(({ method }) => method)).toEqual([
'eth_fillTransaction',
])
})

test('behavior: routes unsponsored fill through default transport', async () => {
await client.request({
method: 'eth_fillTransaction',
params: [
{
from: accounts[0].address,
to: '0x0000000000000000000000000000000000000005',
},
],
})

expect(relayRequests).toHaveLength(0)
})
})
})

describe('walletNamespaceCompat', () => {
Expand Down
57 changes: 56 additions & 1 deletion src/tempo/Transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,63 @@ type RelayProxyParameters = {
}

export type FeePayer = Transport<typeof withFeePayer.type>
export type RemoteFeePayer = Transport<typeof withRemoteFeePayer.type>
export type Relay = Transport<typeof withRelay.type>

/**
* Creates a remote fee-payer transport that routes sponsored
* `eth_fillTransaction` requests to a fee-payer service.
*
* All other requests, including transaction broadcast, use the default
* transport. The fee-payer service must return a sponsored transaction from
* `eth_fillTransaction` without requiring a later signing or broadcast request.
*
* @param defaultTransport - The default transport to use.
* @param remoteFeePayerTransport - The remote fee-payer transport to use for sponsored fills.
* @returns A remote fee-payer transport.
*/
export function withRemoteFeePayer(
defaultTransport: Transport,
remoteFeePayerTransport: Transport,
): withRemoteFeePayer.ReturnValue {
return (config) => {
const transport_default = defaultTransport(config)
const transport_remoteFeePayer = remoteFeePayerTransport(config)

return createTransport({
key: withRemoteFeePayer.type,
name: 'Remote Fee Payer Proxy',
async request({ method, params }, options) {
if (method === 'eth_fillTransaction') {
const request = (params as readonly unknown[] | undefined)?.[0]
if (
request &&
typeof request === 'object' &&
'feePayer' in request &&
request.feePayer === true
)
return transport_remoteFeePayer.request(
{ method, params },
options,
) as never
}

return (await transport_default.request(
{ method, params },
options,
)) as never
},
type: withRemoteFeePayer.type,
})
}
}

export declare namespace withRemoteFeePayer {
export const type = 'remoteFeePayer'

export type ReturnValue = RemoteFeePayer
}

/**
* Creates a relay transport that routes requests between
* the default transport or the relay transport.
Expand Down Expand Up @@ -170,7 +225,7 @@ export declare namespace withRelay {
export type ReturnValue = Relay
}

/** @deprecated Use `withRelay` instead. */
/** @deprecated Use `withRelay` or `withRemoteFeePayer` instead. */
export function withFeePayer(
defaultTransport: Transport,
relayTransport: Transport,
Expand Down
1 change: 1 addition & 0 deletions src/tempo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export {
walletNamespaceCompat,
withFeePayer,
withRelay,
withRemoteFeePayer,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the Tempo entrypoint export snapshot

Adding this named export changes Object.keys(tempo), but the inline snapshot in src/tempo/index.test.ts still ends with withRelay and therefore fails on every test run. Add withRemoteFeePayer to that snapshot so the entrypoint test reflects the new public API.

Useful? React with 👍 / 👎.

} from './Transport.js'
export * as WebAuthnP256 from './WebAuthnP256.js'
export * as WebCryptoP256 from './WebCryptoP256.js'
Expand Down
Loading