mppx 0.8.12 → 0.8.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +16 -1
  3. package/dist/Errors.d.ts +10 -0
  4. package/dist/Errors.d.ts.map +1 -1
  5. package/dist/Errors.js +11 -2
  6. package/dist/Errors.js.map +1 -1
  7. package/dist/Method.d.ts +105 -3
  8. package/dist/Method.d.ts.map +1 -1
  9. package/dist/Method.js +102 -2
  10. package/dist/Method.js.map +1 -1
  11. package/dist/cli/account.d.ts +88 -0
  12. package/dist/cli/account.d.ts.map +1 -1
  13. package/dist/cli/account.js +39 -9
  14. package/dist/cli/account.js.map +1 -1
  15. package/dist/cli/cli.d.ts +2 -1
  16. package/dist/cli/cli.d.ts.map +1 -1
  17. package/dist/cli/cli.js +59 -2
  18. package/dist/cli/cli.js.map +1 -1
  19. package/dist/cli/sessions/Manager.d.ts +31 -0
  20. package/dist/cli/sessions/Manager.d.ts.map +1 -0
  21. package/dist/cli/sessions/Manager.js +56 -0
  22. package/dist/cli/sessions/Manager.js.map +1 -0
  23. package/dist/cli/sessions/commands.d.ts +36 -0
  24. package/dist/cli/sessions/commands.d.ts.map +1 -0
  25. package/dist/cli/sessions/commands.js +407 -0
  26. package/dist/cli/sessions/commands.js.map +1 -0
  27. package/dist/cli/sessions/request.d.ts +35 -0
  28. package/dist/cli/sessions/request.d.ts.map +1 -0
  29. package/dist/cli/sessions/request.js +271 -0
  30. package/dist/cli/sessions/request.js.map +1 -0
  31. package/dist/cli/sessions/store.d.ts +134 -0
  32. package/dist/cli/sessions/store.d.ts.map +1 -0
  33. package/dist/cli/sessions/store.js +723 -0
  34. package/dist/cli/sessions/store.js.map +1 -0
  35. package/dist/cli/utils.d.ts +2 -0
  36. package/dist/cli/utils.d.ts.map +1 -1
  37. package/dist/cli/utils.js +12 -0
  38. package/dist/cli/utils.js.map +1 -1
  39. package/dist/cli/validate/index.d.ts.map +1 -1
  40. package/dist/cli/validate/index.js +12 -6
  41. package/dist/cli/validate/index.js.map +1 -1
  42. package/dist/cli/validate/messages.d.ts +2 -0
  43. package/dist/cli/validate/messages.d.ts.map +1 -0
  44. package/dist/cli/validate/messages.js +8 -0
  45. package/dist/cli/validate/messages.js.map +1 -0
  46. package/dist/client/internal/Fetch.d.ts.map +1 -1
  47. package/dist/client/internal/Fetch.js +12 -2
  48. package/dist/client/internal/Fetch.js.map +1 -1
  49. package/dist/client/internal/MethodChallenge.d.ts +17 -0
  50. package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
  51. package/dist/client/internal/MethodChallenge.js +11 -0
  52. package/dist/client/internal/MethodChallenge.js.map +1 -0
  53. package/dist/client/node.d.ts +29 -0
  54. package/dist/client/node.d.ts.map +1 -0
  55. package/dist/client/node.js +177 -0
  56. package/dist/client/node.js.map +1 -0
  57. package/dist/internal/types.d.ts +24 -0
  58. package/dist/internal/types.d.ts.map +1 -1
  59. package/dist/server/Mppx.d.ts +23 -4
  60. package/dist/server/Mppx.d.ts.map +1 -1
  61. package/dist/server/Mppx.js +91 -82
  62. package/dist/server/Mppx.js.map +1 -1
  63. package/dist/stripe/server/Charge.d.ts +8 -4
  64. package/dist/stripe/server/Charge.d.ts.map +1 -1
  65. package/dist/stripe/server/Charge.js.map +1 -1
  66. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  67. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  68. package/dist/stripe/server/internal/html.gen.js +1 -1
  69. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  70. package/dist/tempo/client/Methods.d.ts +2 -1
  71. package/dist/tempo/client/Methods.d.ts.map +1 -1
  72. package/dist/tempo/internal/fee-payer.d.ts +1 -0
  73. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  74. package/dist/tempo/internal/fee-payer.js +7 -2
  75. package/dist/tempo/internal/fee-payer.js.map +1 -1
  76. package/dist/tempo/internal/types.d.ts +10 -1
  77. package/dist/tempo/internal/types.d.ts.map +1 -1
  78. package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
  79. package/dist/tempo/legacy/client/ChannelOps.js +2 -1
  80. package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
  81. package/dist/tempo/legacy/client/Session.d.ts +2 -2
  82. package/dist/tempo/legacy/server/Session.d.ts +3 -3
  83. package/dist/tempo/legacy/server/Session.d.ts.map +1 -1
  84. package/dist/tempo/legacy/server/Session.js.map +1 -1
  85. package/dist/tempo/server/Charge.d.ts +40 -78
  86. package/dist/tempo/server/Charge.d.ts.map +1 -1
  87. package/dist/tempo/server/Charge.js +289 -209
  88. package/dist/tempo/server/Charge.js.map +1 -1
  89. package/dist/tempo/server/Methods.d.ts +13 -23
  90. package/dist/tempo/server/Methods.d.ts.map +1 -1
  91. package/dist/tempo/server/Methods.js +6 -2
  92. package/dist/tempo/server/Methods.js.map +1 -1
  93. package/dist/tempo/server/Relay.d.ts +48 -0
  94. package/dist/tempo/server/Relay.d.ts.map +1 -0
  95. package/dist/tempo/server/Relay.js +177 -0
  96. package/dist/tempo/server/Relay.js.map +1 -0
  97. package/dist/tempo/server/SponsorBudget.d.ts +59 -0
  98. package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
  99. package/dist/tempo/server/SponsorBudget.js +144 -0
  100. package/dist/tempo/server/SponsorBudget.js.map +1 -0
  101. package/dist/tempo/server/Subscription.d.ts +3 -2
  102. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  103. package/dist/tempo/server/Subscription.js.map +1 -1
  104. package/dist/tempo/server/index.d.ts +1 -1
  105. package/dist/tempo/server/index.d.ts.map +1 -1
  106. package/dist/tempo/server/index.js.map +1 -1
  107. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  108. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  109. package/dist/tempo/server/internal/html.gen.js +1 -1
  110. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  111. package/dist/tempo/session/Snapshot.d.ts +9 -0
  112. package/dist/tempo/session/Snapshot.d.ts.map +1 -1
  113. package/dist/tempo/session/Snapshot.js +6 -0
  114. package/dist/tempo/session/Snapshot.js.map +1 -1
  115. package/dist/tempo/session/client/ChannelOps.d.ts +7 -1
  116. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  117. package/dist/tempo/session/client/ChannelOps.js +17 -3
  118. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  119. package/dist/tempo/session/client/CredentialState.d.ts +26 -2
  120. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  121. package/dist/tempo/session/client/CredentialState.js +99 -16
  122. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  123. package/dist/tempo/session/client/Runtime.d.ts +8 -0
  124. package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
  125. package/dist/tempo/session/client/Runtime.js +17 -0
  126. package/dist/tempo/session/client/Runtime.js.map +1 -1
  127. package/dist/tempo/session/client/Session.d.ts +16 -6
  128. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  129. package/dist/tempo/session/client/Session.js +93 -36
  130. package/dist/tempo/session/client/Session.js.map +1 -1
  131. package/dist/tempo/session/client/SessionManager.d.ts +10 -3
  132. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  133. package/dist/tempo/session/client/SessionManager.js +176 -50
  134. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  135. package/dist/tempo/session/client/Transports.d.ts +18 -7
  136. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  137. package/dist/tempo/session/client/Transports.js +81 -33
  138. package/dist/tempo/session/client/Transports.js.map +1 -1
  139. package/dist/tempo/session/client/internal/SessionManager.d.ts +19 -0
  140. package/dist/tempo/session/client/internal/SessionManager.d.ts.map +1 -0
  141. package/dist/tempo/session/client/internal/SessionManager.js +13 -0
  142. package/dist/tempo/session/client/internal/SessionManager.js.map +1 -0
  143. package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
  144. package/dist/tempo/session/precompile/Chain.js +109 -12
  145. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  146. package/dist/tempo/session/precompile/Channel.d.ts +2 -0
  147. package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
  148. package/dist/tempo/session/precompile/Channel.js +4 -0
  149. package/dist/tempo/session/precompile/Channel.js.map +1 -1
  150. package/dist/tempo/session/server/CredentialVerification.d.ts +3 -0
  151. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  152. package/dist/tempo/session/server/CredentialVerification.js +14 -0
  153. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  154. package/dist/tempo/session/server/MeteredStream.d.ts +7 -1
  155. package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -1
  156. package/dist/tempo/session/server/MeteredStream.js +5 -4
  157. package/dist/tempo/session/server/MeteredStream.js.map +1 -1
  158. package/dist/tempo/session/server/RequestState.d.ts +5 -1
  159. package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
  160. package/dist/tempo/session/server/RequestState.js +13 -5
  161. package/dist/tempo/session/server/RequestState.js.map +1 -1
  162. package/dist/tempo/session/server/Session.d.ts +21 -6
  163. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  164. package/dist/tempo/session/server/Session.js +4 -1
  165. package/dist/tempo/session/server/Session.js.map +1 -1
  166. package/dist/tempo/session/server/Settlement.d.ts +20 -0
  167. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  168. package/dist/tempo/session/server/Settlement.js +20 -0
  169. package/dist/tempo/session/server/Settlement.js.map +1 -1
  170. package/dist/tempo/session/server/Ws.d.ts +2 -0
  171. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  172. package/dist/tempo/session/server/Ws.js.map +1 -1
  173. package/dist/tempo/session/server/index.d.ts +1 -1
  174. package/dist/tempo/session/server/index.d.ts.map +1 -1
  175. package/dist/tempo/subscription/KeyAuthorization.d.ts +21 -21
  176. package/dist/validation/core.d.ts.map +1 -1
  177. package/dist/validation/core.js +6 -2
  178. package/dist/validation/core.js.map +1 -1
  179. package/dist/viem/Client.d.ts.map +1 -1
  180. package/dist/viem/Client.js +24 -17
  181. package/dist/viem/Client.js.map +1 -1
  182. package/package.json +6 -1
  183. package/src/Errors.test.ts +23 -0
  184. package/src/Errors.ts +21 -2
  185. package/src/Method.test.ts +102 -1
  186. package/src/Method.ts +241 -5
  187. package/src/cli/account.ts +45 -10
  188. package/src/cli/cli.test.ts +165 -70
  189. package/src/cli/cli.ts +67 -2
  190. package/src/cli/mcp.test.ts +11 -0
  191. package/src/cli/sessions/Manager.test.ts +249 -0
  192. package/src/cli/sessions/Manager.ts +93 -0
  193. package/src/cli/sessions/commands.ts +444 -0
  194. package/src/cli/sessions/request.test.ts +51 -0
  195. package/src/cli/sessions/request.ts +353 -0
  196. package/src/cli/sessions/store.test.ts +581 -0
  197. package/src/cli/sessions/store.ts +940 -0
  198. package/src/cli/utils.test.ts +23 -0
  199. package/src/cli/utils.ts +10 -0
  200. package/src/cli/validate/index.ts +12 -14
  201. package/src/cli/validate/messages.ts +7 -0
  202. package/src/cli/validate.test.ts +38 -5
  203. package/src/client/Mppx.test-d.ts +3 -3
  204. package/src/client/internal/Fetch.ts +16 -7
  205. package/src/client/internal/MethodChallenge.ts +30 -0
  206. package/src/client/node.test.ts +115 -0
  207. package/src/client/node.ts +247 -0
  208. package/src/internal/types.test-d.ts +21 -0
  209. package/src/internal/types.ts +33 -0
  210. package/src/server/Mppx.test-d.ts +44 -0
  211. package/src/server/Mppx.test.ts +205 -0
  212. package/src/server/Mppx.ts +167 -103
  213. package/src/stripe/Methods.test.ts +10 -0
  214. package/src/stripe/server/Charge.test-d.ts +66 -0
  215. package/src/stripe/server/Charge.ts +8 -5
  216. package/src/stripe/server/internal/html.gen.ts +1 -1
  217. package/src/tempo/PublicExports.test-d.ts +20 -0
  218. package/src/tempo/internal/fee-payer.ts +7 -3
  219. package/src/tempo/internal/types.ts +13 -2
  220. package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
  221. package/src/tempo/legacy/client/ChannelOps.ts +2 -1
  222. package/src/tempo/legacy/server/Defaults.test-d.ts +9 -0
  223. package/src/tempo/legacy/server/Session.ts +6 -5
  224. package/src/tempo/server/Charge.test.ts +337 -63
  225. package/src/tempo/server/Charge.ts +440 -270
  226. package/src/tempo/server/Methods.ts +16 -4
  227. package/src/tempo/server/Relay.test.ts +523 -0
  228. package/src/tempo/server/Relay.ts +288 -0
  229. package/src/tempo/server/SponsorBudget.test.ts +125 -0
  230. package/src/tempo/server/SponsorBudget.ts +213 -0
  231. package/src/tempo/server/Subscription.ts +6 -4
  232. package/src/tempo/server/index.ts +5 -1
  233. package/src/tempo/server/internal/html.gen.ts +1 -1
  234. package/src/tempo/session/README.md +201 -0
  235. package/src/tempo/session/Snapshot.ts +21 -0
  236. package/src/tempo/session/client/ChannelOps.test.ts +55 -0
  237. package/src/tempo/session/client/ChannelOps.ts +21 -3
  238. package/src/tempo/session/client/CredentialState.test.ts +2 -2
  239. package/src/tempo/session/client/CredentialState.ts +159 -16
  240. package/src/tempo/session/client/Runtime.test.ts +12 -0
  241. package/src/tempo/session/client/Runtime.ts +22 -0
  242. package/src/tempo/session/client/Session.test.ts +238 -5
  243. package/src/tempo/session/client/Session.ts +130 -41
  244. package/src/tempo/session/client/SessionManager.test.ts +239 -8
  245. package/src/tempo/session/client/SessionManager.ts +204 -54
  246. package/src/tempo/session/client/Transports.test.ts +181 -8
  247. package/src/tempo/session/client/Transports.ts +127 -45
  248. package/src/tempo/session/client/internal/SessionManager.ts +36 -0
  249. package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
  250. package/src/tempo/session/precompile/Chain.test.ts +223 -10
  251. package/src/tempo/session/precompile/Chain.ts +119 -12
  252. package/src/tempo/session/precompile/Channel.test.ts +9 -0
  253. package/src/tempo/session/precompile/Channel.ts +5 -0
  254. package/src/tempo/session/server/CredentialVerification.ts +18 -1
  255. package/src/tempo/session/server/MeteredStream.ts +12 -5
  256. package/src/tempo/session/server/RequestState.test.ts +47 -4
  257. package/src/tempo/session/server/RequestState.ts +15 -8
  258. package/src/tempo/session/server/Session.test.ts +461 -0
  259. package/src/tempo/session/server/Session.ts +31 -8
  260. package/src/tempo/session/server/Settlement.ts +45 -0
  261. package/src/tempo/session/server/Ws.test.ts +113 -0
  262. package/src/tempo/session/server/Ws.ts +2 -0
  263. package/src/tempo/session/server/index.ts +2 -0
  264. package/src/validation/core.ts +7 -1
  265. package/src/viem/Client.test.ts +44 -1
  266. package/src/viem/Client.ts +31 -17
