Skip to content

feat: add TIP-1034 tempo session#510

Open
brendanjryan wants to merge 6 commits into
wevm:mainfrom
brendanjryan:brendanjryan/tempo-session-refactor
Open

feat: add TIP-1034 tempo session#510
brendanjryan wants to merge 6 commits into
wevm:mainfrom
brendanjryan:brendanjryan/tempo-session-refactor

Conversation

@brendanjryan
Copy link
Copy Markdown
Collaborator

@brendanjryan brendanjryan commented Jun 4, 2026

Summary

Implements the new tempo/session flow on the TIP-1034 precompile.

The PR makes tempo.session a precompile-backed session interface for HTTP, SSE, and WebSocket payments, while moving the previous channel implementation behind explicit legacy exports.

What This PR Implements

  • tempo.session client and server implementations backed by the TIP-1034 TIP20EscrowChannel precompile
  • explicit tempo.session.client, tempo.session.server, and tempo.session.precompile module boundaries
  • legacy session code isolated under tempo/legacy and exposed through sessionLegacy APIs
  • client session manager for HTTP fetch, SSE streams, WebSocket streams, manual top-up, and cooperative close
  • server-side channel state, voucher verification, settlement, SSE metering, and WebSocket metering for precompile sessions
  • server-provided session snapshots in request.methodDetails.sessionSnapshot so clients can resume from server state without local persistence
  • browser playground for manual, auto-managed, and SSE session flows using pathUSD

End-to-End Flow

sequenceDiagram
    participant Client
    participant Manager as SessionManager
    participant Server
    participant Store as Channel store
    participant Chain as TIP-1034 precompile

    Client->>Manager: fetch/sse/ws paid resource
    Manager->>Server: request without credential
    Server->>Store: resolve reusable channel snapshot
    Server-->>Manager: 402 tempo.session challenge

    alt no reusable channel
        Manager->>Chain: open channel transaction
        Manager->>Server: retry with open credential + initial voucher
    else reusable snapshot available
        Manager->>Manager: hydrate runtime from server snapshot
        Manager->>Server: retry with voucher credential
    end

    Server->>Chain: verify/open/top-up/settle as needed
    Server->>Store: persist channel accounting
    Server-->>Manager: 200 + Payment-Receipt
    Manager->>Manager: reconcile receipt into state machine

    loop streaming or repeated requests
        Server-->>Manager: need-voucher event when headroom is low
        alt required cumulative exceeds deposit
            Manager->>Chain: top-up transaction
            Manager->>Server: post top-up credential
        end
        Manager->>Server: post/send voucher credential
        Server-->>Manager: receipt
    end

    Client->>Manager: close()
    Manager->>Server: close credential
    alt close challenge expired
        Server-->>Manager: fresh 402 tempo.session challenge
        Manager->>Server: retry close credential
    end
    Server->>Chain: close channel
    Server-->>Manager: close receipt
Loading

Server Behavior

tempo.session() now creates a precompile-backed server method. It validates open, top-up, voucher, and close credentials against the canonical TIP-1034 channel descriptor, tracks accepted cumulative vouchers separately from spent units, and can settle channels on a configured schedule.

The server attaches reusable session state to challenges when it can resolve a channel. The snapshot includes the channel descriptor, deposit, accepted cumulative amount, required cumulative amount, settled amount, spent amount, close state, and unit count. This lets clients hydrate from the server as the source of session state instead of requiring local persistence.

SSE and WebSocket transports use the same channel accounting model. Both can request additional voucher headroom, enforce local spend accounting, and drive top-up/voucher management posts without duplicating settlement logic.

Client Behavior

The client-side sessionManager() owns a pure state-machine runtime for session lifecycle transitions. It opens channels, signs incremental vouchers, posts top-ups when the server asks for more deposit, reconciles receipts, and cooperatively closes the channel.

HTTP, SSE, and WebSocket paths share the same credential state and voucher policy. The client keeps deposit, spent, and cumulative voucher authorization explicit so server snapshots can hydrate state without letting the server inflate the next signed voucher boundary.

close() now retries once when the close credential was signed against an expired challenge and the server returns a fresh tempo/session challenge.

Public Interfaces and Controls

  • tempo.session.method() creates the precompile-backed client payment method for Mppx.create()
  • tempo.session() creates the auto-driving client session manager
  • tempo.session() on the server creates the precompile-backed server method
  • tempo.session.settle() and tempo.session.settleBatch() expose server settlement controls
  • sessionManager.fetch(), sessionManager.sse(), and sessionManager.ws() power HTTP, SSE, and WebSocket flows
  • sessionManager.topUp() and sessionManager.close() expose manual lifecycle controls
  • VITE_TEMPO_NETWORK=localnet|moderato switches the playground and tests between Docker localnet and Tempo Moderato
  • VITE_RPC_URL overrides the selected network RPC URL

API and Plumbing Changes

  • core constants are centralized in src/Constants.ts
  • session files are organized under src/tempo/session/client, src/tempo/session/server, and src/tempo/session/precompile
  • old session modules are isolated under src/tempo/legacy
  • Fetch.from() and transport helpers understand the session challenge/retry flow used for automatic voucher management
  • test network setup resolves localnet, Moderato, or no-network mode from one configuration path

