mppx 0.13.4 → 0.13.6

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 (73) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/Credential.d.ts +3 -0
  3. package/dist/Credential.js +9 -0
  4. package/dist/Errors.d.ts +63 -2
  5. package/dist/Errors.js +52 -2
  6. package/dist/Method.d.ts +10 -0
  7. package/dist/cli/cli.js +92 -92
  8. package/dist/cli/plugins/evm.js +2 -2
  9. package/dist/cli/plugins/index.js +12 -12
  10. package/dist/client/Mppx.d.ts +2 -0
  11. package/dist/client/Mppx.js +8 -10
  12. package/dist/client/internal/Fetch.js +107 -8
  13. package/dist/client/internal/MethodChallenge.d.ts +2 -0
  14. package/dist/client/internal/MethodChallenge.js +4 -0
  15. package/dist/client/internal/MethodResponse.d.ts +6 -0
  16. package/dist/client/internal/MethodResponse.js +16 -0
  17. package/dist/client/internal/protocols/Mcp.js +36 -5
  18. package/dist/evm/server/Charge.js +3 -1
  19. package/dist/internal/version.d.ts +2 -2
  20. package/dist/internal/version.js +1 -1
  21. package/dist/internal/warnOnce.d.ts +3 -0
  22. package/dist/internal/warnOnce.js +9 -0
  23. package/dist/server/Mppx.d.ts +5 -0
  24. package/dist/server/Mppx.js +45 -18
  25. package/dist/server/Transport.js +10 -0
  26. package/dist/stripe/server/Methods.js +4 -1
  27. package/dist/tempo/Attribution.d.ts +24 -9
  28. package/dist/tempo/Attribution.js +25 -5
  29. package/dist/tempo/client/Charge.d.ts +116 -10
  30. package/dist/tempo/client/Charge.js +117 -22
  31. package/dist/tempo/client/Methods.d.ts +88 -9
  32. package/dist/tempo/index.d.ts +1 -0
  33. package/dist/tempo/index.js +1 -0
  34. package/dist/tempo/internal/auto-swap.d.ts +10 -0
  35. package/dist/tempo/internal/auto-swap.js +35 -8
  36. package/dist/tempo/internal/fee-payer.d.ts +8 -0
  37. package/dist/tempo/internal/fee-payer.js +20 -8
  38. package/dist/tempo/legacy/session/Chain.js +8 -2
  39. package/dist/tempo/server/Charge.d.ts +11 -0
  40. package/dist/tempo/server/Charge.js +160 -29
  41. package/dist/tempo/server/Methods.js +28 -5
  42. package/dist/tempo/server/Relay.js +6 -22
  43. package/dist/tempo/server/Subscription.js +5 -1
  44. package/dist/tempo/server/internal/fallback-store.d.ts +9 -0
  45. package/dist/tempo/server/internal/fallback-store.js +13 -0
  46. package/dist/tempo/server/internal/html.gen.js +1 -1
  47. package/dist/tempo/server/internal/transport.d.ts +3 -0
  48. package/dist/tempo/server/internal/transport.js +5 -1
  49. package/dist/tempo/session/client/ChannelOps.d.ts +2 -0
  50. package/dist/tempo/session/client/ChannelStore.d.ts +12 -1
  51. package/dist/tempo/session/client/ChannelStore.js +47 -0
  52. package/dist/tempo/session/client/CredentialState.js +8 -3
  53. package/dist/tempo/session/client/Session.js +50 -33
  54. package/dist/tempo/session/client/SessionManager.js +186 -65
  55. package/dist/tempo/session/client/Transports.d.ts +6 -0
  56. package/dist/tempo/session/client/Transports.js +52 -7
  57. package/dist/tempo/session/precompile/Chain.d.ts +2 -0
  58. package/dist/tempo/session/precompile/Chain.js +8 -3
  59. package/dist/tempo/session/server/ChannelStore.d.ts +34 -0
  60. package/dist/tempo/session/server/ChannelStore.js +69 -9
  61. package/dist/tempo/session/server/CredentialVerification.d.ts +7 -1
  62. package/dist/tempo/session/server/CredentialVerification.js +220 -48
  63. package/dist/tempo/session/server/MeteredStream.d.ts +6 -5
  64. package/dist/tempo/session/server/MeteredStream.js +10 -26
  65. package/dist/tempo/session/server/Session.d.ts +29 -2
  66. package/dist/tempo/session/server/Session.js +124 -44
  67. package/dist/tempo/session/server/Settlement.d.ts +9 -0
  68. package/dist/tempo/session/server/Settlement.js +23 -3
  69. package/dist/tempo/session/server/Sse.d.ts +4 -3
  70. package/dist/tempo/session/server/Sse.js +3 -3
  71. package/dist/tempo/session/server/Ws.d.ts +9 -1
  72. package/dist/tempo/session/server/Ws.js +1 -0
  73. package/package.json +1 -1
@@ -16,6 +16,8 @@
16
16
  *
17
17
  * The TAG prefix makes MPP transactions trivially distinguishable
18
18
  * from arbitrary memos via `TransferWithMemo` event topic filtering.