@@ -12,7 +12,7 @@ import {
12
12
  type Hex,
13
13
  } from 'viem'
14
14
  import { privateKeyToAccount } from 'viem/accounts'
15
- import { Transaction } from 'viem/tempo'
15
+ import { Abis, Addresses, Transaction } from 'viem/tempo'
16
16
  import { describe, expect, test } from 'vp/test'
17
17
 
18
18
  import { VerificationFailedError } from '../../../Errors.js'
@@ -36,6 +36,7 @@ const descriptor = {
36
36
  const deposit = Types.uint96(1_000_000n)
37
37
  const chainId = 42431
38
38
  const txHash = `0x${'ab'.repeat(32)}` as const
39
+ const sourceToken = '0x6666666666666666666666666666666666666666' as const
39
40
  const feePayer = privateKeyToAccount(
40
41
  '0x59c6995e998f97a5a0044966f0945389d1fc6e60e7346d6c36c49d32f75b9a1b',
41
42
  )
@@ -62,6 +63,7 @@ function createMockClient(
62
63
  parameters.rpcMethods?.push(args.method)
63
64
  parameters.onRequest?.(args.method, args.params)
64
65
  if (args.method === 'eth_chainId') return `0x${chainId.toString(16)}`
66
+ if (args.method === 'eth_sendTransaction') return txHash
65
67
  if (args.method === 'eth_sendRawTransaction') return txHash
66
68
  if (args.method === 'eth_sendRawTransactionSync') return parameters.receipt ?? receipt([])
67
69
  if (args.method === 'eth_getTransactionReceipt') return parameters.receipt ?? null
@@ -161,7 +163,11 @@ function topUpLog(parameters: { channelId: `0x${string}`; newDeposit: bigint })
161
163
  }
162
164
 
163
165
  async function createSerializedTransaction(parameters: {
164
- calls: { to: `0x${string}`; data?: `0x${string}` | undefined }[]
166
+ calls: {
167
+ to?: `0x${string}` | undefined
168
+ data?: `0x${string}` | undefined
169
+ value?: bigint | undefined
170
+ }[]
165
171
  gas?: bigint | undefined
166
172
  signed?: boolean | undefined
167
173
  }) {
@@ -191,6 +197,28 @@ async function createSerializedTransaction(parameters: {
191
197
  } as never)) as `0x${string}`
192
198
  }
