mppx 0.7.0 → 0.8.0

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 (278) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +20 -11
  3. package/dist/Challenge.d.ts.map +1 -1
  4. package/dist/Challenge.js +18 -6
  5. package/dist/Challenge.js.map +1 -1
  6. package/dist/Mcp.d.ts +3 -0
  7. package/dist/Mcp.d.ts.map +1 -1
  8. package/dist/Mcp.js +2 -0
  9. package/dist/Mcp.js.map +1 -1
  10. package/dist/PaymentRequest.d.ts +10 -10
  11. package/dist/PaymentRequest.js +8 -8
  12. package/dist/client/Mppx.js +2 -2
  13. package/dist/client/Mppx.js.map +1 -1
  14. package/dist/client/Transport.d.ts +11 -16
  15. package/dist/client/Transport.d.ts.map +1 -1
  16. package/dist/client/Transport.js +55 -75
  17. package/dist/client/Transport.js.map +1 -1
  18. package/dist/client/index.d.ts +3 -0
  19. package/dist/client/index.d.ts.map +1 -1
  20. package/dist/client/index.js +1 -0
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/client/internal/Fetch.d.ts.map +1 -1
  23. package/dist/client/internal/Fetch.js +46 -7
  24. package/dist/client/internal/Fetch.js.map +1 -1
  25. package/dist/client/internal/protocols/Mcp.d.ts +7 -0
  26. package/dist/client/internal/protocols/Mcp.d.ts.map +1 -0
  27. package/dist/client/internal/protocols/Mcp.js +159 -0
  28. package/dist/client/internal/protocols/Mcp.js.map +1 -0
  29. package/dist/client/internal/protocols/Mpp.d.ts +4 -0
  30. package/dist/client/internal/protocols/Mpp.d.ts.map +1 -0
  31. package/dist/client/internal/protocols/Mpp.js +18 -0
  32. package/dist/client/internal/protocols/Mpp.js.map +1 -0
  33. package/dist/client/internal/protocols/Protocol.d.ts +10 -0
  34. package/dist/client/internal/protocols/Protocol.d.ts.map +1 -0
  35. package/dist/client/internal/protocols/Protocol.js +2 -0
  36. package/dist/client/internal/protocols/Protocol.js.map +1 -0
  37. package/dist/client/internal/protocols/Shared.d.ts +5 -0
  38. package/dist/client/internal/protocols/Shared.d.ts.map +1 -0
  39. package/dist/client/internal/protocols/Shared.js +20 -0
  40. package/dist/client/internal/protocols/Shared.js.map +1 -0
  41. package/dist/client/internal/protocols/X402.d.ts +8 -0
  42. package/dist/client/internal/protocols/X402.d.ts.map +1 -0
  43. package/dist/client/internal/protocols/X402.js +39 -0
  44. package/dist/client/internal/protocols/X402.js.map +1 -0
  45. package/dist/evm/client/index.d.ts +1 -0
  46. package/dist/evm/client/index.d.ts.map +1 -1
  47. package/dist/evm/client/index.js +1 -0
  48. package/dist/evm/client/index.js.map +1 -1
  49. package/dist/evm/index.d.ts +2 -0
  50. package/dist/evm/index.d.ts.map +1 -1
  51. package/dist/evm/index.js +2 -0
  52. package/dist/evm/index.js.map +1 -1
  53. package/dist/evm/server/index.d.ts +1 -0
  54. package/dist/evm/server/index.d.ts.map +1 -1
  55. package/dist/evm/server/index.js +1 -0
  56. package/dist/evm/server/index.js.map +1 -1
  57. package/dist/mcp/client/McpClient.d.ts +101 -0
  58. package/dist/mcp/client/McpClient.d.ts.map +1 -0
  59. package/dist/mcp/client/McpClient.js +162 -0
  60. package/dist/mcp/client/McpClient.js.map +1 -0
  61. package/dist/mcp/client/index.d.ts.map +1 -0
  62. package/dist/mcp/client/index.js.map +1 -0
  63. package/dist/mcp/server/Transport.d.ts.map +1 -0
  64. package/dist/mcp/server/Transport.js.map +1 -0
  65. package/dist/mcp/server/index.d.ts.map +1 -0
  66. package/dist/mcp/server/index.js.map +1 -0
  67. package/dist/server/Mppx.d.ts +1 -1
  68. package/dist/server/Mppx.d.ts.map +1 -1
  69. package/dist/server/Mppx.js +9 -0
  70. package/dist/server/Mppx.js.map +1 -1
  71. package/dist/server/Transport.d.ts +1 -1
  72. package/dist/server/Transport.d.ts.map +1 -1
  73. package/dist/server/Transport.js +1 -1
  74. package/dist/server/Transport.js.map +1 -1
  75. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  76. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  77. package/dist/stripe/server/internal/html.gen.js +1 -1
  78. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  79. package/dist/tempo/Proof.d.ts +85 -1
  80. package/dist/tempo/Proof.d.ts.map +1 -1
  81. package/dist/tempo/Proof.js +35 -0
  82. package/dist/tempo/Proof.js.map +1 -1
  83. package/dist/tempo/client/Charge.d.ts +13 -1
  84. package/dist/tempo/client/Charge.d.ts.map +1 -1
  85. package/dist/tempo/client/Charge.js +38 -25
  86. package/dist/tempo/client/Charge.js.map +1 -1
  87. package/dist/tempo/client/Methods.d.ts +5 -3
  88. package/dist/tempo/client/Methods.d.ts.map +1 -1
  89. package/dist/tempo/client/Methods.js +4 -2
  90. package/dist/tempo/client/Methods.js.map +1 -1
  91. package/dist/tempo/client/ResolveAccount.d.ts +40 -0
  92. package/dist/tempo/client/ResolveAccount.d.ts.map +1 -0
  93. package/dist/tempo/client/ResolveAccount.js +2 -0
  94. package/dist/tempo/client/ResolveAccount.js.map +1 -0
  95. package/dist/tempo/internal/fee-payer.d.ts +9 -1
  96. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  97. package/dist/tempo/internal/fee-payer.js +35 -6
  98. package/dist/tempo/internal/fee-payer.js.map +1 -1
  99. package/dist/tempo/internal/proof.d.ts +71 -5
  100. package/dist/tempo/internal/proof.d.ts.map +1 -1
  101. package/dist/tempo/internal/proof.js +42 -6
  102. package/dist/tempo/internal/proof.js.map +1 -1
  103. package/dist/tempo/legacy/client/SessionManager.d.ts.map +1 -1
  104. package/dist/tempo/legacy/client/SessionManager.js +10 -3
  105. package/dist/tempo/legacy/client/SessionManager.js.map +1 -1
  106. package/dist/tempo/server/Charge.d.ts.map +1 -1
  107. package/dist/tempo/server/Charge.js +42 -18
  108. package/dist/tempo/server/Charge.js.map +1 -1
  109. package/dist/tempo/server/Methods.d.ts +4 -2
  110. package/dist/tempo/server/Methods.d.ts.map +1 -1
  111. package/dist/tempo/server/Methods.js +4 -2
  112. package/dist/tempo/server/Methods.js.map +1 -1
  113. package/dist/tempo/server/Subscription.d.ts +10 -0
  114. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  115. package/dist/tempo/server/Subscription.js +135 -23
  116. package/dist/tempo/server/Subscription.js.map +1 -1
  117. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  118. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  119. package/dist/tempo/server/internal/html.gen.js +1 -1
  120. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  121. package/dist/tempo/session/client/ChannelOps.d.ts +2 -3
  122. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  123. package/dist/tempo/session/client/ChannelOps.js +7 -10
  124. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  125. package/dist/tempo/session/client/ChannelStore.d.ts +51 -0
  126. package/dist/tempo/session/client/ChannelStore.d.ts.map +1 -0
  127. package/dist/tempo/session/client/ChannelStore.js +63 -0
  128. package/dist/tempo/session/client/ChannelStore.js.map +1 -0
  129. package/dist/tempo/session/client/CredentialState.d.ts +7 -24
  130. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  131. package/dist/tempo/session/client/CredentialState.js +51 -49
  132. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  133. package/dist/tempo/session/client/Session.d.ts +8 -2
  134. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  135. package/dist/tempo/session/client/Session.js +22 -8
  136. package/dist/tempo/session/client/Session.js.map +1 -1
  137. package/dist/tempo/session/client/SessionManager.d.ts +4 -40
  138. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  139. package/dist/tempo/session/client/SessionManager.js +124 -174
  140. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  141. package/dist/tempo/session/client/index.d.ts +3 -4
  142. package/dist/tempo/session/client/index.d.ts.map +1 -1
  143. package/dist/tempo/session/client/index.js +1 -0
  144. package/dist/tempo/session/client/index.js.map +1 -1
  145. package/dist/tempo/session/precompile/Voucher.d.ts +3 -3
  146. package/dist/tempo/session/precompile/Voucher.d.ts.map +1 -1
  147. package/dist/tempo/session/precompile/Voucher.js +24 -25
  148. package/dist/tempo/session/precompile/Voucher.js.map +1 -1
  149. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  150. package/dist/tempo/session/server/Settlement.js +4 -2
  151. package/dist/tempo/session/server/Settlement.js.map +1 -1
  152. package/dist/tempo/session/server/Sse.d.ts.map +1 -1
  153. package/dist/tempo/session/server/Sse.js.map +1 -1
  154. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  155. package/dist/tempo/session/server/Ws.js.map +1 -1
  156. package/dist/tempo/subscription/KeyAuthorization.d.ts +712 -1
  157. package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
  158. package/dist/tempo/subscription/Store.d.ts +2 -0
  159. package/dist/tempo/subscription/Store.d.ts.map +1 -1
  160. package/dist/tempo/subscription/Store.js +16 -1
  161. package/dist/tempo/subscription/Store.js.map +1 -1
  162. package/dist/x402/index.d.ts +1 -0
  163. package/dist/x402/index.d.ts.map +1 -1
  164. package/dist/x402/index.js +1 -0
  165. package/dist/x402/index.js.map +1 -1
  166. package/package.json +21 -10
  167. package/src/Challenge.test.ts +40 -0
  168. package/src/Challenge.ts +19 -6
  169. package/src/Mcp.ts +4 -0
  170. package/src/PaymentRequest.ts +10 -10
  171. package/src/cli/cli.test.ts +15 -15
  172. package/src/client/Mppx.test-d.ts +21 -1
  173. package/src/client/Mppx.test.ts +1 -1
  174. package/src/client/Mppx.ts +2 -2
  175. package/src/client/Transport.test.ts +225 -178
  176. package/src/client/Transport.ts +77 -83
  177. package/src/client/index.ts +14 -0
  178. package/src/client/internal/Fetch.test.ts +207 -2
  179. package/src/client/internal/Fetch.ts +52 -6
  180. package/src/client/internal/protocols/Mcp.test.ts +220 -0
  181. package/src/client/internal/protocols/Mcp.ts +162 -0
  182. package/src/client/internal/protocols/Mpp.ts +21 -0
  183. package/src/client/internal/protocols/Protocol.ts +10 -0
  184. package/src/client/internal/protocols/Shared.ts +25 -0
  185. package/src/client/internal/protocols/X402.ts +42 -0
  186. package/src/discovery/OpenApi.test.ts +1 -1
  187. package/src/evm/PublicInterface.test-d.ts +1 -1
  188. package/src/evm/client/index.ts +1 -0
  189. package/src/evm/index.ts +2 -0
  190. package/src/evm/server/Charge.test.ts +1 -1
  191. package/src/evm/server/index.ts +1 -0
  192. package/src/{mcp-sdk → mcp}/client/McpClient.integration.test.ts +10 -4
  193. package/src/{mcp-sdk → mcp}/client/McpClient.test-d.ts +45 -18
  194. package/src/{mcp-sdk → mcp}/client/McpClient.test.ts +211 -5
  195. package/src/mcp/client/McpClient.ts +307 -0
  196. package/src/{mcp-sdk → mcp}/client/McpClient.unit.test.ts +9 -5
  197. package/src/middlewares/elysia.test.ts +1 -1
  198. package/src/middlewares/express.test.ts +1 -1
  199. package/src/middlewares/hono.test.ts +1 -1
  200. package/src/middlewares/internal/mppx.test.ts +1 -1
  201. package/src/middlewares/nextjs.test.ts +1 -1
  202. package/src/proxy/Proxy.test.ts +1 -1
  203. package/src/proxy/services/anthropic.test.ts +1 -1
  204. package/src/proxy/services/openai.test.ts +1 -1
  205. package/src/proxy/services/stripe.test.ts +1 -1
  206. package/src/server/Mppx.authorize.test.ts +1 -1
  207. package/src/server/Mppx.test-d.ts +1 -1
  208. package/src/server/Mppx.test.ts +20 -2
  209. package/src/server/Mppx.ts +14 -1
  210. package/src/server/Transport.test.ts +6 -6
  211. package/src/server/Transport.ts +1 -1
  212. package/src/stripe/Charge.integration.test.ts +1 -1
  213. package/src/stripe/client/Charge.test.ts +1 -1
  214. package/src/stripe/server/Charge.test.ts +1 -1
  215. package/src/stripe/server/internal/html/package.json +1 -1
  216. package/src/stripe/server/internal/html.gen.ts +1 -1
  217. package/src/tempo/Proof.conformance.test.ts +146 -0
  218. package/src/tempo/Proof.test-d.ts +15 -0
  219. package/src/tempo/Proof.ts +52 -1
  220. package/src/tempo/Subscription.integration.test.ts +1 -1
  221. package/src/tempo/client/Charge.test.ts +173 -0
  222. package/src/tempo/client/Charge.ts +65 -36
  223. package/src/tempo/client/Methods.ts +4 -2
  224. package/src/tempo/client/ResolveAccount.ts +46 -0
  225. package/src/tempo/internal/fee-payer.test.ts +65 -10
  226. package/src/tempo/internal/fee-payer.ts +42 -6
  227. package/src/tempo/internal/proof.test.ts +12 -4
  228. package/src/tempo/internal/proof.ts +55 -6
  229. package/src/tempo/legacy/client/SessionManager.ts +11 -3
  230. package/src/tempo/legacy/server/Session.test.ts +91 -26
  231. package/src/tempo/server/Charge.test.ts +388 -17
  232. package/src/tempo/server/Charge.ts +46 -24
  233. package/src/tempo/server/Methods.ts +4 -2
  234. package/src/tempo/server/Subscription.test.ts +465 -3
  235. package/src/tempo/server/Subscription.ts +174 -19
  236. package/src/tempo/server/internal/html/package.json +2 -2
  237. package/src/tempo/server/internal/html.gen.ts +1 -1
  238. package/src/tempo/session/client/ChannelOps.ts +5 -19
  239. package/src/tempo/session/client/ChannelStore.ts +111 -0
  240. package/src/tempo/session/client/CredentialState.test.ts +206 -62
  241. package/src/tempo/session/client/CredentialState.ts +58 -73
  242. package/src/tempo/session/client/Session.test.ts +41 -1
  243. package/src/tempo/session/client/Session.ts +36 -10
  244. package/src/tempo/session/client/SessionManager.test.ts +154 -65
  245. package/src/tempo/session/client/SessionManager.ts +141 -235
  246. package/src/tempo/session/client/index.ts +8 -5
  247. package/src/tempo/session/precompile/Voucher.test.ts +45 -7
  248. package/src/tempo/session/precompile/Voucher.ts +27 -25
  249. package/src/tempo/session/server/Session.test.ts +4 -4
  250. package/src/tempo/session/server/Settlement.test.ts +88 -1
  251. package/src/tempo/session/server/Settlement.ts +2 -1
  252. package/src/tempo/session/server/Sse.ts +0 -2
  253. package/src/tempo/session/server/Ws.ts +0 -4
  254. package/src/tempo/subscription/Store.ts +27 -9
  255. package/src/x402/Exact.e2e.test.ts +1 -1
  256. package/src/x402/PublicInterface.test-d.ts +1 -1
  257. package/src/x402/index.ts +1 -0
  258. package/dist/mcp-sdk/client/McpClient.d.ts +0 -85
  259. package/dist/mcp-sdk/client/McpClient.d.ts.map +0 -1
  260. package/dist/mcp-sdk/client/McpClient.js +0 -118
  261. package/dist/mcp-sdk/client/McpClient.js.map +0 -1
  262. package/dist/mcp-sdk/client/index.d.ts.map +0 -1
  263. package/dist/mcp-sdk/client/index.js.map +0 -1
  264. package/dist/mcp-sdk/server/Transport.d.ts.map +0 -1
  265. package/dist/mcp-sdk/server/Transport.js.map +0 -1
  266. package/dist/mcp-sdk/server/index.d.ts.map +0 -1
  267. package/dist/mcp-sdk/server/index.js.map +0 -1
  268. package/src/mcp-sdk/client/McpClient.ts +0 -228
  269. /package/dist/{mcp-sdk → mcp}/client/index.d.ts +0 -0
  270. /package/dist/{mcp-sdk → mcp}/client/index.js +0 -0
  271. /package/dist/{mcp-sdk → mcp}/server/Transport.d.ts +0 -0
  272. /package/dist/{mcp-sdk → mcp}/server/Transport.js +0 -0
  273. /package/dist/{mcp-sdk → mcp}/server/index.d.ts +0 -0
  274. /package/dist/{mcp-sdk → mcp}/server/index.js +0 -0
  275. /package/src/{mcp-sdk → mcp}/client/index.ts +0 -0
  276. /package/src/{mcp-sdk → mcp}/server/Transport.test.ts +0 -0
  277. /package/src/{mcp-sdk → mcp}/server/Transport.ts +0 -0
  278. /package/src/{mcp-sdk → mcp}/server/index.ts +0 -0