19
+ * The Tempo charge client uses the challenge `realm` as `serverId`, so
20
+ * `verifyServer(memo, realm)` matches on-chain transfers to a server.
19
21
  *
20
22
  * @module
21
23
  */
@@ -26,12 +28,12 @@ export declare const anonymous: '0x00000000000000000000';
26
28
  /**
27
29
  * Encodes an MPP attribution memo as a `bytes32` hex string.
28
30
  *
29
- * @param parameters - The serverId (server identity) and optional clientId.
31
+ * @param parameters - The challengeId, serverId (server identity), and optional clientId.
30
32
  * @returns A `0x`-prefixed 64-char hex string (32 bytes).
31
33
  *
32
34
  * @example
33
35
  * ```ts
34
- * import * as Attribution from './Attribution.js'
36
+ * import { Attribution } from 'mppx/tempo'
35
37
  *
36
38
  * const memo = Attribution.encode({ challengeId: 'challenge-123', clientId: 'my-app', serverId: 'api.example.com' })
37
39
  * ```
@@ -55,7 +57,7 @@ export declare namespace encode {
55
57
  *
56
58
  * @example
57
59
  * ```ts
58
- * import * as Attribution from './Attribution.js'
60
+ * import { Attribution } from 'mppx/tempo'
59
61
  *
60
62
  * Attribution.isMppMemo(Attribution.encode({ challengeId: 'challenge-123', serverId: 'api.example.com' })) // true
61
63
  * Attribution.isMppMemo('0x0000...0000') // false
@@ -70,6 +72,15 @@ export declare function isMppMemo(memo: `0x${string}`): boolean;
70
72
  * @param memo - A `0x`-prefixed hex string (bytes32).
71
73
  * @param serverId - The expected server identity.
72
74
  * @returns `true` if the memo has a valid MPP tag and matching server fingerprint.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * import { Attribution } from 'mppx/tempo'
79
+ *
80
+ * const memo = Attribution.encode({ challengeId: 'challenge-123', serverId: 'api.example.com' })
81
+ * Attribution.verifyServer(memo, 'api.example.com') // true
82
+ * Attribution.verifyServer(memo, 'other.example.com') // false
83
+ * ```
73
84
  */
74
85
  export declare function verifyServer(memo: `0x${string}`, serverId: string): boolean;
75
86
  /**
@@ -80,7 +91,7 @@ export declare function verifyServer(memo: `0x${string}`, serverId: string): boo
80
91
  *
81
92
  * @example
82
93
  * ```ts
83
- * import * as Attribution from './Attribution.js'
94
+ * import { Attribution } from 'mppx/tempo'
84
95
  *
85
96
  * const memo = Attribution.encode({ challengeId: 'challenge-123', clientId: 'my-app', serverId: 'api.example.com' })
86
97
  * const decoded = Attribution.decode(memo)
@@ -100,11 +111,6 @@ export declare namespace decode {
100
111
  nonce: `0x${string}`;
101
112
  };
102
113
  }
103
- /**
104
- * Computes the 7-byte challenge-bound nonce: keccak256(challengeId)[0..6].
105
- * @internal
106
- */
107
- export declare function challengeNonce(challengeId: string): Uint8Array;
108
114
  /**
109
115
  * Verifies that a memo's nonce is bound to the given challengeId.
110
116
  *
@@ -113,6 +119,15 @@ export declare function challengeNonce(challengeId: string): Uint8Array;
113
119
  * @param memo - A `0x`-prefixed hex string (bytes32).
114
120
  * @param challengeId - The expected challenge ID.
115
121
  * @returns `true` if the memo has a valid MPP tag and matching challenge nonce.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * import { Attribution } from 'mppx/tempo'
126
+ *
127
+ * const memo = Attribution.encode({ challengeId: 'challenge-123', serverId: 'api.example.com' })
128
+ * Attribution.verifyChallengeBinding(memo, 'challenge-123') // true
129
+ * Attribution.verifyChallengeBinding(memo, 'challenge-456') // false
130
+ * ```
116
131
  */
117
132
  export declare function verifyChallengeBinding(memo: `0x${string}`, challengeId: string): boolean;
118
133
  //# sourceMappingURL=Attribution.d.ts.map
@@ -17,6 +17,8 @@ import { Bytes, Hash, Hex } from 'ox';
17
17
  *
18
18
  * The TAG prefix makes MPP transactions trivially distinguishable
19
19
  * from arbitrary memos via `TransferWithMemo` event topic filtering.
20
+ * The Tempo charge client uses the challenge `realm` as `serverId`, so
21
+ * `verifyServer(memo, realm)` matches on-chain transfers to a server.
20
22
  *
21
23
  * @module
22
24
  */