Compatibility

  • the old session implementation remains available through explicit legacy exports
  • existing HTTP payment flows continue to use the same MPP challenge, credential, and receipt headers
  • session snapshot fields are additive under request.methodDetails

Public Interfaces

Client

Default managed client. The server challenge supplies protocol details such as chainId, escrowContract, operator, fee-payer support, suggested deposit, and reusable session snapshots.

import { tempo } from 'mppx/client'
import { createClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x...')
const client = createClient({
  account,
  transport: http(),
})

const session = tempo.session({
  account,
  client,
  maxDeposit: '1',
})

const response = await session.fetch('/api/click')
console.log(response.receipt, response.channelId, response.cumulative)

for await (const chunk of await session.sse('/api/stream')) {
  console.log(chunk)
}

const socket = await session.ws('ws://localhost:5174/api/ws')
socket.addEventListener('message', (event) => console.log(event.data))

await session.topUp('0.25')
await session.close()

Managed client store for channel hints and restart recovery. The manager sends stored open channels as Payment-Session on HTTP requests and WebSocket probes. Servers can read that hint in resolveChannelId() and return a snapshot; if no snapshot is returned, the client can use the stored descriptor/cumulative data as a fallback voucher context.

const session = tempo.session({
  account,
  client,
  maxDeposit: '1',
  sessionStore: {
    get() {
      return JSON.parse(localStorage.getItem('mppx-session') ?? 'null')
    },
    set(channel) {
      localStorage.setItem('mppx-session', JSON.stringify(channel))
    },
    delete() {
      localStorage.removeItem('mppx-session')
    },
  },
})

Managed client options:

const session = tempo.session({
  account,
  client, // shorthand for getClient: () => client
  authorizedSigner: account.address,
  decimals: 6, // only parses local human-readable maxDeposit/topUp values
  escrow: '0x4d50500000000000000000000000000000000000', // optional local override
  fetch: globalThis.fetch,
  maxDeposit: '1', // local cap; server still suggests deposit/top-up requirements
  webSocket: globalThis.WebSocket,
})

Low-level client method for Mppx.create():

import { Mppx, tempo } from 'mppx/client'

const mppx = Mppx.create({
  methods: [
    tempo.session.method({
      account,
      getClient: () => client,
      authorizedSigner: account.address,
      decimals: 6,
      escrow: '0x4d50500000000000000000000000000000000000',
      maxDeposit: '1',
      onChannelUpdate(channel) {
        console.log(channel.channelId, channel.cumulativeAmount, channel.deposit)
      },
    }),
  ],
})

Server

Default server method. The server owns session policy and advertises it in the challenge.

import { Mppx, Store, tempo } from 'mppx/server'
import { createClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { tempoLocalnet } from 'viem/chains'

const account = privateKeyToAccount('0x...')
const store = Store.memory()
const client = createClient({
  account,
  chain: tempoLocalnet,
  transport: http('http://localhost:18545'),
})

const mppx = Mppx.create({
  secretKey: 'session-secret',
  methods: [
    tempo.session({
      account,
      getClient: () => client,
      store,
      amount: '0.00005',
      currency: '0x20c0000000000000000000000000000000000001',
      recipient: account.address,
      unitType: 'request',
    }),
  ],
})

Server options:

const method = tempo.session({
  account,
  getClient: () => client,
  store,
  amount: '0.00005',
  currency: '0x20c0000000000000000000000000000000000001',
  recipient: account.address,
  decimals: 6,
  unitType: 'request',
  chainId: tempoLocalnet.id,
  escrowContract: '0x4d50500000000000000000000000000000000000',
  operator: '0x0000000000000000000000000000000000000000',
  feeToken: '0x20c0000000000000000000000000000000000001',
  feePayer: true,
  feePayerPolicy: {},
  channelStateTtl: 5_000,
  minVoucherDelta: '0',
  resolveChannelId({ request, credential, paymentRequest, store }) {
    // Explicit credential/request channel IDs are used before this hook.
    // Client session stores send this hint automatically on the next request.
    const hintedChannelId = request?.headers.get('Payment-Session')
    if (hintedChannelId) return hintedChannelId

    // Apps can also use cookies, auth headers, route params, or an
    // app-maintained payer/session index to find the latest open channel.
    const sessionId = request?.headers.get('cookie')?.match(/sid=([^;]+)/)?.[1]
    if (!sessionId) return undefined
    return channelIdBySession.get(sessionId)
  },
  suggestedDeposit: '0.001',
  settlementSchedule: {
    units: 10,
    amount: '0.01',
    intervalMs: 60_000,
  },
  sse: {
    poll: true,
  },
})

Server lifecycle controls:

const channel = await tempo.session.charge(channelId, 50n)
const settleTx = await tempo.session.settle(channel.channelId)
const batchSettleTx = await tempo.session.settleBatch([channel.channelId])

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 4, 2026

Open in StackBlitz

npm i https://pkg.pr.new/mppx@510

commit: f972b89

@brendanjryan brendanjryan marked this pull request as ready for review June 4, 2026 14:38
@socket-security
Copy link
Copy Markdown

socket-security Bot commented Jun 4, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedvite@​8.0.16991008298100

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant