mppx 0.6.27 → 0.6.29

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 (295) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/Challenge.d.ts.map +1 -1
  3. package/dist/Challenge.js +16 -10
  4. package/dist/Challenge.js.map +1 -1
  5. package/dist/Method.d.ts +1 -1
  6. package/dist/Method.d.ts.map +1 -1
  7. package/dist/Store.d.ts +32 -9
  8. package/dist/Store.d.ts.map +1 -1
  9. package/dist/Store.js +42 -10
  10. package/dist/Store.js.map +1 -1
  11. package/dist/client/Methods.d.ts +1 -0
  12. package/dist/client/Methods.d.ts.map +1 -1
  13. package/dist/client/Methods.js +1 -0
  14. package/dist/client/Methods.js.map +1 -1
  15. package/dist/client/Mppx.d.ts +3 -3
  16. package/dist/client/Mppx.d.ts.map +1 -1
  17. package/dist/client/Mppx.js +1 -0
  18. package/dist/client/Mppx.js.map +1 -1
  19. package/dist/client/Transport.d.ts +10 -3
  20. package/dist/client/Transport.d.ts.map +1 -1
  21. package/dist/client/Transport.js +60 -7
  22. package/dist/client/Transport.js.map +1 -1
  23. package/dist/client/index.d.ts +1 -1
  24. package/dist/client/index.d.ts.map +1 -1
  25. package/dist/client/index.js +1 -1
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/internal/Fetch.d.ts +3 -0
  28. package/dist/client/internal/Fetch.d.ts.map +1 -1
  29. package/dist/client/internal/Fetch.js +12 -20
  30. package/dist/client/internal/Fetch.js.map +1 -1
  31. package/dist/evm/Assets.d.ts +2 -0
  32. package/dist/evm/Assets.d.ts.map +1 -0
  33. package/dist/evm/Assets.js +2 -0
  34. package/dist/evm/Assets.js.map +1 -0
  35. package/dist/evm/Chains.d.ts +5 -0
  36. package/dist/evm/Chains.d.ts.map +1 -0
  37. package/dist/evm/Chains.js +5 -0
  38. package/dist/evm/Chains.js.map +1 -0
  39. package/dist/evm/Methods.d.ts +68 -0
  40. package/dist/evm/Methods.d.ts.map +1 -0
  41. package/dist/evm/Methods.js +28 -0
  42. package/dist/evm/Methods.js.map +1 -0
  43. package/dist/evm/Types.d.ts +143 -0
  44. package/dist/evm/Types.d.ts.map +1 -0
  45. package/dist/evm/Types.js +102 -0
  46. package/dist/evm/Types.js.map +1 -0
  47. package/dist/evm/client/Charge.d.ts +102 -0
  48. package/dist/evm/client/Charge.d.ts.map +1 -0
  49. package/dist/evm/client/Charge.js +141 -0
  50. package/dist/evm/client/Charge.js.map +1 -0
  51. package/dist/evm/client/Methods.d.ts +81 -0
  52. package/dist/evm/client/Methods.d.ts.map +1 -0
  53. package/dist/evm/client/Methods.js +16 -0
  54. package/dist/evm/client/Methods.js.map +1 -0
  55. package/dist/evm/client/index.d.ts +6 -0
  56. package/dist/evm/client/index.d.ts.map +1 -0
  57. package/dist/evm/client/index.js +6 -0
  58. package/dist/evm/client/index.js.map +1 -0
  59. package/dist/evm/index.d.ts +9 -0
  60. package/dist/evm/index.d.ts.map +1 -0
  61. package/dist/evm/index.js +8 -0
  62. package/dist/evm/index.js.map +1 -0
  63. package/dist/evm/server/Charge.d.ts +62 -0
  64. package/dist/evm/server/Charge.d.ts.map +1 -0
  65. package/dist/evm/server/Charge.js +172 -0
  66. package/dist/evm/server/Charge.js.map +1 -0
  67. package/dist/evm/server/Methods.d.ts +80 -0
  68. package/dist/evm/server/Methods.d.ts.map +1 -0
  69. package/dist/evm/server/Methods.js +16 -0
  70. package/dist/evm/server/Methods.js.map +1 -0
  71. package/dist/evm/server/index.d.ts +6 -0
  72. package/dist/evm/server/index.d.ts.map +1 -0
  73. package/dist/evm/server/index.js +6 -0
  74. package/dist/evm/server/index.js.map +1 -0
  75. package/dist/index.d.ts +2 -0
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +2 -0
  78. package/dist/index.js.map +1 -1
  79. package/dist/internal/HeaderCodec.d.ts +18 -0
  80. package/dist/internal/HeaderCodec.d.ts.map +1 -0
  81. package/dist/internal/HeaderCodec.js +31 -0
  82. package/dist/internal/HeaderCodec.js.map +1 -0
  83. package/dist/middlewares/elysia.d.ts.map +1 -1
  84. package/dist/middlewares/elysia.js +2 -3
  85. package/dist/middlewares/elysia.js.map +1 -1
  86. package/dist/middlewares/express.js +2 -1
  87. package/dist/middlewares/express.js.map +1 -1
  88. package/dist/proxy/internal/Headers.d.ts +13 -1
  89. package/dist/proxy/internal/Headers.d.ts.map +1 -1
  90. package/dist/proxy/internal/Headers.js +25 -2
  91. package/dist/proxy/internal/Headers.js.map +1 -1
  92. package/dist/proxy/services/openai.d.ts.map +1 -1
  93. package/dist/proxy/services/openai.js +2 -0
  94. package/dist/proxy/services/openai.js.map +1 -1
  95. package/dist/server/Methods.d.ts +1 -0
  96. package/dist/server/Methods.d.ts.map +1 -1
  97. package/dist/server/Methods.js +1 -0
  98. package/dist/server/Methods.js.map +1 -1
  99. package/dist/server/Mppx.d.ts.map +1 -1
  100. package/dist/server/Mppx.js +90 -12
  101. package/dist/server/Mppx.js.map +1 -1
  102. package/dist/server/Transport.d.ts +10 -0
  103. package/dist/server/Transport.d.ts.map +1 -1
  104. package/dist/server/Transport.js +9 -0
  105. package/dist/server/Transport.js.map +1 -1
  106. package/dist/server/index.d.ts +1 -1
  107. package/dist/server/index.d.ts.map +1 -1
  108. package/dist/server/index.js +1 -1
  109. package/dist/server/index.js.map +1 -1
  110. package/dist/stripe/server/Charge.d.ts +31 -1
  111. package/dist/stripe/server/Charge.d.ts.map +1 -1
  112. package/dist/stripe/server/Charge.js +88 -11
  113. package/dist/stripe/server/Charge.js.map +1 -1
  114. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  115. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  116. package/dist/stripe/server/internal/html.gen.js +1 -1
  117. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  118. package/dist/tempo/client/ChannelOps.d.ts +3 -3
  119. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  120. package/dist/tempo/client/ChannelOps.js +13 -6
  121. package/dist/tempo/client/ChannelOps.js.map +1 -1
  122. package/dist/tempo/client/Charge.d.ts.map +1 -1
  123. package/dist/tempo/client/Charge.js +8 -5
  124. package/dist/tempo/client/Charge.js.map +1 -1
  125. package/dist/tempo/client/Methods.d.ts +0 -1
  126. package/dist/tempo/client/Methods.d.ts.map +1 -1
  127. package/dist/tempo/client/Session.d.ts +2 -4
  128. package/dist/tempo/client/Session.d.ts.map +1 -1
  129. package/dist/tempo/client/Session.js +10 -12
  130. package/dist/tempo/client/Session.js.map +1 -1
  131. package/dist/tempo/client/SessionManager.d.ts +3 -3
  132. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  133. package/dist/tempo/client/SessionManager.js +1 -1
  134. package/dist/tempo/client/SessionManager.js.map +1 -1
  135. package/dist/tempo/internal/account.d.ts +5 -0
  136. package/dist/tempo/internal/account.d.ts.map +1 -1
  137. package/dist/tempo/internal/account.js +8 -0
  138. package/dist/tempo/internal/account.js.map +1 -1
  139. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  140. package/dist/tempo/internal/fee-payer.js +5 -2
  141. package/dist/tempo/internal/fee-payer.js.map +1 -1
  142. package/dist/tempo/server/Charge.d.ts +6 -0
  143. package/dist/tempo/server/Charge.d.ts.map +1 -1
  144. package/dist/tempo/server/Charge.js +30 -3
  145. package/dist/tempo/server/Charge.js.map +1 -1
  146. package/dist/tempo/server/Session.d.ts +6 -0
  147. package/dist/tempo/server/Session.d.ts.map +1 -1
  148. package/dist/tempo/server/Session.js +14 -13
  149. package/dist/tempo/server/Session.js.map +1 -1
  150. package/dist/tempo/server/Subscription.d.ts +6 -0
  151. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  152. package/dist/tempo/server/Subscription.js +1 -1
  153. package/dist/tempo/server/Subscription.js.map +1 -1
  154. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  155. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  156. package/dist/tempo/server/internal/html.gen.js +1 -1
  157. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  158. package/dist/tempo/session/Chain.d.ts +2 -0
  159. package/dist/tempo/session/Chain.d.ts.map +1 -1
  160. package/dist/tempo/session/Chain.js +8 -8
  161. package/dist/tempo/session/Chain.js.map +1 -1
  162. package/dist/tempo/session/Voucher.d.ts +4 -3
  163. package/dist/tempo/session/Voucher.d.ts.map +1 -1
  164. package/dist/tempo/session/Voucher.js +71 -44
  165. package/dist/tempo/session/Voucher.js.map +1 -1
  166. package/dist/tempo/session/Ws.d.ts.map +1 -1
  167. package/dist/tempo/session/Ws.js +15 -0
  168. package/dist/tempo/session/Ws.js.map +1 -1
  169. package/dist/tempo/subscription/KeyAuthorization.d.ts +2 -2
  170. package/dist/x402/Assets.d.ts +29 -0
  171. package/dist/x402/Assets.d.ts.map +1 -0
  172. package/dist/x402/Assets.js +46 -0
  173. package/dist/x402/Assets.js.map +1 -0
  174. package/dist/x402/Header.d.ts +14 -0
  175. package/dist/x402/Header.d.ts.map +1 -0
  176. package/dist/x402/Header.js +18 -0
  177. package/dist/x402/Header.js.map +1 -0
  178. package/dist/x402/Types.d.ts +289 -0
  179. package/dist/x402/Types.d.ts.map +1 -0
  180. package/dist/x402/Types.js +139 -0
  181. package/dist/x402/Types.js.map +1 -0
  182. package/dist/x402/client/Exact.d.ts +38 -0
  183. package/dist/x402/client/Exact.d.ts.map +1 -0
  184. package/dist/x402/client/Exact.js +141 -0
  185. package/dist/x402/client/Exact.js.map +1 -0
  186. package/dist/x402/index.d.ts +6 -0
  187. package/dist/x402/index.d.ts.map +1 -0
  188. package/dist/x402/index.js +6 -0
  189. package/dist/x402/index.js.map +1 -0
  190. package/dist/x402/internal/ChallengeBrand.d.ts +5 -0
  191. package/dist/x402/internal/ChallengeBrand.d.ts.map +1 -0
  192. package/dist/x402/internal/ChallengeBrand.js +13 -0
  193. package/dist/x402/internal/ChallengeBrand.js.map +1 -0
  194. package/dist/x402/internal/RouteBinding.d.ts +8 -0
  195. package/dist/x402/internal/RouteBinding.d.ts.map +1 -0
  196. package/dist/x402/internal/RouteBinding.js +12 -0
  197. package/dist/x402/internal/RouteBinding.js.map +1 -0
  198. package/dist/x402/server/EvmCharge.d.ts +50 -0
  199. package/dist/x402/server/EvmCharge.d.ts.map +1 -0
  200. package/dist/x402/server/EvmCharge.js +301 -0
  201. package/dist/x402/server/EvmCharge.js.map +1 -0
  202. package/dist/x402/server/Facilitator.d.ts +12 -0
  203. package/dist/x402/server/Facilitator.d.ts.map +1 -0
  204. package/dist/x402/server/Facilitator.js +42 -0
  205. package/dist/x402/server/Facilitator.js.map +1 -0
  206. package/package.json +41 -21
  207. package/src/Challenge.test.ts +28 -0
  208. package/src/Challenge.ts +17 -10
  209. package/src/Method.ts +1 -1
  210. package/src/Store.test-d.ts +58 -0
  211. package/src/Store.test.ts +77 -0
  212. package/src/Store.ts +155 -74
  213. package/src/client/Methods.ts +1 -0
  214. package/src/client/Mppx.ts +4 -3
  215. package/src/client/Transport.test.ts +165 -30
  216. package/src/client/Transport.ts +76 -8
  217. package/src/client/index.ts +1 -1
  218. package/src/client/internal/Fetch.test.ts +31 -2
  219. package/src/client/internal/Fetch.ts +26 -19
  220. package/src/evm/Assets.ts +1 -0
  221. package/src/evm/Chains.ts +5 -0
  222. package/src/evm/Methods.ts +44 -0
  223. package/src/evm/PublicInterface.test-d.ts +109 -0
  224. package/src/evm/Types.ts +140 -0
  225. package/src/evm/client/Charge.test.ts +99 -0
  226. package/src/evm/client/Charge.ts +198 -0
  227. package/src/evm/client/Methods.ts +19 -0
  228. package/src/evm/client/index.ts +5 -0
  229. package/src/evm/index.ts +13 -0
  230. package/src/evm/server/Charge.test.ts +199 -0
  231. package/src/evm/server/Charge.ts +283 -0
  232. package/src/evm/server/Methods.ts +22 -0
  233. package/src/evm/server/index.ts +5 -0
  234. package/src/index.ts +2 -0
  235. package/src/internal/HeaderCodec.ts +36 -0
  236. package/src/middlewares/elysia.test.ts +25 -0
  237. package/src/middlewares/elysia.ts +1 -2
  238. package/src/middlewares/express.test.ts +28 -0
  239. package/src/middlewares/express.ts +1 -1
  240. package/src/middlewares/hono.test.ts +138 -2
  241. package/src/middlewares/nextjs.test.ts +22 -0
  242. package/src/proxy/internal/Headers.test.ts +38 -0
  243. package/src/proxy/internal/Headers.ts +26 -2
  244. package/src/proxy/services/openai.test.ts +57 -1
  245. package/src/proxy/services/openai.ts +2 -0
  246. package/src/server/Methods.ts +1 -0
  247. package/src/server/Mppx.test.ts +244 -1
  248. package/src/server/Mppx.ts +124 -11
  249. package/src/server/NodeListener.test.ts +28 -1
  250. package/src/server/Transport.test.ts +19 -0
  251. package/src/server/Transport.ts +20 -0
  252. package/src/server/index.ts +1 -1
  253. package/src/stripe/server/Charge.test.ts +215 -1
  254. package/src/stripe/server/Charge.ts +150 -20
  255. package/src/stripe/server/internal/html.gen.ts +1 -1
  256. package/src/tempo/AccessKeyAuthorization.test.ts +231 -0
  257. package/src/tempo/client/ChannelOps.test.ts +61 -7
  258. package/src/tempo/client/ChannelOps.ts +18 -7
  259. package/src/tempo/client/Charge.test.ts +126 -0
  260. package/src/tempo/client/Charge.ts +10 -6
  261. package/src/tempo/client/Session.test.ts +130 -1
  262. package/src/tempo/client/Session.ts +12 -19
  263. package/src/tempo/client/SessionManager.test.ts +69 -2
  264. package/src/tempo/client/SessionManager.ts +4 -4
  265. package/src/tempo/internal/account.ts +13 -0
  266. package/src/tempo/internal/fee-payer.test.ts +32 -2
  267. package/src/tempo/internal/fee-payer.ts +6 -2
  268. package/src/tempo/server/Charge.test.ts +91 -1
  269. package/src/tempo/server/Charge.ts +48 -2
  270. package/src/tempo/server/Session.test.ts +30 -0
  271. package/src/tempo/server/Session.ts +24 -17
  272. package/src/tempo/server/Subscription.ts +7 -1
  273. package/src/tempo/server/internal/html.gen.ts +1 -1
  274. package/src/tempo/session/Chain.test.ts +4 -4
  275. package/src/tempo/session/Chain.ts +10 -6
  276. package/src/tempo/session/ChannelStore.test.ts +21 -0
  277. package/src/tempo/session/Voucher.test.ts +230 -1
  278. package/src/tempo/session/Voucher.ts +96 -48
  279. package/src/tempo/session/Ws.test.ts +71 -0
  280. package/src/tempo/session/Ws.ts +13 -0
  281. package/src/tempo/subscription/Store.test.ts +55 -0
  282. package/src/x402/Assets.ts +65 -0
  283. package/src/x402/Exact.e2e.test.ts +448 -0
  284. package/src/x402/Header.test.ts +73 -0
  285. package/src/x402/Header.ts +34 -0
  286. package/src/x402/PublicInterface.test-d.ts +39 -0
  287. package/src/x402/Types.ts +248 -0
  288. package/src/x402/client/Exact.test.ts +180 -0
  289. package/src/x402/client/Exact.ts +198 -0
  290. package/src/x402/index.ts +5 -0
  291. package/src/x402/internal/ChallengeBrand.ts +14 -0
  292. package/src/x402/internal/RouteBinding.ts +18 -0
  293. package/src/x402/server/EvmCharge.ts +394 -0
  294. package/src/x402/server/Facilitator.test.ts +111 -0
  295. package/src/x402/server/Facilitator.ts +56 -0