193
199
 
200
+ function autoSwapCalls(amountOut: bigint = deposit) {
201
+ const maxAmountIn = amountOut + 1_000n
202
+ return [
203
+ {
204
+ to: sourceToken,
205
+ data: encodeFunctionData({
206
+ abi: Abis.tip20,
207
+ functionName: 'approve',
208
+ args: [Addresses.stablecoinDex, maxAmountIn],
209
+ }),
210
+ },
211
+ {
212
+ to: Addresses.stablecoinDex,
213
+ data: encodeFunctionData({
214
+ abi: Abis.stablecoinDex,
215
+ functionName: 'swapExactAmountOut',
216
+ args: [sourceToken, descriptor.token, amountOut, maxAmountIn],
217
+ }),
218
+ },
219
+ ] as const
220
+ }
221
+
194
222
  async function createOpenTransaction(
195
223
  parameters: {
196
224
  authorizedSigner?: `0x${string}` | undefined
@@ -200,6 +228,13 @@ async function createOpenTransaction(
200
228
  signed?: boolean | undefined
201
229
  token?: `0x${string}` | undefined
202
230
  to?: `0x${string}` | undefined
231
+ prefixCalls?:
232
+ | readonly {
233
+ to?: `0x${string}` | undefined
234
+ data?: `0x${string}` | undefined
235
+ value?: bigint | undefined
236
+ }[]
237
+ | undefined
203
238
  } = {},
204
239
  ) {
205
240
  const data = encodeFunctionData({
@@ -215,7 +250,7 @@ async function createOpenTransaction(
215
250
  ],
216
251
  })
217
252
  return createSerializedTransaction({
218
- calls: [{ to: parameters.to ?? tip20ChannelEscrow, data }],
253
+ calls: [...(parameters.prefixCalls ?? []), { to: parameters.to ?? tip20ChannelEscrow, data }],
219
254
  gas: parameters.gas,
220
255
  signed: parameters.signed,
221
256
  })
@@ -228,6 +263,13 @@ async function createTopUpTransaction(
228
263
  gas?: bigint | undefined
229
264
  signed?: boolean | undefined
230
265
  to?: `0x${string}` | undefined
266
+ prefixCalls?:
267
+ | readonly {
268
+ to?: `0x${string}` | undefined
269
+ data?: `0x${string}` | undefined
270
+ value?: bigint | undefined
271
+ }[]
272
+ | undefined
231
273
  } = {},
232
274
  ) {
233
275
  const data = encodeFunctionData({
@@ -239,7 +281,7 @@ async function createTopUpTransaction(
239
281
  ],
240
282
  })
241
283
  return createSerializedTransaction({
242
- calls: [{ to: parameters.to ?? tip20ChannelEscrow, data }],
284
+ calls: [...(parameters.prefixCalls ?? []), { to: parameters.to ?? tip20ChannelEscrow, data }],
243
285
  gas: parameters.gas,
244
286
  signed: parameters.signed,
245
287
  })
@@ -290,6 +332,31 @@ describe('precompile receipt wait', () => {
290
332
  })
291
333
  })
