Skip to content

Commit 964dac1

Browse files
committed
fix(tempo): sponsor machine-token sessions with pathUSD
1 parent 225798e commit 964dac1

5 files changed

Lines changed: 111 additions & 10 deletions

File tree

src/tempo/internal/defaults.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ export const machineToken = {
2929
[chainId.testnet]: {
3030
feeToken: tokens.pathUsd,
3131
session: true,
32-
swap: '0x0EdC4d63Daf0FdeA89C95357eC85D6Fc2eD41cB8',
33-
token: '0x20c00000000000000000000004645c8f96629cE6',
32+
swap: '0xD2E54024506079A62ceb2b698148D240036662E4',
33+
token: '0x20C000000000000000000000785C8D7ebcC0b982',
3434
},
3535
} as const satisfies Partial<
3636
Record<

src/tempo/session/precompile/Chain.test.ts

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
type Hex,
1313
} from 'viem'
1414
import { privateKeyToAccount } from 'viem/accounts'
15-
import { Abis, Addresses, Transaction } from 'viem/tempo'
15+
import { Abis, Addresses, Chain as TempoChain, Transaction } from 'viem/tempo'
1616
import { describe, expect, test } from 'vp/test'
1717

1818
import { VerificationFailedError } from '../../../Errors.js'
@@ -57,7 +57,7 @@ function createMockClient(
5757
) {
5858
return createClient({
5959
account: feePayer,
60-
chain: { id: chainId } as never,
60+
chain: TempoChain.testnet,
6161
transport: custom({
6262
async request(args) {
6363
parameters.rpcMethods?.push(args.method)
@@ -168,13 +168,14 @@ async function createSerializedTransaction(parameters: {
168168
data?: `0x${string}` | undefined
169169
value?: bigint | undefined
170170
}[]
171+
feeToken?: Address | false | undefined
171172
gas?: bigint | undefined
172173
signed?: boolean | undefined
173174
}) {
174175
return (await Transaction.serialize({
175176
chainId,
176177
calls: parameters.calls,
177-
feeToken: descriptor.token,
178+
...(parameters.feeToken === false ? {} : { feeToken: parameters.feeToken ?? descriptor.token }),
178179
nonce: 0,
179180
...(parameters.gas !== undefined ? { gas: parameters.gas } : {}),
180181
...(parameters.signed
@@ -222,6 +223,7 @@ function autoSwapCalls(amountOut: bigint = deposit) {
222223
async function createOpenTransaction(
223224
parameters: {
224225
authorizedSigner?: `0x${string}` | undefined
226+
feeToken?: Address | false | undefined
225227
gas?: bigint | undefined
226228
operator?: `0x${string}` | undefined
227229
payee?: `0x${string}` | undefined
@@ -251,6 +253,7 @@ async function createOpenTransaction(
251253
})
252254
return createSerializedTransaction({
253255
calls: [...(parameters.prefixCalls ?? []), { to: parameters.to ?? tip20ChannelEscrow, data }],
256+
feeToken: parameters.feeToken,
254257
gas: parameters.gas,
255258
signed: parameters.signed,
256259
})
@@ -260,6 +263,7 @@ async function createTopUpTransaction(
260263
parameters: {
261264
additionalDeposit?: bigint | undefined
262265
descriptor_?: Channel.ChannelDescriptor | undefined
266+
feeToken?: Address | false | undefined
263267
gas?: bigint | undefined
264268
signed?: boolean | undefined
265269
to?: `0x${string}` | undefined
@@ -282,6 +286,7 @@ async function createTopUpTransaction(
282286
})
283287
return createSerializedTransaction({
284288
calls: [...(parameters.prefixCalls ?? []), { to: parameters.to ?? tip20ChannelEscrow, data }],
289+
feeToken: parameters.feeToken,
285290
gas: parameters.gas,
286291
signed: parameters.signed,
287292
})
@@ -827,6 +832,57 @@ describe('precompile broadcastOpenTransaction', () => {
827832
).toHaveLength(2)
828833
})
829834

835+
test('fee-payer completes open with an explicit allowed fee token', async () => {
836+
let broadcast: Hex | undefined
837+
const serializedTransaction = await createOpenTransaction({
838+
feeToken: false,
839+
gas: 100_000n,
840+
signed: true,
841+
})
842+
const transaction = Transaction.deserialize(
843+
serializedTransaction as Transaction.TransactionSerializedTempo,
844+
)
845+
const payer = transaction.from!
846+
const expiringNonceHash = Channel.computeExpiringNonceHash(
847+
Channel.transactionForExpiringNonceHash({ feePayer, transaction }),
848+
{ sender: payer },
849+
)
850+
const expectedDescriptor = { ...descriptor, payer, expiringNonceHash }
851+
const channelId = Channel.computeId({
852+
...expectedDescriptor,
853+
chainId,
854+
escrow: tip20ChannelEscrow,
855+
})
856+
const state = { settled: 0n, deposit, closeRequestedAt: 0 }
857+
858+
await Chain.broadcastOpenTransaction({
859+
allowedFeeTokens: [sourceToken],
860+
chainId,
861+
client: createMockClient({
862+
channel: { descriptor: expectedDescriptor, state },
863+
onRequest(method, params) {
864+
if (method === 'eth_sendRawTransactionSync') broadcast = (params as readonly [Hex])[0]
865+
},
866+
receipt: receipt([openedLog({ channelId, expiringNonceHash })]),
867+
}),
868+
escrowContract: tip20ChannelEscrow,
869+
expectedAuthorizedSigner: descriptor.authorizedSigner,
870+
expectedChannelId: channelId,
871+
expectedCurrency: descriptor.token,
872+
expectedExpiringNonceHash: expiringNonceHash,
873+
expectedOperator: descriptor.operator,
874+
expectedPayee: descriptor.payee,
875+
expectedPayer: payer,
876+
feePayer,
877+
serializedTransaction,
878+
})
879+
880+
expect(broadcast).toBeDefined()
881+
expect(
882+
Transaction.deserialize(broadcast as Transaction.TransactionSerializedTempo).feeToken,
883+
).toBe(sourceToken)
884+
})
885+
830886
test('hosted fee-payer relays a sender-signed open without local co-signing', async () => {
831887
const rpcMethods: string[] = []
832888
const serializedTransaction = await createOpenTransaction({ signed: true })
@@ -1195,6 +1251,41 @@ describe('precompile broadcastTopUpTransaction', () => {
11951251
).toHaveLength(2)
11961252
})
11971253

1254+
test('fee-payer completes top-up with an explicit allowed fee token', async () => {
1255+
let broadcast: Hex | undefined
1256+
const serializedTransaction = await createTopUpTransaction({
1257+
feeToken: false,
1258+
gas: 100_000n,
1259+
signed: true,
1260+
})
1261+
const channelId = Channel.computeId({ ...descriptor, chainId, escrow: tip20ChannelEscrow })
1262+
const newDeposit = deposit * 2n
1263+
1264+
await Chain.broadcastTopUpTransaction({
1265+
additionalDeposit: deposit,
1266+
allowedFeeTokens: [sourceToken],
1267+
chainId,
1268+
client: createMockClient({
1269+
channel: { descriptor, state: { settled: 0n, deposit: newDeposit, closeRequestedAt: 0 } },
1270+
onRequest(method, params) {
1271+
if (method === 'eth_sendRawTransactionSync') broadcast = (params as readonly [Hex])[0]
1272+
},
1273+
receipt: receipt([topUpLog({ channelId, newDeposit })]),
1274+
}),
1275+
descriptor,
1276+
escrowContract: tip20ChannelEscrow,
1277+
expectedChannelId: channelId,
1278+
expectedCurrency: descriptor.token,
1279+
feePayer,
1280+
serializedTransaction,
1281+
})
1282+
1283+
expect(broadcast).toBeDefined()
1284+
expect(
1285+
Transaction.deserialize(broadcast as Transaction.TransactionSerializedTempo).feeToken,
1286+
).toBe(sourceToken)
1287+
})
1288+
11981289
test('rejects top-up calldata amount mismatches before broadcasting', async () => {
11991290
const serializedTransaction = await createTopUpTransaction({ additionalDeposit: 1n })
12001291

src/tempo/session/precompile/Chain.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,8 @@ export type BroadcastOpenTransactionResult = {
957957

958958
/** Inputs for broadcasting and verifying a client-signed TIP-1034 open transaction. */
959959
export type BroadcastOpenTransactionParameters = {
960+
/** Fee tokens allowed when the server completes a sponsored transaction. Defaults to the channel currency. */
961+
allowedFeeTokens?: readonly Address[] | undefined
960962
/** Hook invoked after calldata validation but before broadcasting. */
961963
beforeBroadcast?:
962964
| ((result: Omit<BroadcastOpenTransactionResult, 'txHash' | 'state'>) => Promise<void> | void)
@@ -1042,7 +1044,7 @@ export function validateOpenCredentialTransaction(
10421044
})
10431045
if (parameters.feePayer) assertSenderSigned(transaction)
10441046
validateCredentialSponsorship({
1045-
allowedFeeTokens: [parameters.expectedCurrency],
1047+
allowedFeeTokens: parameters.allowedFeeTokens ?? [parameters.expectedCurrency],
10461048
challengeExpires: parameters.challengeExpires,
10471049
chainId: parameters.chainId,
10481050
details: {
@@ -1083,7 +1085,7 @@ export async function broadcastOpenTransaction(
10831085
challengeExpires: parameters.challengeExpires,
10841086
chainId: parameters.chainId,
10851087
client: parameters.client,
1086-
allowedFeeTokens: [parameters.expectedCurrency],
1088+
allowedFeeTokens: parameters.allowedFeeTokens ?? [parameters.expectedCurrency],
10871089
details: {
10881090
channelId: parameters.expectedChannelId,
10891091
currency: parameters.expectedCurrency,
@@ -1136,6 +1138,8 @@ export type BroadcastTopUpTransactionResult = {
11361138
export type BroadcastTopUpTransactionParameters = {
11371139
/** Additional deposit amount expected in the top-up calldata. */
11381140
additionalDeposit: bigint
1141+
/** Fee tokens allowed when the server completes a sponsored transaction. Defaults to the channel currency. */
1142+
allowedFeeTokens?: readonly Address[] | undefined
11391143
/** Challenge expiration propagated into fee-payer policy checks. */
11401144
challengeExpires?: string | undefined
11411145
/** Chain ID used for fee-payer transaction signing. */
@@ -1195,7 +1199,7 @@ export function validateTopUpCredentialTransaction(
11951199
})
11961200
if (parameters.feePayer) assertSenderSigned(transaction)
11971201
validateCredentialSponsorship({
1198-
allowedFeeTokens: [parameters.expectedCurrency],
1202+
allowedFeeTokens: parameters.allowedFeeTokens ?? [parameters.expectedCurrency],
11991203
challengeExpires: parameters.challengeExpires,
12001204
chainId: parameters.chainId,
12011205
details: {
@@ -1219,7 +1223,7 @@ export async function broadcastTopUpTransaction(
12191223
challengeExpires: parameters.challengeExpires,
12201224
chainId: parameters.chainId,
12211225
client: parameters.client,
1222-
allowedFeeTokens: [parameters.expectedCurrency],
1226+
allowedFeeTokens: parameters.allowedFeeTokens ?? [parameters.expectedCurrency],
12231227
details: {
12241228
additionalDeposit: parameters.additionalDeposit.toString(),
12251229
channelId: parameters.expectedChannelId,

src/tempo/session/server/CredentialVerification.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ export type BroadcastCredentialPayloadParameters = {
424424
feePayer?: viem_Account | true | undefined
425425
/** Optional policy for fee-sponsored close/open/top-up transactions. */
426426
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
427-
/** Optional fee token override for close transactions. */
427+
/** Optional fee token override for sponsored management and settlement transactions. */
428428
feeToken?: Address | undefined
429429
/** Last successful on-chain refresh timestamp per channel ID. */
430430
lastOnChainVerified: Map<Hex, number>
@@ -483,6 +483,7 @@ export type ValidateCredentialPayloadParameters = Pick<
483483
| 'expectedOperator'
484484
| 'feePayer'
485485
| 'feePayerPolicy'
486+
| 'feeToken'
486487
| 'lastOnChainVerified'
487488
| 'minVoucherDelta'
488489
| 'payload'
@@ -549,6 +550,7 @@ async function validateOpenCredential(
549550
expectedOperator,
550551
)
551552
const transaction = Chain.validateOpenCredentialTransaction({
553+
...(parameters.feeToken ? { allowedFeeTokens: [parameters.feeToken] } : {}),
552554
challengeExpires: challenge.expires,
553555
chainId,
554556
escrowContract: escrow,
@@ -614,6 +616,7 @@ async function validateTopUpCredential(
614616
})
615617
const transaction = Chain.validateTopUpCredentialTransaction({
616618
additionalDeposit,
619+
...(parameters.feeToken ? { allowedFeeTokens: [parameters.feeToken] } : {}),
617620
challengeExpires: challenge.expires,
618621
chainId,
619622
descriptor: channel.descriptor,
@@ -861,6 +864,7 @@ async function handleOpenCredential(
861864
)
862865

863866
const result = await Chain.broadcastOpenTransaction({
867+
...(parameters.feeToken ? { allowedFeeTokens: [parameters.feeToken] } : {}),
864868
challengeExpires: challenge.expires,
865869
chainId,
866870
client,
@@ -950,6 +954,7 @@ async function handleTopUpCredential(
950954
})
951955
const result = await Chain.broadcastTopUpTransaction({
952956
additionalDeposit,
957+
...(parameters.feeToken ? { allowedFeeTokens: [parameters.feeToken] } : {}),
953958
challengeExpires: challenge.expires,
954959
chainId,
955960
client,

src/tempo/session/server/Session.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ export function session<const parameters extends session.Parameters>(
425425
expectedOperator: context.methodDetails.operator,
426426
feePayer: context.feePayer,
427427
feePayerPolicy: parameters.feePayerPolicy,
428+
feeToken: parameters.feeToken,
428429
lastOnChainVerified,
429430
minVoucherDelta: context.minVoucherDelta,
430431
payload,

0 commit comments

Comments
 (0)