mppx 0.10.1 → 0.12.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 (56) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +22 -1
  3. package/dist/cli/agent.d.ts +3 -0
  4. package/dist/cli/agent.js +20 -0
  5. package/dist/cli/cli.d.ts +1 -1
  6. package/dist/cli/cli.js +135 -135
  7. package/dist/cli/plugins/index.js +12 -12
  8. package/dist/cli/validate/payment.js +29 -12
  9. package/dist/client/internal/protocols/X402.js +0 -2
  10. package/dist/evm/server/Methods.d.ts +164 -3
  11. package/dist/evm/server/Methods.js +50 -2
  12. package/dist/internal/version.d.ts +2 -2
  13. package/dist/internal/version.js +1 -1
  14. package/dist/server/Mppx.d.ts +5 -1
  15. package/dist/server/Mppx.js +120 -49
  16. package/dist/server/internal/html/compose.main.gen.d.ts +1 -1
  17. package/dist/server/internal/html/serviceWorker.gen.d.ts +1 -1
  18. package/dist/stripe/server/Methods.js +13 -7
  19. package/dist/stripe/server/internal/analytics.d.ts +1 -0
  20. package/dist/stripe/server/internal/analytics.js +2 -2
  21. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  22. package/dist/stripe/server/internal/record-payment.js +2 -2
  23. package/dist/tempo/Methods.d.ts +5 -3
  24. package/dist/tempo/Methods.js +6 -5
  25. package/dist/tempo/client/Charge.d.ts +6 -4
  26. package/dist/tempo/client/Charge.js +61 -28
  27. package/dist/tempo/client/Methods.d.ts +5 -3
  28. package/dist/tempo/index.d.ts +2 -1
  29. package/dist/tempo/index.js +2 -1
  30. package/dist/tempo/internal/currencies.d.ts +19 -0
  31. package/dist/tempo/internal/currencies.js +41 -0
  32. package/dist/tempo/internal/defaults.d.ts +16 -3
  33. package/dist/tempo/internal/defaults.js +17 -2
  34. package/dist/tempo/internal/fee-payer.d.ts +5 -4
  35. package/dist/tempo/internal/fee-payer.js +7 -5
  36. package/dist/tempo/internal/machine-token-charge.d.ts +56 -0
  37. package/dist/tempo/internal/machine-token-charge.js +108 -0
  38. package/dist/tempo/server/Charge.d.ts +15 -1
  39. package/dist/tempo/server/Charge.js +101 -67
  40. package/dist/tempo/server/Methods.d.ts +5008 -40
  41. package/dist/tempo/server/Methods.js +105 -11
  42. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  43. package/dist/tempo/server/internal/html.gen.js +1 -1
  44. package/dist/tempo/session/server/ChannelStore.d.ts +2 -0
  45. package/dist/tempo/session/server/ChannelStore.js +12 -3
  46. package/dist/tempo/session/server/CredentialVerification.d.ts +2 -0
  47. package/dist/tempo/session/server/CredentialVerification.js +22 -13
  48. package/dist/tempo/session/server/RequestState.d.ts +6 -0
  49. package/dist/tempo/session/server/RequestState.js +11 -4
  50. package/dist/tempo/session/server/Session.js +8 -1
  51. package/dist/tempo/session/server/Settlement.d.ts +2 -0
  52. package/dist/tempo/session/server/Settlement.js +13 -8
  53. package/dist/tempo/subscription/KeyAuthorization.d.ts +70 -8
  54. package/package.json +3 -3
  55. package/dist/tempo/Tokens.d.ts +0 -13
  56. package/dist/tempo/Tokens.js +0 -13
@@ -15,6 +15,7 @@ import * as Charge_internal from '../internal/charge.js';
15
15
  import * as defaults from '../internal/defaults.js';
16
16
  import * as FeePayer from '../internal/fee-payer.js';
17
17
  import { resolveFeeToken } from '../internal/fee-token.js';
