Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1c0a92a
feat: subscription delegation service orchestration
tuna1207 Sep 7, 2026
e7e9b24
feat: update dependencies and enhance subscription delegation service
tuna1207 Sep 7, 2026
5558a54
feat: integrate SubscriptionDelegationService into wallet initialization
tuna1207 Sep 7, 2026
4a47512
chore: update changelog
tuna1207 Sep 7, 2026
0863253
Merge branch 'main' into feat/subscription-delegation
tuna1207 Sep 7, 2026
2874cd1
fix: lint
tuna1207 Sep 7, 2026
db18342
fix: test
tuna1207 Sep 7, 2026
7c78f04
chore: update readme
tuna1207 Sep 7, 2026
c159d25
feat: enhance SubscriptionDelegationService with balance check and tr…
tuna1207 Sep 9, 2026
0d1c3df
feat: update CHOMP intent types and integrate new cash-subscription m…
tuna1207 Sep 9, 2026
b2f28ea
fix: correct error message
tuna1207 Sep 10, 2026
27aee68
fix: temporarily remove redeemer enforcer
tuna1207 Sep 10, 2026
07b4393
fix: remove redeemer enforcer
tuna1207 Sep 10, 2026
aa2a945
feat: add skipChompInteractions flag to SubscriptionDelegationService…
tuna1207 Sep 10, 2026
18b4329
refactor: remove CHOMP intent types and update related tests in chomp…
tuna1207 Sep 10, 2026
d7770ab
Merge branch 'main' into feat/subscription-delegation
tuna1207 Sep 10, 2026
575b9b7
fix: update changelog
tuna1207 Sep 10, 2026
8a12c4b
feat: enhance subscription delegation logic to support trial periods
tuna1207 Sep 10, 2026
eb76f20
chore: update readme
tuna1207 Sep 10, 2026
d8bb44e
fix: yarn constraints
tuna1207 Sep 10, 2026
5209222
chore: dedupe
tuna1207 Sep 10, 2026
44bda61
chore: update Jest configuration and add custom test environment for …
tuna1207 Sep 10, 2026
7a74893
chore: update readme
tuna1207 Sep 10, 2026
c89c65e
chore: update comment
tuna1207 Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -622,12 +622,18 @@ linkStyle default opacity:0.5
social_controllers --> profile_sync_controller;
solana_test_validator_up --> local_node_utils;
storage_service --> messenger;
subscription_controller --> authenticated_user_storage;
subscription_controller --> base_controller;
subscription_controller --> base_data_service;
subscription_controller --> chomp_api_service;
subscription_controller --> controller_utils;
subscription_controller --> delegation_controller;
subscription_controller --> messenger;
subscription_controller --> money_account_balance_service;
subscription_controller --> money_account_utils;
subscription_controller --> polling_controller;
subscription_controller --> profile_sync_controller;
subscription_controller --> remote_feature_flag_controller;
subscription_controller --> transaction_controller;
transaction_controller --> accounts_controller;
transaction_controller --> approval_controller;
Expand Down
14 changes: 14 additions & 0 deletions packages/subscription-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `SubscriptionDelegationService` for Money Account Plus cash-subscription delegation setup. ([#10130](https://github.com/MetaMask/core/pull/10130))
- New messenger action `SubscriptionDelegationService:prepareDelegation` orchestrates periodic caveat construction, signing, CHOMP verification, Authenticated User Storage persistence, and CHOMP intent registration.
- Returns a verified `delegationHash` with `disposition: 'created' | 'reused'` for `SubscriptionController.startSubscriptionWithCrypto`; the controller does not depend on this service.
- `prepareDelegation` accepts the product, recurring interval, payer address, trial selection, optional balance-check flag, and optional `skipChompInteractions` flag; it resolves plan, token, and delegate data through `SubscriptionController:getPricing`.
- When `skipChompInteractions` is true, CHOMP verify and intent registration are skipped; the returned hash is computed locally and AUS persistence still occurs.
- Resolves the chain from `moneyAccountVaultConfig` and Delegation Framework v1.3.0 enforcers from `@metamask/delegation-deployments`.
- Uses pricing `delegateAddress` as both the delegation `delegate` and the RedeemerEnforcer redeemer.
- Offsets the period-transfer `startDate` by pricing `trialPeriodDays` only when the trial is selected.
- New messenger action `SubscriptionDelegationService:checkMoneyAccountBalance` compares Money Account convertible mUSD balance against pricing `unitAmount × minBillingCyclesForBalance`; `prepareDelegation` can gate on it via `checkBalance`.
- Exports `CASH_SUBSCRIPTION_DELEGATION_TYPE` (`'cash-subscription'`) for AUS metadata (and for CHOMP intent metadata once chomp-api-service supports that type).
- Only Money Account Plus is supported; Shield continues to use ERC-20 approval.

## [9.0.0]

### Changed
Expand Down
2 changes: 2 additions & 0 deletions packages/subscription-controller/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ module.exports = merge(baseConfig, {
// The display name when running multiple projects
displayName,

testEnvironment: '<rootDir>/jest.environment.cjs',

// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
Expand Down
18 changes: 18 additions & 0 deletions packages/subscription-controller/jest.environment.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { TestEnvironment } = require('jest-environment-node');

/**
* Subscription delegation salt generation uses the Web Crypto API
* (`crypto.getRandomValues`), which is not exposed as a global by
* jest-environment-node.
*/
class CustomTestEnvironment extends TestEnvironment {
async setup() {
await super.setup();
if (typeof this.global.crypto === 'undefined') {
const { webcrypto } = require('crypto');
this.global.crypto = webcrypto;
}
}
}

module.exports = CustomTestEnvironment;
9 changes: 9 additions & 0 deletions packages/subscription-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,20 @@
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
},
"dependencies": {
"@metamask/authenticated-user-storage": "^4.0.0",
"@metamask/base-controller": "^10.0.0",
"@metamask/base-data-service": "^2.0.0",
"@metamask/chomp-api-service": "^5.0.0",
"@metamask/controller-utils": "^13.0.0",
"@metamask/delegation-controller": "^4.0.0",
"@metamask/delegation-core": "^2.2.1",
"@metamask/delegation-deployments": "^1.4.0",
"@metamask/messenger": "^3.0.0",
"@metamask/money-account-balance-service": "^3.0.0",
"@metamask/money-account-utils": "^2.0.0",
"@metamask/polling-controller": "^17.0.0",
"@metamask/profile-sync-controller": "^31.0.0",
"@metamask/remote-feature-flag-controller": "^7.0.0",
"@metamask/superstruct": "^3.4.1",
"@metamask/transaction-controller": "^70.0.0",
"@metamask/utils": "^11.12.0",
Expand All @@ -69,6 +77,7 @@
"@typescript/native": "npm:typescript@^7.0.2",
"deepmerge": "^4.2.2",
"jest": "^30.4.2",
"jest-environment-node": "^30.4.1",
"rimraf": "^5.0.5",
"ts-jest": "^29.4.11",
"tsx": "^4.20.5",
Expand Down
17 changes: 17 additions & 0 deletions packages/subscription-controller/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,23 @@ export enum SubscriptionServiceErrorMessage {
FailedToGetBillingPortalUrl = 'Failed to get billing portal url',
}

export enum SubscriptionDelegationServiceErrorMessage {
InvalidAmount = 'Subscription delegation amount must be a non-negative integer',
InvalidDecimals = 'Subscription delegation decimals must be a non-negative integer',
InvalidTrialPeriodDays = 'Subscription delegation trial period days must be a non-negative integer',
InvalidMinimumFundingCycles = 'Subscription delegation minimum funding cycles must be a positive integer',
LossyAmountScale = 'Subscription delegation amount cannot be scaled to token decimals without remainder',
UnsupportedRecurringInterval = 'Unsupported subscription recurring interval',
UnsupportedProduct = 'Subscription delegation is only supported for Money Account',
MissingMoneyAccountVaultConfig = 'Money Account vault configuration is missing or invalid',
DelegationContractsNotFound = 'Subscription delegation contracts were not found for the configured chain',
PricingConfigurationNotFound = 'Subscription delegation pricing configuration was not found',
InsufficientBalance = 'Money Account balance is insufficient for the subscription funding requirement',
ChompRejectedDelegation = 'CHOMP rejected the subscription delegation',
ChompMissingDelegationHash = 'CHOMP verify response did not include a delegation hash',
ChompDelegationHashMismatch = 'CHOMP verify response delegation hash does not match the locally computed hash',
}

export const DEFAULT_POLLING_INTERVAL = 5 * 60 * 1_000; // 5 minutes

export const ACTIVE_SUBSCRIPTION_STATUSES = [
Expand Down
21 changes: 21 additions & 0 deletions packages/subscription-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export {
Env,
SubscriptionControllerErrorMessage,
SubscriptionServiceErrorMessage,
SubscriptionDelegationServiceErrorMessage,
} from './constants.js';
export type {
SubscriptionServiceOptions,
Expand Down Expand Up @@ -160,3 +161,23 @@ export type {
SubscriptionServiceGetPricingAction,
SubscriptionServiceGetBillingPortalUrlAction,
} from './SubscriptionService-method-action-types.js';

export type {
SubscriptionDelegationServiceActions,
SubscriptionDelegationServiceEvents,
SubscriptionDelegationServiceMessenger,
SubscriptionDelegationServiceOptions,
} from './subscription-delegation/SubscriptionDelegationService.js';
export {
SubscriptionDelegationService,
serviceName as subscriptionDelegationServiceName,
} from './subscription-delegation/SubscriptionDelegationService.js';
export type { SubscriptionDelegationServicePrepareDelegationAction } from './subscription-delegation/SubscriptionDelegationService-method-action-types.js';
export type { SubscriptionDelegationServiceCheckMoneyAccountBalanceAction } from './subscription-delegation/SubscriptionDelegationService-method-action-types.js';
export type {
MoneyAccountBalanceCheckRequest,
MoneyAccountBalanceCheckResult,
PrepareSubscriptionDelegationRequest,
PreparedSubscriptionDelegation,
} from './subscription-delegation/types.js';
export { CASH_SUBSCRIPTION_DELEGATION_TYPE } from './subscription-delegation/types.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* This file is auto generated.
* Do not edit manually.
*/

import type { SubscriptionDelegationService } from './SubscriptionDelegationService.js';

/**
* Checks whether the Money Account holds enough convertible mUSD value to
* cover pricing `unitAmount × minBillingCyclesForBalance`.
*
* @param request - Payer address and pricing amount fields.
* @returns Balance comparison in mUSD base units (6 decimals).
*/
export type SubscriptionDelegationServiceCheckMoneyAccountBalanceAction = {
type: `SubscriptionDelegationService:checkMoneyAccountBalance`;
handler: SubscriptionDelegationService['checkMoneyAccountBalance'];
};

/**
* Prepares a cash-subscription delegation and returns its hash.
*
* Reuses a stored AUS delegation that matches the semantic fingerprint when
* one exists (ensuring a CHOMP intent is active for its hash, unless
* `skipChompInteractions` is true). Period `startDate` must still be
* trial-deferred when `isTrialRequested` is true, and immediately redeemable
* otherwise. If there is no match, builds, signs, optionally verifies with
* CHOMP, persists, and optionally registers a new delegation.
*
* When `skipChompInteractions` is true (required for alpha), CHOMP verify
* and intent calls are skipped; the returned hash is computed locally. The
* default CHOMP-enabled path requires a follow-up chomp-api-service release
* that accepts `'cash-subscription'` intent metadata.
*
* @param request - Authoritative pricing and payer details for the delegation.
* @returns The delegation hash (CHOMP-verified unless skipped) and whether it
* was created or reused.
*/
export type SubscriptionDelegationServicePrepareDelegationAction = {
type: `SubscriptionDelegationService:prepareDelegation`;
handler: SubscriptionDelegationService['prepareDelegation'];
};

/**
* Union of all SubscriptionDelegationService action types.
*/
export type SubscriptionDelegationServiceMethodActions =
| SubscriptionDelegationServiceCheckMoneyAccountBalanceAction
| SubscriptionDelegationServicePrepareDelegationAction;
Loading