diff --git a/.changeset/tempo-session-fill.md b/.changeset/tempo-session-fill.md new file mode 100644 index 00000000..1a023a42 --- /dev/null +++ b/.changeset/tempo-session-fill.md @@ -0,0 +1,5 @@ +--- +'mppx': patch +--- + +Added stateless Tempo session actions for opening, topping up, voucher signing, and closing channels. diff --git a/src/tempo/Session.test.ts b/src/tempo/Session.test.ts new file mode 100644 index 00000000..13e52b31 --- /dev/null +++ b/src/tempo/Session.test.ts @@ -0,0 +1,303 @@ +import { Challenge, Credential } from 'mppx' +import type { Address, Hex } from 'viem' +import { createClient, decodeFunctionData } from 'viem' +import { Transaction } from 'viem/tempo' +import { beforeAll, describe, expect, test } from 'vp/test' +import { nodeEnv } from '~test/config.js' +import { deployEscrow, openChannel } from '~test/tempo/session.js' +import { accounts, asset, chain, fundAccount, http } from '~test/tempo/viem.js' + +import * as Methods from './Methods.js' +import * as Session from './Session.js' +import { escrowAbi } from './session/Chain.js' +import { verifyVoucher } from './session/Voucher.js' + +const isLocalnet = nodeEnv === 'localnet' + +const account = accounts[2] +const authorizedSigner = accounts[3].address +const chainId = chain.id +const currency = asset +const escrowContract = '0x1234567890abcdef1234567890abcdef12345678' as Address +const recipient = accounts[1].address +const channelId = '0x0000000000000000000000000000000000000000000000000000000000000042' as Hex + +type SessionRequest = ReturnType + +function createChallenge( + overrides: Partial[0]> = {}, +): Challenge.Challenge { + const request = Methods.session.schema.request.parse({ + amount: '1', + chainId, + currency, + decimals: 6, + escrowContract, + recipient, + unitType: 'token', + ...overrides, + }) + return Challenge.from({ + id: 'test-session-challenge-id', + intent: 'session', + method: 'tempo', + realm: 'api.example.com', + request, + }) as Challenge.Challenge +} + +describe('open.fill', () => { + test('behavior: fills open channel calls', async () => { + const client = createClient({ + account, + chain, + transport: http(), + }) + const challenge = createChallenge() + + const filled = await Session.open.fill(client, { + authorizedSigner, + challenge, + deposit: 10_000_000n, + payer: account.address, + }) + + expect(filled.kind).toBe('open') + expect(filled.chainId).toBe(chainId) + expect(filled.payer).toBe(account.address) + expect(filled.authorizedSigner).toBe(authorizedSigner) + expect(filled.calls).toHaveLength(2) + expect(filled.channelId).toMatch(/^0x[0-9a-f]{64}$/) + + const openCall = filled.calls[1] + const openData = decodeFunctionData({ + abi: escrowAbi, + data: openCall?.data ?? '0x', + }) + const openArgs = openData.args as readonly [Address, Address, bigint, Hex, Address] + expect(openArgs[0]).toBe(recipient) + expect(openArgs[1].toLowerCase()).toBe(currency.toLowerCase()) + expect(openArgs[2]).toBe(10_000_000n) + expect(openArgs[4]).toBe(authorizedSigner) + }) +}) + +describe('topUp.fill', () => { + test('behavior: fills topUp calls', async () => { + const client = createClient({ + account, + chain, + transport: http(), + }) + const challenge = createChallenge() + + const filled = await Session.topUp.fill(client, { + additionalDeposit: 5_000_000n, + challenge, + channelId, + }) + + expect(filled.kind).toBe('topUp') + expect(filled.calls).toHaveLength(2) + const topUpCall = filled.calls[1] + const topUpData = decodeFunctionData({ + abi: escrowAbi, + data: topUpCall?.data ?? '0x', + }) + expect(topUpData.args).toEqual([channelId, 5_000_000n]) + }) +}) + +describe('voucher.createCredential', () => { + test('behavior: signs voucher credential', async () => { + const client = createClient({ + account, + chain, + transport: http(), + }) + const challenge = createChallenge() + + const authorization = await Session.voucher.createCredential(client, { + challenge, + channelId, + cumulativeAmount: 2_000_000n, + signer: account, + }) + const credential = Credential.deserialize(authorization) + + expect(credential.challenge.id).toBe(challenge.id) + expect(credential.payload).toMatchObject({ + action: 'voucher', + channelId, + cumulativeAmount: '2000000', + }) + const valid = await verifyVoucher( + escrowContract, + chainId, + { + channelId, + cumulativeAmount: 2_000_000n, + signature: (credential.payload as { signature: Hex }).signature, + }, + account.address, + ) + expect(valid).toBe(true) + }) +}) + +describe('close.createCredential', () => { + test('behavior: signs close credential', async () => { + const client = createClient({ + account, + chain, + transport: http(), + }) + const challenge = createChallenge() + + const authorization = await Session.close.createCredential(client, { + challenge, + channelId, + cumulativeAmount: 3_000_000n, + signer: account, + }) + const credential = Credential.deserialize(authorization) + + expect(credential.payload).toMatchObject({ + action: 'close', + channelId, + cumulativeAmount: '3000000', + }) + const valid = await verifyVoucher( + escrowContract, + chainId, + { + channelId, + cumulativeAmount: 3_000_000n, + signature: (credential.payload as { signature: Hex }).signature, + }, + account.address, + ) + expect(valid).toBe(true) + }) +}) + +describe.runIf(isLocalnet)('topUp.createCredential with filled data', () => { + const payer = accounts[0] + let escrow: Address + let openChannelId: Hex + + beforeAll(async () => { + escrow = await deployEscrow() + await fundAccount({ address: payer.address, token: currency }) + const opened = await openChannel({ + escrow, + payer, + payee: recipient, + token: currency, + deposit: 10_000_000n, + salt: '0x1111111111111111111111111111111111111111111111111111111111111111' as Hex, + }) + openChannelId = opened.channelId + }) + + test('behavior: creates topUp credential from filled data', async () => { + const client = createClient({ + account: payer, + chain, + transport: http(), + }) + const challenge = createChallenge({ escrowContract: escrow }) + const filled = await Session.topUp.fill(client, { + additionalDeposit: 5_000_000n, + challenge, + channelId: openChannelId, + }) + + const authorization = await Session.topUp.createCredential(client, { + filled, + signer: payer, + }) + const credential = Credential.deserialize(authorization) + + expect(credential.payload).toMatchObject({ + action: 'topUp', + additionalDeposit: '5000000', + channelId: openChannelId, + type: 'transaction', + }) + + const transaction = Transaction.deserialize( + (credential.payload as { transaction: Hex }).transaction, + ) + if (!('calls' in transaction)) throw new Error('unexpected transaction type') + expect(transaction.calls).toEqual(filled.calls.map(({ data, to }) => ({ data, to }))) + }) +}) + +describe.runIf(isLocalnet)('open.createCredential', () => { + let escrow: Address + + beforeAll(async () => { + escrow = await deployEscrow() + await fundAccount({ address: account.address, token: currency }) + }) + + test('behavior: creates open credential from filled data', async () => { + const client = createClient({ + account, + chain, + transport: http(), + }) + const challenge = createChallenge({ escrowContract: escrow }) + const filled = await Session.open.fill(client, { + authorizedSigner: account.address, + challenge, + deposit: 10_000_000n, + payer: account.address, + }) + + const authorization = await Session.open.createCredential(client, { + filled, + signer: account, + }) + const credential = Credential.deserialize(authorization) + + expect(credential.challenge.id).toBe(challenge.id) + expect(credential.source).toBe(`did:pkh:eip155:${chainId}:${account.address}`) + expect(credential.payload).toMatchObject({ + action: 'open', + authorizedSigner: account.address, + channelId: filled.channelId, + cumulativeAmount: '1000000', + type: 'transaction', + }) + + const transaction = Transaction.deserialize( + (credential.payload as { transaction: Hex }).transaction, + ) + if (!('calls' in transaction)) throw new Error('unexpected transaction type') + expect(transaction.calls).toEqual(filled.calls.map(({ data, to }) => ({ data, to }))) + }) + + test('error: rejects signer that does not match filled payer', async () => { + const client = createClient({ + account, + chain, + transport: http(), + }) + const challenge = createChallenge({ escrowContract: escrow }) + const filled = await Session.open.fill(client, { + authorizedSigner: account.address, + challenge, + deposit: 10_000_000n, + payer: account.address, + }) + + await expect( + Session.open.createCredential(client, { + filled, + signer: accounts[4], + }), + ).rejects.toThrow('signer does not match filled payer.') + }) +}) diff --git a/src/tempo/Session.ts b/src/tempo/Session.ts new file mode 100644 index 00000000..62c328cf --- /dev/null +++ b/src/tempo/Session.ts @@ -0,0 +1,457 @@ +import { Hex as OxHex } from 'ox' +import type { Address, Call, Client, Hex } from 'viem' +import { encodeFunctionData } from 'viem' +import type { Account } from 'viem/accounts' +import { prepareTransactionRequest, signTransaction } from 'viem/actions' +import { Abis } from 'viem/tempo' + +import type * as Challenge from '../Challenge.js' +import * as Credential from '../Credential.js' +import { getAccountSignerAddress } from './internal/account.js' +import * as defaults from './internal/defaults.js' +import * as Methods from './Methods.js' +import { escrowAbi } from './session/Chain.js' +import * as Channel from './session/Channel.js' +import type { SessionCredentialPayload } from './session/Types.js' +import { signVoucher } from './session/Voucher.js' + +export * as Chain from './session/Chain.js' +export * as Channel from './session/Channel.js' +export * as ChannelStore from './session/ChannelStore.js' +export * as Receipt from './session/Receipt.js' +export * as Sse from './session/Sse.js' +export * as Types from './session/Types.js' +export * as Voucher from './session/Voucher.js' +export * as Ws from './session/Ws.js' + +export type SessionChallenge = Challenge.Challenge< + ReturnType, + 'session', + 'tempo' +> + +export type ChannelEntry = { + channelId: Hex + salt: Hex + cumulativeAmount: bigint + escrowContract: Address + chainId: number + opened: boolean +} + +export type { Call } + +function resolveChainId(client: Client, challenge: SessionChallenge): number { + const chainId = challenge.request.methodDetails?.chainId ?? client.chain?.id + if (chainId === undefined) + throw new Error('No `chainId` provided. Pass a chain ID in the challenge or client.') + return chainId +} + +function resolveEscrow(challenge: SessionChallenge, chainId: number, override?: Address): Address { + const escrow = + override ?? + (challenge.request.methodDetails?.escrowContract as Address | undefined) ?? + defaults.escrowContract[chainId as keyof typeof defaults.escrowContract] + if (!escrow) + throw new Error( + 'No `escrowContract` available. Provide it in parameters or ensure the server challenge includes it.', + ) + return escrow +} + +function source(chainId: number, signer: Account): string { + return `did:pkh:eip155:${chainId}:${signer.address}` +} + +function serializeCredential(parameters: { + challenge: SessionChallenge + payload: SessionCredentialPayload + chainId: number + signer: Account +}): string { + return Credential.serialize({ + challenge: parameters.challenge, + payload: parameters.payload, + source: source(parameters.chainId, parameters.signer), + }) +} + +async function signSessionTransaction( + client: Client, + parameters: { + calls: readonly Call[] + feePayer?: boolean | undefined + feeToken?: Address | undefined + signer: Account + }, +): Promise { + const { calls, feePayer, feeToken, signer } = parameters + const validBefore = Math.floor(Date.now() / 1_000) + 25 + const prepared = await prepareTransactionRequest(client, { + account: signer, + calls, + ...(feeToken ? { feeToken } : {}), + ...(feePayer ? { nonceKey: 'expiring', validBefore } : {}), + } as never) + // Estimate before enabling fee-payer mode so Tempo includes sender + // signature and access-key verification costs in the gas budget. + prepared.gas = (prepared.gas ?? 0n) + 5_000n + if (feePayer) (prepared as Record).feePayer = true + return (await signTransaction(client, prepared as never)) as Hex +} + +async function fillOpen( + client: Client, + parameters: open.fill.Parameters, +): Promise { + const { authorizedSigner, challenge, deposit, escrowContract: escrowOverride, payer } = parameters + const chainId = resolveChainId(client, challenge) + const escrowContract = resolveEscrow(challenge, chainId, escrowOverride) + const payee = challenge.request.recipient as Address + const currency = challenge.request.currency as Address + const initialAmount = BigInt(challenge.request.amount) + const feePayer = Boolean(challenge.request.methodDetails?.feePayer) + const salt = OxHex.random(32) as Hex + const channelId = Channel.computeId({ + authorizedSigner, + chainId, + escrowContract, + payee, + payer, + salt, + token: currency, + }) + const calls = [ + { + to: currency, + data: encodeFunctionData({ + abi: Abis.tip20, + functionName: 'approve', + args: [escrowContract, deposit], + }), + }, + { + to: escrowContract, + data: encodeFunctionData({ + abi: escrowAbi, + functionName: 'open', + args: [payee, currency, deposit, salt, authorizedSigner], + }), + }, + ] satisfies readonly Call[] + + return { + authorizedSigner, + calls, + chainId, + challenge, + channelId, + currency, + deposit, + escrowContract, + feePayer, + initialAmount, + kind: 'open', + payee, + payer, + salt, + } +} + +async function createOpenCredential( + client: Client, + parameters: open.createCredential.Parameters, +): Promise { + const { filled, signer, voucherSigner } = parameters + const resolvedVoucherSigner = voucherSigner ?? signer + const authorizedSigner = getAccountSignerAddress(resolvedVoucherSigner) + if (signer.address.toLowerCase() !== filled.payer.toLowerCase()) + throw new Error('signer does not match filled payer.') + if (authorizedSigner.toLowerCase() !== filled.authorizedSigner.toLowerCase()) + throw new Error('voucherSigner does not match filled authorizedSigner.') + + const transaction = await signSessionTransaction(client, { + calls: filled.calls, + feePayer: filled.feePayer, + feeToken: filled.currency, + signer, + }) + const signature = await signVoucher( + client, + signer, + { channelId: filled.channelId, cumulativeAmount: filled.initialAmount }, + filled.escrowContract, + filled.chainId, + resolvedVoucherSigner, + ) + + return serializeCredential({ + challenge: filled.challenge, + chainId: filled.chainId, + signer, + payload: { + action: 'open', + type: 'transaction', + authorizedSigner: filled.authorizedSigner, + channelId: filled.channelId, + cumulativeAmount: filled.initialAmount.toString(), + signature, + transaction, + }, + }) +} + +async function fillTopUp( + client: Client, + parameters: topUp.fill.Parameters, +): Promise { + const { additionalDeposit, challenge, channelId, escrowContract: escrowOverride } = parameters + const chainId = resolveChainId(client, challenge) + const escrowContract = resolveEscrow(challenge, chainId, escrowOverride) + const currency = challenge.request.currency as Address + const feePayer = Boolean(challenge.request.methodDetails?.feePayer) + const calls = [ + { + to: currency, + data: encodeFunctionData({ + abi: Abis.tip20, + functionName: 'approve', + args: [escrowContract, additionalDeposit], + }), + }, + { + to: escrowContract, + data: encodeFunctionData({ + abi: escrowAbi, + functionName: 'topUp', + args: [channelId, additionalDeposit], + }), + }, + ] satisfies readonly Call[] + + return { + additionalDeposit, + calls, + chainId, + challenge, + channelId, + currency, + escrowContract, + feePayer, + kind: 'topUp', + } +} + +async function createTopUpCredential( + client: Client, + parameters: topUp.createCredential.Parameters, +): Promise { + const { filled, signer } = parameters + const transaction = await signSessionTransaction(client, { + calls: filled.calls, + feePayer: filled.feePayer, + feeToken: filled.feePayer ? filled.currency : undefined, + signer, + }) + + return serializeCredential({ + challenge: filled.challenge, + chainId: filled.chainId, + signer, + payload: { + action: 'topUp', + type: 'transaction', + additionalDeposit: filled.additionalDeposit.toString(), + channelId: filled.channelId, + transaction, + }, + }) +} + +async function createVoucherCredential( + client: Client, + parameters: voucher.createCredential.Parameters, +): Promise { + const { + challenge, + channelId, + cumulativeAmount, + escrowContract: escrowOverride, + signer, + voucherSigner, + } = parameters + const chainId = resolveChainId(client, challenge) + const escrowContract = resolveEscrow(challenge, chainId, escrowOverride) + const signature = await signVoucher( + client, + signer, + { channelId, cumulativeAmount }, + escrowContract, + chainId, + voucherSigner ?? signer, + ) + + return serializeCredential({ + challenge, + chainId, + signer, + payload: { + action: 'voucher', + channelId, + cumulativeAmount: cumulativeAmount.toString(), + signature, + }, + }) +} + +async function createCloseCredential( + client: Client, + parameters: close.createCredential.Parameters, +): Promise { + const { + challenge, + channelId, + cumulativeAmount, + escrowContract: escrowOverride, + signer, + voucherSigner, + } = parameters + const chainId = resolveChainId(client, challenge) + const escrowContract = resolveEscrow(challenge, chainId, escrowOverride) + const signature = await signVoucher( + client, + signer, + { channelId, cumulativeAmount }, + escrowContract, + chainId, + voucherSigner ?? signer, + ) + + return serializeCredential({ + challenge, + chainId, + signer, + payload: { + action: 'close', + channelId, + cumulativeAmount: cumulativeAmount.toString(), + signature, + }, + }) +} + +/** Stateless helpers for opening a Tempo session channel. */ +export const open = { + fill: fillOpen, + createCredential: createOpenCredential, +} + +export declare namespace open { + namespace fill { + type Filled = { + authorizedSigner: Address + calls: readonly Call[] + chainId: number + challenge: SessionChallenge + channelId: Hex + currency: Address + deposit: bigint + escrowContract: Address + feePayer: boolean + initialAmount: bigint + kind: 'open' + payee: Address + payer: Address + salt: Hex + } + + type Parameters = { + authorizedSigner: Address + challenge: SessionChallenge + deposit: bigint + escrowContract?: Address | undefined + payer: Address + } + } + + namespace createCredential { + type Parameters = { + filled: fill.Filled + signer: Account + voucherSigner?: Account | undefined + } + } +} + +/** Stateless helpers for topping up a Tempo session channel. */ +export const topUp = { + fill: fillTopUp, + createCredential: createTopUpCredential, +} + +export declare namespace topUp { + namespace fill { + type Filled = { + additionalDeposit: bigint + calls: readonly Call[] + chainId: number + challenge: SessionChallenge + channelId: Hex + currency: Address + escrowContract: Address + feePayer: boolean + kind: 'topUp' + } + + type Parameters = { + additionalDeposit: bigint + challenge: SessionChallenge + channelId: Hex + escrowContract?: Address | undefined + } + } + + namespace createCredential { + type Parameters = { + filled: fill.Filled + signer: Account + } + } +} + +/** Stateless helper for signing the next voucher for a Tempo session. */ +export const voucher = { + createCredential: createVoucherCredential, +} + +export declare namespace voucher { + namespace createCredential { + type Parameters = { + challenge: SessionChallenge + channelId: Hex + cumulativeAmount: bigint + escrowContract?: Address | undefined + signer: Account + voucherSigner?: Account | undefined + } + } +} + +/** Stateless helper for closing a Tempo session channel. */ +export const close = { + createCredential: createCloseCredential, +} + +export declare namespace close { + namespace createCredential { + type Parameters = { + challenge: SessionChallenge + channelId: Hex + cumulativeAmount: bigint + escrowContract?: Address | undefined + signer: Account + voucherSigner?: Account | undefined + } + } +} diff --git a/src/tempo/client/Session.ts b/src/tempo/client/Session.ts index 1498eeca..f1349f0d 100644 --- a/src/tempo/client/Session.ts +++ b/src/tempo/client/Session.ts @@ -3,6 +3,7 @@ import { type Address, parseUnits, type Account as viem_Account } from 'viem' import { tempo as tempo_chain } from 'viem/tempo/chains' import type * as Challenge from '../../Challenge.js' +import * as Credential from '../../Credential.js' import * as Method from '../../Method.js' import * as Account from '../../viem/Account.js' import * as Client from '../../viem/Client.js' @@ -10,16 +11,10 @@ import * as z from '../../zod.js' import { getAccountSignerAddress } from '../internal/account.js' import * as defaults from '../internal/defaults.js' import * as Methods from '../Methods.js' +import * as SessionActions from '../Session.js' import type { SessionCredentialPayload } from '../session/Types.js' import { signVoucher } from '../session/Voucher.js' -import { - type ChannelEntry, - createOpenPayload, - createVoucherPayload, - resolveEscrow, - serializeCredential, - tryRecoverChannel, -} from './ChannelOps.js' +import { resolveEscrow, tryRecoverChannel } from './ChannelOps.js' export const sessionContextSchema = z.object({ account: z.optional(z.custom()), @@ -84,10 +79,10 @@ export function session(parameters: session.Parameters = {}) { parameters.maxDeposit !== undefined ? parseUnits(parameters.maxDeposit, decimals) : undefined const escrowContractMap = new Map() - const channels = new Map() + const channels = new Map() const channelIdToKey = new Map() - function notifyUpdate(entry: ChannelEntry) { + function notifyUpdate(entry: SessionActions.ChannelEntry) { parameters.onChannelUpdate?.(entry) } @@ -107,6 +102,23 @@ export function session(parameters: session.Parameters = {}) { return resolveEscrow(challenge, chainId, parameters.escrowContract) } + function asSessionChallenge(challenge: Challenge.Challenge): SessionActions.SessionChallenge { + return challenge as SessionActions.SessionChallenge + } + + function serializeManualCredential( + challenge: Challenge.Challenge, + payload: SessionCredentialPayload, + chainId: number, + account: viem_Account, + ): string { + return Credential.serialize({ + challenge, + payload, + source: `did:pkh:eip155:${chainId}:${account.address}`, + }) + } + async function autoManageCredential( challenge: Challenge.Challenge, account: viem_Account, @@ -172,39 +184,45 @@ export function session(parameters: session.Parameters = {}) { } } - let payload: SessionCredentialPayload - if (entry?.opened) { entry.cumulativeAmount += amount - payload = await createVoucherPayload( - client, - account, - entry.channelId, - entry.cumulativeAmount, + const credential = await SessionActions.voucher.createCredential(client, { + challenge: asSessionChallenge(challenge), + channelId: entry.channelId, + cumulativeAmount: entry.cumulativeAmount, escrowContract, - chainId, + signer: account, voucherSigner, - ) + }) notifyUpdate(entry) + return credential } else { - const result = await createOpenPayload(client, account, { + const filled = await SessionActions.open.fill(client, { + authorizedSigner: getAccountSignerAddress(voucherSigner), + challenge: asSessionChallenge(challenge), + deposit, + escrowContract, + payer: account.address, + }) + const credential = await SessionActions.open.createCredential(client, { + filled, + signer: account, voucherSigner, + }) + const entry = { + channelId: filled.channelId, + salt: filled.salt, + cumulativeAmount: amount, escrowContract, - payee, - currency, - deposit, - initialAmount: amount, chainId, - feePayer: md?.feePayer, - }) - channels.set(key, result.entry) - channelIdToKey.set(result.entry.channelId, key) - escrowContractMap.set(result.entry.channelId, escrowContract) - payload = result.payload - notifyUpdate(result.entry) + opened: true, + } satisfies SessionActions.ChannelEntry + channels.set(key, entry) + channelIdToKey.set(entry.channelId, key) + escrowContractMap.set(entry.channelId, escrowContract) + notifyUpdate(entry) + return credential } - - return serializeCredential(challenge, payload, chainId, account) } async function manualCredential( @@ -236,8 +254,6 @@ export function session(parameters: session.Parameters = {}) { const escrowContract = resolveEscrowCached(challenge, chainId, channelId) escrowContractMap.set(channelId, escrowContract) - let payload: SessionCredentialPayload - switch (action) { case 'open': { if (!transaction) throw new Error('transaction required for open action') @@ -251,43 +267,51 @@ export function session(parameters: session.Parameters = {}) { chainId, voucherSigner, ) - payload = { - action: 'open', - type: 'transaction', - channelId, - transaction: transaction as Hex.Hex, - authorizedSigner: getAccountSignerAddress(voucherSigner), - cumulativeAmount: cumulativeAmount.toString(), - signature, - } - break + return serializeManualCredential( + challenge, + { + action: 'open', + type: 'transaction', + channelId, + transaction: transaction as Hex.Hex, + authorizedSigner: getAccountSignerAddress(voucherSigner), + cumulativeAmount: cumulativeAmount.toString(), + signature, + }, + chainId, + account, + ) } - case 'topUp': + case 'topUp': { if (!transaction) throw new Error('transaction required for topUp action') if (resolvedAdditionalDeposit === undefined) throw new Error('additionalDeposit required for topUp action') - payload = { - action: 'topUp', - type: 'transaction', - channelId, - transaction: transaction as Hex.Hex, - additionalDeposit: resolvedAdditionalDeposit.toString(), - } - break + return serializeManualCredential( + challenge, + { + action: 'topUp', + type: 'transaction', + channelId, + transaction: transaction as Hex.Hex, + additionalDeposit: resolvedAdditionalDeposit.toString(), + }, + chainId, + account, + ) + } case 'voucher': { if (cumulativeAmount === undefined) throw new Error('cumulativeAmount required for voucher action') - payload = await createVoucherPayload( - client, - account, + const credential = await SessionActions.voucher.createCredential(client, { + challenge: asSessionChallenge(challenge), channelId, cumulativeAmount, escrowContract, - chainId, + signer: account, voucherSigner, - ) + }) const key = channelIdToKey.get(channelId) if (key) { const entry = channels.get(key) @@ -297,26 +321,20 @@ export function session(parameters: session.Parameters = {}) { notifyUpdate(entry) } } - break + return credential } case 'close': { if (cumulativeAmount === undefined) throw new Error('cumulativeAmount required for close action') - const signature = await signVoucher( - client, - account, - { channelId, cumulativeAmount }, + const credential = await SessionActions.close.createCredential(client, { + challenge: asSessionChallenge(challenge), + channelId, + cumulativeAmount, escrowContract, - chainId, + signer: account, voucherSigner, - ) - payload = { - action: 'close', - channelId, - cumulativeAmount: cumulativeAmount.toString(), - signature, - } + }) const closeKey = channelIdToKey.get(channelId) if (closeKey) { const entry = channels.get(closeKey) @@ -327,11 +345,9 @@ export function session(parameters: session.Parameters = {}) { notifyUpdate(entry) } } - break + return credential } } - - return serializeCredential(challenge, payload, chainId, account) } return Method.toClient(Methods.session, { @@ -368,6 +384,6 @@ export declare namespace session { /** Maximum deposit in human-readable units (e.g. "10"). Caps the server's `suggestedDeposit`. Enables auto-management like `deposit`. */ maxDeposit?: string | undefined /** Called whenever channel state changes (open, voucher, close, recovery). */ - onChannelUpdate?: ((entry: ChannelEntry) => void) | undefined + onChannelUpdate?: ((entry: SessionActions.ChannelEntry) => void) | undefined } } diff --git a/src/tempo/index.ts b/src/tempo/index.ts index c13bf32f..f9690725 100644 --- a/src/tempo/index.ts +++ b/src/tempo/index.ts @@ -1,5 +1,5 @@ export * as Charge from './Charge.js' export * as Proof from './Proof.js' export * as Methods from './Methods.js' -export * as Session from './session/index.js' +export * as Session from './Session.js' export * as Subscription from './subscription/index.js'