@@ -1,9 +1,16 @@
1
- import type { Hex } from 'viem'
1
+ import { createClient, type Hex, http } from 'viem'
2
+ import { privateKeyToAccount } from 'viem/accounts'
3
+ import { Account as TempoAccount } from 'viem/tempo'
2
4
  import { describe, expect, test, vi } from 'vp/test'
3
5
 
4
6
  import * as Challenge from '../../Challenge.js'
7
+ import * as PaymentCredential from '../../Credential.js'
5
8
  import { formatNeedVoucherEvent, parseEvent } from '../session/Sse.js'
6
- import type { NeedVoucherEvent, SessionReceipt } from '../session/Types.js'
9
+ import type {
10
+ NeedVoucherEvent,
11
+ SessionCredentialPayload,
12
+ SessionReceipt,
13
+ } from '../session/Types.js'
7
14
  import { sessionManager } from './SessionManager.js'
8
15
 
9
16
  const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
@@ -82,6 +89,66 @@ describe('Session', () => {
82
89
  expect(s.cumulative).toBe(0n)
83
90
  expect(s.opened).toBe(false)
84
91
  })
92
+
93
+ test('uses voucherSigner for managed open credentials', async () => {
94
+ vi.resetModules()
95
+ vi.doMock('viem/actions', () => ({
96
+ prepareTransactionRequest: vi.fn(async () => ({})),
97
+ sendCallsSync: vi.fn(),
98
+ signTransaction: vi.fn(async () => '0xdeadbeef'),
99
+ signTypedData: vi.fn(),
100
+ }))
101
+
102
+ try {
103
+ const { sessionManager: sessionManagerWithMocks } = await import('./SessionManager.js')
104
+ const account = privateKeyToAccount(
105
+ '0x0000000000000000000000000000000000000000000000000000000000000001',
106
+ )
107
+ const voucherSigner = TempoAccount.fromSecp256k1(
108
+ '0x0000000000000000000000000000000000000000000000000000000000000002',
109
+ { access: account },
110
+ )
111
+ const client = createClient({
112
+ account,
113
+ transport: http('http://127.0.0.1'),
114
+ })
115
+ const challenge = makeChallenge({
116
+ recipient: '0x742d35cc6634c0532925a3b844bc9e7595f8fe00',
117
+ methodDetails: {
118
+ escrowContract: '0x9d136eea063ede5418a6bc7beaff009bbb6cfa70',
119
+ chainId: 4217,
120
+ },
121
+ })
122
+ const mockFetch = vi
123
+ .fn()
124
+ .mockResolvedValueOnce(make402Response(challenge))
125
+ .mockResolvedValueOnce(makeOkResponse())
126
+
127
+ const manager = sessionManagerWithMocks({
128
+ account,
129
+ client,
130
+ fetch: mockFetch as typeof globalThis.fetch,
131
+ maxDeposit: '10',
132
+ voucherSigner,
133
+ })
134
+
135
+ const response = await manager.fetch('https://api.example.com/data')
136
+ const authorization = new Headers((mockFetch.mock.calls[1]![1] as RequestInit).headers).get(
137
+ 'Authorization',
138
+ )
139
+
140
+ expect(response.status).toBe(200)
141
+ expect(authorization).toBeDefined()
142
+ if (!authorization) throw new Error('missing authorization header')
143
+ const credential = PaymentCredential.deserialize<SessionCredentialPayload>(authorization)
144
+ expect(credential.payload.action).toBe('open')
145
+ if (credential.payload.action !== 'open') throw new Error('unexpected action')
146
+ expect(credential.payload.authorizedSigner).toBe(voucherSigner.accessKeyAddress)
147
+ } finally {
148
+ vi.doUnmock('viem/actions')
149
+ vi.resetModules()
150
+ }
151
+ })
85
152
  })
