@metamask/subscription-controller 10.0.0 → 11.0.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.
- package/CHANGELOG.md +50 -1
- package/dist/SubscriptionController.d.ts.map +1 -1
- package/dist/SubscriptionController.js +11 -5
- package/dist/SubscriptionController.js.map +1 -1
- package/dist/SubscriptionService-method-action-types.d.ts +2 -1
- package/dist/SubscriptionService-method-action-types.d.ts.map +1 -1
- package/dist/SubscriptionService-method-action-types.js.map +1 -1
- package/dist/SubscriptionService-structs.d.ts +33 -47
- package/dist/SubscriptionService-structs.d.ts.map +1 -1
- package/dist/SubscriptionService-structs.js +2 -6
- package/dist/SubscriptionService-structs.js.map +1 -1
- package/dist/SubscriptionService.d.ts +2 -1
- package/dist/SubscriptionService.d.ts.map +1 -1
- package/dist/SubscriptionService.js +4 -3
- package/dist/SubscriptionService.js.map +1 -1
- package/dist/constants.d.ts +8 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/subscription-delegation/SubscriptionDelegationService-method-action-types.d.ts +29 -5
- package/dist/subscription-delegation/SubscriptionDelegationService-method-action-types.d.ts.map +1 -1
- package/dist/subscription-delegation/SubscriptionDelegationService-method-action-types.js.map +1 -1
- package/dist/subscription-delegation/SubscriptionDelegationService.d.ts +33 -11
- package/dist/subscription-delegation/SubscriptionDelegationService.d.ts.map +1 -1
- package/dist/subscription-delegation/SubscriptionDelegationService.js +305 -22
- package/dist/subscription-delegation/SubscriptionDelegationService.js.map +1 -1
- package/dist/subscription-delegation/caveats.d.ts +31 -2
- package/dist/subscription-delegation/caveats.d.ts.map +1 -1
- package/dist/subscription-delegation/caveats.js +49 -9
- package/dist/subscription-delegation/caveats.js.map +1 -1
- package/dist/subscription-delegation/fingerprint.d.ts +1 -0
- package/dist/subscription-delegation/fingerprint.d.ts.map +1 -1
- package/dist/subscription-delegation/fingerprint.js +10 -6
- package/dist/subscription-delegation/fingerprint.js.map +1 -1
- package/dist/subscription-delegation/typed-data.d.ts +23 -0
- package/dist/subscription-delegation/typed-data.d.ts.map +1 -0
- package/dist/subscription-delegation/typed-data.js +75 -0
- package/dist/subscription-delegation/typed-data.js.map +1 -0
- package/dist/subscription-delegation/types.d.ts +105 -12
- package/dist/subscription-delegation/types.d.ts.map +1 -1
- package/dist/subscription-delegation/types.js +3 -4
- package/dist/subscription-delegation/types.js.map +1 -1
- package/dist/types.d.ts +15 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/package.json +18 -15
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { ORIGIN_METAMASK } from '@metamask/controller-utils';
|
|
1
2
|
import { hashDelegation } from '@metamask/delegation-core';
|
|
2
3
|
import { DELEGATOR_CONTRACTS } from '@metamask/delegation-deployments';
|
|
3
|
-
import { getMoneyAccountVaultConfig, MUSD_DECIMALS, } from '@metamask/money-account-utils';
|
|
4
|
-
import { add0x, hexToNumber } from '@metamask/utils';
|
|
5
|
-
import { SubscriptionDelegationServiceErrorMessage } from '../constants.js';
|
|
4
|
+
import { getMoneyAccountDepositAssetAddress, getMoneyAccountVaultConfig, MUSD_DECIMALS, } from '@metamask/money-account-utils';
|
|
5
|
+
import { add0x, hexToNumber, isStrictHexString } from '@metamask/utils';
|
|
6
|
+
import { ACTIVE_SUBSCRIPTION_STATUSES, SubscriptionControllerErrorMessage, SubscriptionDelegationServiceErrorMessage, } from '../constants.js';
|
|
6
7
|
import { CRYPTO_AUTH_METHODS, PAYMENT_TYPES, PRODUCT_TYPES } from '../types.js';
|
|
7
8
|
import { assertPositiveInteger, calculatePeriodAmount, getDelegationStartDate, getPeriodDuration, } from './amount.js';
|
|
8
9
|
import { buildUnsignedSubscriptionDelegation } from './caveats.js';
|
|
9
10
|
import { equalsIgnoreCase, makeMatchesSubscriptionDelegation, pickLatestMatchingSubscriptionDelegation, } from './fingerprint.js';
|
|
10
|
-
import {
|
|
11
|
+
import { buildDelegationTypedData, decodeSubscriptionAuthority, } from './typed-data.js';
|
|
12
|
+
import { CASH_SUBSCRIPTION_DELEGATION_TYPE, SUBSCRIPTION_DELEGATION_APPROVAL_TYPE, SUBSCRIPTION_DELEGATION_POLICY_VERSION, } from './types.js';
|
|
11
13
|
/**
|
|
12
14
|
* The name of the {@link SubscriptionDelegationService}, used to namespace the
|
|
13
15
|
* service's actions and events.
|
|
@@ -16,18 +18,24 @@ export const serviceName = 'SubscriptionDelegationService';
|
|
|
16
18
|
const MESSENGER_EXPOSED_METHODS = [
|
|
17
19
|
'prepareDelegation',
|
|
18
20
|
'checkMoneyAccountBalance',
|
|
21
|
+
'startSubscriptionWithDelegation',
|
|
19
22
|
];
|
|
20
23
|
const DELEGATION_FRAMEWORK_VERSION = '1.3.0';
|
|
21
24
|
function resolveEnforcers(chainId) {
|
|
22
25
|
const contracts = DELEGATOR_CONTRACTS[DELEGATION_FRAMEWORK_VERSION]?.[hexToNumber(chainId)];
|
|
23
|
-
if (!contracts?.
|
|
26
|
+
if (!contracts?.ERC20PeriodTransferEnforcer ||
|
|
27
|
+
!contracts.AllowedCalldataEnforcer) {
|
|
24
28
|
throw new Error(`${SubscriptionDelegationServiceErrorMessage.DelegationContractsNotFound}: ${chainId}`);
|
|
25
29
|
}
|
|
26
30
|
return {
|
|
27
|
-
valueLte: contracts.ValueLteEnforcer,
|
|
28
31
|
erc20TokenPeriodTransfer: contracts.ERC20PeriodTransferEnforcer,
|
|
32
|
+
allowedCalldata: contracts.AllowedCalldataEnforcer,
|
|
29
33
|
};
|
|
30
34
|
}
|
|
35
|
+
function removeDelegationSignature(delegation) {
|
|
36
|
+
const { signature: _signature, ...unsignedDelegation } = delegation;
|
|
37
|
+
return unsignedDelegation;
|
|
38
|
+
}
|
|
31
39
|
/**
|
|
32
40
|
* Stateless orchestrator for cash-subscription delegation setup.
|
|
33
41
|
*
|
|
@@ -35,10 +43,8 @@ function resolveEnforcers(chainId) {
|
|
|
35
43
|
* Authenticated User Storage → register CHOMP intent. Returns a verified
|
|
36
44
|
* `delegationHash` for `SubscriptionController.startSubscriptionWithCrypto`.
|
|
37
45
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* metadata. The CHOMP-enabled path (`skipChompInteractions` unset/false)
|
|
41
|
-
* remains dormant and is not production-ready without that package support.
|
|
46
|
+
* Pass `skipChompInteractions: true` to skip CHOMP verify and intent
|
|
47
|
+
* registration and return a locally computed delegation hash.
|
|
42
48
|
*
|
|
43
49
|
* Each call resolves the Money Account chain from remote feature flags, then
|
|
44
50
|
* resolves its price, payment token, and delegate from `SubscriptionController`
|
|
@@ -80,6 +86,284 @@ export class SubscriptionDelegationService {
|
|
|
80
86
|
requiredBalance: requiredBalance.toString(),
|
|
81
87
|
};
|
|
82
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Runs the complete Money Account subscription checkout authorization flow.
|
|
91
|
+
*
|
|
92
|
+
* Subscriptions are refreshed first. An active subscription for the product
|
|
93
|
+
* is rejected before Money Account upgrade, approval, delegation signing,
|
|
94
|
+
* persistence, or CHOMP registration.
|
|
95
|
+
*
|
|
96
|
+
* Before the approval, `MoneyAccountUpgradeController` is asked to ensure
|
|
97
|
+
* the Money Account vault delegations and CHOMP intents exist; the
|
|
98
|
+
* Subscription API validates them server-side. The custom approval is the
|
|
99
|
+
* sole consent and funding boundary for the payment permission. No payment
|
|
100
|
+
* delegation signing, persistence, or intent mutation occurs before it
|
|
101
|
+
* returns a funding transaction hash.
|
|
102
|
+
*
|
|
103
|
+
* When `request.skipApproval` is `true`, the approval request and its
|
|
104
|
+
* result validation are skipped and the flow proceeds directly to signing.
|
|
105
|
+
* The caller is then responsible for consent and funding.
|
|
106
|
+
*
|
|
107
|
+
* @param request - Product selection, Money Account identity, and optional
|
|
108
|
+
* `skipApproval` flag.
|
|
109
|
+
* @returns The result from `SubscriptionController:startSubscriptionWithCrypto`.
|
|
110
|
+
*/
|
|
111
|
+
async startSubscriptionWithDelegation(request) {
|
|
112
|
+
if (request.product !== PRODUCT_TYPES.MONEY_ACCOUNT_PLUS) {
|
|
113
|
+
throw new Error(SubscriptionDelegationServiceErrorMessage.UnsupportedProduct);
|
|
114
|
+
}
|
|
115
|
+
const config = await this.#resolveConfiguration(request.product, request.recurringInterval);
|
|
116
|
+
if (!equalsIgnoreCase(request.chainId, config.chainId)) {
|
|
117
|
+
throw new Error(SubscriptionDelegationServiceErrorMessage.ChainMismatch);
|
|
118
|
+
}
|
|
119
|
+
const subscriptions = await this.#messenger.call('SubscriptionController:getSubscriptions');
|
|
120
|
+
this.#assertUserNotSubscribed(subscriptions, request.product);
|
|
121
|
+
const { trialedProducts } = this.#messenger.call('SubscriptionController:getState');
|
|
122
|
+
const isTrialRequested = config.price.trialPeriodDays > 0 &&
|
|
123
|
+
!trialedProducts.includes(request.product);
|
|
124
|
+
await this.#messenger.call('MoneyAccountUpgradeController:forceUpgradeAccount', request.payerAddress);
|
|
125
|
+
const paymentPermission = await this.#preparePaymentPermission(request, config, isTrialRequested);
|
|
126
|
+
const bundle = {
|
|
127
|
+
policyVersion: SUBSCRIPTION_DELEGATION_POLICY_VERSION,
|
|
128
|
+
account: request.payerAddress,
|
|
129
|
+
chainId: request.chainId,
|
|
130
|
+
permissions: [paymentPermission],
|
|
131
|
+
};
|
|
132
|
+
const approval = request.skipApproval
|
|
133
|
+
? undefined
|
|
134
|
+
: await this.#requestApproval({
|
|
135
|
+
request,
|
|
136
|
+
price: config.price,
|
|
137
|
+
musdAddress: config.musdAddress,
|
|
138
|
+
bundle,
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
if (approval) {
|
|
142
|
+
this.#validateApprovalResult(approval);
|
|
143
|
+
}
|
|
144
|
+
const paymentDelegation = await this.#signPaymentPermissionIfRequired(paymentPermission, config);
|
|
145
|
+
const paymentDelegationHash = await this.#commitPaymentPermission({
|
|
146
|
+
request,
|
|
147
|
+
paymentPermission,
|
|
148
|
+
paymentDelegation,
|
|
149
|
+
config,
|
|
150
|
+
});
|
|
151
|
+
const result = await this.#messenger.call('SubscriptionController:startSubscriptionWithCrypto', {
|
|
152
|
+
products: [request.product],
|
|
153
|
+
isTrialRequested,
|
|
154
|
+
recurringInterval: request.recurringInterval,
|
|
155
|
+
billingCycles: config.price.minBillingCycles,
|
|
156
|
+
chainId: request.chainId,
|
|
157
|
+
payerAddress: request.payerAddress,
|
|
158
|
+
tokenSymbol: config.token.symbol,
|
|
159
|
+
cryptoAuthMethod: CRYPTO_AUTH_METHODS.DELEGATION,
|
|
160
|
+
delegationHash: paymentDelegationHash,
|
|
161
|
+
// Reject if eligibility changed during approval, because the signed
|
|
162
|
+
// delegation start date was derived from the original trial state.
|
|
163
|
+
// This local guard is removed before the backend request.
|
|
164
|
+
assertTrialEligibility: true,
|
|
165
|
+
});
|
|
166
|
+
approval?.resultCallbacks?.success();
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
approval?.resultCallbacks?.error(error);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Rejects when the refreshed list already contains an active subscription
|
|
176
|
+
* for the product. Active includes `active`, `trialing`, and `provisional`.
|
|
177
|
+
*
|
|
178
|
+
* @param subscriptions - Subscriptions returned by the latest refresh.
|
|
179
|
+
* @param product - Product the caller is trying to start.
|
|
180
|
+
*/
|
|
181
|
+
#assertUserNotSubscribed(subscriptions, product) {
|
|
182
|
+
const alreadySubscribed = subscriptions.some((subscription) => ACTIVE_SUBSCRIPTION_STATUSES.includes(subscription.status) &&
|
|
183
|
+
subscription.products.some((entry) => entry.name === product));
|
|
184
|
+
if (alreadySubscribed) {
|
|
185
|
+
throw new Error(SubscriptionControllerErrorMessage.UserAlreadySubscribed);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async #requestApproval({ request, price, musdAddress, bundle, }) {
|
|
189
|
+
const targetAmount = calculatePeriodAmount({
|
|
190
|
+
unitAmount: price.unitAmount,
|
|
191
|
+
unitDecimals: price.unitDecimals,
|
|
192
|
+
tokenDecimals: MUSD_DECIMALS,
|
|
193
|
+
}) * BigInt(price.minBillingCyclesForBalance);
|
|
194
|
+
return (await this.#messenger.call('ApprovalController:addRequest', {
|
|
195
|
+
origin: ORIGIN_METAMASK,
|
|
196
|
+
type: SUBSCRIPTION_DELEGATION_APPROVAL_TYPE,
|
|
197
|
+
expectsResult: true,
|
|
198
|
+
requestData: {
|
|
199
|
+
bundle,
|
|
200
|
+
funding: {
|
|
201
|
+
useCase: 'subscription',
|
|
202
|
+
destinationAccount: request.payerAddress,
|
|
203
|
+
chainId: request.chainId,
|
|
204
|
+
targetToken: {
|
|
205
|
+
symbol: 'mUSD',
|
|
206
|
+
address: musdAddress,
|
|
207
|
+
},
|
|
208
|
+
targetAmount: targetAmount.toString(),
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
}, true));
|
|
212
|
+
}
|
|
213
|
+
async #preparePaymentPermission(request, config, isTrialRequested) {
|
|
214
|
+
const periodAmount = calculatePeriodAmount({
|
|
215
|
+
unitAmount: config.price.unitAmount,
|
|
216
|
+
unitDecimals: config.price.unitDecimals,
|
|
217
|
+
tokenDecimals: config.token.decimals,
|
|
218
|
+
});
|
|
219
|
+
const periodDuration = getPeriodDuration(request.recurringInterval);
|
|
220
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
221
|
+
const startDate = getDelegationStartDate({
|
|
222
|
+
nowSeconds,
|
|
223
|
+
trialPeriodDays: isTrialRequested
|
|
224
|
+
? config.price.trialPeriodDays
|
|
225
|
+
: undefined,
|
|
226
|
+
});
|
|
227
|
+
const matches = makeMatchesSubscriptionDelegation({
|
|
228
|
+
delegatorAddress: request.payerAddress,
|
|
229
|
+
delegateAddress: config.delegateAddress,
|
|
230
|
+
chainId: request.chainId,
|
|
231
|
+
recipientAddress: config.paymentAddress,
|
|
232
|
+
tokenAddress: config.token.address,
|
|
233
|
+
periodAmount,
|
|
234
|
+
periodDuration,
|
|
235
|
+
nowSeconds,
|
|
236
|
+
isTrialDeferred: startDate > nowSeconds,
|
|
237
|
+
enforcers: config.enforcers,
|
|
238
|
+
});
|
|
239
|
+
const reusable = pickLatestMatchingSubscriptionDelegation((await this.#messenger.call('AuthenticatedUserStorageService:listDelegations')).filter(matches), config.enforcers);
|
|
240
|
+
const delegation = reusable
|
|
241
|
+
? removeDelegationSignature(reusable.signedDelegation)
|
|
242
|
+
: buildUnsignedSubscriptionDelegation({
|
|
243
|
+
delegateAddress: config.delegateAddress,
|
|
244
|
+
delegatorAddress: request.payerAddress,
|
|
245
|
+
enforcers: config.enforcers,
|
|
246
|
+
recipientAddress: config.paymentAddress,
|
|
247
|
+
tokenAddress: config.token.address,
|
|
248
|
+
periodAmount,
|
|
249
|
+
periodDuration,
|
|
250
|
+
startDate,
|
|
251
|
+
});
|
|
252
|
+
const typedData = buildDelegationTypedData({
|
|
253
|
+
delegation,
|
|
254
|
+
chainId: request.chainId,
|
|
255
|
+
delegationManager: config.delegationManager,
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
id: CASH_SUBSCRIPTION_DELEGATION_TYPE,
|
|
259
|
+
owner: 'subscription',
|
|
260
|
+
disposition: reusable ? 'reused' : 'new',
|
|
261
|
+
delegation,
|
|
262
|
+
typedData,
|
|
263
|
+
decodedAuthority: decodeSubscriptionAuthority(delegation, config.enforcers),
|
|
264
|
+
...(reusable
|
|
265
|
+
? { existingDelegationHash: reusable.metadata.delegationHash }
|
|
266
|
+
: {}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
#validateApprovalResult(approval) {
|
|
270
|
+
if (!approval?.value || typeof approval.value !== 'object') {
|
|
271
|
+
throw new Error(SubscriptionDelegationServiceErrorMessage.ApprovalResultMissing);
|
|
272
|
+
}
|
|
273
|
+
const value = approval.value;
|
|
274
|
+
if (typeof value.fundingTransactionHash !== 'string' ||
|
|
275
|
+
!isStrictHexString(value.fundingTransactionHash) ||
|
|
276
|
+
value.fundingTransactionHash.length !== 66) {
|
|
277
|
+
throw new Error(SubscriptionDelegationServiceErrorMessage.InvalidFundingTransactionHash);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
fundingTransactionHash: value.fundingTransactionHash,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async #signPaymentPermissionIfRequired(permission, config) {
|
|
284
|
+
if (permission.disposition === 'reused') {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const signature = (await this.#messenger.call('DelegationController:signDelegation', {
|
|
288
|
+
delegation: permission.delegation,
|
|
289
|
+
chainId: config.chainId,
|
|
290
|
+
}));
|
|
291
|
+
return { ...permission.delegation, signature };
|
|
292
|
+
}
|
|
293
|
+
async #commitPaymentPermission({ request, paymentPermission, paymentDelegation, config, }) {
|
|
294
|
+
if (paymentPermission.disposition === 'reused') {
|
|
295
|
+
const delegationHash = paymentPermission.existingDelegationHash;
|
|
296
|
+
const storedDelegation = (await this.#messenger.call('AuthenticatedUserStorageService:listDelegations')).find(({ metadata }) => equalsIgnoreCase(metadata.delegationHash, delegationHash));
|
|
297
|
+
if (!storedDelegation) {
|
|
298
|
+
throw new Error(SubscriptionDelegationServiceErrorMessage.ReusableDelegationInvalid);
|
|
299
|
+
}
|
|
300
|
+
await this.#verifySignedPaymentDelegation(storedDelegation.signedDelegation, delegationHash, request.chainId);
|
|
301
|
+
await this.#ensureIntent({
|
|
302
|
+
account: request.payerAddress,
|
|
303
|
+
chainId: request.chainId,
|
|
304
|
+
delegationHash,
|
|
305
|
+
allowance: add0x(BigInt(paymentPermission.decodedAuthority.periodAmount).toString(16)),
|
|
306
|
+
tokenSymbol: config.token.symbol,
|
|
307
|
+
tokenAddress: config.token.address,
|
|
308
|
+
});
|
|
309
|
+
await this.#assertIntentActive(request.payerAddress, delegationHash);
|
|
310
|
+
return delegationHash;
|
|
311
|
+
}
|
|
312
|
+
const signedDelegation = paymentDelegation;
|
|
313
|
+
const delegationHash = hashDelegation({
|
|
314
|
+
...signedDelegation,
|
|
315
|
+
salt: BigInt(signedDelegation.salt),
|
|
316
|
+
});
|
|
317
|
+
await this.#verifySignedPaymentDelegation(signedDelegation, delegationHash, request.chainId);
|
|
318
|
+
const allowance = add0x(BigInt(paymentPermission.decodedAuthority.periodAmount).toString(16));
|
|
319
|
+
await this.#messenger.call('AuthenticatedUserStorageService:createDelegation', {
|
|
320
|
+
signedDelegation,
|
|
321
|
+
metadata: {
|
|
322
|
+
delegationHash,
|
|
323
|
+
chainIdHex: request.chainId,
|
|
324
|
+
allowance,
|
|
325
|
+
tokenSymbol: config.token.symbol,
|
|
326
|
+
tokenAddress: config.token.address,
|
|
327
|
+
type: CASH_SUBSCRIPTION_DELEGATION_TYPE,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
await this.#createIntent({
|
|
331
|
+
account: request.payerAddress,
|
|
332
|
+
chainId: request.chainId,
|
|
333
|
+
delegationHash,
|
|
334
|
+
allowance,
|
|
335
|
+
tokenSymbol: config.token.symbol,
|
|
336
|
+
tokenAddress: config.token.address,
|
|
337
|
+
});
|
|
338
|
+
await this.#assertIntentActive(request.payerAddress, delegationHash);
|
|
339
|
+
return delegationHash;
|
|
340
|
+
}
|
|
341
|
+
async #assertIntentActive(account, delegationHash) {
|
|
342
|
+
const intents = await this.#messenger.call('ChompApiService:getIntentsByAddress', account);
|
|
343
|
+
const intent = intents.find((candidate) => equalsIgnoreCase(candidate.delegationHash, delegationHash));
|
|
344
|
+
if (intent?.status !== 'active') {
|
|
345
|
+
throw new Error(SubscriptionDelegationServiceErrorMessage.ChompIntentNotActive);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async #verifySignedPaymentDelegation(signedDelegation, expectedDelegationHash, chainId) {
|
|
349
|
+
const localHash = hashDelegation({
|
|
350
|
+
...signedDelegation,
|
|
351
|
+
salt: BigInt(signedDelegation.salt),
|
|
352
|
+
});
|
|
353
|
+
if (!equalsIgnoreCase(localHash, expectedDelegationHash)) {
|
|
354
|
+
throw new Error(SubscriptionDelegationServiceErrorMessage.ReusableDelegationInvalid);
|
|
355
|
+
}
|
|
356
|
+
const verifyResult = await this.#messenger.call('ChompApiService:verifyDelegation', { signedDelegation, chainId });
|
|
357
|
+
if (!verifyResult.valid) {
|
|
358
|
+
throw new Error(`${SubscriptionDelegationServiceErrorMessage.ChompRejectedDelegation}: ${verifyResult.errors?.join(', ') ?? 'unknown error'}`);
|
|
359
|
+
}
|
|
360
|
+
if (!verifyResult.delegationHash) {
|
|
361
|
+
throw new Error(SubscriptionDelegationServiceErrorMessage.ChompMissingDelegationHash);
|
|
362
|
+
}
|
|
363
|
+
if (!equalsIgnoreCase(verifyResult.delegationHash, expectedDelegationHash)) {
|
|
364
|
+
throw new Error(SubscriptionDelegationServiceErrorMessage.ChompDelegationHashMismatch);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
83
367
|
/**
|
|
84
368
|
* Prepares a cash-subscription delegation and returns its hash.
|
|
85
369
|
*
|
|
@@ -93,10 +377,8 @@ export class SubscriptionDelegationService {
|
|
|
93
377
|
* If there is no match, builds, signs, optionally verifies with CHOMP,
|
|
94
378
|
* persists, and optionally registers a new delegation.
|
|
95
379
|
*
|
|
96
|
-
* When `skipChompInteractions` is true
|
|
97
|
-
*
|
|
98
|
-
* default CHOMP-enabled path requires a follow-up chomp-api-service release
|
|
99
|
-
* that accepts `'cash-subscription'` intent metadata.
|
|
380
|
+
* When `skipChompInteractions` is true, CHOMP verify and intent calls are
|
|
381
|
+
* skipped; the returned hash is computed locally.
|
|
100
382
|
*
|
|
101
383
|
* @param request - Authoritative pricing and payer details for the delegation.
|
|
102
384
|
* @param forceNew - Whether to create a replacement instead of reusing a
|
|
@@ -109,7 +391,7 @@ export class SubscriptionDelegationService {
|
|
|
109
391
|
throw new Error(SubscriptionDelegationServiceErrorMessage.UnsupportedProduct);
|
|
110
392
|
}
|
|
111
393
|
const skipChomp = Boolean(request.skipChompInteractions);
|
|
112
|
-
const { chainId, delegateAddress, enforcers, price, token } = await this.#resolveConfiguration(request.product, request.recurringInterval);
|
|
394
|
+
const { chainId, delegateAddress, paymentAddress, enforcers, price, token, } = await this.#resolveConfiguration(request.product, request.recurringInterval);
|
|
113
395
|
if (request.checkBalance) {
|
|
114
396
|
const { hasSufficientBalance } = await this.#compareMoneyAccountBalance(request.payerAddress, price);
|
|
115
397
|
if (!hasSufficientBalance) {
|
|
@@ -136,6 +418,7 @@ export class SubscriptionDelegationService {
|
|
|
136
418
|
request,
|
|
137
419
|
chainId,
|
|
138
420
|
delegateAddress,
|
|
421
|
+
paymentAddress,
|
|
139
422
|
tokenAddress: token.address,
|
|
140
423
|
periodAmount,
|
|
141
424
|
periodDuration,
|
|
@@ -162,6 +445,7 @@ export class SubscriptionDelegationService {
|
|
|
162
445
|
const unsigned = buildUnsignedSubscriptionDelegation({
|
|
163
446
|
delegateAddress,
|
|
164
447
|
delegatorAddress: request.payerAddress,
|
|
448
|
+
recipientAddress: paymentAddress,
|
|
165
449
|
enforcers,
|
|
166
450
|
tokenAddress: token.address,
|
|
167
451
|
periodAmount,
|
|
@@ -217,10 +501,11 @@ export class SubscriptionDelegationService {
|
|
|
217
501
|
disposition: 'created',
|
|
218
502
|
};
|
|
219
503
|
}
|
|
220
|
-
async #findReusableDelegation({ request, chainId, delegateAddress, tokenAddress, periodAmount, periodDuration, nowSeconds, isTrialDeferred, enforcers, }) {
|
|
504
|
+
async #findReusableDelegation({ request, chainId, delegateAddress, paymentAddress, tokenAddress, periodAmount, periodDuration, nowSeconds, isTrialDeferred, enforcers, }) {
|
|
221
505
|
const matches = makeMatchesSubscriptionDelegation({
|
|
222
506
|
delegatorAddress: request.payerAddress,
|
|
223
507
|
delegateAddress,
|
|
508
|
+
recipientAddress: paymentAddress,
|
|
224
509
|
chainId,
|
|
225
510
|
tokenAddress,
|
|
226
511
|
periodAmount,
|
|
@@ -255,7 +540,11 @@ export class SubscriptionDelegationService {
|
|
|
255
540
|
return {
|
|
256
541
|
chainId,
|
|
257
542
|
delegateAddress: chain.delegateAddress,
|
|
543
|
+
delegationManager: DELEGATOR_CONTRACTS[DELEGATION_FRAMEWORK_VERSION][hexToNumber(chainId)]
|
|
544
|
+
.DelegationManager,
|
|
545
|
+
paymentAddress: chain.paymentAddress,
|
|
258
546
|
enforcers,
|
|
547
|
+
musdAddress: getMoneyAccountDepositAssetAddress(chainId),
|
|
259
548
|
price,
|
|
260
549
|
token,
|
|
261
550
|
};
|
|
@@ -282,12 +571,6 @@ export class SubscriptionDelegationService {
|
|
|
282
571
|
await this.#createIntent(params);
|
|
283
572
|
}
|
|
284
573
|
async #createIntent(params) {
|
|
285
|
-
// Published `@metamask/chomp-api-service` only types intent metadata as
|
|
286
|
-
// `'cash-deposit' | 'cash-withdrawal'`. The dormant production path still
|
|
287
|
-
// passes `'cash-subscription'`; a follow-up chomp-api-service release must
|
|
288
|
-
// accept that discriminator before this path is production-ready. Alpha
|
|
289
|
-
// callers must use `skipChompInteractions: true` so this method is not
|
|
290
|
-
// reached.
|
|
291
574
|
await this.#messenger.call('ChompApiService:createIntents', [
|
|
292
575
|
{
|
|
293
576
|
account: params.account,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubscriptionDelegationService.js","sourceRoot":"","sources":["../../src/subscription-delegation/SubscriptionDelegationService.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAGvE,OAAO,EACL,0BAA0B,EAC1B,aAAa,GACd,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGrD,OAAO,EAAE,yCAAyC,EAAE,MAAM,iBAAiB,CAAC;AAE5E,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAQhF,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mCAAmC,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EACL,gBAAgB,EAChB,iCAAiC,EACjC,wCAAwC,GACzC,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAAE,iCAAiC,EAAE,MAAM,YAAY,CAAC;AAE/D;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,+BAA+B,CAAC;AAE3D,MAAM,yBAAyB,GAAG;IAChC,mBAAmB;IACnB,0BAA0B;CAClB,CAAC;AAEX,MAAM,4BAA4B,GAAG,OAAO,CAAC;AAE7C,SAAS,gBAAgB,CAAC,OAAY;IACpC,MAAM,SAAS,GACb,mBAAmB,CAAC,4BAA4B,CAAC,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;IAE5E,IAAI,CAAC,SAAS,EAAE,gBAAgB,IAAI,CAAC,SAAS,CAAC,2BAA2B,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CACb,GAAG,yCAAyC,CAAC,2BAA2B,KAAK,OAAO,EAAE,CACvF,CAAC;IACJ,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,SAAS,CAAC,gBAAgB;QACpC,wBAAwB,EAAE,SAAS,CAAC,2BAA2B;KAChE,CAAC;AACJ,CAAC;AA+DD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,6BAA6B;IAC/B,IAAI,GAAuB,WAAW,CAAC;IAEvC,UAAU,CAAyC;IAE5D,YAAY,OAA6C;QACvD,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;QAEpC,IAAI,CAAC,UAAU,CAAC,4BAA4B,CAC1C,IAAI,EACJ,yBAAyB,CAC1B,CAAC;IACJ,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,wBAAwB,CAC5B,OAAwC;QAExC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAChD,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,iBAAiB,CAC1B,CAAC;QACF,OAAO,IAAI,CAAC,2BAA2B,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACvE,CAAC;IAED,KAAK,CAAC,2BAA2B,CAC/B,YAAiB,EACjB,KAAmB;QAEnB,qBAAqB,CACnB,KAAK,CAAC,0BAA0B,EAChC,yCAAyC,CAAC,2BAA2B,CACtE,CAAC;QAEF,MAAM,YAAY,GAAG,qBAAqB,CAAC;YACzC,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,YAAY,EAAE,KAAK,CAAC,YAAY;YAChC,aAAa,EAAE,aAAa;SAC7B,CAAC,CAAC;QACH,MAAM,eAAe,GACnB,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAE1D,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CACjD,qDAAqD,EACrD,YAAY,CACb,CAAC;QAEF,OAAO;YACL,oBAAoB,EAAE,MAAM,CAAC,YAAY,CAAC,IAAI,eAAe;YAC7D,OAAO,EAAE,YAAY;YACrB,eAAe,EAAE,eAAe,CAAC,QAAQ,EAAE;SAC5C,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,iBAAiB,CACrB,OAA6C,EAC7C,QAAQ,GAAG,KAAK;QAEhB,IAAI,OAAO,CAAC,OAAO,KAAK,aAAa,CAAC,kBAAkB,EAAE,CAAC;YACzD,MAAM,IAAI,KAAK,CACb,yCAAyC,CAAC,kBAAkB,CAC7D,CAAC;QACJ,CAAC;QAED,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;QAEzD,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,GACzD,MAAM,IAAI,CAAC,qBAAqB,CAC9B,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,iBAAiB,CAC1B,CAAC;QAEJ,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;YACzB,MAAM,EAAE,oBAAoB,EAAE,GAAG,MAAM,IAAI,CAAC,2BAA2B,CACrE,OAAO,CAAC,YAAY,EACpB,KAAK,CACN,CAAC;YACF,IAAI,CAAC,oBAAoB,EAAE,CAAC;gBAC1B,MAAM,IAAI,KAAK,CACb,yCAAyC,CAAC,mBAAmB,CAC9D,CAAC;YACJ,CAAC;QACH,CAAC;QAED,MAAM,YAAY,GAAG,qBAAqB,CAAC;YACzC,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,YAAY,EAAE,KAAK,CAAC,YAAY;YAChC,aAAa,EAAE,KAAK,CAAC,QAAQ;SAC9B,CAAC,CAAC;QACH,MAAM,cAAc,GAAG,iBAAiB,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;QACpE,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QACjD,MAAM,SAAS,GAAG,sBAAsB,CAAC;YACvC,UAAU;YACV,eAAe,EAAE,OAAO,CAAC,gBAAgB;gBACvC,CAAC,CAAC,KAAK,CAAC,eAAe;gBACvB,CAAC,CAAC,SAAS;SACd,CAAC,CAAC;QACH,MAAM,eAAe,GAAG,SAAS,GAAG,UAAU,CAAC;QAE/C,MAAM,QAAQ,GAAG,QAAQ;YACvB,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,MAAM,IAAI,CAAC,uBAAuB,CAAC;gBACjC,OAAO;gBACP,OAAO;gBACP,eAAe;gBACf,YAAY,EAAE,KAAK,CAAC,OAAO;gBAC3B,YAAY;gBACZ,cAAc;gBACd,UAAU;gBACV,eAAe;gBACf,SAAS;aACV,CAAC,CAAC;QACP,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,MAAM,IAAI,CAAC,aAAa,CAAC;oBACvB,OAAO,EAAE,OAAO,CAAC,YAAY;oBAC7B,OAAO;oBACP,cAAc,EAAE,QAAQ,CAAC,QAAQ,CAAC,cAAc;oBAChD,SAAS,EAAE,QAAQ,CAAC,QAAQ,CAAC,SAAS;oBACtC,WAAW,EAAE,QAAQ,CAAC,QAAQ,CAAC,WAAW;oBAC1C,YAAY,EAAE,QAAQ,CAAC,QAAQ,CAAC,YAAY;iBAC7C,CAAC,CAAC;YACL,CAAC;YACD,OAAO;gBACL,cAAc,EAAE,QAAQ,CAAC,QAAQ,CAAC,cAAc;gBAChD,WAAW,EAAE,QAAQ;aACtB,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAG,mCAAmC,CAAC;YACnD,eAAe;YACf,gBAAgB,EAAE,OAAO,CAAC,YAAY;YACtC,SAAS;YACT,YAAY,EAAE,KAAK,CAAC,OAAO;YAC3B,YAAY;YACZ,cAAc;YACd,SAAS;SACV,CAAC,CAAC;QAEH,MAAM,SAAS,GAAG,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CAC3C,qCAAqC,EACrC,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,CAClC,CAAQ,CAAC;QAEV,MAAM,gBAAgB,GAAG,EAAE,GAAG,QAAQ,EAAE,SAAS,EAAE,CAAC;QAEpD,MAAM,cAAc,GAAG,cAAc,CAAC;YACpC,GAAG,QAAQ;YACX,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;YAC3B,SAAS;SACV,CAAC,CAAC;QAEH,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CAC7C,kCAAkC,EAClC;gBACE,gBAAgB;gBAChB,OAAO;aACR,CACF,CAAC;YAEF,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;gBACxB,MAAM,IAAI,KAAK,CACb,GAAG,yCAAyC,CAAC,uBAAuB,KAClE,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,eACrC,EAAE,CACH,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC;gBACjC,MAAM,IAAI,KAAK,CACb,yCAAyC,CAAC,0BAA0B,CACrE,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,gBAAgB,CAAC,YAAY,CAAC,cAAc,EAAE,cAAc,CAAC,EAAE,CAAC;gBACnE,MAAM,IAAI,KAAK,CACb,yCAAyC,CAAC,2BAA2B,CACtE,CAAC;YACJ,CAAC;QACH,CAAC;QAED,MAAM,SAAS,GAAQ,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;QAExD,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CACxB,kDAAkD,EAClD;YACE,gBAAgB;YAChB,QAAQ,EAAE;gBACR,cAAc;gBACd,UAAU,EAAE,OAAO;gBACnB,SAAS;gBACT,WAAW,EAAE,KAAK,CAAC,MAAM;gBACzB,YAAY,EAAE,KAAK,CAAC,OAAO;gBAC3B,IAAI,EAAE,iCAAiC;aACxC;SACF,CACF,CAAC;QAEF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,CAAC,aAAa,CAAC;gBACvB,OAAO,EAAE,OAAO,CAAC,YAAY;gBAC7B,OAAO;gBACP,cAAc;gBACd,SAAS;gBACT,WAAW,EAAE,KAAK,CAAC,MAAM;gBACzB,YAAY,EAAE,KAAK,CAAC,OAAO;aAC5B,CAAC,CAAC;QACL,CAAC;QAED,OAAO;YACL,cAAc;YACd,WAAW,EAAE,SAAS;SACvB,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,uBAAuB,CAAC,EAC5B,OAAO,EACP,OAAO,EACP,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,UAAU,EACV,eAAe,EACf,SAAS,GAWV;QACC,MAAM,OAAO,GAAG,iCAAiC,CAAC;YAChD,gBAAgB,EAAE,OAAO,CAAC,YAAY;YACtC,eAAe;YACf,OAAO;YACP,YAAY;YACZ,YAAY;YACZ,cAAc;YACd,UAAU;YACV,eAAe;YACf,SAAS;SACV,CAAC,CAAC;QAEH,MAAM,mBAAmB,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CACpD,iDAAiD,CAClD,CAAC;QAEF,OAAO,wCAAwC,CAC7C,mBAAmB,CAAC,MAAM,CAAC,OAAO,CAAC,EACnC,SAAS,CACV,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,qBAAqB,CACzB,OAAoB,EACpB,iBAAoC;QAEpC,MAAM,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CACjD,sCAAsC,CACvC,CAAC;QACF,MAAM,WAAW,GAAG,0BAA0B,CAAC,kBAAkB,CAAC,CAAC;QACnE,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CACb,yCAAyC,CAAC,8BAA8B,CACzE,CAAC;QACJ,CAAC;QAED,MAAM,EAAE,OAAO,EAAE,GAAG,WAAW,CAAC;QAChC,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CACxC,mCAAmC,CACpC,CAAC;QACF,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ;aAC3B,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC;YACxC,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,iBAAiB,CAAC,CAAC;QAClE,MAAM,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,IAAI,CAC/C,CAAC,KAAK,EAAuC,EAAE,CAC7C,KAAK,CAAC,IAAI,KAAK,aAAa,CAAC,QAAQ;YACrC,KAAK,CAAC,gBAAgB,KAAK,mBAAmB,CAAC,UAAU;YACzD,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,KAAK,IAAI,CAC7C,CAAC;QACF,MAAM,KAAK,GAAG,aAAa,EAAE,MAAM,EAAE,IAAI,CACvC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,KAAK,OAAO,CACrC,CAAC;QACF,MAAM,KAAK,GAAG,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;QAC/B,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,EAAE,eAAe,IAAI,CAAC,KAAK,EAAE,CAAC;YAChD,MAAM,IAAI,KAAK,CACb,yCAAyC,CAAC,4BAA4B,CACvE,CAAC;QACJ,CAAC;QAED,OAAO;YACL,OAAO;YACP,eAAe,EAAE,KAAK,CAAC,eAAe;YACtC,SAAS;YACT,KAAK;YACL,KAAK;SACN,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,aAAa,CAAC,MAAgC;QAClD,MAAM,eAAe,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CAChD,qCAAqC,EACrC,MAAM,CAAC,OAAO,CACf,CAAC;QAEF,MAAM,eAAe,GAAG,eAAe,CAAC,IAAI,CAC1C,CAAC,MAAM,EAAE,EAAE,CACT,gBAAgB,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,cAAc,CAAC;YAC9D,MAAM,CAAC,MAAM,KAAK,QAAQ,CAC7B,CAAC;QAEF,IAAI,eAAe,EAAE,CAAC;YACpB,OAAO;QACT,CAAC;QAED,MAAM,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,MAAgC;QAClD,wEAAwE;QACxE,0EAA0E;QAC1E,2EAA2E;QAC3E,wEAAwE;QACxE,uEAAuE;QACvE,WAAW;QACX,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,+BAA+B,EAAE;YAC1D;gBACE,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,cAAc,EAAE,MAAM,CAAC,cAAc;gBACrC,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,QAAQ,EAAE;oBACR,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;oBAC/B,YAAY,EAAE,MAAM,CAAC,YAAY;oBACjC,IAAI,EAAE,iCAEe;iBACtB;aACF;SACF,CAAC,CAAC;IACL,CAAC;CACF","sourcesContent":["import type {\n DelegationResponse,\n AuthenticatedUserStorageServiceCreateDelegationAction,\n AuthenticatedUserStorageServiceListDelegationsAction,\n} from '@metamask/authenticated-user-storage';\nimport type {\n ChompApiServiceCreateIntentsAction,\n ChompApiServiceGetIntentsByAddressAction,\n ChompApiServiceVerifyDelegationAction,\n} from '@metamask/chomp-api-service';\nimport type { DelegationControllerSignDelegationAction } from '@metamask/delegation-controller';\nimport { hashDelegation } from '@metamask/delegation-core';\nimport { DELEGATOR_CONTRACTS } from '@metamask/delegation-deployments';\nimport type { Messenger } from '@metamask/messenger';\nimport type { MoneyAccountBalanceServiceFetchBalanceWithFallbackAction } from '@metamask/money-account-balance-service';\nimport {\n getMoneyAccountVaultConfig,\n MUSD_DECIMALS,\n} from '@metamask/money-account-utils';\nimport type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller';\nimport { add0x, hexToNumber } from '@metamask/utils';\nimport type { Hex } from '@metamask/utils';\n\nimport { SubscriptionDelegationServiceErrorMessage } from '../constants.js';\nimport type { SubscriptionControllerGetPricingAction } from '../SubscriptionController-method-action-types.js';\nimport { CRYPTO_AUTH_METHODS, PAYMENT_TYPES, PRODUCT_TYPES } from '../types.js';\nimport type {\n ProductPrice,\n ProductType,\n PricingCryptoPaymentMethod,\n RecurringInterval,\n TokenPaymentInfo,\n} from '../types.js';\nimport {\n assertPositiveInteger,\n calculatePeriodAmount,\n getDelegationStartDate,\n getPeriodDuration,\n} from './amount.js';\nimport { buildUnsignedSubscriptionDelegation } from './caveats.js';\nimport {\n equalsIgnoreCase,\n makeMatchesSubscriptionDelegation,\n pickLatestMatchingSubscriptionDelegation,\n} from './fingerprint.js';\nimport type { SubscriptionDelegationServiceMethodActions } from './SubscriptionDelegationService-method-action-types.js';\nimport type {\n MoneyAccountBalanceCheckRequest,\n MoneyAccountBalanceCheckResult,\n PrepareSubscriptionDelegationRequest,\n PreparedSubscriptionDelegation,\n SubscriptionDelegationEnforcers,\n} from './types.js';\nimport { CASH_SUBSCRIPTION_DELEGATION_TYPE } from './types.js';\n\n/**\n * The name of the {@link SubscriptionDelegationService}, used to namespace the\n * service's actions and events.\n */\nexport const serviceName = 'SubscriptionDelegationService';\n\nconst MESSENGER_EXPOSED_METHODS = [\n 'prepareDelegation',\n 'checkMoneyAccountBalance',\n] as const;\n\nconst DELEGATION_FRAMEWORK_VERSION = '1.3.0';\n\nfunction resolveEnforcers(chainId: Hex): SubscriptionDelegationEnforcers {\n const contracts =\n DELEGATOR_CONTRACTS[DELEGATION_FRAMEWORK_VERSION]?.[hexToNumber(chainId)];\n\n if (!contracts?.ValueLteEnforcer || !contracts.ERC20PeriodTransferEnforcer) {\n throw new Error(\n `${SubscriptionDelegationServiceErrorMessage.DelegationContractsNotFound}: ${chainId}`,\n );\n }\n\n return {\n valueLte: contracts.ValueLteEnforcer,\n erc20TokenPeriodTransfer: contracts.ERC20PeriodTransferEnforcer,\n };\n}\n\n/**\n * Actions that {@link SubscriptionDelegationService} exposes to other consumers.\n */\nexport type SubscriptionDelegationServiceActions =\n SubscriptionDelegationServiceMethodActions;\n\n/**\n * Actions from other messengers that {@link SubscriptionDelegationServiceMessenger} calls.\n */\ntype AllowedActions =\n | AuthenticatedUserStorageServiceListDelegationsAction\n | AuthenticatedUserStorageServiceCreateDelegationAction\n | ChompApiServiceVerifyDelegationAction\n | ChompApiServiceCreateIntentsAction\n | ChompApiServiceGetIntentsByAddressAction\n | DelegationControllerSignDelegationAction\n | MoneyAccountBalanceServiceFetchBalanceWithFallbackAction\n | RemoteFeatureFlagControllerGetStateAction\n | SubscriptionControllerGetPricingAction;\n\n/**\n * Events that {@link SubscriptionDelegationService} exposes to other consumers.\n */\nexport type SubscriptionDelegationServiceEvents = never;\n\ntype AllowedEvents = never;\n\n/**\n * The messenger which is restricted to actions and events accessed by\n * {@link SubscriptionDelegationService}.\n */\nexport type SubscriptionDelegationServiceMessenger = Messenger<\n typeof serviceName,\n SubscriptionDelegationServiceActions | AllowedActions,\n SubscriptionDelegationServiceEvents | AllowedEvents\n>;\n\n/**\n * Options for constructing {@link SubscriptionDelegationService}.\n */\nexport type SubscriptionDelegationServiceOptions = {\n messenger: SubscriptionDelegationServiceMessenger;\n};\n\ntype SubscriptionIntentParams = {\n account: Hex;\n chainId: Hex;\n delegationHash: Hex;\n allowance: Hex;\n tokenSymbol: string;\n tokenAddress: Hex;\n};\n\ntype ResolvedSubscriptionDelegationConfig = {\n chainId: Hex;\n delegateAddress: Hex;\n enforcers: SubscriptionDelegationEnforcers;\n price: ProductPrice;\n token: TokenPaymentInfo;\n};\n\n/**\n * Stateless orchestrator for cash-subscription delegation setup.\n *\n * Owns the workflow: size periodic caveats → sign → CHOMP verify → persist to\n * Authenticated User Storage → register CHOMP intent. Returns a verified\n * `delegationHash` for `SubscriptionController.startSubscriptionWithCrypto`.\n *\n * Alpha callers must pass `skipChompInteractions: true` until a follow-up\n * `@metamask/chomp-api-service` release accepts `'cash-subscription'` intent\n * metadata. The CHOMP-enabled path (`skipChompInteractions` unset/false)\n * remains dormant and is not production-ready without that package support.\n *\n * Each call resolves the Money Account chain from remote feature flags, then\n * resolves its price, payment token, and delegate from `SubscriptionController`\n * pricing. The pricing `delegateAddress` is used as the delegation `delegate`.\n *\n * Does not own subscription state; `SubscriptionController` does not depend on\n * this service. Only Money Account Plus is supported.\n */\nexport class SubscriptionDelegationService {\n readonly name: typeof serviceName = serviceName;\n\n readonly #messenger: SubscriptionDelegationServiceMessenger;\n\n constructor(options: SubscriptionDelegationServiceOptions) {\n this.#messenger = options.messenger;\n\n this.#messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n }\n\n /**\n * Checks whether the Money Account holds enough convertible mUSD value to\n * cover pricing `unitAmount × minBillingCyclesForBalance`.\n *\n * @param request - Payer address and pricing amount fields.\n * @returns Balance comparison in mUSD base units (6 decimals).\n */\n async checkMoneyAccountBalance(\n request: MoneyAccountBalanceCheckRequest,\n ): Promise<MoneyAccountBalanceCheckResult> {\n const { price } = await this.#resolveConfiguration(\n request.product,\n request.recurringInterval,\n );\n return this.#compareMoneyAccountBalance(request.payerAddress, price);\n }\n\n async #compareMoneyAccountBalance(\n payerAddress: Hex,\n price: ProductPrice,\n ): Promise<MoneyAccountBalanceCheckResult> {\n assertPositiveInteger(\n price.minBillingCyclesForBalance,\n SubscriptionDelegationServiceErrorMessage.InvalidMinimumFundingCycles,\n );\n\n const periodAmount = calculatePeriodAmount({\n unitAmount: price.unitAmount,\n unitDecimals: price.unitDecimals,\n tokenDecimals: MUSD_DECIMALS,\n });\n const requiredBalance =\n periodAmount * BigInt(price.minBillingCyclesForBalance);\n\n const { totalBalance } = await this.#messenger.call(\n 'MoneyAccountBalanceService:fetchBalanceWithFallback',\n payerAddress,\n );\n\n return {\n hasSufficientBalance: BigInt(totalBalance) >= requiredBalance,\n balance: totalBalance,\n requiredBalance: requiredBalance.toString(),\n };\n }\n\n /**\n * Prepares a cash-subscription delegation and returns its hash.\n *\n * Reuses a stored AUS delegation that matches the semantic fingerprint when\n * one exists (ensuring a CHOMP intent is active for its hash, unless\n * `skipChompInteractions` is true). Reuse classifies period `startDate` as\n * trial-deferred (`> now`) vs immediately redeemable, matching creation.\n * When several records match, the latest period `startDate` is reused\n * so a `forceNew` replacement is preferred over an older equivalent\n * permission.\n * If there is no match, builds, signs, optionally verifies with CHOMP,\n * persists, and optionally registers a new delegation.\n *\n * When `skipChompInteractions` is true (required for alpha), CHOMP verify\n * and intent calls are skipped; the returned hash is computed locally. The\n * default CHOMP-enabled path requires a follow-up chomp-api-service release\n * that accepts `'cash-subscription'` intent metadata.\n *\n * @param request - Authoritative pricing and payer details for the delegation.\n * @param forceNew - Whether to create a replacement instead of reusing a\n * matching stored delegation.\n * @returns The delegation hash (CHOMP-verified unless skipped) and whether it\n * was created or reused.\n */\n async prepareDelegation(\n request: PrepareSubscriptionDelegationRequest,\n forceNew = false,\n ): Promise<PreparedSubscriptionDelegation> {\n if (request.product !== PRODUCT_TYPES.MONEY_ACCOUNT_PLUS) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.UnsupportedProduct,\n );\n }\n\n const skipChomp = Boolean(request.skipChompInteractions);\n\n const { chainId, delegateAddress, enforcers, price, token } =\n await this.#resolveConfiguration(\n request.product,\n request.recurringInterval,\n );\n\n if (request.checkBalance) {\n const { hasSufficientBalance } = await this.#compareMoneyAccountBalance(\n request.payerAddress,\n price,\n );\n if (!hasSufficientBalance) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.InsufficientBalance,\n );\n }\n }\n\n const periodAmount = calculatePeriodAmount({\n unitAmount: price.unitAmount,\n unitDecimals: price.unitDecimals,\n tokenDecimals: token.decimals,\n });\n const periodDuration = getPeriodDuration(request.recurringInterval);\n const nowSeconds = Math.floor(Date.now() / 1000);\n const startDate = getDelegationStartDate({\n nowSeconds,\n trialPeriodDays: request.isTrialRequested\n ? price.trialPeriodDays\n : undefined,\n });\n const isTrialDeferred = startDate > nowSeconds;\n\n const reusable = forceNew\n ? undefined\n : await this.#findReusableDelegation({\n request,\n chainId,\n delegateAddress,\n tokenAddress: token.address,\n periodAmount,\n periodDuration,\n nowSeconds,\n isTrialDeferred,\n enforcers,\n });\n if (reusable) {\n if (!skipChomp) {\n await this.#ensureIntent({\n account: request.payerAddress,\n chainId,\n delegationHash: reusable.metadata.delegationHash,\n allowance: reusable.metadata.allowance,\n tokenSymbol: reusable.metadata.tokenSymbol,\n tokenAddress: reusable.metadata.tokenAddress,\n });\n }\n return {\n delegationHash: reusable.metadata.delegationHash,\n disposition: 'reused',\n };\n }\n\n const unsigned = buildUnsignedSubscriptionDelegation({\n delegateAddress,\n delegatorAddress: request.payerAddress,\n enforcers,\n tokenAddress: token.address,\n periodAmount,\n periodDuration,\n startDate,\n });\n\n const signature = (await this.#messenger.call(\n 'DelegationController:signDelegation',\n { delegation: unsigned, chainId },\n )) as Hex;\n\n const signedDelegation = { ...unsigned, signature };\n\n const delegationHash = hashDelegation({\n ...unsigned,\n salt: BigInt(unsigned.salt),\n signature,\n });\n\n if (!skipChomp) {\n const verifyResult = await this.#messenger.call(\n 'ChompApiService:verifyDelegation',\n {\n signedDelegation,\n chainId,\n },\n );\n\n if (!verifyResult.valid) {\n throw new Error(\n `${SubscriptionDelegationServiceErrorMessage.ChompRejectedDelegation}: ${\n verifyResult.errors?.join(', ') ?? 'unknown error'\n }`,\n );\n }\n\n if (!verifyResult.delegationHash) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.ChompMissingDelegationHash,\n );\n }\n if (!equalsIgnoreCase(verifyResult.delegationHash, delegationHash)) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.ChompDelegationHashMismatch,\n );\n }\n }\n\n const allowance: Hex = add0x(periodAmount.toString(16));\n\n await this.#messenger.call(\n 'AuthenticatedUserStorageService:createDelegation',\n {\n signedDelegation,\n metadata: {\n delegationHash,\n chainIdHex: chainId,\n allowance,\n tokenSymbol: token.symbol,\n tokenAddress: token.address,\n type: CASH_SUBSCRIPTION_DELEGATION_TYPE,\n },\n },\n );\n\n if (!skipChomp) {\n await this.#createIntent({\n account: request.payerAddress,\n chainId,\n delegationHash,\n allowance,\n tokenSymbol: token.symbol,\n tokenAddress: token.address,\n });\n }\n\n return {\n delegationHash,\n disposition: 'created',\n };\n }\n\n async #findReusableDelegation({\n request,\n chainId,\n delegateAddress,\n tokenAddress,\n periodAmount,\n periodDuration,\n nowSeconds,\n isTrialDeferred,\n enforcers,\n }: {\n request: PrepareSubscriptionDelegationRequest;\n chainId: Hex;\n delegateAddress: Hex;\n tokenAddress: Hex;\n periodAmount: bigint;\n periodDuration: number;\n nowSeconds: number;\n isTrialDeferred: boolean;\n enforcers: SubscriptionDelegationEnforcers;\n }): Promise<DelegationResponse | undefined> {\n const matches = makeMatchesSubscriptionDelegation({\n delegatorAddress: request.payerAddress,\n delegateAddress,\n chainId,\n tokenAddress,\n periodAmount,\n periodDuration,\n nowSeconds,\n isTrialDeferred,\n enforcers,\n });\n\n const existingDelegations = await this.#messenger.call(\n 'AuthenticatedUserStorageService:listDelegations',\n );\n\n return pickLatestMatchingSubscriptionDelegation(\n existingDelegations.filter(matches),\n enforcers,\n );\n }\n\n async #resolveConfiguration(\n product: ProductType,\n recurringInterval: RecurringInterval,\n ): Promise<ResolvedSubscriptionDelegationConfig> {\n const { remoteFeatureFlags } = this.#messenger.call(\n 'RemoteFeatureFlagController:getState',\n );\n const vaultConfig = getMoneyAccountVaultConfig(remoteFeatureFlags);\n if (!vaultConfig) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.MissingMoneyAccountVaultConfig,\n );\n }\n\n const { chainId } = vaultConfig;\n const enforcers = resolveEnforcers(chainId);\n const pricing = await this.#messenger.call(\n 'SubscriptionController:getPricing',\n );\n const price = pricing.products\n .find((entry) => entry.name === product)\n ?.prices?.find((entry) => entry.interval === recurringInterval);\n const paymentMethod = pricing.paymentMethods.find(\n (entry): entry is PricingCryptoPaymentMethod =>\n entry.type === PAYMENT_TYPES.byCrypto &&\n entry.cryptoAuthMethod === CRYPTO_AUTH_METHODS.DELEGATION &&\n entry.products?.includes(product) === true,\n );\n const chain = paymentMethod?.chains?.find(\n (entry) => entry.chainId === chainId,\n );\n const token = chain?.tokens[0];\n if (!price || !chain?.delegateAddress || !token) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.PricingConfigurationNotFound,\n );\n }\n\n return {\n chainId,\n delegateAddress: chain.delegateAddress,\n enforcers,\n price,\n token,\n };\n }\n\n /**\n * Ensures an active CHOMP intent exists for the given delegation hash,\n * registering one when missing or revoked.\n *\n * @param params - Intent identity and metadata.\n * @param params.account - Delegator / payer address.\n * @param params.chainId - Chain ID of the delegation.\n * @param params.delegationHash - Hash of the stored delegation.\n * @param params.allowance - Period allowance stored with the delegation.\n * @param params.tokenSymbol - Payment token symbol.\n * @param params.tokenAddress - Payment token address.\n */\n async #ensureIntent(params: SubscriptionIntentParams): Promise<void> {\n const existingIntents = await this.#messenger.call(\n 'ChompApiService:getIntentsByAddress',\n params.account,\n );\n\n const hasActiveIntent = existingIntents.some(\n (intent) =>\n equalsIgnoreCase(intent.delegationHash, params.delegationHash) &&\n intent.status === 'active',\n );\n\n if (hasActiveIntent) {\n return;\n }\n\n await this.#createIntent(params);\n }\n\n async #createIntent(params: SubscriptionIntentParams): Promise<void> {\n // Published `@metamask/chomp-api-service` only types intent metadata as\n // `'cash-deposit' | 'cash-withdrawal'`. The dormant production path still\n // passes `'cash-subscription'`; a follow-up chomp-api-service release must\n // accept that discriminator before this path is production-ready. Alpha\n // callers must use `skipChompInteractions: true` so this method is not\n // reached.\n await this.#messenger.call('ChompApiService:createIntents', [\n {\n account: params.account,\n delegationHash: params.delegationHash,\n chainId: params.chainId,\n metadata: {\n allowance: params.allowance,\n tokenSymbol: params.tokenSymbol,\n tokenAddress: params.tokenAddress,\n type: CASH_SUBSCRIPTION_DELEGATION_TYPE as\n | 'cash-deposit'\n | 'cash-withdrawal',\n },\n },\n ]);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"SubscriptionDelegationService.js","sourceRoot":"","sources":["../../src/subscription-delegation/SubscriptionDelegationService.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AddResult,\n ApprovalControllerAddRequestAction,\n} from '@metamask/approval-controller';\nimport type {\n DelegationResponse,\n AuthenticatedUserStorageServiceCreateDelegationAction,\n AuthenticatedUserStorageServiceListDelegationsAction,\n} from '@metamask/authenticated-user-storage';\nimport type {\n ChompApiServiceCreateIntentsAction,\n ChompApiServiceGetIntentsByAddressAction,\n ChompApiServiceVerifyDelegationAction,\n} from '@metamask/chomp-api-service';\nimport { ORIGIN_METAMASK } from '@metamask/controller-utils';\nimport type { DelegationControllerSignDelegationAction } from '@metamask/delegation-controller';\nimport { hashDelegation } from '@metamask/delegation-core';\nimport { DELEGATOR_CONTRACTS } from '@metamask/delegation-deployments';\nimport type { Messenger } from '@metamask/messenger';\nimport type { MoneyAccountBalanceServiceFetchBalanceWithFallbackAction } from '@metamask/money-account-balance-service';\nimport type { MoneyAccountUpgradeControllerForceUpgradeAccountAction } from '@metamask/money-account-upgrade-controller';\nimport {\n getMoneyAccountDepositAssetAddress,\n getMoneyAccountVaultConfig,\n MUSD_DECIMALS,\n} from '@metamask/money-account-utils';\nimport type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller';\nimport { add0x, hexToNumber, isStrictHexString } from '@metamask/utils';\nimport type { Hex } from '@metamask/utils';\n\nimport {\n ACTIVE_SUBSCRIPTION_STATUSES,\n SubscriptionControllerErrorMessage,\n SubscriptionDelegationServiceErrorMessage,\n} from '../constants.js';\nimport type {\n SubscriptionControllerGetPricingAction,\n SubscriptionControllerGetSubscriptionsAction,\n SubscriptionControllerStartSubscriptionWithCryptoAction,\n} from '../SubscriptionController-method-action-types.js';\nimport type { SubscriptionControllerGetStateAction } from '../SubscriptionController.js';\nimport { CRYPTO_AUTH_METHODS, PAYMENT_TYPES, PRODUCT_TYPES } from '../types.js';\nimport type {\n ProductPrice,\n ProductType,\n PricingCryptoPaymentMethod,\n RecurringInterval,\n Subscription,\n TokenPaymentInfo,\n} from '../types.js';\nimport {\n assertPositiveInteger,\n calculatePeriodAmount,\n getDelegationStartDate,\n getPeriodDuration,\n} from './amount.js';\nimport { buildUnsignedSubscriptionDelegation } from './caveats.js';\nimport {\n equalsIgnoreCase,\n makeMatchesSubscriptionDelegation,\n pickLatestMatchingSubscriptionDelegation,\n} from './fingerprint.js';\nimport type { SubscriptionDelegationServiceMethodActions } from './SubscriptionDelegationService-method-action-types.js';\nimport {\n buildDelegationTypedData,\n decodeSubscriptionAuthority,\n} from './typed-data.js';\nimport type {\n MoneyAccountBalanceCheckRequest,\n MoneyAccountBalanceCheckResult,\n PrepareSubscriptionDelegationRequest,\n PreparedSubscriptionDelegationBundle,\n PreparedSubscriptionPermission,\n PreparedSubscriptionDelegation,\n SignedSubscriptionDelegation,\n StartSubscriptionWithDelegationRequest,\n StartSubscriptionWithDelegationResult,\n SubscriptionDelegationApprovalResult,\n SubscriptionDelegationEnforcers,\n UnsignedSubscriptionDelegation,\n} from './types.js';\nimport {\n CASH_SUBSCRIPTION_DELEGATION_TYPE,\n SUBSCRIPTION_DELEGATION_APPROVAL_TYPE,\n SUBSCRIPTION_DELEGATION_POLICY_VERSION,\n} from './types.js';\n\n/**\n * The name of the {@link SubscriptionDelegationService}, used to namespace the\n * service's actions and events.\n */\nexport const serviceName = 'SubscriptionDelegationService';\n\nconst MESSENGER_EXPOSED_METHODS = [\n 'prepareDelegation',\n 'checkMoneyAccountBalance',\n 'startSubscriptionWithDelegation',\n] as const;\n\nconst DELEGATION_FRAMEWORK_VERSION = '1.3.0';\n\nfunction resolveEnforcers(chainId: Hex): SubscriptionDelegationEnforcers {\n const contracts =\n DELEGATOR_CONTRACTS[DELEGATION_FRAMEWORK_VERSION]?.[hexToNumber(chainId)];\n\n if (\n !contracts?.ERC20PeriodTransferEnforcer ||\n !contracts.AllowedCalldataEnforcer\n ) {\n throw new Error(\n `${SubscriptionDelegationServiceErrorMessage.DelegationContractsNotFound}: ${chainId}`,\n );\n }\n\n return {\n erc20TokenPeriodTransfer: contracts.ERC20PeriodTransferEnforcer,\n allowedCalldata: contracts.AllowedCalldataEnforcer,\n };\n}\n\nfunction removeDelegationSignature(\n delegation: SignedSubscriptionDelegation,\n): UnsignedSubscriptionDelegation {\n const { signature: _signature, ...unsignedDelegation } = delegation;\n return unsignedDelegation;\n}\n\n/**\n * Actions that {@link SubscriptionDelegationService} exposes to other consumers.\n */\nexport type SubscriptionDelegationServiceActions =\n SubscriptionDelegationServiceMethodActions;\n\n/**\n * Actions from other messengers that {@link SubscriptionDelegationServiceMessenger} calls.\n */\ntype AllowedActions =\n | AuthenticatedUserStorageServiceListDelegationsAction\n | AuthenticatedUserStorageServiceCreateDelegationAction\n | ChompApiServiceVerifyDelegationAction\n | ChompApiServiceCreateIntentsAction\n | ChompApiServiceGetIntentsByAddressAction\n | DelegationControllerSignDelegationAction\n | ApprovalControllerAddRequestAction\n | MoneyAccountUpgradeControllerForceUpgradeAccountAction\n | MoneyAccountBalanceServiceFetchBalanceWithFallbackAction\n | RemoteFeatureFlagControllerGetStateAction\n | SubscriptionControllerGetStateAction\n | SubscriptionControllerGetPricingAction\n | SubscriptionControllerGetSubscriptionsAction\n | SubscriptionControllerStartSubscriptionWithCryptoAction;\n\n/**\n * Events that {@link SubscriptionDelegationService} exposes to other consumers.\n */\nexport type SubscriptionDelegationServiceEvents = never;\n\ntype AllowedEvents = never;\n\n/**\n * The messenger which is restricted to actions and events accessed by\n * {@link SubscriptionDelegationService}.\n */\nexport type SubscriptionDelegationServiceMessenger = Messenger<\n typeof serviceName,\n SubscriptionDelegationServiceActions | AllowedActions,\n SubscriptionDelegationServiceEvents | AllowedEvents\n>;\n\n/**\n * Options for constructing {@link SubscriptionDelegationService}.\n */\nexport type SubscriptionDelegationServiceOptions = {\n messenger: SubscriptionDelegationServiceMessenger;\n};\n\ntype SubscriptionIntentParams = {\n account: Hex;\n chainId: Hex;\n delegationHash: Hex;\n allowance: Hex;\n tokenSymbol: string;\n tokenAddress: Hex;\n};\n\ntype ResolvedSubscriptionDelegationConfig = {\n chainId: Hex;\n delegateAddress: Hex;\n delegationManager: Hex;\n paymentAddress: Hex;\n enforcers: SubscriptionDelegationEnforcers;\n musdAddress: Hex;\n price: ProductPrice;\n token: TokenPaymentInfo;\n};\n\n/**\n * Stateless orchestrator for cash-subscription delegation setup.\n *\n * Owns the workflow: size periodic caveats → sign → CHOMP verify → persist to\n * Authenticated User Storage → register CHOMP intent. Returns a verified\n * `delegationHash` for `SubscriptionController.startSubscriptionWithCrypto`.\n *\n * Pass `skipChompInteractions: true` to skip CHOMP verify and intent\n * registration and return a locally computed delegation hash.\n *\n * Each call resolves the Money Account chain from remote feature flags, then\n * resolves its price, payment token, and delegate from `SubscriptionController`\n * pricing. The pricing `delegateAddress` is used as the delegation `delegate`.\n *\n * Does not own subscription state; `SubscriptionController` does not depend on\n * this service. Only Money Account Plus is supported.\n */\nexport class SubscriptionDelegationService {\n readonly name: typeof serviceName = serviceName;\n\n readonly #messenger: SubscriptionDelegationServiceMessenger;\n\n constructor(options: SubscriptionDelegationServiceOptions) {\n this.#messenger = options.messenger;\n\n this.#messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n }\n\n /**\n * Checks whether the Money Account holds enough convertible mUSD value to\n * cover pricing `unitAmount × minBillingCyclesForBalance`.\n *\n * @param request - Payer address and pricing amount fields.\n * @returns Balance comparison in mUSD base units (6 decimals).\n */\n async checkMoneyAccountBalance(\n request: MoneyAccountBalanceCheckRequest,\n ): Promise<MoneyAccountBalanceCheckResult> {\n const { price } = await this.#resolveConfiguration(\n request.product,\n request.recurringInterval,\n );\n return this.#compareMoneyAccountBalance(request.payerAddress, price);\n }\n\n async #compareMoneyAccountBalance(\n payerAddress: Hex,\n price: ProductPrice,\n ): Promise<MoneyAccountBalanceCheckResult> {\n assertPositiveInteger(\n price.minBillingCyclesForBalance,\n SubscriptionDelegationServiceErrorMessage.InvalidMinimumFundingCycles,\n );\n\n const periodAmount = calculatePeriodAmount({\n unitAmount: price.unitAmount,\n unitDecimals: price.unitDecimals,\n tokenDecimals: MUSD_DECIMALS,\n });\n const requiredBalance =\n periodAmount * BigInt(price.minBillingCyclesForBalance);\n\n const { totalBalance } = await this.#messenger.call(\n 'MoneyAccountBalanceService:fetchBalanceWithFallback',\n payerAddress,\n );\n\n return {\n hasSufficientBalance: BigInt(totalBalance) >= requiredBalance,\n balance: totalBalance,\n requiredBalance: requiredBalance.toString(),\n };\n }\n\n /**\n * Runs the complete Money Account subscription checkout authorization flow.\n *\n * Subscriptions are refreshed first. An active subscription for the product\n * is rejected before Money Account upgrade, approval, delegation signing,\n * persistence, or CHOMP registration.\n *\n * Before the approval, `MoneyAccountUpgradeController` is asked to ensure\n * the Money Account vault delegations and CHOMP intents exist; the\n * Subscription API validates them server-side. The custom approval is the\n * sole consent and funding boundary for the payment permission. No payment\n * delegation signing, persistence, or intent mutation occurs before it\n * returns a funding transaction hash.\n *\n * When `request.skipApproval` is `true`, the approval request and its\n * result validation are skipped and the flow proceeds directly to signing.\n * The caller is then responsible for consent and funding.\n *\n * @param request - Product selection, Money Account identity, and optional\n * `skipApproval` flag.\n * @returns The result from `SubscriptionController:startSubscriptionWithCrypto`.\n */\n async startSubscriptionWithDelegation(\n request: StartSubscriptionWithDelegationRequest,\n ): Promise<StartSubscriptionWithDelegationResult> {\n if (request.product !== PRODUCT_TYPES.MONEY_ACCOUNT_PLUS) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.UnsupportedProduct,\n );\n }\n\n const config = await this.#resolveConfiguration(\n request.product,\n request.recurringInterval,\n );\n if (!equalsIgnoreCase(request.chainId, config.chainId)) {\n throw new Error(SubscriptionDelegationServiceErrorMessage.ChainMismatch);\n }\n const subscriptions = await this.#messenger.call(\n 'SubscriptionController:getSubscriptions',\n );\n this.#assertUserNotSubscribed(subscriptions, request.product);\n const { trialedProducts } = this.#messenger.call(\n 'SubscriptionController:getState',\n );\n const isTrialRequested =\n config.price.trialPeriodDays > 0 &&\n !trialedProducts.includes(request.product);\n\n await this.#messenger.call(\n 'MoneyAccountUpgradeController:forceUpgradeAccount',\n request.payerAddress,\n );\n\n const paymentPermission = await this.#preparePaymentPermission(\n request,\n config,\n isTrialRequested,\n );\n const bundle: PreparedSubscriptionDelegationBundle = {\n policyVersion: SUBSCRIPTION_DELEGATION_POLICY_VERSION,\n account: request.payerAddress,\n chainId: request.chainId,\n permissions: [paymentPermission],\n };\n const approval = request.skipApproval\n ? undefined\n : await this.#requestApproval({\n request,\n price: config.price,\n musdAddress: config.musdAddress,\n bundle,\n });\n\n try {\n if (approval) {\n this.#validateApprovalResult(approval);\n }\n\n const paymentDelegation = await this.#signPaymentPermissionIfRequired(\n paymentPermission,\n config,\n );\n const paymentDelegationHash = await this.#commitPaymentPermission({\n request,\n paymentPermission,\n paymentDelegation,\n config,\n });\n const result = await this.#messenger.call(\n 'SubscriptionController:startSubscriptionWithCrypto',\n {\n products: [request.product],\n isTrialRequested,\n recurringInterval: request.recurringInterval,\n billingCycles: config.price.minBillingCycles,\n chainId: request.chainId,\n payerAddress: request.payerAddress,\n tokenSymbol: config.token.symbol,\n cryptoAuthMethod: CRYPTO_AUTH_METHODS.DELEGATION,\n delegationHash: paymentDelegationHash,\n // Reject if eligibility changed during approval, because the signed\n // delegation start date was derived from the original trial state.\n // This local guard is removed before the backend request.\n assertTrialEligibility: true,\n },\n );\n approval?.resultCallbacks?.success();\n return result;\n } catch (error) {\n approval?.resultCallbacks?.error(error as Error);\n throw error;\n }\n }\n\n /**\n * Rejects when the refreshed list already contains an active subscription\n * for the product. Active includes `active`, `trialing`, and `provisional`.\n *\n * @param subscriptions - Subscriptions returned by the latest refresh.\n * @param product - Product the caller is trying to start.\n */\n #assertUserNotSubscribed(\n subscriptions: Subscription[],\n product: ProductType,\n ): void {\n const alreadySubscribed = subscriptions.some(\n (subscription) =>\n ACTIVE_SUBSCRIPTION_STATUSES.includes(subscription.status) &&\n subscription.products.some((entry) => entry.name === product),\n );\n if (alreadySubscribed) {\n throw new Error(SubscriptionControllerErrorMessage.UserAlreadySubscribed);\n }\n }\n\n async #requestApproval({\n request,\n price,\n musdAddress,\n bundle,\n }: {\n request: StartSubscriptionWithDelegationRequest;\n price: ProductPrice;\n musdAddress: Hex;\n bundle: PreparedSubscriptionDelegationBundle;\n }): Promise<AddResult> {\n const targetAmount =\n calculatePeriodAmount({\n unitAmount: price.unitAmount,\n unitDecimals: price.unitDecimals,\n tokenDecimals: MUSD_DECIMALS,\n }) * BigInt(price.minBillingCyclesForBalance);\n\n return (await this.#messenger.call(\n 'ApprovalController:addRequest',\n {\n origin: ORIGIN_METAMASK,\n type: SUBSCRIPTION_DELEGATION_APPROVAL_TYPE,\n expectsResult: true,\n requestData: {\n bundle,\n funding: {\n useCase: 'subscription',\n destinationAccount: request.payerAddress,\n chainId: request.chainId,\n targetToken: {\n symbol: 'mUSD',\n address: musdAddress,\n },\n targetAmount: targetAmount.toString(),\n },\n },\n },\n true,\n )) as AddResult;\n }\n\n async #preparePaymentPermission(\n request: StartSubscriptionWithDelegationRequest,\n config: ResolvedSubscriptionDelegationConfig,\n isTrialRequested: boolean,\n ): Promise<PreparedSubscriptionPermission> {\n const periodAmount = calculatePeriodAmount({\n unitAmount: config.price.unitAmount,\n unitDecimals: config.price.unitDecimals,\n tokenDecimals: config.token.decimals,\n });\n const periodDuration = getPeriodDuration(request.recurringInterval);\n const nowSeconds = Math.floor(Date.now() / 1000);\n const startDate = getDelegationStartDate({\n nowSeconds,\n trialPeriodDays: isTrialRequested\n ? config.price.trialPeriodDays\n : undefined,\n });\n const matches = makeMatchesSubscriptionDelegation({\n delegatorAddress: request.payerAddress,\n delegateAddress: config.delegateAddress,\n chainId: request.chainId,\n recipientAddress: config.paymentAddress,\n tokenAddress: config.token.address,\n periodAmount,\n periodDuration,\n nowSeconds,\n isTrialDeferred: startDate > nowSeconds,\n enforcers: config.enforcers,\n });\n const reusable = pickLatestMatchingSubscriptionDelegation(\n (\n await this.#messenger.call(\n 'AuthenticatedUserStorageService:listDelegations',\n )\n ).filter(matches),\n config.enforcers,\n );\n const delegation = reusable\n ? removeDelegationSignature(reusable.signedDelegation)\n : buildUnsignedSubscriptionDelegation({\n delegateAddress: config.delegateAddress,\n delegatorAddress: request.payerAddress,\n enforcers: config.enforcers,\n recipientAddress: config.paymentAddress,\n tokenAddress: config.token.address,\n periodAmount,\n periodDuration,\n startDate,\n });\n const typedData = buildDelegationTypedData({\n delegation,\n chainId: request.chainId,\n delegationManager: config.delegationManager,\n });\n\n return {\n id: CASH_SUBSCRIPTION_DELEGATION_TYPE,\n owner: 'subscription',\n disposition: reusable ? 'reused' : 'new',\n delegation,\n typedData,\n decodedAuthority: decodeSubscriptionAuthority(\n delegation,\n config.enforcers,\n ),\n ...(reusable\n ? { existingDelegationHash: reusable.metadata.delegationHash }\n : {}),\n };\n }\n\n #validateApprovalResult(\n approval: AddResult,\n ): SubscriptionDelegationApprovalResult {\n if (!approval?.value || typeof approval.value !== 'object') {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.ApprovalResultMissing,\n );\n }\n const value = approval.value as Record<string, unknown>;\n if (\n typeof value.fundingTransactionHash !== 'string' ||\n !isStrictHexString(value.fundingTransactionHash) ||\n value.fundingTransactionHash.length !== 66\n ) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.InvalidFundingTransactionHash,\n );\n }\n return {\n fundingTransactionHash: value.fundingTransactionHash,\n };\n }\n\n async #signPaymentPermissionIfRequired(\n permission: PreparedSubscriptionPermission,\n config: ResolvedSubscriptionDelegationConfig,\n ): Promise<SignedSubscriptionDelegation | undefined> {\n if (permission.disposition === 'reused') {\n return undefined;\n }\n const signature = (await this.#messenger.call(\n 'DelegationController:signDelegation',\n {\n delegation: permission.delegation,\n chainId: config.chainId,\n },\n )) as Hex;\n return { ...permission.delegation, signature };\n }\n\n async #commitPaymentPermission({\n request,\n paymentPermission,\n paymentDelegation,\n config,\n }: {\n request: StartSubscriptionWithDelegationRequest;\n paymentPermission: PreparedSubscriptionPermission;\n paymentDelegation?: SignedSubscriptionDelegation;\n config: ResolvedSubscriptionDelegationConfig;\n }): Promise<Hex> {\n if (paymentPermission.disposition === 'reused') {\n const delegationHash = paymentPermission.existingDelegationHash as Hex;\n const storedDelegation = (\n await this.#messenger.call(\n 'AuthenticatedUserStorageService:listDelegations',\n )\n ).find(({ metadata }) =>\n equalsIgnoreCase(metadata.delegationHash, delegationHash),\n );\n if (!storedDelegation) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.ReusableDelegationInvalid,\n );\n }\n await this.#verifySignedPaymentDelegation(\n storedDelegation.signedDelegation,\n delegationHash,\n request.chainId,\n );\n await this.#ensureIntent({\n account: request.payerAddress,\n chainId: request.chainId,\n delegationHash,\n allowance: add0x(\n BigInt(paymentPermission.decodedAuthority.periodAmount).toString(16),\n ),\n tokenSymbol: config.token.symbol,\n tokenAddress: config.token.address,\n });\n await this.#assertIntentActive(request.payerAddress, delegationHash);\n return delegationHash;\n }\n\n const signedDelegation = paymentDelegation as SignedSubscriptionDelegation;\n const delegationHash = hashDelegation({\n ...signedDelegation,\n salt: BigInt(signedDelegation.salt),\n });\n await this.#verifySignedPaymentDelegation(\n signedDelegation,\n delegationHash,\n request.chainId,\n );\n\n const allowance = add0x(\n BigInt(paymentPermission.decodedAuthority.periodAmount).toString(16),\n );\n await this.#messenger.call(\n 'AuthenticatedUserStorageService:createDelegation',\n {\n signedDelegation,\n metadata: {\n delegationHash,\n chainIdHex: request.chainId,\n allowance,\n tokenSymbol: config.token.symbol,\n tokenAddress: config.token.address,\n type: CASH_SUBSCRIPTION_DELEGATION_TYPE,\n },\n },\n );\n await this.#createIntent({\n account: request.payerAddress,\n chainId: request.chainId,\n delegationHash,\n allowance,\n tokenSymbol: config.token.symbol,\n tokenAddress: config.token.address,\n });\n await this.#assertIntentActive(request.payerAddress, delegationHash);\n return delegationHash;\n }\n\n async #assertIntentActive(account: Hex, delegationHash: Hex): Promise<void> {\n const intents = await this.#messenger.call(\n 'ChompApiService:getIntentsByAddress',\n account,\n );\n const intent = intents.find((candidate) =>\n equalsIgnoreCase(candidate.delegationHash, delegationHash),\n );\n if (intent?.status !== 'active') {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.ChompIntentNotActive,\n );\n }\n }\n\n async #verifySignedPaymentDelegation(\n signedDelegation: SignedSubscriptionDelegation,\n expectedDelegationHash: Hex,\n chainId: Hex,\n ): Promise<void> {\n const localHash = hashDelegation({\n ...signedDelegation,\n salt: BigInt(signedDelegation.salt),\n });\n if (!equalsIgnoreCase(localHash, expectedDelegationHash)) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.ReusableDelegationInvalid,\n );\n }\n\n const verifyResult = await this.#messenger.call(\n 'ChompApiService:verifyDelegation',\n { signedDelegation, chainId },\n );\n if (!verifyResult.valid) {\n throw new Error(\n `${SubscriptionDelegationServiceErrorMessage.ChompRejectedDelegation}: ${\n verifyResult.errors?.join(', ') ?? 'unknown error'\n }`,\n );\n }\n if (!verifyResult.delegationHash) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.ChompMissingDelegationHash,\n );\n }\n if (\n !equalsIgnoreCase(verifyResult.delegationHash, expectedDelegationHash)\n ) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.ChompDelegationHashMismatch,\n );\n }\n }\n\n /**\n * Prepares a cash-subscription delegation and returns its hash.\n *\n * Reuses a stored AUS delegation that matches the semantic fingerprint when\n * one exists (ensuring a CHOMP intent is active for its hash, unless\n * `skipChompInteractions` is true). Reuse classifies period `startDate` as\n * trial-deferred (`> now`) vs immediately redeemable, matching creation.\n * When several records match, the latest period `startDate` is reused\n * so a `forceNew` replacement is preferred over an older equivalent\n * permission.\n * If there is no match, builds, signs, optionally verifies with CHOMP,\n * persists, and optionally registers a new delegation.\n *\n * When `skipChompInteractions` is true, CHOMP verify and intent calls are\n * skipped; the returned hash is computed locally.\n *\n * @param request - Authoritative pricing and payer details for the delegation.\n * @param forceNew - Whether to create a replacement instead of reusing a\n * matching stored delegation.\n * @returns The delegation hash (CHOMP-verified unless skipped) and whether it\n * was created or reused.\n */\n async prepareDelegation(\n request: PrepareSubscriptionDelegationRequest,\n forceNew = false,\n ): Promise<PreparedSubscriptionDelegation> {\n if (request.product !== PRODUCT_TYPES.MONEY_ACCOUNT_PLUS) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.UnsupportedProduct,\n );\n }\n\n const skipChomp = Boolean(request.skipChompInteractions);\n\n const {\n chainId,\n delegateAddress,\n paymentAddress,\n enforcers,\n price,\n token,\n } = await this.#resolveConfiguration(\n request.product,\n request.recurringInterval,\n );\n\n if (request.checkBalance) {\n const { hasSufficientBalance } = await this.#compareMoneyAccountBalance(\n request.payerAddress,\n price,\n );\n if (!hasSufficientBalance) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.InsufficientBalance,\n );\n }\n }\n\n const periodAmount = calculatePeriodAmount({\n unitAmount: price.unitAmount,\n unitDecimals: price.unitDecimals,\n tokenDecimals: token.decimals,\n });\n const periodDuration = getPeriodDuration(request.recurringInterval);\n const nowSeconds = Math.floor(Date.now() / 1000);\n const startDate = getDelegationStartDate({\n nowSeconds,\n trialPeriodDays: request.isTrialRequested\n ? price.trialPeriodDays\n : undefined,\n });\n const isTrialDeferred = startDate > nowSeconds;\n\n const reusable = forceNew\n ? undefined\n : await this.#findReusableDelegation({\n request,\n chainId,\n delegateAddress,\n paymentAddress,\n tokenAddress: token.address,\n periodAmount,\n periodDuration,\n nowSeconds,\n isTrialDeferred,\n enforcers,\n });\n if (reusable) {\n if (!skipChomp) {\n await this.#ensureIntent({\n account: request.payerAddress,\n chainId,\n delegationHash: reusable.metadata.delegationHash,\n allowance: reusable.metadata.allowance,\n tokenSymbol: reusable.metadata.tokenSymbol,\n tokenAddress: reusable.metadata.tokenAddress,\n });\n }\n return {\n delegationHash: reusable.metadata.delegationHash,\n disposition: 'reused',\n };\n }\n\n const unsigned = buildUnsignedSubscriptionDelegation({\n delegateAddress,\n delegatorAddress: request.payerAddress,\n recipientAddress: paymentAddress,\n enforcers,\n tokenAddress: token.address,\n periodAmount,\n periodDuration,\n startDate,\n });\n\n const signature = (await this.#messenger.call(\n 'DelegationController:signDelegation',\n { delegation: unsigned, chainId },\n )) as Hex;\n\n const signedDelegation = { ...unsigned, signature };\n\n const delegationHash = hashDelegation({\n ...unsigned,\n salt: BigInt(unsigned.salt),\n signature,\n });\n\n if (!skipChomp) {\n const verifyResult = await this.#messenger.call(\n 'ChompApiService:verifyDelegation',\n {\n signedDelegation,\n chainId,\n },\n );\n\n if (!verifyResult.valid) {\n throw new Error(\n `${SubscriptionDelegationServiceErrorMessage.ChompRejectedDelegation}: ${\n verifyResult.errors?.join(', ') ?? 'unknown error'\n }`,\n );\n }\n\n if (!verifyResult.delegationHash) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.ChompMissingDelegationHash,\n );\n }\n if (!equalsIgnoreCase(verifyResult.delegationHash, delegationHash)) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.ChompDelegationHashMismatch,\n );\n }\n }\n\n const allowance: Hex = add0x(periodAmount.toString(16));\n\n await this.#messenger.call(\n 'AuthenticatedUserStorageService:createDelegation',\n {\n signedDelegation,\n metadata: {\n delegationHash,\n chainIdHex: chainId,\n allowance,\n tokenSymbol: token.symbol,\n tokenAddress: token.address,\n type: CASH_SUBSCRIPTION_DELEGATION_TYPE,\n },\n },\n );\n\n if (!skipChomp) {\n await this.#createIntent({\n account: request.payerAddress,\n chainId,\n delegationHash,\n allowance,\n tokenSymbol: token.symbol,\n tokenAddress: token.address,\n });\n }\n\n return {\n delegationHash,\n disposition: 'created',\n };\n }\n\n async #findReusableDelegation({\n request,\n chainId,\n delegateAddress,\n paymentAddress,\n tokenAddress,\n periodAmount,\n periodDuration,\n nowSeconds,\n isTrialDeferred,\n enforcers,\n }: {\n request: PrepareSubscriptionDelegationRequest;\n chainId: Hex;\n delegateAddress: Hex;\n paymentAddress: Hex;\n tokenAddress: Hex;\n periodAmount: bigint;\n periodDuration: number;\n nowSeconds: number;\n isTrialDeferred: boolean;\n enforcers: SubscriptionDelegationEnforcers;\n }): Promise<DelegationResponse | undefined> {\n const matches = makeMatchesSubscriptionDelegation({\n delegatorAddress: request.payerAddress,\n delegateAddress,\n recipientAddress: paymentAddress,\n chainId,\n tokenAddress,\n periodAmount,\n periodDuration,\n nowSeconds,\n isTrialDeferred,\n enforcers,\n });\n\n const existingDelegations = await this.#messenger.call(\n 'AuthenticatedUserStorageService:listDelegations',\n );\n\n return pickLatestMatchingSubscriptionDelegation(\n existingDelegations.filter(matches),\n enforcers,\n );\n }\n\n async #resolveConfiguration(\n product: ProductType,\n recurringInterval: RecurringInterval,\n ): Promise<ResolvedSubscriptionDelegationConfig> {\n const { remoteFeatureFlags } = this.#messenger.call(\n 'RemoteFeatureFlagController:getState',\n );\n const vaultConfig = getMoneyAccountVaultConfig(remoteFeatureFlags);\n if (!vaultConfig) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.MissingMoneyAccountVaultConfig,\n );\n }\n\n const { chainId } = vaultConfig;\n const enforcers = resolveEnforcers(chainId);\n const pricing = await this.#messenger.call(\n 'SubscriptionController:getPricing',\n );\n const price = pricing.products\n .find((entry) => entry.name === product)\n ?.prices?.find((entry) => entry.interval === recurringInterval);\n const paymentMethod = pricing.paymentMethods.find(\n (entry): entry is PricingCryptoPaymentMethod =>\n entry.type === PAYMENT_TYPES.byCrypto &&\n entry.cryptoAuthMethod === CRYPTO_AUTH_METHODS.DELEGATION &&\n entry.products?.includes(product) === true,\n );\n const chain = paymentMethod?.chains?.find(\n (entry) => entry.chainId === chainId,\n );\n const token = chain?.tokens[0];\n if (!price || !chain?.delegateAddress || !token) {\n throw new Error(\n SubscriptionDelegationServiceErrorMessage.PricingConfigurationNotFound,\n );\n }\n\n return {\n chainId,\n delegateAddress: chain.delegateAddress,\n delegationManager:\n DELEGATOR_CONTRACTS[DELEGATION_FRAMEWORK_VERSION][hexToNumber(chainId)]\n .DelegationManager,\n paymentAddress: chain.paymentAddress,\n enforcers,\n musdAddress: getMoneyAccountDepositAssetAddress(chainId),\n price,\n token,\n };\n }\n\n /**\n * Ensures an active CHOMP intent exists for the given delegation hash,\n * registering one when missing or revoked.\n *\n * @param params - Intent identity and metadata.\n * @param params.account - Delegator / payer address.\n * @param params.chainId - Chain ID of the delegation.\n * @param params.delegationHash - Hash of the stored delegation.\n * @param params.allowance - Period allowance stored with the delegation.\n * @param params.tokenSymbol - Payment token symbol.\n * @param params.tokenAddress - Payment token address.\n */\n async #ensureIntent(params: SubscriptionIntentParams): Promise<void> {\n const existingIntents = await this.#messenger.call(\n 'ChompApiService:getIntentsByAddress',\n params.account,\n );\n\n const hasActiveIntent = existingIntents.some(\n (intent) =>\n equalsIgnoreCase(intent.delegationHash, params.delegationHash) &&\n intent.status === 'active',\n );\n\n if (hasActiveIntent) {\n return;\n }\n\n await this.#createIntent(params);\n }\n\n async #createIntent(params: SubscriptionIntentParams): Promise<void> {\n await this.#messenger.call('ChompApiService:createIntents', [\n {\n account: params.account,\n delegationHash: params.delegationHash,\n chainId: params.chainId,\n metadata: {\n allowance: params.allowance,\n tokenSymbol: params.tokenSymbol,\n tokenAddress: params.tokenAddress,\n type: CASH_SUBSCRIPTION_DELEGATION_TYPE,\n },\n },\n ]);\n }\n}\n"]}
|