mppx 0.7.0 → 0.8.1

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 (290) hide show
  1. package/CHANGELOG.md +41 -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/cli/internal.d.ts +1 -0
  13. package/dist/cli/internal.d.ts.map +1 -1
  14. package/dist/cli/internal.js +1 -15
  15. package/dist/cli/internal.js.map +1 -1
  16. package/dist/client/Mppx.js +2 -2
  17. package/dist/client/Mppx.js.map +1 -1
  18. package/dist/client/Transport.d.ts +11 -16
  19. package/dist/client/Transport.d.ts.map +1 -1
  20. package/dist/client/Transport.js +55 -75
  21. package/dist/client/Transport.js.map +1 -1
  22. package/dist/client/index.d.ts +3 -0
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js +1 -0
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/client/internal/Fetch.d.ts.map +1 -1
  27. package/dist/client/internal/Fetch.js +46 -7
  28. package/dist/client/internal/Fetch.js.map +1 -1
  29. package/dist/client/internal/protocols/Mcp.d.ts +7 -0
  30. package/dist/client/internal/protocols/Mcp.d.ts.map +1 -0
  31. package/dist/client/internal/protocols/Mcp.js +159 -0
  32. package/dist/client/internal/protocols/Mcp.js.map +1 -0
  33. package/dist/client/internal/protocols/Mpp.d.ts +4 -0
  34. package/dist/client/internal/protocols/Mpp.d.ts.map +1 -0
  35. package/dist/client/internal/protocols/Mpp.js +18 -0
  36. package/dist/client/internal/protocols/Mpp.js.map +1 -0
  37. package/dist/client/internal/protocols/Protocol.d.ts +10 -0
  38. package/dist/client/internal/protocols/Protocol.d.ts.map +1 -0
  39. package/dist/client/internal/protocols/Protocol.js +2 -0
  40. package/dist/client/internal/protocols/Protocol.js.map +1 -0
  41. package/dist/client/internal/protocols/Shared.d.ts +5 -0
  42. package/dist/client/internal/protocols/Shared.d.ts.map +1 -0
  43. package/dist/client/internal/protocols/Shared.js +20 -0
  44. package/dist/client/internal/protocols/Shared.js.map +1 -0
  45. package/dist/client/internal/protocols/X402.d.ts +8 -0
  46. package/dist/client/internal/protocols/X402.d.ts.map +1 -0
  47. package/dist/client/internal/protocols/X402.js +39 -0
  48. package/dist/client/internal/protocols/X402.js.map +1 -0
  49. package/dist/evm/client/index.d.ts +1 -0
  50. package/dist/evm/client/index.d.ts.map +1 -1
  51. package/dist/evm/client/index.js +1 -0
  52. package/dist/evm/client/index.js.map +1 -1
  53. package/dist/evm/index.d.ts +2 -0
  54. package/dist/evm/index.d.ts.map +1 -1
  55. package/dist/evm/index.js +2 -0
  56. package/dist/evm/index.js.map +1 -1
  57. package/dist/evm/server/index.d.ts +1 -0
  58. package/dist/evm/server/index.d.ts.map +1 -1
  59. package/dist/evm/server/index.js +1 -0
  60. package/dist/evm/server/index.js.map +1 -1
  61. package/dist/mcp/client/McpClient.d.ts +101 -0
  62. package/dist/mcp/client/McpClient.d.ts.map +1 -0
  63. package/dist/mcp/client/McpClient.js +162 -0
  64. package/dist/mcp/client/McpClient.js.map +1 -0
  65. package/dist/mcp/client/index.d.ts.map +1 -0
  66. package/dist/mcp/client/index.js.map +1 -0
  67. package/dist/mcp/server/Transport.d.ts.map +1 -0
  68. package/dist/mcp/server/Transport.js.map +1 -0
  69. package/dist/mcp/server/index.d.ts.map +1 -0
  70. package/dist/mcp/server/index.js.map +1 -0
  71. package/dist/server/Mppx.d.ts +1 -1
  72. package/dist/server/Mppx.d.ts.map +1 -1
  73. package/dist/server/Mppx.js +9 -0
  74. package/dist/server/Mppx.js.map +1 -1
  75. package/dist/server/Transport.d.ts +1 -1
  76. package/dist/server/Transport.d.ts.map +1 -1
  77. package/dist/server/Transport.js +1 -1
  78. package/dist/server/Transport.js.map +1 -1
  79. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  80. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  81. package/dist/stripe/server/internal/html.gen.js +1 -1
  82. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  83. package/dist/tempo/Proof.d.ts +85 -1
  84. package/dist/tempo/Proof.d.ts.map +1 -1
  85. package/dist/tempo/Proof.js +35 -0
  86. package/dist/tempo/Proof.js.map +1 -1
  87. package/dist/tempo/client/Charge.d.ts +13 -1
  88. package/dist/tempo/client/Charge.d.ts.map +1 -1
  89. package/dist/tempo/client/Charge.js +38 -25
  90. package/dist/tempo/client/Charge.js.map +1 -1
  91. package/dist/tempo/client/Methods.d.ts +5 -3
  92. package/dist/tempo/client/Methods.d.ts.map +1 -1
  93. package/dist/tempo/client/Methods.js +4 -2
  94. package/dist/tempo/client/Methods.js.map +1 -1
  95. package/dist/tempo/client/ResolveAccount.d.ts +40 -0
  96. package/dist/tempo/client/ResolveAccount.d.ts.map +1 -0
  97. package/dist/tempo/client/ResolveAccount.js +2 -0
  98. package/dist/tempo/client/ResolveAccount.js.map +1 -0
  99. package/dist/tempo/internal/fee-payer.d.ts +26 -1
  100. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  101. package/dist/tempo/internal/fee-payer.js +83 -30
  102. package/dist/tempo/internal/fee-payer.js.map +1 -1
  103. package/dist/tempo/internal/proof.d.ts +71 -5
  104. package/dist/tempo/internal/proof.d.ts.map +1 -1
  105. package/dist/tempo/internal/proof.js +42 -6
  106. package/dist/tempo/internal/proof.js.map +1 -1
  107. package/dist/tempo/legacy/client/SessionManager.d.ts.map +1 -1
  108. package/dist/tempo/legacy/client/SessionManager.js +10 -3
  109. package/dist/tempo/legacy/client/SessionManager.js.map +1 -1
  110. package/dist/tempo/server/Charge.d.ts.map +1 -1
  111. package/dist/tempo/server/Charge.js +46 -18
  112. package/dist/tempo/server/Charge.js.map +1 -1
  113. package/dist/tempo/server/Methods.d.ts +4 -2
  114. package/dist/tempo/server/Methods.d.ts.map +1 -1
  115. package/dist/tempo/server/Methods.js +4 -2
  116. package/dist/tempo/server/Methods.js.map +1 -1
  117. package/dist/tempo/server/Subscription.d.ts +10 -0
  118. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  119. package/dist/tempo/server/Subscription.js +135 -23
  120. package/dist/tempo/server/Subscription.js.map +1 -1
  121. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  122. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  123. package/dist/tempo/server/internal/html.gen.js +1 -1
  124. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  125. package/dist/tempo/session/client/ChannelOps.d.ts +2 -3
  126. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  127. package/dist/tempo/session/client/ChannelOps.js +7 -10
  128. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  129. package/dist/tempo/session/client/ChannelStore.d.ts +51 -0
  130. package/dist/tempo/session/client/ChannelStore.d.ts.map +1 -0
  131. package/dist/tempo/session/client/ChannelStore.js +63 -0
  132. package/dist/tempo/session/client/ChannelStore.js.map +1 -0
  133. package/dist/tempo/session/client/CredentialState.d.ts +7 -24
  134. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  135. package/dist/tempo/session/client/CredentialState.js +51 -49
  136. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  137. package/dist/tempo/session/client/Session.d.ts +8 -2
  138. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  139. package/dist/tempo/session/client/Session.js +22 -8
  140. package/dist/tempo/session/client/Session.js.map +1 -1
  141. package/dist/tempo/session/client/SessionManager.d.ts +4 -40
  142. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  143. package/dist/tempo/session/client/SessionManager.js +124 -174
  144. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  145. package/dist/tempo/session/client/index.d.ts +3 -4
  146. package/dist/tempo/session/client/index.d.ts.map +1 -1
  147. package/dist/tempo/session/client/index.js +1 -0
  148. package/dist/tempo/session/client/index.js.map +1 -1
  149. package/dist/tempo/session/precompile/Voucher.d.ts +3 -3
  150. package/dist/tempo/session/precompile/Voucher.d.ts.map +1 -1
  151. package/dist/tempo/session/precompile/Voucher.js +24 -25
  152. package/dist/tempo/session/precompile/Voucher.js.map +1 -1
  153. package/dist/tempo/session/server/CredentialVerification.d.ts +6 -0
  154. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  155. package/dist/tempo/session/server/CredentialVerification.js +13 -0
  156. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  157. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  158. package/dist/tempo/session/server/Settlement.js +4 -2
  159. package/dist/tempo/session/server/Settlement.js.map +1 -1
  160. package/dist/tempo/session/server/Sse.d.ts.map +1 -1
  161. package/dist/tempo/session/server/Sse.js.map +1 -1
  162. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  163. package/dist/tempo/session/server/Ws.js.map +1 -1
  164. package/dist/tempo/subscription/KeyAuthorization.d.ts +712 -1
  165. package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
  166. package/dist/tempo/subscription/Store.d.ts +2 -0
  167. package/dist/tempo/subscription/Store.d.ts.map +1 -1
  168. package/dist/tempo/subscription/Store.js +16 -1
  169. package/dist/tempo/subscription/Store.js.map +1 -1
  170. package/dist/x402/index.d.ts +1 -0
  171. package/dist/x402/index.d.ts.map +1 -1
  172. package/dist/x402/index.js +1 -0
  173. package/dist/x402/index.js.map +1 -1
  174. package/package.json +21 -10
  175. package/src/Challenge.test.ts +40 -0
  176. package/src/Challenge.ts +19 -6
  177. package/src/Mcp.ts +4 -0
  178. package/src/PaymentRequest.ts +10 -10
  179. package/src/cli/cli.test.ts +15 -15
  180. package/src/cli/config.test.ts +13 -7
  181. package/src/cli/internal.ts +1 -16
  182. package/src/client/Mppx.test-d.ts +21 -1
  183. package/src/client/Mppx.test.ts +1 -1
  184. package/src/client/Mppx.ts +2 -2
  185. package/src/client/Transport.test.ts +225 -178
  186. package/src/client/Transport.ts +77 -83
  187. package/src/client/index.ts +14 -0
  188. package/src/client/internal/Fetch.test.ts +207 -2
  189. package/src/client/internal/Fetch.ts +52 -6
  190. package/src/client/internal/protocols/Mcp.test.ts +220 -0
  191. package/src/client/internal/protocols/Mcp.ts +162 -0
  192. package/src/client/internal/protocols/Mpp.ts +21 -0
  193. package/src/client/internal/protocols/Protocol.ts +10 -0
  194. package/src/client/internal/protocols/Shared.ts +25 -0
  195. package/src/client/internal/protocols/X402.ts +42 -0
  196. package/src/discovery/OpenApi.test.ts +1 -1
  197. package/src/evm/PublicInterface.test-d.ts +1 -1
  198. package/src/evm/client/index.ts +1 -0
  199. package/src/evm/index.ts +2 -0
  200. package/src/evm/server/Charge.test.ts +1 -1
  201. package/src/evm/server/index.ts +1 -0
  202. package/src/{mcp-sdk → mcp}/client/McpClient.integration.test.ts +10 -4
  203. package/src/{mcp-sdk → mcp}/client/McpClient.test-d.ts +45 -18
  204. package/src/{mcp-sdk → mcp}/client/McpClient.test.ts +211 -5
  205. package/src/mcp/client/McpClient.ts +307 -0
  206. package/src/{mcp-sdk → mcp}/client/McpClient.unit.test.ts +9 -5
  207. package/src/middlewares/elysia.test.ts +1 -1
  208. package/src/middlewares/express.test.ts +1 -1
  209. package/src/middlewares/hono.test.ts +1 -1
  210. package/src/middlewares/internal/mppx.test.ts +1 -1
  211. package/src/middlewares/nextjs.test.ts +1 -1
  212. package/src/proxy/Proxy.test.ts +1 -1
  213. package/src/proxy/services/anthropic.test.ts +1 -1
  214. package/src/proxy/services/openai.test.ts +1 -1
  215. package/src/proxy/services/stripe.test.ts +1 -1
  216. package/src/server/Mppx.authorize.test.ts +1 -1
  217. package/src/server/Mppx.test-d.ts +1 -1
  218. package/src/server/Mppx.test.ts +20 -2
  219. package/src/server/Mppx.ts +14 -1
  220. package/src/server/Transport.test.ts +6 -6
  221. package/src/server/Transport.ts +1 -1
  222. package/src/stripe/Charge.integration.test.ts +1 -1
  223. package/src/stripe/client/Charge.test.ts +1 -1
  224. package/src/stripe/server/Charge.test.ts +1 -1
  225. package/src/stripe/server/internal/html/package.json +1 -1
  226. package/src/stripe/server/internal/html.gen.ts +1 -1
  227. package/src/tempo/Proof.conformance.test.ts +146 -0
  228. package/src/tempo/Proof.test-d.ts +15 -0
  229. package/src/tempo/Proof.ts +52 -1
  230. package/src/tempo/Subscription.integration.test.ts +1 -1
  231. package/src/tempo/client/Charge.test.ts +173 -0
  232. package/src/tempo/client/Charge.ts +65 -36
  233. package/src/tempo/client/Methods.ts +4 -2
  234. package/src/tempo/client/ResolveAccount.ts +46 -0
  235. package/src/tempo/internal/fee-payer.test.ts +89 -10
  236. package/src/tempo/internal/fee-payer.ts +128 -41
  237. package/src/tempo/internal/proof.test.ts +12 -4
  238. package/src/tempo/internal/proof.ts +55 -6
  239. package/src/tempo/legacy/client/SessionManager.ts +11 -3
  240. package/src/tempo/legacy/server/Session.test.ts +91 -26
  241. package/src/tempo/server/Charge.test.ts +388 -17
  242. package/src/tempo/server/Charge.ts +50 -24
  243. package/src/tempo/server/Methods.ts +4 -2
  244. package/src/tempo/server/Subscription.test.ts +465 -3
  245. package/src/tempo/server/Subscription.ts +174 -19
  246. package/src/tempo/server/internal/html/package.json +2 -2
  247. package/src/tempo/server/internal/html.gen.ts +1 -1
  248. package/src/tempo/session/client/ChannelOps.ts +5 -19
  249. package/src/tempo/session/client/ChannelStore.ts +111 -0
  250. package/src/tempo/session/client/CredentialState.test.ts +206 -62
  251. package/src/tempo/session/client/CredentialState.ts +58 -73
  252. package/src/tempo/session/client/Session.test.ts +41 -1
  253. package/src/tempo/session/client/Session.ts +36 -10
  254. package/src/tempo/session/client/SessionManager.test.ts +154 -65
  255. package/src/tempo/session/client/SessionManager.ts +141 -235
  256. package/src/tempo/session/client/index.ts +8 -5
  257. package/src/tempo/session/precompile/Voucher.test.ts +45 -7
  258. package/src/tempo/session/precompile/Voucher.ts +27 -25
  259. package/src/tempo/session/server/CredentialVerification.test.ts +36 -0
  260. package/src/tempo/session/server/CredentialVerification.ts +18 -0
  261. package/src/tempo/session/server/Session.test.ts +4 -4
  262. package/src/tempo/session/server/Settlement.test.ts +88 -1
  263. package/src/tempo/session/server/Settlement.ts +2 -1
  264. package/src/tempo/session/server/Sse.ts +0 -2
  265. package/src/tempo/session/server/Ws.ts +0 -4
  266. package/src/tempo/subscription/Store.ts +27 -9
  267. package/src/x402/Exact.e2e.test.ts +1 -1
  268. package/src/x402/PublicInterface.test-d.ts +1 -1
  269. package/src/x402/index.ts +1 -0
  270. package/dist/mcp-sdk/client/McpClient.d.ts +0 -85
  271. package/dist/mcp-sdk/client/McpClient.d.ts.map +0 -1
  272. package/dist/mcp-sdk/client/McpClient.js +0 -118
  273. package/dist/mcp-sdk/client/McpClient.js.map +0 -1
  274. package/dist/mcp-sdk/client/index.d.ts.map +0 -1
  275. package/dist/mcp-sdk/client/index.js.map +0 -1
  276. package/dist/mcp-sdk/server/Transport.d.ts.map +0 -1
  277. package/dist/mcp-sdk/server/Transport.js.map +0 -1
  278. package/dist/mcp-sdk/server/index.d.ts.map +0 -1
  279. package/dist/mcp-sdk/server/index.js.map +0 -1
  280. package/src/mcp-sdk/client/McpClient.ts +0 -228
  281. /package/dist/{mcp-sdk → mcp}/client/index.d.ts +0 -0
  282. /package/dist/{mcp-sdk → mcp}/client/index.js +0 -0
  283. /package/dist/{mcp-sdk → mcp}/server/Transport.d.ts +0 -0
  284. /package/dist/{mcp-sdk → mcp}/server/Transport.js +0 -0
  285. /package/dist/{mcp-sdk → mcp}/server/index.d.ts +0 -0
  286. /package/dist/{mcp-sdk → mcp}/server/index.js +0 -0
  287. /package/src/{mcp-sdk → mcp}/client/index.ts +0 -0
  288. /package/src/{mcp-sdk → mcp}/server/Transport.test.ts +0 -0
  289. /package/src/{mcp-sdk → mcp}/server/Transport.ts +0 -0
  290. /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'