@@ -37,12 +39,12 @@ function fingerprint(value) {
37
39
  /**
38
40
  * Encodes an MPP attribution memo as a `bytes32` hex string.
39
41
  *
40
- * @param parameters - The serverId (server identity) and optional clientId.
42
+ * @param parameters - The challengeId, serverId (server identity), and optional clientId.
41
43
  * @returns A `0x`-prefixed 64-char hex string (32 bytes).
42
44
  *
43
45
  * @example
44
46
  * ```ts
45
- * import * as Attribution from './Attribution.js'
47
+ * import { Attribution } from 'mppx/tempo'
46
48
  *
47
49
  * const memo = Attribution.encode({ challengeId: 'challenge-123', clientId: 'my-app', serverId: 'api.example.com' })
48
50
  * ```
@@ -69,7 +71,7 @@ export function encode(parameters) {
69
71
  *
70
72
  * @example
71
73
  * ```ts
72
- * import * as Attribution from './Attribution.js'
74
+ * import { Attribution } from 'mppx/tempo'
73
75
  *
74
76
  * Attribution.isMppMemo(Attribution.encode({ challengeId: 'challenge-123', serverId: 'api.example.com' })) // true
75
77
  * Attribution.isMppMemo('0x0000...0000') // false
@@ -90,6 +92,15 @@ export function isMppMemo(memo) {
90
92
  * @param memo - A `0x`-prefixed hex string (bytes32).
91
93
  * @param serverId - The expected server identity.
92
94
  * @returns `true` if the memo has a valid MPP tag and matching server fingerprint.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * import { Attribution } from 'mppx/tempo'
99
+ *
100
+ * const memo = Attribution.encode({ challengeId: 'challenge-123', serverId: 'api.example.com' })
101
+ * Attribution.verifyServer(memo, 'api.example.com') // true
102
+ * Attribution.verifyServer(memo, 'other.example.com') // false
103
+ * ```
93
104
  */
94
105
  export function verifyServer(memo, serverId) {
95
106
  if (!isMppMemo(memo))
@@ -106,7 +117,7 @@ export function verifyServer(memo, serverId) {
106
117
  *
107
118
  * @example
108
119
  * ```ts
109
- * import * as Attribution from './Attribution.js'
120
+ * import { Attribution } from 'mppx/tempo'
110
121
  *
111
122
  * const memo = Attribution.encode({ challengeId: 'challenge-123', clientId: 'my-app', serverId: 'api.example.com' })
112
123
  * const decoded = Attribution.decode(memo)
@@ -127,7 +138,7 @@ export function decode(memo) {
127
138
  * Computes the 7-byte challenge-bound nonce: keccak256(challengeId)[0..6].
128
139
  * @internal
129
140
  */
130
- export function challengeNonce(challengeId) {
141
+ function challengeNonce(challengeId) {
131
142
  const hash = Hash.keccak256(Bytes.fromString(challengeId), { as: 'Hex' });
132
143
  return Hex.toBytes(Hex.slice(hash, 0, 7));
133
144
  }
@@ -139,6 +150,15 @@ export function challengeNonce(challengeId) {
139
150
  * @param memo - A `0x`-prefixed hex string (bytes32).
140
151
  * @param challengeId - The expected challenge ID.
141
152
  * @returns `true` if the memo has a valid MPP tag and matching challenge nonce.
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * import { Attribution } from 'mppx/tempo'
157
+ *
158
+ * const memo = Attribution.encode({ challengeId: 'challenge-123', serverId: 'api.example.com' })
159
+ * Attribution.verifyChallengeBinding(memo, 'challenge-123') // true
160
+ * Attribution.verifyChallengeBinding(memo, 'challenge-456') // false
161
+ * ```
142
162
  */
143
163
  export function verifyChallengeBinding(memo, challengeId) {
144
164
  const decoded = decode(memo);
@@ -1,4 +1,4 @@
1
- import type { Address } from 'viem';
1
+ import { type Address } from 'viem';
2
2
  import * as Method from '../../Method.js';
3
3
  import * as Account from '../../viem/Account.js';
4
4
  import * as Client from '../../viem/Client.js';
@@ -25,7 +25,16 @@ export type ChargeContext = {
25
25
  * })
26
26
  * ```
27
27
  */
28
- export declare function charge(parameters?: charge.Parameters): Method.Client<{
28
+ export declare function charge(parameters?: charge.Parameters): {
29
+ readonly canHandleChallenge?: Method.CanHandleChallengeFn | undefined;
30
+ readonly context?: z.ZodMiniObject<{
31
+ account: z.ZodMiniOptional<z.ZodMiniCustom<`0x${string}` | Account.Account | undefined, `0x${string}` | Account.Account | undefined>>;
32
+ autoSwap: z.ZodMiniOptional<z.ZodMiniCustom<AutoSwap.resolve.Value | undefined, AutoSwap.resolve.Value | undefined>>;
33
+ mode: z.ZodMiniOptional<z.ZodMiniEnum<{
34
+ pull: "pull";
35
+ push: "push";
36
+ }>>;
37
+ }, z.core.$strip> | undefined;
29
38
  readonly name: "tempo";
30
39
  readonly intent: "charge";
31
40
  readonly schema: {
@@ -97,14 +106,84 @@ export declare function charge(parameters?: charge.Parameters): Method.Client<{
97
106
  supportedModes?: ("pull" | "push")[] | undefined;
98
107
  }>>;
99
108
  };
100
- }, z.ZodMiniObject<{
101
- account: z.ZodMiniOptional<z.ZodMiniCustom<`0x${string}` | Account.Account | undefined, `0x${string}` | Account.Account | undefined>>;
102
- autoSwap: z.ZodMiniOptional<z.ZodMiniCustom<AutoSwap.resolve.Value | undefined, AutoSwap.resolve.Value | undefined>>;
103
- mode: z.ZodMiniOptional<z.ZodMiniEnum<{
104
- pull: "pull";
105
- push: "push";
106
- }>>;
107
- }, z.core.$strip>>;
109
+ readonly createCredential: Method.CreateCredentialFn<{
110
+ readonly name: "tempo";
111
+ readonly intent: "charge";
112
+ readonly schema: {
113
+ credential: {
114
+ payload: z.ZodMiniDiscriminatedUnion<[z.ZodMiniObject<{
115
+ hash: z.ZodMiniString<string>;
116
+ type: z.ZodMiniLiteral<"hash">;
117
+ }, z.core.$strip>, z.ZodMiniObject<{
118
+ signature: z.ZodMiniString<string>;
119
+ type: z.ZodMiniLiteral<"transaction">;
120
+ }, z.core.$strip>, z.ZodMiniObject<{
121
+ signature: z.ZodMiniString<string>;
122
+ type: z.ZodMiniLiteral<"proof">;
123
+ }, z.core.$strip>], "type">;
124
+ };
125
+ request: z.ZodMiniPipe<z.ZodMiniObject<{
126
+ machineTokenEnabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
127
+ amount: z.ZodMiniString<string>;
128
+ chainId: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
129
+ currency: z.ZodMiniString<string>;
130
+ decimals: z.ZodMiniNumber<number>;
131
+ description: z.ZodMiniOptional<z.ZodMiniString<string>>;
132
+ externalId: z.ZodMiniOptional<z.ZodMiniString<string>>;
133
+ feePayer: z.ZodMiniOptional<z.ZodMiniPipe<z.ZodMiniUnion<readonly [z.ZodMiniBoolean<boolean>, z.ZodMiniCustom<import("viem").Account, import("viem").Account>]>, z.ZodMiniTransform<boolean, boolean | import("viem").Account>>>;
134
+ memo: z.ZodMiniOptional<z.ZodMiniNever>;
135
+ recipient: z.ZodMiniOptional<z.ZodMiniString<string>>;
136
+ splits: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
137
+ amount: z.ZodMiniString<string>;
138
+ memo: z.ZodMiniOptional<z.ZodMiniString<string>>;
139
+ recipient: z.ZodMiniPipe<z.ZodMiniString<string>, z.ZodMiniTransform<`0x${string}`, string>>;
140
+ }, z.core.$strip>>>;
141
+ supportedModes: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniEnum<{
142
+ pull: "pull";
143
+ push: "push";
144
+ }>>>;
145
+ }, z.core.$strip>, z.ZodMiniTransform<{
146
+ currency: string;
147
+ description?: string | undefined;
148
+ externalId?: string | undefined;
149
+ recipient?: string | undefined;
150
+ amount: string;
151
+ methodDetails?: {
152
+ machineTokenEnabled?: boolean | undefined;
153
+ chainId?: number | undefined;
154
+ feePayer?: boolean | undefined;
155
+ splits?: {
156
+ memo?: string | undefined;
157
+ recipient: `0x${string}`;
158
+ amount: string;
159
+ }[] | undefined;
160
+ supportedModes?: ("pull" | "push")[] | undefined;
161
+ } | undefined;
162
+ }, {
163
+ machineTokenEnabled?: boolean | undefined;
164
+ amount: string;
165
+ chainId?: number | undefined;
166
+ currency: string;
167
+ decimals: number;
168
+ description?: string | undefined;
169
+ externalId?: string | undefined;
170
+ feePayer?: boolean | undefined;
171
+ memo?: undefined;
172
+ recipient?: string | undefined;
173
+ splits?: {
174
+ amount: string;
175
+ memo?: string | undefined;
176
+ recipient: `0x${string}`;
177
+ }[] | undefined;
178
+ supportedModes?: ("pull" | "push")[] | undefined;
179
+ }>>;
180
+ };
181
+ }, {
182
+ account?: `0x${string}` | Account.Account | undefined;
183
+ autoSwap?: AutoSwap.resolve.Value | undefined;
184
+ mode?: "pull" | "push" | undefined;
185
+ }>;
186
+ };
108
187
  export declare namespace charge {
109
188
  type AutoSwap = AutoSwap.resolve.Value;
110
189
  type Context = ChargeContext;
@@ -137,6 +216,33 @@ export declare namespace charge {
137
216
  * recipient and every split recipient must be included in this list.
138
217
  */
139
218
  expectedRecipients?: readonly Address[] | undefined;
219
+ /**
220
+ * Allowlist of TIP-20 currency addresses this client will pay in, compared
221
+ * case-insensitively. Offers in other currencies are skipped during selection
222
+ * and rejected before signing; an empty list rejects all. Required with
223
+ * `maxAmount` or `maxAtomicAmount`, which are denominated in the challenge currency.
224
+ */
225
+ currencies?: readonly Address[] | undefined;
226
+ /**
227
+ * Decimals of the `currencies`, used to parse `maxAmount`. Required with
228
+ * `maxAmount` because challenges do not carry currency decimals.
229
+ */
230
+ decimals?: number | undefined;
231
+ /**
232
+ * Maximum display-unit amount (e.g. `'1.5'`) the client is willing to pay per
233
+ * charge, parsed with `decimals`. Values with more fractional digits than
234
+ * `decimals` are rejected rather than rounded. Compared against the challenge
235
+ * amount, which includes splits; larger offers are skipped during selection and
236
+ * rejected before signing. Applies per credential, not cumulatively across
237
+ * payment retries. Requires `currencies`.
238
+ */
239
+ maxAmount?: string | undefined;
240
+ /**
241
+ * Maximum amount the client is willing to pay per charge, in raw base units of
242
+ * the challenge currency (compared directly with the challenge `amount`).
243
+ * Checked like `maxAmount`. Requires `currencies`.
244
+ */
245
+ maxAtomicAmount?: string | undefined;
140
246
  /**
141
247
  * Controls how the charge transaction is submitted.
142
248
  *
@@ -1,6 +1,9 @@
1
+ import { TxEnvelopeTempo } from 'ox/tempo';
2
+ import { keccak256, parseUnits } from 'viem';
1
3
  import { prepareTransactionRequest, sendCallsSync, sendTransactionSync, signTypedData, signTransaction, } from 'viem/actions';
2
4
  import { Actions } from 'viem/tempo';
3
5
  import { tempo as tempo_chain } from 'viem/tempo/chains';
6
+ import * as MethodResponse from '../../client/internal/MethodResponse.js';
4
7
  import * as Credential from '../../Credential.js';
5
8
  import * as Method from '../../Method.js';
6
9
  import * as Account from '../../viem/Account.js';
@@ -34,24 +37,36 @@ const chargeContextSchema = z.object({
34
37
  */
35
38
  export function charge(parameters = {}) {
36
39
  const { clientId } = parameters;
40
+ // Caps are denominated in the challenge currency, so they require a currency pin.
41
+ if ((parameters.maxAmount !== undefined || parameters.maxAtomicAmount !== undefined) &&
42
+ parameters.currencies === undefined)
43
+ throw new Error('Tempo charge maxAmount and maxAtomicAmount require currencies.');
44
+ const caps = {
45
+ maxAmount: parseMaxAmount(parameters.maxAmount, parameters.decimals),
46
+ maxAtomicAmount: parameters.maxAtomicAmount === undefined ? undefined : BigInt(parameters.maxAtomicAmount),
47
+ };
37
48
  const getClient = Client.getResolver({
38
49
  chain: tempo_chain,
39
50
  getClient: parameters.getClient,
40
51
  rpcUrl: defaults.rpcUrl,
41
52
  });
42
53
  const getAccount = Account.getResolver({ account: parameters.account });
43
- return Method.toClient(Methods.charge, {
54
+ const method = Method.toClient(Methods.charge, {
55
+ // Skip offers that violate client policy so an acceptable offer can be chosen.
56
+ canHandleChallenge({ challenge }) {
57
+ try {
58
+ assertChallengePolicy(parameters, caps, challenge.request);
59
+ return true;
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ },
44
65
  context: chargeContextSchema,
45
66
  async createCredential({ challenge, context }) {
46
- // Chain pinning: reject a challenge whose chain ID conflicts with the
47
- // pinned one, and sign on the pin when the challenge omits a chain ID.
67
+ assertChallengePolicy(parameters, caps, challenge.request);
68
+ // Sign on the pinned chain when the challenge omits a chain ID.
48
69
  const challengeChainId = challenge.request.methodDetails?.chainId;
49
- if (parameters.expectedChainId !== undefined &&
50
- challengeChainId !== undefined &&
51
- challengeChainId !== parameters.expectedChainId)
52
- throw new Error(`Chain ID mismatch: expected ${parameters.expectedChainId}, got ${challengeChainId}.`);
53
- if (challengeChainId !== undefined || parameters.allowedChainIds?.length === 0)
54
- Client.assertAllowedChainId(parameters.allowedChainIds, challengeChainId);
55
70
  const resolvedChainId = challengeChainId ??
56
71
  parameters.expectedChainId ??
57
72
  (parameters.allowedChainIds?.length === 1 ? parameters.allowedChainIds[0] : undefined);
@@ -70,18 +85,6 @@ export function charge(parameters = {}) {
70
85
  chainId,
71
86
  operation,
72
87
  })) ?? defaultAccount;
73
- if (parameters.expectedRecipients) {
74
- const allowed = new Set(parameters.expectedRecipients.map((a) => a.toLowerCase()));
75
- if (!request.recipient || !allowed.has(request.recipient.toLowerCase()))
76
- throw new Error(`Unexpected primary recipient: ${request.recipient}`);
77
- const splits = methodDetails?.splits;
78
- if (splits) {
79
- for (const split of splits) {
80
- if (!allowed.has(split.recipient.toLowerCase()))
81
- throw new Error(`Unexpected split recipient: ${split.recipient}`);
82
- }
83
- }
84
- }
85
88
  // Zero-amount: sign EIP-712 typed data instead of creating a transaction.
86
89
  if (BigInt(amount) === 0n) {
87
90
  const account = await resolveAccount({ kind: 'signPaymentProof' });
@@ -168,6 +171,7 @@ export function charge(parameters = {}) {
168
171
  tokenOut: currency,
169
172
  tokenIn: autoSwap.tokenIn,
170
173
  slippage: autoSwap.slippage,
174
+ maxAmountIn: autoSwap.maxAmountIn,
171
175
  })
172
176
  : undefined;
173
177
  }
@@ -198,7 +202,9 @@ export function charge(parameters = {}) {
198
202
  const defaultExpiry = Math.floor(Date.now() / 1000) + 25;
199
203
  if (!challenge.expires)
200
204
  return defaultExpiry;
201
- const challengeExpiry = Math.floor(new Date(challenge.expires).getTime() / 1000);
205
+ // `validBefore` is an exclusive whole-second bound, so cap at the second
206
+ // after a (possibly fractional) expiry to stay valid as long as the challenge.
207
+ const challengeExpiry = Math.floor(new Date(challenge.expires).getTime() / 1000) + 1;
202
208
  return Math.min(defaultExpiry, challengeExpiry);
203
209
  })();
204
210
  if (mode === 'push') {
@@ -254,5 +260,94 @@ export function charge(parameters = {}) {
254
260
  });
255
261
  },
256
262
  });
263
+ // Share an in-flight signing so concurrent calls for one challenge cannot yield
264
+ // two independently settleable transactions. Settled signings are not reused.
265
+ const signing = new Map();
266
+ const createCredential = async (options) => {
267
+ // Enforce client policy on every call, including ones joining a signing.
268
+ assertChallengePolicy(parameters, caps, options.challenge.request);
269
+ const account = options.context?.account ?? parameters.account;
270
+ const signer = (typeof account === 'string' ? account : account?.address)?.toLowerCase();
271
+ const key = `${signer ?? ''}:${options.challenge.id}`;
272
+ let promise = signing.get(key);
273
+ if (!promise) {
274
+ promise = method.createCredential(options).finally(() => signing.delete(key));
275
+ signing.set(key, promise);
276
+ }
277
+ return promise;
278
+ };
279
+ return MethodResponse.registerTransactionHash({ ...method, createCredential }, transactionHash);
280
+ }
281
+ /**
282
+ * Returns the final transaction hash of a credential, when known before settlement.
283
+ *
284
+ * Push credentials carry the broadcast hash; pull credentials hash a canonical,
285
+ * sender-signed 0x76 transaction. Sponsored transactions (0x78 envelopes or 0x76
286
+ * awaiting a fee payer) return `undefined` because the fee payer's co-signature
287
+ * changes the final hash.
288
+ */
289
+ function transactionHash(credential) {
290
+ const parsed = Methods.charge.schema.credential.payload.safeParse(Credential.deserialize(credential).payload);
291
+ if (!parsed.success)
292
+ return undefined;
293
+ const payload = parsed.data;
294
+ if (payload.type === 'hash')
295
+ return payload.hash;
296
+ const serialized = payload.signature;
297
+ if (payload.type !== 'transaction' || !serialized.startsWith(TxEnvelopeTempo.serializedType))
298
+ return undefined;
299
+ const transaction = TxEnvelopeTempo.deserialize(serialized);
300
+ if (!transaction.signature || transaction.feePayerSignature === null)
301
+ return undefined;
302
+ // Reject trailing or non-canonical bytes, which never identify an on-chain transaction.
303
+ if (TxEnvelopeTempo.serialize(transaction) !== serialized)
304
+ return undefined;
305
+ return keccak256(serialized);
306
+ }
307
+ /** Parses `maxAmount` to base units, rejecting values that cannot be represented exactly. */
308
+ function parseMaxAmount(maxAmount, decimals) {
309
+ if (maxAmount === undefined)
310
+ return undefined;
311
+ if (decimals === undefined)
312
+ throw new Error('Tempo charge maxAmount requires currency decimals.');
313
+ // `parseUnits` rounds excess fractional digits, which could loosen the cap.
314
+ const fraction = maxAmount.split('.')[1]?.replace(/0+$/, '') ?? '';
315
+ if (fraction.length > decimals)
316
+ throw new Error(`Tempo charge maxAmount ${maxAmount} has more fractional digits than ${decimals} decimals allow.`);
317
+ return parseUnits(maxAmount, decimals);
318
+ }
319
+ /**
320
+ * Rejects a challenge whose chain, currency, recipients, or amount violate the client policy.
321
+ * Runs during challenge selection and again before any client, account, or signing work.
322
+ */
323
+ function assertChallengePolicy(parameters, caps, request) {
324
+ const { amount, methodDetails, recipient } = request;
325
+ // Chain pinning: reject a challenge whose chain ID conflicts with the pinned one.
326
+ const challengeChainId = methodDetails?.chainId;
327
+ if (parameters.expectedChainId !== undefined &&
328
+ challengeChainId !== undefined &&
329
+ challengeChainId !== parameters.expectedChainId)
330
+ throw new Error(`Chain ID mismatch: expected ${parameters.expectedChainId}, got ${challengeChainId}.`);
331
+ if (challengeChainId !== undefined || parameters.allowedChainIds?.length === 0)
332
+ Client.assertAllowedChainId(parameters.allowedChainIds, challengeChainId);
333
+ if (parameters.currencies &&
334
+ !parameters.currencies.some((c) => c.toLowerCase() === request.currency.toLowerCase()))
335
+ throw new Error(`Tempo charge currency is not allowed: ${request.currency}.`);
336
+ if (parameters.expectedRecipients) {
337
+ const allowed = new Set(parameters.expectedRecipients.map((a) => a.toLowerCase()));
338
+ if (!recipient || !allowed.has(recipient.toLowerCase()))
339
+ throw new Error(`Unexpected primary recipient: ${recipient}`);
340
+ const splits = methodDetails?.splits;
341
+ if (splits) {
342
+ for (const split of splits) {
343
+ if (!allowed.has(split.recipient.toLowerCase()))
344
+ throw new Error(`Unexpected split recipient: ${split.recipient}`);
345
+ }
346
+ }
347
+ }
348
+ if (caps.maxAtomicAmount !== undefined && BigInt(amount) > caps.maxAtomicAmount)
349
+ throw new Error('Tempo charge amount exceeds maxAtomicAmount.');
350
+ if (caps.maxAmount !== undefined && BigInt(amount) > caps.maxAmount)
351
+ throw new Error('Tempo charge amount exceeds maxAmount.');
257
352
  }
258
353
  //# sourceMappingURL=Charge.js.map
@@ -19,7 +19,16 @@ export { sessionClient as session };
19
19
  * })
20
20
  * ```
21
21
  */
22
- export declare function tempo(parameters?: tempo.Parameters): readonly [import("../../Method.js").Client<{
22
+ export declare function tempo(parameters?: tempo.Parameters): readonly [{
23
+ readonly canHandleChallenge?: import("../../Method.js").CanHandleChallengeFn | undefined;
24
+ readonly context?: import("zod/mini").ZodMiniObject<{
25
+ account: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniCustom<`0x${string}` | import("../../viem/Account.js").Account | undefined, `0x${string}` | import("../../viem/Account.js").Account | undefined>>;
26
+ autoSwap: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniCustom<import("../internal/auto-swap.js").resolve.Value | undefined, import("../internal/auto-swap.js").resolve.Value | undefined>>;
27
+ mode: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniEnum<{
28
+ pull: "pull";
29
+ push: "push";
30
+ }>>;
31
+ }, import("zod/v4/core").$strip> | undefined;
23
32
  readonly name: "tempo";
24
33
  readonly intent: "charge";
25
34
  readonly schema: {
@@ -91,14 +100,84 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [import("
91
100
  supportedModes?: ("pull" | "push")[] | undefined;
92
101
  }>>;
93
102
  };
94
- }, import("zod/mini").ZodMiniObject<{
95
- account: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniCustom<`0x${string}` | import("../../viem/Account.js").Account | undefined, `0x${string}` | import("../../viem/Account.js").Account | undefined>>;
96
- autoSwap: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniCustom<import("../internal/auto-swap.js").resolve.Value | undefined, import("../internal/auto-swap.js").resolve.Value | undefined>>;
97
- mode: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniEnum<{
98
- pull: "pull";
99
- push: "push";
100
- }>>;
101
- }, import("zod/v4/core").$strip>>, import("../../Method.js").Client<{
103
+ readonly createCredential: import("../../Method.js").CreateCredentialFn<{
104
+ readonly name: "tempo";
105
+ readonly intent: "charge";
106
+ readonly schema: {
107
+ credential: {
108
+ payload: import("zod/mini").ZodMiniDiscriminatedUnion<[import("zod/mini").ZodMiniObject<{
109
+ hash: import("zod/mini").ZodMiniString<string>;
110
+ type: import("zod/mini").ZodMiniLiteral<"hash">;
111
+ }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
112
+ signature: import("zod/mini").ZodMiniString<string>;
113
+ type: import("zod/mini").ZodMiniLiteral<"transaction">;
114
+ }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
115
+ signature: import("zod/mini").ZodMiniString<string>;
116
+ type: import("zod/mini").ZodMiniLiteral<"proof">;
117
+ }, import("zod/v4/core").$strip>], "type">;
118
+ };
119
+ request: import("zod/mini").ZodMiniPipe<import("zod/mini").ZodMiniObject<{
120
+ machineTokenEnabled: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniBoolean<boolean>>;
121
+ amount: import("zod/mini").ZodMiniString<string>;
122
+ chainId: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniNumber<number>>;
123
+ currency: import("zod/mini").ZodMiniString<string>;
124
+ decimals: import("zod/mini").ZodMiniNumber<number>;
125
+ description: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniString<string>>;
126
+ externalId: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniString<string>>;
127
+ feePayer: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniPipe<import("zod/mini").ZodMiniUnion<readonly [import("zod/mini").ZodMiniBoolean<boolean>, import("zod/mini").ZodMiniCustom<import("viem").Account, import("viem").Account>]>, import("zod/mini").ZodMiniTransform<boolean, boolean | import("viem").Account>>>;
128
+ memo: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniNever>;
129
+ recipient: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniString<string>>;
130
+ splits: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniArray<import("zod/mini").ZodMiniObject<{
131
+ amount: import("zod/mini").ZodMiniString<string>;
132
+ memo: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniString<string>>;
133
+ recipient: import("zod/mini").ZodMiniPipe<import("zod/mini").ZodMiniString<string>, import("zod/mini").ZodMiniTransform<`0x${string}`, string>>;
134
+ }, import("zod/v4/core").$strip>>>;
135
+ supportedModes: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniArray<import("zod/mini").ZodMiniEnum<{
136
+ pull: "pull";
137
+ push: "push";
138
+ }>>>;
139
+ }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniTransform<{
140
+ currency: string;
141
+ description?: string | undefined;
142
+ externalId?: string | undefined;
143
+ recipient?: string | undefined;
144
+ amount: string;
145
+ methodDetails?: {
146
+ machineTokenEnabled?: boolean | undefined;
147
+ chainId?: number | undefined;
148
+ feePayer?: boolean | undefined;
149
+ splits?: {
150
+ memo?: string | undefined;
151
+ recipient: `0x${string}`;
152
+ amount: string;
153
+ }[] | undefined;
154
+ supportedModes?: ("pull" | "push")[] | undefined;
155
+ } | undefined;
156
+ }, {
157
+ machineTokenEnabled?: boolean | undefined;
158
+ amount: string;
159
+ chainId?: number | undefined;
160
+ currency: string;
161
+ decimals: number;
162
+ description?: string | undefined;
163
+ externalId?: string | undefined;
164
+ feePayer?: boolean | undefined;
165
+ memo?: undefined;
166
+ recipient?: string | undefined;
167
+ splits?: {
168
+ amount: string;
169
+ memo?: string | undefined;
170
+ recipient: `0x${string}`;
171
+ }[] | undefined;
172
+ supportedModes?: ("pull" | "push")[] | undefined;
173
+ }>>;
174
+ };
175
+ }, {
176
+ account?: `0x${string}` | import("../../viem/Account.js").Account | undefined;
177
+ autoSwap?: import("../internal/auto-swap.js").resolve.Value | undefined;
178
+ mode?: "pull" | "push" | undefined;
179
+ }>;
180
+ }, import("../../Method.js").Client<{
102
181
  readonly name: "tempo";
103
182
  readonly intent: "session";
104
183
  readonly schema: {
@@ -1,3 +1,4 @@
1
+ export * as Attribution from './Attribution.js';
1
2
  export * as Proof from './Proof.js';
2
3
  export * as Methods from './Methods.js';
3
4
  export * as Session from './session/index.js';
@@ -1,3 +1,4 @@
1
+ export * as Attribution from './Attribution.js';
1
2
  export * as Proof from './Proof.js';
2
3
  export * as Methods from './Methods.js';
3
4
  export * as Session from './session/index.js';
@@ -17,6 +17,8 @@ export declare namespace findCalls {
17
17
  amountOut: bigint;
18
18
  /** Candidate input tokens to swap from, in priority order. */
19
19
  tokenIn: readonly Address[];
20
+ /** Maximum input-token units to spend. Defaults to the missing output amount plus slippage (1:1 units). */
21
+ maxAmountIn?: bigint | undefined;
20
22
  /** Max slippage tolerance as a percentage (e.g. `1` = 1%). */
21
23
  slippage: number;
22
24
  /** Address of the target token to acquire. */
@@ -29,8 +31,15 @@ export declare namespace findCalls {
29
31
  export declare function resolve(value: resolve.Value | undefined, defaultCurrencies: readonly Address[]): resolve.Resolved | false;
30
32
  export declare namespace resolve {
31
33
  type Options = {
34
+ /** Maximum input-token units per swap. Defaults to parity with the missing output amount plus slippage; set explicitly for non-parity pairs. */
35
+ maxAmountIn?: bigint | undefined;
32
36
  /** Fallback tokens to try swapping from, in priority order. */
33
37
  tokenIn?: Address[] | undefined;
38
+ /**
39
+ * Whether to also try the default currencies after `tokenIn`. Set `false` to swap only from
40
+ * `tokenIn`, as an allowlist. Ignored without `tokenIn`. @default true
41
+ */
42
+ fallback?: boolean | undefined;
34
43
  /** Max slippage tolerance as a percentage (e.g. `1` = 1%). @default 1 */
35
44
  slippage?: number | undefined;
36
45
  };
@@ -38,6 +47,7 @@ export declare namespace resolve {
38
47
  type Resolved = {
39
48
  tokenIn: readonly Address[];
40
49
  slippage: number;
50
+ maxAmountIn?: bigint | undefined;
41
51
  };
42
52
  }
43
53
  export declare class InsufficientFundsError extends Error {