18
+ import * as MachineTokenCharge from '../internal/machine-token-charge.js';
18
19
  import * as Proof from '../internal/proof.js';
19
20
  import * as Selectors from '../internal/selectors.js';
20
21
  import * as Methods from '../Methods.js';
@@ -32,7 +33,9 @@ import * as SponsorBudget from './SponsorBudget.js';
32
33
  * ```
33
34
  */
34
35
  export function charge(parameters = {}) {
35
- const { amount, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, description, externalId, feeToken: configuredFeeToken, feePayerPolicy, html, memo, relay, sponsorBudget, splits, supportedModes, validateSender, waitForConfirmation = true, } = parameters;
36
+ const { machineTokenEnabled, allowedFeeTokens: configuredAllowedFeeTokens, amount, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, description, externalId, feeToken: configuredFeeToken, feePayerPolicy, html, relay, sponsorBudget, splits, supportedModes, validateSender, waitForConfirmation = true, } = parameters;
37
+ if (configuredAllowedFeeTokens?.length === 0)
38
+ throw new Error('`allowedFeeTokens` must contain at least one token.');
36
39
  const storeKeyPrefix = parameters.storeKeyPrefix ?? '';
37
40
  const rawStore = (parameters.store ?? Store.memory());
38
41
  const store = Store.from(rawStore, { keyPrefix: storeKeyPrefix });
@@ -83,36 +86,45 @@ export function charge(parameters = {}) {
83
86
  const supportedModes = methodDetails?.supportedModes;
84
87
  const currency = resolvedRequest.currency;
85
88
  const recipient = resolvedRequest.recipient;
86
- const memo = methodDetails?.memo;
89
+ const machineTokenEnabled = methodDetails?.machineTokenEnabled === true;
90
+ const settlementSender = machineTokenEnabled
91
+ ? MachineTokenCharge.getSettlementSender(chainId)
92
+ : undefined;
87
93
  const isZeroAmount = BigInt(amount) === 0n;
88
94
  Expires.assert(challenge.expires, challenge.id);
89
95
  if (isZeroAmount && payload.type !== 'proof')
90
96
  throw new MismatchError('Zero-amount challenges require a proof credential.', {});
97
+ // Request-level local sponsorship takes precedence over the hosted provider.
98
+ const localFeePayer = Account.is(request.feePayer) ? request.feePayer : feePayer;
99
+ const allowedFeeTokens = configuredAllowedFeeTokens ??
100
+ (localFeePayer || !remoteFeePayer ? FeePayer.defaultAllowedFeeTokens(chainId) : undefined);
91
101
  return {
102
+ allowedFeeTokens,
92
103
  amount,
104
+ machineTokenEnabled,
93
105
  chainId,
94
106
  challenge,
95
107
  client: await getClient({ chainId }),
96
108
  currency,
97
109
  isZeroAmount,
98
- memo,
99
110
  methodDetails,
100
111
  payload,
101
112
  recipient,
102
113
  requestAllowsFeePayer: request.feePayer !== false,
103
114
  resolvedRequest,
115
+ settlementSenders: settlementSender ? [settlementSender] : [],
104
116
  supportedModes,
105
117
  };
106
118
  }
107
119
  async function validateHashCredential(credential, payload, context) {
108
- const { amount, chainId, challenge, client, currency, memo, methodDetails, recipient, supportedModes, } = context;
120
+ const { amount, chainId, challenge, client, currency, methodDetails, recipient, settlementSenders, supportedModes, } = context;
109
121
  if (supportedModes && !supportedModes.includes('push'))
110
122
  throw new MismatchError('Hash credentials are not supported for this challenge.', {});
111
123
  const source = parseHashCredentialSource({
112
124
  chainId: chainId ?? client.chain?.id,
113
125
  source: credential.source,
114
126
  });
115
- const transfers = getExpectedTransfers({ amount, memo, methodDetails, recipient });
127
+ const transfers = getExpectedTransfers({ amount, methodDetails, recipient });
116
128
  const receipt = await getTransactionReceipt(client, {
117
129
  hash: payload.hash,
118
130
  });
@@ -120,15 +132,15 @@ export function charge(parameters = {}) {
120
132
  const matchedLogs = await assertTransferLogs(receipt, {
121
133
  currency,
122
134
  sender,
135
+ settlementSenders,
123
136
  source,
124
137
  transfers,
125
138
  validateSender,
126
139
  });
127
- if (!memo)
128
- assertChallengeBoundMemo(matchedLogs, {
129
- challengeId: challenge.id,
130
- realm: challenge.realm,
131
- });
140
+ assertChallengeBoundMemo(matchedLogs, {
141
+ challengeId: challenge.id,
142
+ realm: challenge.realm,
143
+ });
132
144
  return { receipt: toReceipt(receipt), sender, transfers };
133
145
  }
134
146
  async function validateProofCredential(credential, payload, context) {
@@ -169,7 +181,7 @@ export function charge(parameters = {}) {
169
181
  return { sender: source.address };
170
182
  }
171
183
  async function validateTransactionCredential(credential, payload, request, context) {
172
- const { amount, chainId, challenge, client, currency, memo, methodDetails, recipient, requestAllowsFeePayer, supportedModes, } = context;
184
+ const { machineTokenEnabled, amount, chainId, challenge, client, currency, methodDetails, recipient, requestAllowsFeePayer, supportedModes, } = context;
173
185
  if (supportedModes && !supportedModes.includes('pull'))
174
186
  throw new MismatchError('Transaction credentials are not supported for this challenge.', {});
175
187
  const serializedTransaction = payload.signature;
@@ -181,25 +193,40 @@ export function charge(parameters = {}) {
181
193
  const isFeePayerTx = methodDetails?.feePayer === true &&
182
194
  requestAllowsFeePayer &&
183
195
  !!(Account.is(request.feePayer) ? request.feePayer : feePayer || remoteFeePayer);
184
- const transfers = getExpectedTransfers({ amount, memo, methodDetails, recipient });
185
- const matchedCalls = assertTransferCalls(transaction.calls ?? [], {
186
- currency,
187
- exactCount: isFeePayerTx,
188
- transfers,
189
- });
190
- if (!memo)
191
- assertChallengeBoundCallMemo(matchedCalls, {
192
- challengeId: challenge.id,
193
- realm: challenge.realm,
196
+ const transfers = getExpectedTransfers({ amount, methodDetails, recipient });
197
+ const machineTokenRoute = machineTokenEnabled
198
+ ? MachineTokenCharge.matchRoute({
199
+ calls: transaction.calls ?? [],
200
+ chainId: chainId ?? client.chain?.id,
201
+ currency,
202
+ transfers,
203
+ })
204
+ : undefined;
205
+ const matchedCalls = machineTokenRoute?.transfers ??
206
+ assertTransferCalls(transaction.calls ?? [], {
207
+ currency,
208
+ exactCount: isFeePayerTx,
209
+ transfers,
194
210
  });
211
+ assertChallengeBoundCallMemo(matchedCalls, {
212
+ challengeId: challenge.id,
213
+ realm: challenge.realm,
214
+ });
195
215
  if (isFeePayerTx) {
196
- FeePayer.validateCalls(transaction.calls, { amount, currency, recipient }, { currency, expectedTransfers: transfers });
197
- FeePayer.assertAllowedFeeToken(transaction, FeePayer.defaultAllowedFeeTokens(chainId));
216
+ if (!machineTokenRoute)
217
+ FeePayer.validateCalls(transaction.calls, { amount, currency, recipient }, { currency, expectedTransfers: transfers });
218
+ FeePayer.assertAllowedFeeToken(transaction, context.allowedFeeTokens);
198
219
  }
199
220
  else {
200
221
  await viem_call(client, FeePayer.simulationTransaction(transaction, { feePayer: false }));
201
222
  }
202
- return { isFeePayerTx, serializedTransaction, transaction, transfers };
223
+ return {
224
+ isFeePayerTx,
225
+ serializedTransaction,
226
+ settlementSenders: machineTokenRoute ? [machineTokenRoute.settlementSender] : [],
227
+ transaction,
228
+ transfers,
229
+ };
203
230
  }
204
231
  async function validateCredential(credential, request, context) {
205
232
  switch (context.payload.type) {
@@ -217,7 +244,7 @@ export function charge(parameters = {}) {
217
244
  return { details: { mode: 'proof', sender }, type: 'proof' };
218
245
  }
219
246
  case 'transaction': {
220
- const { isFeePayerTx, serializedTransaction, transaction, transfers } = await validateTransactionCredential(credential, context.payload, request, context);
247
+ const { isFeePayerTx, serializedTransaction, settlementSenders, transaction, transfers } = await validateTransactionCredential(credential, context.payload, request, context);
221
248
  return {
222
249
  details: {
223
250
  mode: 'pull',
@@ -227,6 +254,7 @@ export function charge(parameters = {}) {
227
254
  },
228
255
  isFeePayerTx,
229
256
  serializedTransaction,
257
+ settlementSenders,
230
258
  transaction,
231
259
  transfers,
232
260
  type: 'transaction',
@@ -238,12 +266,12 @@ export function charge(parameters = {}) {
238
266
  canOffer: parameters.canOffer,
239
267
  onPaymentSuccess: parameters.onPaymentSuccess,
240
268
  defaults: {
269
+ machineTokenEnabled,
241
270
  amount,
242
271
  currency,
243
272
  decimals,
244
273
  description,
245
274
  externalId,
246
- memo,
247
275
  recipient,
248
276
  splits,
249
277
  supportedModes,
@@ -284,6 +312,8 @@ export function charge(parameters = {}) {
284
312
  const chainId = await (async () => {
285
313
  if (request.chainId)
286
314
  return request.chainId;
315
+ if (parameters.chainId !== undefined)
316
+ return parameters.chainId;
287
317
  if (parameters.testnet)
288
318
  return defaults.chainId.testnet;
289
319
  return (await getClient({})).chain?.id;
@@ -298,6 +328,13 @@ export function charge(parameters = {}) {
298
328
  })();
299
329
  if (client.chain?.id !== chainId)
300
330
  throw new Error(`Client not configured with chainId ${chainId}.`);
331
+ if (MachineTokenCharge.isMachineToken({
332
+ chainId,
333
+ currency: request.currency,
334
+ }))
335
+ throw new Error('MACH cannot be advertised as a charge currency. Advertise the settlement currency with `machineTokenEnabled: true`.');
336
+ if (request.machineTokenEnabled && !MachineTokenCharge.isSupported(chainId))
337
+ throw new Error(`Machine tokens are not supported on chainId ${chainId}.`);
301
338
  const resolvedFeePayer = (() => {
302
339
  if (request.feePayer === false)
303
340
  return credential ? false : undefined;
@@ -308,9 +345,9 @@ export function charge(parameters = {}) {
308
345
  })();
309
346
  return {
310
347
  ...request,
348
+ machineTokenEnabled: request.machineTokenEnabled || undefined,
311
349
  chainId,
312
350
  feePayer: resolvedFeePayer,
313
- memo: request.memo || undefined,
314
351
  };
315
352
  },
316
353
  async validate({ credential, request }) {
@@ -328,7 +365,7 @@ export function charge(parameters = {}) {
328
365
  },
329
366
  async broadcast({ credential, request }) {
330
367
  const context = await resolveCredentialContext({ credential, request });
331
- const { amount, chainId, challenge, client, currency, memo, methodDetails, recipient, requestAllowsFeePayer, } = context;
368
+ const { amount, chainId, challenge, client, currency, methodDetails, recipient, requestAllowsFeePayer, } = context;
332
369
  const validated = await validateCredential(credential, request, context);
333
370
  const feePayerAccount = methodDetails?.feePayer === true && requestAllowsFeePayer
334
371
  ? Account.is(request.feePayer)
@@ -362,7 +399,7 @@ export function charge(parameters = {}) {
362
399
  };
363
400
  }
364
401
  case 'transaction': {
365
- const { isFeePayerTx, serializedTransaction, transaction, transfers } = validated;
402
+ const { isFeePayerTx, serializedTransaction, settlementSenders, transaction, transfers } = validated;
366
403
  // Pre-broadcast dedup: catch exact byte-for-byte replays early.
367
404
  const hash = keccak256(serializedTransaction);
368
405
  if (!(await tryClaimHash(store, hash, replayExpires))) {
@@ -374,12 +411,10 @@ export function charge(parameters = {}) {
374
411
  let finalHash;
375
412
  let reservation;
376
413
  try {
377
- const allowedFeeTokens = FeePayer.defaultAllowedFeeTokens(chainId);
378
- if (isFeePayerTx)
379
- FeePayer.assertAllowedFeeToken(transaction, allowedFeeTokens);
380
- const selectableFeeTokens = allowedFeeTokens;
414
+ const { allowedFeeTokens } = context;
381
415
  const completedTransaction = await (async () => {
382
416
  if (feePayerAccount && methodDetails?.feePayer !== false) {
417
+ const selectableFeeTokens = allowedFeeTokens ?? FeePayer.defaultAllowedFeeTokens(chainId);
383
418
  const completed = await FeePayer.preflightSponsorship({
384
419
  transaction,
385
420
  simulate: (request) => viem_call(client, request),
@@ -394,7 +429,7 @@ export function charge(parameters = {}) {
394
429
  }));
395
430
  const sponsored = FeePayer.prepareSponsoredTransaction({
396
431
  account: feePayerAccount,
397
- allowedFeeTokens,
432
+ allowedFeeTokens: selectableFeeTokens,
398
433
  challengeExpires: expires,
399
434
  chainId: chainId ?? client.chain.id,
400
435
  details: { amount, currency, recipient },
@@ -495,13 +530,13 @@ export function charge(parameters = {}) {
495
530
  const matchedLogs = await assertTransferLogs(receipt, {
496
531
  currency,
497
532
  sender: transaction.from,
533
+ settlementSenders,
498
534
  transfers,
499
535
  });
500
- if (!memo)
501
- assertChallengeBoundMemo(matchedLogs, {
502
- challengeId: challenge.id,
503
- realm: challenge.realm,
504
- });
536
+ assertChallengeBoundMemo(matchedLogs, {
537
+ challengeId: challenge.id,
538
+ realm: challenge.realm,
539
+ });
505
540
  if (receipt.transactionHash.toLowerCase() !== finalHash.toLowerCase())
506
541
  throw new VerificationFailedError({
507
542
  reason: 'Broadcast transaction hash does not match the signed transaction',
@@ -550,16 +585,16 @@ function chargeBinding(request) {
550
585
  // Exhaustively destructure so new charge request fields require an explicit binding decision.
551
586
  const { amount, currency, description, externalId, methodDetails, recipient, ...requestRest } = request;
552
587
  requestRest;
553
- const { chainId, feePayer, memo, splits, supportedModes, ...methodDetailsRest } = methodDetails ?? {};
588
+ const { chainId, feePayer, machineTokenEnabled, splits, supportedModes, ...methodDetailsRest } = methodDetails ?? {};
554
589
  methodDetailsRest;
555
590
  void feePayer;
556
591
  return {
592
+ machineTokenEnabled,
557
593
  amount,
558
594
  chainId,
559
595
  currency,
560
596
  description,
561
597
  externalId,
562
- memo,
563
598
  recipient,
564
599
  splits,
565
600
  supportedModes,
@@ -569,7 +604,6 @@ function getExpectedTransfers(parameters) {
569
604
  return Charge_internal.getTransfers({
570
605
  amount: parameters.amount,
571
606
  methodDetails: {
572
- memo: parameters.memo,
573
607
  splits: parameters.methodDetails?.splits,
574
608
  },
575
609
  recipient: parameters.recipient,
@@ -695,7 +729,9 @@ async function assertTransferLogs(receipt, parameters) {
695
729
  if (!(await isValidTransferSender({
696
730
  expectedSender: parameters.sender,
697
731
  sender: log.args.from,
732
+ settlementSenders: parameters.settlementSenders,
698
733
  source: parameters.source,
734
+ transactionSender: receipt.from,
699
735
  validateSender: parameters.validateSender,
700
736
  })))
701
737
  continue;
@@ -770,6 +806,9 @@ function isSameTransferLog(a, b) {
770
806
  async function isValidTransferSender(parameters) {
771
807
  if (TempoAddress.isEqual(parameters.sender, parameters.expectedSender))
772
808
  return true;
809
+ if (TempoAddress.isEqual(parameters.transactionSender, parameters.expectedSender) &&
810
+ parameters.settlementSenders?.some((sender) => TempoAddress.isEqual(parameters.sender, sender)))
811
+ return true;
773
812
  if (!parameters.validateSender)
774
813
  return false;
775
814
  return parameters.validateSender({
@@ -869,34 +908,29 @@ function toReceipt(receipt) {
869
908
  reference: transactionHash,
870
909
  };
871
910
  }
872
- /**
873
- * Asserts that at least one of the matched payment logs carries a
874
- * challenge-bound memo nonce (keccak256(challengeId)[0..6] in bytes 25–31).
875
- * Only checks logs that were matched by `assertTransferLogs`, not the
876
- * entire receipt — preventing unrelated dust transfers from satisfying
877
- * the binding.
878
- * @internal
879
- */
880
911
  function assertChallengeBoundMemo(matchedLogs, parameters) {
881
- const bound = matchedLogs.some((log) => {
882
- if (log.kind !== 'memo')
883
- return false;
884
- if (!Attribution.verifyServer(log.args.memo, parameters.realm))
885
- return false;
886
- return Attribution.verifyChallengeBinding(log.args.memo, parameters.challengeId);
887
- });
888
- if (!bound)
889
- throw new MismatchError('Payment verification failed: memo is not bound to this challenge.', {});
912
+ assertChallengeBoundMemos(matchedLogs.map((log) => (log.kind === 'memo' ? log.args.memo : undefined)), parameters);
890
913
  }
891
914
  function assertChallengeBoundCallMemo(matchedCalls, parameters) {
892
- const bound = matchedCalls.some((call) => {
893
- if (!call.memo)
894
- return false;
895
- const memo = call.memo;
896
- if (!Attribution.verifyServer(memo, parameters.realm))
897
- return false;
898
- return Attribution.verifyChallengeBinding(memo, parameters.challengeId);
899
- });
915
+ assertChallengeBoundMemos(matchedCalls.map((call) => call.memo), parameters);
916
+ }
917
+ /**
918
+ * Requires a challenge-bound attribution memo and rejects conflicting attribution
919
+ * on matched payment transfers. Otherwise, a split payment could authorize another
920
+ * challenge after the first challenge's replay marker expires. Unmatched transfers
921
+ * cannot establish the binding; plain transfers and non-MPP memos remain allowed.
922
+ * @internal
923
+ */
924
+ function assertChallengeBoundMemos(memos, parameters) {
925
+ let bound = false;
926
+ for (const memo of memos) {
927
+ if (!memo || !Attribution.isMppMemo(memo))
928
+ continue;
929
+ if (!Attribution.verifyServer(memo, parameters.realm) ||
930
+ !Attribution.verifyChallengeBinding(memo, parameters.challengeId))
931
+ throw new MismatchError('Payment verification failed: memo is not bound to this challenge.', {});
932
+ bound = true;
933
+ }
900
934
  if (!bound)
901
935
  throw new MismatchError('Payment verification failed: memo is not bound to this challenge.', {});
902
936
  }