@@ -497,32 +499,89 @@ describe('fillHostedFeePayerTransaction', () => {
497
499
  signature: { r: 1n, s: 1n, yParity: 0 } as any,
498
500
  validBefore: Math.floor(Date.now() / 1_000) + 300,
499
501
  } as const
502
+ const hostedContext = {
503
+ chainId: defaults.chainId.mainnet,
504
+ details,
505
+ } as const
500
506
 
501
507
  test('uses hosted fillTransaction and preserves sender-committed fields', async () => {
508
+ // Sign over the payload built from the actual RPC request body so this
509
+ // verifies recovery parity with the real request shape.
510
+ const sponsorPrivateKey =
511
+ '0x0000000000000000000000000000000000000000000000000000000000000042' as const
512
+ const sponsorAddress = Address.fromPublicKey(
513
+ Secp256k1.getPublicKey({ privateKey: sponsorPrivateKey }),
514
+ )
515
+ let realFeePayerSignature: ReturnType<typeof Secp256k1.sign> | undefined
516
+
502
517
  const calls: { init?: RequestInit | undefined; input: RequestInfo | URL }[] = []
503
518
  const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
504
519
  calls.push({ init, input })
520
+ const rpc = JSON.parse(init!.body as string).params[0]
521
+ const quantity = (value: unknown) =>
522
+ value === undefined ? undefined : BigInt(value as string)
523
+ realFeePayerSignature = Secp256k1.sign({
524
+ payload: TxEnvelopeTempo.getFeePayerSignPayload(
525
+ TxEnvelopeTempo.from({
526
+ accessList: rpc.accessList,
527
+ calls: rpc.calls.map(({ value, ...call }: any) => ({
528
+ ...call,
529
+ ...(value && value !== '0x' ? { value: BigInt(value) } : {}),
530
+ })),
531
+ chainId: hostedTransaction.chainId,
532
+ feeToken: defaults.tokens.pathUsd,
533
+ from: rpc.from,
534
+ ...(quantity(rpc.gas) !== undefined ? { gas: quantity(rpc.gas) } : {}),
535
+ ...(rpc.keyAuthorization !== undefined
536
+ ? { keyAuthorization: rpc.keyAuthorization }
537
+ : {}),
538
+ ...(quantity(rpc.maxFeePerGas) !== undefined
539
+ ? { maxFeePerGas: quantity(rpc.maxFeePerGas) }
540
+ : {}),
541
+ ...(quantity(rpc.maxPriorityFeePerGas) !== undefined
542
+ ? { maxPriorityFeePerGas: quantity(rpc.maxPriorityFeePerGas) }
543
+ : {}),
544
+ ...(quantity(rpc.nonce) !== undefined ? { nonce: quantity(rpc.nonce) } : {}),
545
+ ...(quantity(rpc.nonceKey) !== undefined ? { nonceKey: quantity(rpc.nonceKey) } : {}),
546
+ type: 'tempo',
547
+ ...(rpc.validAfter !== undefined ? { validAfter: Number(BigInt(rpc.validAfter)) } : {}),
548
+ ...(rpc.validBefore !== undefined
549
+ ? { validBefore: Number(BigInt(rpc.validBefore)) }
550
+ : {}),
551
+ } as any) as any,
552
+ { sender: rpc.from },
553
+ ),
554
+ privateKey: sponsorPrivateKey,
555
+ })
505
556
  return new Response(
506
- JSON.stringify({
507
- result: {
508
- tx: {
509
- feePayerSignature,
510
- feeToken: defaults.tokens.pathUsd,
511
- gas: '0x1',
512
- maxFeePerGas: '0x2',
557
+ JSON.stringify(
558
+ {
559
+ result: {
560
+ tx: {
561
+ feePayerSignature: realFeePayerSignature,
562
+ feeToken: defaults.tokens.pathUsd,
563
+ gas: '0x1',
564
+ maxFeePerGas: '0x2',
565
+ },
513
566
  },
514
567
  },
515
- }),
568
+ (_key, value) => (typeof value === 'bigint' ? toHex(value) : value),
569
+ ),
516
570
  )
517
571
  })
518
572
  vi.stubGlobal('fetch', fetchMock)
519
573
 
520
- const serialized = await fillHostedFeePayerTransaction({
574
+ const result = await fillHostedFeePayerTransaction({
521
575
  allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
576
+ ...hostedContext,
522
577
  transaction: hostedTransaction as any,
523
578
  url: 'https://sponsor.example/tp_key',
524
579
  })
525
580
 
581
+ expect(result.feeToken).toBe(defaults.tokens.pathUsd)
582
+ expect(result.feePayer.toLowerCase()).toBe(sponsorAddress.toLowerCase())
583
+ const serialized = result.serializedTransaction
584
+
526
585
  expect(fetchMock).toHaveBeenCalledOnce()
527
586
  expect(calls[0]!.input).toBe('https://sponsor.example/tp_key')
528
587
  const body = JSON.parse(calls[0]!.init!.body as string)
@@ -554,7 +613,8 @@ describe('fillHostedFeePayerTransaction', () => {
554
613
  expect(transaction.maxFeePerGas).toBe(hostedTransaction.maxFeePerGas)
555
614
  expect(transaction.calls).toEqual(hostedTransaction.calls)
556
615
  expect(transaction.feeToken).toBe(defaults.tokens.pathUsd)
557
- expect(transaction.feePayerSignature).toEqual(feePayerSignature)
616
+ expect(BigInt(transaction.feePayerSignature!.r)).toBe(realFeePayerSignature!.r)
617
+ expect(BigInt(transaction.feePayerSignature!.s)).toBe(realFeePayerSignature!.s)
558
618
  })
559
619
 
560
620
  test('error: requires hosted fee payer to return a feeToken', async () => {
@@ -566,6 +626,7 @@ describe('fillHostedFeePayerTransaction', () => {
566
626
  await expect(
567
627
  fillHostedFeePayerTransaction({
568
628
  allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
629
+ ...hostedContext,
569
630
  transaction: hostedTransaction as any,
570
631
  url: 'https://sponsor.example/tp_key',
571
632
  }),
@@ -588,6 +649,7 @@ describe('fillHostedFeePayerTransaction', () => {
588
649
  await expect(
589
650
  fillHostedFeePayerTransaction({
590
651
  allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
652
+ ...hostedContext,
591
653
  transaction: hostedTransaction as any,
592
654
  url: 'https://sponsor.example/tp_key',
593
655
  }),
@@ -608,11 +670,28 @@ describe('fillHostedFeePayerTransaction', () => {
608
670
  await expect(
609
671
  fillHostedFeePayerTransaction({
610
672
  allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
673
+ ...hostedContext,
611
674
  transaction: hostedTransaction as any,
612
675
  url: 'https://sponsor.example/tp_key',
613
676
  }),
614
677
  ).rejects.toThrow('Invalid or revoked API key')
615
678
  })
679
+
680
+ test('error: enforces sponsor policy before requesting hosted fill', async () => {
681
+ const fetchMock = vi.fn()
682
+ vi.stubGlobal('fetch', fetchMock)
683
+
684
+ await expect(
685
+ fillHostedFeePayerTransaction({
686
+ allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
687
+ ...hostedContext,
688
+ policy: { maxGas: hostedTransaction.gas - 1n },
689
+ transaction: hostedTransaction as any,
690
+ url: 'https://sponsor.example/tp_key',
691
+ }),
692
+ ).rejects.toThrow('gas exceeds sponsor policy')
693
+ expect(fetchMock).not.toHaveBeenCalled()
694
+ })
616
695
  })
617
696
 
618
697
  describe('simulationTransaction', () => {