From 4ac90fbaa433c78109b1baba88985ec995b838d3 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 10 Apr 2026 13:58:53 +0100 Subject: [PATCH 01/26] feat: add scaffold for money account upgrade controller --- .../CHANGELOG.md | 14 +++ .../money-account-upgrade-controller/LICENSE | 21 ++++ .../README.md | 15 +++ .../jest.config.js | 26 +++++ .../package.json | 74 +++++++++++++ ...ntUpgradeController-method-action-types.ts | 23 ++++ .../src/MoneyAccountUpgradeController.test.ts | 87 +++++++++++++++ .../src/MoneyAccountUpgradeController.ts | 100 ++++++++++++++++++ .../src/index.ts | 14 +++ .../tsconfig.build.json | 15 +++ .../tsconfig.json | 11 ++ .../typedoc.json | 7 ++ yarn.lock | 21 +++- 13 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 packages/money-account-upgrade-controller/CHANGELOG.md create mode 100644 packages/money-account-upgrade-controller/LICENSE create mode 100644 packages/money-account-upgrade-controller/README.md create mode 100644 packages/money-account-upgrade-controller/jest.config.js create mode 100644 packages/money-account-upgrade-controller/package.json create mode 100644 packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts create mode 100644 packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts create mode 100644 packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts create mode 100644 packages/money-account-upgrade-controller/src/index.ts create mode 100644 packages/money-account-upgrade-controller/tsconfig.build.json create mode 100644 packages/money-account-upgrade-controller/tsconfig.json create mode 100644 packages/money-account-upgrade-controller/typedoc.json diff --git a/packages/money-account-upgrade-controller/CHANGELOG.md b/packages/money-account-upgrade-controller/CHANGELOG.md new file mode 100644 index 00000000000..7369581f594 --- /dev/null +++ b/packages/money-account-upgrade-controller/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Add `MoneyAccountUpgradeController` with `upgradeAccount` method + +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/money-account-upgrade-controller/LICENSE b/packages/money-account-upgrade-controller/LICENSE new file mode 100644 index 00000000000..fe29e78e0fe --- /dev/null +++ b/packages/money-account-upgrade-controller/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/money-account-upgrade-controller/README.md b/packages/money-account-upgrade-controller/README.md new file mode 100644 index 00000000000..dde58824dd1 --- /dev/null +++ b/packages/money-account-upgrade-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/money-account-upgrade-controller` + +MetaMask Money account upgrade controller. + +## Installation + +`yarn add @metamask/money-account-upgrade-controller` + +or + +`npm install @metamask/money-account-upgrade-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/money-account-upgrade-controller/jest.config.js b/packages/money-account-upgrade-controller/jest.config.js new file mode 100644 index 00000000000..ca084133399 --- /dev/null +++ b/packages/money-account-upgrade-controller/jest.config.js @@ -0,0 +1,26 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const merge = require('deepmerge'); +const path = require('path'); + +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json new file mode 100644 index 00000000000..2516a1a990b --- /dev/null +++ b/packages/money-account-upgrade-controller/package.json @@ -0,0 +1,74 @@ +{ + "name": "@metamask/money-account-upgrade-controller", + "version": "0.1.0", + "description": "MetaMask Money account upgrade controller", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/money-account-upgrade-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", + "build:all": "ts-bridge --project tsconfig.build.json --verbose --clean", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/money-account-upgrade-controller", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/money-account-upgrade-controller", + "messenger-action-types:check": "tsx ../../packages/messenger-cli/src/cli.ts --check", + "messenger-action-types:generate": "tsx ../../packages/messenger-cli/src/cli.ts --generate", + "since-latest-release": "../../scripts/since-latest-release.sh", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", + "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "^9.0.1", + "@metamask/messenger": "^1.1.1" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@ts-bridge/cli": "^0.6.4", + "@types/jest": "^29.5.14", + "deepmerge": "^4.2.2", + "jest": "^29.2.5", + "ts-jest": "^29.2.5", + "tsx": "^4.20.5", + "typedoc": "^0.25.13", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.3.3" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts new file mode 100644 index 00000000000..fb358efefcc --- /dev/null +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts @@ -0,0 +1,23 @@ +/** + * This file is auto generated. + * Do not edit manually. + */ + +import type { MoneyAccountUpgradeController } from './MoneyAccountUpgradeController'; + +/** + * Upgrades a money account. This method iterates over a number of + * steps to perform the upgrade. + * + * @returns A promise that resolves when the upgrade is complete. + */ +export type MoneyAccountUpgradeControllerUpgradeAccountAction = { + type: `MoneyAccountUpgradeController:upgradeAccount`; + handler: MoneyAccountUpgradeController['upgradeAccount']; +}; + +/** + * Union of all MoneyAccountUpgradeController action types. + */ +export type MoneyAccountUpgradeControllerMethodActions = + MoneyAccountUpgradeControllerUpgradeAccountAction; diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts new file mode 100644 index 00000000000..374d1ab8cca --- /dev/null +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -0,0 +1,87 @@ +import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; +import type { + MockAnyNamespace, + MessengerActions, + MessengerEvents, +} from '@metamask/messenger'; + +import type { MoneyAccountUpgradeControllerMessenger } from '.'; +import { + MoneyAccountUpgradeController, + getDefaultMoneyAccountUpgradeControllerState, +} from '.'; + +type AllMoneyAccountUpgradeControllerActions = + MessengerActions; + +type AllMoneyAccountUpgradeControllerEvents = + MessengerEvents; + +type RootMessenger = Messenger< + MockAnyNamespace, + AllMoneyAccountUpgradeControllerActions, + AllMoneyAccountUpgradeControllerEvents +>; + +function setup(): { + controller: MoneyAccountUpgradeController; + rootMessenger: RootMessenger; + messenger: MoneyAccountUpgradeControllerMessenger; +} { + const rootMessenger = new Messenger< + MockAnyNamespace, + AllMoneyAccountUpgradeControllerActions, + AllMoneyAccountUpgradeControllerEvents + >({ namespace: MOCK_ANY_NAMESPACE }); + + const messenger: MoneyAccountUpgradeControllerMessenger = new Messenger({ + namespace: 'MoneyAccountUpgradeController', + parent: rootMessenger, + }); + + rootMessenger.delegate({ + actions: [], + events: [], + messenger, + }); + + const controller = new MoneyAccountUpgradeController({ + messenger, + }); + + return { + controller, + rootMessenger, + messenger, + }; +} + +describe('MoneyAccountUpgradeController', () => { + describe('constructor', () => { + it('initializes with default state when no state is provided', () => { + const { controller } = setup(); + + expect(controller.state).toStrictEqual( + getDefaultMoneyAccountUpgradeControllerState(), + ); + }); + }); + + describe('upgradeAccount', () => { + it('resolves without error', async () => { + const { controller } = setup(); + + expect(await controller.upgradeAccount()).toBeUndefined(); + }); + + it('is callable via the messenger', async () => { + const { rootMessenger } = setup(); + + expect( + await rootMessenger.call( + 'MoneyAccountUpgradeController:upgradeAccount', + ), + ).toBeUndefined(); + }); + }); +}); diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts new file mode 100644 index 00000000000..a53b791381d --- /dev/null +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -0,0 +1,100 @@ +import type { + ControllerGetStateAction, + ControllerStateChangeEvent, + StateMetadata, +} from '@metamask/base-controller'; +import { BaseController } from '@metamask/base-controller'; +import type { Messenger } from '@metamask/messenger'; + +import type { MoneyAccountUpgradeControllerMethodActions } from './MoneyAccountUpgradeController-method-action-types'; + +export const controllerName = 'MoneyAccountUpgradeController'; + +export type MoneyAccountUpgradeControllerState = Record; + +const moneyAccountUpgradeControllerMetadata = + {} satisfies StateMetadata; + +export function getDefaultMoneyAccountUpgradeControllerState(): MoneyAccountUpgradeControllerState { + return {}; +} + +const MESSENGER_EXPOSED_METHODS = ['upgradeAccount'] as const; + +export type MoneyAccountUpgradeControllerGetStateAction = + ControllerGetStateAction< + typeof controllerName, + MoneyAccountUpgradeControllerState + >; + +export type MoneyAccountUpgradeControllerActions = + | MoneyAccountUpgradeControllerGetStateAction + | MoneyAccountUpgradeControllerMethodActions; + +type AllowedActions = never; + +export type MoneyAccountUpgradeControllerStateChangeEvent = + ControllerStateChangeEvent< + typeof controllerName, + MoneyAccountUpgradeControllerState + >; + +export type MoneyAccountUpgradeControllerEvents = + MoneyAccountUpgradeControllerStateChangeEvent; + +type AllowedEvents = never; + +export type MoneyAccountUpgradeControllerMessenger = Messenger< + typeof controllerName, + MoneyAccountUpgradeControllerActions | AllowedActions, + MoneyAccountUpgradeControllerEvents | AllowedEvents +>; + +/** + * Controller for managing money account upgrades. + */ +export class MoneyAccountUpgradeController extends BaseController< + typeof controllerName, + MoneyAccountUpgradeControllerState, + MoneyAccountUpgradeControllerMessenger +> { + /** + * Constructor for the MoneyAccountUpgradeController. + * + * @param options - The options for constructing the controller. + * @param options.messenger - The messenger to use for inter-controller communication. + * @param options.state - The initial state of the controller. If not provided, the default state will be used. + */ + constructor({ + messenger, + state, + }: { + messenger: MoneyAccountUpgradeControllerMessenger; + state?: Partial; + }) { + super({ + messenger, + metadata: moneyAccountUpgradeControllerMetadata, + name: controllerName, + state: { + ...getDefaultMoneyAccountUpgradeControllerState(), + ...state, + }, + }); + + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, + ); + } + + /** + * Upgrades a money account. This method iterates over a number of + * steps to perform the upgrade. + * + * @returns A promise that resolves when the upgrade is complete. + */ + async upgradeAccount(): Promise { + // TODO: Implement upgrade steps + } +} diff --git a/packages/money-account-upgrade-controller/src/index.ts b/packages/money-account-upgrade-controller/src/index.ts new file mode 100644 index 00000000000..582346b5dcb --- /dev/null +++ b/packages/money-account-upgrade-controller/src/index.ts @@ -0,0 +1,14 @@ +export { + MoneyAccountUpgradeController, + controllerName, + getDefaultMoneyAccountUpgradeControllerState, +} from './MoneyAccountUpgradeController'; +export type { + MoneyAccountUpgradeControllerState, + MoneyAccountUpgradeControllerGetStateAction, + MoneyAccountUpgradeControllerActions, + MoneyAccountUpgradeControllerStateChangeEvent, + MoneyAccountUpgradeControllerEvents, + MoneyAccountUpgradeControllerMessenger, +} from './MoneyAccountUpgradeController'; +export type { MoneyAccountUpgradeControllerUpgradeAccountAction } from './MoneyAccountUpgradeController-method-action-types'; diff --git a/packages/money-account-upgrade-controller/tsconfig.build.json b/packages/money-account-upgrade-controller/tsconfig.build.json new file mode 100644 index 00000000000..64434b397eb --- /dev/null +++ b/packages/money-account-upgrade-controller/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { + "path": "../base-controller/tsconfig.build.json" + }, + { "path": "../messenger/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/money-account-upgrade-controller/tsconfig.json b/packages/money-account-upgrade-controller/tsconfig.json new file mode 100644 index 00000000000..8af2380112d --- /dev/null +++ b/packages/money-account-upgrade-controller/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../messenger" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/money-account-upgrade-controller/typedoc.json b/packages/money-account-upgrade-controller/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/money-account-upgrade-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/yarn.lock b/yarn.lock index 2e975dfceac..4f4ad790ca9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4484,6 +4484,25 @@ __metadata: languageName: unknown linkType: soft +"@metamask/money-account-upgrade-controller@workspace:packages/money-account-upgrade-controller": + version: 0.0.0-use.local + resolution: "@metamask/money-account-upgrade-controller@workspace:packages/money-account-upgrade-controller" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^9.0.1" + "@metamask/messenger": "npm:^1.1.1" + "@ts-bridge/cli": "npm:^0.6.4" + "@types/jest": "npm:^29.5.14" + deepmerge: "npm:^4.2.2" + jest: "npm:^29.2.5" + ts-jest: "npm:^29.2.5" + tsx: "npm:^4.20.5" + typedoc: "npm:^0.25.13" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.3.3" + languageName: unknown + linkType: soft + "@metamask/multichain-account-service@npm:^8.0.1, @metamask/multichain-account-service@workspace:packages/multichain-account-service": version: 0.0.0-use.local resolution: "@metamask/multichain-account-service@workspace:packages/multichain-account-service" @@ -11536,7 +11555,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:^29.7.0": +"jest@npm:^29.2.5, jest@npm:^29.7.0": version: 29.7.0 resolution: "jest@npm:29.7.0" dependencies: From 745a963f4d6bfd685455da008c2c36c5e90f050e Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 10 Apr 2026 16:48:35 +0100 Subject: [PATCH 02/26] feat: add initial version of the money account upgrade controller --- .../CHANGELOG.md | 2 +- .../money-account-upgrade-controller/TODO.md | 29 + .../package.json | 7 +- ...ntUpgradeController-method-action-types.ts | 5 +- .../src/MoneyAccountUpgradeController.test.ts | 465 +++++++++++- .../src/MoneyAccountUpgradeController.ts | 314 +++++++- .../src/index.ts | 1 + .../src/types.ts | 44 ++ .../tsconfig.build.json | 10 +- .../tsconfig.json | 6 +- yarn.lock | 718 ++++++++++++++++-- 11 files changed, 1511 insertions(+), 90 deletions(-) create mode 100644 packages/money-account-upgrade-controller/TODO.md create mode 100644 packages/money-account-upgrade-controller/src/types.ts diff --git a/packages/money-account-upgrade-controller/CHANGELOG.md b/packages/money-account-upgrade-controller/CHANGELOG.md index 7369581f594..15294a254a7 100644 --- a/packages/money-account-upgrade-controller/CHANGELOG.md +++ b/packages/money-account-upgrade-controller/CHANGELOG.md @@ -9,6 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `MoneyAccountUpgradeController` with `upgradeAccount` method +- Add `MoneyAccountUpgradeController` with multi-step `upgradeAccount` method that orchestrates address association, EIP-7702 authorization, delegation verification, and intent registration via CHOMP [Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/money-account-upgrade-controller/TODO.md b/packages/money-account-upgrade-controller/TODO.md new file mode 100644 index 00000000000..e86e1bbc404 --- /dev/null +++ b/packages/money-account-upgrade-controller/TODO.md @@ -0,0 +1,29 @@ +# MoneyAccountUpgradeController — Remaining Work + +## Step 0: Associate address + +- [ ] **Idempotency check**: `#associateAddress` currently always signs and submits. The CHOMP API returns 409 when already associated (which is handled as success), but we could skip the signing step entirely by checking state or querying CHOMP first. + +## Step 1: Submit authorization + +- [ ] **Fetch on-chain nonce**: The nonce is hardcoded to `0` (line 202). This needs to be replaced with an actual on-chain nonce fetch for the account — likely via an `eth_getTransactionCount` call or equivalent messenger action. CHOMP validates that the nonce matches. + +## Step 2: Verify delegation + +- [ ] **Caveat term encoding**: `#encodeCaveatTerms` does naive left-padded hex concatenation. Verify this matches the exact ABI encoding expected by the caveat enforcer contracts (may need proper ABI encoding via `@metamask/abi-utils` or similar). +- [ ] **Use `@metamask/smart-accounts-kit`**: The description mentions using `createDelegation` from `@metamask/smart-accounts-kit` to build the delegation. This package is not yet in the repo — once it lands, the delegation construction in `#verifyDelegation` should use it instead of manual assembly. +- [ ] **Import `ROOT_AUTHORITY` from `@metamask/delegation-controller`**: Currently defined locally as a constant. The delegation-controller has it but doesn't export it — once it's exported, import it instead. + +## Step 3: Save delegation + +- [ ] **Implement Authenticated User Storage save**: `#saveDelegation` is a stub (line 312-316). Needs the `@metamask/authenticated-user-storage` wrapper (PR currently open). Once available, save the signed delegation so CHOMP can read it at execution time via its internal VPC endpoint. + +## Step 4: Register intents + +- [ ] **Intent configuration**: The deposit/withdrawal intents are hardcoded with `mUSD` token symbol and `MAX_UINT256` allowance. These may need to come from config or be parameterised once requirements solidify. + +## General + +- [ ] **Resumability**: `upgradeAccount` runs all steps sequentially. If it fails mid-way and is retried, steps 0 and 2-4 will re-run from scratch. Consider checking persisted state at the start of each step to skip already-completed work (step 1 already does this via `getUpgrade`). +- [ ] **`MoneyAccountController:getMoneyAccount`**: This action is declared in `AllowedActions` but not currently used. It was included anticipating the controller may need to look up account details. Remove if not needed, or use it to validate the address before starting. +- [ ] **`#saveDelegation` unused `_chainId` parameter**: Will be needed once the stub is implemented — the storage call will likely need it. diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json index 2516a1a990b..27c79e873d9 100644 --- a/packages/money-account-upgrade-controller/package.json +++ b/packages/money-account-upgrade-controller/package.json @@ -50,10 +50,15 @@ }, "dependencies": { "@metamask/base-controller": "^9.0.1", - "@metamask/messenger": "^1.1.1" + "@metamask/chomp-api-service": "^0.0.0", + "@metamask/delegation-controller": "^2.1.0", + "@metamask/keyring-controller": "^25.2.0", + "@metamask/messenger": "^1.1.1", + "@metamask/money-account-controller": "^0.1.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", + "@metamask/utils": "^11.9.0", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", "deepmerge": "^4.2.2", diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts index fb358efefcc..bf1040d3d17 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts @@ -6,9 +6,10 @@ import type { MoneyAccountUpgradeController } from './MoneyAccountUpgradeController'; /** - * Upgrades a money account. This method iterates over a number of - * steps to perform the upgrade. + * Runs the full upgrade sequence for a Money Account. * + * @param address - The Money Account address to upgrade. + * @param chainId - The target chain for the upgrade. * @returns A promise that resolves when the upgrade is complete. */ export type MoneyAccountUpgradeControllerUpgradeAccountAction = { diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts index 374d1ab8cca..955b9b793b4 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -4,35 +4,131 @@ import type { MessengerActions, MessengerEvents, } from '@metamask/messenger'; +import type { Hex } from '@metamask/utils'; import type { MoneyAccountUpgradeControllerMessenger } from '.'; import { MoneyAccountUpgradeController, getDefaultMoneyAccountUpgradeControllerState, } from '.'; +import type { UpgradeConfig } from './types'; -type AllMoneyAccountUpgradeControllerActions = - MessengerActions; +const MOCK_ADDRESS = '0xabcdef1234567890abcdef1234567890abcdef12' as Hex; +const MOCK_CHAIN_ID = '0x1' as Hex; +const MOCK_SIGNATURE = + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + + '1b'; +const MOCK_DELEGATION_SIGNATURE = '0xdeadbeef' as Hex; +const MOCK_DELEGATION_HASH = '0xabcdef1234567890'; -type AllMoneyAccountUpgradeControllerEvents = - MessengerEvents; +const MOCK_CONFIG: UpgradeConfig = { + delegateAddress: '0x1111111111111111111111111111111111111111' as Hex, + delegatorImplAddress: '0x2222222222222222222222222222222222222222' as Hex, + musdTokenAddress: '0x3333333333333333333333333333333333333333' as Hex, + vedaVaultAdapterAddress: '0x4444444444444444444444444444444444444444' as Hex, + erc20TransferAmountEnforcer: + '0x5555555555555555555555555555555555555555' as Hex, + redeemerEnforcer: '0x6666666666666666666666666666666666666666' as Hex, + valueLteEnforcer: '0x7777777777777777777777777777777777777777' as Hex, +}; -type RootMessenger = Messenger< - MockAnyNamespace, - AllMoneyAccountUpgradeControllerActions, - AllMoneyAccountUpgradeControllerEvents ->; +type AllActions = MessengerActions; + +type AllEvents = MessengerEvents; + +type RootMessenger = Messenger; + +type Mocks = { + signPersonalMessage: jest.Mock; + signEip7702Authorization: jest.Mock; + associateAddress: jest.Mock; + createUpgrade: jest.Mock; + getUpgrade: jest.Mock; + verifyDelegation: jest.Mock; + createIntents: jest.Mock; + signDelegation: jest.Mock; + getMoneyAccount: jest.Mock; +}; -function setup(): { +function setup({ + state, +}: { + state?: Partial< + ReturnType + >; +} = {}): { controller: MoneyAccountUpgradeController; rootMessenger: RootMessenger; messenger: MoneyAccountUpgradeControllerMessenger; + mocks: Mocks; } { - const rootMessenger = new Messenger< - MockAnyNamespace, - AllMoneyAccountUpgradeControllerActions, - AllMoneyAccountUpgradeControllerEvents - >({ namespace: MOCK_ANY_NAMESPACE }); + const mocks: Mocks = { + signPersonalMessage: jest.fn().mockResolvedValue(MOCK_SIGNATURE), + signEip7702Authorization: jest.fn().mockResolvedValue(MOCK_SIGNATURE), + associateAddress: jest.fn().mockResolvedValue({ + profileId: 'profile-1', + address: MOCK_ADDRESS, + status: 'created', + }), + createUpgrade: jest.fn().mockResolvedValue({ + signerAddress: MOCK_ADDRESS, + status: 'created', + createdAt: '2026-04-10T00:00:00Z', + }), + getUpgrade: jest.fn().mockResolvedValue(null), + verifyDelegation: jest.fn().mockResolvedValue({ + valid: true, + delegationHash: MOCK_DELEGATION_HASH, + }), + createIntents: jest.fn().mockResolvedValue([]), + signDelegation: jest.fn().mockResolvedValue(MOCK_DELEGATION_SIGNATURE), + getMoneyAccount: jest.fn().mockReturnValue({ + id: 'account-1', + address: MOCK_ADDRESS, + }), + }; + + const rootMessenger = new Messenger({ + namespace: MOCK_ANY_NAMESPACE, + }); + + rootMessenger.registerActionHandler( + 'KeyringController:signPersonalMessage', + mocks.signPersonalMessage, + ); + rootMessenger.registerActionHandler( + 'KeyringController:signEip7702Authorization', + mocks.signEip7702Authorization, + ); + rootMessenger.registerActionHandler( + 'ChompApiService:associateAddress', + mocks.associateAddress, + ); + rootMessenger.registerActionHandler( + 'ChompApiService:createUpgrade', + mocks.createUpgrade, + ); + rootMessenger.registerActionHandler( + 'ChompApiService:getUpgrade', + mocks.getUpgrade, + ); + rootMessenger.registerActionHandler( + 'ChompApiService:verifyDelegation', + mocks.verifyDelegation, + ); + rootMessenger.registerActionHandler( + 'ChompApiService:createIntents', + mocks.createIntents, + ); + rootMessenger.registerActionHandler( + 'DelegationController:signDelegation', + mocks.signDelegation, + ); + rootMessenger.registerActionHandler( + 'MoneyAccountController:getMoneyAccount', + mocks.getMoneyAccount, + ); const messenger: MoneyAccountUpgradeControllerMessenger = new Messenger({ namespace: 'MoneyAccountUpgradeController', @@ -40,20 +136,28 @@ function setup(): { }); rootMessenger.delegate({ - actions: [], + actions: [ + 'KeyringController:signPersonalMessage', + 'KeyringController:signEip7702Authorization', + 'ChompApiService:associateAddress', + 'ChompApiService:createUpgrade', + 'ChompApiService:getUpgrade', + 'ChompApiService:verifyDelegation', + 'ChompApiService:createIntents', + 'DelegationController:signDelegation', + 'MoneyAccountController:getMoneyAccount', + ], events: [], messenger, }); const controller = new MoneyAccountUpgradeController({ messenger, + state, + config: MOCK_CONFIG, }); - return { - controller, - rootMessenger, - messenger, - }; + return { controller, rootMessenger, messenger, mocks }; } describe('MoneyAccountUpgradeController', () => { @@ -65,13 +169,52 @@ describe('MoneyAccountUpgradeController', () => { getDefaultMoneyAccountUpgradeControllerState(), ); }); + + it('accepts initial state', () => { + const { controller } = setup({ + state: { + upgrades: { + [MOCK_ADDRESS]: { + step: 'associate-address', + chainId: MOCK_CHAIN_ID, + }, + }, + }, + }); + + expect(controller.state.upgrades[MOCK_ADDRESS]).toStrictEqual({ + step: 'associate-address', + chainId: MOCK_CHAIN_ID, + }); + }); }); describe('upgradeAccount', () => { - it('resolves without error', async () => { + it('runs the full upgrade sequence', async () => { + const { controller, mocks } = setup(); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.signPersonalMessage).toHaveBeenCalledTimes(1); + expect(mocks.associateAddress).toHaveBeenCalledTimes(1); + expect(mocks.getUpgrade).toHaveBeenCalledTimes(1); + expect(mocks.signEip7702Authorization).toHaveBeenCalledTimes(1); + expect(mocks.createUpgrade).toHaveBeenCalledTimes(1); + expect(mocks.signDelegation).toHaveBeenCalledTimes(1); + expect(mocks.verifyDelegation).toHaveBeenCalledTimes(1); + expect(mocks.createIntents).toHaveBeenCalledTimes(1); + }); + + it('records final state as register-intents with delegationHash', async () => { const { controller } = setup(); - expect(await controller.upgradeAccount()).toBeUndefined(); + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(controller.state.upgrades[MOCK_ADDRESS]).toStrictEqual({ + step: 'register-intents', + chainId: MOCK_CHAIN_ID, + delegationHash: MOCK_DELEGATION_HASH, + }); }); it('is callable via the messenger', async () => { @@ -80,8 +223,284 @@ describe('MoneyAccountUpgradeController', () => { expect( await rootMessenger.call( 'MoneyAccountUpgradeController:upgradeAccount', + MOCK_ADDRESS, + MOCK_CHAIN_ID, ), ).toBeUndefined(); }); }); + + describe('step 0: associate address', () => { + it('signs the authentication message and submits to CHOMP', async () => { + const { controller, mocks } = setup(); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.signPersonalMessage).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.stringMatching(/^CHOMP Authentication \d+$/u), + from: MOCK_ADDRESS, + }), + ); + + expect(mocks.associateAddress).toHaveBeenCalledWith( + expect.objectContaining({ + signature: MOCK_SIGNATURE, + address: MOCK_ADDRESS, + timestamp: expect.stringMatching(/^\d+$/u), + }), + ); + }); + + it('updates state to associate-address after completion', async () => { + const { controller, mocks } = setup(); + + // Make subsequent steps fail so we can check state after step 0. + mocks.getUpgrade.mockRejectedValue(new Error('stop')); + + await expect( + controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), + ).rejects.toThrow('stop'); + + expect(controller.state.upgrades[MOCK_ADDRESS]?.step).toBe( + 'associate-address', + ); + }); + }); + + describe('step 1: submit authorization', () => { + it('skips signing when CHOMP already has an upgrade record', async () => { + const { controller, mocks } = setup(); + + mocks.getUpgrade.mockResolvedValue({ + signerAddress: MOCK_ADDRESS, + status: 'upgraded', + createdAt: '2026-04-09T00:00:00Z', + }); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.signEip7702Authorization).not.toHaveBeenCalled(); + expect(mocks.createUpgrade).not.toHaveBeenCalled(); + }); + + it('signs and submits an EIP-7702 authorization', async () => { + const { controller, mocks } = setup(); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.signEip7702Authorization).toHaveBeenCalledWith({ + chainId: 1, + contractAddress: MOCK_CONFIG.delegatorImplAddress, + nonce: 0, + from: MOCK_ADDRESS, + }); + + expect(mocks.createUpgrade).toHaveBeenCalledWith( + expect.objectContaining({ + address: MOCK_ADDRESS, + chainId: MOCK_CHAIN_ID, + }), + ); + }); + + it('computes yParity=1 when v is 28', async () => { + const { controller, mocks } = setup(); + + const sigWithV28 = + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + + '1c'; + mocks.signEip7702Authorization.mockResolvedValue(sigWithV28); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.createUpgrade).toHaveBeenCalledWith( + expect.objectContaining({ + v: 28, + yParity: 1, + }), + ); + }); + + it('parses signature components correctly', async () => { + const { controller, mocks } = setup(); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.createUpgrade).toHaveBeenCalledWith( + expect.objectContaining({ + // r = first 66 chars (0x + 64 hex chars) + r: MOCK_SIGNATURE.slice(0, 66), + // s = 0x + next 64 hex chars + s: `0x${MOCK_SIGNATURE.slice(66, 130)}`, + // v = last 2 hex chars parsed as int + v: parseInt(MOCK_SIGNATURE.slice(130, 132), 16), + // yParity derived from v + yParity: + parseInt(MOCK_SIGNATURE.slice(130, 132), 16) - 27 === 0 ? 0 : 1, + }), + ); + }); + }); + + describe('step 2: verify delegation', () => { + it('builds a delegation with three caveats and verifies with CHOMP', async () => { + const { controller, mocks } = setup(); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.signDelegation).toHaveBeenCalledWith({ + delegation: expect.objectContaining({ + delegate: MOCK_CONFIG.delegateAddress, + delegator: MOCK_ADDRESS, + caveats: expect.arrayContaining([ + expect.objectContaining({ + enforcer: MOCK_CONFIG.erc20TransferAmountEnforcer, + }), + expect.objectContaining({ + enforcer: MOCK_CONFIG.redeemerEnforcer, + }), + expect.objectContaining({ + enforcer: MOCK_CONFIG.valueLteEnforcer, + }), + ]), + }), + chainId: MOCK_CHAIN_ID, + }); + + expect(mocks.verifyDelegation).toHaveBeenCalledWith( + expect.objectContaining({ + signedDelegation: expect.objectContaining({ + delegate: MOCK_CONFIG.delegateAddress, + delegator: MOCK_ADDRESS, + signature: MOCK_DELEGATION_SIGNATURE, + }), + chainId: MOCK_CHAIN_ID, + }), + ); + }); + + it('includes exactly three caveats', async () => { + const { controller, mocks } = setup(); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + const { delegation } = mocks.signDelegation.mock.calls[0][0]; + expect(delegation.caveats).toHaveLength(3); + }); + + it('throws when delegation verification fails', async () => { + const { controller, mocks } = setup(); + + mocks.verifyDelegation.mockResolvedValue({ + valid: false, + errors: ['delegate mismatch'], + }); + + await expect( + controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), + ).rejects.toThrow('Delegation verification failed: delegate mismatch'); + }); + + it('throws with unknown error when verification fails without error details', async () => { + const { controller, mocks } = setup(); + + mocks.verifyDelegation.mockResolvedValue({ + valid: false, + }); + + await expect( + controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), + ).rejects.toThrow('Delegation verification failed: unknown error'); + }); + + it('stores delegationHash in state after successful verification', async () => { + const { controller } = setup(); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(controller.state.upgrades[MOCK_ADDRESS]?.delegationHash).toBe( + MOCK_DELEGATION_HASH, + ); + }); + }); + + describe('step 3: save delegation (stub)', () => { + it('updates state to save-delegation', async () => { + const { controller, mocks } = setup(); + + // Make step 4 fail so we can check state after step 3. + mocks.createIntents.mockRejectedValue(new Error('stop')); + + await expect( + controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), + ).rejects.toThrow('stop'); + + expect(controller.state.upgrades[MOCK_ADDRESS]?.step).toBe( + 'save-delegation', + ); + }); + }); + + describe('step 4: register intents', () => { + it('submits deposit and withdrawal intents', async () => { + const { controller, mocks } = setup(); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.createIntents).toHaveBeenCalledWith([ + expect.objectContaining({ + account: MOCK_ADDRESS, + delegationHash: MOCK_DELEGATION_HASH, + chainId: MOCK_CHAIN_ID, + metadata: expect.objectContaining({ type: 'cash-deposit' }), + }), + expect.objectContaining({ + account: MOCK_ADDRESS, + delegationHash: MOCK_DELEGATION_HASH, + chainId: MOCK_CHAIN_ID, + metadata: expect.objectContaining({ type: 'cash-withdrawal' }), + }), + ]); + }); + + it('throws if delegationHash is missing', async () => { + const { controller, mocks } = setup(); + + // Skip the verify step by making it not store a hash + mocks.verifyDelegation.mockResolvedValue({ + valid: true, + // No delegationHash returned + }); + + await expect( + controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), + ).rejects.toThrow('Cannot register intents: no delegationHash found'); + }); + }); + + describe('error propagation', () => { + it('propagates signing errors', async () => { + const { controller, mocks } = setup(); + + mocks.signPersonalMessage.mockRejectedValue(new Error('signing failed')); + + await expect( + controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), + ).rejects.toThrow('signing failed'); + }); + + it('propagates CHOMP API errors', async () => { + const { controller, mocks } = setup(); + + mocks.associateAddress.mockRejectedValue( + new Error("POST /v1/auth/address failed with status '500'"), + ); + + await expect( + controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), + ).rejects.toThrow("POST /v1/auth/address failed with status '500'"); + }); + }); }); diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index a53b791381d..f5b157bc0c5 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -4,19 +4,53 @@ import type { StateMetadata, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; +import type { + ChompApiServiceAssociateAddressAction, + ChompApiServiceCreateIntentsAction, + ChompApiServiceCreateUpgradeAction, + ChompApiServiceGetUpgradeAction, + ChompApiServiceVerifyDelegationAction, + SignedDelegation, +} from '@metamask/chomp-api-service'; +import type { DelegationControllerSignDelegationAction } from '@metamask/delegation-controller'; +import type { + KeyringControllerSignEip7702AuthorizationAction, + KeyringControllerSignPersonalMessageAction, +} from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; +import type { MoneyAccountControllerGetMoneyAccountAction } from '@metamask/money-account-controller'; +import type { Hex } from '@metamask/utils'; import type { MoneyAccountUpgradeControllerMethodActions } from './MoneyAccountUpgradeController-method-action-types'; +import type { AccountUpgradeEntry, UpgradeConfig } from './types'; export const controllerName = 'MoneyAccountUpgradeController'; -export type MoneyAccountUpgradeControllerState = Record; +// The root authority constant for top-level delegations. +const ROOT_AUTHORITY = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex; + +// Maximum uint256 — used as the allowance for the ERC20TransferAmountEnforcer. +const MAX_UINT256 = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex; -const moneyAccountUpgradeControllerMetadata = - {} satisfies StateMetadata; +export type MoneyAccountUpgradeControllerState = { + upgrades: Record; +}; + +const moneyAccountUpgradeControllerMetadata = { + upgrades: { + includeInDebugSnapshot: false, + includeInStateLogs: false, + persist: true, + usedInUi: false, + }, +} satisfies StateMetadata; export function getDefaultMoneyAccountUpgradeControllerState(): MoneyAccountUpgradeControllerState { - return {}; + return { + upgrades: {}, + }; } const MESSENGER_EXPOSED_METHODS = ['upgradeAccount'] as const; @@ -31,7 +65,16 @@ export type MoneyAccountUpgradeControllerActions = | MoneyAccountUpgradeControllerGetStateAction | MoneyAccountUpgradeControllerMethodActions; -type AllowedActions = never; +type AllowedActions = + | ChompApiServiceAssociateAddressAction + | ChompApiServiceCreateUpgradeAction + | ChompApiServiceGetUpgradeAction + | ChompApiServiceVerifyDelegationAction + | ChompApiServiceCreateIntentsAction + | KeyringControllerSignPersonalMessageAction + | KeyringControllerSignEip7702AuthorizationAction + | DelegationControllerSignDelegationAction + | MoneyAccountControllerGetMoneyAccountAction; export type MoneyAccountUpgradeControllerStateChangeEvent = ControllerStateChangeEvent< @@ -51,26 +94,31 @@ export type MoneyAccountUpgradeControllerMessenger = Messenger< >; /** - * Controller for managing money account upgrades. + * Controller that orchestrates the multi-step Money Account upgrade sequence. */ export class MoneyAccountUpgradeController extends BaseController< typeof controllerName, MoneyAccountUpgradeControllerState, MoneyAccountUpgradeControllerMessenger > { + readonly #config: UpgradeConfig; + /** * Constructor for the MoneyAccountUpgradeController. * * @param options - The options for constructing the controller. * @param options.messenger - The messenger to use for inter-controller communication. - * @param options.state - The initial state of the controller. If not provided, the default state will be used. + * @param options.state - The initial state of the controller. + * @param options.config - Contract addresses and configuration for the upgrade sequence. */ constructor({ messenger, state, + config, }: { messenger: MoneyAccountUpgradeControllerMessenger; state?: Partial; + config: UpgradeConfig; }) { super({ messenger, @@ -82,6 +130,8 @@ export class MoneyAccountUpgradeController extends BaseController< }, }); + this.#config = config; + this.messenger.registerMethodActionHandlers( this, MESSENGER_EXPOSED_METHODS, @@ -89,12 +139,252 @@ export class MoneyAccountUpgradeController extends BaseController< } /** - * Upgrades a money account. This method iterates over a number of - * steps to perform the upgrade. + * Runs the full upgrade sequence for a Money Account. Each step is + * idempotent — if the step has already been completed, it is skipped. + * + * @param address - The Money Account address to upgrade. + * @param chainId - The target chain for the upgrade. + */ + async upgradeAccount(address: Hex, chainId: Hex): Promise { + await this.#associateAddress(address); + await this.#submitAuthorization(address, chainId); + await this.#verifyDelegation(address, chainId); + await this.#saveDelegation(address, chainId); + await this.#registerIntents(address, chainId); + } + + /** + * Step 0: Associate the Money Account address with the user's CHOMP profile. + * + * Signs "CHOMP Authentication {timestamp}" via personal_sign and submits + * it to CHOMP. The API accepts 409 (already associated) as success. + * + * @param address - The Money Account address. + */ + async #associateAddress(address: Hex): Promise { + const timestamp = Date.now().toString(); + const message = `CHOMP Authentication ${timestamp}`; + + const signature = await this.messenger.call( + 'KeyringController:signPersonalMessage', + { data: message, from: address }, + ); + + await this.messenger.call('ChompApiService:associateAddress', { + signature, + timestamp, + address, + }); + + this.#updateUpgrade(address, { step: 'associate-address' }); + } + + /** + * Step 1: Sign and submit an EIP-7702 authorization to CHOMP. + * + * Skips if CHOMP already has an upgrade record for this address. + * + * @param address - The Money Account address. + * @param chainId - The target chain. + */ + async #submitAuthorization(address: Hex, chainId: Hex): Promise { + const existing = await this.messenger.call( + 'ChompApiService:getUpgrade', + address, + ); + + if (existing) { + this.#updateUpgrade(address, { step: 'submit-authorization', chainId }); + return; + } + + // TODO: Fetch on-chain nonce. Using 0 as placeholder. + const nonce = 0; + const chainIdDecimal = parseInt(chainId, 16); + + const signature = await this.messenger.call( + 'KeyringController:signEip7702Authorization', + { + chainId: chainIdDecimal, + contractAddress: this.#config.delegatorImplAddress, + nonce, + from: address, + }, + ); + + const sigR = signature.slice(0, 66); + const sigS = `0x${signature.slice(66, 130)}`; + const sigV = parseInt(signature.slice(130, 132), 16); + const yParity = sigV - 27 === 0 ? 0 : 1; + + await this.messenger.call('ChompApiService:createUpgrade', { + r: sigR, + s: sigS, + v: sigV, + yParity, + address, + chainId, + nonce: nonce.toString(), + }); + + this.#updateUpgrade(address, { step: 'submit-authorization', chainId }); + } + + /** + * Step 2: Build, sign, and verify a delegation with CHOMP. + * + * Constructs an unsigned delegation with three caveat enforcers + * (ERC20TransferAmount, Redeemer, ValueLte), signs it via the + * DelegationController, and verifies it with CHOMP. + * + * @param address - The Money Account address (delegator). + * @param chainId - The target chain. + */ + async #verifyDelegation(address: Hex, chainId: Hex): Promise { + const salt: Hex = `0x${Array.from( + // TODO: do I need to read this off of globalThis? + globalThis.crypto.getRandomValues(new Uint8Array(32)), + ) + .map((b) => b.toString(16).padStart(2, '0')) + .join('')}`; + + const delegation = { + delegate: this.#config.delegateAddress, + delegator: address, + authority: ROOT_AUTHORITY, + caveats: [ + { + enforcer: this.#config.erc20TransferAmountEnforcer, + terms: this.#encodeCaveatTerms( + MAX_UINT256, + this.#config.musdTokenAddress, + ), + args: '0x' as Hex, + }, + { + enforcer: this.#config.redeemerEnforcer, + terms: this.#encodeCaveatTerms(this.#config.vedaVaultAdapterAddress), + args: '0x' as Hex, + }, + { + enforcer: this.#config.valueLteEnforcer, + terms: + '0x0000000000000000000000000000000000000000000000000000000000000000' as Hex, + args: '0x' as Hex, + }, + ], + salt, + }; + + const signature: string = await this.messenger.call( + 'DelegationController:signDelegation', + { delegation, chainId }, + ); + + const signedDelegation: SignedDelegation = { + ...delegation, + signature: signature as Hex, + }; + + const result = await this.messenger.call( + 'ChompApiService:verifyDelegation', + { signedDelegation, chainId }, + ); + + if (!result.valid) { + throw new Error( + `Delegation verification failed: ${result.errors?.join(', ') ?? 'unknown error'}`, + ); + } + + this.#updateUpgrade(address, { + step: 'verify-delegation', + chainId, + delegationHash: result.delegationHash, + }); + } + + /** + * Step 3: Save the signed delegation to Authenticated User Storage. * - * @returns A promise that resolves when the upgrade is complete. + * @param address - The Money Account address. + * @param _chainId - The target chain (unused in stub). */ - async upgradeAccount(): Promise { - // TODO: Implement upgrade steps + async #saveDelegation(address: Hex, _chainId: Hex): Promise { + // TODO: Save delegation to Authenticated User Storage once the + // @metamask/authenticated-user-storage wrapper is available. + this.#updateUpgrade(address, { step: 'save-delegation' }); + } + + /** + * Step 4: Register intents with CHOMP so it begins monitoring the account. + * + * @param address - The Money Account address. + * @param chainId - The target chain. + */ + async #registerIntents(address: Hex, chainId: Hex): Promise { + const entry = this.state.upgrades[address]; + const delegationHash = entry?.delegationHash; + + if (!delegationHash) { + throw new Error( + 'Cannot register intents: no delegationHash found. Run verify-delegation first.', + ); + } + + await this.messenger.call('ChompApiService:createIntents', [ + { + account: address, + delegationHash: delegationHash as Hex, + chainId, + metadata: { + allowance: MAX_UINT256, + tokenSymbol: 'mUSD', + tokenAddress: this.#config.musdTokenAddress, + type: 'cash-deposit', + }, + }, + { + account: address, + delegationHash: delegationHash as Hex, + chainId, + metadata: { + allowance: MAX_UINT256, + tokenSymbol: 'mUSD', + tokenAddress: this.#config.musdTokenAddress, + type: 'cash-withdrawal', + }, + }, + ]); + + this.#updateUpgrade(address, { step: 'register-intents' }); + } + + /** + * Encodes caveat terms by concatenating hex values (ABI-style). + * + * @param values - The hex values to pack. + * @returns The concatenated hex string. + */ + #encodeCaveatTerms(...values: Hex[]): Hex { + return `0x${values.map((v) => v.slice(2).padStart(64, '0')).join('')}`; + } + + /** + * Merges an update into the upgrade entry for the given address. + * + * @param address - The account address. + * @param update - Fields to merge into the existing entry. + */ + #updateUpgrade( + address: Hex, + update: Partial & Pick, + ): void { + this.update((state) => { + state.upgrades[address] = { + ...state.upgrades[address], + ...update, + } as AccountUpgradeEntry; + }); } } diff --git a/packages/money-account-upgrade-controller/src/index.ts b/packages/money-account-upgrade-controller/src/index.ts index 582346b5dcb..7be107296d6 100644 --- a/packages/money-account-upgrade-controller/src/index.ts +++ b/packages/money-account-upgrade-controller/src/index.ts @@ -1,3 +1,4 @@ +export type { UpgradeConfig, UpgradeStep, AccountUpgradeEntry } from './types'; export { MoneyAccountUpgradeController, controllerName, diff --git a/packages/money-account-upgrade-controller/src/types.ts b/packages/money-account-upgrade-controller/src/types.ts new file mode 100644 index 00000000000..ed75e1890ad --- /dev/null +++ b/packages/money-account-upgrade-controller/src/types.ts @@ -0,0 +1,44 @@ +import type { Hex } from '@metamask/utils'; + +/** + * Contract addresses and configuration required to perform the + * Money Account upgrade sequence. + */ +export type UpgradeConfig = { + /** CHOMP's delegate address — receives the delegation. */ + delegateAddress: Hex; + /** The EIP-7702 delegation target (EIP7702StatelessDeleGatorImpl). */ + delegatorImplAddress: Hex; + /** The mUSD token contract address. */ + musdTokenAddress: Hex; + /** The Veda vault adapter contract address. */ + vedaVaultAdapterAddress: Hex; + /** Address of the ERC20TransferAmountEnforcer caveat enforcer. */ + erc20TransferAmountEnforcer: Hex; + /** Address of the RedeemerEnforcer caveat enforcer. */ + redeemerEnforcer: Hex; + /** Address of the ValueLteEnforcer caveat enforcer. */ + valueLteEnforcer: Hex; +}; + +/** + * The discrete steps of the upgrade sequence, in order. + */ +export type UpgradeStep = + | 'associate-address' + | 'submit-authorization' + | 'verify-delegation' + | 'save-delegation' + | 'register-intents'; + +/** + * Persisted record tracking the progress of an individual account upgrade. + */ +export type AccountUpgradeEntry = { + /** The last successfully completed step. */ + step: UpgradeStep; + /** The chain the upgrade is targeting. */ + chainId: Hex; + /** The delegation hash returned by CHOMP after verify-delegation. */ + delegationHash?: string; +}; diff --git a/packages/money-account-upgrade-controller/tsconfig.build.json b/packages/money-account-upgrade-controller/tsconfig.build.json index 64434b397eb..1885ad844ba 100644 --- a/packages/money-account-upgrade-controller/tsconfig.build.json +++ b/packages/money-account-upgrade-controller/tsconfig.build.json @@ -6,10 +6,12 @@ "rootDir": "./src" }, "references": [ - { - "path": "../base-controller/tsconfig.build.json" - }, - { "path": "../messenger/tsconfig.build.json" } + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../chomp-api-service/tsconfig.build.json" }, + { "path": "../delegation-controller/tsconfig.build.json" }, + { "path": "../keyring-controller/tsconfig.build.json" }, + { "path": "../messenger/tsconfig.build.json" }, + { "path": "../money-account-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/money-account-upgrade-controller/tsconfig.json b/packages/money-account-upgrade-controller/tsconfig.json index 8af2380112d..84f2d5918d3 100644 --- a/packages/money-account-upgrade-controller/tsconfig.json +++ b/packages/money-account-upgrade-controller/tsconfig.json @@ -5,7 +5,11 @@ }, "references": [ { "path": "../base-controller" }, - { "path": "../messenger" } + { "path": "../chomp-api-service" }, + { "path": "../delegation-controller" }, + { "path": "../keyring-controller" }, + { "path": "../messenger" }, + { "path": "../money-account-controller" } ], "include": ["../../types", "./src"] } diff --git a/yarn.lock b/yarn.lock index 4f4ad790ca9..2c6c1f6f914 100644 --- a/yarn.lock +++ b/yarn.lock @@ -154,7 +154,18 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": + version: 7.29.2 + resolution: "@babel/parser@npm:7.29.2" + dependencies: + "@babel/types": "npm:^7.29.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10/45d050bf75aa5194b3255f156173e8553d615ff5a2434674cc4a10cdc7c261931befb8618c996a1c449b87f0ef32a3407879af2ac967d95dc7b4fdbae7037efa + languageName: node + linkType: hard + +"@babel/parser@npm:^7.24.7": version: 7.29.0 resolution: "@babel/parser@npm:7.29.0" dependencies: @@ -709,13 +720,20 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.39.1, @eslint/js@npm:^9.11.0": +"@eslint/js@npm:9.39.1": version: 9.39.1 resolution: "@eslint/js@npm:9.39.1" checksum: 10/b10b9b953212c0f3ffca475159bbe519e9e98847200c7432d1637d444fddcd7b712d2b7710a7dc20510f9cfbe8db330039b2aad09cb55d9545b116d940dbeed2 languageName: node linkType: hard +"@eslint/js@npm:^9.11.0": + version: 9.39.4 + resolution: "@eslint/js@npm:9.39.4" + checksum: 10/0a7ab4c4108cf2cadf66849ebd20f5957cc53052b88d8807d0b54e489dbf6ffcaf741e144e7f9b187c395499ce2e6ddc565dbfa4f60c6df455cf2b30bcbdc5a3 + languageName: node + linkType: hard + "@eslint/object-schema@npm:^2.1.7": version: 2.1.7 resolution: "@eslint/object-schema@npm:2.1.7" @@ -1730,6 +1748,13 @@ __metadata: languageName: node linkType: hard +"@gar/promise-retry@npm:^1.0.0": + version: 1.0.3 + resolution: "@gar/promise-retry@npm:1.0.3" + checksum: 10/0d13ea3bb1025755e055648f6e290d2a7e0c87affaf552218f09f66b3fcd9ea9d5c9cc5fe2aa6e285e1530437768e40f9448fe9a86f4f3417b216dcf488d3d1a + languageName: node + linkType: hard + "@grpc/grpc-js@npm:~1.9.0": version: 1.9.15 resolution: "@grpc/grpc-js@npm:1.9.15" @@ -2560,7 +2585,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/accounts-controller@npm:^37.2.0, @metamask/accounts-controller@workspace:packages/accounts-controller": +"@metamask/accounts-controller@npm:^37.1.0, @metamask/accounts-controller@npm:^37.2.0, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" dependencies: @@ -2920,6 +2945,21 @@ __metadata: languageName: node linkType: hard +"@metamask/auto-changelog@npm:^3.4.4": + version: 3.4.4 + resolution: "@metamask/auto-changelog@npm:3.4.4" + dependencies: + diff: "npm:^5.0.0" + execa: "npm:^5.1.1" + prettier: "npm:^2.8.8" + semver: "npm:^7.3.5" + yargs: "npm:^17.0.1" + bin: + auto-changelog: dist/cli.js + checksum: 10/70e98529a153ebeab10410dbc3f567014999f77ed82f2b52f1b36501b28a4e3614c809a90c89600a739d7710595bfecc30e2260410e6afac7539f8db65a48f2c + languageName: node + linkType: hard + "@metamask/auto-changelog@npm:^6.1.0": version: 6.1.0 resolution: "@metamask/auto-changelog@npm:6.1.0" @@ -3119,7 +3159,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/chomp-api-service@workspace:packages/chomp-api-service": +"@metamask/chomp-api-service@npm:^0.0.0, @metamask/chomp-api-service@workspace:packages/chomp-api-service": version: 0.0.0-use.local resolution: "@metamask/chomp-api-service@workspace:packages/chomp-api-service" dependencies: @@ -3430,6 +3470,19 @@ __metadata: languageName: node linkType: hard +"@metamask/delegation-controller@npm:^2.1.0": + version: 2.1.0 + resolution: "@metamask/delegation-controller@npm:2.1.0" + dependencies: + "@metamask/accounts-controller": "npm:^37.1.0" + "@metamask/base-controller": "npm:^9.0.1" + "@metamask/keyring-controller": "npm:^25.1.1" + "@metamask/messenger": "npm:^1.0.0" + "@metamask/utils": "npm:^11.9.0" + checksum: 10/4baa6c23918cf17d8fffde446dbd4a6b6dfaf6039961b2998164ea67320e6a1dc3dbb13d41baf63a6a24220cd6898e075688e158205b8e41a8c02e0f33f76d83 + languageName: node + linkType: hard + "@metamask/delegation-controller@workspace:packages/delegation-controller": version: 0.0.0-use.local resolution: "@metamask/delegation-controller@workspace:packages/delegation-controller" @@ -4195,7 +4248,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^25.2.0, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@npm:^25.1.1, @metamask/keyring-controller@npm:^25.2.0, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: @@ -4398,7 +4451,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/messenger@npm:^1.1.0, @metamask/messenger@npm:^1.1.1, @metamask/messenger@workspace:packages/messenger": +"@metamask/messenger@npm:^1.0.0, @metamask/messenger@npm:^1.1.0, @metamask/messenger@npm:^1.1.1, @metamask/messenger@workspace:packages/messenger": version: 0.0.0-use.local resolution: "@metamask/messenger@workspace:packages/messenger" dependencies: @@ -4458,7 +4511,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/money-account-controller@workspace:packages/money-account-controller": +"@metamask/money-account-controller@npm:^0.1.0, @metamask/money-account-controller@workspace:packages/money-account-controller": version: 0.0.0-use.local resolution: "@metamask/money-account-controller@workspace:packages/money-account-controller" dependencies: @@ -4490,7 +4543,12 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^9.0.1" + "@metamask/chomp-api-service": "npm:^0.0.0" + "@metamask/delegation-controller": "npm:^2.1.0" + "@metamask/keyring-controller": "npm:^25.2.0" "@metamask/messenger": "npm:^1.1.1" + "@metamask/money-account-controller": "npm:^0.1.0" + "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" deepmerge: "npm:^4.2.2" @@ -5702,7 +5760,26 @@ __metadata: languageName: unknown linkType: soft -"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.10.0, @metamask/utils@npm:^11.11.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.1, @metamask/utils@npm:^11.9.0": +"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.10.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.1, @metamask/utils@npm:^11.9.0": + version: 11.10.0 + resolution: "@metamask/utils@npm:11.10.0" + dependencies: + "@ethereumjs/tx": "npm:^4.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.3" + "@types/debug": "npm:^4.1.7" + "@types/lodash": "npm:^4.17.20" + debug: "npm:^4.3.4" + lodash: "npm:^4.17.21" + pony-cause: "npm:^2.1.10" + semver: "npm:^7.5.4" + uuid: "npm:^9.0.1" + checksum: 10/691a268af66593b60e9807a069127993cea3cdc941f99d5d7ca4664868754f08945821f1787b2f3e99e4497df63ceb0af37a2419ad494da29a1fddffe94f5797 + languageName: node + linkType: hard + +"@metamask/utils@npm:^11.11.0": version: 11.11.0 resolution: "@metamask/utils@npm:11.11.0" dependencies: @@ -5915,6 +5992,19 @@ __metadata: languageName: node linkType: hard +"@npmcli/agent@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/agent@npm:4.0.0" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^11.2.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10/1a81573becc60515031accc696e6405e9b894e65c12b98ef4aeee03b5617c41948633159dbf6caf5dde5b47367eeb749bdc7b7dfb21960930a9060a935c6f636 + languageName: node + linkType: hard + "@npmcli/fs@npm:^3.1.0": version: 3.1.1 resolution: "@npmcli/fs@npm:3.1.1" @@ -5924,6 +6014,15 @@ __metadata: languageName: node linkType: hard +"@npmcli/fs@npm:^5.0.0": + version: 5.0.0 + resolution: "@npmcli/fs@npm:5.0.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10/4935c7719d17830d0f9fa46c50be17b2a3c945cec61760f6d0909bce47677c42e1810ca673305890f9e84f008ec4d8e841182f371e42100a8159d15f22249208 + languageName: node + linkType: hard + "@npmcli/git@npm:^5.0.0": version: 5.0.8 resolution: "@npmcli/git@npm:5.0.8" @@ -5972,6 +6071,13 @@ __metadata: languageName: node linkType: hard +"@npmcli/redact@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/redact@npm:4.0.0" + checksum: 10/5d52df2b5267f4369c97a2b2f7c427e3d7aa4b6a83e7a1b522e196f6e9d50024c620bd0cb2052067c74d1aaa0c330d9bc04e1d335bfb46180e705bb33423e74c + languageName: node + linkType: hard + "@npmcli/run-script@npm:8.1.0": version: 8.1.0 resolution: "@npmcli/run-script@npm:8.1.0" @@ -6584,13 +6690,20 @@ __metadata: languageName: node linkType: hard -"@tanstack/query-core@npm:4.43.0, @tanstack/query-core@npm:^4.43.0": +"@tanstack/query-core@npm:4.43.0": version: 4.43.0 resolution: "@tanstack/query-core@npm:4.43.0" checksum: 10/c2a5a151c7adaea8311e01a643255f31946ae3164a71567ba80048242821ae14043f13f5516b695baebe5ea7e4b2cf717fd60908a929d18a5c5125fee925ff67 languageName: node linkType: hard +"@tanstack/query-core@npm:^4.43.0": + version: 4.44.0 + resolution: "@tanstack/query-core@npm:4.44.0" + checksum: 10/f7f5c69cb2d44b58e1a9bfaa304a82724bb49f0ff63fd3abdfe377f15825ad910e1d31dd529d94e0650ff17229930eef2f54e281dbe99fa6cbd6a85e7c27bb9d + languageName: node + linkType: hard + "@tanstack/query-core@npm:^5.62.16": version: 5.90.20 resolution: "@tanstack/query-core@npm:5.90.20" @@ -6955,7 +7068,16 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:22.7.5, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": +"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": + version: 25.6.0 + resolution: "@types/node@npm:25.6.0" + dependencies: + undici-types: "npm:~7.19.0" + checksum: 10/99b18690a4be55904cbf8f6a6ac8eed5ec5b8d791fdd8ee2ae598b46c0fa9b83cda7b70dd7f00dbfb18189dcfc67648fdc7fdd3fcced2619a5a6453d9aec107d + languageName: node + linkType: hard + +"@types/node@npm:22.7.5": version: 22.7.5 resolution: "@types/node@npm:22.7.5" dependencies: @@ -7105,7 +7227,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.54.0, @typescript-eslint/eslint-plugin@npm:^8.48.0": +"@typescript-eslint/eslint-plugin@npm:8.54.0": version: 8.54.0 resolution: "@typescript-eslint/eslint-plugin@npm:8.54.0" dependencies: @@ -7125,7 +7247,27 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.54.0, @typescript-eslint/parser@npm:^8.48.0": +"@typescript-eslint/eslint-plugin@npm:^8.48.0": + version: 8.58.1 + resolution: "@typescript-eslint/eslint-plugin@npm:8.58.1" + dependencies: + "@eslint-community/regexpp": "npm:^4.12.2" + "@typescript-eslint/scope-manager": "npm:8.58.1" + "@typescript-eslint/type-utils": "npm:8.58.1" + "@typescript-eslint/utils": "npm:8.58.1" + "@typescript-eslint/visitor-keys": "npm:8.58.1" + ignore: "npm:^7.0.5" + natural-compare: "npm:^1.4.0" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + "@typescript-eslint/parser": ^8.58.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/0fcbe6faadb77313aa91c895c977a24fc72a79eed62f46f7b2d5804db52a9af99351b33b9c4d73fdabb0f69772d5d4a9acdef249a0d1526a44d3817fb51419b5 + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:8.54.0": version: 8.54.0 resolution: "@typescript-eslint/parser@npm:8.54.0" dependencies: @@ -7141,6 +7283,22 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:^8.48.0": + version: 8.58.1 + resolution: "@typescript-eslint/parser@npm:8.58.1" + dependencies: + "@typescript-eslint/scope-manager": "npm:8.58.1" + "@typescript-eslint/types": "npm:8.58.1" + "@typescript-eslint/typescript-estree": "npm:8.58.1" + "@typescript-eslint/visitor-keys": "npm:8.58.1" + debug: "npm:^4.4.3" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/062584d26609e82169459ebf0c59f4925ba6596f4ea1637a320c34a25c34117585c458b9c6c268f5eeaee1988f4c7257d34d4bd05a214a88de12110e71b48493 + languageName: node + linkType: hard + "@typescript-eslint/project-service@npm:8.54.0": version: 8.54.0 resolution: "@typescript-eslint/project-service@npm:8.54.0" @@ -7154,6 +7312,19 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/project-service@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/project-service@npm:8.58.1" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.58.1" + "@typescript-eslint/types": "npm:^8.58.1" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10/2f3136268fc262e77e8c8c14291e60c54e0228b63ccb022826b6def6d80b83ce9c3a92fef11c888889fb204343c845556868c49495c3aa0a115e9a861dd5fe99 + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:8.54.0, @typescript-eslint/scope-manager@npm:^8.1.0": version: 8.54.0 resolution: "@typescript-eslint/scope-manager@npm:8.54.0" @@ -7164,6 +7335,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/scope-manager@npm:8.58.1" + dependencies: + "@typescript-eslint/types": "npm:8.58.1" + "@typescript-eslint/visitor-keys": "npm:8.58.1" + checksum: 10/dc070fd73847807e32cb7dfc37512abd0b1a485b0037d8cfb6c593555a5b673d3ee9d19c61504ea71d067ad610c66f64d70d56f3a5db51895c0a25e45621cd08 + languageName: node + linkType: hard + "@typescript-eslint/tsconfig-utils@npm:8.54.0, @typescript-eslint/tsconfig-utils@npm:^8.54.0": version: 8.54.0 resolution: "@typescript-eslint/tsconfig-utils@npm:8.54.0" @@ -7173,6 +7354,15 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/tsconfig-utils@npm:8.58.1, @typescript-eslint/tsconfig-utils@npm:^8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.1" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10/4a5cf9a5eb834d05f2d37f7d80319575cf4a75aa52807b96edc0db24349ba417b41cb6f5257ffb07b8b9b4c59c7438637e8c75ed7c2b513bcb07e259b49e058e + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.54.0": version: 8.54.0 resolution: "@typescript-eslint/type-utils@npm:8.54.0" @@ -7189,6 +7379,22 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/type-utils@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/type-utils@npm:8.58.1" + dependencies: + "@typescript-eslint/types": "npm:8.58.1" + "@typescript-eslint/typescript-estree": "npm:8.58.1" + "@typescript-eslint/utils": "npm:8.58.1" + debug: "npm:^4.4.3" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/39d62d6711590e817cf9a36257c19ea18e201ceca42b900350e121ea8986c167fbdd9da385ced29c61e38a1b5c76b6c320d59e21d4dd7f32767520e31aef4654 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:8.54.0, @typescript-eslint/types@npm:^8.54.0": version: 8.54.0 resolution: "@typescript-eslint/types@npm:8.54.0" @@ -7196,6 +7402,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.58.1, @typescript-eslint/types@npm:^8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/types@npm:8.58.1" + checksum: 10/447e1351af8a47297096f063b327c69b1c986af89e39cb39e142bb35d7bec2ce8f34f31edcf62d1beb2e09a38e2029b12b50b335dae4e7c9ff49bd82f9127523 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.54.0": version: 8.54.0 resolution: "@typescript-eslint/typescript-estree@npm:8.54.0" @@ -7215,7 +7428,26 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.54.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.1.0": +"@typescript-eslint/typescript-estree@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.58.1" + dependencies: + "@typescript-eslint/project-service": "npm:8.58.1" + "@typescript-eslint/tsconfig-utils": "npm:8.58.1" + "@typescript-eslint/types": "npm:8.58.1" + "@typescript-eslint/visitor-keys": "npm:8.58.1" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10/107510b484148a8a9a5874f5451b9a6649609607ee5e67de36cded786157987a5262b145398b1bd1935afab66134532369a4d6abb53c6f5b7744e3ace0b13f07 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:8.54.0, @typescript-eslint/utils@npm:^8.1.0": version: 8.54.0 resolution: "@typescript-eslint/utils@npm:8.54.0" dependencies: @@ -7230,6 +7462,21 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:8.58.1, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0": + version: 8.58.1 + resolution: "@typescript-eslint/utils@npm:8.58.1" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.58.1" + "@typescript-eslint/types": "npm:8.58.1" + "@typescript-eslint/typescript-estree": "npm:8.58.1" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10/c51a5e116d1a09d0eb701c5884d5b9b8c22f79c427cb4c46357e4bcb7dfdfd9beba92e5d518572f42111b7335541a4ccefe3c05595fc3d666c1b62ddd1522e54 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.54.0": version: 8.54.0 resolution: "@typescript-eslint/visitor-keys@npm:8.54.0" @@ -7240,6 +7487,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.58.1" + dependencies: + "@typescript-eslint/types": "npm:8.58.1" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10/e9f34741da6fc0cb8e9eb67828ea4427ac2004a33ce8d1e1e9ba038471f9ed68405eca871651bb2efa793a467bc5233a4310c5571ad1497cb2a84a600e1733a8 + languageName: node + linkType: hard + "@vercel/stega@npm:^0.1.2": version: 0.1.2 resolution: "@vercel/stega@npm:0.1.2" @@ -7334,6 +7591,13 @@ __metadata: languageName: node linkType: hard +"abbrev@npm:^4.0.0": + version: 4.0.0 + resolution: "abbrev@npm:4.0.0" + checksum: 10/e2f0c6a6708ad738b3e8f50233f4800de31ad41a6cdc50e0cbe51b76fed69fd0213516d92c15ce1a9985fca71a14606a9be22bf00f8475a58987b9bfb671c582 + languageName: node + linkType: hard + "abitype@npm:1.2.3, abitype@npm:^1.2.3": version: 1.2.3 resolution: "abitype@npm:1.2.3" @@ -7435,7 +7699,7 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.1": version: 7.1.1 resolution: "agent-base@npm:7.1.1" dependencies: @@ -7444,6 +7708,13 @@ __metadata: languageName: node linkType: hard +"agent-base@npm:^7.1.0": + version: 7.1.4 + resolution: "agent-base@npm:7.1.4" + checksum: 10/79bef167247789f955aaba113bae74bf64aa1e1acca4b1d6bb444bdf91d82c3e07e9451ef6a6e2e35e8f71a6f97ce33e3d855a5328eb9fad1bc3cc4cfd031ed8 + languageName: node + linkType: hard + "aggregate-error@npm:^3.0.0": version: 3.1.0 resolution: "aggregate-error@npm:3.1.0" @@ -7608,6 +7879,20 @@ __metadata: languageName: node linkType: hard +"async-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-function@npm:1.0.0" + checksum: 10/1a09379937d846f0ce7614e75071c12826945d4e417db634156bf0e4673c495989302f52186dfa9767a1d9181794554717badd193ca2bbab046ef1da741d8efd + languageName: node + linkType: hard + +"async-generator-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-generator-function@npm:1.0.0" + checksum: 10/3d49e7acbeee9e84537f4cb0e0f91893df8eba976759875ae8ee9e3d3c82f6ecdebdb347c2fad9926b92596d93cdfc78ecc988bcdf407e40433e8e8e6fe5d78e + languageName: node + linkType: hard + "async-mutex@npm:^0.3.1": version: 0.3.2 resolution: "async-mutex@npm:0.3.2" @@ -7747,6 +8032,13 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 + languageName: node + linkType: hard + "bare-events@npm:^2.2.0": version: 2.4.2 resolution: "bare-events@npm:2.4.2" @@ -7909,6 +8201,15 @@ __metadata: languageName: node linkType: hard +"brace-expansion@npm:^5.0.5": + version: 5.0.5 + resolution: "brace-expansion@npm:5.0.5" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10/f259b2ddf04489da9512ad637ba6b4ef2d77abd4445d20f7f1714585f153435200a53fa6a2e4a5ee974df14ddad4cd16421f6f803e96e8b452bd48598878d0ee + languageName: node + linkType: hard + "braces@npm:^3.0.3": version: 3.0.3 resolution: "braces@npm:3.0.3" @@ -8051,6 +8352,24 @@ __metadata: languageName: node linkType: hard +"cacache@npm:^20.0.1": + version: 20.0.4 + resolution: "cacache@npm:20.0.4" + dependencies: + "@npmcli/fs": "npm:^5.0.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^13.0.0" + lru-cache: "npm:^11.1.0" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^7.0.2" + ssri: "npm:^13.0.0" + checksum: 10/02c1b4c57dc2473e6f4654220c9405b73ae5fcdb392f82a7cf535468a52b842690cdb3694861d13bbe4dc067d5f8abe9697b4f791ae5b65cd73d62abad1e3e54 + languageName: node + linkType: hard + "call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": version: 1.0.2 resolution: "call-bind-apply-helpers@npm:1.0.2" @@ -8073,6 +8392,16 @@ __metadata: languageName: node linkType: hard +"call-bound@npm:^1.0.2": + version: 1.0.4 + resolution: "call-bound@npm:1.0.4" + dependencies: + call-bind-apply-helpers: "npm:^1.0.2" + get-intrinsic: "npm:^1.3.0" + checksum: 10/ef2b96e126ec0e58a7ff694db43f4d0d44f80e641370c21549ed911fecbdbc2df3ebc9bddad918d6bbdefeafb60bb3337902006d5176d72bcd2da74820991af7 + languageName: node + linkType: hard + "callsite@npm:^1.0.0": version: 1.0.0 resolution: "callsite@npm:1.0.0" @@ -8930,7 +9259,7 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.15.0, enhanced-resolve@npm:^5.17.1": +"enhanced-resolve@npm:^5.15.0": version: 5.18.0 resolution: "enhanced-resolve@npm:5.18.0" dependencies: @@ -8940,6 +9269,16 @@ __metadata: languageName: node linkType: hard +"enhanced-resolve@npm:^5.17.1": + version: 5.20.1 + resolution: "enhanced-resolve@npm:5.20.1" + dependencies: + graceful-fs: "npm:^4.2.4" + tapable: "npm:^2.3.0" + checksum: 10/588afc56de97334e5742faebcf8177a504da08ea817d399f9901f35d8e9e5e6fa86b4c2ce95a99081f947764e09c9991cc0fc0ba5751bae455c329643a709187 + languageName: node + linkType: hard + "entities@npm:^4.5.0": version: 4.5.0 resolution: "entities@npm:4.5.0" @@ -9371,6 +9710,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^5.0.0": + version: 5.0.1 + resolution: "eslint-visitor-keys@npm:5.0.1" + checksum: 10/f9cc1a57b75e0ef949545cac33d01e8367e302de4c1483266ed4d8646ee5c306376660196bbb38b004e767b7043d1e661cb4336b49eff634a1bbe75c1db709ec + languageName: node + linkType: hard + "eslint@npm:^9.39.1": version: 9.39.1 resolution: "eslint@npm:9.39.1" @@ -10182,6 +10528,13 @@ __metadata: languageName: node linkType: hard +"generator-function@npm:^2.0.0": + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 10/eb7e7eb896c5433f3d40982b2ccacdb3dd990dd3499f14040e002b5d54572476513be8a2e6f9609f6e41ab29f2c4469307611ddbfc37ff4e46b765c326663805 + languageName: node + linkType: hard + "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -10196,21 +10549,24 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.4": - version: 1.3.0 - resolution: "get-intrinsic@npm:1.3.0" +"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.3.0": + version: 1.3.1 + resolution: "get-intrinsic@npm:1.3.1" dependencies: + async-function: "npm:^1.0.0" + async-generator-function: "npm:^1.0.0" call-bind-apply-helpers: "npm:^1.0.2" es-define-property: "npm:^1.0.1" es-errors: "npm:^1.3.0" es-object-atoms: "npm:^1.1.1" function-bind: "npm:^1.1.2" + generator-function: "npm:^2.0.0" get-proto: "npm:^1.0.1" gopd: "npm:^1.2.0" has-symbols: "npm:^1.1.0" hasown: "npm:^2.0.2" math-intrinsics: "npm:^1.1.0" - checksum: 10/6e9dd920ff054147b6f44cb98104330e87caafae051b6d37b13384a45ba15e71af33c3baeac7cb630a0aaa23142718dcf25b45cfdd86c184c5dcb4e56d953a10 + checksum: 10/bb579dda84caa4a3a41611bdd483dade7f00f246f2a7992eb143c5861155290df3fdb48a8406efa3dfb0b434e2c8fafa4eebd469e409d0439247f85fc3fa2cc1 languageName: node linkType: hard @@ -10279,7 +10635,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7": +"glob@npm:^10.2.2, glob@npm:^10.3.10": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -10295,6 +10651,33 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.3.7": + version: 10.5.0 + resolution: "glob@npm:10.5.0" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10/ab3bccfefcc0afaedbd1f480cd0c4a2c0e322eb3f0aa7ceaa31b3f00b825069f17cf0f1fc8b6f256795074b903f37c0ade37ddda6a176aa57f1c2bbfe7240653 + languageName: node + linkType: hard + +"glob@npm:^13.0.0": + version: 13.0.6 + resolution: "glob@npm:13.0.6" + dependencies: + minimatch: "npm:^10.2.2" + minipass: "npm:^7.1.3" + path-scurry: "npm:^2.0.2" + checksum: 10/201ad69e5f0aa74e1d8c00a481581f8b8c804b6a4fbfabeeb8541f5d756932800331daeba99b58fb9e4cd67e12ba5a7eba5b82fb476691588418060b84353214 + languageName: node + linkType: hard + "glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.7": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -10971,6 +11354,13 @@ __metadata: languageName: node linkType: hard +"isexe@npm:^4.0.0": + version: 4.0.0 + resolution: "isexe@npm:4.0.0" + checksum: 10/2ead327ef596042ef9c9ec5f236b316acfaedb87f4bb61b3c3d574fb2e9c8a04b67305e04733bde52c24d9622fdebd3270aadb632adfbf9cadef88fe30f479e5 + languageName: node + linkType: hard + "isomorphic-fetch@npm:^3.0.0": version: 3.0.0 resolution: "isomorphic-fetch@npm:3.0.0" @@ -11952,6 +12342,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": + version: 11.3.3 + resolution: "lru-cache@npm:11.3.3" + checksum: 10/d45c1992232d0ab6ee5a9b93f62f43205ba5eeca51b2cfe6fa40a6aeb91b3cb0a318b273ab29ab36b66f779d4111df514c01bb7613ca4d4028953de49ecb82a9 + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -12029,6 +12426,26 @@ __metadata: languageName: node linkType: hard +"make-fetch-happen@npm:^15.0.0": + version: 15.0.5 + resolution: "make-fetch-happen@npm:15.0.5" + dependencies: + "@gar/promise-retry": "npm:^1.0.0" + "@npmcli/agent": "npm:^4.0.0" + "@npmcli/redact": "npm:^4.0.0" + cacache: "npm:^20.0.1" + http-cache-semantics: "npm:^4.1.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^5.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^1.0.0" + proc-log: "npm:^6.0.0" + ssri: "npm:^13.0.0" + checksum: 10/d2649effb06c00cb2b266057cb1c8c1e99cfc8d1378e7d9c26cc8f00be41bc63d59b77a5576ed28f8105acc57fb16220b64217f8d3a6a066a594c004aa163afa + languageName: node + linkType: hard + "makeerror@npm:1.0.12": version: 1.0.12 resolution: "makeerror@npm:1.0.12" @@ -12240,6 +12657,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^10.2.2": + version: 10.2.5 + resolution: "minimatch@npm:10.2.5" + dependencies: + brace-expansion: "npm:^5.0.5" + checksum: 10/19e87a931aff60ee7b9d80f39f817b8bfc54f61f8356ee3549fbf636dbccacacfec8d803eac73293955c4527cd085247dfc064bce4a5e349f8f3b85e2bf5da0f + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -12309,6 +12735,21 @@ __metadata: languageName: node linkType: hard +"minipass-fetch@npm:^5.0.0": + version: 5.0.2 + resolution: "minipass-fetch@npm:5.0.2" + dependencies: + iconv-lite: "npm:^0.7.2" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^2.0.0" + minizlib: "npm:^3.0.1" + dependenciesMeta: + iconv-lite: + optional: true + checksum: 10/4f3f65ea5b20a3a287765ebf21cc73e62031f754944272df2a3039296cc75a8fc2dc50b8a3c4f39ce3ac6e5cc583e8dc664d12c6ab98e0883d263e49f344bc86 + languageName: node + linkType: hard + "minipass-flush@npm:^1.0.5": version: 1.0.5 resolution: "minipass-flush@npm:1.0.5" @@ -12336,6 +12777,15 @@ __metadata: languageName: node linkType: hard +"minipass-sized@npm:^2.0.0": + version: 2.0.0 + resolution: "minipass-sized@npm:2.0.0" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10/3b89adf64ca705662f77481e278eff5ec0a57aeffb5feba7cc8843722b1e7770efc880f2a17d1d4877b2d7bf227873cd46afb4da44c0fd18088b601ea50f96bb + languageName: node + linkType: hard + "minipass@npm:^3.0.0": version: 3.3.6 resolution: "minipass@npm:3.3.6" @@ -12352,10 +12802,10 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": - version: 7.1.2 - resolution: "minipass@npm:7.1.2" - checksum: 10/c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950 +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6 languageName: node linkType: hard @@ -12369,12 +12819,12 @@ __metadata: languageName: node linkType: hard -"minizlib@npm:^3.0.1": - version: 3.0.2 - resolution: "minizlib@npm:3.0.2" +"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" dependencies: minipass: "npm:^7.1.2" - checksum: 10/c075bed1594f68dcc8c35122333520112daefd4d070e5d0a228bd4cf5580e9eed3981b96c0ae1d62488e204e80fd27b2b9d0068ca9a5ef3993e9565faf63ca41 + checksum: 10/f47365cc2cb7f078cbe7e046eb52655e2e7e97f8c0a9a674f4da60d94fb0624edfcec9b5db32e8ba5a99a5f036f595680ae6fe02a262beaa73026e505cc52f99 languageName: node linkType: hard @@ -12394,15 +12844,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^3.0.1": - version: 3.0.1 - resolution: "mkdirp@npm:3.0.1" - bin: - mkdirp: dist/cjs/src/bin.js - checksum: 10/16fd79c28645759505914561e249b9a1f5fe3362279ad95487a4501e4467abeb714fd35b95307326b8fd03f3c7719065ef11a6f97b7285d7888306d1bd2232ba - languageName: node - linkType: hard - "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -12474,6 +12915,13 @@ __metadata: languageName: node linkType: hard +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10/b5734e87295324fabf868e36fb97c84b7d7f3156ec5f4ee5bf6e488079c11054f818290fc33804cef7b1ee21f55eeb14caea83e7dafae6492a409b3e573153e5 + languageName: node + linkType: hard + "neo-async@npm:^2.6.2": version: 2.6.2 resolution: "neo-async@npm:2.6.2" @@ -12535,7 +12983,7 @@ __metadata: languageName: node linkType: hard -"node-gyp@npm:^10.0.0, node-gyp@npm:latest": +"node-gyp@npm:^10.0.0": version: 10.2.0 resolution: "node-gyp@npm:10.2.0" dependencies: @@ -12555,6 +13003,26 @@ __metadata: languageName: node linkType: hard +"node-gyp@npm:latest": + version: 12.2.0 + resolution: "node-gyp@npm:12.2.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^15.0.0" + nopt: "npm:^9.0.0" + proc-log: "npm:^6.0.0" + semver: "npm:^7.3.5" + tar: "npm:^7.5.4" + tinyglobby: "npm:^0.2.12" + which: "npm:^6.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10/4ebab5b77585a637315e969c2274b5520562473fe75de850639a580c2599652fb9f33959ec782ea45a2e149d8f04b548030f472eeeb3dbdf19a7f2ccbc30b908 + languageName: node + linkType: hard + "node-int64@npm:^0.4.0": version: 0.4.0 resolution: "node-int64@npm:0.4.0" @@ -12580,6 +13048,17 @@ __metadata: languageName: node linkType: hard +"nopt@npm:^9.0.0": + version: 9.0.0 + resolution: "nopt@npm:9.0.0" + dependencies: + abbrev: "npm:^4.0.0" + bin: + nopt: bin/nopt.js + checksum: 10/56a1ccd2ad711fb5115918e2c96828703cddbe12ba2c3bd00591758f6fa30e6f47dd905c59dbfcf9b773f3a293b45996609fb6789ae29d6bfcc3cf3a6f7d9fda + languageName: node + linkType: hard + "normalize-package-data@npm:^2.5.0": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" @@ -12704,6 +13183,13 @@ __metadata: languageName: node linkType: hard +"object-inspect@npm:^1.13.3, object-inspect@npm:^1.13.4": + version: 1.13.4 + resolution: "object-inspect@npm:1.13.4" + checksum: 10/aa13b1190ad3e366f6c83ad8a16ed37a19ed57d267385aa4bfdccda833d7b90465c057ff6c55d035a6b2e52c1a2295582b294217a0a3a1ae7abdd6877ef781fb + languageName: node + linkType: hard + "on-finished@npm:2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -12901,6 +13387,13 @@ __metadata: languageName: node linkType: hard +"p-map@npm:^7.0.2": + version: 7.0.4 + resolution: "p-map@npm:7.0.4" + checksum: 10/ef48c3b2e488f31c693c9fcc0df0ef76518cf6426a495cf9486ebbb0fd7f31aef7f90e96f72e0070c0ff6e3177c9318f644b512e2c29e3feee8d7153fcb6782e + languageName: node + linkType: hard + "p-throttle@npm:^4.1.1": version: 4.1.1 resolution: "p-throttle@npm:4.1.1" @@ -13028,6 +13521,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" + dependencies: + lru-cache: "npm:^11.0.0" + minipass: "npm:^7.1.2" + checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3 + languageName: node + linkType: hard + "path-to-regexp@npm:0.1.12": version: 0.1.12 resolution: "path-to-regexp@npm:0.1.12" @@ -13076,6 +13579,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.4": + version: 4.0.4 + resolution: "picomatch@npm:4.0.4" + checksum: 10/f6ef80a3590827ce20378ae110ac78209cc4f74d39236370f1780f957b7ee41c12acde0e4651b90f39983506fd2f5e449994716f516db2e9752924aff8de93ce + languageName: node + linkType: hard + "pify@npm:^5.0.0": version: 5.0.0 resolution: "pify@npm:5.0.0" @@ -13142,7 +13652,7 @@ __metadata: languageName: node linkType: hard -"prettier-2@npm:prettier@^2.8.8": +"prettier-2@npm:prettier@^2.8.8, prettier@npm:^2.8.8": version: 2.8.8 resolution: "prettier@npm:2.8.8" bin: @@ -13187,6 +13697,13 @@ __metadata: languageName: node linkType: hard +"proc-log@npm:^6.0.0": + version: 6.1.0 + resolution: "proc-log@npm:6.1.0" + checksum: 10/9033f30f168ed5a0991b773d0c50ff88384c4738e9a0a67d341de36bf7293771eed648ab6a0562f62276da12fde91f3bbfc75ffff6e71ad49aafd74fc646be66 + languageName: node + linkType: hard + "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -13310,7 +13827,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.13.0, qs@npm:^6.11.2": +"qs@npm:6.13.0": version: 6.13.0 resolution: "qs@npm:6.13.0" dependencies: @@ -13319,6 +13836,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.11.2": + version: 6.15.1 + resolution: "qs@npm:6.15.1" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10/ec10b9957446b3f4a38000940f6374720b4e2985209b89df197066038c951472ea24cd98d6bc6df73a0cbec75bc056f638032e3fb447345017ff7e0f0a2693ac + languageName: node + linkType: hard + "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -13935,6 +14461,41 @@ __metadata: languageName: node linkType: hard +"side-channel-list@npm:^1.0.0": + version: 1.0.1 + resolution: "side-channel-list@npm:1.0.1" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.4" + checksum: 10/3499671cd52adaee739eac1e14d07530b8e3530192741aeb05e7fe4ad1b51d1368ceea2cd3c21b0f62b05410a5c70a7c4d997ba4b143303ef73d0c65dfd1c252 + languageName: node + linkType: hard + +"side-channel-map@npm:^1.0.1": + version: 1.0.1 + resolution: "side-channel-map@npm:1.0.1" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + checksum: 10/5771861f77feefe44f6195ed077a9e4f389acc188f895f570d56445e251b861754b547ea9ef73ecee4e01fdada6568bfe9020d2ec2dfc5571e9fa1bbc4a10615 + languageName: node + linkType: hard + +"side-channel-weakmap@npm:^1.0.2": + version: 1.0.2 + resolution: "side-channel-weakmap@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + side-channel-map: "npm:^1.0.1" + checksum: 10/a815c89bc78c5723c714ea1a77c938377ea710af20d4fb886d362b0d1f8ac73a17816a5f6640f354017d7e292a43da9c5e876c22145bac00b76cfb3468001736 + languageName: node + linkType: hard + "side-channel@npm:^1.0.6": version: 1.0.6 resolution: "side-channel@npm:1.0.6" @@ -13947,6 +14508,19 @@ __metadata: languageName: node linkType: hard +"side-channel@npm:^1.1.0": + version: 1.1.0 + resolution: "side-channel@npm:1.1.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + side-channel-list: "npm:^1.0.0" + side-channel-map: "npm:^1.0.1" + side-channel-weakmap: "npm:^1.0.2" + checksum: 10/7d53b9db292c6262f326b6ff3bc1611db84ece36c2c7dc0e937954c13c73185b0406c56589e2bb8d071d6fee468e14c39fb5d203ee39be66b7b8174f179afaba + languageName: node + linkType: hard + "signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -14131,6 +14705,15 @@ __metadata: languageName: node linkType: hard +"ssri@npm:^13.0.0": + version: 13.0.1 + resolution: "ssri@npm:13.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10/ae560d0378d074006a71b06af71bfbe84a3fe1ac6e16c1f07575f69e670d40170507fe52b21bcc23399429bc6a15f4bc3ea8d9bc88e9dfd7e87de564e6da6a72 + languageName: node + linkType: hard + "stable-hash@npm:^0.0.4": version: 0.0.4 resolution: "stable-hash@npm:0.0.4" @@ -14349,6 +14932,13 @@ __metadata: languageName: node linkType: hard +"tapable@npm:^2.3.0": + version: 2.3.2 + resolution: "tapable@npm:2.3.2" + checksum: 10/fd3affe2e34efb3970883f934b1828f10b48dffb1eb71a52b7f955bfdd88bf80e94ec388704d95334f72ddf77e34d813b19e1f4bf56897d20252fa025d44bede + languageName: node + linkType: hard + "tar-stream@npm:^3.1.7": version: 3.1.7 resolution: "tar-stream@npm:3.1.7" @@ -14374,17 +14964,16 @@ __metadata: languageName: node linkType: hard -"tar@npm:^7.4.3": - version: 7.4.3 - resolution: "tar@npm:7.4.3" +"tar@npm:^7.4.3, tar@npm:^7.5.4": + version: 7.5.13 + resolution: "tar@npm:7.5.13" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" - minizlib: "npm:^3.0.1" - mkdirp: "npm:^3.0.1" + minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10/12a2a4fc6dee23e07cc47f1aeb3a14a1afd3f16397e1350036a8f4cdfee8dcac7ef5978337a4e7b2ac2c27a9a6d46388fc2088ea7c80cb6878c814b1425f8ecf + checksum: 10/2bc2b6f0349038a6621dbba1c4522d45752d5071b2994692257113c2050cd23fafc30308f820e5f8ad6fda3f7d7f92adc9a432aa733daa04c42af2061c021c3f languageName: node linkType: hard @@ -14408,6 +14997,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.12": + version: 0.2.16 + resolution: "tinyglobby@npm:0.2.16" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.4" + checksum: 10/5c2c41b572ada38449e7c86a5fe034f204a1dbba577225a761a14f29f48dc3f2fc0d81a6c56fcc67c5a742cc3aa9fb5e2ca18dbf22b610b0bc0e549b34d5a0f8 + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" @@ -14492,6 +15091,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^2.5.0": + version: 2.5.0 + resolution: "ts-api-utils@npm:2.5.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: 10/d5f1936f5618c6ab6942a97b78802217540ced00e7501862ae1f578d9a3aa189fc06050e64cb8951d21f7088e5fd35f53d2bf0d0370a883861c7b05e993ebc44 + languageName: node + linkType: hard + "ts-jest@npm:^29.2.5": version: 29.4.6 resolution: "ts-jest@npm:29.4.6" @@ -14739,6 +15347,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~7.19.0": + version: 7.19.2 + resolution: "undici-types@npm:7.19.2" + checksum: 10/05c34c63444c8caca7137f122b29ed50c1d7d05d1e0b2337f423575d3264054c4a0139e47e82e65723d09b97fcad6d8b0223b3550430a9006cc00e72a1e035bf + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0" @@ -15128,6 +15743,17 @@ __metadata: languageName: node linkType: hard +"which@npm:^6.0.0": + version: 6.0.1 + resolution: "which@npm:6.0.1" + dependencies: + isexe: "npm:^4.0.0" + bin: + node-which: bin/which.js + checksum: 10/dbea77c7d3058bf6c78bf9659d2dce4d2b57d39a15b826b2af6ac2e5a219b99dc8a831b79fdbc453c0598adb4f3f84cf9c2491fd52beb9f5d2dececcad117f68 + languageName: node + linkType: hard + "word-wrap@npm:^1.2.5": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" From 2a3e478216c530b84c3b4919578335ad27815a6c Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 14 Apr 2026 18:58:12 +0100 Subject: [PATCH 03/26] feat: use webcrypto instead of global this --- .../src/MoneyAccountUpgradeController.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index f5b157bc0c5..d87ba411de0 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -20,6 +20,7 @@ import type { import type { Messenger } from '@metamask/messenger'; import type { MoneyAccountControllerGetMoneyAccountAction } from '@metamask/money-account-controller'; import type { Hex } from '@metamask/utils'; +import { webcrypto } from 'node:crypto'; import type { MoneyAccountUpgradeControllerMethodActions } from './MoneyAccountUpgradeController-method-action-types'; import type { AccountUpgradeEntry, UpgradeConfig } from './types'; @@ -242,8 +243,7 @@ export class MoneyAccountUpgradeController extends BaseController< */ async #verifyDelegation(address: Hex, chainId: Hex): Promise { const salt: Hex = `0x${Array.from( - // TODO: do I need to read this off of globalThis? - globalThis.crypto.getRandomValues(new Uint8Array(32)), + webcrypto.getRandomValues(new Uint8Array(32)), ) .map((b) => b.toString(16).padStart(2, '0')) .join('')}`; @@ -293,7 +293,9 @@ export class MoneyAccountUpgradeController extends BaseController< if (!result.valid) { throw new Error( - `Delegation verification failed: ${result.errors?.join(', ') ?? 'unknown error'}`, + `Delegation verification failed: ${ + result.errors?.join(', ') ?? 'unknown error' + }`, ); } From a1b9450c23ec4c4b600af257b83abf2052b3bd2e Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 14 Apr 2026 19:03:08 +0100 Subject: [PATCH 04/26] chore: fix version of auto-changelog --- .github/CODEOWNERS | 3 ++ README.md | 7 +++ .../CHANGELOG.md | 2 +- .../package.json | 10 ++--- yarn.lock | 45 +++++++------------ 5 files changed, 33 insertions(+), 34 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1819430016d..06069960fcf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -41,6 +41,7 @@ /packages/earn-controller @MetaMask/earn /packages/money-account-balance-service @MetaMask/earn /packages/chomp-api-service @MetaMask/earn @MetaMask/delegation +/packages/money-account-upgrade-controller @MetaMask/earn ## Social AI Team /packages/ai-controllers @MetaMask/social-ai @@ -232,3 +233,5 @@ /packages/money-account-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform /packages/chomp-api-service/package.json @MetaMask/earn @MetaMask/delegation @MetaMask/core-platform /packages/chomp-api-service/CHANGELOG.md @MetaMask/earn @MetaMask/delegation @MetaMask/core-platform +/packages/money-account-upgrade-controller/package.json @MetaMask/accounts-engineers @MetaMask/core-platform +/packages/money-account-upgrade-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform diff --git a/README.md b/README.md index 98c5599b03d..bb674647ea2 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Each package in this repository has its own README where you can find installati - [`@metamask/messenger-cli`](packages/messenger-cli) - [`@metamask/money-account-balance-service`](packages/money-account-balance-service) - [`@metamask/money-account-controller`](packages/money-account-controller) +- [`@metamask/money-account-upgrade-controller`](packages/money-account-upgrade-controller) - [`@metamask/multichain-account-service`](packages/multichain-account-service) - [`@metamask/multichain-api-middleware`](packages/multichain-api-middleware) - [`@metamask/multichain-network-controller`](packages/multichain-network-controller) @@ -156,6 +157,7 @@ linkStyle default opacity:0.5 messenger_cli(["@metamask/messenger-cli"]); money_account_balance_service(["@metamask/money-account-balance-service"]); money_account_controller(["@metamask/money-account-controller"]); + money_account_upgrade_controller(["@metamask/money-account-upgrade-controller"]); multichain_account_service(["@metamask/multichain-account-service"]); multichain_api_middleware(["@metamask/multichain-api-middleware"]); multichain_network_controller(["@metamask/multichain-network-controller"]); @@ -367,6 +369,11 @@ linkStyle default opacity:0.5 money_account_controller --> base_controller; money_account_controller --> keyring_controller; money_account_controller --> messenger; + money_account_upgrade_controller --> base_controller; + money_account_upgrade_controller --> chomp_api_service; + money_account_upgrade_controller --> keyring_controller; + money_account_upgrade_controller --> messenger; + money_account_upgrade_controller --> money_account_controller; multichain_account_service --> accounts_controller; multichain_account_service --> base_controller; multichain_account_service --> keyring_controller; diff --git a/packages/money-account-upgrade-controller/CHANGELOG.md b/packages/money-account-upgrade-controller/CHANGELOG.md index 15294a254a7..05cd7b12065 100644 --- a/packages/money-account-upgrade-controller/CHANGELOG.md +++ b/packages/money-account-upgrade-controller/CHANGELOG.md @@ -9,6 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `MoneyAccountUpgradeController` with multi-step `upgradeAccount` method that orchestrates address association, EIP-7702 authorization, delegation verification, and intent registration via CHOMP +- Add `MoneyAccountUpgradeController` with `upgradeAccount` method ([#8426](https://github.com/MetaMask/core/pull/8426)) [Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json index 27c79e873d9..a78544f6649 100644 --- a/packages/money-account-upgrade-controller/package.json +++ b/packages/money-account-upgrade-controller/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "description": "MetaMask Money account upgrade controller", "keywords": [ - "MetaMask", - "Ethereum" + "Ethereum", + "MetaMask" ], "homepage": "https://github.com/MetaMask/core/tree/main/packages/money-account-upgrade-controller#readme", "bugs": { @@ -51,18 +51,18 @@ "dependencies": { "@metamask/base-controller": "^9.0.1", "@metamask/chomp-api-service": "^0.0.0", - "@metamask/delegation-controller": "^2.1.0", + "@metamask/delegation-controller": "^3.0.0", "@metamask/keyring-controller": "^25.2.0", "@metamask/messenger": "^1.1.1", "@metamask/money-account-controller": "^0.1.0" }, "devDependencies": { - "@metamask/auto-changelog": "^3.4.4", + "@metamask/auto-changelog": "^6.0.0", "@metamask/utils": "^11.9.0", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", "deepmerge": "^4.2.2", - "jest": "^29.2.5", + "jest": "^29.7.0", "ts-jest": "^29.2.5", "tsx": "^4.20.5", "typedoc": "^0.25.13", diff --git a/yarn.lock b/yarn.lock index 2c6c1f6f914..544973ff8f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2585,7 +2585,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/accounts-controller@npm:^37.1.0, @metamask/accounts-controller@npm:^37.2.0, @metamask/accounts-controller@workspace:packages/accounts-controller": +"@metamask/accounts-controller@npm:^37.2.0, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" dependencies: @@ -2945,18 +2945,20 @@ __metadata: languageName: node linkType: hard -"@metamask/auto-changelog@npm:^3.4.4": - version: 3.4.4 - resolution: "@metamask/auto-changelog@npm:3.4.4" +"@metamask/auto-changelog@npm:^6.0.0": + version: 6.0.0 + resolution: "@metamask/auto-changelog@npm:6.0.0" dependencies: + "@octokit/rest": "npm:^20.0.0" diff: "npm:^5.0.0" execa: "npm:^5.1.1" - prettier: "npm:^2.8.8" semver: "npm:^7.3.5" yargs: "npm:^17.0.1" + peerDependencies: + prettier: ">=3.0.0" bin: - auto-changelog: dist/cli.js - checksum: 10/70e98529a153ebeab10410dbc3f567014999f77ed82f2b52f1b36501b28a4e3614c809a90c89600a739d7710595bfecc30e2260410e6afac7539f8db65a48f2c + auto-changelog: dist/cli.mjs + checksum: 10/870dde1f24d3bc3d34238d7cda8a326ff3adb5709939159cd7ac8885fa5f17343789e72711e7cc2c4333999c5ac085a027b53e98d9475bf92f4a918db6ceaf7a languageName: node linkType: hard @@ -3470,20 +3472,7 @@ __metadata: languageName: node linkType: hard -"@metamask/delegation-controller@npm:^2.1.0": - version: 2.1.0 - resolution: "@metamask/delegation-controller@npm:2.1.0" - dependencies: - "@metamask/accounts-controller": "npm:^37.1.0" - "@metamask/base-controller": "npm:^9.0.1" - "@metamask/keyring-controller": "npm:^25.1.1" - "@metamask/messenger": "npm:^1.0.0" - "@metamask/utils": "npm:^11.9.0" - checksum: 10/4baa6c23918cf17d8fffde446dbd4a6b6dfaf6039961b2998164ea67320e6a1dc3dbb13d41baf63a6a24220cd6898e075688e158205b8e41a8c02e0f33f76d83 - languageName: node - linkType: hard - -"@metamask/delegation-controller@workspace:packages/delegation-controller": +"@metamask/delegation-controller@npm:^3.0.0, @metamask/delegation-controller@workspace:packages/delegation-controller": version: 0.0.0-use.local resolution: "@metamask/delegation-controller@workspace:packages/delegation-controller" dependencies: @@ -4248,7 +4237,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^25.1.1, @metamask/keyring-controller@npm:^25.2.0, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@npm:^25.2.0, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: @@ -4451,7 +4440,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/messenger@npm:^1.0.0, @metamask/messenger@npm:^1.1.0, @metamask/messenger@npm:^1.1.1, @metamask/messenger@workspace:packages/messenger": +"@metamask/messenger@npm:^1.1.0, @metamask/messenger@npm:^1.1.1, @metamask/messenger@workspace:packages/messenger": version: 0.0.0-use.local resolution: "@metamask/messenger@workspace:packages/messenger" dependencies: @@ -4541,10 +4530,10 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/money-account-upgrade-controller@workspace:packages/money-account-upgrade-controller" dependencies: - "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/auto-changelog": "npm:^6.0.0" "@metamask/base-controller": "npm:^9.0.1" "@metamask/chomp-api-service": "npm:^0.0.0" - "@metamask/delegation-controller": "npm:^2.1.0" + "@metamask/delegation-controller": "npm:^3.0.0" "@metamask/keyring-controller": "npm:^25.2.0" "@metamask/messenger": "npm:^1.1.1" "@metamask/money-account-controller": "npm:^0.1.0" @@ -4552,7 +4541,7 @@ __metadata: "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" deepmerge: "npm:^4.2.2" - jest: "npm:^29.2.5" + jest: "npm:^29.7.0" ts-jest: "npm:^29.2.5" tsx: "npm:^4.20.5" typedoc: "npm:^0.25.13" @@ -11945,7 +11934,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:^29.2.5, jest@npm:^29.7.0": +"jest@npm:^29.7.0": version: 29.7.0 resolution: "jest@npm:29.7.0" dependencies: @@ -13652,7 +13641,7 @@ __metadata: languageName: node linkType: hard -"prettier-2@npm:prettier@^2.8.8, prettier@npm:^2.8.8": +"prettier-2@npm:prettier@^2.8.8": version: 2.8.8 resolution: "prettier@npm:2.8.8" bin: From 104e425124be531e77ad9271dbce0c74371eb7df Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 14 Apr 2026 19:59:22 +0100 Subject: [PATCH 05/26] chore: update teams.json --- teams.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/teams.json b/teams.json index 7ea11e2eab9..3146f994433 100644 --- a/teams.json +++ b/teams.json @@ -77,5 +77,6 @@ "metamask/remote-feature-flag-controller": "team-extension-platform,team-mobile-platform", "metamask/storage-service": "team-extension-platform,team-mobile-platform", "metamask/config-registry-controller": "team-networks", - "metamask/money-account-controller": "team-accounts-framework" + "metamask/money-account-controller": "team-accounts-framework", + "metamask/money-account-upgrade-controller": "team-earn" } From 0faa0aa7ed8dd70d390074a066da6091e534a3f8 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 14 Apr 2026 20:02:49 +0100 Subject: [PATCH 06/26] chore: fix linting of package.json files --- .../package.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json index a78544f6649..60de36b38de 100644 --- a/packages/money-account-upgrade-controller/package.json +++ b/packages/money-account-upgrade-controller/package.json @@ -10,12 +10,17 @@ "bugs": { "url": "https://github.com/MetaMask/core/issues" }, + "license": "MIT", "repository": { "type": "git", "url": "https://github.com/MetaMask/core.git" }, - "license": "MIT", + "files": [ + "dist/" + ], "sideEffects": false, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", "exports": { ".": { "import": { @@ -29,11 +34,10 @@ }, "./package.json": "./package.json" }, - "main": "./dist/index.cjs", - "types": "./dist/index.d.cts", - "files": [ - "dist/" - ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, "scripts": { "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", "build:all": "ts-bridge --project tsconfig.build.json --verbose --clean", @@ -71,9 +75,5 @@ }, "engines": { "node": "^18.18 || >=20" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" } } From 2f2bfdaa08c6cfb3b0106d4946a8efa7e96e8575 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 14 Apr 2026 20:38:02 +0100 Subject: [PATCH 07/26] fix: include new upgrade controller in build --- tsconfig.build.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tsconfig.build.json b/tsconfig.build.json index 0abc4163fbc..2555a029cde 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -151,6 +151,9 @@ { "path": "./packages/money-account-controller/tsconfig.build.json" }, + { + "path": "./packages/money-account-upgrade-controller/tsconfig.build.json" + }, { "path": "./packages/multichain-account-service/tsconfig.build.json" }, From 258484c458f3b417075da9b7f64f1567bfd3709a Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 16 Apr 2026 14:13:58 +0100 Subject: [PATCH 08/26] feat: add init method to upgrade controller --- .../src/MoneyAccountUpgradeController.test.ts | 205 ++++++++++++++++-- .../src/MoneyAccountUpgradeController.ts | 107 +++++++-- .../src/index.ts | 7 +- .../src/types.ts | 12 + 4 files changed, 291 insertions(+), 40 deletions(-) diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts index 955b9b793b4..cd39c68411b 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -33,6 +33,34 @@ const MOCK_CONFIG: UpgradeConfig = { valueLteEnforcer: '0x7777777777777777777777777777777777777777' as Hex, }; +const MOCK_INIT_CONFIG = { + delegatorImplAddress: MOCK_CONFIG.delegatorImplAddress, + musdTokenAddress: MOCK_CONFIG.musdTokenAddress, + redeemerEnforcer: MOCK_CONFIG.redeemerEnforcer, + valueLteEnforcer: MOCK_CONFIG.valueLteEnforcer, +}; + +const MOCK_SERVICE_DETAILS_RESPONSE = { + auth: { message: 'CHOMP Authentication' }, + chains: { + [MOCK_CHAIN_ID]: { + autoDepositDelegate: MOCK_CONFIG.delegateAddress, + protocol: { + vedaProtocol: { + supportedTokens: [ + { + tokenAddress: MOCK_CONFIG.erc20TransferAmountEnforcer, + tokenDecimals: 18, + }, + ], + adapterAddress: MOCK_CONFIG.vedaVaultAdapterAddress, + intentTypes: ['cash-deposit', 'cash-withdrawal'] as const, + }, + }, + }, + }, +}; + type AllActions = MessengerActions; type AllEvents = MessengerEvents; @@ -49,6 +77,7 @@ type Mocks = { createIntents: jest.Mock; signDelegation: jest.Mock; getMoneyAccount: jest.Mock; + getServiceDetails: jest.Mock; }; function setup({ @@ -87,6 +116,9 @@ function setup({ id: 'account-1', address: MOCK_ADDRESS, }), + getServiceDetails: jest + .fn() + .mockResolvedValue(MOCK_SERVICE_DETAILS_RESPONSE), }; const rootMessenger = new Messenger({ @@ -129,6 +161,10 @@ function setup({ 'MoneyAccountController:getMoneyAccount', mocks.getMoneyAccount, ); + rootMessenger.registerActionHandler( + 'ChompApiService:getServiceDetails', + mocks.getServiceDetails, + ); const messenger: MoneyAccountUpgradeControllerMessenger = new Messenger({ namespace: 'MoneyAccountUpgradeController', @@ -144,6 +180,7 @@ function setup({ 'ChompApiService:getUpgrade', 'ChompApiService:verifyDelegation', 'ChompApiService:createIntents', + 'ChompApiService:getServiceDetails', 'DelegationController:signDelegation', 'MoneyAccountController:getMoneyAccount', ], @@ -154,12 +191,28 @@ function setup({ const controller = new MoneyAccountUpgradeController({ messenger, state, - config: MOCK_CONFIG, }); return { controller, rootMessenger, messenger, mocks }; } +async function setupInitialized({ + state, +}: { + state?: Partial< + ReturnType + >; +} = {}): Promise<{ + controller: MoneyAccountUpgradeController; + rootMessenger: RootMessenger; + messenger: MoneyAccountUpgradeControllerMessenger; + mocks: Mocks; +}> { + const result = setup({ state }); + await result.controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG); + return result; +} + describe('MoneyAccountUpgradeController', () => { describe('constructor', () => { it('initializes with default state when no state is provided', () => { @@ -187,11 +240,123 @@ describe('MoneyAccountUpgradeController', () => { chainId: MOCK_CHAIN_ID, }); }); + + it('starts with initialized set to false', () => { + const { controller } = setup(); + + expect(controller.initialized).toBe(false); + }); + }); + + describe('init', () => { + it('fetches service details and builds config', async () => { + const { controller, mocks } = setup(); + + await controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG); + + expect(mocks.getServiceDetails).toHaveBeenCalledWith([MOCK_CHAIN_ID]); + }); + + it('sets initialized to true after successful init', async () => { + const { controller } = setup(); + + await controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG); + + expect(controller.initialized).toBe(true); + }); + + it('throws when the chain is not found in service details', async () => { + const { controller, mocks } = setup(); + + mocks.getServiceDetails.mockResolvedValue({ + auth: { message: 'CHOMP Authentication' }, + chains: {}, + }); + + await expect( + controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG), + ).rejects.toThrow( + `Chain ${MOCK_CHAIN_ID} not found in service details response`, + ); + + expect(controller.initialized).toBe(false); + }); + + it('throws when vedaProtocol is not found', async () => { + const { controller, mocks } = setup(); + + mocks.getServiceDetails.mockResolvedValue({ + auth: { message: 'CHOMP Authentication' }, + chains: { + [MOCK_CHAIN_ID]: { + autoDepositDelegate: MOCK_CONFIG.delegateAddress, + protocol: {}, + }, + }, + }); + + await expect( + controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG), + ).rejects.toThrow( + `vedaProtocol not found for chain ${MOCK_CHAIN_ID} in service details response`, + ); + + expect(controller.initialized).toBe(false); + }); + + it('throws when supportedTokens is empty', async () => { + const { controller, mocks } = setup(); + + mocks.getServiceDetails.mockResolvedValue({ + auth: { message: 'CHOMP Authentication' }, + chains: { + [MOCK_CHAIN_ID]: { + autoDepositDelegate: MOCK_CONFIG.delegateAddress, + protocol: { + vedaProtocol: { + supportedTokens: [], + adapterAddress: MOCK_CONFIG.vedaVaultAdapterAddress, + intentTypes: ['cash-deposit', 'cash-withdrawal'], + }, + }, + }, + }, + }); + + await expect( + controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG), + ).rejects.toThrow( + `No supported tokens found for vedaProtocol on chain ${MOCK_CHAIN_ID}`, + ); + + expect(controller.initialized).toBe(false); + }); }); describe('upgradeAccount', () => { + it('throws when controller is not initialized', async () => { + const { controller } = setup(); + + await expect( + controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), + ).rejects.toThrow( + 'MoneyAccountUpgradeController is not initialized. Call init() first.', + ); + }); + + it('throws when called with a different chain ID than init', async () => { + const { controller } = await setupInitialized(); + const otherChainId = '0xa4b1' as Hex; + + await expect( + controller.upgradeAccount(MOCK_ADDRESS, otherChainId), + ).rejects.toThrow( + `Chain ID mismatch: controller was initialized for ${MOCK_CHAIN_ID} but upgradeAccount was called with ${otherChainId}`, + ); + }); + it('runs the full upgrade sequence', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); @@ -206,7 +371,7 @@ describe('MoneyAccountUpgradeController', () => { }); it('records final state as register-intents with delegationHash', async () => { - const { controller } = setup(); + const { controller } = await setupInitialized(); await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); @@ -218,7 +383,7 @@ describe('MoneyAccountUpgradeController', () => { }); it('is callable via the messenger', async () => { - const { rootMessenger } = setup(); + const { rootMessenger } = await setupInitialized(); expect( await rootMessenger.call( @@ -232,7 +397,7 @@ describe('MoneyAccountUpgradeController', () => { describe('step 0: associate address', () => { it('signs the authentication message and submits to CHOMP', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); @@ -253,7 +418,7 @@ describe('MoneyAccountUpgradeController', () => { }); it('updates state to associate-address after completion', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); // Make subsequent steps fail so we can check state after step 0. mocks.getUpgrade.mockRejectedValue(new Error('stop')); @@ -270,7 +435,7 @@ describe('MoneyAccountUpgradeController', () => { describe('step 1: submit authorization', () => { it('skips signing when CHOMP already has an upgrade record', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); mocks.getUpgrade.mockResolvedValue({ signerAddress: MOCK_ADDRESS, @@ -285,7 +450,7 @@ describe('MoneyAccountUpgradeController', () => { }); it('signs and submits an EIP-7702 authorization', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); @@ -305,7 +470,7 @@ describe('MoneyAccountUpgradeController', () => { }); it('computes yParity=1 when v is 28', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); const sigWithV28 = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + @@ -324,7 +489,7 @@ describe('MoneyAccountUpgradeController', () => { }); it('parses signature components correctly', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); @@ -346,7 +511,7 @@ describe('MoneyAccountUpgradeController', () => { describe('step 2: verify delegation', () => { it('builds a delegation with three caveats and verifies with CHOMP', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); @@ -382,7 +547,7 @@ describe('MoneyAccountUpgradeController', () => { }); it('includes exactly three caveats', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); @@ -391,7 +556,7 @@ describe('MoneyAccountUpgradeController', () => { }); it('throws when delegation verification fails', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); mocks.verifyDelegation.mockResolvedValue({ valid: false, @@ -404,7 +569,7 @@ describe('MoneyAccountUpgradeController', () => { }); it('throws with unknown error when verification fails without error details', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); mocks.verifyDelegation.mockResolvedValue({ valid: false, @@ -416,7 +581,7 @@ describe('MoneyAccountUpgradeController', () => { }); it('stores delegationHash in state after successful verification', async () => { - const { controller } = setup(); + const { controller } = await setupInitialized(); await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); @@ -428,7 +593,7 @@ describe('MoneyAccountUpgradeController', () => { describe('step 3: save delegation (stub)', () => { it('updates state to save-delegation', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); // Make step 4 fail so we can check state after step 3. mocks.createIntents.mockRejectedValue(new Error('stop')); @@ -445,7 +610,7 @@ describe('MoneyAccountUpgradeController', () => { describe('step 4: register intents', () => { it('submits deposit and withdrawal intents', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); @@ -466,7 +631,7 @@ describe('MoneyAccountUpgradeController', () => { }); it('throws if delegationHash is missing', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); // Skip the verify step by making it not store a hash mocks.verifyDelegation.mockResolvedValue({ @@ -482,7 +647,7 @@ describe('MoneyAccountUpgradeController', () => { describe('error propagation', () => { it('propagates signing errors', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); mocks.signPersonalMessage.mockRejectedValue(new Error('signing failed')); @@ -492,7 +657,7 @@ describe('MoneyAccountUpgradeController', () => { }); it('propagates CHOMP API errors', async () => { - const { controller, mocks } = setup(); + const { controller, mocks } = await setupInitialized(); mocks.associateAddress.mockRejectedValue( new Error("POST /v1/auth/address failed with status '500'"), diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index d87ba411de0..4242589d9ce 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -8,6 +8,7 @@ import type { ChompApiServiceAssociateAddressAction, ChompApiServiceCreateIntentsAction, ChompApiServiceCreateUpgradeAction, + ChompApiServiceGetServiceDetailsAction, ChompApiServiceGetUpgradeAction, ChompApiServiceVerifyDelegationAction, SignedDelegation, @@ -23,7 +24,7 @@ import type { Hex } from '@metamask/utils'; import { webcrypto } from 'node:crypto'; import type { MoneyAccountUpgradeControllerMethodActions } from './MoneyAccountUpgradeController-method-action-types'; -import type { AccountUpgradeEntry, UpgradeConfig } from './types'; +import type { AccountUpgradeEntry, InitConfig, UpgradeConfig } from './types'; export const controllerName = 'MoneyAccountUpgradeController'; @@ -72,6 +73,7 @@ type AllowedActions = | ChompApiServiceGetUpgradeAction | ChompApiServiceVerifyDelegationAction | ChompApiServiceCreateIntentsAction + | ChompApiServiceGetServiceDetailsAction | KeyringControllerSignPersonalMessageAction | KeyringControllerSignEip7702AuthorizationAction | DelegationControllerSignDelegationAction @@ -102,7 +104,11 @@ export class MoneyAccountUpgradeController extends BaseController< MoneyAccountUpgradeControllerState, MoneyAccountUpgradeControllerMessenger > { - readonly #config: UpgradeConfig; + #config: UpgradeConfig | undefined; + + #chainId: Hex | undefined; + + initialized: boolean; /** * Constructor for the MoneyAccountUpgradeController. @@ -110,16 +116,13 @@ export class MoneyAccountUpgradeController extends BaseController< * @param options - The options for constructing the controller. * @param options.messenger - The messenger to use for inter-controller communication. * @param options.state - The initial state of the controller. - * @param options.config - Contract addresses and configuration for the upgrade sequence. */ constructor({ messenger, state, - config, }: { messenger: MoneyAccountUpgradeControllerMessenger; state?: Partial; - config: UpgradeConfig; }) { super({ messenger, @@ -131,7 +134,7 @@ export class MoneyAccountUpgradeController extends BaseController< }, }); - this.#config = config; + this.initialized = false; this.messenger.registerMethodActionHandlers( this, @@ -139,6 +142,61 @@ export class MoneyAccountUpgradeController extends BaseController< ); } + /** + * Fetches service details and builds the upgrade config. + * + * @param chainId - The chain to initialize for. + * @param initConfig - Contract addresses not available from the service details API. + */ + async init(chainId: Hex, initConfig: InitConfig): Promise { + const response = await this.messenger.call( + 'ChompApiService:getServiceDetails', + [chainId], + ); + + const chain = response.chains[chainId]; + if (!chain) { + throw new Error(`Chain ${chainId} not found in service details response`); + } + + const { vedaProtocol } = chain.protocol; + if (!vedaProtocol) { + throw new Error( + `vedaProtocol not found for chain ${chainId} in service details response`, + ); + } + + const [firstToken] = vedaProtocol.supportedTokens; + if (!firstToken) { + throw new Error( + `No supported tokens found for vedaProtocol on chain ${chainId}`, + ); + } + + this.#config = { + delegateAddress: chain.autoDepositDelegate as Hex, + erc20TransferAmountEnforcer: firstToken.tokenAddress as Hex, + vedaVaultAdapterAddress: vedaProtocol.adapterAddress as Hex, + ...initConfig, + }; + this.#chainId = chainId; + this.initialized = true; + } + + /** + * Returns the upgrade config, throwing if the controller has not been initialized. + * + * @returns The upgrade config. + */ + #requireConfig(): UpgradeConfig { + if (!this.#config) { + throw new Error( + 'MoneyAccountUpgradeController is not initialized. Call init() first.', + ); + } + return this.#config; + } + /** * Runs the full upgrade sequence for a Money Account. Each step is * idempotent — if the step has already been completed, it is skipped. @@ -147,6 +205,16 @@ export class MoneyAccountUpgradeController extends BaseController< * @param chainId - The target chain for the upgrade. */ async upgradeAccount(address: Hex, chainId: Hex): Promise { + // Validates that init() has been called, throwing if not. + this.#requireConfig(); + + if (chainId !== this.#chainId) { + throw new Error( + `Chain ID mismatch: controller was initialized for ${ + this.#chainId + } but upgradeAccount was called with ${chainId}`, + ); + } await this.#associateAddress(address); await this.#submitAuthorization(address, chainId); await this.#verifyDelegation(address, chainId); @@ -203,11 +271,12 @@ export class MoneyAccountUpgradeController extends BaseController< const nonce = 0; const chainIdDecimal = parseInt(chainId, 16); + const config = this.#requireConfig(); const signature = await this.messenger.call( 'KeyringController:signEip7702Authorization', { chainId: chainIdDecimal, - contractAddress: this.#config.delegatorImplAddress, + contractAddress: config.delegatorImplAddress, nonce, from: address, }, @@ -248,26 +317,25 @@ export class MoneyAccountUpgradeController extends BaseController< .map((b) => b.toString(16).padStart(2, '0')) .join('')}`; + const config = this.#requireConfig(); + const delegation = { - delegate: this.#config.delegateAddress, + delegate: config.delegateAddress, delegator: address, authority: ROOT_AUTHORITY, caveats: [ { - enforcer: this.#config.erc20TransferAmountEnforcer, - terms: this.#encodeCaveatTerms( - MAX_UINT256, - this.#config.musdTokenAddress, - ), + enforcer: config.erc20TransferAmountEnforcer, + terms: this.#encodeCaveatTerms(MAX_UINT256, config.musdTokenAddress), args: '0x' as Hex, }, { - enforcer: this.#config.redeemerEnforcer, - terms: this.#encodeCaveatTerms(this.#config.vedaVaultAdapterAddress), + enforcer: config.redeemerEnforcer, + terms: this.#encodeCaveatTerms(config.vedaVaultAdapterAddress), args: '0x' as Hex, }, { - enforcer: this.#config.valueLteEnforcer, + enforcer: config.valueLteEnforcer, terms: '0x0000000000000000000000000000000000000000000000000000000000000000' as Hex, args: '0x' as Hex, @@ -334,6 +402,7 @@ export class MoneyAccountUpgradeController extends BaseController< ); } + const config = this.#requireConfig(); await this.messenger.call('ChompApiService:createIntents', [ { account: address, @@ -342,7 +411,7 @@ export class MoneyAccountUpgradeController extends BaseController< metadata: { allowance: MAX_UINT256, tokenSymbol: 'mUSD', - tokenAddress: this.#config.musdTokenAddress, + tokenAddress: config.musdTokenAddress, type: 'cash-deposit', }, }, @@ -353,7 +422,7 @@ export class MoneyAccountUpgradeController extends BaseController< metadata: { allowance: MAX_UINT256, tokenSymbol: 'mUSD', - tokenAddress: this.#config.musdTokenAddress, + tokenAddress: config.musdTokenAddress, type: 'cash-withdrawal', }, }, @@ -369,7 +438,7 @@ export class MoneyAccountUpgradeController extends BaseController< * @returns The concatenated hex string. */ #encodeCaveatTerms(...values: Hex[]): Hex { - return `0x${values.map((v) => v.slice(2).padStart(64, '0')).join('')}`; + return `0x${values.map((val) => val.slice(2).padStart(64, '0')).join('')}`; } /** diff --git a/packages/money-account-upgrade-controller/src/index.ts b/packages/money-account-upgrade-controller/src/index.ts index 7be107296d6..c33134ae32f 100644 --- a/packages/money-account-upgrade-controller/src/index.ts +++ b/packages/money-account-upgrade-controller/src/index.ts @@ -1,4 +1,9 @@ -export type { UpgradeConfig, UpgradeStep, AccountUpgradeEntry } from './types'; +export type { + InitConfig, + UpgradeConfig, + UpgradeStep, + AccountUpgradeEntry, +} from './types'; export { MoneyAccountUpgradeController, controllerName, diff --git a/packages/money-account-upgrade-controller/src/types.ts b/packages/money-account-upgrade-controller/src/types.ts index ed75e1890ad..82705946594 100644 --- a/packages/money-account-upgrade-controller/src/types.ts +++ b/packages/money-account-upgrade-controller/src/types.ts @@ -21,6 +21,18 @@ export type UpgradeConfig = { valueLteEnforcer: Hex; }; +/** + * Configuration values passed to {@link MoneyAccountUpgradeController.init} + * that cannot be derived from the service details API. + */ +export type InitConfig = Pick< + UpgradeConfig, + | 'delegatorImplAddress' + | 'musdTokenAddress' + | 'redeemerEnforcer' + | 'valueLteEnforcer' +>; + /** * The discrete steps of the upgrade sequence, in order. */ From df2d10b76d28456faba8a20f405c1b4e881fd669 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 16 Apr 2026 15:25:25 +0100 Subject: [PATCH 09/26] feat: add step-level resumability to upgrade controller Each step now checks persisted state via #isStepCompleted before running, so a retry after a mid-sequence failure resumes from the last incomplete step rather than re-running everything from scratch. Co-Authored-By: Claude Opus 4.6 --- .../money-account-upgrade-controller/TODO.md | 10 +- .../src/MoneyAccountUpgradeController.test.ts | 116 ++++++++++++++++++ .../src/MoneyAccountUpgradeController.ts | 53 +++++++- 3 files changed, 173 insertions(+), 6 deletions(-) diff --git a/packages/money-account-upgrade-controller/TODO.md b/packages/money-account-upgrade-controller/TODO.md index e86e1bbc404..232dc9b3a2b 100644 --- a/packages/money-account-upgrade-controller/TODO.md +++ b/packages/money-account-upgrade-controller/TODO.md @@ -2,21 +2,21 @@ ## Step 0: Associate address -- [ ] **Idempotency check**: `#associateAddress` currently always signs and submits. The CHOMP API returns 409 when already associated (which is handled as success), but we could skip the signing step entirely by checking state or querying CHOMP first. +- [x] **Idempotency check**: `#associateAddress` now checks persisted state and skips signing/submission if the step has already been completed. ## Step 1: Submit authorization -- [ ] **Fetch on-chain nonce**: The nonce is hardcoded to `0` (line 202). This needs to be replaced with an actual on-chain nonce fetch for the account — likely via an `eth_getTransactionCount` call or equivalent messenger action. CHOMP validates that the nonce matches. +- [ ] **Fetch on-chain nonce**: The nonce is hardcoded to `0` (line 271). This needs to be replaced with an actual on-chain nonce fetch for the account — likely via an `eth_getTransactionCount` call or equivalent messenger action. CHOMP validates that the nonce matches. ## Step 2: Verify delegation -- [ ] **Caveat term encoding**: `#encodeCaveatTerms` does naive left-padded hex concatenation. Verify this matches the exact ABI encoding expected by the caveat enforcer contracts (may need proper ABI encoding via `@metamask/abi-utils` or similar). +- [x] **Caveat term encoding**: `#encodeCaveatTerms` now uses proper 256-bit zero-padded ABI encoding (`padStart(64, '0')`). - [ ] **Use `@metamask/smart-accounts-kit`**: The description mentions using `createDelegation` from `@metamask/smart-accounts-kit` to build the delegation. This package is not yet in the repo — once it lands, the delegation construction in `#verifyDelegation` should use it instead of manual assembly. - [ ] **Import `ROOT_AUTHORITY` from `@metamask/delegation-controller`**: Currently defined locally as a constant. The delegation-controller has it but doesn't export it — once it's exported, import it instead. ## Step 3: Save delegation -- [ ] **Implement Authenticated User Storage save**: `#saveDelegation` is a stub (line 312-316). Needs the `@metamask/authenticated-user-storage` wrapper (PR currently open). Once available, save the signed delegation so CHOMP can read it at execution time via its internal VPC endpoint. +- [ ] **Implement Authenticated User Storage save**: `#saveDelegation` is a stub. Needs the `@metamask/authenticated-user-storage` wrapper (PR currently open). Once available, save the signed delegation so CHOMP can read it at execution time via its internal VPC endpoint. ## Step 4: Register intents @@ -24,6 +24,6 @@ ## General -- [ ] **Resumability**: `upgradeAccount` runs all steps sequentially. If it fails mid-way and is retried, steps 0 and 2-4 will re-run from scratch. Consider checking persisted state at the start of each step to skip already-completed work (step 1 already does this via `getUpgrade`). +- [x] **Resumability**: Every step now checks persisted state via `#isStepCompleted` and skips if already completed. Retrying after a mid-sequence failure resumes from the last incomplete step. - [ ] **`MoneyAccountController:getMoneyAccount`**: This action is declared in `AllowedActions` but not currently used. It was included anticipating the controller may need to look up account details. Remove if not needed, or use it to validate the address before starting. - [ ] **`#saveDelegation` unused `_chainId` parameter**: Will be needed once the stub is implemented — the storage call will likely need it. diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts index cd39c68411b..b9855f1b1dd 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -382,6 +382,104 @@ describe('MoneyAccountUpgradeController', () => { }); }); + it('skips all steps when upgrade is already complete', async () => { + const { controller, mocks } = await setupInitialized({ + state: { + upgrades: { + [MOCK_ADDRESS]: { + step: 'register-intents', + chainId: MOCK_CHAIN_ID, + delegationHash: MOCK_DELEGATION_HASH, + }, + }, + }, + }); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.signPersonalMessage).not.toHaveBeenCalled(); + expect(mocks.associateAddress).not.toHaveBeenCalled(); + expect(mocks.getUpgrade).not.toHaveBeenCalled(); + expect(mocks.signEip7702Authorization).not.toHaveBeenCalled(); + expect(mocks.signDelegation).not.toHaveBeenCalled(); + expect(mocks.verifyDelegation).not.toHaveBeenCalled(); + expect(mocks.createIntents).not.toHaveBeenCalled(); + }); + + it('resumes from submit-authorization, skipping steps 0-1', async () => { + const { controller, mocks } = await setupInitialized({ + state: { + upgrades: { + [MOCK_ADDRESS]: { + step: 'submit-authorization', + chainId: MOCK_CHAIN_ID, + }, + }, + }, + }); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.signPersonalMessage).not.toHaveBeenCalled(); + expect(mocks.associateAddress).not.toHaveBeenCalled(); + expect(mocks.getUpgrade).not.toHaveBeenCalled(); + expect(mocks.signEip7702Authorization).not.toHaveBeenCalled(); + // Steps 2-4 run + expect(mocks.signDelegation).toHaveBeenCalledTimes(1); + expect(mocks.verifyDelegation).toHaveBeenCalledTimes(1); + expect(mocks.createIntents).toHaveBeenCalledTimes(1); + }); + + it('resumes from verify-delegation, skipping steps 0-2', async () => { + const { controller, mocks } = await setupInitialized({ + state: { + upgrades: { + [MOCK_ADDRESS]: { + step: 'verify-delegation', + chainId: MOCK_CHAIN_ID, + delegationHash: MOCK_DELEGATION_HASH, + }, + }, + }, + }); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.signPersonalMessage).not.toHaveBeenCalled(); + expect(mocks.associateAddress).not.toHaveBeenCalled(); + expect(mocks.getUpgrade).not.toHaveBeenCalled(); + expect(mocks.signEip7702Authorization).not.toHaveBeenCalled(); + expect(mocks.signDelegation).not.toHaveBeenCalled(); + expect(mocks.verifyDelegation).not.toHaveBeenCalled(); + // Steps 3-4 run + expect(mocks.createIntents).toHaveBeenCalledTimes(1); + }); + + it('resumes from save-delegation, skipping steps 0-3', async () => { + const { controller, mocks } = await setupInitialized({ + state: { + upgrades: { + [MOCK_ADDRESS]: { + step: 'save-delegation', + chainId: MOCK_CHAIN_ID, + delegationHash: MOCK_DELEGATION_HASH, + }, + }, + }, + }); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.signPersonalMessage).not.toHaveBeenCalled(); + expect(mocks.associateAddress).not.toHaveBeenCalled(); + expect(mocks.getUpgrade).not.toHaveBeenCalled(); + expect(mocks.signEip7702Authorization).not.toHaveBeenCalled(); + expect(mocks.signDelegation).not.toHaveBeenCalled(); + expect(mocks.verifyDelegation).not.toHaveBeenCalled(); + // Only step 4 runs + expect(mocks.createIntents).toHaveBeenCalledTimes(1); + }); + it('is callable via the messenger', async () => { const { rootMessenger } = await setupInitialized(); @@ -417,6 +515,24 @@ describe('MoneyAccountUpgradeController', () => { ); }); + it('skips signing when address already has an upgrade entry', async () => { + const { controller, mocks } = await setupInitialized({ + state: { + upgrades: { + [MOCK_ADDRESS]: { + step: 'associate-address', + chainId: MOCK_CHAIN_ID, + }, + }, + }, + }); + + await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + + expect(mocks.signPersonalMessage).not.toHaveBeenCalled(); + expect(mocks.associateAddress).not.toHaveBeenCalled(); + }); + it('updates state to associate-address after completion', async () => { const { controller, mocks } = await setupInitialized(); diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index 4242589d9ce..f13ff484ec5 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -24,7 +24,12 @@ import type { Hex } from '@metamask/utils'; import { webcrypto } from 'node:crypto'; import type { MoneyAccountUpgradeControllerMethodActions } from './MoneyAccountUpgradeController-method-action-types'; -import type { AccountUpgradeEntry, InitConfig, UpgradeConfig } from './types'; +import type { + AccountUpgradeEntry, + InitConfig, + UpgradeConfig, + UpgradeStep, +} from './types'; export const controllerName = 'MoneyAccountUpgradeController'; @@ -32,6 +37,16 @@ export const controllerName = 'MoneyAccountUpgradeController'; const ROOT_AUTHORITY = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex; +// The ordered list of upgrade steps, used to determine whether a step +// has already been completed during a previous (possibly interrupted) run. +const STEP_ORDER: UpgradeStep[] = [ + 'associate-address', + 'submit-authorization', + 'verify-delegation', + 'save-delegation', + 'register-intents', +]; + // Maximum uint256 — used as the allowance for the ERC20TransferAmountEnforcer. const MAX_UINT256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex; @@ -222,6 +237,22 @@ export class MoneyAccountUpgradeController extends BaseController< await this.#registerIntents(address, chainId); } + /** + * Returns true if the persisted upgrade entry for the given address + * shows a step at or past the target step. + * + * @param address - The account address. + * @param step - The step to check. + * @returns Whether the step has already been completed. + */ + #isStepCompleted(address: Hex, step: UpgradeStep): boolean { + const entry = this.state.upgrades[address]; + if (!entry) { + return false; + } + return STEP_ORDER.indexOf(entry.step) >= STEP_ORDER.indexOf(step); + } + /** * Step 0: Associate the Money Account address with the user's CHOMP profile. * @@ -231,6 +262,10 @@ export class MoneyAccountUpgradeController extends BaseController< * @param address - The Money Account address. */ async #associateAddress(address: Hex): Promise { + if (this.#isStepCompleted(address, 'associate-address')) { + return; + } + const timestamp = Date.now().toString(); const message = `CHOMP Authentication ${timestamp}`; @@ -257,6 +292,10 @@ export class MoneyAccountUpgradeController extends BaseController< * @param chainId - The target chain. */ async #submitAuthorization(address: Hex, chainId: Hex): Promise { + if (this.#isStepCompleted(address, 'submit-authorization')) { + return; + } + const existing = await this.messenger.call( 'ChompApiService:getUpgrade', address, @@ -311,6 +350,10 @@ export class MoneyAccountUpgradeController extends BaseController< * @param chainId - The target chain. */ async #verifyDelegation(address: Hex, chainId: Hex): Promise { + if (this.#isStepCompleted(address, 'verify-delegation')) { + return; + } + const salt: Hex = `0x${Array.from( webcrypto.getRandomValues(new Uint8Array(32)), ) @@ -381,6 +424,10 @@ export class MoneyAccountUpgradeController extends BaseController< * @param _chainId - The target chain (unused in stub). */ async #saveDelegation(address: Hex, _chainId: Hex): Promise { + if (this.#isStepCompleted(address, 'save-delegation')) { + return; + } + // TODO: Save delegation to Authenticated User Storage once the // @metamask/authenticated-user-storage wrapper is available. this.#updateUpgrade(address, { step: 'save-delegation' }); @@ -393,6 +440,10 @@ export class MoneyAccountUpgradeController extends BaseController< * @param chainId - The target chain. */ async #registerIntents(address: Hex, chainId: Hex): Promise { + if (this.#isStepCompleted(address, 'register-intents')) { + return; + } + const entry = this.state.upgrades[address]; const delegationHash = entry?.delegationHash; From 5f1798ae7596e53c6bc2915222d4ce6d96863afc Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 17 Apr 2026 11:26:22 +0100 Subject: [PATCH 10/26] chore: update codeowners --- .github/CODEOWNERS | 6 +- yarn.lock | 692 +++------------------------------------------ 2 files changed, 44 insertions(+), 654 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 06069960fcf..fe3fd8d6775 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -41,7 +41,7 @@ /packages/earn-controller @MetaMask/earn /packages/money-account-balance-service @MetaMask/earn /packages/chomp-api-service @MetaMask/earn @MetaMask/delegation -/packages/money-account-upgrade-controller @MetaMask/earn +/packages/money-account-upgrade-controller @MetaMask/earn @MetaMask/delegation ## Social AI Team /packages/ai-controllers @MetaMask/social-ai @@ -233,5 +233,5 @@ /packages/money-account-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform /packages/chomp-api-service/package.json @MetaMask/earn @MetaMask/delegation @MetaMask/core-platform /packages/chomp-api-service/CHANGELOG.md @MetaMask/earn @MetaMask/delegation @MetaMask/core-platform -/packages/money-account-upgrade-controller/package.json @MetaMask/accounts-engineers @MetaMask/core-platform -/packages/money-account-upgrade-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform +/packages/money-account-upgrade-controller/package.json @MetaMask/earn @MetaMask/delegation @MetaMask/core-platform +/packages/money-account-upgrade-controller/CHANGELOG.md @MetaMask/earn @MetaMask/delegation @MetaMask/core-platform diff --git a/yarn.lock b/yarn.lock index 544973ff8f2..edad0aeca48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -154,18 +154,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": - version: 7.29.2 - resolution: "@babel/parser@npm:7.29.2" - dependencies: - "@babel/types": "npm:^7.29.0" - bin: - parser: ./bin/babel-parser.js - checksum: 10/45d050bf75aa5194b3255f156173e8553d615ff5a2434674cc4a10cdc7c261931befb8618c996a1c449b87f0ef32a3407879af2ac967d95dc7b4fdbae7037efa - languageName: node - linkType: hard - -"@babel/parser@npm:^7.24.7": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": version: 7.29.0 resolution: "@babel/parser@npm:7.29.0" dependencies: @@ -720,20 +709,13 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.39.1": +"@eslint/js@npm:9.39.1, @eslint/js@npm:^9.11.0": version: 9.39.1 resolution: "@eslint/js@npm:9.39.1" checksum: 10/b10b9b953212c0f3ffca475159bbe519e9e98847200c7432d1637d444fddcd7b712d2b7710a7dc20510f9cfbe8db330039b2aad09cb55d9545b116d940dbeed2 languageName: node linkType: hard -"@eslint/js@npm:^9.11.0": - version: 9.39.4 - resolution: "@eslint/js@npm:9.39.4" - checksum: 10/0a7ab4c4108cf2cadf66849ebd20f5957cc53052b88d8807d0b54e489dbf6ffcaf741e144e7f9b187c395499ce2e6ddc565dbfa4f60c6df455cf2b30bcbdc5a3 - languageName: node - linkType: hard - "@eslint/object-schema@npm:^2.1.7": version: 2.1.7 resolution: "@eslint/object-schema@npm:2.1.7" @@ -1748,13 +1730,6 @@ __metadata: languageName: node linkType: hard -"@gar/promise-retry@npm:^1.0.0": - version: 1.0.3 - resolution: "@gar/promise-retry@npm:1.0.3" - checksum: 10/0d13ea3bb1025755e055648f6e290d2a7e0c87affaf552218f09f66b3fcd9ea9d5c9cc5fe2aa6e285e1530437768e40f9448fe9a86f4f3417b216dcf488d3d1a - languageName: node - linkType: hard - "@grpc/grpc-js@npm:~1.9.0": version: 1.9.15 resolution: "@grpc/grpc-js@npm:1.9.15" @@ -2945,24 +2920,7 @@ __metadata: languageName: node linkType: hard -"@metamask/auto-changelog@npm:^6.0.0": - version: 6.0.0 - resolution: "@metamask/auto-changelog@npm:6.0.0" - dependencies: - "@octokit/rest": "npm:^20.0.0" - diff: "npm:^5.0.0" - execa: "npm:^5.1.1" - semver: "npm:^7.3.5" - yargs: "npm:^17.0.1" - peerDependencies: - prettier: ">=3.0.0" - bin: - auto-changelog: dist/cli.mjs - checksum: 10/870dde1f24d3bc3d34238d7cda8a326ff3adb5709939159cd7ac8885fa5f17343789e72711e7cc2c4333999c5ac085a027b53e98d9475bf92f4a918db6ceaf7a - languageName: node - linkType: hard - -"@metamask/auto-changelog@npm:^6.1.0": +"@metamask/auto-changelog@npm:^6.0.0, @metamask/auto-changelog@npm:^6.1.0": version: 6.1.0 resolution: "@metamask/auto-changelog@npm:6.1.0" dependencies: @@ -5749,26 +5707,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.10.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.1, @metamask/utils@npm:^11.9.0": - version: 11.10.0 - resolution: "@metamask/utils@npm:11.10.0" - dependencies: - "@ethereumjs/tx": "npm:^4.2.0" - "@metamask/superstruct": "npm:^3.1.0" - "@noble/hashes": "npm:^1.3.1" - "@scure/base": "npm:^1.1.3" - "@types/debug": "npm:^4.1.7" - "@types/lodash": "npm:^4.17.20" - debug: "npm:^4.3.4" - lodash: "npm:^4.17.21" - pony-cause: "npm:^2.1.10" - semver: "npm:^7.5.4" - uuid: "npm:^9.0.1" - checksum: 10/691a268af66593b60e9807a069127993cea3cdc941f99d5d7ca4664868754f08945821f1787b2f3e99e4497df63ceb0af37a2419ad494da29a1fddffe94f5797 - languageName: node - linkType: hard - -"@metamask/utils@npm:^11.11.0": +"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.10.0, @metamask/utils@npm:^11.11.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.1, @metamask/utils@npm:^11.9.0": version: 11.11.0 resolution: "@metamask/utils@npm:11.11.0" dependencies: @@ -5981,19 +5920,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/agent@npm:^4.0.0": - version: 4.0.0 - resolution: "@npmcli/agent@npm:4.0.0" - dependencies: - agent-base: "npm:^7.1.0" - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.1" - lru-cache: "npm:^11.2.1" - socks-proxy-agent: "npm:^8.0.3" - checksum: 10/1a81573becc60515031accc696e6405e9b894e65c12b98ef4aeee03b5617c41948633159dbf6caf5dde5b47367eeb749bdc7b7dfb21960930a9060a935c6f636 - languageName: node - linkType: hard - "@npmcli/fs@npm:^3.1.0": version: 3.1.1 resolution: "@npmcli/fs@npm:3.1.1" @@ -6003,15 +5929,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/fs@npm:^5.0.0": - version: 5.0.0 - resolution: "@npmcli/fs@npm:5.0.0" - dependencies: - semver: "npm:^7.3.5" - checksum: 10/4935c7719d17830d0f9fa46c50be17b2a3c945cec61760f6d0909bce47677c42e1810ca673305890f9e84f008ec4d8e841182f371e42100a8159d15f22249208 - languageName: node - linkType: hard - "@npmcli/git@npm:^5.0.0": version: 5.0.8 resolution: "@npmcli/git@npm:5.0.8" @@ -6060,13 +5977,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/redact@npm:^4.0.0": - version: 4.0.0 - resolution: "@npmcli/redact@npm:4.0.0" - checksum: 10/5d52df2b5267f4369c97a2b2f7c427e3d7aa4b6a83e7a1b522e196f6e9d50024c620bd0cb2052067c74d1aaa0c330d9bc04e1d335bfb46180e705bb33423e74c - languageName: node - linkType: hard - "@npmcli/run-script@npm:8.1.0": version: 8.1.0 resolution: "@npmcli/run-script@npm:8.1.0" @@ -6679,20 +6589,13 @@ __metadata: languageName: node linkType: hard -"@tanstack/query-core@npm:4.43.0": +"@tanstack/query-core@npm:4.43.0, @tanstack/query-core@npm:^4.43.0": version: 4.43.0 resolution: "@tanstack/query-core@npm:4.43.0" checksum: 10/c2a5a151c7adaea8311e01a643255f31946ae3164a71567ba80048242821ae14043f13f5516b695baebe5ea7e4b2cf717fd60908a929d18a5c5125fee925ff67 languageName: node linkType: hard -"@tanstack/query-core@npm:^4.43.0": - version: 4.44.0 - resolution: "@tanstack/query-core@npm:4.44.0" - checksum: 10/f7f5c69cb2d44b58e1a9bfaa304a82724bb49f0ff63fd3abdfe377f15825ad910e1d31dd529d94e0650ff17229930eef2f54e281dbe99fa6cbd6a85e7c27bb9d - languageName: node - linkType: hard - "@tanstack/query-core@npm:^5.62.16": version: 5.90.20 resolution: "@tanstack/query-core@npm:5.90.20" @@ -7057,16 +6960,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": - version: 25.6.0 - resolution: "@types/node@npm:25.6.0" - dependencies: - undici-types: "npm:~7.19.0" - checksum: 10/99b18690a4be55904cbf8f6a6ac8eed5ec5b8d791fdd8ee2ae598b46c0fa9b83cda7b70dd7f00dbfb18189dcfc67648fdc7fdd3fcced2619a5a6453d9aec107d - languageName: node - linkType: hard - -"@types/node@npm:22.7.5": +"@types/node@npm:*, @types/node@npm:22.7.5, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": version: 22.7.5 resolution: "@types/node@npm:22.7.5" dependencies: @@ -7216,7 +7110,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.54.0": +"@typescript-eslint/eslint-plugin@npm:8.54.0, @typescript-eslint/eslint-plugin@npm:^8.48.0": version: 8.54.0 resolution: "@typescript-eslint/eslint-plugin@npm:8.54.0" dependencies: @@ -7236,27 +7130,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^8.48.0": - version: 8.58.1 - resolution: "@typescript-eslint/eslint-plugin@npm:8.58.1" - dependencies: - "@eslint-community/regexpp": "npm:^4.12.2" - "@typescript-eslint/scope-manager": "npm:8.58.1" - "@typescript-eslint/type-utils": "npm:8.58.1" - "@typescript-eslint/utils": "npm:8.58.1" - "@typescript-eslint/visitor-keys": "npm:8.58.1" - ignore: "npm:^7.0.5" - natural-compare: "npm:^1.4.0" - ts-api-utils: "npm:^2.5.0" - peerDependencies: - "@typescript-eslint/parser": ^8.58.1 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.1.0" - checksum: 10/0fcbe6faadb77313aa91c895c977a24fc72a79eed62f46f7b2d5804db52a9af99351b33b9c4d73fdabb0f69772d5d4a9acdef249a0d1526a44d3817fb51419b5 - languageName: node - linkType: hard - -"@typescript-eslint/parser@npm:8.54.0": +"@typescript-eslint/parser@npm:8.54.0, @typescript-eslint/parser@npm:^8.48.0": version: 8.54.0 resolution: "@typescript-eslint/parser@npm:8.54.0" dependencies: @@ -7272,22 +7146,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^8.48.0": - version: 8.58.1 - resolution: "@typescript-eslint/parser@npm:8.58.1" - dependencies: - "@typescript-eslint/scope-manager": "npm:8.58.1" - "@typescript-eslint/types": "npm:8.58.1" - "@typescript-eslint/typescript-estree": "npm:8.58.1" - "@typescript-eslint/visitor-keys": "npm:8.58.1" - debug: "npm:^4.4.3" - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.1.0" - checksum: 10/062584d26609e82169459ebf0c59f4925ba6596f4ea1637a320c34a25c34117585c458b9c6c268f5eeaee1988f4c7257d34d4bd05a214a88de12110e71b48493 - languageName: node - linkType: hard - "@typescript-eslint/project-service@npm:8.54.0": version: 8.54.0 resolution: "@typescript-eslint/project-service@npm:8.54.0" @@ -7301,19 +7159,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.58.1": - version: 8.58.1 - resolution: "@typescript-eslint/project-service@npm:8.58.1" - dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.58.1" - "@typescript-eslint/types": "npm:^8.58.1" - debug: "npm:^4.4.3" - peerDependencies: - typescript: ">=4.8.4 <6.1.0" - checksum: 10/2f3136268fc262e77e8c8c14291e60c54e0228b63ccb022826b6def6d80b83ce9c3a92fef11c888889fb204343c845556868c49495c3aa0a115e9a861dd5fe99 - languageName: node - linkType: hard - "@typescript-eslint/scope-manager@npm:8.54.0, @typescript-eslint/scope-manager@npm:^8.1.0": version: 8.54.0 resolution: "@typescript-eslint/scope-manager@npm:8.54.0" @@ -7324,16 +7169,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.58.1": - version: 8.58.1 - resolution: "@typescript-eslint/scope-manager@npm:8.58.1" - dependencies: - "@typescript-eslint/types": "npm:8.58.1" - "@typescript-eslint/visitor-keys": "npm:8.58.1" - checksum: 10/dc070fd73847807e32cb7dfc37512abd0b1a485b0037d8cfb6c593555a5b673d3ee9d19c61504ea71d067ad610c66f64d70d56f3a5db51895c0a25e45621cd08 - languageName: node - linkType: hard - "@typescript-eslint/tsconfig-utils@npm:8.54.0, @typescript-eslint/tsconfig-utils@npm:^8.54.0": version: 8.54.0 resolution: "@typescript-eslint/tsconfig-utils@npm:8.54.0" @@ -7343,15 +7178,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.58.1, @typescript-eslint/tsconfig-utils@npm:^8.58.1": - version: 8.58.1 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.1" - peerDependencies: - typescript: ">=4.8.4 <6.1.0" - checksum: 10/4a5cf9a5eb834d05f2d37f7d80319575cf4a75aa52807b96edc0db24349ba417b41cb6f5257ffb07b8b9b4c59c7438637e8c75ed7c2b513bcb07e259b49e058e - languageName: node - linkType: hard - "@typescript-eslint/type-utils@npm:8.54.0": version: 8.54.0 resolution: "@typescript-eslint/type-utils@npm:8.54.0" @@ -7368,22 +7194,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.58.1": - version: 8.58.1 - resolution: "@typescript-eslint/type-utils@npm:8.58.1" - dependencies: - "@typescript-eslint/types": "npm:8.58.1" - "@typescript-eslint/typescript-estree": "npm:8.58.1" - "@typescript-eslint/utils": "npm:8.58.1" - debug: "npm:^4.4.3" - ts-api-utils: "npm:^2.5.0" - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.1.0" - checksum: 10/39d62d6711590e817cf9a36257c19ea18e201ceca42b900350e121ea8986c167fbdd9da385ced29c61e38a1b5c76b6c320d59e21d4dd7f32767520e31aef4654 - languageName: node - linkType: hard - "@typescript-eslint/types@npm:8.54.0, @typescript-eslint/types@npm:^8.54.0": version: 8.54.0 resolution: "@typescript-eslint/types@npm:8.54.0" @@ -7391,13 +7201,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.58.1, @typescript-eslint/types@npm:^8.58.1": - version: 8.58.1 - resolution: "@typescript-eslint/types@npm:8.58.1" - checksum: 10/447e1351af8a47297096f063b327c69b1c986af89e39cb39e142bb35d7bec2ce8f34f31edcf62d1beb2e09a38e2029b12b50b335dae4e7c9ff49bd82f9127523 - languageName: node - linkType: hard - "@typescript-eslint/typescript-estree@npm:8.54.0": version: 8.54.0 resolution: "@typescript-eslint/typescript-estree@npm:8.54.0" @@ -7417,26 +7220,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.58.1": - version: 8.58.1 - resolution: "@typescript-eslint/typescript-estree@npm:8.58.1" - dependencies: - "@typescript-eslint/project-service": "npm:8.58.1" - "@typescript-eslint/tsconfig-utils": "npm:8.58.1" - "@typescript-eslint/types": "npm:8.58.1" - "@typescript-eslint/visitor-keys": "npm:8.58.1" - debug: "npm:^4.4.3" - minimatch: "npm:^10.2.2" - semver: "npm:^7.7.3" - tinyglobby: "npm:^0.2.15" - ts-api-utils: "npm:^2.5.0" - peerDependencies: - typescript: ">=4.8.4 <6.1.0" - checksum: 10/107510b484148a8a9a5874f5451b9a6649609607ee5e67de36cded786157987a5262b145398b1bd1935afab66134532369a4d6abb53c6f5b7744e3ace0b13f07 - languageName: node - linkType: hard - -"@typescript-eslint/utils@npm:8.54.0, @typescript-eslint/utils@npm:^8.1.0": +"@typescript-eslint/utils@npm:8.54.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.1.0": version: 8.54.0 resolution: "@typescript-eslint/utils@npm:8.54.0" dependencies: @@ -7451,21 +7235,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.58.1, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0": - version: 8.58.1 - resolution: "@typescript-eslint/utils@npm:8.58.1" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.9.1" - "@typescript-eslint/scope-manager": "npm:8.58.1" - "@typescript-eslint/types": "npm:8.58.1" - "@typescript-eslint/typescript-estree": "npm:8.58.1" - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.1.0" - checksum: 10/c51a5e116d1a09d0eb701c5884d5b9b8c22f79c427cb4c46357e4bcb7dfdfd9beba92e5d518572f42111b7335541a4ccefe3c05595fc3d666c1b62ddd1522e54 - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:8.54.0": version: 8.54.0 resolution: "@typescript-eslint/visitor-keys@npm:8.54.0" @@ -7476,16 +7245,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.58.1": - version: 8.58.1 - resolution: "@typescript-eslint/visitor-keys@npm:8.58.1" - dependencies: - "@typescript-eslint/types": "npm:8.58.1" - eslint-visitor-keys: "npm:^5.0.0" - checksum: 10/e9f34741da6fc0cb8e9eb67828ea4427ac2004a33ce8d1e1e9ba038471f9ed68405eca871651bb2efa793a467bc5233a4310c5571ad1497cb2a84a600e1733a8 - languageName: node - linkType: hard - "@vercel/stega@npm:^0.1.2": version: 0.1.2 resolution: "@vercel/stega@npm:0.1.2" @@ -7580,13 +7339,6 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:^4.0.0": - version: 4.0.0 - resolution: "abbrev@npm:4.0.0" - checksum: 10/e2f0c6a6708ad738b3e8f50233f4800de31ad41a6cdc50e0cbe51b76fed69fd0213516d92c15ce1a9985fca71a14606a9be22bf00f8475a58987b9bfb671c582 - languageName: node - linkType: hard - "abitype@npm:1.2.3, abitype@npm:^1.2.3": version: 1.2.3 resolution: "abitype@npm:1.2.3" @@ -7688,7 +7440,7 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^7.0.2, agent-base@npm:^7.1.1": +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": version: 7.1.1 resolution: "agent-base@npm:7.1.1" dependencies: @@ -7697,13 +7449,6 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^7.1.0": - version: 7.1.4 - resolution: "agent-base@npm:7.1.4" - checksum: 10/79bef167247789f955aaba113bae74bf64aa1e1acca4b1d6bb444bdf91d82c3e07e9451ef6a6e2e35e8f71a6f97ce33e3d855a5328eb9fad1bc3cc4cfd031ed8 - languageName: node - linkType: hard - "aggregate-error@npm:^3.0.0": version: 3.1.0 resolution: "aggregate-error@npm:3.1.0" @@ -7868,20 +7613,6 @@ __metadata: languageName: node linkType: hard -"async-function@npm:^1.0.0": - version: 1.0.0 - resolution: "async-function@npm:1.0.0" - checksum: 10/1a09379937d846f0ce7614e75071c12826945d4e417db634156bf0e4673c495989302f52186dfa9767a1d9181794554717badd193ca2bbab046ef1da741d8efd - languageName: node - linkType: hard - -"async-generator-function@npm:^1.0.0": - version: 1.0.0 - resolution: "async-generator-function@npm:1.0.0" - checksum: 10/3d49e7acbeee9e84537f4cb0e0f91893df8eba976759875ae8ee9e3d3c82f6ecdebdb347c2fad9926b92596d93cdfc78ecc988bcdf407e40433e8e8e6fe5d78e - languageName: node - linkType: hard - "async-mutex@npm:^0.3.1": version: 0.3.2 resolution: "async-mutex@npm:0.3.2" @@ -8021,13 +7752,6 @@ __metadata: languageName: node linkType: hard -"balanced-match@npm:^4.0.2": - version: 4.0.4 - resolution: "balanced-match@npm:4.0.4" - checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 - languageName: node - linkType: hard - "bare-events@npm:^2.2.0": version: 2.4.2 resolution: "bare-events@npm:2.4.2" @@ -8190,15 +7914,6 @@ __metadata: languageName: node linkType: hard -"brace-expansion@npm:^5.0.5": - version: 5.0.5 - resolution: "brace-expansion@npm:5.0.5" - dependencies: - balanced-match: "npm:^4.0.2" - checksum: 10/f259b2ddf04489da9512ad637ba6b4ef2d77abd4445d20f7f1714585f153435200a53fa6a2e4a5ee974df14ddad4cd16421f6f803e96e8b452bd48598878d0ee - languageName: node - linkType: hard - "braces@npm:^3.0.3": version: 3.0.3 resolution: "braces@npm:3.0.3" @@ -8341,24 +8056,6 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^20.0.1": - version: 20.0.4 - resolution: "cacache@npm:20.0.4" - dependencies: - "@npmcli/fs": "npm:^5.0.0" - fs-minipass: "npm:^3.0.0" - glob: "npm:^13.0.0" - lru-cache: "npm:^11.1.0" - minipass: "npm:^7.0.3" - minipass-collect: "npm:^2.0.1" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - p-map: "npm:^7.0.2" - ssri: "npm:^13.0.0" - checksum: 10/02c1b4c57dc2473e6f4654220c9405b73ae5fcdb392f82a7cf535468a52b842690cdb3694861d13bbe4dc067d5f8abe9697b4f791ae5b65cd73d62abad1e3e54 - languageName: node - linkType: hard - "call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": version: 1.0.2 resolution: "call-bind-apply-helpers@npm:1.0.2" @@ -8381,16 +8078,6 @@ __metadata: languageName: node linkType: hard -"call-bound@npm:^1.0.2": - version: 1.0.4 - resolution: "call-bound@npm:1.0.4" - dependencies: - call-bind-apply-helpers: "npm:^1.0.2" - get-intrinsic: "npm:^1.3.0" - checksum: 10/ef2b96e126ec0e58a7ff694db43f4d0d44f80e641370c21549ed911fecbdbc2df3ebc9bddad918d6bbdefeafb60bb3337902006d5176d72bcd2da74820991af7 - languageName: node - linkType: hard - "callsite@npm:^1.0.0": version: 1.0.0 resolution: "callsite@npm:1.0.0" @@ -9248,7 +8935,7 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.15.0": +"enhanced-resolve@npm:^5.15.0, enhanced-resolve@npm:^5.17.1": version: 5.18.0 resolution: "enhanced-resolve@npm:5.18.0" dependencies: @@ -9258,16 +8945,6 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.17.1": - version: 5.20.1 - resolution: "enhanced-resolve@npm:5.20.1" - dependencies: - graceful-fs: "npm:^4.2.4" - tapable: "npm:^2.3.0" - checksum: 10/588afc56de97334e5742faebcf8177a504da08ea817d399f9901f35d8e9e5e6fa86b4c2ce95a99081f947764e09c9991cc0fc0ba5751bae455c329643a709187 - languageName: node - linkType: hard - "entities@npm:^4.5.0": version: 4.5.0 resolution: "entities@npm:4.5.0" @@ -9699,13 +9376,6 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^5.0.0": - version: 5.0.1 - resolution: "eslint-visitor-keys@npm:5.0.1" - checksum: 10/f9cc1a57b75e0ef949545cac33d01e8367e302de4c1483266ed4d8646ee5c306376660196bbb38b004e767b7043d1e661cb4336b49eff634a1bbe75c1db709ec - languageName: node - linkType: hard - "eslint@npm:^9.39.1": version: 9.39.1 resolution: "eslint@npm:9.39.1" @@ -10517,13 +10187,6 @@ __metadata: languageName: node linkType: hard -"generator-function@npm:^2.0.0": - version: 2.0.1 - resolution: "generator-function@npm:2.0.1" - checksum: 10/eb7e7eb896c5433f3d40982b2ccacdb3dd990dd3499f14040e002b5d54572476513be8a2e6f9609f6e41ab29f2c4469307611ddbfc37ff4e46b765c326663805 - languageName: node - linkType: hard - "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -10538,24 +10201,21 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.3.0": - version: 1.3.1 - resolution: "get-intrinsic@npm:1.3.1" +"get-intrinsic@npm:^1.2.4": + version: 1.3.0 + resolution: "get-intrinsic@npm:1.3.0" dependencies: - async-function: "npm:^1.0.0" - async-generator-function: "npm:^1.0.0" call-bind-apply-helpers: "npm:^1.0.2" es-define-property: "npm:^1.0.1" es-errors: "npm:^1.3.0" es-object-atoms: "npm:^1.1.1" function-bind: "npm:^1.1.2" - generator-function: "npm:^2.0.0" get-proto: "npm:^1.0.1" gopd: "npm:^1.2.0" has-symbols: "npm:^1.1.0" hasown: "npm:^2.0.2" math-intrinsics: "npm:^1.1.0" - checksum: 10/bb579dda84caa4a3a41611bdd483dade7f00f246f2a7992eb143c5861155290df3fdb48a8406efa3dfb0b434e2c8fafa4eebd469e409d0439247f85fc3fa2cc1 + checksum: 10/6e9dd920ff054147b6f44cb98104330e87caafae051b6d37b13384a45ba15e71af33c3baeac7cb630a0aaa23142718dcf25b45cfdd86c184c5dcb4e56d953a10 languageName: node linkType: hard @@ -10624,7 +10284,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10": +"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -10640,33 +10300,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.3.7": - version: 10.5.0 - resolution: "glob@npm:10.5.0" - dependencies: - foreground-child: "npm:^3.1.0" - jackspeak: "npm:^3.1.2" - minimatch: "npm:^9.0.4" - minipass: "npm:^7.1.2" - package-json-from-dist: "npm:^1.0.0" - path-scurry: "npm:^1.11.1" - bin: - glob: dist/esm/bin.mjs - checksum: 10/ab3bccfefcc0afaedbd1f480cd0c4a2c0e322eb3f0aa7ceaa31b3f00b825069f17cf0f1fc8b6f256795074b903f37c0ade37ddda6a176aa57f1c2bbfe7240653 - languageName: node - linkType: hard - -"glob@npm:^13.0.0": - version: 13.0.6 - resolution: "glob@npm:13.0.6" - dependencies: - minimatch: "npm:^10.2.2" - minipass: "npm:^7.1.3" - path-scurry: "npm:^2.0.2" - checksum: 10/201ad69e5f0aa74e1d8c00a481581f8b8c804b6a4fbfabeeb8541f5d756932800331daeba99b58fb9e4cd67e12ba5a7eba5b82fb476691588418060b84353214 - languageName: node - linkType: hard - "glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.7": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -11343,13 +10976,6 @@ __metadata: languageName: node linkType: hard -"isexe@npm:^4.0.0": - version: 4.0.0 - resolution: "isexe@npm:4.0.0" - checksum: 10/2ead327ef596042ef9c9ec5f236b316acfaedb87f4bb61b3c3d574fb2e9c8a04b67305e04733bde52c24d9622fdebd3270aadb632adfbf9cadef88fe30f479e5 - languageName: node - linkType: hard - "isomorphic-fetch@npm:^3.0.0": version: 3.0.0 resolution: "isomorphic-fetch@npm:3.0.0" @@ -12331,13 +11957,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": - version: 11.3.3 - resolution: "lru-cache@npm:11.3.3" - checksum: 10/d45c1992232d0ab6ee5a9b93f62f43205ba5eeca51b2cfe6fa40a6aeb91b3cb0a318b273ab29ab36b66f779d4111df514c01bb7613ca4d4028953de49ecb82a9 - languageName: node - linkType: hard - "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -12415,26 +12034,6 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^15.0.0": - version: 15.0.5 - resolution: "make-fetch-happen@npm:15.0.5" - dependencies: - "@gar/promise-retry": "npm:^1.0.0" - "@npmcli/agent": "npm:^4.0.0" - "@npmcli/redact": "npm:^4.0.0" - cacache: "npm:^20.0.1" - http-cache-semantics: "npm:^4.1.1" - minipass: "npm:^7.0.2" - minipass-fetch: "npm:^5.0.0" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^1.0.0" - proc-log: "npm:^6.0.0" - ssri: "npm:^13.0.0" - checksum: 10/d2649effb06c00cb2b266057cb1c8c1e99cfc8d1378e7d9c26cc8f00be41bc63d59b77a5576ed28f8105acc57fb16220b64217f8d3a6a066a594c004aa163afa - languageName: node - linkType: hard - "makeerror@npm:1.0.12": version: 1.0.12 resolution: "makeerror@npm:1.0.12" @@ -12646,15 +12245,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.2.2": - version: 10.2.5 - resolution: "minimatch@npm:10.2.5" - dependencies: - brace-expansion: "npm:^5.0.5" - checksum: 10/19e87a931aff60ee7b9d80f39f817b8bfc54f61f8356ee3549fbf636dbccacacfec8d803eac73293955c4527cd085247dfc064bce4a5e349f8f3b85e2bf5da0f - languageName: node - linkType: hard - "minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -12724,21 +12314,6 @@ __metadata: languageName: node linkType: hard -"minipass-fetch@npm:^5.0.0": - version: 5.0.2 - resolution: "minipass-fetch@npm:5.0.2" - dependencies: - iconv-lite: "npm:^0.7.2" - minipass: "npm:^7.0.3" - minipass-sized: "npm:^2.0.0" - minizlib: "npm:^3.0.1" - dependenciesMeta: - iconv-lite: - optional: true - checksum: 10/4f3f65ea5b20a3a287765ebf21cc73e62031f754944272df2a3039296cc75a8fc2dc50b8a3c4f39ce3ac6e5cc583e8dc664d12c6ab98e0883d263e49f344bc86 - languageName: node - linkType: hard - "minipass-flush@npm:^1.0.5": version: 1.0.5 resolution: "minipass-flush@npm:1.0.5" @@ -12766,15 +12341,6 @@ __metadata: languageName: node linkType: hard -"minipass-sized@npm:^2.0.0": - version: 2.0.0 - resolution: "minipass-sized@npm:2.0.0" - dependencies: - minipass: "npm:^7.1.2" - checksum: 10/3b89adf64ca705662f77481e278eff5ec0a57aeffb5feba7cc8843722b1e7770efc880f2a17d1d4877b2d7bf227873cd46afb4da44c0fd18088b601ea50f96bb - languageName: node - linkType: hard - "minipass@npm:^3.0.0": version: 3.3.6 resolution: "minipass@npm:3.3.6" @@ -12791,10 +12357,10 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": - version: 7.1.3 - resolution: "minipass@npm:7.1.3" - checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6 +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": + version: 7.1.2 + resolution: "minipass@npm:7.1.2" + checksum: 10/c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950 languageName: node linkType: hard @@ -12808,12 +12374,12 @@ __metadata: languageName: node linkType: hard -"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": - version: 3.1.0 - resolution: "minizlib@npm:3.1.0" +"minizlib@npm:^3.0.1": + version: 3.0.2 + resolution: "minizlib@npm:3.0.2" dependencies: minipass: "npm:^7.1.2" - checksum: 10/f47365cc2cb7f078cbe7e046eb52655e2e7e97f8c0a9a674f4da60d94fb0624edfcec9b5db32e8ba5a99a5f036f595680ae6fe02a262beaa73026e505cc52f99 + checksum: 10/c075bed1594f68dcc8c35122333520112daefd4d070e5d0a228bd4cf5580e9eed3981b96c0ae1d62488e204e80fd27b2b9d0068ca9a5ef3993e9565faf63ca41 languageName: node linkType: hard @@ -12833,6 +12399,15 @@ __metadata: languageName: node linkType: hard +"mkdirp@npm:^3.0.1": + version: 3.0.1 + resolution: "mkdirp@npm:3.0.1" + bin: + mkdirp: dist/cjs/src/bin.js + checksum: 10/16fd79c28645759505914561e249b9a1f5fe3362279ad95487a4501e4467abeb714fd35b95307326b8fd03f3c7719065ef11a6f97b7285d7888306d1bd2232ba + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -12904,13 +12479,6 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:^1.0.0": - version: 1.0.0 - resolution: "negotiator@npm:1.0.0" - checksum: 10/b5734e87295324fabf868e36fb97c84b7d7f3156ec5f4ee5bf6e488079c11054f818290fc33804cef7b1ee21f55eeb14caea83e7dafae6492a409b3e573153e5 - languageName: node - linkType: hard - "neo-async@npm:^2.6.2": version: 2.6.2 resolution: "neo-async@npm:2.6.2" @@ -12972,7 +12540,7 @@ __metadata: languageName: node linkType: hard -"node-gyp@npm:^10.0.0": +"node-gyp@npm:^10.0.0, node-gyp@npm:latest": version: 10.2.0 resolution: "node-gyp@npm:10.2.0" dependencies: @@ -12992,26 +12560,6 @@ __metadata: languageName: node linkType: hard -"node-gyp@npm:latest": - version: 12.2.0 - resolution: "node-gyp@npm:12.2.0" - dependencies: - env-paths: "npm:^2.2.0" - exponential-backoff: "npm:^3.1.1" - graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^15.0.0" - nopt: "npm:^9.0.0" - proc-log: "npm:^6.0.0" - semver: "npm:^7.3.5" - tar: "npm:^7.5.4" - tinyglobby: "npm:^0.2.12" - which: "npm:^6.0.0" - bin: - node-gyp: bin/node-gyp.js - checksum: 10/4ebab5b77585a637315e969c2274b5520562473fe75de850639a580c2599652fb9f33959ec782ea45a2e149d8f04b548030f472eeeb3dbdf19a7f2ccbc30b908 - languageName: node - linkType: hard - "node-int64@npm:^0.4.0": version: 0.4.0 resolution: "node-int64@npm:0.4.0" @@ -13037,17 +12585,6 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^9.0.0": - version: 9.0.0 - resolution: "nopt@npm:9.0.0" - dependencies: - abbrev: "npm:^4.0.0" - bin: - nopt: bin/nopt.js - checksum: 10/56a1ccd2ad711fb5115918e2c96828703cddbe12ba2c3bd00591758f6fa30e6f47dd905c59dbfcf9b773f3a293b45996609fb6789ae29d6bfcc3cf3a6f7d9fda - languageName: node - linkType: hard - "normalize-package-data@npm:^2.5.0": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" @@ -13172,13 +12709,6 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.13.3, object-inspect@npm:^1.13.4": - version: 1.13.4 - resolution: "object-inspect@npm:1.13.4" - checksum: 10/aa13b1190ad3e366f6c83ad8a16ed37a19ed57d267385aa4bfdccda833d7b90465c057ff6c55d035a6b2e52c1a2295582b294217a0a3a1ae7abdd6877ef781fb - languageName: node - linkType: hard - "on-finished@npm:2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -13376,13 +12906,6 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^7.0.2": - version: 7.0.4 - resolution: "p-map@npm:7.0.4" - checksum: 10/ef48c3b2e488f31c693c9fcc0df0ef76518cf6426a495cf9486ebbb0fd7f31aef7f90e96f72e0070c0ff6e3177c9318f644b512e2c29e3feee8d7153fcb6782e - languageName: node - linkType: hard - "p-throttle@npm:^4.1.1": version: 4.1.1 resolution: "p-throttle@npm:4.1.1" @@ -13510,16 +13033,6 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^2.0.2": - version: 2.0.2 - resolution: "path-scurry@npm:2.0.2" - dependencies: - lru-cache: "npm:^11.0.0" - minipass: "npm:^7.1.2" - checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3 - languageName: node - linkType: hard - "path-to-regexp@npm:0.1.12": version: 0.1.12 resolution: "path-to-regexp@npm:0.1.12" @@ -13568,13 +13081,6 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^4.0.4": - version: 4.0.4 - resolution: "picomatch@npm:4.0.4" - checksum: 10/f6ef80a3590827ce20378ae110ac78209cc4f74d39236370f1780f957b7ee41c12acde0e4651b90f39983506fd2f5e449994716f516db2e9752924aff8de93ce - languageName: node - linkType: hard - "pify@npm:^5.0.0": version: 5.0.0 resolution: "pify@npm:5.0.0" @@ -13686,13 +13192,6 @@ __metadata: languageName: node linkType: hard -"proc-log@npm:^6.0.0": - version: 6.1.0 - resolution: "proc-log@npm:6.1.0" - checksum: 10/9033f30f168ed5a0991b773d0c50ff88384c4738e9a0a67d341de36bf7293771eed648ab6a0562f62276da12fde91f3bbfc75ffff6e71ad49aafd74fc646be66 - languageName: node - linkType: hard - "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -13816,7 +13315,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.13.0": +"qs@npm:6.13.0, qs@npm:^6.11.2": version: 6.13.0 resolution: "qs@npm:6.13.0" dependencies: @@ -13825,15 +13324,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.11.2": - version: 6.15.1 - resolution: "qs@npm:6.15.1" - dependencies: - side-channel: "npm:^1.1.0" - checksum: 10/ec10b9957446b3f4a38000940f6374720b4e2985209b89df197066038c951472ea24cd98d6bc6df73a0cbec75bc056f638032e3fb447345017ff7e0f0a2693ac - languageName: node - linkType: hard - "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -14450,41 +13940,6 @@ __metadata: languageName: node linkType: hard -"side-channel-list@npm:^1.0.0": - version: 1.0.1 - resolution: "side-channel-list@npm:1.0.1" - dependencies: - es-errors: "npm:^1.3.0" - object-inspect: "npm:^1.13.4" - checksum: 10/3499671cd52adaee739eac1e14d07530b8e3530192741aeb05e7fe4ad1b51d1368ceea2cd3c21b0f62b05410a5c70a7c4d997ba4b143303ef73d0c65dfd1c252 - languageName: node - linkType: hard - -"side-channel-map@npm:^1.0.1": - version: 1.0.1 - resolution: "side-channel-map@npm:1.0.1" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.5" - object-inspect: "npm:^1.13.3" - checksum: 10/5771861f77feefe44f6195ed077a9e4f389acc188f895f570d56445e251b861754b547ea9ef73ecee4e01fdada6568bfe9020d2ec2dfc5571e9fa1bbc4a10615 - languageName: node - linkType: hard - -"side-channel-weakmap@npm:^1.0.2": - version: 1.0.2 - resolution: "side-channel-weakmap@npm:1.0.2" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.5" - object-inspect: "npm:^1.13.3" - side-channel-map: "npm:^1.0.1" - checksum: 10/a815c89bc78c5723c714ea1a77c938377ea710af20d4fb886d362b0d1f8ac73a17816a5f6640f354017d7e292a43da9c5e876c22145bac00b76cfb3468001736 - languageName: node - linkType: hard - "side-channel@npm:^1.0.6": version: 1.0.6 resolution: "side-channel@npm:1.0.6" @@ -14497,19 +13952,6 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.1.0": - version: 1.1.0 - resolution: "side-channel@npm:1.1.0" - dependencies: - es-errors: "npm:^1.3.0" - object-inspect: "npm:^1.13.3" - side-channel-list: "npm:^1.0.0" - side-channel-map: "npm:^1.0.1" - side-channel-weakmap: "npm:^1.0.2" - checksum: 10/7d53b9db292c6262f326b6ff3bc1611db84ece36c2c7dc0e937954c13c73185b0406c56589e2bb8d071d6fee468e14c39fb5d203ee39be66b7b8174f179afaba - languageName: node - linkType: hard - "signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -14694,15 +14136,6 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^13.0.0": - version: 13.0.1 - resolution: "ssri@npm:13.0.1" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10/ae560d0378d074006a71b06af71bfbe84a3fe1ac6e16c1f07575f69e670d40170507fe52b21bcc23399429bc6a15f4bc3ea8d9bc88e9dfd7e87de564e6da6a72 - languageName: node - linkType: hard - "stable-hash@npm:^0.0.4": version: 0.0.4 resolution: "stable-hash@npm:0.0.4" @@ -14921,13 +14354,6 @@ __metadata: languageName: node linkType: hard -"tapable@npm:^2.3.0": - version: 2.3.2 - resolution: "tapable@npm:2.3.2" - checksum: 10/fd3affe2e34efb3970883f934b1828f10b48dffb1eb71a52b7f955bfdd88bf80e94ec388704d95334f72ddf77e34d813b19e1f4bf56897d20252fa025d44bede - languageName: node - linkType: hard - "tar-stream@npm:^3.1.7": version: 3.1.7 resolution: "tar-stream@npm:3.1.7" @@ -14953,16 +14379,17 @@ __metadata: languageName: node linkType: hard -"tar@npm:^7.4.3, tar@npm:^7.5.4": - version: 7.5.13 - resolution: "tar@npm:7.5.13" +"tar@npm:^7.4.3": + version: 7.4.3 + resolution: "tar@npm:7.4.3" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" - minizlib: "npm:^3.1.0" + minizlib: "npm:^3.0.1" + mkdirp: "npm:^3.0.1" yallist: "npm:^5.0.0" - checksum: 10/2bc2b6f0349038a6621dbba1c4522d45752d5071b2994692257113c2050cd23fafc30308f820e5f8ad6fda3f7d7f92adc9a432aa733daa04c42af2061c021c3f + checksum: 10/12a2a4fc6dee23e07cc47f1aeb3a14a1afd3f16397e1350036a8f4cdfee8dcac7ef5978337a4e7b2ac2c27a9a6d46388fc2088ea7c80cb6878c814b1425f8ecf languageName: node linkType: hard @@ -14986,16 +14413,6 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12": - version: 0.2.16 - resolution: "tinyglobby@npm:0.2.16" - dependencies: - fdir: "npm:^6.5.0" - picomatch: "npm:^4.0.4" - checksum: 10/5c2c41b572ada38449e7c86a5fe034f204a1dbba577225a761a14f29f48dc3f2fc0d81a6c56fcc67c5a742cc3aa9fb5e2ca18dbf22b610b0bc0e549b34d5a0f8 - languageName: node - linkType: hard - "tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" @@ -15080,15 +14497,6 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^2.5.0": - version: 2.5.0 - resolution: "ts-api-utils@npm:2.5.0" - peerDependencies: - typescript: ">=4.8.4" - checksum: 10/d5f1936f5618c6ab6942a97b78802217540ced00e7501862ae1f578d9a3aa189fc06050e64cb8951d21f7088e5fd35f53d2bf0d0370a883861c7b05e993ebc44 - languageName: node - linkType: hard - "ts-jest@npm:^29.2.5": version: 29.4.6 resolution: "ts-jest@npm:29.4.6" @@ -15336,13 +14744,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.19.0": - version: 7.19.2 - resolution: "undici-types@npm:7.19.2" - checksum: 10/05c34c63444c8caca7137f122b29ed50c1d7d05d1e0b2337f423575d3264054c4a0139e47e82e65723d09b97fcad6d8b0223b3550430a9006cc00e72a1e035bf - languageName: node - linkType: hard - "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0" @@ -15732,17 +15133,6 @@ __metadata: languageName: node linkType: hard -"which@npm:^6.0.0": - version: 6.0.1 - resolution: "which@npm:6.0.1" - dependencies: - isexe: "npm:^4.0.0" - bin: - node-which: bin/which.js - checksum: 10/dbea77c7d3058bf6c78bf9659d2dce4d2b57d39a15b826b2af6ac2e5a219b99dc8a831b79fdbc453c0598adb4f3f84cf9c2491fd52beb9f5d2dececcad117f68 - languageName: node - linkType: hard - "word-wrap@npm:^1.2.5": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" From 75dfb8506c2ea432702ebea5c1543b240a8457b4 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 17 Apr 2026 11:43:23 +0100 Subject: [PATCH 11/26] refactor: extract constants and caveat helper from upgrade controller Moves ROOT_AUTHORITY, MAX_UINT256, and STEP_ORDER into constants.ts, and the inline #encodeCaveatTerms private method into a standalone encode-caveat-terms.ts module with direct unit tests. First step in splitting up the growing MoneyAccountUpgradeController file. Co-Authored-By: Claude Opus 4.7 --- .../src/MoneyAccountUpgradeController.ts | 34 ++------------- .../src/constants.ts | 22 ++++++++++ .../src/encode-caveat-terms.test.ts | 41 +++++++++++++++++++ .../src/encode-caveat-terms.ts | 12 ++++++ 4 files changed, 79 insertions(+), 30 deletions(-) create mode 100644 packages/money-account-upgrade-controller/src/constants.ts create mode 100644 packages/money-account-upgrade-controller/src/encode-caveat-terms.test.ts create mode 100644 packages/money-account-upgrade-controller/src/encode-caveat-terms.ts diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index f13ff484ec5..b321709579a 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -23,6 +23,8 @@ import type { MoneyAccountControllerGetMoneyAccountAction } from '@metamask/mone import type { Hex } from '@metamask/utils'; import { webcrypto } from 'node:crypto'; +import { MAX_UINT256, ROOT_AUTHORITY, STEP_ORDER } from './constants'; +import { encodeCaveatTerms } from './encode-caveat-terms'; import type { MoneyAccountUpgradeControllerMethodActions } from './MoneyAccountUpgradeController-method-action-types'; import type { AccountUpgradeEntry, @@ -33,24 +35,6 @@ import type { export const controllerName = 'MoneyAccountUpgradeController'; -// The root authority constant for top-level delegations. -const ROOT_AUTHORITY = - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex; - -// The ordered list of upgrade steps, used to determine whether a step -// has already been completed during a previous (possibly interrupted) run. -const STEP_ORDER: UpgradeStep[] = [ - 'associate-address', - 'submit-authorization', - 'verify-delegation', - 'save-delegation', - 'register-intents', -]; - -// Maximum uint256 — used as the allowance for the ERC20TransferAmountEnforcer. -const MAX_UINT256 = - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex; - export type MoneyAccountUpgradeControllerState = { upgrades: Record; }; @@ -369,12 +353,12 @@ export class MoneyAccountUpgradeController extends BaseController< caveats: [ { enforcer: config.erc20TransferAmountEnforcer, - terms: this.#encodeCaveatTerms(MAX_UINT256, config.musdTokenAddress), + terms: encodeCaveatTerms(MAX_UINT256, config.musdTokenAddress), args: '0x' as Hex, }, { enforcer: config.redeemerEnforcer, - terms: this.#encodeCaveatTerms(config.vedaVaultAdapterAddress), + terms: encodeCaveatTerms(config.vedaVaultAdapterAddress), args: '0x' as Hex, }, { @@ -482,16 +466,6 @@ export class MoneyAccountUpgradeController extends BaseController< this.#updateUpgrade(address, { step: 'register-intents' }); } - /** - * Encodes caveat terms by concatenating hex values (ABI-style). - * - * @param values - The hex values to pack. - * @returns The concatenated hex string. - */ - #encodeCaveatTerms(...values: Hex[]): Hex { - return `0x${values.map((val) => val.slice(2).padStart(64, '0')).join('')}`; - } - /** * Merges an update into the upgrade entry for the given address. * diff --git a/packages/money-account-upgrade-controller/src/constants.ts b/packages/money-account-upgrade-controller/src/constants.ts new file mode 100644 index 00000000000..352446e1b25 --- /dev/null +++ b/packages/money-account-upgrade-controller/src/constants.ts @@ -0,0 +1,22 @@ +import type { Hex } from '@metamask/utils'; + +import type { UpgradeStep } from './types'; + +// The root authority constant for top-level delegations. +export const ROOT_AUTHORITY = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex; + +// Maximum uint256 — used as the allowance for the ERC20TransferAmountEnforcer. +// 115792089237316195423570985008687907853269984665640564039457584007913129639935 as a number +export const MAX_UINT256 = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex; + +// The ordered list of upgrade steps, used to determine whether a step +// has already been completed during a previous (possibly interrupted) run. +export const STEP_ORDER: UpgradeStep[] = [ + 'associate-address', + 'submit-authorization', + 'verify-delegation', + 'save-delegation', + 'register-intents', +]; diff --git a/packages/money-account-upgrade-controller/src/encode-caveat-terms.test.ts b/packages/money-account-upgrade-controller/src/encode-caveat-terms.test.ts new file mode 100644 index 00000000000..e17c5f55795 --- /dev/null +++ b/packages/money-account-upgrade-controller/src/encode-caveat-terms.test.ts @@ -0,0 +1,41 @@ +import type { Hex } from '@metamask/utils'; + +import { encodeCaveatTerms } from './encode-caveat-terms'; + +describe('encodeCaveatTerms', () => { + it('returns just "0x" when called with no values', () => { + expect(encodeCaveatTerms()).toBe('0x'); + }); + + it('left-pads a single value to 32 bytes (64 hex chars)', () => { + const result = encodeCaveatTerms('0xff' as Hex); + + expect(result).toBe( + '0x00000000000000000000000000000000000000000000000000000000000000ff', + ); + }); + + it('left-pads an address to 32 bytes', () => { + const address = '0x1111111111111111111111111111111111111111' as Hex; + + expect(encodeCaveatTerms(address)).toBe( + '0x0000000000000000000000001111111111111111111111111111111111111111', + ); + }); + + it('concatenates multiple values, each padded to 32 bytes', () => { + const a = '0xaa' as Hex; + const b = '0xbb' as Hex; + + expect(encodeCaveatTerms(a, b)).toBe( + `0x${'aa'.padStart(64, '0')}${'bb'.padStart(64, '0')}`, + ); + }); + + it('leaves a value that is already 32 bytes unchanged', () => { + const value = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex; + + expect(encodeCaveatTerms(value)).toBe(value); + }); +}); diff --git a/packages/money-account-upgrade-controller/src/encode-caveat-terms.ts b/packages/money-account-upgrade-controller/src/encode-caveat-terms.ts new file mode 100644 index 00000000000..96fb29b9b9a --- /dev/null +++ b/packages/money-account-upgrade-controller/src/encode-caveat-terms.ts @@ -0,0 +1,12 @@ +import type { Hex } from '@metamask/utils'; + +/** + * Encodes caveat terms by concatenating hex values (ABI-style), + * left-padding each value to 32 bytes. + * + * @param values - The hex values to pack. + * @returns The concatenated hex string. + */ +export function encodeCaveatTerms(...values: Hex[]): Hex { + return `0x${values.map((val) => val.slice(2).padStart(64, '0')).join('')}`; +} From e04728c689f3aec02e7baff40039524009d821c3 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 17 Apr 2026 11:49:17 +0100 Subject: [PATCH 12/26] refactor: extract associate-address step into its own module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces src/steps/ with a StepContext/StepResult/Step type scaffold and a #runStep orchestration helper on the controller that handles the skip check, context build, and state patch merge. The associate-address step is the first extracted — direct unit tests cover its behavior, and the corresponding describe block is removed from the controller tests since orchestration is already covered by the resumability tests. Co-Authored-By: Claude Opus 4.7 --- .../src/MoneyAccountUpgradeController.test.ts | 56 ----------- .../src/MoneyAccountUpgradeController.ts | 44 ++++----- .../src/steps/associate-address.test.ts | 93 +++++++++++++++++++ .../src/steps/associate-address.ts | 30 ++++++ .../src/steps/types.ts | 25 +++++ 5 files changed, 170 insertions(+), 78 deletions(-) create mode 100644 packages/money-account-upgrade-controller/src/steps/associate-address.test.ts create mode 100644 packages/money-account-upgrade-controller/src/steps/associate-address.ts create mode 100644 packages/money-account-upgrade-controller/src/steps/types.ts diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts index b9855f1b1dd..a59fada8b17 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -493,62 +493,6 @@ describe('MoneyAccountUpgradeController', () => { }); }); - describe('step 0: associate address', () => { - it('signs the authentication message and submits to CHOMP', async () => { - const { controller, mocks } = await setupInitialized(); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(mocks.signPersonalMessage).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.stringMatching(/^CHOMP Authentication \d+$/u), - from: MOCK_ADDRESS, - }), - ); - - expect(mocks.associateAddress).toHaveBeenCalledWith( - expect.objectContaining({ - signature: MOCK_SIGNATURE, - address: MOCK_ADDRESS, - timestamp: expect.stringMatching(/^\d+$/u), - }), - ); - }); - - it('skips signing when address already has an upgrade entry', async () => { - const { controller, mocks } = await setupInitialized({ - state: { - upgrades: { - [MOCK_ADDRESS]: { - step: 'associate-address', - chainId: MOCK_CHAIN_ID, - }, - }, - }, - }); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(mocks.signPersonalMessage).not.toHaveBeenCalled(); - expect(mocks.associateAddress).not.toHaveBeenCalled(); - }); - - it('updates state to associate-address after completion', async () => { - const { controller, mocks } = await setupInitialized(); - - // Make subsequent steps fail so we can check state after step 0. - mocks.getUpgrade.mockRejectedValue(new Error('stop')); - - await expect( - controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), - ).rejects.toThrow('stop'); - - expect(controller.state.upgrades[MOCK_ADDRESS]?.step).toBe( - 'associate-address', - ); - }); - }); - describe('step 1: submit authorization', () => { it('skips signing when CHOMP already has an upgrade record', async () => { const { controller, mocks } = await setupInitialized(); diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index b321709579a..a9af3aa0af8 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -26,6 +26,8 @@ import { webcrypto } from 'node:crypto'; import { MAX_UINT256, ROOT_AUTHORITY, STEP_ORDER } from './constants'; import { encodeCaveatTerms } from './encode-caveat-terms'; import type { MoneyAccountUpgradeControllerMethodActions } from './MoneyAccountUpgradeController-method-action-types'; +import { associateAddress } from './steps/associate-address'; +import type { Step } from './steps/types'; import type { AccountUpgradeEntry, InitConfig, @@ -214,7 +216,7 @@ export class MoneyAccountUpgradeController extends BaseController< } but upgradeAccount was called with ${chainId}`, ); } - await this.#associateAddress(address); + await this.#runStep(address, chainId, 'associate-address', associateAddress); await this.#submitAuthorization(address, chainId); await this.#verifyDelegation(address, chainId); await this.#saveDelegation(address, chainId); @@ -238,33 +240,31 @@ export class MoneyAccountUpgradeController extends BaseController< } /** - * Step 0: Associate the Money Account address with the user's CHOMP profile. + * Runs an extracted step function unless it has already been completed, + * then merges the returned patch into state. * - * Signs "CHOMP Authentication {timestamp}" via personal_sign and submits - * it to CHOMP. The API accepts 409 (already associated) as success. - * - * @param address - The Money Account address. + * @param address - The account address. + * @param chainId - The target chain. + * @param step - The step being run (used for the skip check). + * @param fn - The extracted step function. */ - async #associateAddress(address: Hex): Promise { - if (this.#isStepCompleted(address, 'associate-address')) { + async #runStep( + address: Hex, + chainId: Hex, + step: UpgradeStep, + fn: Step, + ): Promise { + if (this.#isStepCompleted(address, step)) { return; } - - const timestamp = Date.now().toString(); - const message = `CHOMP Authentication ${timestamp}`; - - const signature = await this.messenger.call( - 'KeyringController:signPersonalMessage', - { data: message, from: address }, - ); - - await this.messenger.call('ChompApiService:associateAddress', { - signature, - timestamp, + const patch = await fn({ + messenger: this.messenger, + config: this.#requireConfig(), address, + chainId, + entry: this.state.upgrades[address], }); - - this.#updateUpgrade(address, { step: 'associate-address' }); + this.#updateUpgrade(address, patch); } /** diff --git a/packages/money-account-upgrade-controller/src/steps/associate-address.test.ts b/packages/money-account-upgrade-controller/src/steps/associate-address.test.ts new file mode 100644 index 00000000000..7bfed8971d3 --- /dev/null +++ b/packages/money-account-upgrade-controller/src/steps/associate-address.test.ts @@ -0,0 +1,93 @@ +import type { Hex } from '@metamask/utils'; + +import { associateAddress } from './associate-address'; +import type { StepContext } from './types'; +import type { UpgradeConfig } from '../types'; + +const MOCK_ADDRESS = '0xabcdef1234567890abcdef1234567890abcdef12' as Hex; +const MOCK_CHAIN_ID = '0x1' as Hex; +const MOCK_SIGNATURE = '0xdeadbeef' as Hex; + +type Handlers = { + 'KeyringController:signPersonalMessage'?: jest.Mock; + 'ChompApiService:associateAddress'?: jest.Mock; +}; + +function buildContext(handlers: Handlers = {}): { + context: StepContext; + call: jest.Mock; +} { + const call = jest.fn((action: string, ...args: unknown[]) => { + const handler = handlers[action as keyof Handlers]; + if (!handler) { + throw new Error(`No handler registered for ${action}`); + } + return handler(...args); + }); + const context: StepContext = { + messenger: { call } as unknown as StepContext['messenger'], + config: {} as UpgradeConfig, + address: MOCK_ADDRESS, + chainId: MOCK_CHAIN_ID, + entry: undefined, + }; + return { context, call }; +} + +describe('associateAddress', () => { + it('signs a timestamped CHOMP Authentication message', async () => { + const signPersonalMessage = jest.fn().mockResolvedValue(MOCK_SIGNATURE); + const { context } = buildContext({ + 'KeyringController:signPersonalMessage': signPersonalMessage, + 'ChompApiService:associateAddress': jest.fn().mockResolvedValue({}), + }); + + await associateAddress(context); + + expect(signPersonalMessage).toHaveBeenCalledWith({ + data: expect.stringMatching(/^CHOMP Authentication \d+$/u), + from: MOCK_ADDRESS, + }); + }); + + it('submits the signature, timestamp, and address to CHOMP', async () => { + const associate = jest.fn().mockResolvedValue({}); + const { context } = buildContext({ + 'KeyringController:signPersonalMessage': jest + .fn() + .mockResolvedValue(MOCK_SIGNATURE), + 'ChompApiService:associateAddress': associate, + }); + + await associateAddress(context); + + expect(associate).toHaveBeenCalledWith({ + signature: MOCK_SIGNATURE, + timestamp: expect.stringMatching(/^\d+$/u), + address: MOCK_ADDRESS, + }); + }); + + it('returns a patch marking associate-address complete', async () => { + const { context } = buildContext({ + 'KeyringController:signPersonalMessage': jest + .fn() + .mockResolvedValue(MOCK_SIGNATURE), + 'ChompApiService:associateAddress': jest.fn().mockResolvedValue({}), + }); + + expect(await associateAddress(context)).toStrictEqual({ + step: 'associate-address', + }); + }); + + it('propagates signing errors', async () => { + const { context } = buildContext({ + 'KeyringController:signPersonalMessage': jest + .fn() + .mockRejectedValue(new Error('signing failed')), + }); + + await expect(associateAddress(context)).rejects.toThrow('signing failed'); + }); +}); diff --git a/packages/money-account-upgrade-controller/src/steps/associate-address.ts b/packages/money-account-upgrade-controller/src/steps/associate-address.ts new file mode 100644 index 00000000000..3671012020e --- /dev/null +++ b/packages/money-account-upgrade-controller/src/steps/associate-address.ts @@ -0,0 +1,30 @@ +import type { Step } from './types'; + +/** + * Step 0: Associate the Money Account address with the user's CHOMP profile. + * + * Signs "CHOMP Authentication {timestamp}" via personal_sign and submits + * it to CHOMP. The API accepts 409 (already associated) as success. + * + * @param context - The step context provided by the controller. + * @param context.messenger - The controller messenger. + * @param context.address - The Money Account address to associate. + * @returns A patch marking `associate-address` as the completed step. + */ +export const associateAddress: Step = async ({ messenger, address }) => { + const timestamp = Date.now().toString(); + const message = `CHOMP Authentication ${timestamp}`; + + const signature = await messenger.call( + 'KeyringController:signPersonalMessage', + { data: message, from: address }, + ); + + await messenger.call('ChompApiService:associateAddress', { + signature, + timestamp, + address, + }); + + return { step: 'associate-address' }; +}; diff --git a/packages/money-account-upgrade-controller/src/steps/types.ts b/packages/money-account-upgrade-controller/src/steps/types.ts new file mode 100644 index 00000000000..e1f45221203 --- /dev/null +++ b/packages/money-account-upgrade-controller/src/steps/types.ts @@ -0,0 +1,25 @@ +import type { Hex } from '@metamask/utils'; + +import type { MoneyAccountUpgradeControllerMessenger } from '../MoneyAccountUpgradeController'; +import type { AccountUpgradeEntry, UpgradeConfig } from '../types'; + +/** + * Context passed to each upgrade step. + */ +export type StepContext = { + messenger: MoneyAccountUpgradeControllerMessenger; + config: UpgradeConfig; + address: Hex; + chainId: Hex; + entry: AccountUpgradeEntry | undefined; +}; + +/** + * Patch returned by a step, merged into the account's upgrade entry by + * the controller. The `step` field is mandatory so the controller can + * record progress. + */ +export type StepResult = Partial & + Pick; + +export type Step = (ctx: StepContext) => Promise; From f6953b3f41ea1cc917b2e21c8c1e55750153b979 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 17 Apr 2026 12:58:53 +0100 Subject: [PATCH 13/26] chore: remove first version --- .../money-account-upgrade-controller/TODO.md | 29 - .../package.json | 2 +- ...ntUpgradeController-method-action-types.ts | 6 +- .../src/MoneyAccountUpgradeController.test.ts | 500 +----------------- .../src/MoneyAccountUpgradeController.ts | 364 +------------ .../src/constants.ts | 22 - .../src/encode-caveat-terms.test.ts | 41 -- .../src/encode-caveat-terms.ts | 12 - .../src/index.ts | 7 +- .../src/steps/associate-address.test.ts | 93 ---- .../src/steps/associate-address.ts | 30 -- .../src/steps/types.ts | 25 - .../src/types.ts | 22 - .../src/utils/source-amounts.ts | 2 +- 14 files changed, 30 insertions(+), 1125 deletions(-) delete mode 100644 packages/money-account-upgrade-controller/TODO.md delete mode 100644 packages/money-account-upgrade-controller/src/constants.ts delete mode 100644 packages/money-account-upgrade-controller/src/encode-caveat-terms.test.ts delete mode 100644 packages/money-account-upgrade-controller/src/encode-caveat-terms.ts delete mode 100644 packages/money-account-upgrade-controller/src/steps/associate-address.test.ts delete mode 100644 packages/money-account-upgrade-controller/src/steps/associate-address.ts delete mode 100644 packages/money-account-upgrade-controller/src/steps/types.ts diff --git a/packages/money-account-upgrade-controller/TODO.md b/packages/money-account-upgrade-controller/TODO.md deleted file mode 100644 index 232dc9b3a2b..00000000000 --- a/packages/money-account-upgrade-controller/TODO.md +++ /dev/null @@ -1,29 +0,0 @@ -# MoneyAccountUpgradeController — Remaining Work - -## Step 0: Associate address - -- [x] **Idempotency check**: `#associateAddress` now checks persisted state and skips signing/submission if the step has already been completed. - -## Step 1: Submit authorization - -- [ ] **Fetch on-chain nonce**: The nonce is hardcoded to `0` (line 271). This needs to be replaced with an actual on-chain nonce fetch for the account — likely via an `eth_getTransactionCount` call or equivalent messenger action. CHOMP validates that the nonce matches. - -## Step 2: Verify delegation - -- [x] **Caveat term encoding**: `#encodeCaveatTerms` now uses proper 256-bit zero-padded ABI encoding (`padStart(64, '0')`). -- [ ] **Use `@metamask/smart-accounts-kit`**: The description mentions using `createDelegation` from `@metamask/smart-accounts-kit` to build the delegation. This package is not yet in the repo — once it lands, the delegation construction in `#verifyDelegation` should use it instead of manual assembly. -- [ ] **Import `ROOT_AUTHORITY` from `@metamask/delegation-controller`**: Currently defined locally as a constant. The delegation-controller has it but doesn't export it — once it's exported, import it instead. - -## Step 3: Save delegation - -- [ ] **Implement Authenticated User Storage save**: `#saveDelegation` is a stub. Needs the `@metamask/authenticated-user-storage` wrapper (PR currently open). Once available, save the signed delegation so CHOMP can read it at execution time via its internal VPC endpoint. - -## Step 4: Register intents - -- [ ] **Intent configuration**: The deposit/withdrawal intents are hardcoded with `mUSD` token symbol and `MAX_UINT256` allowance. These may need to come from config or be parameterised once requirements solidify. - -## General - -- [x] **Resumability**: Every step now checks persisted state via `#isStepCompleted` and skips if already completed. Retrying after a mid-sequence failure resumes from the last incomplete step. -- [ ] **`MoneyAccountController:getMoneyAccount`**: This action is declared in `AllowedActions` but not currently used. It was included anticipating the controller may need to look up account details. Remove if not needed, or use it to validate the address before starting. -- [ ] **`#saveDelegation` unused `_chainId` parameter**: Will be needed once the stub is implemented — the storage call will likely need it. diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json index 60de36b38de..af3102a0f7b 100644 --- a/packages/money-account-upgrade-controller/package.json +++ b/packages/money-account-upgrade-controller/package.json @@ -53,7 +53,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^9.0.1", + "@metamask/base-controller": "^9.1.0", "@metamask/chomp-api-service": "^0.0.0", "@metamask/delegation-controller": "^3.0.0", "@metamask/keyring-controller": "^25.2.0", diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts index bf1040d3d17..30acd34bf74 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts @@ -6,11 +6,7 @@ import type { MoneyAccountUpgradeController } from './MoneyAccountUpgradeController'; /** - * Runs the full upgrade sequence for a Money Account. - * - * @param address - The Money Account address to upgrade. - * @param chainId - The target chain for the upgrade. - * @returns A promise that resolves when the upgrade is complete. + * Upgrades a Money Account. Currently a no-op. */ export type MoneyAccountUpgradeControllerUpgradeAccountAction = { type: `MoneyAccountUpgradeController:upgradeAccount`; diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts index a59fada8b17..a44dcaba379 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -13,14 +13,7 @@ import { } from '.'; import type { UpgradeConfig } from './types'; -const MOCK_ADDRESS = '0xabcdef1234567890abcdef1234567890abcdef12' as Hex; const MOCK_CHAIN_ID = '0x1' as Hex; -const MOCK_SIGNATURE = - '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + - 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + - '1b'; -const MOCK_DELEGATION_SIGNATURE = '0xdeadbeef' as Hex; -const MOCK_DELEGATION_HASH = '0xabcdef1234567890'; const MOCK_CONFIG: UpgradeConfig = { delegateAddress: '0x1111111111111111111111111111111111111111' as Hex, @@ -68,15 +61,6 @@ type AllEvents = MessengerEvents; type RootMessenger = Messenger; type Mocks = { - signPersonalMessage: jest.Mock; - signEip7702Authorization: jest.Mock; - associateAddress: jest.Mock; - createUpgrade: jest.Mock; - getUpgrade: jest.Mock; - verifyDelegation: jest.Mock; - createIntents: jest.Mock; - signDelegation: jest.Mock; - getMoneyAccount: jest.Mock; getServiceDetails: jest.Mock; }; @@ -93,29 +77,6 @@ function setup({ mocks: Mocks; } { const mocks: Mocks = { - signPersonalMessage: jest.fn().mockResolvedValue(MOCK_SIGNATURE), - signEip7702Authorization: jest.fn().mockResolvedValue(MOCK_SIGNATURE), - associateAddress: jest.fn().mockResolvedValue({ - profileId: 'profile-1', - address: MOCK_ADDRESS, - status: 'created', - }), - createUpgrade: jest.fn().mockResolvedValue({ - signerAddress: MOCK_ADDRESS, - status: 'created', - createdAt: '2026-04-10T00:00:00Z', - }), - getUpgrade: jest.fn().mockResolvedValue(null), - verifyDelegation: jest.fn().mockResolvedValue({ - valid: true, - delegationHash: MOCK_DELEGATION_HASH, - }), - createIntents: jest.fn().mockResolvedValue([]), - signDelegation: jest.fn().mockResolvedValue(MOCK_DELEGATION_SIGNATURE), - getMoneyAccount: jest.fn().mockReturnValue({ - id: 'account-1', - address: MOCK_ADDRESS, - }), getServiceDetails: jest .fn() .mockResolvedValue(MOCK_SERVICE_DETAILS_RESPONSE), @@ -125,42 +86,6 @@ function setup({ namespace: MOCK_ANY_NAMESPACE, }); - rootMessenger.registerActionHandler( - 'KeyringController:signPersonalMessage', - mocks.signPersonalMessage, - ); - rootMessenger.registerActionHandler( - 'KeyringController:signEip7702Authorization', - mocks.signEip7702Authorization, - ); - rootMessenger.registerActionHandler( - 'ChompApiService:associateAddress', - mocks.associateAddress, - ); - rootMessenger.registerActionHandler( - 'ChompApiService:createUpgrade', - mocks.createUpgrade, - ); - rootMessenger.registerActionHandler( - 'ChompApiService:getUpgrade', - mocks.getUpgrade, - ); - rootMessenger.registerActionHandler( - 'ChompApiService:verifyDelegation', - mocks.verifyDelegation, - ); - rootMessenger.registerActionHandler( - 'ChompApiService:createIntents', - mocks.createIntents, - ); - rootMessenger.registerActionHandler( - 'DelegationController:signDelegation', - mocks.signDelegation, - ); - rootMessenger.registerActionHandler( - 'MoneyAccountController:getMoneyAccount', - mocks.getMoneyAccount, - ); rootMessenger.registerActionHandler( 'ChompApiService:getServiceDetails', mocks.getServiceDetails, @@ -172,18 +97,7 @@ function setup({ }); rootMessenger.delegate({ - actions: [ - 'KeyringController:signPersonalMessage', - 'KeyringController:signEip7702Authorization', - 'ChompApiService:associateAddress', - 'ChompApiService:createUpgrade', - 'ChompApiService:getUpgrade', - 'ChompApiService:verifyDelegation', - 'ChompApiService:createIntents', - 'ChompApiService:getServiceDetails', - 'DelegationController:signDelegation', - 'MoneyAccountController:getMoneyAccount', - ], + actions: ['ChompApiService:getServiceDetails'], events: [], messenger, }); @@ -196,23 +110,6 @@ function setup({ return { controller, rootMessenger, messenger, mocks }; } -async function setupInitialized({ - state, -}: { - state?: Partial< - ReturnType - >; -} = {}): Promise<{ - controller: MoneyAccountUpgradeController; - rootMessenger: RootMessenger; - messenger: MoneyAccountUpgradeControllerMessenger; - mocks: Mocks; -}> { - const result = setup({ state }); - await result.controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG); - return result; -} - describe('MoneyAccountUpgradeController', () => { describe('constructor', () => { it('initializes with default state when no state is provided', () => { @@ -227,18 +124,16 @@ describe('MoneyAccountUpgradeController', () => { const { controller } = setup({ state: { upgrades: { - [MOCK_ADDRESS]: { - step: 'associate-address', + '0xabcdef1234567890abcdef1234567890abcdef12': { chainId: MOCK_CHAIN_ID, }, }, }, }); - expect(controller.state.upgrades[MOCK_ADDRESS]).toStrictEqual({ - step: 'associate-address', - chainId: MOCK_CHAIN_ID, - }); + expect( + controller.state.upgrades['0xabcdef1234567890abcdef1234567890abcdef12'], + ).toStrictEqual({ chainId: MOCK_CHAIN_ID }); }); it('starts with initialized set to false', () => { @@ -334,398 +229,29 @@ describe('MoneyAccountUpgradeController', () => { }); describe('upgradeAccount', () => { - it('throws when controller is not initialized', async () => { + it('resolves without doing anything', async () => { const { controller } = setup(); - await expect( - controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), - ).rejects.toThrow( - 'MoneyAccountUpgradeController is not initialized. Call init() first.', - ); - }); - - it('throws when called with a different chain ID than init', async () => { - const { controller } = await setupInitialized(); - const otherChainId = '0xa4b1' as Hex; - - await expect( - controller.upgradeAccount(MOCK_ADDRESS, otherChainId), - ).rejects.toThrow( - `Chain ID mismatch: controller was initialized for ${MOCK_CHAIN_ID} but upgradeAccount was called with ${otherChainId}`, - ); + expect(await controller.upgradeAccount()).toBeUndefined(); }); - it('runs the full upgrade sequence', async () => { - const { controller, mocks } = await setupInitialized(); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(mocks.signPersonalMessage).toHaveBeenCalledTimes(1); - expect(mocks.associateAddress).toHaveBeenCalledTimes(1); - expect(mocks.getUpgrade).toHaveBeenCalledTimes(1); - expect(mocks.signEip7702Authorization).toHaveBeenCalledTimes(1); - expect(mocks.createUpgrade).toHaveBeenCalledTimes(1); - expect(mocks.signDelegation).toHaveBeenCalledTimes(1); - expect(mocks.verifyDelegation).toHaveBeenCalledTimes(1); - expect(mocks.createIntents).toHaveBeenCalledTimes(1); - }); - - it('records final state as register-intents with delegationHash', async () => { - const { controller } = await setupInitialized(); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(controller.state.upgrades[MOCK_ADDRESS]).toStrictEqual({ - step: 'register-intents', - chainId: MOCK_CHAIN_ID, - delegationHash: MOCK_DELEGATION_HASH, - }); - }); - - it('skips all steps when upgrade is already complete', async () => { - const { controller, mocks } = await setupInitialized({ - state: { - upgrades: { - [MOCK_ADDRESS]: { - step: 'register-intents', - chainId: MOCK_CHAIN_ID, - delegationHash: MOCK_DELEGATION_HASH, - }, - }, - }, - }); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(mocks.signPersonalMessage).not.toHaveBeenCalled(); - expect(mocks.associateAddress).not.toHaveBeenCalled(); - expect(mocks.getUpgrade).not.toHaveBeenCalled(); - expect(mocks.signEip7702Authorization).not.toHaveBeenCalled(); - expect(mocks.signDelegation).not.toHaveBeenCalled(); - expect(mocks.verifyDelegation).not.toHaveBeenCalled(); - expect(mocks.createIntents).not.toHaveBeenCalled(); - }); - - it('resumes from submit-authorization, skipping steps 0-1', async () => { - const { controller, mocks } = await setupInitialized({ - state: { - upgrades: { - [MOCK_ADDRESS]: { - step: 'submit-authorization', - chainId: MOCK_CHAIN_ID, - }, - }, - }, - }); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(mocks.signPersonalMessage).not.toHaveBeenCalled(); - expect(mocks.associateAddress).not.toHaveBeenCalled(); - expect(mocks.getUpgrade).not.toHaveBeenCalled(); - expect(mocks.signEip7702Authorization).not.toHaveBeenCalled(); - // Steps 2-4 run - expect(mocks.signDelegation).toHaveBeenCalledTimes(1); - expect(mocks.verifyDelegation).toHaveBeenCalledTimes(1); - expect(mocks.createIntents).toHaveBeenCalledTimes(1); - }); - - it('resumes from verify-delegation, skipping steps 0-2', async () => { - const { controller, mocks } = await setupInitialized({ - state: { - upgrades: { - [MOCK_ADDRESS]: { - step: 'verify-delegation', - chainId: MOCK_CHAIN_ID, - delegationHash: MOCK_DELEGATION_HASH, - }, - }, - }, - }); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(mocks.signPersonalMessage).not.toHaveBeenCalled(); - expect(mocks.associateAddress).not.toHaveBeenCalled(); - expect(mocks.getUpgrade).not.toHaveBeenCalled(); - expect(mocks.signEip7702Authorization).not.toHaveBeenCalled(); - expect(mocks.signDelegation).not.toHaveBeenCalled(); - expect(mocks.verifyDelegation).not.toHaveBeenCalled(); - // Steps 3-4 run - expect(mocks.createIntents).toHaveBeenCalledTimes(1); - }); - - it('resumes from save-delegation, skipping steps 0-3', async () => { - const { controller, mocks } = await setupInitialized({ - state: { - upgrades: { - [MOCK_ADDRESS]: { - step: 'save-delegation', - chainId: MOCK_CHAIN_ID, - delegationHash: MOCK_DELEGATION_HASH, - }, - }, - }, - }); + it('does not mutate state', async () => { + const { controller } = setup(); + const stateBefore = controller.state; - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); + await controller.upgradeAccount(); - expect(mocks.signPersonalMessage).not.toHaveBeenCalled(); - expect(mocks.associateAddress).not.toHaveBeenCalled(); - expect(mocks.getUpgrade).not.toHaveBeenCalled(); - expect(mocks.signEip7702Authorization).not.toHaveBeenCalled(); - expect(mocks.signDelegation).not.toHaveBeenCalled(); - expect(mocks.verifyDelegation).not.toHaveBeenCalled(); - // Only step 4 runs - expect(mocks.createIntents).toHaveBeenCalledTimes(1); + expect(controller.state).toStrictEqual(stateBefore); }); it('is callable via the messenger', async () => { - const { rootMessenger } = await setupInitialized(); + const { rootMessenger } = setup(); expect( await rootMessenger.call( 'MoneyAccountUpgradeController:upgradeAccount', - MOCK_ADDRESS, - MOCK_CHAIN_ID, ), ).toBeUndefined(); }); }); - - describe('step 1: submit authorization', () => { - it('skips signing when CHOMP already has an upgrade record', async () => { - const { controller, mocks } = await setupInitialized(); - - mocks.getUpgrade.mockResolvedValue({ - signerAddress: MOCK_ADDRESS, - status: 'upgraded', - createdAt: '2026-04-09T00:00:00Z', - }); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(mocks.signEip7702Authorization).not.toHaveBeenCalled(); - expect(mocks.createUpgrade).not.toHaveBeenCalled(); - }); - - it('signs and submits an EIP-7702 authorization', async () => { - const { controller, mocks } = await setupInitialized(); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(mocks.signEip7702Authorization).toHaveBeenCalledWith({ - chainId: 1, - contractAddress: MOCK_CONFIG.delegatorImplAddress, - nonce: 0, - from: MOCK_ADDRESS, - }); - - expect(mocks.createUpgrade).toHaveBeenCalledWith( - expect.objectContaining({ - address: MOCK_ADDRESS, - chainId: MOCK_CHAIN_ID, - }), - ); - }); - - it('computes yParity=1 when v is 28', async () => { - const { controller, mocks } = await setupInitialized(); - - const sigWithV28 = - '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + - 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + - '1c'; - mocks.signEip7702Authorization.mockResolvedValue(sigWithV28); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(mocks.createUpgrade).toHaveBeenCalledWith( - expect.objectContaining({ - v: 28, - yParity: 1, - }), - ); - }); - - it('parses signature components correctly', async () => { - const { controller, mocks } = await setupInitialized(); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(mocks.createUpgrade).toHaveBeenCalledWith( - expect.objectContaining({ - // r = first 66 chars (0x + 64 hex chars) - r: MOCK_SIGNATURE.slice(0, 66), - // s = 0x + next 64 hex chars - s: `0x${MOCK_SIGNATURE.slice(66, 130)}`, - // v = last 2 hex chars parsed as int - v: parseInt(MOCK_SIGNATURE.slice(130, 132), 16), - // yParity derived from v - yParity: - parseInt(MOCK_SIGNATURE.slice(130, 132), 16) - 27 === 0 ? 0 : 1, - }), - ); - }); - }); - - describe('step 2: verify delegation', () => { - it('builds a delegation with three caveats and verifies with CHOMP', async () => { - const { controller, mocks } = await setupInitialized(); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(mocks.signDelegation).toHaveBeenCalledWith({ - delegation: expect.objectContaining({ - delegate: MOCK_CONFIG.delegateAddress, - delegator: MOCK_ADDRESS, - caveats: expect.arrayContaining([ - expect.objectContaining({ - enforcer: MOCK_CONFIG.erc20TransferAmountEnforcer, - }), - expect.objectContaining({ - enforcer: MOCK_CONFIG.redeemerEnforcer, - }), - expect.objectContaining({ - enforcer: MOCK_CONFIG.valueLteEnforcer, - }), - ]), - }), - chainId: MOCK_CHAIN_ID, - }); - - expect(mocks.verifyDelegation).toHaveBeenCalledWith( - expect.objectContaining({ - signedDelegation: expect.objectContaining({ - delegate: MOCK_CONFIG.delegateAddress, - delegator: MOCK_ADDRESS, - signature: MOCK_DELEGATION_SIGNATURE, - }), - chainId: MOCK_CHAIN_ID, - }), - ); - }); - - it('includes exactly three caveats', async () => { - const { controller, mocks } = await setupInitialized(); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - const { delegation } = mocks.signDelegation.mock.calls[0][0]; - expect(delegation.caveats).toHaveLength(3); - }); - - it('throws when delegation verification fails', async () => { - const { controller, mocks } = await setupInitialized(); - - mocks.verifyDelegation.mockResolvedValue({ - valid: false, - errors: ['delegate mismatch'], - }); - - await expect( - controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), - ).rejects.toThrow('Delegation verification failed: delegate mismatch'); - }); - - it('throws with unknown error when verification fails without error details', async () => { - const { controller, mocks } = await setupInitialized(); - - mocks.verifyDelegation.mockResolvedValue({ - valid: false, - }); - - await expect( - controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), - ).rejects.toThrow('Delegation verification failed: unknown error'); - }); - - it('stores delegationHash in state after successful verification', async () => { - const { controller } = await setupInitialized(); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(controller.state.upgrades[MOCK_ADDRESS]?.delegationHash).toBe( - MOCK_DELEGATION_HASH, - ); - }); - }); - - describe('step 3: save delegation (stub)', () => { - it('updates state to save-delegation', async () => { - const { controller, mocks } = await setupInitialized(); - - // Make step 4 fail so we can check state after step 3. - mocks.createIntents.mockRejectedValue(new Error('stop')); - - await expect( - controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), - ).rejects.toThrow('stop'); - - expect(controller.state.upgrades[MOCK_ADDRESS]?.step).toBe( - 'save-delegation', - ); - }); - }); - - describe('step 4: register intents', () => { - it('submits deposit and withdrawal intents', async () => { - const { controller, mocks } = await setupInitialized(); - - await controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID); - - expect(mocks.createIntents).toHaveBeenCalledWith([ - expect.objectContaining({ - account: MOCK_ADDRESS, - delegationHash: MOCK_DELEGATION_HASH, - chainId: MOCK_CHAIN_ID, - metadata: expect.objectContaining({ type: 'cash-deposit' }), - }), - expect.objectContaining({ - account: MOCK_ADDRESS, - delegationHash: MOCK_DELEGATION_HASH, - chainId: MOCK_CHAIN_ID, - metadata: expect.objectContaining({ type: 'cash-withdrawal' }), - }), - ]); - }); - - it('throws if delegationHash is missing', async () => { - const { controller, mocks } = await setupInitialized(); - - // Skip the verify step by making it not store a hash - mocks.verifyDelegation.mockResolvedValue({ - valid: true, - // No delegationHash returned - }); - - await expect( - controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), - ).rejects.toThrow('Cannot register intents: no delegationHash found'); - }); - }); - - describe('error propagation', () => { - it('propagates signing errors', async () => { - const { controller, mocks } = await setupInitialized(); - - mocks.signPersonalMessage.mockRejectedValue(new Error('signing failed')); - - await expect( - controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), - ).rejects.toThrow('signing failed'); - }); - - it('propagates CHOMP API errors', async () => { - const { controller, mocks } = await setupInitialized(); - - mocks.associateAddress.mockRejectedValue( - new Error("POST /v1/auth/address failed with status '500'"), - ); - - await expect( - controller.upgradeAccount(MOCK_ADDRESS, MOCK_CHAIN_ID), - ).rejects.toThrow("POST /v1/auth/address failed with status '500'"); - }); - }); }); diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index a9af3aa0af8..3559f628bd2 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -4,41 +4,17 @@ import type { StateMetadata, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; -import type { - ChompApiServiceAssociateAddressAction, - ChompApiServiceCreateIntentsAction, - ChompApiServiceCreateUpgradeAction, - ChompApiServiceGetServiceDetailsAction, - ChompApiServiceGetUpgradeAction, - ChompApiServiceVerifyDelegationAction, - SignedDelegation, -} from '@metamask/chomp-api-service'; -import type { DelegationControllerSignDelegationAction } from '@metamask/delegation-controller'; -import type { - KeyringControllerSignEip7702AuthorizationAction, - KeyringControllerSignPersonalMessageAction, -} from '@metamask/keyring-controller'; +import type { ChompApiServiceGetServiceDetailsAction } from '@metamask/chomp-api-service'; import type { Messenger } from '@metamask/messenger'; -import type { MoneyAccountControllerGetMoneyAccountAction } from '@metamask/money-account-controller'; import type { Hex } from '@metamask/utils'; -import { webcrypto } from 'node:crypto'; -import { MAX_UINT256, ROOT_AUTHORITY, STEP_ORDER } from './constants'; -import { encodeCaveatTerms } from './encode-caveat-terms'; import type { MoneyAccountUpgradeControllerMethodActions } from './MoneyAccountUpgradeController-method-action-types'; -import { associateAddress } from './steps/associate-address'; -import type { Step } from './steps/types'; -import type { - AccountUpgradeEntry, - InitConfig, - UpgradeConfig, - UpgradeStep, -} from './types'; +import type { InitConfig } from './types'; export const controllerName = 'MoneyAccountUpgradeController'; export type MoneyAccountUpgradeControllerState = { - upgrades: Record; + upgrades: Record; }; const moneyAccountUpgradeControllerMetadata = { @@ -68,17 +44,7 @@ export type MoneyAccountUpgradeControllerActions = | MoneyAccountUpgradeControllerGetStateAction | MoneyAccountUpgradeControllerMethodActions; -type AllowedActions = - | ChompApiServiceAssociateAddressAction - | ChompApiServiceCreateUpgradeAction - | ChompApiServiceGetUpgradeAction - | ChompApiServiceVerifyDelegationAction - | ChompApiServiceCreateIntentsAction - | ChompApiServiceGetServiceDetailsAction - | KeyringControllerSignPersonalMessageAction - | KeyringControllerSignEip7702AuthorizationAction - | DelegationControllerSignDelegationAction - | MoneyAccountControllerGetMoneyAccountAction; +type AllowedActions = ChompApiServiceGetServiceDetailsAction; export type MoneyAccountUpgradeControllerStateChangeEvent = ControllerStateChangeEvent< @@ -98,17 +64,13 @@ export type MoneyAccountUpgradeControllerMessenger = Messenger< >; /** - * Controller that orchestrates the multi-step Money Account upgrade sequence. + * Controller that orchestrates the Money Account upgrade sequence. */ export class MoneyAccountUpgradeController extends BaseController< typeof controllerName, MoneyAccountUpgradeControllerState, MoneyAccountUpgradeControllerMessenger > { - #config: UpgradeConfig | undefined; - - #chainId: Hex | undefined; - initialized: boolean; /** @@ -144,12 +106,13 @@ export class MoneyAccountUpgradeController extends BaseController< } /** - * Fetches service details and builds the upgrade config. + * Fetches service details and validates the controller can operate on the + * given chain. * * @param chainId - The chain to initialize for. - * @param initConfig - Contract addresses not available from the service details API. + * @param _initConfig - Contract addresses not available from the service details API. */ - async init(chainId: Hex, initConfig: InitConfig): Promise { + async init(chainId: Hex, _initConfig: InitConfig): Promise { const response = await this.messenger.call( 'ChompApiService:getServiceDetails', [chainId], @@ -167,320 +130,19 @@ export class MoneyAccountUpgradeController extends BaseController< ); } - const [firstToken] = vedaProtocol.supportedTokens; - if (!firstToken) { + if (vedaProtocol.supportedTokens.length === 0) { throw new Error( `No supported tokens found for vedaProtocol on chain ${chainId}`, ); } - this.#config = { - delegateAddress: chain.autoDepositDelegate as Hex, - erc20TransferAmountEnforcer: firstToken.tokenAddress as Hex, - vedaVaultAdapterAddress: vedaProtocol.adapterAddress as Hex, - ...initConfig, - }; - this.#chainId = chainId; this.initialized = true; } /** - * Returns the upgrade config, throwing if the controller has not been initialized. - * - * @returns The upgrade config. - */ - #requireConfig(): UpgradeConfig { - if (!this.#config) { - throw new Error( - 'MoneyAccountUpgradeController is not initialized. Call init() first.', - ); - } - return this.#config; - } - - /** - * Runs the full upgrade sequence for a Money Account. Each step is - * idempotent — if the step has already been completed, it is skipped. - * - * @param address - The Money Account address to upgrade. - * @param chainId - The target chain for the upgrade. - */ - async upgradeAccount(address: Hex, chainId: Hex): Promise { - // Validates that init() has been called, throwing if not. - this.#requireConfig(); - - if (chainId !== this.#chainId) { - throw new Error( - `Chain ID mismatch: controller was initialized for ${ - this.#chainId - } but upgradeAccount was called with ${chainId}`, - ); - } - await this.#runStep(address, chainId, 'associate-address', associateAddress); - await this.#submitAuthorization(address, chainId); - await this.#verifyDelegation(address, chainId); - await this.#saveDelegation(address, chainId); - await this.#registerIntents(address, chainId); - } - - /** - * Returns true if the persisted upgrade entry for the given address - * shows a step at or past the target step. - * - * @param address - The account address. - * @param step - The step to check. - * @returns Whether the step has already been completed. - */ - #isStepCompleted(address: Hex, step: UpgradeStep): boolean { - const entry = this.state.upgrades[address]; - if (!entry) { - return false; - } - return STEP_ORDER.indexOf(entry.step) >= STEP_ORDER.indexOf(step); - } - - /** - * Runs an extracted step function unless it has already been completed, - * then merges the returned patch into state. - * - * @param address - The account address. - * @param chainId - The target chain. - * @param step - The step being run (used for the skip check). - * @param fn - The extracted step function. + * Upgrades a Money Account. Currently a no-op. */ - async #runStep( - address: Hex, - chainId: Hex, - step: UpgradeStep, - fn: Step, - ): Promise { - if (this.#isStepCompleted(address, step)) { - return; - } - const patch = await fn({ - messenger: this.messenger, - config: this.#requireConfig(), - address, - chainId, - entry: this.state.upgrades[address], - }); - this.#updateUpgrade(address, patch); - } - - /** - * Step 1: Sign and submit an EIP-7702 authorization to CHOMP. - * - * Skips if CHOMP already has an upgrade record for this address. - * - * @param address - The Money Account address. - * @param chainId - The target chain. - */ - async #submitAuthorization(address: Hex, chainId: Hex): Promise { - if (this.#isStepCompleted(address, 'submit-authorization')) { - return; - } - - const existing = await this.messenger.call( - 'ChompApiService:getUpgrade', - address, - ); - - if (existing) { - this.#updateUpgrade(address, { step: 'submit-authorization', chainId }); - return; - } - - // TODO: Fetch on-chain nonce. Using 0 as placeholder. - const nonce = 0; - const chainIdDecimal = parseInt(chainId, 16); - - const config = this.#requireConfig(); - const signature = await this.messenger.call( - 'KeyringController:signEip7702Authorization', - { - chainId: chainIdDecimal, - contractAddress: config.delegatorImplAddress, - nonce, - from: address, - }, - ); - - const sigR = signature.slice(0, 66); - const sigS = `0x${signature.slice(66, 130)}`; - const sigV = parseInt(signature.slice(130, 132), 16); - const yParity = sigV - 27 === 0 ? 0 : 1; - - await this.messenger.call('ChompApiService:createUpgrade', { - r: sigR, - s: sigS, - v: sigV, - yParity, - address, - chainId, - nonce: nonce.toString(), - }); - - this.#updateUpgrade(address, { step: 'submit-authorization', chainId }); - } - - /** - * Step 2: Build, sign, and verify a delegation with CHOMP. - * - * Constructs an unsigned delegation with three caveat enforcers - * (ERC20TransferAmount, Redeemer, ValueLte), signs it via the - * DelegationController, and verifies it with CHOMP. - * - * @param address - The Money Account address (delegator). - * @param chainId - The target chain. - */ - async #verifyDelegation(address: Hex, chainId: Hex): Promise { - if (this.#isStepCompleted(address, 'verify-delegation')) { - return; - } - - const salt: Hex = `0x${Array.from( - webcrypto.getRandomValues(new Uint8Array(32)), - ) - .map((b) => b.toString(16).padStart(2, '0')) - .join('')}`; - - const config = this.#requireConfig(); - - const delegation = { - delegate: config.delegateAddress, - delegator: address, - authority: ROOT_AUTHORITY, - caveats: [ - { - enforcer: config.erc20TransferAmountEnforcer, - terms: encodeCaveatTerms(MAX_UINT256, config.musdTokenAddress), - args: '0x' as Hex, - }, - { - enforcer: config.redeemerEnforcer, - terms: encodeCaveatTerms(config.vedaVaultAdapterAddress), - args: '0x' as Hex, - }, - { - enforcer: config.valueLteEnforcer, - terms: - '0x0000000000000000000000000000000000000000000000000000000000000000' as Hex, - args: '0x' as Hex, - }, - ], - salt, - }; - - const signature: string = await this.messenger.call( - 'DelegationController:signDelegation', - { delegation, chainId }, - ); - - const signedDelegation: SignedDelegation = { - ...delegation, - signature: signature as Hex, - }; - - const result = await this.messenger.call( - 'ChompApiService:verifyDelegation', - { signedDelegation, chainId }, - ); - - if (!result.valid) { - throw new Error( - `Delegation verification failed: ${ - result.errors?.join(', ') ?? 'unknown error' - }`, - ); - } - - this.#updateUpgrade(address, { - step: 'verify-delegation', - chainId, - delegationHash: result.delegationHash, - }); - } - - /** - * Step 3: Save the signed delegation to Authenticated User Storage. - * - * @param address - The Money Account address. - * @param _chainId - The target chain (unused in stub). - */ - async #saveDelegation(address: Hex, _chainId: Hex): Promise { - if (this.#isStepCompleted(address, 'save-delegation')) { - return; - } - - // TODO: Save delegation to Authenticated User Storage once the - // @metamask/authenticated-user-storage wrapper is available. - this.#updateUpgrade(address, { step: 'save-delegation' }); - } - - /** - * Step 4: Register intents with CHOMP so it begins monitoring the account. - * - * @param address - The Money Account address. - * @param chainId - The target chain. - */ - async #registerIntents(address: Hex, chainId: Hex): Promise { - if (this.#isStepCompleted(address, 'register-intents')) { - return; - } - - const entry = this.state.upgrades[address]; - const delegationHash = entry?.delegationHash; - - if (!delegationHash) { - throw new Error( - 'Cannot register intents: no delegationHash found. Run verify-delegation first.', - ); - } - - const config = this.#requireConfig(); - await this.messenger.call('ChompApiService:createIntents', [ - { - account: address, - delegationHash: delegationHash as Hex, - chainId, - metadata: { - allowance: MAX_UINT256, - tokenSymbol: 'mUSD', - tokenAddress: config.musdTokenAddress, - type: 'cash-deposit', - }, - }, - { - account: address, - delegationHash: delegationHash as Hex, - chainId, - metadata: { - allowance: MAX_UINT256, - tokenSymbol: 'mUSD', - tokenAddress: config.musdTokenAddress, - type: 'cash-withdrawal', - }, - }, - ]); - - this.#updateUpgrade(address, { step: 'register-intents' }); - } - - /** - * Merges an update into the upgrade entry for the given address. - * - * @param address - The account address. - * @param update - Fields to merge into the existing entry. - */ - #updateUpgrade( - address: Hex, - update: Partial & Pick, - ): void { - this.update((state) => { - state.upgrades[address] = { - ...state.upgrades[address], - ...update, - } as AccountUpgradeEntry; - }); + async upgradeAccount(): Promise { + // No-op. } } diff --git a/packages/money-account-upgrade-controller/src/constants.ts b/packages/money-account-upgrade-controller/src/constants.ts deleted file mode 100644 index 352446e1b25..00000000000 --- a/packages/money-account-upgrade-controller/src/constants.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Hex } from '@metamask/utils'; - -import type { UpgradeStep } from './types'; - -// The root authority constant for top-level delegations. -export const ROOT_AUTHORITY = - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex; - -// Maximum uint256 — used as the allowance for the ERC20TransferAmountEnforcer. -// 115792089237316195423570985008687907853269984665640564039457584007913129639935 as a number -export const MAX_UINT256 = - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex; - -// The ordered list of upgrade steps, used to determine whether a step -// has already been completed during a previous (possibly interrupted) run. -export const STEP_ORDER: UpgradeStep[] = [ - 'associate-address', - 'submit-authorization', - 'verify-delegation', - 'save-delegation', - 'register-intents', -]; diff --git a/packages/money-account-upgrade-controller/src/encode-caveat-terms.test.ts b/packages/money-account-upgrade-controller/src/encode-caveat-terms.test.ts deleted file mode 100644 index e17c5f55795..00000000000 --- a/packages/money-account-upgrade-controller/src/encode-caveat-terms.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Hex } from '@metamask/utils'; - -import { encodeCaveatTerms } from './encode-caveat-terms'; - -describe('encodeCaveatTerms', () => { - it('returns just "0x" when called with no values', () => { - expect(encodeCaveatTerms()).toBe('0x'); - }); - - it('left-pads a single value to 32 bytes (64 hex chars)', () => { - const result = encodeCaveatTerms('0xff' as Hex); - - expect(result).toBe( - '0x00000000000000000000000000000000000000000000000000000000000000ff', - ); - }); - - it('left-pads an address to 32 bytes', () => { - const address = '0x1111111111111111111111111111111111111111' as Hex; - - expect(encodeCaveatTerms(address)).toBe( - '0x0000000000000000000000001111111111111111111111111111111111111111', - ); - }); - - it('concatenates multiple values, each padded to 32 bytes', () => { - const a = '0xaa' as Hex; - const b = '0xbb' as Hex; - - expect(encodeCaveatTerms(a, b)).toBe( - `0x${'aa'.padStart(64, '0')}${'bb'.padStart(64, '0')}`, - ); - }); - - it('leaves a value that is already 32 bytes unchanged', () => { - const value = - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex; - - expect(encodeCaveatTerms(value)).toBe(value); - }); -}); diff --git a/packages/money-account-upgrade-controller/src/encode-caveat-terms.ts b/packages/money-account-upgrade-controller/src/encode-caveat-terms.ts deleted file mode 100644 index 96fb29b9b9a..00000000000 --- a/packages/money-account-upgrade-controller/src/encode-caveat-terms.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Hex } from '@metamask/utils'; - -/** - * Encodes caveat terms by concatenating hex values (ABI-style), - * left-padding each value to 32 bytes. - * - * @param values - The hex values to pack. - * @returns The concatenated hex string. - */ -export function encodeCaveatTerms(...values: Hex[]): Hex { - return `0x${values.map((val) => val.slice(2).padStart(64, '0')).join('')}`; -} diff --git a/packages/money-account-upgrade-controller/src/index.ts b/packages/money-account-upgrade-controller/src/index.ts index c33134ae32f..f613b279ed9 100644 --- a/packages/money-account-upgrade-controller/src/index.ts +++ b/packages/money-account-upgrade-controller/src/index.ts @@ -1,9 +1,4 @@ -export type { - InitConfig, - UpgradeConfig, - UpgradeStep, - AccountUpgradeEntry, -} from './types'; +export type { InitConfig, UpgradeConfig } from './types'; export { MoneyAccountUpgradeController, controllerName, diff --git a/packages/money-account-upgrade-controller/src/steps/associate-address.test.ts b/packages/money-account-upgrade-controller/src/steps/associate-address.test.ts deleted file mode 100644 index 7bfed8971d3..00000000000 --- a/packages/money-account-upgrade-controller/src/steps/associate-address.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { Hex } from '@metamask/utils'; - -import { associateAddress } from './associate-address'; -import type { StepContext } from './types'; -import type { UpgradeConfig } from '../types'; - -const MOCK_ADDRESS = '0xabcdef1234567890abcdef1234567890abcdef12' as Hex; -const MOCK_CHAIN_ID = '0x1' as Hex; -const MOCK_SIGNATURE = '0xdeadbeef' as Hex; - -type Handlers = { - 'KeyringController:signPersonalMessage'?: jest.Mock; - 'ChompApiService:associateAddress'?: jest.Mock; -}; - -function buildContext(handlers: Handlers = {}): { - context: StepContext; - call: jest.Mock; -} { - const call = jest.fn((action: string, ...args: unknown[]) => { - const handler = handlers[action as keyof Handlers]; - if (!handler) { - throw new Error(`No handler registered for ${action}`); - } - return handler(...args); - }); - const context: StepContext = { - messenger: { call } as unknown as StepContext['messenger'], - config: {} as UpgradeConfig, - address: MOCK_ADDRESS, - chainId: MOCK_CHAIN_ID, - entry: undefined, - }; - return { context, call }; -} - -describe('associateAddress', () => { - it('signs a timestamped CHOMP Authentication message', async () => { - const signPersonalMessage = jest.fn().mockResolvedValue(MOCK_SIGNATURE); - const { context } = buildContext({ - 'KeyringController:signPersonalMessage': signPersonalMessage, - 'ChompApiService:associateAddress': jest.fn().mockResolvedValue({}), - }); - - await associateAddress(context); - - expect(signPersonalMessage).toHaveBeenCalledWith({ - data: expect.stringMatching(/^CHOMP Authentication \d+$/u), - from: MOCK_ADDRESS, - }); - }); - - it('submits the signature, timestamp, and address to CHOMP', async () => { - const associate = jest.fn().mockResolvedValue({}); - const { context } = buildContext({ - 'KeyringController:signPersonalMessage': jest - .fn() - .mockResolvedValue(MOCK_SIGNATURE), - 'ChompApiService:associateAddress': associate, - }); - - await associateAddress(context); - - expect(associate).toHaveBeenCalledWith({ - signature: MOCK_SIGNATURE, - timestamp: expect.stringMatching(/^\d+$/u), - address: MOCK_ADDRESS, - }); - }); - - it('returns a patch marking associate-address complete', async () => { - const { context } = buildContext({ - 'KeyringController:signPersonalMessage': jest - .fn() - .mockResolvedValue(MOCK_SIGNATURE), - 'ChompApiService:associateAddress': jest.fn().mockResolvedValue({}), - }); - - expect(await associateAddress(context)).toStrictEqual({ - step: 'associate-address', - }); - }); - - it('propagates signing errors', async () => { - const { context } = buildContext({ - 'KeyringController:signPersonalMessage': jest - .fn() - .mockRejectedValue(new Error('signing failed')), - }); - - await expect(associateAddress(context)).rejects.toThrow('signing failed'); - }); -}); diff --git a/packages/money-account-upgrade-controller/src/steps/associate-address.ts b/packages/money-account-upgrade-controller/src/steps/associate-address.ts deleted file mode 100644 index 3671012020e..00000000000 --- a/packages/money-account-upgrade-controller/src/steps/associate-address.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Step } from './types'; - -/** - * Step 0: Associate the Money Account address with the user's CHOMP profile. - * - * Signs "CHOMP Authentication {timestamp}" via personal_sign and submits - * it to CHOMP. The API accepts 409 (already associated) as success. - * - * @param context - The step context provided by the controller. - * @param context.messenger - The controller messenger. - * @param context.address - The Money Account address to associate. - * @returns A patch marking `associate-address` as the completed step. - */ -export const associateAddress: Step = async ({ messenger, address }) => { - const timestamp = Date.now().toString(); - const message = `CHOMP Authentication ${timestamp}`; - - const signature = await messenger.call( - 'KeyringController:signPersonalMessage', - { data: message, from: address }, - ); - - await messenger.call('ChompApiService:associateAddress', { - signature, - timestamp, - address, - }); - - return { step: 'associate-address' }; -}; diff --git a/packages/money-account-upgrade-controller/src/steps/types.ts b/packages/money-account-upgrade-controller/src/steps/types.ts deleted file mode 100644 index e1f45221203..00000000000 --- a/packages/money-account-upgrade-controller/src/steps/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Hex } from '@metamask/utils'; - -import type { MoneyAccountUpgradeControllerMessenger } from '../MoneyAccountUpgradeController'; -import type { AccountUpgradeEntry, UpgradeConfig } from '../types'; - -/** - * Context passed to each upgrade step. - */ -export type StepContext = { - messenger: MoneyAccountUpgradeControllerMessenger; - config: UpgradeConfig; - address: Hex; - chainId: Hex; - entry: AccountUpgradeEntry | undefined; -}; - -/** - * Patch returned by a step, merged into the account's upgrade entry by - * the controller. The `step` field is mandatory so the controller can - * record progress. - */ -export type StepResult = Partial & - Pick; - -export type Step = (ctx: StepContext) => Promise; diff --git a/packages/money-account-upgrade-controller/src/types.ts b/packages/money-account-upgrade-controller/src/types.ts index 82705946594..c6a18dc179d 100644 --- a/packages/money-account-upgrade-controller/src/types.ts +++ b/packages/money-account-upgrade-controller/src/types.ts @@ -32,25 +32,3 @@ export type InitConfig = Pick< | 'redeemerEnforcer' | 'valueLteEnforcer' >; - -/** - * The discrete steps of the upgrade sequence, in order. - */ -export type UpgradeStep = - | 'associate-address' - | 'submit-authorization' - | 'verify-delegation' - | 'save-delegation' - | 'register-intents'; - -/** - * Persisted record tracking the progress of an individual account upgrade. - */ -export type AccountUpgradeEntry = { - /** The last successfully completed step. */ - step: UpgradeStep; - /** The chain the upgrade is targeting. */ - chainId: Hex; - /** The delegation hash returned by CHOMP after verify-delegation. */ - delegationHash?: string; -}; diff --git a/packages/transaction-pay-controller/src/utils/source-amounts.ts b/packages/transaction-pay-controller/src/utils/source-amounts.ts index a8dcd1e302b..311fccd1c46 100644 --- a/packages/transaction-pay-controller/src/utils/source-amounts.ts +++ b/packages/transaction-pay-controller/src/utils/source-amounts.ts @@ -1,4 +1,5 @@ import { TransactionType } from '@metamask/transaction-controller'; +import type { TransactionMeta } from '@metamask/transaction-controller/src/src'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; @@ -7,7 +8,6 @@ import type { TransactionPaymentToken, } from '..'; import { TransactionPayStrategy } from '..'; -import type { TransactionMeta } from '../../../transaction-controller/src'; import { ARBITRUM_USDC_ADDRESS, CHAIN_ID_ARBITRUM } from '../constants'; import { projectLogger } from '../logger'; import type { From 5d427e78dcca145a320c132f3a68c63da93fc5e9 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 17 Apr 2026 13:05:41 +0100 Subject: [PATCH 14/26] feat: add step abstraction to upgrade controller Introduce a Step type ({ name, run }) with a StepResult union ('already-done' | 'completed') and iterate an internal steps array from upgradeAccount. Errors from a step propagate and halt the sequence. Co-Authored-By: Claude Opus 4.7 --- .../src/MoneyAccountUpgradeController.ts | 12 +++++++++-- .../src/index.ts | 1 + .../src/step.ts | 20 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 packages/money-account-upgrade-controller/src/step.ts diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index 3559f628bd2..bc45ed7da93 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -9,6 +9,7 @@ import type { Messenger } from '@metamask/messenger'; import type { Hex } from '@metamask/utils'; import type { MoneyAccountUpgradeControllerMethodActions } from './MoneyAccountUpgradeController-method-action-types'; +import type { Step } from './step'; import type { InitConfig } from './types'; export const controllerName = 'MoneyAccountUpgradeController'; @@ -73,6 +74,8 @@ export class MoneyAccountUpgradeController extends BaseController< > { initialized: boolean; + readonly #steps: Step[] = []; + /** * Constructor for the MoneyAccountUpgradeController. * @@ -140,9 +143,14 @@ export class MoneyAccountUpgradeController extends BaseController< } /** - * Upgrades a Money Account. Currently a no-op. + * Runs each step in the upgrade sequence in order. A step that reports + * `'already-done'` is skipped without performing any action; a step that + * reports `'completed'` has performed its action. An error thrown by any + * step propagates and halts the sequence. */ async upgradeAccount(): Promise { - // No-op. + for (const step of this.#steps) { + await step.run(); + } } } diff --git a/packages/money-account-upgrade-controller/src/index.ts b/packages/money-account-upgrade-controller/src/index.ts index f613b279ed9..429d74d9ace 100644 --- a/packages/money-account-upgrade-controller/src/index.ts +++ b/packages/money-account-upgrade-controller/src/index.ts @@ -1,3 +1,4 @@ +export type { Step, StepResult } from './step'; export type { InitConfig, UpgradeConfig } from './types'; export { MoneyAccountUpgradeController, diff --git a/packages/money-account-upgrade-controller/src/step.ts b/packages/money-account-upgrade-controller/src/step.ts new file mode 100644 index 00000000000..37d5e93523c --- /dev/null +++ b/packages/money-account-upgrade-controller/src/step.ts @@ -0,0 +1,20 @@ +/** + * The outcome of running a single step in the Money Account upgrade sequence. + * + * - `'already-done'` — the step's remote check determined that no work was + * required; no action was taken. + * - `'completed'` — the step performed its action and is now done. + */ +export type StepResult = 'already-done' | 'completed'; + +/** + * A single step in the Money Account upgrade sequence. + * + * Each step is responsible for checking whether its action has already been + * applied (returning `'already-done'` if so) and otherwise performing the + * action and returning `'completed'`. + */ +export type Step = { + name: string; + run: () => Promise; +}; From 1e70bb385a02ae3df727113adab91f9e9d6c364b Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 17 Apr 2026 13:16:32 +0100 Subject: [PATCH 15/26] feat: add associate-address step to upgrade controller Implements step 0 of the Money Account upgrade: signs a fresh "CHOMP Authentication {timestamp}" message via KeyringController and submits it to ChompApiService.associateAddress. The endpoint is idempotent, so the step has no pre-check and always returns 'completed'. upgradeAccount now takes the account address as a parameter and passes a { messenger, address } context to each step. Co-Authored-By: Claude Opus 4.7 --- ...ntUpgradeController-method-action-types.ts | 7 +- .../src/MoneyAccountUpgradeController.test.ts | 49 ++++++- .../src/MoneyAccountUpgradeController.ts | 20 ++- .../src/associate-address.test.ts | 131 ++++++++++++++++++ .../src/associate-address.ts | 34 +++++ .../src/step.ts | 14 +- .../src/utils/source-amounts.ts | 2 +- 7 files changed, 244 insertions(+), 13 deletions(-) create mode 100644 packages/money-account-upgrade-controller/src/associate-address.test.ts create mode 100644 packages/money-account-upgrade-controller/src/associate-address.ts diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts index 30acd34bf74..ab45da1779d 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController-method-action-types.ts @@ -6,7 +6,12 @@ import type { MoneyAccountUpgradeController } from './MoneyAccountUpgradeController'; /** - * Upgrades a Money Account. Currently a no-op. + * Runs each step in the upgrade sequence in order. A step that reports + * `'already-done'` is skipped without performing any action; a step that + * reports `'completed'` has performed its action. An error thrown by any + * step propagates and halts the sequence. + * + * @param address - The Money Account address to upgrade. */ export type MoneyAccountUpgradeControllerUpgradeAccountAction = { type: `MoneyAccountUpgradeController:upgradeAccount`; diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts index a44dcaba379..768b46bd33a 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -14,6 +14,8 @@ import { import type { UpgradeConfig } from './types'; const MOCK_CHAIN_ID = '0x1' as Hex; +const MOCK_ACCOUNT_ADDRESS = + '0xabcdef1234567890abcdef1234567890abcdef12' as Hex; const MOCK_CONFIG: UpgradeConfig = { delegateAddress: '0x1111111111111111111111111111111111111111' as Hex, @@ -62,6 +64,8 @@ type RootMessenger = Messenger; type Mocks = { getServiceDetails: jest.Mock; + signPersonalMessage: jest.Mock; + associateAddress: jest.Mock; }; function setup({ @@ -80,6 +84,12 @@ function setup({ getServiceDetails: jest .fn() .mockResolvedValue(MOCK_SERVICE_DETAILS_RESPONSE), + signPersonalMessage: jest.fn().mockResolvedValue('0xdeadbeef'), + associateAddress: jest.fn().mockResolvedValue({ + profileId: 'profile-1', + address: MOCK_ACCOUNT_ADDRESS, + status: 'CREATED', + }), }; const rootMessenger = new Messenger({ @@ -90,6 +100,14 @@ function setup({ 'ChompApiService:getServiceDetails', mocks.getServiceDetails, ); + rootMessenger.registerActionHandler( + 'KeyringController:signPersonalMessage', + mocks.signPersonalMessage, + ); + rootMessenger.registerActionHandler( + 'ChompApiService:associateAddress', + mocks.associateAddress, + ); const messenger: MoneyAccountUpgradeControllerMessenger = new Messenger({ namespace: 'MoneyAccountUpgradeController', @@ -97,7 +115,11 @@ function setup({ }); rootMessenger.delegate({ - actions: ['ChompApiService:getServiceDetails'], + actions: [ + 'ChompApiService:getServiceDetails', + 'KeyringController:signPersonalMessage', + 'ChompApiService:associateAddress', + ], events: [], messenger, }); @@ -229,17 +251,24 @@ describe('MoneyAccountUpgradeController', () => { }); describe('upgradeAccount', () => { - it('resolves without doing anything', async () => { - const { controller } = setup(); + it('runs each step for the given address', async () => { + const { controller, mocks } = setup(); + + await controller.upgradeAccount(MOCK_ACCOUNT_ADDRESS); - expect(await controller.upgradeAccount()).toBeUndefined(); + expect(mocks.signPersonalMessage).toHaveBeenCalledWith( + expect.objectContaining({ from: MOCK_ACCOUNT_ADDRESS }), + ); + expect(mocks.associateAddress).toHaveBeenCalledWith( + expect.objectContaining({ address: MOCK_ACCOUNT_ADDRESS }), + ); }); it('does not mutate state', async () => { const { controller } = setup(); const stateBefore = controller.state; - await controller.upgradeAccount(); + await controller.upgradeAccount(MOCK_ACCOUNT_ADDRESS); expect(controller.state).toStrictEqual(stateBefore); }); @@ -250,8 +279,18 @@ describe('MoneyAccountUpgradeController', () => { expect( await rootMessenger.call( 'MoneyAccountUpgradeController:upgradeAccount', + MOCK_ACCOUNT_ADDRESS, ), ).toBeUndefined(); }); + + it('propagates errors thrown by a step', async () => { + const { controller, mocks } = setup(); + mocks.signPersonalMessage.mockRejectedValue(new Error('signing failed')); + + await expect( + controller.upgradeAccount(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow('signing failed'); + }); }); }); diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index bc45ed7da93..9e367e6383f 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -4,10 +4,15 @@ import type { StateMetadata, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; -import type { ChompApiServiceGetServiceDetailsAction } from '@metamask/chomp-api-service'; +import type { + ChompApiServiceAssociateAddressAction, + ChompApiServiceGetServiceDetailsAction, +} from '@metamask/chomp-api-service'; +import type { KeyringControllerSignPersonalMessageAction } from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; import type { Hex } from '@metamask/utils'; +import { associateAddressStep } from './associate-address'; import type { MoneyAccountUpgradeControllerMethodActions } from './MoneyAccountUpgradeController-method-action-types'; import type { Step } from './step'; import type { InitConfig } from './types'; @@ -45,7 +50,10 @@ export type MoneyAccountUpgradeControllerActions = | MoneyAccountUpgradeControllerGetStateAction | MoneyAccountUpgradeControllerMethodActions; -type AllowedActions = ChompApiServiceGetServiceDetailsAction; +type AllowedActions = + | ChompApiServiceAssociateAddressAction + | ChompApiServiceGetServiceDetailsAction + | KeyringControllerSignPersonalMessageAction; export type MoneyAccountUpgradeControllerStateChangeEvent = ControllerStateChangeEvent< @@ -74,7 +82,7 @@ export class MoneyAccountUpgradeController extends BaseController< > { initialized: boolean; - readonly #steps: Step[] = []; + readonly #steps: Step[] = [associateAddressStep]; /** * Constructor for the MoneyAccountUpgradeController. @@ -147,10 +155,12 @@ export class MoneyAccountUpgradeController extends BaseController< * `'already-done'` is skipped without performing any action; a step that * reports `'completed'` has performed its action. An error thrown by any * step propagates and halts the sequence. + * + * @param address - The Money Account address to upgrade. */ - async upgradeAccount(): Promise { + async upgradeAccount(address: Hex): Promise { for (const step of this.#steps) { - await step.run(); + await step.run({ messenger: this.messenger, address }); } } } diff --git a/packages/money-account-upgrade-controller/src/associate-address.test.ts b/packages/money-account-upgrade-controller/src/associate-address.test.ts new file mode 100644 index 00000000000..e91f45c4d0d --- /dev/null +++ b/packages/money-account-upgrade-controller/src/associate-address.test.ts @@ -0,0 +1,131 @@ +import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; +import type { + MockAnyNamespace, + MessengerActions, + MessengerEvents, +} from '@metamask/messenger'; +import type { Hex } from '@metamask/utils'; + +import { associateAddressStep } from './associate-address'; +import type { MoneyAccountUpgradeControllerMessenger } from './MoneyAccountUpgradeController'; + +const MOCK_ADDRESS = '0xabcdef1234567890abcdef1234567890abcdef12' as Hex; +const MOCK_SIGNATURE = '0xdeadbeefcafebabe'; +const MOCK_NOW = new Date('2026-04-17T12:00:00.000Z').getTime(); + +type AllActions = MessengerActions; +type AllEvents = MessengerEvents; + +type Mocks = { + signPersonalMessage: jest.Mock; + associateAddress: jest.Mock; +}; + +function setup(): { + messenger: MoneyAccountUpgradeControllerMessenger; + mocks: Mocks; +} { + const mocks: Mocks = { + signPersonalMessage: jest.fn().mockResolvedValue(MOCK_SIGNATURE), + associateAddress: jest.fn().mockResolvedValue({ + profileId: 'profile-1', + address: MOCK_ADDRESS, + status: 'CREATED', + }), + }; + + const rootMessenger = new Messenger({ + namespace: MOCK_ANY_NAMESPACE, + }); + rootMessenger.registerActionHandler( + 'KeyringController:signPersonalMessage', + mocks.signPersonalMessage, + ); + rootMessenger.registerActionHandler( + 'ChompApiService:associateAddress', + mocks.associateAddress, + ); + + const messenger: MoneyAccountUpgradeControllerMessenger = new Messenger({ + namespace: 'MoneyAccountUpgradeController', + parent: rootMessenger, + }); + rootMessenger.delegate({ + actions: [ + 'KeyringController:signPersonalMessage', + 'ChompApiService:associateAddress', + ], + events: [], + messenger, + }); + + return { messenger, mocks }; +} + +describe('associateAddressStep', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(MOCK_NOW); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('is named "associate-address"', () => { + expect(associateAddressStep.name).toBe('associate-address'); + }); + + it('signs the CHOMP Authentication message with the given address', async () => { + const { messenger, mocks } = setup(); + + await associateAddressStep.run({ messenger, address: MOCK_ADDRESS }); + + expect(mocks.signPersonalMessage).toHaveBeenCalledWith({ + data: `CHOMP Authentication ${MOCK_NOW}`, + from: MOCK_ADDRESS, + }); + }); + + it('submits the signature, timestamp, and address to the CHOMP API', async () => { + const { messenger, mocks } = setup(); + + await associateAddressStep.run({ messenger, address: MOCK_ADDRESS }); + + expect(mocks.associateAddress).toHaveBeenCalledWith({ + signature: MOCK_SIGNATURE, + timestamp: MOCK_NOW.toString(), + address: MOCK_ADDRESS, + }); + }); + + it('returns "completed"', async () => { + const { messenger } = setup(); + + const result = await associateAddressStep.run({ + messenger, + address: MOCK_ADDRESS, + }); + + expect(result).toBe('completed'); + }); + + it('propagates errors from signing and does not submit to the API', async () => { + const { messenger, mocks } = setup(); + mocks.signPersonalMessage.mockRejectedValue(new Error('signing failed')); + + await expect( + associateAddressStep.run({ messenger, address: MOCK_ADDRESS }), + ).rejects.toThrow('signing failed'); + expect(mocks.associateAddress).not.toHaveBeenCalled(); + }); + + it('propagates errors from the CHOMP API', async () => { + const { messenger, mocks } = setup(); + mocks.associateAddress.mockRejectedValue(new Error('api failed')); + + await expect( + associateAddressStep.run({ messenger, address: MOCK_ADDRESS }), + ).rejects.toThrow('api failed'); + }); +}); diff --git a/packages/money-account-upgrade-controller/src/associate-address.ts b/packages/money-account-upgrade-controller/src/associate-address.ts new file mode 100644 index 00000000000..f9994c6fc78 --- /dev/null +++ b/packages/money-account-upgrade-controller/src/associate-address.ts @@ -0,0 +1,34 @@ +import type { Hex } from '@metamask/utils'; + +import type { Step } from './step'; + +/** + * Associates the Money Account address with the user's CHOMP profile. + * + * Signs `CHOMP Authentication {timestamp}` (EIP-191) with the account's key + * and submits the signature to CHOMP, which verifies the timestamp is fresh, + * recovers the signer, and records the profile–address mapping. + * + * The CHOMP endpoint is idempotent, so this step always executes and has no + * pre-check. + */ +export const associateAddressStep: Step = { + name: 'associate-address', + async run({ messenger, address }) { + const timestamp = Date.now().toString(); + const message = `CHOMP Authentication ${timestamp}`; + + const signature = (await messenger.call( + 'KeyringController:signPersonalMessage', + { data: message, from: address }, + )) as Hex; + + await messenger.call('ChompApiService:associateAddress', { + signature, + timestamp, + address, + }); + + return 'completed'; + }, +}; diff --git a/packages/money-account-upgrade-controller/src/step.ts b/packages/money-account-upgrade-controller/src/step.ts index 37d5e93523c..588dbae59c8 100644 --- a/packages/money-account-upgrade-controller/src/step.ts +++ b/packages/money-account-upgrade-controller/src/step.ts @@ -1,3 +1,15 @@ +import type { Hex } from '@metamask/utils'; + +import type { MoneyAccountUpgradeControllerMessenger } from './MoneyAccountUpgradeController'; + +/** + * Context supplied to each step when it is run. + */ +export type StepContext = { + messenger: MoneyAccountUpgradeControllerMessenger; + address: Hex; +}; + /** * The outcome of running a single step in the Money Account upgrade sequence. * @@ -16,5 +28,5 @@ export type StepResult = 'already-done' | 'completed'; */ export type Step = { name: string; - run: () => Promise; + run: (context: StepContext) => Promise; }; diff --git a/packages/transaction-pay-controller/src/utils/source-amounts.ts b/packages/transaction-pay-controller/src/utils/source-amounts.ts index 311fccd1c46..a8dcd1e302b 100644 --- a/packages/transaction-pay-controller/src/utils/source-amounts.ts +++ b/packages/transaction-pay-controller/src/utils/source-amounts.ts @@ -1,5 +1,4 @@ import { TransactionType } from '@metamask/transaction-controller'; -import type { TransactionMeta } from '@metamask/transaction-controller/src/src'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; @@ -8,6 +7,7 @@ import type { TransactionPaymentToken, } from '..'; import { TransactionPayStrategy } from '..'; +import type { TransactionMeta } from '../../../transaction-controller/src'; import { ARBITRUM_USDC_ADDRESS, CHAIN_ID_ARBITRUM } from '../constants'; import { projectLogger } from '../logger'; import type { From 2aac32e65ed95eccdaf40ddbc9e8b4099661cdc7 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 17 Apr 2026 14:56:56 +0100 Subject: [PATCH 16/26] chore: update package version --- packages/money-account-upgrade-controller/package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json index af3102a0f7b..4ae05862898 100644 --- a/packages/money-account-upgrade-controller/package.json +++ b/packages/money-account-upgrade-controller/package.json @@ -61,7 +61,7 @@ "@metamask/money-account-controller": "^0.1.0" }, "devDependencies": { - "@metamask/auto-changelog": "^6.0.0", + "@metamask/auto-changelog": "^6.1.0", "@metamask/utils": "^11.9.0", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", diff --git a/yarn.lock b/yarn.lock index edad0aeca48..38a73b75e84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2920,7 +2920,7 @@ __metadata: languageName: node linkType: hard -"@metamask/auto-changelog@npm:^6.0.0, @metamask/auto-changelog@npm:^6.1.0": +"@metamask/auto-changelog@npm:^6.1.0": version: 6.1.0 resolution: "@metamask/auto-changelog@npm:6.1.0" dependencies: @@ -4488,8 +4488,8 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/money-account-upgrade-controller@workspace:packages/money-account-upgrade-controller" dependencies: - "@metamask/auto-changelog": "npm:^6.0.0" - "@metamask/base-controller": "npm:^9.0.1" + "@metamask/auto-changelog": "npm:^6.1.0" + "@metamask/base-controller": "npm:^9.1.0" "@metamask/chomp-api-service": "npm:^0.0.0" "@metamask/delegation-controller": "npm:^3.0.0" "@metamask/keyring-controller": "npm:^25.2.0" From 51aa0805721de34d4038f60cbff233bb96690317 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 20 Apr 2026 09:47:07 +0100 Subject: [PATCH 17/26] chore: bump chomp api to v1 --- packages/money-account-upgrade-controller/package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json index 4ae05862898..b2d33ae359a 100644 --- a/packages/money-account-upgrade-controller/package.json +++ b/packages/money-account-upgrade-controller/package.json @@ -54,7 +54,7 @@ }, "dependencies": { "@metamask/base-controller": "^9.1.0", - "@metamask/chomp-api-service": "^0.0.0", + "@metamask/chomp-api-service": "^1.0.0", "@metamask/delegation-controller": "^3.0.0", "@metamask/keyring-controller": "^25.2.0", "@metamask/messenger": "^1.1.1", diff --git a/yarn.lock b/yarn.lock index 38a73b75e84..51613566f9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3119,7 +3119,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/chomp-api-service@npm:^0.0.0, @metamask/chomp-api-service@workspace:packages/chomp-api-service": +"@metamask/chomp-api-service@npm:^1.0.0, @metamask/chomp-api-service@workspace:packages/chomp-api-service": version: 0.0.0-use.local resolution: "@metamask/chomp-api-service@workspace:packages/chomp-api-service" dependencies: @@ -4490,7 +4490,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/chomp-api-service": "npm:^0.0.0" + "@metamask/chomp-api-service": "npm:^1.0.0" "@metamask/delegation-controller": "npm:^3.0.0" "@metamask/keyring-controller": "npm:^25.2.0" "@metamask/messenger": "npm:^1.1.1" From d6e28e02c0e0eed3610a90b21fa863416e90be83 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 20 Apr 2026 09:50:03 +0100 Subject: [PATCH 18/26] fix: enforce that upgrade controller is initialised --- .../src/MoneyAccountUpgradeController.test.ts | 33 ++++++++++++++++++- .../src/MoneyAccountUpgradeController.ts | 6 ++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts index 768b46bd33a..075a16cc38f 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -251,8 +251,36 @@ describe('MoneyAccountUpgradeController', () => { }); describe('upgradeAccount', () => { + it('throws when called before init', async () => { + const { controller } = setup(); + + await expect( + controller.upgradeAccount(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow( + 'MoneyAccountUpgradeController must be initialized via init() before upgradeAccount() can be called', + ); + }); + + it('throws when a previous init attempt failed', async () => { + const { controller, mocks } = setup(); + mocks.getServiceDetails.mockResolvedValueOnce({ + auth: { message: 'CHOMP Authentication' }, + chains: {}, + }); + await expect( + controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG), + ).rejects.toThrow(); + + await expect( + controller.upgradeAccount(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow( + 'MoneyAccountUpgradeController must be initialized via init() before upgradeAccount() can be called', + ); + }); + it('runs each step for the given address', async () => { const { controller, mocks } = setup(); + await controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG); await controller.upgradeAccount(MOCK_ACCOUNT_ADDRESS); @@ -266,6 +294,7 @@ describe('MoneyAccountUpgradeController', () => { it('does not mutate state', async () => { const { controller } = setup(); + await controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG); const stateBefore = controller.state; await controller.upgradeAccount(MOCK_ACCOUNT_ADDRESS); @@ -274,7 +303,8 @@ describe('MoneyAccountUpgradeController', () => { }); it('is callable via the messenger', async () => { - const { rootMessenger } = setup(); + const { controller, rootMessenger } = setup(); + await controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG); expect( await rootMessenger.call( @@ -286,6 +316,7 @@ describe('MoneyAccountUpgradeController', () => { it('propagates errors thrown by a step', async () => { const { controller, mocks } = setup(); + await controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG); mocks.signPersonalMessage.mockRejectedValue(new Error('signing failed')); await expect( diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index 9e367e6383f..fd13193f3a3 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -159,6 +159,12 @@ export class MoneyAccountUpgradeController extends BaseController< * @param address - The Money Account address to upgrade. */ async upgradeAccount(address: Hex): Promise { + if (!this.initialized) { + throw new Error( + 'MoneyAccountUpgradeController must be initialized via init() before upgradeAccount() can be called', + ); + } + for (const step of this.#steps) { await step.run({ messenger: this.messenger, address }); } From b56ec109cf9c760c40a218e3740a89a11b0f46ec Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 20 Apr 2026 09:50:38 +0100 Subject: [PATCH 19/26] chore: update readme content --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index bb674647ea2..5fd249dfb88 100644 --- a/README.md +++ b/README.md @@ -373,7 +373,6 @@ linkStyle default opacity:0.5 money_account_upgrade_controller --> chomp_api_service; money_account_upgrade_controller --> keyring_controller; money_account_upgrade_controller --> messenger; - money_account_upgrade_controller --> money_account_controller; multichain_account_service --> accounts_controller; multichain_account_service --> base_controller; multichain_account_service --> keyring_controller; From 9f11833fa17857d5d9ac634538ae7c635580a86f Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 20 Apr 2026 11:07:10 +0100 Subject: [PATCH 20/26] feat: continue in the case of 409 for associate address --- .../src/associate-address.test.ts | 20 +++++++++++++++++-- .../src/associate-address.ts | 14 ++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/money-account-upgrade-controller/src/associate-address.test.ts b/packages/money-account-upgrade-controller/src/associate-address.test.ts index e91f45c4d0d..2b58e41a91e 100644 --- a/packages/money-account-upgrade-controller/src/associate-address.test.ts +++ b/packages/money-account-upgrade-controller/src/associate-address.test.ts @@ -30,7 +30,7 @@ function setup(): { associateAddress: jest.fn().mockResolvedValue({ profileId: 'profile-1', address: MOCK_ADDRESS, - status: 'CREATED', + status: 'created', }), }; @@ -99,7 +99,7 @@ describe('associateAddressStep', () => { }); }); - it('returns "completed"', async () => { + it('returns "completed" when CHOMP creates the association', async () => { const { messenger } = setup(); const result = await associateAddressStep.run({ @@ -110,6 +110,22 @@ describe('associateAddressStep', () => { expect(result).toBe('completed'); }); + it('returns "already-done" when CHOMP responds with already_associated (409)', async () => { + const { messenger, mocks } = setup(); + mocks.associateAddress.mockResolvedValue({ + profileId: 'profile-1', + address: MOCK_ADDRESS, + status: 'already_associated', + }); + + const result = await associateAddressStep.run({ + messenger, + address: MOCK_ADDRESS, + }); + + expect(result).toBe('already-done'); + }); + it('propagates errors from signing and does not submit to the API', async () => { const { messenger, mocks } = setup(); mocks.signPersonalMessage.mockRejectedValue(new Error('signing failed')); diff --git a/packages/money-account-upgrade-controller/src/associate-address.ts b/packages/money-account-upgrade-controller/src/associate-address.ts index f9994c6fc78..3161fee73fd 100644 --- a/packages/money-account-upgrade-controller/src/associate-address.ts +++ b/packages/money-account-upgrade-controller/src/associate-address.ts @@ -2,6 +2,8 @@ import type { Hex } from '@metamask/utils'; import type { Step } from './step'; +const ALREADY_ASSOCIATED_STATUS = 'already_associated'; + /** * Associates the Money Account address with the user's CHOMP profile. * @@ -9,8 +11,10 @@ import type { Step } from './step'; * and submits the signature to CHOMP, which verifies the timestamp is fresh, * recovers the signer, and records the profile–address mapping. * - * The CHOMP endpoint is idempotent, so this step always executes and has no - * pre-check. + * CHOMP responds with 201 and `status: 'created'` when the association is + * made, and 409 with `status: 'already_associated'` when the address is + * already linked to a profile. The service surfaces both responses, so this + * step reports `'already-done'` for the 409 case and `'completed'` otherwise. */ export const associateAddressStep: Step = { name: 'associate-address', @@ -23,12 +27,16 @@ export const associateAddressStep: Step = { { data: message, from: address }, )) as Hex; - await messenger.call('ChompApiService:associateAddress', { + const response = await messenger.call('ChompApiService:associateAddress', { signature, timestamp, address, }); + if (response.status === ALREADY_ASSOCIATED_STATUS) { + return 'already-done'; + } + return 'completed'; }, }; From ca4a2040894b9b7e40e6dbbe82e8445f7c9f51c5 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 20 Apr 2026 11:09:33 +0100 Subject: [PATCH 21/26] chore: remove currently un-used depedencies --- packages/money-account-upgrade-controller/package.json | 4 +--- yarn.lock | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json index b2d33ae359a..f7b45173da9 100644 --- a/packages/money-account-upgrade-controller/package.json +++ b/packages/money-account-upgrade-controller/package.json @@ -55,10 +55,8 @@ "dependencies": { "@metamask/base-controller": "^9.1.0", "@metamask/chomp-api-service": "^1.0.0", - "@metamask/delegation-controller": "^3.0.0", "@metamask/keyring-controller": "^25.2.0", - "@metamask/messenger": "^1.1.1", - "@metamask/money-account-controller": "^0.1.0" + "@metamask/messenger": "^1.1.1" }, "devDependencies": { "@metamask/auto-changelog": "^6.1.0", diff --git a/yarn.lock b/yarn.lock index 51613566f9e..cb79307000b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3430,7 +3430,7 @@ __metadata: languageName: node linkType: hard -"@metamask/delegation-controller@npm:^3.0.0, @metamask/delegation-controller@workspace:packages/delegation-controller": +"@metamask/delegation-controller@workspace:packages/delegation-controller": version: 0.0.0-use.local resolution: "@metamask/delegation-controller@workspace:packages/delegation-controller" dependencies: @@ -4458,7 +4458,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/money-account-controller@npm:^0.1.0, @metamask/money-account-controller@workspace:packages/money-account-controller": +"@metamask/money-account-controller@workspace:packages/money-account-controller": version: 0.0.0-use.local resolution: "@metamask/money-account-controller@workspace:packages/money-account-controller" dependencies: @@ -4491,10 +4491,8 @@ __metadata: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/chomp-api-service": "npm:^1.0.0" - "@metamask/delegation-controller": "npm:^3.0.0" "@metamask/keyring-controller": "npm:^25.2.0" "@metamask/messenger": "npm:^1.1.1" - "@metamask/money-account-controller": "npm:^0.1.0" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" From f338de1e10ae0259ab628e801fe0aec3c1aaea77 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 20 Apr 2026 12:19:55 +0100 Subject: [PATCH 22/26] chore: add error message to test --- .../src/MoneyAccountUpgradeController.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts index 075a16cc38f..0afdc6ca777 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -269,7 +269,7 @@ describe('MoneyAccountUpgradeController', () => { }); await expect( controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG), - ).rejects.toThrow(); + ).rejects.toThrow('Chain 0x1 not found in service details response'); await expect( controller.upgradeAccount(MOCK_ACCOUNT_ADDRESS), From 3b80144980297163ddab7efa843d38b80f70a560 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 20 Apr 2026 13:58:14 +0100 Subject: [PATCH 23/26] chore: remove extraneous tsconfig links --- packages/money-account-upgrade-controller/tsconfig.build.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/money-account-upgrade-controller/tsconfig.build.json b/packages/money-account-upgrade-controller/tsconfig.build.json index 1885ad844ba..acb5f8ad014 100644 --- a/packages/money-account-upgrade-controller/tsconfig.build.json +++ b/packages/money-account-upgrade-controller/tsconfig.build.json @@ -8,10 +8,8 @@ "references": [ { "path": "../base-controller/tsconfig.build.json" }, { "path": "../chomp-api-service/tsconfig.build.json" }, - { "path": "../delegation-controller/tsconfig.build.json" }, { "path": "../keyring-controller/tsconfig.build.json" }, - { "path": "../messenger/tsconfig.build.json" }, - { "path": "../money-account-controller/tsconfig.build.json" } + { "path": "../messenger/tsconfig.build.json" } ], "include": ["../../types", "./src"] } From d6441304d7efca78b81db15ebe4971dd4c9c8599 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 20 Apr 2026 15:56:14 +0100 Subject: [PATCH 24/26] chore: make initialise private --- .../src/MoneyAccountUpgradeController.test.ts | 20 +++---------------- .../src/MoneyAccountUpgradeController.ts | 8 ++++---- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts index 0afdc6ca777..194c5c9f9fb 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -158,10 +158,10 @@ describe('MoneyAccountUpgradeController', () => { ).toStrictEqual({ chainId: MOCK_CHAIN_ID }); }); - it('starts with initialized set to false', () => { - const { controller } = setup(); + it('Does not make async init calls when constructed', () => { + const { mocks } = setup(); - expect(controller.initialized).toBe(false); + expect(mocks.getServiceDetails).not.toHaveBeenCalled(); }); }); @@ -174,14 +174,6 @@ describe('MoneyAccountUpgradeController', () => { expect(mocks.getServiceDetails).toHaveBeenCalledWith([MOCK_CHAIN_ID]); }); - it('sets initialized to true after successful init', async () => { - const { controller } = setup(); - - await controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG); - - expect(controller.initialized).toBe(true); - }); - it('throws when the chain is not found in service details', async () => { const { controller, mocks } = setup(); @@ -195,8 +187,6 @@ describe('MoneyAccountUpgradeController', () => { ).rejects.toThrow( `Chain ${MOCK_CHAIN_ID} not found in service details response`, ); - - expect(controller.initialized).toBe(false); }); it('throws when vedaProtocol is not found', async () => { @@ -217,8 +207,6 @@ describe('MoneyAccountUpgradeController', () => { ).rejects.toThrow( `vedaProtocol not found for chain ${MOCK_CHAIN_ID} in service details response`, ); - - expect(controller.initialized).toBe(false); }); it('throws when supportedTokens is empty', async () => { @@ -245,8 +233,6 @@ describe('MoneyAccountUpgradeController', () => { ).rejects.toThrow( `No supported tokens found for vedaProtocol on chain ${MOCK_CHAIN_ID}`, ); - - expect(controller.initialized).toBe(false); }); }); diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index fd13193f3a3..32258a03f87 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -80,7 +80,7 @@ export class MoneyAccountUpgradeController extends BaseController< MoneyAccountUpgradeControllerState, MoneyAccountUpgradeControllerMessenger > { - initialized: boolean; + #initialized: boolean; readonly #steps: Step[] = [associateAddressStep]; @@ -108,7 +108,7 @@ export class MoneyAccountUpgradeController extends BaseController< }, }); - this.initialized = false; + this.#initialized = false; this.messenger.registerMethodActionHandlers( this, @@ -147,7 +147,7 @@ export class MoneyAccountUpgradeController extends BaseController< ); } - this.initialized = true; + this.#initialized = true; } /** @@ -159,7 +159,7 @@ export class MoneyAccountUpgradeController extends BaseController< * @param address - The Money Account address to upgrade. */ async upgradeAccount(address: Hex): Promise { - if (!this.initialized) { + if (!this.#initialized) { throw new Error( 'MoneyAccountUpgradeController must be initialized via init() before upgradeAccount() can be called', ); From 89c36c5e884ec0b4810f5dc0b99554174c531029 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 20 Apr 2026 16:23:11 +0100 Subject: [PATCH 25/26] feat: respond to pr feedback 1. use correct version 2. remove unused state 3. remove unused imports in tsconfig --- .../package.json | 2 +- .../src/MoneyAccountUpgradeController.test.ts | 50 ++----------------- .../src/MoneyAccountUpgradeController.ts | 38 ++++---------- .../src/index.ts | 8 +-- .../tsconfig.json | 4 +- 5 files changed, 16 insertions(+), 86 deletions(-) diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json index f7b45173da9..31a0d25ac94 100644 --- a/packages/money-account-upgrade-controller/package.json +++ b/packages/money-account-upgrade-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/money-account-upgrade-controller", - "version": "0.1.0", + "version": "0.0.0", "description": "MetaMask Money account upgrade controller", "keywords": [ "Ethereum", diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts index 194c5c9f9fb..b4133ef8318 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -7,10 +7,7 @@ import type { import type { Hex } from '@metamask/utils'; import type { MoneyAccountUpgradeControllerMessenger } from '.'; -import { - MoneyAccountUpgradeController, - getDefaultMoneyAccountUpgradeControllerState, -} from '.'; +import { MoneyAccountUpgradeController } from '.'; import type { UpgradeConfig } from './types'; const MOCK_CHAIN_ID = '0x1' as Hex; @@ -68,13 +65,7 @@ type Mocks = { associateAddress: jest.Mock; }; -function setup({ - state, -}: { - state?: Partial< - ReturnType - >; -} = {}): { +function setup(): { controller: MoneyAccountUpgradeController; rootMessenger: RootMessenger; messenger: MoneyAccountUpgradeControllerMessenger; @@ -126,7 +117,6 @@ function setup({ const controller = new MoneyAccountUpgradeController({ messenger, - state, }); return { controller, rootMessenger, messenger, mocks }; @@ -134,31 +124,7 @@ function setup({ describe('MoneyAccountUpgradeController', () => { describe('constructor', () => { - it('initializes with default state when no state is provided', () => { - const { controller } = setup(); - - expect(controller.state).toStrictEqual( - getDefaultMoneyAccountUpgradeControllerState(), - ); - }); - - it('accepts initial state', () => { - const { controller } = setup({ - state: { - upgrades: { - '0xabcdef1234567890abcdef1234567890abcdef12': { - chainId: MOCK_CHAIN_ID, - }, - }, - }, - }); - - expect( - controller.state.upgrades['0xabcdef1234567890abcdef1234567890abcdef12'], - ).toStrictEqual({ chainId: MOCK_CHAIN_ID }); - }); - - it('Does not make async init calls when constructed', () => { + it('does not make async init calls when constructed', () => { const { mocks } = setup(); expect(mocks.getServiceDetails).not.toHaveBeenCalled(); @@ -278,16 +244,6 @@ describe('MoneyAccountUpgradeController', () => { ); }); - it('does not mutate state', async () => { - const { controller } = setup(); - await controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG); - const stateBefore = controller.state; - - await controller.upgradeAccount(MOCK_ACCOUNT_ADDRESS); - - expect(controller.state).toStrictEqual(stateBefore); - }); - it('is callable via the messenger', async () => { const { controller, rootMessenger } = setup(); await controller.init(MOCK_CHAIN_ID, MOCK_INIT_CONFIG); diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index 32258a03f87..516104b249d 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -1,6 +1,6 @@ import type { ControllerGetStateAction, - ControllerStateChangeEvent, + ControllerStateChangedEvent, StateMetadata, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; @@ -19,24 +19,10 @@ import type { InitConfig } from './types'; export const controllerName = 'MoneyAccountUpgradeController'; -export type MoneyAccountUpgradeControllerState = { - upgrades: Record; -}; - -const moneyAccountUpgradeControllerMetadata = { - upgrades: { - includeInDebugSnapshot: false, - includeInStateLogs: false, - persist: true, - usedInUi: false, - }, -} satisfies StateMetadata; - -export function getDefaultMoneyAccountUpgradeControllerState(): MoneyAccountUpgradeControllerState { - return { - upgrades: {}, - }; -} +export type MoneyAccountUpgradeControllerState = Record; + +const moneyAccountUpgradeControllerMetadata = + {} satisfies StateMetadata; const MESSENGER_EXPOSED_METHODS = ['upgradeAccount'] as const; @@ -55,14 +41,14 @@ type AllowedActions = | ChompApiServiceGetServiceDetailsAction | KeyringControllerSignPersonalMessageAction; -export type MoneyAccountUpgradeControllerStateChangeEvent = - ControllerStateChangeEvent< +export type MoneyAccountUpgradeControllerStateChangedEvent = + ControllerStateChangedEvent< typeof controllerName, MoneyAccountUpgradeControllerState >; export type MoneyAccountUpgradeControllerEvents = - MoneyAccountUpgradeControllerStateChangeEvent; + MoneyAccountUpgradeControllerStateChangedEvent; type AllowedEvents = never; @@ -89,23 +75,17 @@ export class MoneyAccountUpgradeController extends BaseController< * * @param options - The options for constructing the controller. * @param options.messenger - The messenger to use for inter-controller communication. - * @param options.state - The initial state of the controller. */ constructor({ messenger, - state, }: { messenger: MoneyAccountUpgradeControllerMessenger; - state?: Partial; }) { super({ messenger, metadata: moneyAccountUpgradeControllerMetadata, name: controllerName, - state: { - ...getDefaultMoneyAccountUpgradeControllerState(), - ...state, - }, + state: {}, }); this.#initialized = false; diff --git a/packages/money-account-upgrade-controller/src/index.ts b/packages/money-account-upgrade-controller/src/index.ts index 429d74d9ace..8c3d79a6d18 100644 --- a/packages/money-account-upgrade-controller/src/index.ts +++ b/packages/money-account-upgrade-controller/src/index.ts @@ -1,15 +1,11 @@ export type { Step, StepResult } from './step'; export type { InitConfig, UpgradeConfig } from './types'; -export { - MoneyAccountUpgradeController, - controllerName, - getDefaultMoneyAccountUpgradeControllerState, -} from './MoneyAccountUpgradeController'; +export { MoneyAccountUpgradeController } from './MoneyAccountUpgradeController'; export type { MoneyAccountUpgradeControllerState, MoneyAccountUpgradeControllerGetStateAction, MoneyAccountUpgradeControllerActions, - MoneyAccountUpgradeControllerStateChangeEvent, + MoneyAccountUpgradeControllerStateChangedEvent, MoneyAccountUpgradeControllerEvents, MoneyAccountUpgradeControllerMessenger, } from './MoneyAccountUpgradeController'; diff --git a/packages/money-account-upgrade-controller/tsconfig.json b/packages/money-account-upgrade-controller/tsconfig.json index 84f2d5918d3..9bd846f6b48 100644 --- a/packages/money-account-upgrade-controller/tsconfig.json +++ b/packages/money-account-upgrade-controller/tsconfig.json @@ -6,10 +6,8 @@ "references": [ { "path": "../base-controller" }, { "path": "../chomp-api-service" }, - { "path": "../delegation-controller" }, { "path": "../keyring-controller" }, - { "path": "../messenger" }, - { "path": "../money-account-controller" } + { "path": "../messenger" } ], "include": ["../../types", "./src"] } From c4311aad3dbd8a39e47d544ff16d50b845e34fab Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 20 Apr 2026 18:14:43 +0100 Subject: [PATCH 26/26] fix: add money upgrade package to tsconfig --- tsconfig.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tsconfig.json b/tsconfig.json index 9311cfcfeb8..641b9b3c7ee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -143,6 +143,9 @@ { "path": "./packages/money-account-controller" }, + { + "path": "./packages/money-account-upgrade-controller" + }, { "path": "./packages/multichain-account-service" },