86
153
 
87
154
  describe('.fetch()', () => {
@@ -1,5 +1,5 @@
1
1
  import type { Hex } from 'ox'
2
- import { parseUnits, type Address } from 'viem'
2
+ import { parseUnits, type Account as viem_Account, type Address } from 'viem'
3
3
 
4
4
  import * as Challenge from '../../Challenge.js'
5
5
  import * as Fetch from '../../client/internal/Fetch.js'
@@ -124,7 +124,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
124
124
 
125
125
  const method = sessionPlugin({
126
126
  account: parameters.account,
127
- authorizedSigner: parameters.authorizedSigner,
127
+ voucherSigner: parameters.voucherSigner,
128
128
  getClient: parameters.client ? () => parameters.client! : parameters.getClient,
129
129
  escrowContract: parameters.escrowContract,
130
130
  decimals: parameters.decimals,
@@ -853,8 +853,8 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
853
853
  export declare namespace sessionManager {
854
854
  type Parameters = Account.getResolver.Parameters &
855
855
  Client.getResolver.Parameters & {
856
- /** Address authorized to sign vouchers. Defaults to the account address. */
857
- authorizedSigner?: Address | undefined
856
+ /** Account that signs voucher digests. Defaults to `account`; access-key accounts sign raw vouchers as their access-key address. */
857
+ voucherSigner?: viem_Account | undefined
858
858
  /** Viem client instance. Shorthand for `getClient: () => client`. */
859
859
  client?: import('viem').Client | undefined
860
860
  /** Token decimals used to convert `maxDeposit` to raw units. Defaults to `6`. */
@@ -1,4 +1,17 @@
1
1
  import type { Account, Address } from 'viem'
2
+ import type { Account as TempoAccount } from 'viem/tempo'
3
+
4
+ /** Returns whether an account is a Tempo access-key account. */
5
+ export function isAccessKeyAccount(
6
+ account: Account,
7
+ ): account is Account & TempoAccount.AccessKeyAccount {
8
+ return 'accessKeyAddress' in account
9
+ }
10
+
11
+ /** Returns the address that should authorize direct account signatures. */
12
+ export function getAccountSignerAddress(account: Account): Address {
13
+ return isAccessKeyAccount(account) ? account.accessKeyAddress : account.address
14
+ }
2
15
 
3
16
  /**
4
17
  * Resolves a recipient address and optional fee payer from flexible input parameters.
@@ -1,4 +1,4 @@
1
- import { encodeFunctionData } from 'viem'
1
+ import { encodeFunctionData, maxUint256 } from 'viem'
2
2
  import { Abis, Addresses } from 'viem/tempo'
3
3
  import { describe, expect, test } from 'vp/test'
4
4
 
@@ -443,7 +443,7 @@ describe('prepareSponsoredTransaction', () => {
443
443
  maxFeePerGas: 1_000_000_000n,
444
444
  maxPriorityFeePerGas: 1_000_000_000n,
445
445
  nonce: 1n,
446
- nonceKey: 1n,
446
+ nonceKey: 'expiring',
447
447
  signature: { r: 1n, s: 1n, yParity: 0 } as any,
448
448
  validBefore: Math.floor(Date.now() / 1_000) + 300,
449
449
  } as const
@@ -460,6 +460,36 @@ describe('prepareSponsoredTransaction', () => {
460
460
  ).not.toThrow()
461
461
  })
462
462
 
463
+ test('accepts serialized expiring nonce key', () => {
464
+ expect(() =>
465
+ prepareSponsoredTransaction({
466
+ account: sponsor,
467
+ chainId: 42431,
468
+ details,
469
+ expectedFeeToken: bogus,
470
+ transaction: {
471
+ ...baseTransaction,
472
+ nonceKey: maxUint256,
473
+ } as any,
474
+ }),
475
+ ).not.toThrow()
476
+ })
477
+
478
+ test('error: rejects non-expiring nonce keys', () => {
479
+ expect(() =>
480
+ prepareSponsoredTransaction({
481
+ account: sponsor,
482
+ chainId: 42431,
483
+ details,
484
+ expectedFeeToken: bogus,
485
+ transaction: {
486
+ ...baseTransaction,
487
+ nonceKey: 1n,
488
+ } as any,
489
+ }),
490
+ ).toThrow('must use an expiring nonce')
491
+ })
492
+
463
493
  test('accepts higher Moderato priority fees by default', () => {
464
494
  expect(() =>
465
495
  prepareSponsoredTransaction({
@@ -2,7 +2,7 @@ import type { TempoAddress } from 'ox/tempo'
2
2
  import { TxEnvelopeTempo } from 'ox/tempo'
3
3
  import type { Hex } from 'viem'
4
4
  import type { Account } from 'viem'
5
- import { decodeFunctionData } from 'viem'
5
+ import { decodeFunctionData, maxUint256 } from 'viem'
6
6
  import { Abis, Addresses, Transaction } from 'viem/tempo'
7
7
 
8
8
  import * as TempoAddress_internal from './address.js'
@@ -126,6 +126,10 @@ function getPolicy(chainId: number, overrides: Partial<Policy> | undefined): Pol
126
126
  }
127
127
  }
128
128
 
129
+ function isExpiringNonceKey(nonceKey: SponsoredTransaction['nonceKey']): boolean {
130
+ return nonceKey === 'expiring' || nonceKey === maxUint256
131
+ }
132
+
129
133
  /** Validates that a set of transaction calls matches an allowed fee-payer pattern. */
130
134
  export function validateCalls(
131
135
  calls: readonly { data?: `0x${string}` | undefined; to?: TempoAddress.Address | undefined }[],
@@ -358,7 +362,7 @@ export function prepareSponsoredTransaction(parameters: {
358
362
  maxPriorityFeePerGas: maxPriorityFeePerGas.toString(),
359
363
  })
360
364
 
361
- if (nonceKey === undefined) fail('fee-sponsored transaction must use an expiring nonce')
365
+ if (!isExpiringNonceKey(nonceKey)) fail('fee-sponsored transaction must use an expiring nonce')
362
366
  if (validBefore === undefined)
363
367
  fail('fee-sponsored transaction must declare validBefore for the expiring nonce')
364
368
  const validBeforeValue = validBefore
@@ -286,6 +286,7 @@ describe('tempo', () => {
286
286
  })
287
287
 
288
288
  test('behavior: rejects replayed transaction hash', async () => {
289
+ const dedupStore = Store.memory()
289
290
  const dedupServer = Mppx_server.create({
290
291
  methods: [
291
292
  tempo_server.charge({
@@ -294,7 +295,8 @@ describe('tempo', () => {
294
295
  },
295
296
  currency: asset,
296
297
  account: accounts[0],
297
- store: Store.memory(),
298
+ store: dedupStore,
299
+ storeKeyPrefix: 'tenant:',
298
300
  }),
299
301
  ],
300
302
  realm,
@@ -336,6 +338,8 @@ describe('tempo', () => {
336
338
  })
337
339
  expect(response.status).toBe(200)
338
340
  }
341
+ expect(await dedupStore.get(`tenant:mppx:charge:${receipt.transactionHash}`)).not.toBeNull()
342
+ expect(await dedupStore.get(`mppx:charge:${receipt.transactionHash}`)).toBeNull()
339
343
 
340
344
  const response2 = await fetch(httpServer.url)
341
345
  expect(response2.status).toBe(402)
@@ -1616,6 +1620,7 @@ describe('tempo', () => {
1616
1620
  currency: asset,
1617
1621
  account: accounts[0],
1618
1622
  store: sponsoredStore,
1623
+ storeKeyPrefix: 'tenant:',
1619
1624
  }),
1620
1625
  ],
1621
1626
  realm,
@@ -1661,6 +1666,16 @@ describe('tempo', () => {
1661
1666
 
1662
1667
  const first = fetch(httpServer.url, { headers: { Authorization: credential1 } })
1663
1668
  await simulationStarted
1669
+ expect(
1670
+ await sponsoredStore.get(
1671
+ `tenant:mppx:charge:sponsor:${chain.id}:${accounts[1].address.toLowerCase()}`,
1672
+ ),
1673
+ ).not.toBeNull()
1674
+ expect(
1675
+ await sponsoredStore.get(
1676
+ `mppx:charge:sponsor:${chain.id}:${accounts[1].address.toLowerCase()}`,
1677
+ ),
1678
+ ).toBeNull()
1664
1679
  const second = fetch(httpServer.url, { headers: { Authorization: credential2 } })
1665
1680
 
1666
1681
  try {
@@ -2870,6 +2885,7 @@ describe('tempo', () => {
2870
2885
 
2871
2886
  test('behavior: store keys proof replay protection by challenge ID', async () => {
2872
2887
  const replayStore = Store.memory()
2888
+ const storeKeyPrefix = 'tenant:'
2873
2889
  const server_ = Mppx_server.create({
2874
2890
  methods: [
2875
2891
  tempo_server.charge({
@@ -2879,6 +2895,7 @@ describe('tempo', () => {
2879
2895
  currency: asset,
2880
2896
  account: accounts[0],
2881
2897
  store: replayStore,
2898
+ storeKeyPrefix,
2882
2899
  }),
2883
2900
  ],
2884
2901
  realm,
@@ -2918,6 +2935,10 @@ describe('tempo', () => {
2918
2935
  headers: { Authorization: Credential.serialize(credential1) },
2919
2936
  })
2920
2937
  expect(response2.status).toBe(200)
2938
+ expect(
2939
+ await replayStore.get(`${storeKeyPrefix}mppx:charge:proof:${challenge1.id}`),
2940
+ ).not.toBeNull()
2941
+ expect(await replayStore.get(`mppx:charge:proof:${challenge1.id}`)).toBeNull()
2921
2942
 
2922
2943
  const response3 = await fetch(httpServer.url)
2923
2944
  expect(response3.status).toBe(402)
@@ -3633,6 +3654,75 @@ describe('tempo', () => {
3633
3654
  })
3634
3655
  expect(challenge.request.methodDetails?.supportedModes).toEqual(['pull'])
3635
3656
  })
3657
+
3658
+ test('challenge contains supportedModes from method defaults', async () => {
3659
+ const handler = Mppx_server.create({
3660
+ methods: [
3661
+ tempo_server.charge({
3662
+ getClient: () => client,
3663
+ account: accounts[0].address,
3664
+ currency: asset,
3665
+ supportedModes: ['pull'],
3666
+ }),
3667
+ ],
3668
+ realm,
3669
+ secretKey,
3670
+ })
3671
+
3672
+ const result = await handler.charge({ amount: '1' })(new Request('https://example.com'))
3673
+ expect(result.status).toBe(402)
3674
+ if (result.status !== 402) throw new Error()
3675
+
3676
+ const challenge = Challenge.fromResponse(result.challenge, {
3677
+ methods: [tempo_client.charge()],
3678
+ })
3679
+ expect(challenge.request.methodDetails?.supportedModes).toEqual(['pull'])
3680
+ })
3681
+
3682
+ test('stable binding contains supportedModes', () => {
3683
+ const method = tempo_server.charge({
3684
+ getClient: () => client,
3685
+ account: accounts[0].address,
3686
+ currency: asset,
3687
+ })
3688
+ const request = method.schema.request.parse({
3689
+ amount: '1',
3690
+ currency: asset,
3691
+ decimals: 6,
3692
+ supportedModes: ['pull'],
3693
+ })
3694
+
3695
+ expect(method.stableBinding?.(request)).toMatchObject({
3696
+ supportedModes: ['pull'],
3697
+ })
3698
+ })
3699
+
3700
+ test('challenge contains splits from method defaults', async () => {
3701
+ const split = { amount: '0.2', recipient: accounts[2].address }
3702
+ const handler = Mppx_server.create({
3703
+ methods: [
3704
+ tempo_server.charge({
3705
+ getClient: () => client,
3706
+ account: accounts[0].address,
3707
+ currency: asset,
3708
+ splits: [split],
3709
+ }),
3710
+ ],
3711
+ realm,
3712
+ secretKey,
3713
+ })
3714
+
3715
+ const result = await handler.charge({ amount: '1' })(new Request('https://example.com'))
3716
+ expect(result.status).toBe(402)
3717
+ if (result.status !== 402) throw new Error()
3718
+
3719
+ const challenge = Challenge.fromResponse(result.challenge, {
3720
+ methods: [tempo_client.charge()],
3721
+ })
3722
+ expect(challenge.request.methodDetails?.splits).toEqual([
3723
+ { amount: '200000', recipient: split.recipient },
3724
+ ])
3725
+ })
3636
3726
  })
3637
3727
 
3638
3728
  describe('attribution memo', () => {
@@ -63,11 +63,21 @@ export function charge<const parameters extends charge.Parameters>(
63
63
  feePayerPolicy,
64
64
  html,
65
65
  memo,
66
+ splits,
67
+ supportedModes,
66
68
  validateSender,
67
69
  waitForConfirmation = true,
68
70
  } = parameters
69
- const store = (parameters.store ?? Store.memory()) as Store.AtomicStore<charge.StoreItemMap>
70
- const proofStore = parameters.store as Store.AtomicStore<charge.StoreItemMap> | undefined
71
+ const storeKeyPrefix = parameters.storeKeyPrefix ?? ''
72
+ const store = Store.from(
73
+ (parameters.store ?? Store.memory()) as Store.AtomicStore<charge.StoreItemMap>,
74
+ { keyPrefix: storeKeyPrefix },
75
+ )
76
+ const proofStore = parameters.store
77
+ ? Store.from(parameters.store as Store.AtomicStore<charge.StoreItemMap>, {
78
+ keyPrefix: storeKeyPrefix,
79
+ })
80
+ : undefined
71
81
 
72
82
  const { recipient, feePayer, feePayerUrl } = Account.resolve(parameters)
73
83
 
@@ -88,8 +98,12 @@ export function charge<const parameters extends charge.Parameters>(
88
98
  externalId,
89
99
  memo,
90
100
  recipient,
101
+ splits,
102
+ supportedModes,
91
103
  } as unknown as Defaults,
92
104
 
105
+ stableBinding: chargeBinding,
106
+
93
107
  html: html
94
108
  ? {
95
109
  config: {},
@@ -539,6 +553,12 @@ export declare namespace charge {
539
553
  * memo binding, transaction success, and replay protection.
540
554
  */
541
555
  validateSender?: ValidateSender | undefined
556
+ /**
557
+ * Prefix prepended to charge replay-protection store keys.
558
+ *
559
+ * By default, no prefix is applied.
560
+ */
561
+ storeKeyPrefix?: string | undefined
542
562
  /**
543
563
  * Whether to wait for the charge transaction to confirm on-chain before
544
564
  * responding. @default true
@@ -565,6 +585,32 @@ export declare namespace charge {
565
585
  type FeePayerPolicy = Partial<FeePayer.Policy>
566
586
  }
567
587
 
588
+ type ChargeRequest = z.output<typeof Methods.charge.schema.request>
589
+
590
+ function chargeBinding(request: ChargeRequest) {
591
+ // Exhaustively destructure so new charge request fields require an explicit binding decision.
592
+ const { amount, currency, description, externalId, methodDetails, recipient, ...requestRest } =
593
+ request
594
+ requestRest satisfies Record<string, never>
595
+
596
+ const { chainId, feePayer, memo, splits, supportedModes, ...methodDetailsRest } =
597
+ methodDetails ?? {}
598
+ methodDetailsRest satisfies Record<string, never>
599
+ void feePayer
600
+
601
+ return {
602
+ amount,
603
+ chainId,
604
+ currency,
605
+ description,
606
+ externalId,
607
+ memo,
608
+ recipient,
609
+ splits,
610
+ supportedModes,
611
+ }
612
+ }
613
+
568
614
  type ExpectedTransfer = {
569
615
  amount: string
570
616
  allowAnyMemo?: boolean | undefined
@@ -668,6 +668,36 @@ describe.runIf(isLocalnet)('session', () => {
668
668
  expect(ch!.highestVoucherAmount).toBe(2000000n)
669
669
  })
670
670
 
671
+ test('rejects voucher signed for route escrow instead of stored channel escrow', async () => {
672
+ const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
673
+ const server = createServer()
674
+ await openServerChannel(server, channelId, serializedTransaction)
675
+
676
+ const routeEscrow = '0x0000000000000000000000000000000000000999' as Address
677
+ const signature = await signVoucher(
678
+ client,
679
+ payer,
680
+ { channelId, cumulativeAmount: 2000000n },
681
+ routeEscrow,
682
+ chain.id,
683
+ )
684
+
685
+ await expect(
686
+ server.verify({
687
+ credential: {
688
+ challenge: makeChallenge({ id: 'route-escrow-replay', channelId }),
689
+ payload: {
690
+ action: 'voucher' as const,
691
+ channelId,
692
+ cumulativeAmount: '2000000',
693
+ signature,
694
+ },
695
+ },
696
+ request: makeRequest({ escrowContract: routeEscrow }),
697
+ }),
698
+ ).rejects.toThrow(InvalidSignatureError)
699
+ })
700
+
671
701
  test('rejects non-increasing voucher replay', async () => {
672
702
  const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
673
703
  const server = createServer()
@@ -104,7 +104,9 @@ export function session<const parameters extends session.Parameters>(
104
104
 
105
105
  const lastOnChainVerified = new Map<Hex, number>()
106
106
 
107
- const store = ChannelStore.fromStore(rawStore)
107
+ const store = ChannelStore.fromStore(
108
+ Store.from(rawStore, { keyPrefix: parameters.storeKeyPrefix }),
109
+ )
108
110
 
109
111
  const { account, recipient, feePayer, feePayerUrl } = Account.resolve(parameters)
110
112
 
@@ -139,7 +141,7 @@ export function session<const parameters extends session.Parameters>(
139
141
  unitType,
140
142
  } as unknown as Defaults,
141
143
 
142
- transport: transport as never,
144
+ transport,
143
145
 
144
146
  // TODO: dedupe `{charge,session}.request`
145
147
  async request({ credential, request }) {
@@ -224,6 +226,7 @@ export function session<const parameters extends session.Parameters>(
224
226
  payload,
225
227
  methodDetails,
226
228
  resolvedFeePayer,
229
+ methodDetails.feePayer === true,
227
230
  feePayerPolicy,
228
231
  waitForConfirmation,
229
232
  )
@@ -238,6 +241,7 @@ export function session<const parameters extends session.Parameters>(
238
241
  payload,
239
242
  methodDetails,
240
243
  resolvedFeePayer,
244
+ methodDetails.feePayer === true,
241
245
  feePayerPolicy,
242
246
  )
243
247
  lastOnChainVerified.set(sessionReceipt.channelId, Date.now())
@@ -357,6 +361,12 @@ export declare namespace session {
357
361
  * local single-process usage.
358
362
  */
359
363
  store?: Store.AtomicStore | undefined
364
+ /**
365
+ * Prefix prepended to channel state store keys.
366
+ *
367
+ * By default, no prefix is applied.
368
+ */
369
+ storeKeyPrefix?: string | undefined
360
370
  /**
361
371
  * Enable SSE streaming.
362
372
  *
@@ -522,10 +532,8 @@ async function verifyAndAcceptVoucher(parameters: {
522
532
  channelId: Hex
523
533
  voucher: SignedVoucher
524
534
  onChain: OnChainChannel
525
- methodDetails: SessionMethodDetails
526
535
  }): Promise<SessionReceipt> {
527
- const { store, minVoucherDelta, challenge, channel, channelId, voucher, onChain, methodDetails } =
528
- parameters
536
+ const { store, minVoucherDelta, challenge, channel, channelId, voucher, onChain } = parameters
529
537
 
530
538
  if (onChain.finalized) {
531
539
  throw new ChannelClosedError({ reason: 'channel is finalized on-chain' })
@@ -560,8 +568,8 @@ async function verifyAndAcceptVoucher(parameters: {
560
568
  }
561
569
 
562
570
  const isValid = await verifyVoucher(
563
- methodDetails.escrowContract,
564
- methodDetails.chainId,
571
+ channel.escrowContract,
572
+ channel.chainId,
565
573
  voucher,
566
574
  channel.authorizedSigner,
567
575
  )
@@ -627,6 +635,7 @@ async function handleOpen(
627
635
  payload: SessionCredentialPayload & { action: 'open' },
628
636
  methodDetails: SessionMethodDetails,
629
637
  feePayer: viem_Account | undefined,
638
+ isSponsored: boolean,
630
639
  feePayerPolicy: session.FeePayerPolicy | undefined,
631
640
  waitForConfirmation: boolean,
632
641
  ): Promise<SessionReceipt> {
@@ -679,6 +688,7 @@ async function handleOpen(
679
688
  challengeExpires: challenge.expires,
680
689
  feePayerPolicy,
681
690
  feePayer,
691
+ isSponsored,
682
692
  beforeBroadcast: async (pendingOnChain) => {
683
693
  await validateOpenVoucher(pendingOnChain)
684
694
  },
@@ -764,6 +774,7 @@ async function handleTopUp(
764
774
  payload: SessionCredentialPayload & { action: 'topUp' },
765
775
  methodDetails: SessionMethodDetails,
766
776
  feePayer: viem_Account | undefined,
777
+ isSponsored: boolean,
767
778
  feePayerPolicy: session.FeePayerPolicy | undefined,
768
779
  ): Promise<SessionReceipt> {
769
780
  const channelId = ChannelStore.normalizeChannelId(payload.channelId)
@@ -785,6 +796,7 @@ async function handleTopUp(
785
796
  challengeExpires: challenge.expires,
786
797
  feePayerPolicy,
787
798
  feePayer,
799
+ isSponsored,
788
800
  })
789
801
 
790
802
  const updated = await store.updateChannel(channelId, (current) => {
@@ -840,11 +852,7 @@ async function handleVoucher(
840
852
 
841
853
  const onChain = await (async () => {
842
854
  if (isStale) {
843
- const onChainChannel = await getOnChainChannel(
844
- client,
845
- methodDetails.escrowContract,
846
- channelId,
847
- )
855
+ const onChainChannel = await getOnChainChannel(client, channel.escrowContract, channelId)
848
856
  lastOnChainVerified.set(channelId, Date.now())
849
857
  // Persist closeRequestedAt so the cached path detects force-close
850
858
  // between re-queries.
@@ -875,7 +883,6 @@ async function handleVoucher(
875
883
  channelId,
876
884
  voucher,
877
885
  onChain,
878
- methodDetails,
879
886
  })
880
887
  }
881
888
 
@@ -902,7 +909,7 @@ async function handleClose(
902
909
 
903
910
  const voucher = parseVoucherFromPayload(channelId, payload.cumulativeAmount, payload.signature)
904
911
 
905
- const onChain = await getOnChainChannel(client, methodDetails.escrowContract, channelId)
912
+ const onChain = await getOnChainChannel(client, channel.escrowContract, channelId)
906
913
 
907
914
  if (onChain.finalized) {
908
915
  throw new ChannelClosedError({ reason: 'channel is finalized on-chain' })
@@ -928,8 +935,8 @@ async function handleClose(
928
935
  }
929
936
 
930
937
  const isValid = await verifyVoucher(
931
- methodDetails.escrowContract,
932
- methodDetails.chainId,
938
+ channel.escrowContract,
939
+ channel.chainId,
933
940
  voucher,
934
941
  channel.authorizedSigner,
935
942
  )
@@ -964,7 +971,7 @@ async function handleClose(
964
971
  sender: account?.address ?? client.account?.address,
965
972
  })
966
973
 
967
- txHash = await closeOnChain(client, methodDetails.escrowContract, voucher, {
974
+ txHash = await closeOnChain(client, channel.escrowContract, voucher, {
968
975
  ...(feePayer && account ? { feePayer, account } : { account }),
969
976
  candidateFeeTokens: [channel.token],
970
977
  })
@@ -72,7 +72,7 @@ export function subscription<const parameters extends subscription.Parameters>(
72
72
 
73
73
  const { recipient } = Account.resolve(parameters)
74
74
  const context = createContext(parameters, {
75
- store: rawStore,
75
+ store: Store.from(rawStore, { keyPrefix: parameters.storeKeyPrefix }),
76
76
  options: {
77
77
  activationTimeoutMs: parameters.activationTimeoutMs,
78
78
  renewalTimeoutMs: parameters.renewalTimeoutMs,
@@ -1110,6 +1110,12 @@ export declare namespace subscription {
1110
1110
  subscription: SubscriptionRecord
1111
1111
  }) => Promise<RenewalResult>
1112
1112
  store?: Store.AtomicStore<Record<string, unknown>> | undefined
1113
+ /**
1114
+ * Prefix prepended to all subscription store keys.
1115
+ *
1116
+ * By default, no prefix is applied.
1117
+ */
1118
+ storeKeyPrefix?: string | undefined
1113
1119
  testnet?: boolean | undefined
1114
1120
  waitForConfirmation?: boolean | undefined
1115
1121
  } & Defaults