292
334
 
335
+ describe('precompile server transactions', () => {
336
+ test('does not add a fee-payer signature when the sender and fee payer are the same account', async () => {
337
+ const rpcMethods: string[] = []
338
+ const client = createMockClient({ rpcMethods })
339
+ const sender = { address: feePayer.address, type: 'json-rpc' } as const
340
+
341
+ await Chain.closeOnChain(
342
+ client,
343
+ descriptor,
344
+ 1n,
345
+ 1n,
346
+ `0x${'11'.repeat(65)}`,
347
+ tip20ChannelEscrow,
348
+ {
349
+ account: sender,
350
+ candidateFeeTokens: [descriptor.token],
351
+ feePayer,
352
+ },
353
+ )
354
+
355
+ expect(rpcMethods).toContain('eth_sendTransaction')
356
+ expect(rpcMethods).not.toContain('eth_sendRawTransactionSync')
357
+ })
358
+ })
359
+
293
360
  describe('precompile open calldata parsing', () => {
294
361
  test('parseOpenCall accepts TIP-1034 open calldata', () => {
295
362
  const data = encodeFunctionData({
@@ -420,8 +487,152 @@ describe('precompile broadcastOpenTransaction', () => {
420
487
  expectedPayer: descriptor.payer,
421
488
  serializedTransaction,
422
489
  }),
423
- ).rejects.toThrow('TIP-1034 open transaction must contain exactly one call')
490
+ ).rejects.toThrow(
491
+ 'TIP-1034 open transaction must contain one management call, optionally preceded by an auto-swap',
492
+ )
493
+ })
494
+
495
+ const [validApprove, validSwap] = autoSwapCalls()
496
+ const wrongToken = '0x7777777777777777777777777777777777777777' as const
497
+ const maxAmountIn = deposit + 1_000n
498
+ const approve = (parameters: {
499
+ amount?: bigint | undefined
500
+ spender?: Address | undefined
501
+ token?: Address | undefined
502
+ }) => ({
503
+ to: parameters.token ?? sourceToken,
504
+ data: encodeFunctionData({
505
+ abi: Abis.tip20,
506
+ functionName: 'approve',
507
+ args: [parameters.spender ?? Addresses.stablecoinDex, parameters.amount ?? maxAmountIn],
508
+ }),
509
+ })
510
+ const swap = (parameters: {
511
+ amountOut?: bigint | undefined
512
+ maxAmount?: bigint | undefined
513
+ tokenIn?: Address | undefined
514
+ tokenOut?: Address | undefined
515
+ }) => ({
516
+ to: Addresses.stablecoinDex as Address,
517
+ data: encodeFunctionData({
518
+ abi: Abis.stablecoinDex,
519
+ functionName: 'swapExactAmountOut',
520
+ args: [
521
+ parameters.tokenIn ?? sourceToken,
522
+ parameters.tokenOut ?? descriptor.token,
523
+ parameters.amountOut ?? deposit,
524
+ parameters.maxAmount ?? maxAmountIn,
525
+ ],
526
+ }),
424
527
  })
528
+ const invalidAutoSwapPrefixes = [
529
+ {
530
+ name: 'missing call target',
531
+ prefixCalls: [{ data: validApprove.data }, validSwap],
532
+ reason: 'call is missing a target or calldata',
533
+ },
534
+ {
535
+ name: 'native value',
536
+ prefixCalls: [{ ...validApprove, value: 1n }, validSwap],
537
+ reason: 'calls must not transfer native value',
538
+ },
539
+ {
540
+ name: 'wrong DEX target',
541
+ prefixCalls: [validApprove, { ...validSwap, to: wrongToken }],
542
+ reason: 'targets the wrong DEX',
543
+ },
544
+ {
545
+ name: 'invalid approval calldata',
546
+ prefixCalls: [{ ...validApprove, data: '0x12345678' as const }, validSwap],
547
+ reason: 'approval calldata is invalid',
548
+ },
549
+ {
550
+ name: 'invalid swap calldata',
551
+ prefixCalls: [validApprove, { ...validSwap, data: '0x12345678' as const }],
552
+ reason: 'swap calldata is invalid',
553
+ },
554
+ {
555
+ name: 'wrong approval function',
556
+ prefixCalls: [
557
+ {
558
+ to: sourceToken,
559
+ data: encodeFunctionData({
560
+ abi: Abis.tip20,
561
+ functionName: 'transfer',
562
+ args: [Addresses.stablecoinDex, maxAmountIn],
563
+ }),
564
+ },
565
+ validSwap,
566
+ ],
567
+ reason: 'must contain approve followed by swapExactAmountOut',
568
+ },
569
+ {
570
+ name: 'approval token mismatch',
571
+ prefixCalls: [approve({ token: wrongToken }), validSwap],
572
+ reason: 'approval token does not match swap input',
573
+ },
574
+ {
575
+ name: 'approval spender mismatch',
576
+ prefixCalls: [approve({ spender: wrongToken }), validSwap],
577
+ reason: 'approval spender is not the DEX',
578
+ },
579
+ {
580
+ name: 'approval amount mismatch',
581
+ prefixCalls: [approve({ amount: maxAmountIn + 1n }), validSwap],
582
+ reason: 'approval amount does not match swap maximum input',
583
+ },
584
+ {
585
+ name: 'output token mismatch',
586
+ prefixCalls: [validApprove, swap({ tokenOut: wrongToken })],
587
+ reason: 'output token does not match channel currency',
588
+ },
589
+ {
590
+ name: 'output amount mismatch',
591
+ prefixCalls: [
592
+ approve({ amount: maxAmountIn + 1n }),
593
+ swap({ amountOut: deposit + 1n, maxAmount: maxAmountIn + 1n }),
594
+ ],
595
+ reason: 'output amount does not match channel deposit',
596
+ },
597
+ {
598
+ name: 'identical input and output tokens',
599
+ prefixCalls: [approve({ token: descriptor.token }), swap({ tokenIn: descriptor.token })],
600
+ reason: 'input and output tokens must differ',
601
+ },
602
+ {
603
+ name: 'non-canonical approval calldata',
604
+ prefixCalls: [{ ...validApprove, data: `${validApprove.data}00` as Hex }, validSwap],
605
+ reason: 'approval calldata is not canonical',
606
+ },
607
+ {
608
+ name: 'non-canonical swap calldata',
609
+ prefixCalls: [validApprove, { ...validSwap, data: `${validSwap.data}00` as Hex }],
610
+ reason: 'swap calldata is not canonical',
611
+ },
612
+ ] as const
613
+
614
+ test.each(invalidAutoSwapPrefixes)(
615
+ 'rejects an auto-swap with $name',
616
+ async ({ prefixCalls, reason }) => {
617
+ const serializedTransaction = await createOpenTransaction({ prefixCalls })
618
+
619
+ await expect(
620
+ Chain.broadcastOpenTransaction({
621
+ chainId,
622
+ client: createMockClient(),
623
+ escrowContract: tip20ChannelEscrow,
624
+ expectedAuthorizedSigner: descriptor.authorizedSigner,
625
+ expectedChannelId: `0x${'11'.repeat(32)}`,
626
+ expectedCurrency: descriptor.token,
627
+ expectedExpiringNonceHash: expectedExpiringNonceHash(serializedTransaction),
628
+ expectedOperator: descriptor.operator,
629
+ expectedPayee: descriptor.payee,
630
+ expectedPayer: descriptor.payer,
631
+ serializedTransaction,
632
+ }),
633
+ ).rejects.toThrow(`TIP-1034 open auto-swap ${reason}`)
634
+ },
635
+ )
425
636
 
426
637
  test('rejects open transactions targeting the wrong escrow contract', async () => {
427
638
  const serializedTransaction = await createOpenTransaction({ to: descriptor.token })
@@ -689,8 +900,8 @@ describe('precompile broadcastOpenTransaction', () => {
689
900
  ).rejects.toThrow('credential expiringNonceHash does not match transaction')
690
901
  })
691
902
 
692
- test('returns tx hash, descriptor, event fields, and read-back state on success', async () => {
693
- const serializedTransaction = await createOpenTransaction()
903
+ test('accepts an exact-output auto-swap before open', async () => {
904
+ const serializedTransaction = await createOpenTransaction({ prefixCalls: autoSwapCalls() })
694
905
  const expiringNonceHash = expectedExpiringNonceHash(serializedTransaction)
695
906
  const expectedDescriptor = { ...descriptor, expiringNonceHash }
696
907
  const channelId = Channel.computeId({
@@ -832,7 +1043,9 @@ describe('precompile broadcastTopUpTransaction', () => {
832
1043
  expectedCurrency: descriptor.token,
833
1044
  serializedTransaction,
834
1045
  }),
835
- ).rejects.toThrow('TIP-1034 topUp transaction must contain exactly one call')
1046
+ ).rejects.toThrow(
1047
+ 'TIP-1034 topUp transaction must contain one management call, optionally preceded by an auto-swap',
1048
+ )
836
1049
  })
837
1050
 
838
1051
  test('rejects top-up transactions targeting the wrong escrow contract', async () => {
@@ -999,8 +1212,8 @@ describe('precompile broadcastTopUpTransaction', () => {
999
1212
  ).rejects.toThrow('topUp deposit does not match credential')
1000
1213
  })
1001
1214
 
1002
- test('returns tx hash, new deposit, and read-back state on success', async () => {
1003
- const serializedTransaction = await createTopUpTransaction()
1215
+ test('accepts an exact-output auto-swap before top-up', async () => {
1216
+ const serializedTransaction = await createTopUpTransaction({ prefixCalls: autoSwapCalls() })
1004
1217
  const channelId = Channel.computeId({ ...descriptor, chainId, escrow: tip20ChannelEscrow })
1005
1218
  const newDeposit = deposit * 2n
1006
1219
  const state = { settled: 0n, deposit: newDeposit, closeRequestedAt: 0 }
@@ -1,5 +1,5 @@
1
1
  import type { Account, Address, Client, Hex } from 'viem'
2
- import { encodeFunctionData, isAddressEqual, parseEventLogs } from 'viem'
2
+ import { decodeFunctionData, encodeFunctionData, isAddressEqual, parseEventLogs } from 'viem'
3
3
  import {
4
4
  call,
5
5
  prepareTransactionRequest,
@@ -10,7 +10,7 @@ import {
10
10
  signTransaction,
11
11
  waitForTransactionReceipt,
12
12
  } from 'viem/actions'
13
- import { Transaction } from 'viem/tempo'
13
+ import { Abis, Addresses, Transaction } from 'viem/tempo'
14
14
 
15
15
  import { BadRequestError, VerificationFailedError } from '../../../Errors.js'
16
16
  import * as FeePayer from '../../internal/fee-payer.js'
@@ -401,6 +401,7 @@ export type ChannelTransactionOptions = {
401
401
 
402
402
  type ParsedPrecompileCredentialTransaction = {
403
403
  call: Transaction.TransactionTempo['calls'][number] & { data: Hex; to: Address }
404
+ prefixCalls: readonly Transaction.TransactionTempo['calls'][number][]
404
405
  transaction: ReturnType<(typeof Transaction)['deserialize']>
405
406
  }
406
407
 
@@ -418,11 +419,11 @@ function parsePrecompileCredentialTransaction(parameters: {
418
419
  serializedTransaction as Transaction.TransactionSerializedTempo,
419
420
  )
420
421
  const calls = transaction.calls
421
- if (calls.length !== 1)
422
+ if (calls.length !== 1 && calls.length !== 3)
422
423
  throw new VerificationFailedError({
423
- reason: `TIP-1034 ${label} transaction must contain exactly one call`,
424
+ reason: `TIP-1034 ${label} transaction must contain one management call, optionally preceded by an auto-swap`,
424
425
  })
425
- const call = calls[0]!
426
+ const call = calls[calls.length - 1]!
426
427
  if (!call.to || !isAddressEqual(call.to, escrowContract))
427
428
  throw new VerificationFailedError({
428
429
  reason: `TIP-1034 ${label} transaction targets the wrong address`,
@@ -431,7 +432,88 @@ function parsePrecompileCredentialTransaction(parameters: {
431
432
  throw new VerificationFailedError({
432
433
  reason: `TIP-1034 ${label} transaction is missing calldata`,
433
434
  })
434
- return { transaction, call: { ...call, data: call.data, to: call.to } }
435
+ return {
436
+ transaction,
437
+ call: { ...call, data: call.data, to: call.to },
438
+ prefixCalls: calls.slice(0, -1),
439
+ }
440
+ }
441
+
442
+ function validateAutoSwapPrefix(parameters: {
443
+ amountOut: bigint
444
+ currency: Address
445
+ label: 'open' | 'topUp'
446
+ prefixCalls: readonly Transaction.TransactionTempo['calls'][number][]
447
+ }) {
448
+ const { amountOut, currency, label, prefixCalls } = parameters
449
+ if (prefixCalls.length === 0) return
450
+
451
+ const fail = (reason: string): never => {
452
+ throw new VerificationFailedError({ reason: `TIP-1034 ${label} auto-swap ${reason}` })
453
+ }
454
+ if (prefixCalls.length !== 2) fail('must contain exactly approve and swap calls')
455
+
456
+ const approveCall = prefixCalls[0]!
457
+ const swapCall = prefixCalls[1]!
458
+ const approveTo = approveCall.to
459
+ const approveData = approveCall.data
460
+ const swapTo = swapCall.to
461
+ const swapData = swapCall.data
462
+ if (!approveTo || !approveData || !swapTo || !swapData)
463
+ fail('call is missing a target or calldata')
464
+ const checkedApproveTo = approveTo as Address
465
+ const checkedApproveData = approveData as Hex
466
+ const checkedSwapTo = swapTo as Address
467
+ const checkedSwapData = swapData as Hex
468
+ if ((approveCall.value ?? 0n) !== 0n || (swapCall.value ?? 0n) !== 0n)
469
+ fail('calls must not transfer native value')
470
+ if (!isAddressEqual(checkedSwapTo, Addresses.stablecoinDex)) fail('targets the wrong DEX')
471
+
472
+ const approve = (() => {
473
+ try {
474
+ return decodeFunctionData({ abi: Abis.tip20, data: checkedApproveData })
475
+ } catch {
476
+ return fail('approval calldata is invalid')
477
+ }
478
+ })()
479
+ const swap = (() => {
480
+ try {
481
+ return decodeFunctionData({ abi: Abis.stablecoinDex, data: checkedSwapData })
482
+ } catch {
483
+ return fail('swap calldata is invalid')
484
+ }
485
+ })()
486
+ if (approve.functionName !== 'approve' || swap.functionName !== 'swapExactAmountOut')
487
+ fail('must contain approve followed by swapExactAmountOut')
488
+
489
+ const [spender, approvedAmount] = approve.args as readonly [Address, bigint]
490
+ const [tokenIn, tokenOut, swapAmountOut, maxAmountIn] = swap.args as readonly [
491
+ Address,
492
+ Address,
493
+ bigint,
494
+ bigint,
495
+ ]
496
+ if (!isAddressEqual(checkedApproveTo, tokenIn)) fail('approval token does not match swap input')
497
+ if (!isAddressEqual(spender, Addresses.stablecoinDex)) fail('approval spender is not the DEX')
498
+ if (approvedAmount !== maxAmountIn) fail('approval amount does not match swap maximum input')
499
+ if (!isAddressEqual(tokenOut, currency)) fail('output token does not match channel currency')
500
+ if (swapAmountOut !== amountOut) fail('output amount does not match channel deposit')
501
+ if (isAddressEqual(tokenIn, tokenOut)) fail('input and output tokens must differ')
502
+
503
+ const canonicalApprove = encodeFunctionData({
504
+ abi: Abis.tip20,
505
+ functionName: 'approve',
506
+ args: [spender, approvedAmount],
507
+ })
508
+ const canonicalSwap = encodeFunctionData({
509
+ abi: Abis.stablecoinDex,
510
+ functionName: 'swapExactAmountOut',
511
+ args: [tokenIn, tokenOut, swapAmountOut, maxAmountIn],
512
+ })
513
+ if (checkedApproveData.toLowerCase() !== canonicalApprove.toLowerCase())
514
+ fail('approval calldata is not canonical')
515
+ if (checkedSwapData.toLowerCase() !== canonicalSwap.toLowerCase())
516
+ fail('swap calldata is not canonical')
435
517
  }
436
518
 
437
519
  async function simulateTempoTransaction(client: Client, request: unknown) {
@@ -796,7 +878,7 @@ export type BroadcastOpenTransactionParameters = {
796
878
  export async function broadcastOpenTransaction(
797
879
  parameters: BroadcastOpenTransactionParameters,
798
880
  ): Promise<BroadcastOpenTransactionResult> {
799
- const { transaction, call } = parsePrecompileCredentialTransaction({
881
+ const { transaction, call, prefixCalls } = parsePrecompileCredentialTransaction({
800
882
  escrowContract: parameters.escrowContract,
801
883
  feePayer: parameters.feePayer,
802
884
  label: 'open',
@@ -812,6 +894,12 @@ export async function broadcastOpenTransaction(
812
894
  authorizedSigner: parameters.expectedAuthorizedSigner,
813
895
  },
814
896
  })
897
+ validateAutoSwapPrefix({
898
+ amountOut: open.deposit,
899
+ currency: parameters.expectedCurrency,
900
+ label: 'open',
901
+ prefixCalls,
902
+ })
815
903
  const descriptor = ChannelOps.descriptorFromOpen({
816
904
  chainId: parameters.chainId,
817
905
  escrow: parameters.escrowContract,
@@ -920,7 +1008,7 @@ export type BroadcastTopUpTransactionParameters = {
920
1008
  export async function broadcastTopUpTransaction(
921
1009
  parameters: BroadcastTopUpTransactionParameters,
922
1010
  ): Promise<BroadcastTopUpTransactionResult> {
923
- const { transaction, call } = parsePrecompileCredentialTransaction({
1011
+ const { transaction, call, prefixCalls } = parsePrecompileCredentialTransaction({
924
1012
  escrowContract: parameters.escrowContract,
925
1013
  feePayer: parameters.feePayer,
926
1014
  label: 'topUp',
@@ -933,6 +1021,12 @@ export async function broadcastTopUpTransaction(
933
1021
  additionalDeposit: parameters.additionalDeposit,
934
1022
  },
935
1023
  })
1024
+ validateAutoSwapPrefix({
1025
+ amountOut: parameters.additionalDeposit,
1026
+ currency: parameters.expectedCurrency,
1027
+ label: 'topUp',
1028
+ prefixCalls,
1029
+ })
936
1030
  const receipt = await sendCredentialTransaction({
937
1031
  challengeExpires: parameters.challengeExpires,
938
1032
  chainId: parameters.chainId,
@@ -1001,8 +1095,11 @@ async function sendPrecompileTransaction(
1001
1095
  label: string,
1002
1096
  options?: ChannelTransactionOptions,
1003
1097
  ): Promise<Hex> {
1004
- if (options?.feePayer) {
1005
- const account = options.account ?? client.account
1098
+ const account = options?.account ?? client.account
1099
+ const selfSponsored =
1100
+ account && options?.feePayer && isAddressEqual(account.address, options.feePayer.address)
1101
+
1102
+ if (options?.feePayer && !selfSponsored) {
1006
1103
  if (!account) throw new Error(`Cannot ${label} precompile channel: no account available.`)
1007
1104
  const feeToken =
1008
1105
  options.feeToken ??
@@ -1033,10 +1130,20 @@ async function sendPrecompileTransaction(
1033
1130
  return receipt.transactionHash
1034
1131
  }
1035
1132
 
1133
+ const feeToken =
1134
+ options?.feeToken ??
1135
+ (selfSponsored
1136
+ ? await resolveFeeToken({
1137
+ account: account.address,
1138
+ candidateTokens: options?.candidateFeeTokens,
1139
+ client,
1140
+ })
1141
+ : undefined)
1142
+
1036
1143
  return sendPrecompileContractCall(client, {
1037
- account: options?.account,
1144
+ account,
1038
1145
  to,
1039
1146
  data,
1040
- feeToken: options?.feeToken,
1147
+ feeToken,
1041
1148
  })
1042
1149
  }
@@ -27,6 +27,15 @@ const transaction = {
27
27
  } as const satisfies Channel.ExpiringNonceTransaction
28
28
 
29
29
  describe('precompile Channel.computeId', () => {
30
+ test('uses the payer when the descriptor delegates to the zero address', () => {
31
+ expect(
32
+ Channel.resolveAuthorizedSigner({
33
+ ...descriptor,
34
+ authorizedSigner: '0x0000000000000000000000000000000000000000',
35
+ }),
36
+ ).toBe(descriptor.payer)
37
+ })
38
+
30
39
  test('returns deterministic 32-byte hash for fixed inputs', () => {
31
40
  const id = Channel.computeId({ ...descriptor, chainId })
32
41
  expect(Channel.computeId({ ...descriptor, chainId })).toBe(id)
@@ -9,6 +9,11 @@ import type { ChannelDescriptor } from './Protocol.js'
9
9
  /** Re-export of the TIP-1034 channel descriptor shape. */
10
10
  export type { ChannelDescriptor } from './Protocol.js'
11
11
 
12
+ /** Resolves the descriptor's effective voucher signer; zero delegates to the payer. */
13
+ export function resolveAuthorizedSigner(descriptor: ChannelDescriptor): Address {
14
+ return BigInt(descriptor.authorizedSigner) === 0n ? descriptor.payer : descriptor.authorizedSigner
15
+ }
16
+
12
17
  /** Tempo transaction shape used to derive the TIP-1034 `expiringNonceHash`. */
13
18
  export type ExpiringNonceTransaction = (
14
19
  | z_TransactionSerializableTempo
@@ -30,7 +30,7 @@ import {
30
30
  import * as Voucher from '../precompile/Voucher.js'
31
31
  import * as ChannelStore from './ChannelStore.js'
32
32
  import { getChallengePaymentFields } from './RequestState.js'
33
- import { assertSettlementSender, getClientAccount } from './Settlement.js'
33
+ import { assertSettlementSender, getClientAccount, type OnSessionSettlement } from './Settlement.js'
34
34
 
35
35
  /** Returns the effective voucher signer for a TIP-1034 descriptor. */
36
36
  export function authorizedSigner(descriptor: Channel.ChannelDescriptor): Address {
@@ -344,6 +344,8 @@ export type VerifyCredentialPayloadParameters = {
344
344
  lastOnChainVerified: Map<Hex, number>
345
345
  /** Minimum allowed voucher delta in raw units. */
346
346
  minVoucherDelta: bigint
347
+ /** Callback invoked after an on-chain settlement or close transaction is confirmed. */
348
+ onSessionSettlement?: OnSessionSettlement | undefined
347
349
  /** Discriminated session credential payload to verify. */
348
350
  payload: SessionCredentialPayload
349
351
  /** Server-side channel store. */
@@ -747,6 +749,21 @@ async function handleCloseCredential(
747
749
  signature: payload.signature,
748
750
  }),
749
751
  )
752
+ if (parameters.onSessionSettlement && txHash) {
753
+ try {
754
+ await parameters.onSessionSettlement(
755
+ Object.freeze({
756
+ txHash,
757
+ channelId,
758
+ trigger: 'close' as const,
759
+ amount: settledToPayee,
760
+ delta: settledToPayee - state.settled,
761
+ }),
762
+ )
763
+ } catch {
764
+ // Errors are isolated — observers cannot break the settlement flow.
765
+ }
766
+ }
750
767
  return createSessionReceipt({
751
768
  challengeId: challenge.id,
752
769
  channelId,
@@ -12,8 +12,14 @@ export type SessionController = {
12
12
  * The reservation blocks until sufficient voucher headroom exists, but the
13
13
  * charge is only committed once a chunk is actually emitted. If the stream
14
14
  * ends or aborts before that emission, the reservation is dropped.
15
+ *
16
+ * Pass an explicit raw-unit `amount` for request-aware or otherwise dynamic
17
+ * pricing. When omitted, the session challenge's configured tick cost is
18
+ * used.
15
19
  */
16
- charge(): Promise<void>
20
+ charge(amount?: bigint): Promise<void>
21
+ /** Aborted when the client closes or requests the final session receipt. */
22
+ signal: AbortSignal
17
23
  }
18
24
 
19
25
  /** Async stream source accepted by paid session transports. */
@@ -49,7 +55,7 @@ export async function* meterIterable(options: MeteredStreamOptions): AsyncGenera
49
55
  let reservedAmount = 0n
50
56
  let reservedUnits = 0
51
57
 
52
- const charge = async () => {
58
+ const charge = async (amount = options.tickCost) => {
53
59
  if (prepaidUnits > 0) {
54
60
  prepaidUnits -= 1
55
61
  return
@@ -58,19 +64,20 @@ export async function* meterIterable(options: MeteredStreamOptions): AsyncGenera
58
64
  await reserveChargeOrWait({
59
65
  store: options.store,
60
66
  channelId: options.channelId,
61
- amount: options.tickCost,
67
+ amount,
62
68
  reservedAmount,
63
69
  emit: options.emitNeedVoucher,
64
70
  formatNeedVoucher: options.formatNeedVoucher,
65
71
  pollIntervalMs: options.pollIntervalMs,
66
72
  signal: options.signal,
67
73
  })
68
- reservedAmount += options.tickCost
74
+ reservedAmount += amount
69
75
  reservedUnits += 1
70
76
  }
71
77
 
78
+ const signal = options.signal ?? new AbortController().signal
72
79
  const iterable =
73
- typeof options.generate === 'function' ? options.generate({ charge }) : options.generate
80
+ typeof options.generate === 'function' ? options.generate({ charge, signal }) : options.generate
74
81
 
75
82
  for await (const value of iterable) {
76
83
  if (options.signal?.aborted) break