@@ -1,7 +1,22 @@
1
+ import type { Address, Hex } from 'viem'
1
2
  import { expectTypeOf, test } from 'vp/test'
2
3
 
3
4
  import { Proof } from './index.js'
4
5
 
6
+ test('Proof exports the wallet-bound typed-data contract helpers', () => {
7
+ expectTypeOf(Proof.message).toEqualTypeOf<
8
+ (parameters: { account: Address; challengeId: string; realm: string }) => {
9
+ readonly account: Address
10
+ readonly challengeId: string
11
+ readonly realm: string
12
+ }
13
+ >()
14
+
15
+ expectTypeOf(Proof.hash).toEqualTypeOf<
16
+ (parameters: { account: Address; chainId: number; challengeId: string; realm: string }) => Hex
17
+ >()
18
+ })
19
+
5
20
  test('Proof exports public proof source helpers', () => {
6
21
  expectTypeOf(Proof.proofSource).toEqualTypeOf<
7
22
  (parameters: { address: string; chainId: number }) => string
@@ -1,7 +1,58 @@
1
- import type { Address } from 'viem'
1
+ import type { Address, Hex } from 'viem'
2
2
 
3
3
  import * as Proof_internal from './internal/proof.js'
4
4
 
5
+ /** EIP-712 primary type for Tempo proof credentials. */
6
+ export const primaryType = Proof_internal.primaryType
7
+
8
+ /**
9
+ * EIP-712 typed-data field definitions for Tempo zero-amount proof credentials.
10
+ *
11
+ * The `account` field cryptographically binds the signature to the payer
12
+ * wallet, so a proof signed for one account cannot be replayed against another.
13
+ */
14
+ export const types = Proof_internal.types
15
+
16
+ /** Constructs the EIP-712 domain for a Tempo proof credential. */
17
+ export function domain(chainId: number) {
18
+ return Proof_internal.domain(chainId)
19
+ }
20
+
21
+ /**
22
+ * Constructs the EIP-712 message for a Tempo proof credential.
23
+ *
24
+ * @param parameters - Proof message parameters.
25
+ * @param parameters.account - Payer wallet address the proof is bound to.
26
+ * @param parameters.challengeId - Challenge `id` being proven.
27
+ * @param parameters.realm - Challenge `realm` being proven.
28
+ */
29
+ export function message(parameters: { account: Address; challengeId: string; realm: string }) {
30
+ return Proof_internal.message(parameters)
31
+ }
32
+
33
+ /**
34
+ * Constructs the complete EIP-712 typed-data payload for a Tempo proof
35
+ * credential — the canonical, wallet-bound proof contract.
36
+ */
37
+ export function typedData(parameters: {
38
+ account: Address
39
+ chainId: number
40
+ challengeId: string
41
+ realm: string
42
+ }) {
43
+ return Proof_internal.typedData(parameters)
44
+ }
45
+
46
+ /** Computes the EIP-712 digest (signing payload) for a Tempo proof credential. */
47
+ export function hash(parameters: {
48
+ account: Address
49
+ chainId: number
50
+ challengeId: string
51
+ realm: string
52
+ }): Hex {
53
+ return Proof_internal.hash(parameters)
54
+ }
55
+
5
56
  /** Constructs the canonical `did:pkh:eip155` source DID for Tempo proof credentials. */
6
57
  export function proofSource(parameters: { address: string; chainId: number }): string {
7
58
  return Proof_internal.proofSource(parameters)
@@ -10,7 +10,7 @@ import * as SubscriptionStore from './subscription/Store.js'
10
10
  import type { SubscriptionAccessKey, SubscriptionRecord } from './subscription/Types.js'
11
11
 
12
12
  const realm = 'news.example.com'
13
- const secretKey = 'subscription-lifecycle-secret'
13
+ const secretKey = 'subscription-lifecycle-secret-key-32'
14
14
  const currency = '0x20c0000000000000000000000000000000000001'
15
15
  const recipient = '0x1234567890abcdef1234567890abcdef12345678'
16
16
  const periodCount = '30'
@@ -2,6 +2,7 @@ import { Challenge, Credential } from 'mppx'
2
2
  import { createClient, http } from 'viem'
3
3
  import { privateKeyToAccount } from 'viem/accounts'
4
4
  import { tempoLocalnet } from 'viem/chains'
5
+ import { Account, Secp256k1 } from 'viem/tempo'
5
6
  import { describe, expect, test, vi } from 'vp/test'
6
7
 
7
8
  import * as Methods from '../Methods.js'
@@ -83,6 +84,178 @@ describe('tempo.charge client', () => {
83
84
  expect(credential.source).toBe(`did:pkh:eip155:${chainId}:${account.address}`)
84
85
  })
85
86
 
87
+ test('resolveAccount selects the transaction account from executable calls', async () => {
88
+ vi.resetModules()
89
+ const selectedAccount = privateKeyToAccount(
90
+ '0x0000000000000000000000000000000000000000000000000000000000000002',
91
+ )
92
+ const chainId = 42431
93
+ const calls: charge.ResolveAccountInfo[] = []
94
+ const prepareTransactionRequest = vi.fn(async () => ({}))
95
+ const signTransaction = vi.fn(async () => '0xdeadbeef')
96
+ vi.doMock('viem/actions', () => ({
97
+ prepareTransactionRequest,
98
+ sendCallsSync: vi.fn(),
99
+ signTransaction,
100
+ signTypedData: vi.fn(),
101
+ }))
102
+
103
+ try {
104
+ const { charge: chargeWithMockedActions } = await import('./Charge.js')
105
+ const client = createClient({
106
+ account,
107
+ chain: tempoLocalnet,
108
+ transport: http('http://127.0.0.1'),
109
+ })
110
+ const method = chargeWithMockedActions({
111
+ account,
112
+ getClient: () => client,
113
+ resolveAccount(info) {
114
+ calls.push(info)
115
+ return selectedAccount
116
+ },
117
+ })
118
+
119
+ const credential = Credential.deserialize(
120
+ await method.createCredential({
121
+ challenge: createChallenge({ amount: '1', chainId, supportedModes: ['pull'] }),
122
+ context: {},
123
+ }),
124
+ )
125
+
126
+ expect(calls).toHaveLength(1)
127
+ expect(calls[0]!.account.address).toBe(account.address)
128
+ expect(calls[0]!.chainId).toBe(chainId)
129
+ expect(calls[0]!.operation.kind).toBe('executeCalls')
130
+ if (calls[0]!.operation.kind !== 'executeCalls') throw new Error('expected executeCalls')
131
+ expect(calls[0]!.operation.calls).toHaveLength(1)
132
+ expect(calls[0]!.operation.calls?.[0]?.to.toLowerCase()).toBe(currency.toLowerCase())
133
+ expect(prepareTransactionRequest).toHaveBeenCalledOnce()
134
+ expect(signTransaction).toHaveBeenCalledOnce()
135
+ expect(credential.payload).toEqual({ signature: '0xdeadbeef', type: 'transaction' })
136
+ expect(credential.source).toBe(`did:pkh:eip155:${chainId}:${selectedAccount.address}`)
137
+ } finally {
138
+ vi.doUnmock('viem/actions')
139
+ vi.resetModules()
140
+ }
141
+ })
142
+
143
+ test('resolveAccount omits executable calls when auto-swap routing is account-dependent', async () => {
144
+ vi.resetModules()
145
+ const selectedAccount = privateKeyToAccount(
146
+ '0x0000000000000000000000000000000000000000000000000000000000000002',
147
+ )
148
+ const chainId = 42431
149
+ const calls: charge.ResolveAccountInfo[] = []
150
+ const prepareTransactionRequest = vi.fn(async () => ({}))
151
+ const signTransaction = vi.fn(async () => '0xdeadbeef')
152
+ const findCalls = vi.fn(async (_client: unknown, _parameters: { account: string }) => undefined)
153
+ vi.doMock('viem/actions', () => ({
154
+ prepareTransactionRequest,
155
+ sendCallsSync: vi.fn(),
156
+ signTransaction,
157
+ signTypedData: vi.fn(),
158
+ }))
159
+ vi.doMock('../internal/auto-swap.js', () => ({
160
+ defaultCurrencies: [currency],
161
+ findCalls,
162
+ resolve: vi.fn(() => ({ tokenIn: [currency], slippage: 1 })),
163
+ }))
164
+
165
+ try {
166
+ const { charge: chargeWithMockedActions } = await import('./Charge.js')
167
+ const client = createClient({
168
+ account,
169
+ chain: tempoLocalnet,
170
+ transport: http('http://127.0.0.1'),
171
+ })
172
+ const method = chargeWithMockedActions({
173
+ account,
174
+ autoSwap: true,
175
+ getClient: () => client,
176
+ resolveAccount(info) {
177
+ calls.push(info)
178
+ return selectedAccount
179
+ },
180
+ })
181
+
182
+ const credential = Credential.deserialize(
183
+ await method.createCredential({
184
+ challenge: createChallenge({ amount: '1', chainId, supportedModes: ['pull'] }),
185
+ context: {},
186
+ }),
187
+ )
188
+
189
+ expect(calls).toHaveLength(1)
190
+ expect(calls[0]!.operation.kind).toBe('executeCalls')
191
+ if (calls[0]!.operation.kind !== 'executeCalls') throw new Error('expected executeCalls')
192
+ expect(calls[0]!.operation.calls).toBeUndefined()
193
+ expect(findCalls).toHaveBeenCalledOnce()
194
+ expect(findCalls.mock.calls[0]?.[1].account).toBe(selectedAccount.address)
195
+ expect(credential.payload).toEqual({ signature: '0xdeadbeef', type: 'transaction' })
196
+ } finally {
197
+ vi.doUnmock('viem/actions')
198
+ vi.doUnmock('../internal/auto-swap.js')
199
+ vi.resetModules()
200
+ }
201
+ })
202
+
203
+ test('zero-amount proof binds to the root payer for an access-key account', async () => {
204
+ vi.resetModules()
205
+ // Capture the typed data so we can assert what the proof commits to.
206
+ let signedTypedData: { message: { account: string } } | undefined
207
+ const signTypedData = vi.fn(async (_client: unknown, parameters: typeof signedTypedData) => {
208
+ signedTypedData = parameters
209
+ return '0xdeadbeef'
210
+ })
211
+ vi.doMock('viem/actions', () => ({
212
+ prepareTransactionRequest: vi.fn(),
213
+ sendCallsSync: vi.fn(),
214
+ signTransaction: vi.fn(),
215
+ signTypedData,
216
+ }))
217
+
218
+ try {
219
+ const { charge: chargeWithMockedActions } = await import('./Charge.js')
220
+ const chainId = 42431
221
+ // An access-key account signs with its own key but reports the root
222
+ // account as `address`; the proof must bind to that root payer.
223
+ const accessKey = Account.fromSecp256k1(Secp256k1.randomPrivateKey(), {
224
+ access: account,
225
+ })
226
+ expect(accessKey.address).toBe(account.address)
227
+ expect(accessKey.accessKeyAddress).not.toBe(account.address)
228
+
229
+ const client = createClient({
230
+ account: accessKey,
231
+ chain: tempoLocalnet,
232
+ transport: http('http://127.0.0.1'),
233
+ })
234
+ const resolveAccount = vi.fn()
235
+ const method = chargeWithMockedActions({
236
+ account: accessKey,
237
+ getClient: () => client,
238
+ resolveAccount,
239
+ })
240
+
241
+ const credential = Credential.deserialize(
242
+ await method.createCredential({
243
+ challenge: createChallenge({ chainId }),
244
+ context: {},
245
+ }),
246
+ )
247
+
248
+ expect(signTypedData).toHaveBeenCalledOnce()
249
+ expect(resolveAccount).not.toHaveBeenCalled()
250
+ expect(signedTypedData?.message.account).toBe(account.address)
251
+ expect(credential.payload).toEqual({ signature: '0xdeadbeef', type: 'proof' })
252
+ expect(credential.source).toBe(`did:pkh:eip155:${chainId}:${account.address}`)
253
+ } finally {
254
+ vi.doUnmock('viem/actions')
255
+ vi.resetModules()
256
+ }
257
+ })
258
+
86
259
  test('uses challenge chainId for non-zero transaction source', async () => {
87
260
  vi.resetModules()
88
261
  const prepareTransactionRequest = vi.fn(async () => ({}))
@@ -20,6 +20,20 @@ import * as Charge_internal from '../internal/charge.js'
20
20
  import * as defaults from '../internal/defaults.js'
21
21
  import * as Proof from '../internal/proof.js'
22
22
  import * as Methods from '../Methods.js'
23
+ import type * as AccountResolution from './ResolveAccount.js'
24
+
25
+ /** Runtime context accepted by the Tempo charge client method. */
26
+ export type ChargeContext = {
27
+ account?: Account.getResolver.Parameters['account'] | undefined
28
+ autoSwap?: AutoSwap.resolve.Value | undefined
29
+ mode?: Methods.ChargeMode | undefined
30
+ }
31
+
32
+ const chargeContextSchema = z.object({
33
+ account: z.optional(z.custom<ChargeContext['account']>()),
34
+ autoSwap: z.optional(z.custom<ChargeContext['autoSwap']>()),
35
+ mode: z.optional(z.enum(Methods.chargeModes)),
36
+ })
23
37
 
24
38
  /**
25
39
  * Creates a Tempo charge method intent for usage on the client.
@@ -44,11 +58,7 @@ export function charge(parameters: charge.Parameters = {}) {
44
58
  const getAccount = Account.getResolver({ account: parameters.account })
45
59
 
46
60
  return Method.toClient(Methods.charge, {
47
- context: z.object({
48
- account: z.optional(z.custom<Account.getResolver.Parameters['account']>()),
49
- autoSwap: z.optional(z.custom<charge.AutoSwap>()),
50
- mode: z.optional(z.enum(Methods.chargeModes)),
51
- }),
61
+ context: chargeContextSchema,
52
62
 
53
63
  async createCredential({ challenge, context }) {
54
64
  // Chain pinning: reject a challenge whose chain ID conflicts with the
@@ -68,24 +78,30 @@ export function charge(parameters: charge.Parameters = {}) {
68
78
  if (chainId === undefined)
69
79
  throw new Error('No `chainId` provided. Pass a chain ID in the challenge or client.')
70
80
 
71
- const account = getAccount(client, context)
72
-
73
81
  const { request } = challenge
74
82
  const { amount, methodDetails } = request
83
+ const supportedModes = (methodDetails?.supportedModes as
84
+ | readonly Methods.ChargeMode[]
85
+ | undefined) ?? ['pull', 'push']
86
+ const defaultAccount = getAccount(client, context)
75
87
 
76
88
  // Zero-amount: sign EIP-712 typed data instead of creating a transaction.
77
89
  if (BigInt(amount) === 0n) {
78
90
  const signature = await signTypedData(client, {
79
- account,
80
- domain: Proof.domain(chainId),
81
- types: Proof.types,
82
- primaryType: 'Proof',
83
- message: Proof.message(challenge.id, challenge.realm),
91
+ account: defaultAccount,
92
+ // `account` here is the signing account; the proof's bound payer is
93
+ // `account.address` (echoed in the credential `source` below).
94
+ ...Proof.typedData({
95
+ account: defaultAccount.address,
96
+ chainId,
97
+ challengeId: challenge.id,
98
+ realm: challenge.realm,
99
+ }),
84
100
  })
85
101
  return Credential.serialize({
86
102
  challenge,
87
103
  payload: { signature, type: 'proof' },
88
- source: Proof.proofSource({ address: account.address, chainId }),
104
+ source: Proof.proofSource({ address: defaultAccount.address, chainId }),
89
105
  })
90
106
  }
91
107
 
@@ -100,22 +116,6 @@ export function charge(parameters: charge.Parameters = {}) {
100
116
  }
101
117
  }
102
118
  }
103
- const supportedModes = (methodDetails?.supportedModes as
104
- | readonly Methods.ChargeMode[]
105
- | undefined) ?? ['pull', 'push']
106
- const mode = (() => {
107
- const explicitMode = context?.mode ?? parameters.mode
108
- if (explicitMode) {
109
- if (!supportedModes.includes(explicitMode))
110
- throw new Error(`Challenge does not support ${explicitMode} mode.`)
111
- return explicitMode
112
- }
113
-
114
- const preferredMode = account.type === 'json-rpc' ? 'push' : 'pull'
115
- if (supportedModes.includes(preferredMode)) return preferredMode
116
- return supportedModes[0]!
117
- })()
118
-
119
119
  const memo = methodDetails?.memo
120
120
  ? (methodDetails.memo as Hex.Hex)
121
121
  : Attribution.encode({ challengeId: challenge.id, clientId, serverId: challenge.realm })
@@ -127,13 +127,14 @@ export function charge(parameters: charge.Parameters = {}) {
127
127
  },
128
128
  recipient: request.recipient as Address,
129
129
  })
130
- const transferCalls = transfers.map((transfer) =>
131
- Actions.token.transfer.call({
132
- amount: BigInt(transfer.amount),
133
- ...(transfer.memo && { memo: transfer.memo as Hex.Hex }),
134
- to: transfer.recipient as Address,
135
- token: currency,
136
- }),
130
+ const transferCalls = transfers.map(
131
+ (transfer): AccountResolution.ResolveAccountCall =>
132
+ Actions.token.transfer.call({
133
+ amount: BigInt(transfer.amount),
134
+ ...(transfer.memo && { memo: transfer.memo as Hex.Hex }),
135
+ to: transfer.recipient as Address,
136
+ token: currency,
137
+ }) as AccountResolution.ResolveAccountCall,
137
138
  )
138
139
 
139
140
  const autoSwap = AutoSwap.resolve(
@@ -141,6 +142,29 @@ export function charge(parameters: charge.Parameters = {}) {
141
142
  AutoSwap.defaultCurrencies,
142
143
  )
143
144
 
145
+ const account =
146
+ (await parameters.resolveAccount?.({
147
+ account: defaultAccount,
148
+ chainId,
149
+ operation: {
150
+ kind: 'executeCalls',
151
+ ...(autoSwap ? {} : { calls: transferCalls }),
152
+ },
153
+ })) ?? defaultAccount
154
+
155
+ const mode = (() => {
156
+ const explicitMode = context?.mode ?? parameters.mode
157
+ if (explicitMode) {
158
+ if (!supportedModes.includes(explicitMode))
159
+ throw new Error(`Challenge does not support ${explicitMode} mode.`)
160
+ return explicitMode
161
+ }
162
+
163
+ const preferredMode = account.type === 'json-rpc' ? 'push' : 'pull'
164
+ if (supportedModes.includes(preferredMode)) return preferredMode
165
+ return supportedModes[0]!
166
+ })()
167
+
144
168
  const swapCalls = autoSwap
145
169
  ? await AutoSwap.findCalls(client, {
146
170
  account: account.address,
@@ -198,6 +222,9 @@ export function charge(parameters: charge.Parameters = {}) {
198
222
 
199
223
  export declare namespace charge {
200
224
  type AutoSwap = AutoSwap.resolve.Value
225
+ type Context = ChargeContext
226
+ type ResolveAccount = AccountResolution.ResolveAccount
227
+ type ResolveAccountInfo = AccountResolution.ResolveAccountInfo
201
228
 
202
229
  type Parameters = {
203
230
  /**
@@ -232,6 +259,8 @@ export declare namespace charge {
232
259
  * @default `'push'` for JSON-RPC accounts, `'pull'` for local accounts.
233
260
  */
234
261
  mode?: Methods.ChargeMode | undefined
262
+ /** Selects the account that signs this charge after the challenge and chain are known. */
263
+ resolveAccount?: ResolveAccount | undefined
235
264
  } & Account.getResolver.Parameters &
236
265
  Client.getResolver.Parameters
237
266
  }
@@ -14,14 +14,14 @@ const sessionLegacyClient = Object.assign(sessionLegacy_, { method: sessionLegac
14
14
  export { sessionClient as session }
15
15
 
16
16
  /**
17
- * Creates both Tempo `charge` and `session` client methods from shared parameters.
17
+ * Creates the common Tempo `charge` and `session` client methods from shared parameters.
18
18
  *
19
19
  * @example
20
20
  * ```ts
21
21
  * import { Mppx, tempo } from 'mppx/client'
22
22
  *
23
23
  * const mppx = Mppx.create({
24
- * methods: [tempo({ account })],
24
+ * methods: [tempo.common({ account })],
25
25
  * })
26
26
  * ```
27
27
  */
@@ -34,6 +34,8 @@ export namespace tempo {
34
34
 
35
35
  /** Creates a Tempo `charge` client method for one-time TIP-20 token transfers. */
36
36
  export const charge = charge_
37
+ /** Creates the common Tempo `charge` and `session` client methods from shared parameters. */
38
+ export const common = tempo
37
39
  /** Creates a TIP-1034 client method for Mppx registration. Use `tempo.session.manager()` for direct lifecycle control. */
38
40
  export const session = sessionClient
39
41
  /** @deprecated Use `tempo.session()` for the TIP-1034 session client method. */
@@ -0,0 +1,46 @@
1
+ import type * as Hex from 'ox/Hex'
2
+ import type { Account, Address } from 'viem'
3
+
4
+ import type { MaybePromise } from '../../internal/types.js'
5
+
6
+ /** Resolves the account that should satisfy an mppx account operation. */
7
+ export type ResolveAccount = (info: ResolveAccountInfo) => MaybePromise<Account | undefined>
8
+
9
+ /** Account-resolution details for a client credential operation. */
10
+ export type ResolveAccountInfo = {
11
+ /** Account mppx will use when the hook returns `undefined`. */
12
+ account: Account
13
+ /** EIP-155 chain ID used for the operation. */
14
+ chainId: number
15
+ /** Capability the selected account must satisfy. */
16
+ operation: ResolveAccountOperation
17
+ }
18
+
19
+ /** Capability an mppx-selected account must satisfy. */
20
+ export type ResolveAccountOperation =
21
+ | {
22
+ kind: 'executeCalls'
23
+ /**
24
+ * Exact EVM calls the selected account will execute.
25
+ *
26
+ * Omitted when the calls depend on which account is selected, such as
27
+ * account-balance-dependent auto-swap routing.
28
+ */
29
+ calls?: readonly ResolveAccountCall[] | undefined
30
+ }
31
+ | {
32
+ kind: 'authorizePaymentChannel'
33
+ /**
34
+ * Signer required by an existing reusable channel. Omitted when opening
35
+ * a new channel or when no existing channel has fixed a signer yet.
36
+ */
37
+ authority?: Address | undefined
38
+ }
39
+
40
+ /** EVM call data used by account resolvers for scoped account selection. */
41
+ export type ResolveAccountCall = {
42
+ /** Contract address being called. */
43
+ to: Address
44
+ /** Calldata being sent. */
45
+ data: Hex.Hex
46
+ }
@@ -1,3 +1,5 @@
1
+ import { Address, Secp256k1 } from 'ox'
2
+ import { TxEnvelopeTempo } from 'ox/tempo'
1
3
  import { encodeFunctionData, maxUint256, toHex } from 'viem'
2
4
  import { Abis, Addresses, Transaction } from 'viem/tempo'
3
5
  import { afterEach, describe, expect, test, vi } from 'vp/test'
@@ -499,30 +501,82 @@ describe('fillHostedFeePayerTransaction', () => {
499
501
  } as const
500
502
 
501
503
  test('uses hosted fillTransaction and preserves sender-committed fields', async () => {
504
+ // Sign over the payload built from the actual RPC request body so this
505
+ // verifies recovery parity with the real request shape.
506
+ const sponsorPrivateKey =
507
+ '0x0000000000000000000000000000000000000000000000000000000000000042' as const
508
+ const sponsorAddress = Address.fromPublicKey(
509
+ Secp256k1.getPublicKey({ privateKey: sponsorPrivateKey }),
510
+ )
511
+ let realFeePayerSignature: ReturnType<typeof Secp256k1.sign> | undefined
512
+
502
513
  const calls: { init?: RequestInit | undefined; input: RequestInfo | URL }[] = []
503
514
  const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
504
515
  calls.push({ init, input })
516
+ const rpc = JSON.parse(init!.body as string).params[0]
517
+ const quantity = (value: unknown) =>
518
+ value === undefined ? undefined : BigInt(value as string)
519
+ realFeePayerSignature = Secp256k1.sign({
520
+ payload: TxEnvelopeTempo.getFeePayerSignPayload(
521
+ TxEnvelopeTempo.from({
522
+ accessList: rpc.accessList,
523
+ calls: rpc.calls.map(({ value, ...call }: any) => ({
524
+ ...call,
525
+ ...(value && value !== '0x' ? { value: BigInt(value) } : {}),
526
+ })),
527
+ chainId: hostedTransaction.chainId,
528
+ feeToken: defaults.tokens.pathUsd,
529
+ from: rpc.from,
530
+ ...(quantity(rpc.gas) !== undefined ? { gas: quantity(rpc.gas) } : {}),
531
+ ...(rpc.keyAuthorization !== undefined
532
+ ? { keyAuthorization: rpc.keyAuthorization }
533
+ : {}),
534
+ ...(quantity(rpc.maxFeePerGas) !== undefined
535
+ ? { maxFeePerGas: quantity(rpc.maxFeePerGas) }
536
+ : {}),
537
+ ...(quantity(rpc.maxPriorityFeePerGas) !== undefined
538
+ ? { maxPriorityFeePerGas: quantity(rpc.maxPriorityFeePerGas) }
539
+ : {}),
540
+ ...(quantity(rpc.nonce) !== undefined ? { nonce: quantity(rpc.nonce) } : {}),
541
+ ...(quantity(rpc.nonceKey) !== undefined ? { nonceKey: quantity(rpc.nonceKey) } : {}),
542
+ type: 'tempo',
543
+ ...(rpc.validAfter !== undefined ? { validAfter: Number(BigInt(rpc.validAfter)) } : {}),
544
+ ...(rpc.validBefore !== undefined
545
+ ? { validBefore: Number(BigInt(rpc.validBefore)) }
546
+ : {}),
547
+ } as any) as any,
548
+ { sender: rpc.from },
549
+ ),
550
+ privateKey: sponsorPrivateKey,
551
+ })
505
552
  return new Response(
506
- JSON.stringify({
507
- result: {
508
- tx: {
509
- feePayerSignature,
510
- feeToken: defaults.tokens.pathUsd,
511
- gas: '0x1',
512
- maxFeePerGas: '0x2',
553
+ JSON.stringify(
554
+ {
555
+ result: {
556
+ tx: {
557
+ feePayerSignature: realFeePayerSignature,
558
+ feeToken: defaults.tokens.pathUsd,
559
+ gas: '0x1',
560
+ maxFeePerGas: '0x2',
561
+ },
513
562
  },
514
563
  },
515
- }),
564
+ (_key, value) => (typeof value === 'bigint' ? toHex(value) : value),
565
+ ),
516
566
  )
517
567
  })
518
568
  vi.stubGlobal('fetch', fetchMock)
519
569
 
520
- const serialized = await fillHostedFeePayerTransaction({
570
+ const result = await fillHostedFeePayerTransaction({
521
571
  allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
522
572
  transaction: hostedTransaction as any,
523
573
  url: 'https://sponsor.example/tp_key',
524
574
  })
525
575
 
576
+ expect(result.feeToken).toBe(defaults.tokens.pathUsd)
577
+ expect(result.feePayer.toLowerCase()).toBe(sponsorAddress.toLowerCase())
578
+ const serialized = result.serializedTransaction
579
+
526
580
  expect(fetchMock).toHaveBeenCalledOnce()
527
581
  expect(calls[0]!.input).toBe('https://sponsor.example/tp_key')
528
582
  const body = JSON.parse(calls[0]!.init!.body as string)
@@ -554,7 +608,8 @@ describe('fillHostedFeePayerTransaction', () => {
554
608
  expect(transaction.maxFeePerGas).toBe(hostedTransaction.maxFeePerGas)
555
609
  expect(transaction.calls).toEqual(hostedTransaction.calls)
556
610
  expect(transaction.feeToken).toBe(defaults.tokens.pathUsd)
557
- expect(transaction.feePayerSignature).toEqual(feePayerSignature)
611
+ expect(BigInt(transaction.feePayerSignature!.r)).toBe(realFeePayerSignature!.r)
612
+ expect(BigInt(transaction.feePayerSignature!.s)).toBe(realFeePayerSignature!.s)
558
613
  })
559
614
 
560
615
  test('error: requires hosted fee payer to return a feeToken', async () => {