From 4612cec6189072b70f597cd966148c1144aaf3a2 Mon Sep 17 00:00:00 2001 From: Tony D'Addeo Date: Thu, 28 May 2026 15:42:52 -0500 Subject: [PATCH] feat(tempo): add stateless charge actions --- .changeset/tempo-charge-fill.md | 5 + src/tempo/Charge.test.ts | 224 ++++++++++++++++++++++++++++++++ src/tempo/Charge.ts | 215 ++++++++++++++++++++++++++++++ src/tempo/client/Charge.test.ts | 91 +++++-------- src/tempo/client/Charge.ts | 149 ++------------------- src/tempo/index.ts | 1 + src/tempo/internal/auto-swap.ts | 4 +- 7 files changed, 493 insertions(+), 196 deletions(-) create mode 100644 .changeset/tempo-charge-fill.md create mode 100644 src/tempo/Charge.test.ts create mode 100644 src/tempo/Charge.ts diff --git a/.changeset/tempo-charge-fill.md b/.changeset/tempo-charge-fill.md new file mode 100644 index 00000000..6fc451f5 --- /dev/null +++ b/.changeset/tempo-charge-fill.md @@ -0,0 +1,5 @@ +--- +'mppx': patch +--- + +Added Tempo Charge module for stateless fill and credential creation. diff --git a/src/tempo/Charge.test.ts b/src/tempo/Charge.test.ts new file mode 100644 index 00000000..373b9e95 --- /dev/null +++ b/src/tempo/Charge.test.ts @@ -0,0 +1,224 @@ +import { Challenge, Credential } from 'mppx' +import type * as Hex from 'ox/Hex' +import { createClient, type Address } from 'viem' +import { + Account as TempoAccount, + KeyAuthorizationManager, + Secp256k1, + Transaction, +} from 'viem/tempo' +import { describe, expect, test } from 'vp/test' +import { accounts, asset, chain, http } from '~test/tempo/viem.js' + +import * as Charge from './Charge.js' +import * as Methods from './Methods.js' + +const account = accounts[1] +const chainId = chain.id +const currency = asset +const recipient = '0x2222222222222222222222222222222222222222' as Address +const splitRecipient = '0x4444444444444444444444444444444444444444' as Address +const rootAccount = TempoAccount.fromSecp256k1( + '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', +) + +type ChargeRequest = ReturnType + +function createChallenge( + overrides: Partial[0]> = {}, +): Challenge.Challenge { + const request = Methods.charge.schema.request.parse({ + amount: '1', + chainId, + currency, + decimals: 6, + recipient, + ...overrides, + }) + return Challenge.from({ + id: 'test-challenge-id', + intent: 'charge', + method: 'tempo', + realm: 'api.example.com', + request, + }) as Challenge.Challenge +} + +async function createAccessKey() { + const keyAuthorizationManager = KeyAuthorizationManager.memory() + const accessKey = TempoAccount.fromSecp256k1(Secp256k1.randomPrivateKey(), { + access: rootAccount, + keyAuthorizationManager, + }) + const keyAuthorization = await rootAccount.signKeyAuthorization( + { + accessKeyAddress: accessKey.accessKeyAddress, + keyType: accessKey.keyType, + }, + { chainId: BigInt(chainId) }, + ) + await keyAuthorizationManager.set( + { + accessKey: accessKey.accessKeyAddress, + address: rootAccount.address, + chainId, + }, + keyAuthorization, + ) + return { accessKey, keyAuthorization } +} + +describe('fill', () => { + test('behavior: fills split payment calls', async () => { + const client = createClient({ + account, + chain, + transport: http(), + }) + const filled = await Charge.fill(client, { + challenge: createChallenge({ + splits: [{ amount: '0.25', recipient: splitRecipient }], + }), + payer: account.address, + }) + + expect(filled.kind).toBe('calls') + if (filled.kind !== 'calls') throw new Error('expected filled calls') + expect(filled.chainId).toBe(chainId) + expect(filled.payer).toBe(account.address) + expect(filled.supportedModes).toEqual(['pull', 'push']) + expect(filled.calls).toHaveLength(2) + }) + + test('error: rejects unexpected split recipients', async () => { + const client = createClient({ + account, + chain, + transport: http(), + }) + + await expect( + Charge.fill(client, { + challenge: createChallenge({ + splits: [{ amount: '0.25', recipient: splitRecipient }], + }), + expectedRecipients: [recipient], + payer: account.address, + }), + ).rejects.toThrow(`Unexpected split recipient: ${splitRecipient}`) + }) +}) + +describe('createCredential', () => { + test('error: rejects unsupported mode', async () => { + const client = createClient({ + account, + chain, + transport: http(), + }) + const filled = await Charge.fill(client, { + challenge: createChallenge({ supportedModes: ['push'] }), + payer: account.address, + }) + + await expect( + Charge.createCredential(client, { + filled, + mode: 'pull', + signer: account, + }), + ).rejects.toThrow('Challenge does not support pull mode.') + }) + + test('behavior: creates pull transaction credential', async () => { + const client = createClient({ + account, + chain, + transport: http(), + }) + const challenge = createChallenge() + const filled = await Charge.fill(client, { + challenge, + payer: account.address, + }) + + const authorization = await Charge.createCredential(client, { filled, signer: account }) + const credential = Credential.deserialize(authorization) + + expect(credential.challenge.id).toBe(challenge.id) + expect(credential.payload).toMatchObject({ type: 'transaction' }) + const signature = (credential.payload as { signature: Hex.Hex }).signature + const transaction = Transaction.deserialize(signature as Transaction.TransactionSerializedTempo) + if (!('calls' in transaction)) throw new Error('unexpected transaction type') + if (filled.kind !== 'calls') throw new Error('expected filled calls') + expect(transaction.calls).toEqual(filled.calls.map(({ data, to }) => ({ data, to }))) + expect(credential.source).toBe(`did:pkh:eip155:${chainId}:${account.address}`) + }) + + test('behavior: creates pull transaction credential with access key', async () => { + const { accessKey, keyAuthorization } = await createAccessKey() + const client = createClient({ + account: accessKey, + chain, + transport: http(), + }) + const challenge = createChallenge() + const filled = await Charge.fill(client, { + challenge, + payer: accessKey.address, + }) + + const authorization = await Charge.createCredential(client, { filled, signer: accessKey }) + const credential = Credential.deserialize(authorization) + + expect(accessKey.address).toBe(rootAccount.address) + expect(accessKey.accessKeyAddress).not.toBe(rootAccount.address) + expect(credential.source).toBe(`did:pkh:eip155:${chainId}:${rootAccount.address}`) + expect(credential.payload).toMatchObject({ type: 'transaction' }) + const signature = (credential.payload as { signature: Hex.Hex }).signature + const transaction = Transaction.deserialize(signature as Transaction.TransactionSerializedTempo) + expect(transaction.keyAuthorization).toEqual(keyAuthorization) + }) + + test('behavior: creates proof credential for zero-amount charge', async () => { + const client = createClient({ + account, + chain, + transport: http(), + }) + const challenge = createChallenge({ amount: '0' }) + const filled = await Charge.fill(client, { + challenge, + payer: account.address, + }) + + const authorization = await Charge.createCredential(client, { filled, signer: account }) + const credential = Credential.deserialize(authorization) + + expect(credential.challenge.id).toBe(challenge.id) + expect(credential.payload).toMatchObject({ type: 'proof' }) + expect(credential.source).toBe(`did:pkh:eip155:${chainId}:${account.address}`) + }) + + test('behavior: creates proof credential with access key source account', async () => { + const { accessKey } = await createAccessKey() + const client = createClient({ + account: accessKey, + chain, + transport: http(), + }) + const challenge = createChallenge({ amount: '0' }) + const filled = await Charge.fill(client, { + challenge, + payer: accessKey.address, + }) + + const authorization = await Charge.createCredential(client, { filled, signer: accessKey }) + const credential = Credential.deserialize(authorization) + + expect(accessKey.address).toBe(rootAccount.address) + expect(accessKey.accessKeyAddress).not.toBe(rootAccount.address) + expect(credential.payload).toMatchObject({ type: 'proof' }) + expect(credential.source).toBe(`did:pkh:eip155:${chainId}:${rootAccount.address}`) + }) +}) diff --git a/src/tempo/Charge.ts b/src/tempo/Charge.ts new file mode 100644 index 00000000..e9307c31 --- /dev/null +++ b/src/tempo/Charge.ts @@ -0,0 +1,215 @@ +import type * as Hex from 'ox/Hex' +import type { Address, Call, Client } from 'viem' +import type { Account } from 'viem/accounts' +import { + prepareTransactionRequest, + sendCallsSync, + signTypedData, + signTransaction, +} from 'viem/actions' +import { Actions } from 'viem/tempo' + +import type * as Challenge from '../Challenge.js' +import * as Credential from '../Credential.js' +import * as Attribution from './Attribution.js' +import * as AutoSwap from './internal/auto-swap.js' +import * as Charge_internal from './internal/charge.js' +import * as Proof from './internal/proof.js' +import * as Methods from './Methods.js' + +export type ChargeChallenge = Challenge.Challenge< + ReturnType, + 'charge', + 'tempo' +> + +export type { Call } + +/** + * Fills a Tempo charge challenge into signer-selectable payment data. + * + * The returned value is plain data: callers can inspect `calls` to choose an + * access key, then pass the selected signer to {@link createCredential}. + */ +export async function fill(client: Client, parameters: fill.Parameters): Promise { + const { autoSwap: autoSwapOption, challenge, clientId, expectedRecipients, payer } = parameters + const challengeChainId = challenge.request.methodDetails?.chainId + const chainId = challengeChainId ?? client.chain?.id + if (chainId === undefined) + throw new Error('No `chainId` provided. Pass a chain ID in the challenge or client.') + + const { amount, methodDetails } = challenge.request + if (BigInt(amount) === 0n) return { challenge, chainId, kind: 'proof', payer } + + if (expectedRecipients) { + const allowed = new Set(expectedRecipients.map((a) => a.toLowerCase())) + const splits = methodDetails?.splits as readonly { recipient: string }[] | undefined + if (splits) { + for (const split of splits) { + if (!allowed.has(split.recipient.toLowerCase())) + throw new Error(`Unexpected split recipient: ${split.recipient}`) + } + } + } + + const supportedModes = (methodDetails?.supportedModes as + | readonly Methods.ChargeMode[] + | undefined) ?? ['pull', 'push'] + const currency = challenge.request.currency as Address + const memo = methodDetails?.memo + ? (methodDetails.memo as Hex.Hex) + : Attribution.encode({ + challengeId: challenge.id, + clientId, + serverId: challenge.realm, + }) + const transfers = Charge_internal.getTransfers({ + amount, + methodDetails: { + ...methodDetails, + memo, + }, + recipient: challenge.request.recipient as Address, + }) + const transferCalls = transfers.map((transfer) => + Actions.token.transfer.call({ + amount: BigInt(transfer.amount), + ...(transfer.memo && { memo: transfer.memo as Hex.Hex }), + to: transfer.recipient as Address, + token: currency, + }), + ) satisfies readonly Call[] + + const autoSwap = AutoSwap.resolve(autoSwapOption, AutoSwap.defaultCurrencies) + const swapCalls = autoSwap + ? await AutoSwap.findCalls(client, { + account: payer, + amountOut: BigInt(amount), + tokenOut: currency, + tokenIn: autoSwap.tokenIn, + slippage: autoSwap.slippage, + }) + : undefined + + return { + calls: [...(swapCalls ?? []), ...transferCalls], + challenge, + chainId, + feePayer: Boolean(methodDetails?.feePayer), + kind: 'calls', + payer, + supportedModes, + } +} + +export declare namespace fill { + type ReturnType = + | { + challenge: ChargeChallenge + chainId: number + kind: 'proof' + payer: Address + } + | { + calls: readonly Call[] + challenge: ChargeChallenge + chainId: number + feePayer: boolean + kind: 'calls' + payer: Address + supportedModes: readonly Methods.ChargeMode[] + } + + type Parameters = { + challenge: ChargeChallenge + payer: Address + autoSwap?: AutoSwap.resolve.Value | undefined + clientId?: string | undefined + expectedRecipients?: readonly Address[] | undefined + } +} + +/** + * Creates a Tempo charge credential from a filled charge and selected signer. + */ +export async function createCredential( + client: Client, + parameters: createCredential.Parameters, +): Promise { + const { filled, mode: modeOption, signer } = parameters + + if (filled.kind === 'proof') { + const signature = await signTypedData(client, { + account: signer, + domain: Proof.domain(filled.chainId), + types: Proof.types, + primaryType: 'Proof', + message: Proof.message(filled.challenge.id, filled.challenge.realm), + }) + return Credential.serialize({ + challenge: filled.challenge, + payload: { signature, type: 'proof' }, + source: Proof.proofSource({ address: signer.address, chainId: filled.chainId }), + }) + } + + const mode = (() => { + if (modeOption) { + if (!filled.supportedModes.includes(modeOption)) + throw new Error(`Challenge does not support ${modeOption} mode.`) + return modeOption + } + + const preferredMode = signer.type === 'json-rpc' ? 'push' : 'pull' + if (filled.supportedModes.includes(preferredMode)) return preferredMode + return filled.supportedModes[0]! + })() + + if (mode === 'push') { + const { receipts } = await sendCallsSync(client, { + account: signer, + calls: filled.calls, + experimental_fallback: true, + }) + const hash = receipts?.[0]?.transactionHash + if (!hash) throw new Error('No transaction receipt returned.') + return Credential.serialize({ + challenge: filled.challenge, + payload: { hash, type: 'hash' }, + source: Proof.proofSource({ address: signer.address, chainId: filled.chainId }), + }) + } + + const validBefore = (() => { + const defaultExpiry = Math.floor(Date.now() / 1000) + 25 + if (!filled.challenge.expires) return defaultExpiry + const challengeExpiry = Math.floor(new Date(filled.challenge.expires).getTime() / 1000) + return Math.min(defaultExpiry, challengeExpiry) + })() + + const prepared = await prepareTransactionRequest(client, { + account: signer, + calls: filled.calls, + 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 (filled.feePayer) (prepared as Record).feePayer = true + const signature = await signTransaction(client, prepared as never) + + return Credential.serialize({ + challenge: filled.challenge, + payload: { signature, type: 'transaction' }, + source: Proof.proofSource({ address: signer.address, chainId: filled.chainId }), + }) +} + +export declare namespace createCredential { + type Parameters = { + filled: fill.ReturnType + mode?: Methods.ChargeMode | undefined + signer: Account + } +} diff --git a/src/tempo/client/Charge.test.ts b/src/tempo/client/Charge.test.ts index e7895884..2d845841 100644 --- a/src/tempo/client/Charge.test.ts +++ b/src/tempo/client/Charge.test.ts @@ -1,16 +1,15 @@ import { Challenge, Credential } from 'mppx' -import { createClient, http } from 'viem' -import { privateKeyToAccount } from 'viem/accounts' -import { tempoLocalnet } from 'viem/chains' -import { describe, expect, test, vi } from 'vp/test' +import { createClient } from 'viem' +import { describe, expect, test } from 'vp/test' +import { accounts, asset, chain, http } from '~test/tempo/viem.js' import * as Methods from '../Methods.js' import { charge } from './Charge.js' -const account = privateKeyToAccount( - '0x0000000000000000000000000000000000000000000000000000000000000001', -) -const currency = '0x3333333333333333333333333333333333333333' +const account = accounts[1] +const otherAccount = accounts[2] +const chainId = chain.id +const currency = asset const recipient = '0x2222222222222222222222222222222222222222' type ChargeRequest = ReturnType @@ -35,11 +34,11 @@ function createChallenge( } describe('tempo.charge client', () => { - test('uses client chain ID when the challenge omits chainId', async () => { + test('behavior: uses client chain ID when challenge omits chainId', async () => { const client = createClient({ account, - chain: tempoLocalnet, - transport: http('http://127.0.0.1'), + chain, + transport: http(), }) const method = charge({ account, @@ -53,16 +52,16 @@ describe('tempo.charge client', () => { }), ) - expect(credential.source).toBe(`did:pkh:eip155:${tempoLocalnet.id}:${account.address}`) + expect(credential.source).toBe(`did:pkh:eip155:${chainId}:${account.address}`) }) - test('uses challenge chainId for client resolution and proof source', async () => { + test('behavior: uses challenge chainId for client resolution and proof source', async () => { let requestedChainId: number | undefined - const chainId = 42431 + const challengeChainId = 42431 const client = createClient({ account, - chain: tempoLocalnet, - transport: http('http://127.0.0.1'), + chain, + transport: http(), }) const method = charge({ account, @@ -74,53 +73,33 @@ describe('tempo.charge client', () => { const credential = Credential.deserialize( await method.createCredential({ - challenge: createChallenge({ chainId }), + challenge: createChallenge({ chainId: challengeChainId }), context: {}, }), ) - expect(requestedChainId).toBe(chainId) - expect(credential.source).toBe(`did:pkh:eip155:${chainId}:${account.address}`) + expect(requestedChainId).toBe(challengeChainId) + expect(credential.source).toBe(`did:pkh:eip155:${challengeChainId}:${account.address}`) }) - test('uses challenge chainId for non-zero transaction source', async () => { - vi.resetModules() - const prepareTransactionRequest = vi.fn(async () => ({})) - const signTransaction = vi.fn(async () => '0xdeadbeef') - vi.doMock('viem/actions', () => ({ - prepareTransactionRequest, - sendCallsSync: vi.fn(), - signTransaction, - signTypedData: vi.fn(), - })) - - try { - const { charge: chargeWithMockedActions } = await import('./Charge.js') - const chainId = 42431 - const client = createClient({ - account, - chain: tempoLocalnet, - transport: http('http://127.0.0.1'), - }) - const method = chargeWithMockedActions({ - account, - getClient: () => client, - }) + test('behavior: context account overrides default account', async () => { + const client = createClient({ + account, + chain, + transport: http(), + }) + const method = charge({ + account, + getClient: () => client, + }) - const credential = Credential.deserialize( - await method.createCredential({ - challenge: createChallenge({ amount: '1', chainId, supportedModes: ['pull'] }), - context: {}, - }), - ) + const credential = Credential.deserialize( + await method.createCredential({ + challenge: createChallenge({ chainId }), + context: { account: otherAccount }, + }), + ) - expect(prepareTransactionRequest).toHaveBeenCalledOnce() - expect(signTransaction).toHaveBeenCalledOnce() - expect(credential.payload).toEqual({ signature: '0xdeadbeef', type: 'transaction' }) - expect(credential.source).toBe(`did:pkh:eip155:${chainId}:${account.address}`) - } finally { - vi.doUnmock('viem/actions') - vi.resetModules() - } + expect(credential.source).toBe(`did:pkh:eip155:${chainId}:${otherAccount.address}`) }) }) diff --git a/src/tempo/client/Charge.ts b/src/tempo/client/Charge.ts index a4318ad7..6c07e9c1 100644 --- a/src/tempo/client/Charge.ts +++ b/src/tempo/client/Charge.ts @@ -1,24 +1,13 @@ -import type * as Hex from 'ox/Hex' import type { Address } from 'viem' -import { - prepareTransactionRequest, - sendCallsSync, - signTypedData, - signTransaction, -} from 'viem/actions' -import { Actions } from 'viem/tempo' import { tempo as tempo_chain } from 'viem/tempo/chains' -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' import * as z from '../../zod.js' -import * as Attribution from '../Attribution.js' +import * as Charge from '../Charge.js' import * as AutoSwap from '../internal/auto-swap.js' -import * as Charge_internal from '../internal/charge.js' import * as defaults from '../internal/defaults.js' -import * as Proof from '../internal/proof.js' import * as Methods from '../Methods.js' /** @@ -35,7 +24,6 @@ import * as Methods from '../Methods.js' * ``` */ export function charge(parameters: charge.Parameters = {}) { - const { clientId } = parameters const getClient = Client.getResolver({ chain: tempo_chain, getClient: parameters.getClient, @@ -53,133 +41,18 @@ export function charge(parameters: charge.Parameters = {}) { async createCredential({ challenge, context }) { const challengeChainId = challenge.request.methodDetails?.chainId const client = await getClient({ chainId: challengeChainId }) - const chainId = challengeChainId ?? client.chain?.id - if (chainId === undefined) - throw new Error('No `chainId` provided. Pass a chain ID in the challenge or client.') - const account = getAccount(client, context) - - const { request } = challenge - const { amount, methodDetails } = request - - // Zero-amount: sign EIP-712 typed data instead of creating a transaction. - if (BigInt(amount) === 0n) { - const signature = await signTypedData(client, { - account, - domain: Proof.domain(chainId), - types: Proof.types, - primaryType: 'Proof', - message: Proof.message(challenge.id, challenge.realm), - }) - return Credential.serialize({ - challenge, - payload: { signature, type: 'proof' }, - source: Proof.proofSource({ address: account.address, chainId }), - }) - } - - const currency = request.currency as Address - if (parameters.expectedRecipients) { - const allowed = new Set(parameters.expectedRecipients.map((a) => a.toLowerCase())) - const splits = methodDetails?.splits as readonly { recipient: string }[] | undefined - if (splits) { - for (const split of splits) { - if (!allowed.has(split.recipient.toLowerCase())) - throw new Error(`Unexpected split recipient: ${split.recipient}`) - } - } - } - const supportedModes = (methodDetails?.supportedModes as - | readonly Methods.ChargeMode[] - | undefined) ?? ['pull', 'push'] - const mode = (() => { - const explicitMode = context?.mode ?? parameters.mode - if (explicitMode) { - if (!supportedModes.includes(explicitMode)) - throw new Error(`Challenge does not support ${explicitMode} mode.`) - return explicitMode - } - - const preferredMode = account.type === 'json-rpc' ? 'push' : 'pull' - if (supportedModes.includes(preferredMode)) return preferredMode - return supportedModes[0]! - })() - - const memo = methodDetails?.memo - ? (methodDetails.memo as Hex.Hex) - : Attribution.encode({ challengeId: challenge.id, clientId, serverId: challenge.realm }) - const transfers = Charge_internal.getTransfers({ - amount, - methodDetails: { - ...methodDetails, - memo, - }, - recipient: request.recipient as Address, - }) - const transferCalls = transfers.map((transfer) => - Actions.token.transfer.call({ - amount: BigInt(transfer.amount), - ...(transfer.memo && { memo: transfer.memo as Hex.Hex }), - to: transfer.recipient as Address, - token: currency, - }), - ) - - const autoSwap = AutoSwap.resolve( - context?.autoSwap ?? parameters.autoSwap, - AutoSwap.defaultCurrencies, - ) - - const swapCalls = autoSwap - ? await AutoSwap.findCalls(client, { - account: account.address, - amountOut: BigInt(amount), - tokenOut: currency, - tokenIn: autoSwap.tokenIn, - slippage: autoSwap.slippage, - }) - : undefined - - const calls = [...(swapCalls ?? []), ...transferCalls] - - const validBefore = (() => { - const defaultExpiry = Math.floor(Date.now() / 1000) + 25 - if (!challenge.expires) return defaultExpiry - const challengeExpiry = Math.floor(new Date(challenge.expires).getTime() / 1000) - return Math.min(defaultExpiry, challengeExpiry) - })() - - if (mode === 'push') { - const { receipts } = await sendCallsSync(client, { - account, - calls: calls as never, - experimental_fallback: true, - }) - const hash = receipts?.[0]?.transactionHash - if (!hash) throw new Error('No transaction receipt returned.') - return Credential.serialize({ - challenge, - payload: { hash, type: 'hash' }, - source: Proof.proofSource({ address: account.address, chainId }), - }) - } - - const prepared = await prepareTransactionRequest(client, { - account, - calls, - 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 (methodDetails?.feePayer) (prepared as Record).feePayer = true - const signature = await signTransaction(client, prepared as never) - - return Credential.serialize({ + const filled = await Charge.fill(client, { + autoSwap: context?.autoSwap ?? parameters.autoSwap, challenge, - payload: { signature, type: 'transaction' }, - source: Proof.proofSource({ address: account.address, chainId }), + clientId: parameters.clientId, + expectedRecipients: parameters.expectedRecipients, + payer: account.address, + }) + return Charge.createCredential(client, { + filled, + mode: context?.mode ?? parameters.mode, + signer: account, }) }, }) diff --git a/src/tempo/index.ts b/src/tempo/index.ts index 65875ac7..c13bf32f 100644 --- a/src/tempo/index.ts +++ b/src/tempo/index.ts @@ -1,3 +1,4 @@ +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' diff --git a/src/tempo/internal/auto-swap.ts b/src/tempo/internal/auto-swap.ts index f18621c2..80f5de78 100644 --- a/src/tempo/internal/auto-swap.ts +++ b/src/tempo/internal/auto-swap.ts @@ -1,4 +1,4 @@ -import type { Address, Client } from 'viem' +import type { Address, Call, Client } from 'viem' import { readContract } from 'viem/actions' import { Actions, Addresses } from 'viem/tempo' @@ -96,7 +96,7 @@ export declare namespace findCalls { } /** `undefined` when no swap is needed (account has sufficient balance). */ - type ReturnType = readonly object[] | undefined + type ReturnType = readonly Call[] | undefined } /** Resolves an auto-swap configuration value into concrete currencies and slippage. */