From 5e7a8047c869a04156c3d1693479b3517867b41d Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Thu, 7 May 2026 13:12:50 -0400 Subject: [PATCH 01/15] feat: replacing constructor config object with remote feature flag subscription; first pass --- .../CHANGELOG.md | 7 + .../package.json | 1 + .../src/constants.ts | 8 + .../src/errors.ts | 31 +++ .../src/index.ts | 5 + .../src/money-account-balance-service.ts | 226 +++++++++++++----- .../src/structs.ts | 15 ++ .../src/types.ts | 13 + .../tsconfig.build.json | 3 +- .../tsconfig.json | 3 +- 10 files changed, 249 insertions(+), 63 deletions(-) create mode 100644 packages/money-account-balance-service/src/types.ts diff --git a/packages/money-account-balance-service/CHANGELOG.md b/packages/money-account-balance-service/CHANGELOG.md index 5b1de0d1f6..482766601e 100644 --- a/packages/money-account-balance-service/CHANGELOG.md +++ b/packages/money-account-balance-service/CHANGELOG.md @@ -7,8 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `VaultConfigNotAvailableError` and `VaultConfigValidationError` error classes for typed consumer error handling ([#TODO](https://github.com/MetaMask/core/pull/TODO)) +- `MoneyAccountBalanceService` now subscribes to `RemoteFeatureFlagController:stateChange` and invalidates all cached queries when vault config changes ([#TODO](https://github.com/MetaMask/core/pull/TODO)) + ### Changed +- **BREAKING:** `MoneyAccountBalanceService` no longer accepts vault config via constructor (`vaultAddress`, `vaultChainId`, `accountantAddress`, `underlyingTokenAddress`, `underlyingTokenDecimals`). Vault config is now read from remote feature flag via `RemoteFeatureFlagController`. The service requires `RemoteFeatureFlagController` to be registered on the messenger. Methods throw `VaultConfigNotAvailableError` until a valid config has been loaded from remote flags. ([#TODO](https://github.com/MetaMask/core/pull/TODO)) + - Add `@metamask/remote-feature-flag-controller` as a dependency and ensure `RemoteFeatureFlagController:getState` and `RemoteFeatureFlagController:stateChange` are permitted on the messenger passed to `MoneyAccountBalanceService`. - Bump `@metamask/messenger` from `^1.1.1` to `^1.2.0` ([#8632](https://github.com/MetaMask/core/pull/8632)) - Bump `@metamask/network-controller` from `^30.0.1` to `^30.1.0` ([#8636](https://github.com/MetaMask/core/pull/8636)) diff --git a/packages/money-account-balance-service/package.json b/packages/money-account-balance-service/package.json index 42f4d43f28..c8dac58a5f 100644 --- a/packages/money-account-balance-service/package.json +++ b/packages/money-account-balance-service/package.json @@ -60,6 +60,7 @@ "@metamask/messenger": "^1.2.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/network-controller": "^30.1.0", + "@metamask/remote-feature-flag-controller": "^4.2.0", "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^11.9.0" }, diff --git a/packages/money-account-balance-service/src/constants.ts b/packages/money-account-balance-service/src/constants.ts index 2ea80a1495..93a2cf57d0 100644 --- a/packages/money-account-balance-service/src/constants.ts +++ b/packages/money-account-balance-service/src/constants.ts @@ -2,6 +2,14 @@ import { Hex } from '@metamask/utils'; export const VEDA_PERFORMANCE_API_BASE_URL = 'https://api.sevenseas.capital'; +/** + * The key under which vault config is stored in + * `RemoteFeatureFlagController` state's `remoteFeatureFlags` map. + */ +// TOOD: Update this to the actual flag key when it is available. +export const VAULT_CONFIG_FEATURE_FLAG_KEY = 'moneyVaultConfig'; + +// TODO: Add Monad network name export const VEDA_API_NETWORK_NAMES: Record = { '0xa4b1': 'arbitrum', }; diff --git a/packages/money-account-balance-service/src/errors.ts b/packages/money-account-balance-service/src/errors.ts index d0de4dc0b2..329d6c1058 100644 --- a/packages/money-account-balance-service/src/errors.ts +++ b/packages/money-account-balance-service/src/errors.ts @@ -4,3 +4,34 @@ export class VedaResponseValidationError extends Error { this.name = 'VedaResponseValidationError'; } } + +/** + * Thrown when a public method is called but vault config has not yet been + * loaded from RemoteFeatureFlagController, or the flag key is absent. + * This is a transient condition — the service will recover once flags are + * fetched and a valid config arrives. + */ +export class VaultConfigNotAvailableError extends Error { + constructor() { + super( + 'MoneyAccountBalanceService: vault config is not available. ' + + 'RemoteFeatureFlagController may not have fetched flags yet.', + ); + this.name = 'VaultConfigNotAvailableError'; + } +} + +/** + * Thrown when the vault config flag value is present but fails superstruct + * validation. This surfaces to error monitoring service (e.g. Sentry) via the messenger's captureException + * handler. + */ +export class VaultConfigValidationError extends Error { + constructor(message?: string) { + super( + message ?? + 'MoneyAccountBalanceService: vault config from remote feature flags is malformed.', + ); + this.name = 'VaultConfigValidationError'; + } +} diff --git a/packages/money-account-balance-service/src/index.ts b/packages/money-account-balance-service/src/index.ts index 4b647515f4..d1d6fcef80 100644 --- a/packages/money-account-balance-service/src/index.ts +++ b/packages/money-account-balance-service/src/index.ts @@ -16,3 +16,8 @@ export type { MusdEquivalentValueResponse, NormalizedVaultApyResponse, } from './response.types'; +export { + VaultConfigNotAvailableError, + VaultConfigValidationError, +} from './errors'; +export type { VaultConfig } from './types'; diff --git a/packages/money-account-balance-service/src/money-account-balance-service.ts b/packages/money-account-balance-service/src/money-account-balance-service.ts index b23ab8ea2b..391a67b117 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.ts @@ -14,17 +14,26 @@ import type { NetworkControllerGetNetworkClientByIdAction, NetworkControllerGetNetworkConfigurationByChainIdAction, } from '@metamask/network-controller'; -import { is } from '@metamask/superstruct'; -import type { Hex } from '@metamask/utils'; +import type { + RemoteFeatureFlagControllerGetStateAction, + RemoteFeatureFlagControllerStateChangeEvent, +} from '@metamask/remote-feature-flag-controller'; +import { assert, is } from '@metamask/superstruct'; +import type { Hex, Json } from '@metamask/utils'; import { Duration, inMilliseconds } from '@metamask/utils'; import { ACCOUNTANT_ABI, DEFAULT_VEDA_API_NETWORK_NAME, + VAULT_CONFIG_FEATURE_FLAG_KEY, VEDA_API_NETWORK_NAMES, VEDA_PERFORMANCE_API_BASE_URL, } from './constants'; -import { VedaResponseValidationError } from './errors'; +import { + VaultConfigNotAvailableError, + VaultConfigValidationError, + VedaResponseValidationError, +} from './errors'; import type { MoneyAccountBalanceServiceMethodActions } from './money-account-balance-service-method-action-types'; import { normalizeVaultApyResponse } from './requestNormalization'; import type { @@ -32,7 +41,8 @@ import type { MusdEquivalentValueResponse, NormalizedVaultApyResponse, } from './response.types'; -import { VaultApyRawResponseStruct } from './structs'; +import type { VaultConfig } from './types'; +import { VaultApyRawResponseStruct, VaultConfigStruct } from './structs'; // === GENERAL === @@ -70,7 +80,8 @@ export type MoneyAccountBalanceServiceActions = */ type AllowedActions = | NetworkControllerGetNetworkConfigurationByChainIdAction - | NetworkControllerGetNetworkClientByIdAction; + | NetworkControllerGetNetworkClientByIdAction + | RemoteFeatureFlagControllerGetStateAction; /** * Published when {@link MoneyAccountBalanceService}'s cache is updated. @@ -96,7 +107,7 @@ export type MoneyAccountBalanceServiceEvents = * Events from other messengers that {@link MoneyAccountBalanceService} * subscribes to. */ -type AllowedEvents = never; +type AllowedEvents = RemoteFeatureFlagControllerStateChangeEvent; /** * The messenger which is restricted to actions and events accessed by @@ -119,16 +130,16 @@ export type MoneyAccountBalanceServiceMessenger = Messenger< * {@link BaseDataService}) and protected by a service policy that provides * automatic retries and circuit-breaking. * + * Vault configuration (addresses, chain ID, decimals) is read from the + * remote feature flag via {@link RemoteFeatureFlagControllerGetStateAction}. + * Methods throw {@link VaultConfigNotAvailableError} until flags have been fetched and a + * valid config is present. + * * @example * * ```ts * const service = new MoneyAccountBalanceService({ * messenger: moneyAccountBalanceServiceMessenger, - * vaultAddress: '0x...', - * vaultChainId: '0xa4b1', - * accountantAddress: '0x...', - * underlyingTokenAddress: '0x...', - * underlyingTokenDecimals: 6, * }); * * const { balance } = await service.getMusdBalance('0xYourMoneyAccount...'); @@ -137,11 +148,6 @@ export type MoneyAccountBalanceServiceMessenger = Messenger< type MoneyAccountBalanceServiceOptions = { messenger: MoneyAccountBalanceServiceMessenger; - vaultAddress: Hex; - vaultChainId: Hex; - accountantAddress: Hex; - underlyingTokenAddress: Hex; - underlyingTokenDecimals: number; policyOptions?: CreateServicePolicyOptions; }; @@ -149,37 +155,17 @@ export class MoneyAccountBalanceService extends BaseDataService< typeof serviceName, MoneyAccountBalanceServiceMessenger > { - readonly #networkName: string; - - readonly #vaultAddress: Hex; - - readonly #vaultChainId: Hex; - - readonly #accountantAddress: Hex; - - readonly #underlyingTokenAddress: Hex; - - readonly #underlyingTokenDecimals: number; + #vaultConfig: VaultConfig | undefined; /** * Constructs a new MoneyAccountBalanceService. * * @param args - The constructor arguments. * @param args.messenger - The messenger suited for this service. - * @param args.vaultAddress - The address of the Veda vault (e.g. musdSHFvd token contract). - * @param args.vaultChainId - The chain ID of the Veda vault. - * @param args.accountantAddress - The address of the Veda Accountant contract. - * @param args.underlyingTokenAddress - The address of the underlying token (e.g. mUSD). Must be on the same chain as the vault. - * @param args.underlyingTokenDecimals - The decimals of the underlying token. - * @param args.policyOptions - Options to pass to `createServicePolicy`, + * @param args.policyOptions - Options to pass to `createServicePolicy`. */ constructor({ messenger, - vaultAddress, - vaultChainId, - accountantAddress, - underlyingTokenAddress, - underlyingTokenDecimals, policyOptions = {}, }: MoneyAccountBalanceServiceOptions) { super({ @@ -193,15 +179,32 @@ export class MoneyAccountBalanceService extends BaseDataService< }, }); - this.#vaultAddress = vaultAddress; - this.#vaultChainId = vaultChainId; - this.#accountantAddress = accountantAddress; - this.#underlyingTokenAddress = underlyingTokenAddress; - this.#underlyingTokenDecimals = underlyingTokenDecimals; + this.messenger.subscribe( + // TODO: Add to eslint exceptions + 'RemoteFeatureFlagController:stateChange', + (state) => { + const flagValue = + state.remoteFeatureFlags[VAULT_CONFIG_FEATURE_FLAG_KEY]; + this.#onRemoteFeatureFlagChange(flagValue); + }, + ); - this.#networkName = - VEDA_API_NETWORK_NAMES[this.#vaultChainId] ?? - DEFAULT_VEDA_API_NETWORK_NAME; + // Eagerly read already-loaded flags. Wrapped in try/catch because + // RemoteFeatureFlagController may not be registered yet at construction + // time. Validation errors are also swallowed here — the service degrades + // gracefully and fails loud on the first method call. + try { + // TODO: Fix by moving to init() method according to the controller guidelines. + const { remoteFeatureFlags } = this.messenger.call( + 'RemoteFeatureFlagController:getState', + ); + const flagValue = remoteFeatureFlags[VAULT_CONFIG_FEATURE_FLAG_KEY]; + if (flagValue !== undefined) { + this.#vaultConfig = this.#parseAndValidateVaultConfig(flagValue); + } + } catch { + // RFFC not registered or flag is malformed — stay undefined. + } this.messenger.registerMethodActionHandlers( this, @@ -210,22 +213,100 @@ export class MoneyAccountBalanceService extends BaseDataService< } /** - * Resolves a Web3Provider for {@link MoneyAccountBalanceServiceOptions.vaultChainId} by looking up the - * network configuration and client via the messenger. + * Returns the current vault config, or throws {@link VaultConfigNotAvailableError} + * if it has not been loaded yet. + */ + #requireConfig(): VaultConfig { + if (!this.#vaultConfig) { + throw new VaultConfigNotAvailableError(); + } + return this.#vaultConfig; + } + + /** + * Called on every `RemoteFeatureFlagController:stateChange` event. + * Validates the flag value, updates `#vaultConfig`, and invalidates all + * cached queries when the config changes. + * + * Throws {@link VaultConfigValidationError} when the flag value is malformed. + * The messenger catches throws from event subscribers and routes them to + * `captureException` (Sentry) — the error does NOT propagate to the + * stateChange publisher. + * + * @param flagValue - The raw flag value from `remoteFeatureFlags`. + */ + #onRemoteFeatureFlagChange(flagValue: Json | undefined): void { + const hadConfig = this.#vaultConfig !== undefined; + + if (flagValue === undefined) { + // Flag key absent — treat as "not loaded". + if (hadConfig) { + this.#vaultConfig = undefined; + this.invalidateQueries(); + } + return; + } + + let newConfig: VaultConfig; + try { + newConfig = this.#parseAndValidateVaultConfig(flagValue); + } catch (error) { + // Clear previously valid config and purge stale cache. + if (hadConfig) { + this.#vaultConfig = undefined; + this.invalidateQueries(); + } + throw error; + } + + if (JSON.stringify(newConfig) === JSON.stringify(this.#vaultConfig)) { + return; + } + + this.#vaultConfig = newConfig; + if (hadConfig) { + this.invalidateQueries(); + } + } + + /** + * Validates `flagValue` against {@link VaultConfigStruct} and returns it + * cast as {@link VaultConfig}. * - * @returns A Web3Provider connected to the vault chain. - * @throws If no network configuration exists for the vault chain, or if the + * @param flagValue - The raw JSON value from the feature flag. + * @returns The validated vault config. + * @throws {@link VaultConfigValidationError} if the value does not match the + * expected shape. + */ + #parseAndValidateVaultConfig(flagValue: Json): VaultConfig { + try { + assert(flagValue, VaultConfigStruct); + } catch (error) { + throw new VaultConfigValidationError( + error instanceof Error ? error.message : undefined, + ); + } + return flagValue as unknown as VaultConfig; + } + + /** + * Resolves a Web3Provider for the given chain ID by looking up the network + * configuration and client via the messenger. + * + * @param vaultChainId - The chain ID to resolve a provider for. + * @returns A Web3Provider connected to the given chain. + * @throws If no network configuration exists for the chain, or if the * resolved network client has no provider. */ - #getProvider(): Web3Provider { + #getProvider(vaultChainId: Hex): Web3Provider { const config = this.messenger.call( 'NetworkController:getNetworkConfigurationByChainId', - this.#vaultChainId, + vaultChainId, ); if (!config) { throw new Error( - `No network configuration found for chain ${this.#vaultChainId}`, + `No network configuration found for chain ${vaultChainId}`, ); } @@ -238,7 +319,7 @@ export class MoneyAccountBalanceService extends BaseDataService< ); if (!networkClient?.provider) { - throw new Error(`No provider found for chain ${this.#vaultChainId}`); + throw new Error(`No provider found for chain ${vaultChainId}`); } return new Web3Provider(networkClient.provider); @@ -249,13 +330,15 @@ export class MoneyAccountBalanceService extends BaseDataService< * * @param contractAddress - The address of the ERC-20 contract. * @param accountAddress - The address of the account. + * @param vaultChainId - The chain ID to use for the provider. * @returns The balance as a raw uint256 string. */ async #fetchErc20Balance( contractAddress: Hex, accountAddress: Hex, + vaultChainId: Hex, ): Promise { - const provider = this.#getProvider(); + const provider = this.#getProvider(vaultChainId); const contract = new Contract(contractAddress, abiERC20, provider); const balance = await contract.balanceOf(accountAddress); return balance.toString(); @@ -266,14 +349,17 @@ export class MoneyAccountBalanceService extends BaseDataService< * * @param accountAddress - The Money account's Ethereum address. * @returns The mUSD balance as a raw uint256 string. + * @throws {@link VaultConfigNotAvailableError} if vault config has not been loaded. */ async getMusdBalance(accountAddress: Hex): Promise<{ balance: string }> { return this.fetchQuery({ queryKey: [`${this.name}:getMusdBalance`, accountAddress], queryFn: async () => { + const { underlyingTokenAddress, vaultChainId } = this.#requireConfig(); const balance = await this.#fetchErc20Balance( - this.#underlyingTokenAddress, + underlyingTokenAddress, accountAddress, + vaultChainId, ); return { balance }; }, @@ -287,14 +373,17 @@ export class MoneyAccountBalanceService extends BaseDataService< * * @param accountAddress - The Money account's Ethereum address. * @returns The musdSHFvd balance as a raw uint256 string. + * @throws {@link VaultConfigNotAvailableError} if vault config has not been loaded. */ async getMusdSHFvdBalance(accountAddress: Hex): Promise<{ balance: string }> { return this.fetchQuery({ queryKey: [`${this.name}:getMusdSHFvdBalance`, accountAddress], queryFn: async () => { + const { vaultAddress, vaultChainId } = this.#requireConfig(); const balance = await this.#fetchErc20Balance( - this.#vaultAddress, + vaultAddress, accountAddress, + vaultChainId, ); return { balance }; }, @@ -310,6 +399,7 @@ export class MoneyAccountBalanceService extends BaseDataService< * @param options - The options for the query. * @param options.staleTime - The stale time for the query. Defaults to 30 seconds. * @returns The exchange rate as a raw uint256 string. + * @throws {@link VaultConfigNotAvailableError} if vault config has not been loaded. */ async getExchangeRate({ staleTime = inMilliseconds(30, Duration.Second), @@ -317,9 +407,10 @@ export class MoneyAccountBalanceService extends BaseDataService< return this.fetchQuery({ queryKey: [`${this.name}:getExchangeRate`], queryFn: async () => { - const provider = this.#getProvider(); + const { accountantAddress, vaultChainId } = this.#requireConfig(); + const provider = this.#getProvider(vaultChainId); const contract = new Contract( - this.#accountantAddress, + accountantAddress, ACCOUNTANT_ABI, provider, ); @@ -339,6 +430,7 @@ export class MoneyAccountBalanceService extends BaseDataService< * @param accountAddress - The Money account's Ethereum address. * @returns The musdSHFvd balance, exchange rate, and computed * mUSD-equivalent value as raw uint256 strings. + * @throws {@link VaultConfigNotAvailableError} if vault config has not been loaded. */ async getMusdEquivalentValue( accountAddress: Hex, @@ -349,12 +441,14 @@ export class MoneyAccountBalanceService extends BaseDataService< this.getExchangeRate(), ]); + const { underlyingTokenDecimals } = this.#requireConfig(); + const balanceBigInt = BigInt(musdSHFvdBalance); const rateBigInt = BigInt(exchangeRate); const musdEquivalentValue = ( (balanceBigInt * rateBigInt) / - 10n ** BigInt(this.#underlyingTokenDecimals) + 10n ** BigInt(underlyingTokenDecimals) ).toString(); return { @@ -368,13 +462,23 @@ export class MoneyAccountBalanceService extends BaseDataService< * Fetches the vault's APY and fee breakdown from the Veda performance REST API. * * @returns The normalized vault APY response. + * @throws {@link VaultConfigNotAvailableError} if vault config has not been loaded. */ async getVaultApy(): Promise { return this.fetchQuery({ queryKey: [`${this.name}:getVaultApy`], queryFn: async () => { + const { vaultChainId, vaultAddress } = this.#requireConfig(); + const networkName = VEDA_API_NETWORK_NAMES[vaultChainId]; + + if (!networkName) { + throw new Error( + `No Veda API network name found for chain ${vaultChainId}`, + ); + } + const url = new URL( - `/performance/${this.#networkName}/${this.#vaultAddress}`, + `/performance/${networkName}/${vaultAddress}`, VEDA_PERFORMANCE_API_BASE_URL, ); diff --git a/packages/money-account-balance-service/src/structs.ts b/packages/money-account-balance-service/src/structs.ts index c892959fe9..9eb56439b7 100644 --- a/packages/money-account-balance-service/src/structs.ts +++ b/packages/money-account-balance-service/src/structs.ts @@ -6,6 +6,21 @@ import { string, type, } from '@metamask/superstruct'; +import { StrictHexStruct } from '@metamask/utils'; + +/** + * Superstruct schema for {@link VaultConfig}. + * + * Uses `type()` (loose validation) so that extra keys added to the feature + * flag in future do not break existing clients. + */ +export const VaultConfigStruct = type({ + vaultAddress: StrictHexStruct, + vaultChainId: StrictHexStruct, + accountantAddress: StrictHexStruct, + underlyingTokenAddress: StrictHexStruct, + underlyingTokenDecimals: number(), +}); /** * Superstruct schema for {@link NormalizedVaultApyResponse}. diff --git a/packages/money-account-balance-service/src/types.ts b/packages/money-account-balance-service/src/types.ts new file mode 100644 index 0000000000..b367738ef5 --- /dev/null +++ b/packages/money-account-balance-service/src/types.ts @@ -0,0 +1,13 @@ +import type { Hex } from '@metamask/utils'; + +/** + * The vault configuration read from the remote feature flag. + * Runtime validation is performed by {@link VaultConfigStruct}. + */ +export type VaultConfig = { + vaultAddress: Hex; + vaultChainId: Hex; + accountantAddress: Hex; + underlyingTokenAddress: Hex; + underlyingTokenDecimals: number; +}; diff --git a/packages/money-account-balance-service/tsconfig.build.json b/packages/money-account-balance-service/tsconfig.build.json index 0d76f933e7..6b76ddf531 100644 --- a/packages/money-account-balance-service/tsconfig.build.json +++ b/packages/money-account-balance-service/tsconfig.build.json @@ -9,7 +9,8 @@ { "path": "../base-data-service/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, { "path": "../messenger/tsconfig.build.json" }, - { "path": "../network-controller/tsconfig.build.json" } + { "path": "../network-controller/tsconfig.build.json" }, + { "path": "../remote-feature-flag-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/money-account-balance-service/tsconfig.json b/packages/money-account-balance-service/tsconfig.json index fd635fefde..e55b9f62c4 100644 --- a/packages/money-account-balance-service/tsconfig.json +++ b/packages/money-account-balance-service/tsconfig.json @@ -7,7 +7,8 @@ { "path": "../base-data-service" }, { "path": "../controller-utils" }, { "path": "../messenger" }, - { "path": "../network-controller" } + { "path": "../network-controller" }, + { "path": "../remote-feature-flag-controller" } ], "include": ["../../types", "./src"] } From 40298f70f233084c1e851013ebd9c624785ec5d7 Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Thu, 7 May 2026 13:40:01 -0400 Subject: [PATCH 02/15] feat: cleaning up eslint errors --- .../src/money-account-balance-service.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/money-account-balance-service/src/money-account-balance-service.ts b/packages/money-account-balance-service/src/money-account-balance-service.ts index 391a67b117..29502d3339 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.ts @@ -24,7 +24,6 @@ import { Duration, inMilliseconds } from '@metamask/utils'; import { ACCOUNTANT_ABI, - DEFAULT_VEDA_API_NETWORK_NAME, VAULT_CONFIG_FEATURE_FLAG_KEY, VEDA_API_NETWORK_NAMES, VEDA_PERFORMANCE_API_BASE_URL, @@ -180,7 +179,7 @@ export class MoneyAccountBalanceService extends BaseDataService< }); this.messenger.subscribe( - // TODO: Add to eslint exceptions + // eslint-disable-next-line no-restricted-syntax 'RemoteFeatureFlagController:stateChange', (state) => { const flagValue = @@ -189,12 +188,23 @@ export class MoneyAccountBalanceService extends BaseDataService< }, ); - // Eagerly read already-loaded flags. Wrapped in try/catch because - // RemoteFeatureFlagController may not be registered yet at construction - // time. Validation errors are also swallowed here — the service degrades - // gracefully and fails loud on the first method call. + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, + ); + } + + /** + * Eagerly reads already-loaded feature flags and initialises `#vaultConfig`. + * + * Must be called after all controllers and services have been instantiated so + * that the `RemoteFeatureFlagController:getState` action is guaranteed to be + * registered. Validation errors are swallowed — the service degrades + * gracefully and throws {@link VaultConfigNotAvailableError} on the first + * method call instead. + */ + init(): void { try { - // TODO: Fix by moving to init() method according to the controller guidelines. const { remoteFeatureFlags } = this.messenger.call( 'RemoteFeatureFlagController:getState', ); @@ -203,18 +213,15 @@ export class MoneyAccountBalanceService extends BaseDataService< this.#vaultConfig = this.#parseAndValidateVaultConfig(flagValue); } } catch { - // RFFC not registered or flag is malformed — stay undefined. + // RemoteFeatureFlagController not registered or flag is malformed — stay undefined. } - - this.messenger.registerMethodActionHandlers( - this, - MESSENGER_EXPOSED_METHODS, - ); } /** * Returns the current vault config, or throws {@link VaultConfigNotAvailableError} * if it has not been loaded yet. + * + * @returns The validated vault configuration. */ #requireConfig(): VaultConfig { if (!this.#vaultConfig) { @@ -242,6 +249,7 @@ export class MoneyAccountBalanceService extends BaseDataService< // Flag key absent — treat as "not loaded". if (hadConfig) { this.#vaultConfig = undefined; + // eslint-disable-next-line @typescript-eslint/no-floating-promises this.invalidateQueries(); } return; @@ -254,6 +262,7 @@ export class MoneyAccountBalanceService extends BaseDataService< // Clear previously valid config and purge stale cache. if (hadConfig) { this.#vaultConfig = undefined; + // eslint-disable-next-line @typescript-eslint/no-floating-promises this.invalidateQueries(); } throw error; @@ -265,6 +274,7 @@ export class MoneyAccountBalanceService extends BaseDataService< this.#vaultConfig = newConfig; if (hadConfig) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises this.invalidateQueries(); } } From 3f5ac7856d0a2ebea60ccf80d9f633375592c60b Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Thu, 7 May 2026 16:17:11 -0400 Subject: [PATCH 03/15] feat: added test coverage --- .../src/money-account-balance-service.test.ts | 458 ++++++++++++++++-- .../src/money-account-balance-service.ts | 6 +- 2 files changed, 418 insertions(+), 46 deletions(-) diff --git a/packages/money-account-balance-service/src/money-account-balance-service.test.ts b/packages/money-account-balance-service/src/money-account-balance-service.test.ts index d56278468b..869b18c9cc 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.test.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.test.ts @@ -7,9 +7,15 @@ import type { MessengerActions, MessengerEvents, } from '@metamask/messenger'; +import type { Json } from '@metamask/utils'; import nock, { cleanAll as nockCleanAll } from 'nock'; -import { VedaResponseValidationError } from './errors'; +import { VAULT_CONFIG_FEATURE_FLAG_KEY } from './constants'; +import { + VaultConfigNotAvailableError, + VaultConfigValidationError, + VedaResponseValidationError, +} from './errors'; import type { MoneyAccountBalanceServiceMessenger } from './money-account-balance-service'; import { MoneyAccountBalanceService, @@ -27,16 +33,16 @@ const MockWeb3Provider = Web3Provider as jest.MockedClass; // ============================================================ const MOCK_VAULT_ADDRESS = - '0xVaultAddress000000000000000000000000000000' as const; + '0x1111111111111111111111111111111111111111' as const; const MOCK_ACCOUNTANT_ADDRESS = - '0xAccountantAddr000000000000000000000000000' as const; + '0x2222222222222222222222222222222222222222' as const; const MOCK_UNDERLYING_TOKEN_ADDRESS = - '0xMusdAddress0000000000000000000000000000000' as const; + '0x3333333333333333333333333333333333333333' as const; const MOCK_ACCOUNT_ADDRESS = - '0xUserAccount0000000000000000000000000000000' as const; + '0x4444444444444444444444444444444444444444' as const; const MOCK_NETWORK_CLIENT_ID = 'arbitrum-mainnet'; -const DEFAULT_CONFIG = { +const MOCK_VAULT_CONFIG = { vaultAddress: MOCK_VAULT_ADDRESS, vaultChainId: '0xa4b1' as const, accountantAddress: MOCK_ACCOUNTANT_ADDRESS, @@ -58,9 +64,10 @@ const MOCK_NETWORK_CONFIG = { blockExplorerUrls: [], }; -// A bare object suffices — Web3Provider and Contract are mocked at the module level. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const MOCK_PROVIDER = {} as any; +// Web3Provider is mocked at the module level so type correctness is irrelevant. +const MOCK_PROVIDER = {} as unknown as ConstructorParameters< + typeof Web3Provider +>[0]; const MOCK_VAULT_APY_RAW_RESPONSE = { Response: { @@ -120,8 +127,10 @@ type RootMessenger = Messenger< MessengerEvents >; -function createRootMessenger(): RootMessenger { - return new Messenger({ namespace: MOCK_ANY_NAMESPACE }); +function createRootMessenger( + captureException?: (error: Error) => void, +): RootMessenger { + return new Messenger({ namespace: MOCK_ANY_NAMESPACE, captureException }); } function createServiceMessenger( @@ -138,16 +147,51 @@ function createServiceMessenger( // ============================================================ /** - * Builds the service under test with messenger action stubs for the two - * NetworkController dependencies. + * Publishes a `RemoteFeatureFlagController:stateChange` event via the root + * messenger, simulating a flag update from RemoteFeatureFlagController. + * + * @param rootMessenger - The root messenger to publish on. + * @param remoteFeatureFlags - The new flags object. + */ +function publishRFFCStateChange( + rootMessenger: RootMessenger, + remoteFeatureFlags: Record, +): void { + rootMessenger.publish( + 'RemoteFeatureFlagController:stateChange', + { remoteFeatureFlags, cacheTimestamp: Date.now() }, + [], + ); +} + +/** + * Builds the service under test with messenger action stubs for all + * dependencies, including RemoteFeatureFlagController. + * + * By default, `init()` is called and the RFFC state contains a valid + * `moneyVaultConfig`. Pass `rffcFlags` to override, or `callInit: false` to + * skip the eager init. + * + * A `captureException` mock is wired onto the root messenger by default so + * that subscriber errors (e.g. `VaultConfigValidationError`) are routed there + * instead of `console.error`. Pass your own mock to assert on it. * - * @param args - Optional overrides for the service constructor options. - * @param args.options - Partial constructor options merged over {@link DEFAULT_CONFIG}. + * @param args - Optional overrides. + * @param args.rffcFlags - Flags to return from `RemoteFeatureFlagController:getState`. + * @param args.callInit - Whether to call `service.init()` after construction. Defaults to true. + * @param args.captureException - Error reporter wired on the root messenger. + * @param args.options - Partial constructor options for the service. * @returns The constructed service together with messenger instances and mock stubs. */ function createService({ + rffcFlags = { [VAULT_CONFIG_FEATURE_FLAG_KEY]: MOCK_VAULT_CONFIG }, + callInit = true, + captureException = jest.fn(), options = {}, }: { + rffcFlags?: Record; + callInit?: boolean; + captureException?: jest.Mock; options?: Partial< ConstructorParameters[0] >; @@ -157,14 +201,19 @@ function createService({ messenger: MoneyAccountBalanceServiceMessenger; mockGetNetworkConfig: jest.Mock; mockGetNetworkClient: jest.Mock; + mockGetRFFCState: jest.Mock; + captureException: jest.Mock; } { - const rootMessenger = createRootMessenger(); + const rootMessenger = createRootMessenger(captureException); const messenger = createServiceMessenger(rootMessenger); const mockGetNetworkConfig = jest.fn().mockReturnValue(MOCK_NETWORK_CONFIG); const mockGetNetworkClient = jest.fn().mockReturnValue({ provider: MOCK_PROVIDER, }); + const mockGetRFFCState = jest + .fn() + .mockReturnValue({ remoteFeatureFlags: rffcFlags, cacheTimestamp: 0 }); rootMessenger.registerActionHandler( 'NetworkController:getNetworkConfigurationByChainId', @@ -174,21 +223,27 @@ function createService({ 'NetworkController:getNetworkClientById', mockGetNetworkClient, ); + rootMessenger.registerActionHandler( + 'RemoteFeatureFlagController:getState', + mockGetRFFCState, + ); rootMessenger.delegate({ actions: [ 'NetworkController:getNetworkConfigurationByChainId', 'NetworkController:getNetworkClientById', + 'RemoteFeatureFlagController:getState', ], - events: [], + // eslint-disable-next-line no-restricted-syntax + events: ['RemoteFeatureFlagController:stateChange'], messenger, }); - const service = new MoneyAccountBalanceService({ - messenger, - ...DEFAULT_CONFIG, - ...options, - }); + const service = new MoneyAccountBalanceService({ messenger, ...options }); + + if (callInit) { + service.init(); + } return { service, @@ -196,6 +251,8 @@ function createService({ messenger, mockGetNetworkConfig, mockGetNetworkClient, + mockGetRFFCState, + captureException, }; } @@ -262,6 +319,7 @@ function mockContractsByAddress( describe('MoneyAccountBalanceService', () => { beforeEach(() => { + MockContract.mockReset(); MockWeb3Provider.mockImplementation(() => ({}) as unknown as Web3Provider); nockCleanAll(); }); @@ -270,6 +328,287 @@ describe('MoneyAccountBalanceService', () => { jest.resetAllMocks(); }); + // ---------------------------------------------------------- + // init + // ---------------------------------------------------------- + + describe('init', () => { + it('loads vault config when RemoteFeatureFlagController already has valid flags', async () => { + mockErc20BalanceOf('5000000'); + const { service } = createService(); + + // If vault config was loaded, getMusdBalance succeeds without throwing. + expect( + await service.getMusdBalance(MOCK_ACCOUNT_ADDRESS), + ).toStrictEqual({ balance: '5000000' }); + }); + + it('leaves config undefined and degrades gracefully when flag key is absent', async () => { + const { service } = createService({ rffcFlags: {} }); + + await expect( + service.getMusdBalance(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow(VaultConfigNotAvailableError); + }); + + it('leaves config undefined and degrades gracefully when the flag value is malformed', async () => { + const { service } = createService({ + rffcFlags: { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: { notAValidConfig: true }, + }, + }); + + await expect( + service.getMusdBalance(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow(VaultConfigNotAvailableError); + }); + + it('does not throw when RemoteFeatureFlagController is not yet registered', () => { + const rootMessenger = createRootMessenger(); + const messenger = createServiceMessenger(rootMessenger); + + // Do NOT register RemoteFeatureFlagController:getState or delegate it. + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkConfigurationByChainId', + jest.fn(), + ); + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + jest.fn(), + ); + rootMessenger.delegate({ + actions: [ + 'NetworkController:getNetworkConfigurationByChainId', + 'NetworkController:getNetworkClientById', + ], + events: [], + messenger, + }); + + const service = new MoneyAccountBalanceService({ messenger }); + + expect(() => service.init()).not.toThrow(); + }); + }); + + // ---------------------------------------------------------- + // RemoteFeatureFlagController:stateChange subscription + // ---------------------------------------------------------- + + describe('RemoteFeatureFlagController:stateChange subscription', () => { + describe('config lifecycle', () => { + it('sets vault config when a valid config arrives via subscription', async () => { + mockErc20BalanceOf('9000000'); + // Start with no flags so config is absent after init. + const { service, rootMessenger } = createService({ rffcFlags: {} }); + + publishRFFCStateChange(rootMessenger, { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: MOCK_VAULT_CONFIG, + }); + + expect( + await service.getMusdBalance(MOCK_ACCOUNT_ADDRESS), + ).toStrictEqual({ balance: '9000000' }); + }); + + it('uses the updated vault address after config changes', async () => { + const NEW_VAULT_ADDRESS = + '0x5555555555555555555555555555555555555555' as const; + mockErc20BalanceOf('1000000'); + const { service, rootMessenger } = createService(); + + publishRFFCStateChange(rootMessenger, { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: { + ...MOCK_VAULT_CONFIG, + vaultAddress: NEW_VAULT_ADDRESS, + }, + }); + + await service.getMusdSHFvdBalance(MOCK_ACCOUNT_ADDRESS); + + // getMusdSHFvdBalance uses vaultAddress — verify the new address was used. + expect(MockContract).toHaveBeenCalledWith( + NEW_VAULT_ADDRESS, + expect.anything(), + expect.anything(), + ); + expect(MockContract).not.toHaveBeenCalledWith( + MOCK_VAULT_ADDRESS, + expect.anything(), + expect.anything(), + ); + }); + + it('clears vault config when the flag key is removed from remoteFeatureFlags', async () => { + const { service, rootMessenger } = createService(); + + publishRFFCStateChange(rootMessenger, {}); + + await expect( + service.getMusdBalance(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow(VaultConfigNotAvailableError); + }); + + it('clears vault config and routes VaultConfigValidationError when a malformed config arrives after valid config', async () => { + const captureException = jest.fn(); + const { service, rootMessenger } = createService({ captureException }); + + publishRFFCStateChange(rootMessenger, { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: { malformed: true }, + }); + + // Messenger routes the thrown VaultConfigValidationError to captureException. + expect(captureException).toHaveBeenCalledWith( + expect.any(VaultConfigValidationError), + ); + + // Config has been cleared so subsequent calls throw VaultConfigNotAvailableError. + await expect( + service.getMusdBalance(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow(VaultConfigNotAvailableError); + }); + + it('routes VaultConfigValidationError to captureException when malformed config arrives with no prior config', () => { + const captureException = jest.fn(); + const { rootMessenger } = createService({ rffcFlags: {}, captureException }); + + publishRFFCStateChange(rootMessenger, { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: { malformed: true }, + }); + + expect(captureException).toHaveBeenCalledWith( + expect.any(VaultConfigValidationError), + ); + }); + + }); + + describe('cache invalidation', () => { + it('does NOT invalidate queries when config is set for the first time via subscription', () => { + const { service, rootMessenger } = createService({ rffcFlags: {} }); + const invalidateQueriesSpy = jest.spyOn(service, 'invalidateQueries'); + + publishRFFCStateChange(rootMessenger, { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: MOCK_VAULT_CONFIG, + }); + + expect(invalidateQueriesSpy).not.toHaveBeenCalled(); + }); + + it('invalidates queries when config changes to a different valid config', () => { + const { service, rootMessenger } = createService(); + const invalidateQueriesSpy = jest.spyOn(service, 'invalidateQueries'); + + const updatedConfig = { + ...MOCK_VAULT_CONFIG, + underlyingTokenDecimals: 18, + }; + publishRFFCStateChange(rootMessenger, { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: updatedConfig, + }); + + expect(invalidateQueriesSpy).toHaveBeenCalledTimes(1); + }); + + it('does NOT invalidate queries when the same config arrives again', () => { + const { service, rootMessenger } = createService(); + const invalidateQueriesSpy = jest.spyOn(service, 'invalidateQueries'); + + publishRFFCStateChange(rootMessenger, { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: { ...MOCK_VAULT_CONFIG }, + }); + + expect(invalidateQueriesSpy).not.toHaveBeenCalled(); + }); + + it('invalidates queries when the flag key is removed after valid config was set', () => { + const { service, rootMessenger } = createService(); + const invalidateQueriesSpy = jest.spyOn(service, 'invalidateQueries'); + + publishRFFCStateChange(rootMessenger, {}); + + expect(invalidateQueriesSpy).toHaveBeenCalledTimes(1); + }); + + it('does NOT invalidate queries when absent flag key arrives with no prior config', () => { + const { service, rootMessenger } = createService({ rffcFlags: {} }); + const invalidateQueriesSpy = jest.spyOn(service, 'invalidateQueries'); + + publishRFFCStateChange(rootMessenger, {}); + + expect(invalidateQueriesSpy).not.toHaveBeenCalled(); + }); + + it('invalidates queries when a malformed config arrives after valid config was set', () => { + const { service, rootMessenger } = createService(); + const invalidateQueriesSpy = jest.spyOn(service, 'invalidateQueries'); + + publishRFFCStateChange(rootMessenger, { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: { malformed: true }, + }); + + expect(invalidateQueriesSpy).toHaveBeenCalledTimes(1); + }); + + it('does NOT invalidate queries when a malformed config arrives with no prior config', () => { + const { service, rootMessenger } = createService({ rffcFlags: {} }); + const invalidateQueriesSpy = jest.spyOn(service, 'invalidateQueries'); + + publishRFFCStateChange(rootMessenger, { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: { malformed: true }, + }); + + expect(invalidateQueriesSpy).not.toHaveBeenCalled(); + }); + }); + }); + + // ---------------------------------------------------------- + // VaultConfigNotAvailableError — all public methods + // ---------------------------------------------------------- + + describe('when vault config is not available', () => { + it('getMusdBalance throws VaultConfigNotAvailableError', async () => { + const { service } = createService({ rffcFlags: {} }); + + await expect( + service.getMusdBalance(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow(VaultConfigNotAvailableError); + }); + + it('getMusdSHFvdBalance throws VaultConfigNotAvailableError', async () => { + const { service } = createService({ rffcFlags: {} }); + + await expect( + service.getMusdSHFvdBalance(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow(VaultConfigNotAvailableError); + }); + + it('getExchangeRate throws VaultConfigNotAvailableError', async () => { + const { service } = createService({ rffcFlags: {} }); + + await expect(service.getExchangeRate()).rejects.toThrow( + VaultConfigNotAvailableError, + ); + }); + + it('getMusdEquivalentValue throws VaultConfigNotAvailableError', async () => { + const { service } = createService({ rffcFlags: {} }); + + await expect( + service.getMusdEquivalentValue(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow(VaultConfigNotAvailableError); + }); + + it('getVaultApy throws VaultConfigNotAvailableError', async () => { + const { service } = createService({ rffcFlags: {} }); + + await expect(service.getVaultApy()).rejects.toThrow( + VaultConfigNotAvailableError, + ); + }); + }); + // ---------------------------------------------------------- // getMusdBalance // ---------------------------------------------------------- @@ -465,12 +804,12 @@ describe('MoneyAccountBalanceService', () => { ); const { service } = createService(); - // Seed the cache + // Seed the cache. await service.getExchangeRate(); mockGetRate.mockResolvedValue({ toString: () => '1100000' }); - // Get cached value + // Second call should return the cached value. const result = await service.getExchangeRate(); expect(result).toStrictEqual({ rate: '1050000' }); @@ -486,13 +825,13 @@ describe('MoneyAccountBalanceService', () => { ); const { service } = createService(); - // Seed the cache + // Seed the cache. const firstResult = await service.getExchangeRate(); expect(firstResult).toStrictEqual({ rate: '1050000' }); mockGetRate.mockResolvedValue({ toString: () => '1100000' }); - // Refetch the value + // Refetch using staleTime: 0. const freshResult = await service.getExchangeRate({ staleTime: 0 }); expect(freshResult).toStrictEqual({ rate: '1100000' }); @@ -626,15 +965,17 @@ describe('MoneyAccountBalanceService', () => { }, ); - it('accepts and normalizes a sparse response where only apy and timestamp are present', async () => { - const sparseResponse = { + it('accepts and normalizes a response with zero values and empty array breakdowns', async () => { + // All optional fields are present but carry zero / empty values — verifies + // that falsy values are not accidentally dropped during normalization. + const zeroValuesResponse = { Response: { aggregation_period: '7 days', apy: 0, chain_allocation: { arbitrum: 0 }, fees: 0, global_apy_breakdown: { fee: 0, maturity_apy: 0, real_apy: 0 }, - maturity_apy_breakdown: [], + performance_fees: 0, real_apy_breakdown: [], timestamp: 'Fri, 10 Apr 2026 22:05:54 GMT', }, @@ -642,13 +983,14 @@ describe('MoneyAccountBalanceService', () => { nock('https://api.sevenseas.capital') .get(`/performance/arbitrum/${MOCK_VAULT_ADDRESS}`) - .reply(200, sparseResponse); + .reply(200, zeroValuesResponse); const { service } = createService(); const result = await service.getVaultApy(); expect(result.apy).toBe(0); + expect(result.fees).toBe(0); expect(result.timestamp).toBe('Fri, 10 Apr 2026 22:05:54 GMT'); expect(result.realApyBreakdown).toStrictEqual([]); }); @@ -696,25 +1038,57 @@ describe('MoneyAccountBalanceService', () => { ); }); - it('falls back to the default network name for unknown chain IDs', async () => { - // 0x1 is not in VEDA_API_NETWORK_NAMES, so DEFAULT_VEDA_API_NETWORK_NAME - // ('arbitrum') should be used. Nock matches on exact URL, so if the wrong - // network name were used the request would throw instead of returning data. - nock('https://api.sevenseas.capital') - .get(`/performance/arbitrum/${MOCK_VAULT_ADDRESS}`) - .reply(200, MOCK_VAULT_APY_RAW_RESPONSE); - + it('throws when the vault chain ID has no Veda API network name mapping', async () => { const { service } = createService({ - options: { vaultChainId: '0x1' as const }, + rffcFlags: { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: { + ...MOCK_VAULT_CONFIG, + vaultChainId: '0x1', + }, + }, }); - const result = await service.getVaultApy(); - - expect(result).toStrictEqual(MOCK_VAULT_APY_NORMALIZED); + await expect(service.getVaultApy()).rejects.toThrow( + 'No Veda API network name found for chain 0x1', + ); }); }); }); +// ============================================================ +// Error class unit tests +// ============================================================ + +describe('VaultConfigNotAvailableError', () => { + it('has the expected message and name', () => { + const error = new VaultConfigNotAvailableError(); + + expect(error.message).toBe( + 'MoneyAccountBalanceService: vault config is not available. ' + + 'RemoteFeatureFlagController may not have fetched flags yet.', + ); + expect(error.name).toBe('VaultConfigNotAvailableError'); + }); +}); + +describe('VaultConfigValidationError', () => { + it('uses the default message when constructed with no argument', () => { + const error = new VaultConfigValidationError(); + + expect(error.message).toBe( + 'MoneyAccountBalanceService: vault config from remote feature flags is malformed.', + ); + expect(error.name).toBe('VaultConfigValidationError'); + }); + + it('uses a custom message when one is provided', () => { + const error = new VaultConfigValidationError('custom message'); + + expect(error.message).toBe('custom message'); + expect(error.name).toBe('VaultConfigValidationError'); + }); +}); + describe('VedaResponseValidationError', () => { it('uses the default message when constructed with no argument', () => { const error = new VedaResponseValidationError(); diff --git a/packages/money-account-balance-service/src/money-account-balance-service.ts b/packages/money-account-balance-service/src/money-account-balance-service.ts index 29502d3339..21cd0cfdb2 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.ts @@ -291,10 +291,8 @@ export class MoneyAccountBalanceService extends BaseDataService< #parseAndValidateVaultConfig(flagValue: Json): VaultConfig { try { assert(flagValue, VaultConfigStruct); - } catch (error) { - throw new VaultConfigValidationError( - error instanceof Error ? error.message : undefined, - ); + } catch { + throw new VaultConfigValidationError(); } return flagValue as unknown as VaultConfig; } From e948da7485666d6cf1a7cc47750d74163476a38b Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Thu, 7 May 2026 16:18:48 -0400 Subject: [PATCH 04/15] feat: add monad to network names mapping and set as default --- packages/money-account-balance-service/src/constants.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/money-account-balance-service/src/constants.ts b/packages/money-account-balance-service/src/constants.ts index 93a2cf57d0..da57037861 100644 --- a/packages/money-account-balance-service/src/constants.ts +++ b/packages/money-account-balance-service/src/constants.ts @@ -6,15 +6,14 @@ export const VEDA_PERFORMANCE_API_BASE_URL = 'https://api.sevenseas.capital'; * The key under which vault config is stored in * `RemoteFeatureFlagController` state's `remoteFeatureFlags` map. */ -// TOOD: Update this to the actual flag key when it is available. -export const VAULT_CONFIG_FEATURE_FLAG_KEY = 'moneyVaultConfig'; +export const VAULT_CONFIG_FEATURE_FLAG_KEY = 'testMoneyVaultConfig'; -// TODO: Add Monad network name export const VEDA_API_NETWORK_NAMES: Record = { '0xa4b1': 'arbitrum', + '0x8f': 'monad', }; -export const DEFAULT_VEDA_API_NETWORK_NAME = VEDA_API_NETWORK_NAMES['0xa4b1']; +export const DEFAULT_VEDA_API_NETWORK_NAME = VEDA_API_NETWORK_NAMES['0x8f']; /** * Minimal ABI for the Veda Accountant's `getRate()` function (selector 0x679aefce). From 823fb08e19cd59a804ded68ecf294137c245b109 Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Thu, 7 May 2026 16:22:24 -0400 Subject: [PATCH 05/15] chore: update yarn.lock --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index 0a955bf5f9..b8a511c615 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4473,6 +4473,7 @@ __metadata: "@metamask/messenger": "npm:^1.2.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/network-controller": "npm:^30.1.0" + "@metamask/remote-feature-flag-controller": "npm:^4.2.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" From c0cf0e3289ecfc0f058b54b3190d94ecf34ef79e Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Thu, 7 May 2026 16:42:41 -0400 Subject: [PATCH 06/15] feat: added config logging --- .../src/logger.ts | 7 +++ .../src/money-account-balance-service.ts | 54 +++++++++++++++++-- 2 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 packages/money-account-balance-service/src/logger.ts diff --git a/packages/money-account-balance-service/src/logger.ts b/packages/money-account-balance-service/src/logger.ts new file mode 100644 index 0000000000..32c17d104f --- /dev/null +++ b/packages/money-account-balance-service/src/logger.ts @@ -0,0 +1,7 @@ +import { createProjectLogger, createModuleLogger } from '@metamask/utils'; + +export const projectLogger = createProjectLogger( + 'money-account-balance-service', +); + +export { createModuleLogger }; diff --git a/packages/money-account-balance-service/src/money-account-balance-service.ts b/packages/money-account-balance-service/src/money-account-balance-service.ts index 21cd0cfdb2..4ef00f64a9 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.ts @@ -33,6 +33,7 @@ import { VaultConfigValidationError, VedaResponseValidationError, } from './errors'; +import { projectLogger, createModuleLogger } from './logger'; import type { MoneyAccountBalanceServiceMethodActions } from './money-account-balance-service-method-action-types'; import { normalizeVaultApyResponse } from './requestNormalization'; import type { @@ -51,6 +52,8 @@ import { VaultApyRawResponseStruct, VaultConfigStruct } from './structs'; */ export const serviceName = 'MoneyAccountBalanceService'; +const configLogger = createModuleLogger(projectLogger, 'config'); + // === MESSENGER === const MESSENGER_EXPOSED_METHODS = [ @@ -209,11 +212,26 @@ export class MoneyAccountBalanceService extends BaseDataService< 'RemoteFeatureFlagController:getState', ); const flagValue = remoteFeatureFlags[VAULT_CONFIG_FEATURE_FLAG_KEY]; - if (flagValue !== undefined) { - this.#vaultConfig = this.#parseAndValidateVaultConfig(flagValue); + if (flagValue === undefined) { + configLogger( + 'Init complete — no vault config flag present, awaiting remote flags', + ); + return; + } + this.#vaultConfig = this.#parseAndValidateVaultConfig(flagValue); + configLogger('Vault config loaded during init', this.#vaultConfig); + } catch (error) { + if (error instanceof VaultConfigValidationError) { + configLogger( + 'Init failed — vault config validation error, service will start without config', + { error }, + ); + } else { + configLogger( + 'Init failed — RemoteFeatureFlagController not available, service will start without config', + { error }, + ); } - } catch { - // RemoteFeatureFlagController not registered or flag is malformed — stay undefined. } } @@ -243,14 +261,23 @@ export class MoneyAccountBalanceService extends BaseDataService< * @param flagValue - The raw flag value from `remoteFeatureFlags`. */ #onRemoteFeatureFlagChange(flagValue: Json | undefined): void { - const hadConfig = this.#vaultConfig !== undefined; + const previousConfig = this.#vaultConfig; + const hadConfig = previousConfig !== undefined; if (flagValue === undefined) { // Flag key absent — treat as "not loaded". if (hadConfig) { this.#vaultConfig = undefined; + configLogger( + 'Vault config cleared — flag key absent; cache invalidated', + previousConfig, + ); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.invalidateQueries(); + } else { + configLogger( + 'Flag key still absent after remote flag change — config remains unavailable', + ); } return; } @@ -262,8 +289,19 @@ export class MoneyAccountBalanceService extends BaseDataService< // Clear previously valid config and purge stale cache. if (hadConfig) { this.#vaultConfig = undefined; + configLogger( + 'Vault config validation failed — previous config cleared; cache invalidated', + { previousConfig, error }, + ); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.invalidateQueries(); + } else { + configLogger( + 'Vault config validation failed — config was already absent', + { + error, + }, + ); } throw error; } @@ -274,8 +312,14 @@ export class MoneyAccountBalanceService extends BaseDataService< this.#vaultConfig = newConfig; if (hadConfig) { + configLogger('Vault config updated; cache invalidated', { + previous: previousConfig, + next: newConfig, + }); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.invalidateQueries(); + } else { + configLogger('Vault config loaded', newConfig); } } From 348aa7b416427e30661892a597be4005d0a50dca Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Thu, 7 May 2026 18:04:50 -0400 Subject: [PATCH 07/15] feat: updated retryFilterPolicy to exlucde VaultConfigNotAvailableError --- .../src/money-account-balance-service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/money-account-balance-service/src/money-account-balance-service.ts b/packages/money-account-balance-service/src/money-account-balance-service.ts index 4ef00f64a9..4abaac1a4a 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.ts @@ -175,7 +175,9 @@ export class MoneyAccountBalanceService extends BaseDataService< messenger, policyOptions: { retryFilterPolicy: handleWhen( - (error) => !(error instanceof VedaResponseValidationError), + (error) => + !(error instanceof VedaResponseValidationError) && + !(error instanceof VaultConfigNotAvailableError), ), ...policyOptions, }, From dda87c1e70e81de59f74de5fc8a24d15b992dbb6 Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Thu, 7 May 2026 18:11:35 -0400 Subject: [PATCH 08/15] chore: fix formatting issues --- ...ey-account-balance-service-method-action-types.ts | 5 +++++ .../src/money-account-balance-service.test.ts | 12 +++++++----- .../src/money-account-balance-service.ts | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/money-account-balance-service/src/money-account-balance-service-method-action-types.ts b/packages/money-account-balance-service/src/money-account-balance-service-method-action-types.ts index cfd8833236..7be34d1553 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service-method-action-types.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service-method-action-types.ts @@ -10,6 +10,7 @@ import type { MoneyAccountBalanceService } from './money-account-balance-service * * @param accountAddress - The Money account's Ethereum address. * @returns The mUSD balance as a raw uint256 string. + * @throws {@link VaultConfigNotAvailableError} if vault config has not been loaded. */ export type MoneyAccountBalanceServiceGetMusdBalanceAction = { type: `MoneyAccountBalanceService:getMusdBalance`; @@ -22,6 +23,7 @@ export type MoneyAccountBalanceServiceGetMusdBalanceAction = { * * @param accountAddress - The Money account's Ethereum address. * @returns The musdSHFvd balance as a raw uint256 string. + * @throws {@link VaultConfigNotAvailableError} if vault config has not been loaded. */ export type MoneyAccountBalanceServiceGetMusdSHFvdBalanceAction = { type: `MoneyAccountBalanceService:getMusdSHFvdBalance`; @@ -36,6 +38,7 @@ export type MoneyAccountBalanceServiceGetMusdSHFvdBalanceAction = { * @param options - The options for the query. * @param options.staleTime - The stale time for the query. Defaults to 30 seconds. * @returns The exchange rate as a raw uint256 string. + * @throws {@link VaultConfigNotAvailableError} if vault config has not been loaded. */ export type MoneyAccountBalanceServiceGetExchangeRateAction = { type: `MoneyAccountBalanceService:getExchangeRate`; @@ -51,6 +54,7 @@ export type MoneyAccountBalanceServiceGetExchangeRateAction = { * @param accountAddress - The Money account's Ethereum address. * @returns The musdSHFvd balance, exchange rate, and computed * mUSD-equivalent value as raw uint256 strings. + * @throws {@link VaultConfigNotAvailableError} if vault config has not been loaded. */ export type MoneyAccountBalanceServiceGetMusdEquivalentValueAction = { type: `MoneyAccountBalanceService:getMusdEquivalentValue`; @@ -61,6 +65,7 @@ export type MoneyAccountBalanceServiceGetMusdEquivalentValueAction = { * Fetches the vault's APY and fee breakdown from the Veda performance REST API. * * @returns The normalized vault APY response. + * @throws {@link VaultConfigNotAvailableError} if vault config has not been loaded. */ export type MoneyAccountBalanceServiceGetVaultApyAction = { type: `MoneyAccountBalanceService:getVaultApy`; diff --git a/packages/money-account-balance-service/src/money-account-balance-service.test.ts b/packages/money-account-balance-service/src/money-account-balance-service.test.ts index 869b18c9cc..0ab3a18223 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.test.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.test.ts @@ -338,9 +338,9 @@ describe('MoneyAccountBalanceService', () => { const { service } = createService(); // If vault config was loaded, getMusdBalance succeeds without throwing. - expect( - await service.getMusdBalance(MOCK_ACCOUNT_ADDRESS), - ).toStrictEqual({ balance: '5000000' }); + expect(await service.getMusdBalance(MOCK_ACCOUNT_ADDRESS)).toStrictEqual({ + balance: '5000000', + }); }); it('leaves config undefined and degrades gracefully when flag key is absent', async () => { @@ -470,7 +470,10 @@ describe('MoneyAccountBalanceService', () => { it('routes VaultConfigValidationError to captureException when malformed config arrives with no prior config', () => { const captureException = jest.fn(); - const { rootMessenger } = createService({ rffcFlags: {}, captureException }); + const { rootMessenger } = createService({ + rffcFlags: {}, + captureException, + }); publishRFFCStateChange(rootMessenger, { [VAULT_CONFIG_FEATURE_FLAG_KEY]: { malformed: true }, @@ -480,7 +483,6 @@ describe('MoneyAccountBalanceService', () => { expect.any(VaultConfigValidationError), ); }); - }); describe('cache invalidation', () => { diff --git a/packages/money-account-balance-service/src/money-account-balance-service.ts b/packages/money-account-balance-service/src/money-account-balance-service.ts index 4abaac1a4a..4d91052fe5 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.ts @@ -41,8 +41,8 @@ import type { MusdEquivalentValueResponse, NormalizedVaultApyResponse, } from './response.types'; -import type { VaultConfig } from './types'; import { VaultApyRawResponseStruct, VaultConfigStruct } from './structs'; +import type { VaultConfig } from './types'; // === GENERAL === From d05aea3eb73fa556beb0f2f4cfc2d73145e81c96 Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Thu, 7 May 2026 19:18:44 -0400 Subject: [PATCH 09/15] chore: updated readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1ece233c59..b163294787 100644 --- a/README.md +++ b/README.md @@ -375,6 +375,7 @@ linkStyle default opacity:0.5 money_account_balance_service --> controller_utils; money_account_balance_service --> messenger; money_account_balance_service --> network_controller; + money_account_balance_service --> remote_feature_flag_controller; money_account_controller --> accounts_controller; money_account_controller --> base_controller; money_account_controller --> keyring_controller; From cde9eab50154a1b0f1d235ebad3c2833029a9beb Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Thu, 7 May 2026 19:35:47 -0400 Subject: [PATCH 10/15] chore: updated changelog --- packages/money-account-balance-service/CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/money-account-balance-service/CHANGELOG.md b/packages/money-account-balance-service/CHANGELOG.md index 482766601e..ffeb4bfca7 100644 --- a/packages/money-account-balance-service/CHANGELOG.md +++ b/packages/money-account-balance-service/CHANGELOG.md @@ -9,12 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `VaultConfigNotAvailableError` and `VaultConfigValidationError` error classes for typed consumer error handling ([#TODO](https://github.com/MetaMask/core/pull/TODO)) -- `MoneyAccountBalanceService` now subscribes to `RemoteFeatureFlagController:stateChange` and invalidates all cached queries when vault config changes ([#TODO](https://github.com/MetaMask/core/pull/TODO)) +- Add `VaultConfigNotAvailableError` and `VaultConfigValidationError` error classes for typed consumer error handling ([#8742](https://github.com/MetaMask/core/pull/8742)) +- `MoneyAccountBalanceService` now subscribes to `RemoteFeatureFlagController:stateChange` and invalidates all cached queries when vault config changes ([#8742](https://github.com/MetaMask/core/pull/8742)) ### Changed -- **BREAKING:** `MoneyAccountBalanceService` no longer accepts vault config via constructor (`vaultAddress`, `vaultChainId`, `accountantAddress`, `underlyingTokenAddress`, `underlyingTokenDecimals`). Vault config is now read from remote feature flag via `RemoteFeatureFlagController`. The service requires `RemoteFeatureFlagController` to be registered on the messenger. Methods throw `VaultConfigNotAvailableError` until a valid config has been loaded from remote flags. ([#TODO](https://github.com/MetaMask/core/pull/TODO)) +- **BREAKING:** `MoneyAccountBalanceService` no longer accepts vault config via constructor (`vaultAddress`, `vaultChainId`, `accountantAddress`, `underlyingTokenAddress`, `underlyingTokenDecimals`). Vault config is now read from remote feature flag via `RemoteFeatureFlagController`. The service requires `RemoteFeatureFlagController` to be registered on the messenger. Methods throw `VaultConfigNotAvailableError` until a valid config has been loaded from remote flags. ([#8742](https://github.com/MetaMask/core/pull/8742)) - Add `@metamask/remote-feature-flag-controller` as a dependency and ensure `RemoteFeatureFlagController:getState` and `RemoteFeatureFlagController:stateChange` are permitted on the messenger passed to `MoneyAccountBalanceService`. - Bump `@metamask/messenger` from `^1.1.1` to `^1.2.0` ([#8632](https://github.com/MetaMask/core/pull/8632)) - Bump `@metamask/network-controller` from `^30.0.1` to `^30.1.0` ([#8636](https://github.com/MetaMask/core/pull/8636)) From 386f74c040928b29c487c5b0946966002ca8ad25 Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Fri, 8 May 2026 13:22:11 -0400 Subject: [PATCH 11/15] feat: refactored service to use new vault config type --- .../src/constants.ts | 47 +++++- .../src/money-account-balance-service.test.ts | 151 ++++++++++++------ .../src/money-account-balance-service.ts | 98 +++++++----- .../src/response.types.ts | 7 +- .../src/structs.ts | 8 +- .../src/types.ts | 8 +- 6 files changed, 209 insertions(+), 110 deletions(-) diff --git a/packages/money-account-balance-service/src/constants.ts b/packages/money-account-balance-service/src/constants.ts index da57037861..f6197597b5 100644 --- a/packages/money-account-balance-service/src/constants.ts +++ b/packages/money-account-balance-service/src/constants.ts @@ -6,7 +6,7 @@ export const VEDA_PERFORMANCE_API_BASE_URL = 'https://api.sevenseas.capital'; * The key under which vault config is stored in * `RemoteFeatureFlagController` state's `remoteFeatureFlags` map. */ -export const VAULT_CONFIG_FEATURE_FLAG_KEY = 'testMoneyVaultConfig'; +export const VAULT_CONFIG_FEATURE_FLAG_KEY = 'moneyAccountVaultConfig'; export const VEDA_API_NETWORK_NAMES: Record = { '0xa4b1': 'arbitrum', @@ -16,11 +16,19 @@ export const VEDA_API_NETWORK_NAMES: Record = { export const DEFAULT_VEDA_API_NETWORK_NAME = VEDA_API_NETWORK_NAMES['0x8f']; /** - * Minimal ABI for the Veda Accountant's `getRate()` function (selector 0x679aefce). - * Returns the exchange rate between vault shares (musdSHFvd) and the - * underlying asset (mUSD) as a uint256. + * Minimal ABI for the Veda Accountant contract. Covers: + * - base (0x5001f3b5) — the underlying ERC20 base asset address + * - getRate (0x679aefce) — exchange rate between vault shares and the + * underlying asset (mUSD) as a uint256 */ export const ACCOUNTANT_ABI = [ + { + inputs: [], + name: 'base', + outputs: [{ internalType: 'contract ERC20', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, { inputs: [], name: 'getRate', @@ -29,3 +37,34 @@ export const ACCOUNTANT_ABI = [ type: 'function', }, ] as const; + +/** + * Minimal ABI for the Arctic Architecture Lens contract. + * Covers: + * - balanceOf (0xf7888aec) — shares held by an account in a BoringVault + * - balanceOfInAssets (0x789fd871) — share balance denominated in underlying assets + * - exchangeRate (0xdc3b7c8b) — current rate from an AccountantWithRateProviders + */ +export const LENS_ABI = [ + { + inputs: [ + { name: 'account', type: 'address' }, + { name: 'boringVault', type: 'address' }, + ], + name: 'balanceOf', + outputs: [{ name: 'shares', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { name: 'account', type: 'address' }, + { name: 'boringVault', type: 'address' }, + { name: 'accountant', type: 'address' }, + ], + name: 'balanceOfInAssets', + outputs: [{ name: 'assets', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, +] as const; diff --git a/packages/money-account-balance-service/src/money-account-balance-service.test.ts b/packages/money-account-balance-service/src/money-account-balance-service.test.ts index 0ab3a18223..6437b98380 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.test.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.test.ts @@ -40,14 +40,17 @@ const MOCK_UNDERLYING_TOKEN_ADDRESS = '0x3333333333333333333333333333333333333333' as const; const MOCK_ACCOUNT_ADDRESS = '0x4444444444444444444444444444444444444444' as const; +const MOCK_TELLER_ADDRESS = + '0x5555555555555555555555555555555555555555' as const; +const MOCK_LENS_ADDRESS = '0x6666666666666666666666666666666666666666' as const; const MOCK_NETWORK_CLIENT_ID = 'arbitrum-mainnet'; const MOCK_VAULT_CONFIG = { - vaultAddress: MOCK_VAULT_ADDRESS, - vaultChainId: '0xa4b1' as const, + boringVault: MOCK_VAULT_ADDRESS, accountantAddress: MOCK_ACCOUNTANT_ADDRESS, - underlyingTokenAddress: MOCK_UNDERLYING_TOKEN_ADDRESS, - underlyingTokenDecimals: 6, + tellerAddress: MOCK_TELLER_ADDRESS, + lensAddress: MOCK_LENS_ADDRESS, + chainId: '0xa4b1' as const, }; const MOCK_NETWORK_CONFIG = { @@ -258,7 +261,8 @@ function createService({ /** * Configures the Contract mock so that `balanceOf` resolves to an object - * whose `.toString()` returns `balance`. + * whose `.toString()` returns `balance`. Used for single-contract flows + * such as `getMusdSHFvdBalance`. * * @param balance - The raw uint256 balance string to return. */ @@ -271,6 +275,24 @@ function mockErc20BalanceOf(balance: string): void { ); } +/** + * Configures the first Contract instantiation to respond to `.base()` with + * `MOCK_UNDERLYING_TOKEN_ADDRESS`. Used alongside `mockErc20BalanceOf` to + * stub the two-step flow inside `getMusdBalance`: the Accountant is + * instantiated first to resolve the underlying token address, then the ERC-20 + * is instantiated to read the balance. + */ +function mockAccountantBase(): void { + MockContract.mockImplementationOnce( + () => + ({ + base: jest + .fn() + .mockResolvedValue(MOCK_UNDERLYING_TOKEN_ADDRESS), + }) as unknown as Contract, + ); +} + /** * Configures the Contract mock so that `getRate` resolves to an object * whose `.toString()` returns `rate`. @@ -287,29 +309,21 @@ function mockAccountantGetRate(rate: string): void { } /** - * Configures the Contract mock to route calls to the correct contract method - * based on the address. Used when `getMusdEquivalentValue` creates two - * contracts in the same call — the vault (balanceOf) and the accountant - * (getRate). + * Configures the Contract mock so that `balanceOfInAssets` resolves to an + * object whose `.toString()` returns `balanceOfInAssets`. Used for + * `getMusdEquivalentValue`, which delegates the share-to-asset conversion to + * the Veda Lens contract. * - * @param vaultBalance - The raw uint256 balance string for the vault share contract. - * @param exchangeRate - The raw uint256 rate string for the accountant contract. + * @param balanceOfInAssets - The raw uint256 asset balance string to return. */ -function mockContractsByAddress( - vaultBalance: string, - exchangeRate: string, -): void { - const contractMocksByAddress: Record> = { - [MOCK_VAULT_ADDRESS]: { - balanceOf: jest.fn().mockResolvedValue({ toString: () => vaultBalance }), - }, - [MOCK_ACCOUNTANT_ADDRESS]: { - getRate: jest.fn().mockResolvedValue({ toString: () => exchangeRate }), - }, - }; - +function mockLensBalanceOfInAssets(balanceOfInAssets: string): void { MockContract.mockImplementation( - (address) => contractMocksByAddress[address] as unknown as Contract, + () => + ({ + balanceOfInAssets: jest + .fn() + .mockResolvedValue({ toString: () => balanceOfInAssets }), + }) as unknown as Contract, ); } @@ -334,6 +348,7 @@ describe('MoneyAccountBalanceService', () => { describe('init', () => { it('loads vault config when RemoteFeatureFlagController already has valid flags', async () => { + mockAccountantBase(); mockErc20BalanceOf('5000000'); const { service } = createService(); @@ -398,6 +413,7 @@ describe('MoneyAccountBalanceService', () => { describe('RemoteFeatureFlagController:stateChange subscription', () => { describe('config lifecycle', () => { it('sets vault config when a valid config arrives via subscription', async () => { + mockAccountantBase(); mockErc20BalanceOf('9000000'); // Start with no flags so config is absent after init. const { service, rootMessenger } = createService({ rffcFlags: {} }); @@ -413,14 +429,14 @@ describe('MoneyAccountBalanceService', () => { it('uses the updated vault address after config changes', async () => { const NEW_VAULT_ADDRESS = - '0x5555555555555555555555555555555555555555' as const; + '0x9999999999999999999999999999999999999999' as const; mockErc20BalanceOf('1000000'); const { service, rootMessenger } = createService(); publishRFFCStateChange(rootMessenger, { [VAULT_CONFIG_FEATURE_FLAG_KEY]: { ...MOCK_VAULT_CONFIG, - vaultAddress: NEW_VAULT_ADDRESS, + boringVault: NEW_VAULT_ADDRESS, }, }); @@ -503,7 +519,7 @@ describe('MoneyAccountBalanceService', () => { const updatedConfig = { ...MOCK_VAULT_CONFIG, - underlyingTokenDecimals: 18, + lensAddress: '0x7777777777777777777777777777777777777777' as const, }; publishRFFCStateChange(rootMessenger, { [VAULT_CONFIG_FEATURE_FLAG_KEY]: updatedConfig, @@ -617,6 +633,7 @@ describe('MoneyAccountBalanceService', () => { describe('getMusdBalance', () => { it('returns the mUSD balance for the given address', async () => { + mockAccountantBase(); mockErc20BalanceOf('5000000'); const { service } = createService(); @@ -625,25 +642,29 @@ describe('MoneyAccountBalanceService', () => { expect(result).toStrictEqual({ balance: '5000000' }); }); - it('calls balanceOf on the underlying token contract, not the vault', async () => { + it('first calls base() on the Accountant to resolve the underlying token, then calls balanceOf on it', async () => { + mockAccountantBase(); mockErc20BalanceOf('5000000'); const { service } = createService(); await service.getMusdBalance(MOCK_ACCOUNT_ADDRESS); + // Step 1: Accountant contract instantiated to fetch underlying token address. expect(MockContract).toHaveBeenCalledWith( - MOCK_UNDERLYING_TOKEN_ADDRESS, + MOCK_ACCOUNTANT_ADDRESS, expect.anything(), expect.anything(), ); - expect(MockContract).not.toHaveBeenCalledWith( - MOCK_VAULT_ADDRESS, + // Step 2: ERC-20 contract instantiated with the resolved underlying token address. + expect(MockContract).toHaveBeenCalledWith( + MOCK_UNDERLYING_TOKEN_ADDRESS, expect.anything(), expect.anything(), ); }); it('is also callable via the messenger action', async () => { + mockAccountantBase(); mockErc20BalanceOf('5000000'); const { rootMessenger } = createService(); @@ -674,6 +695,7 @@ describe('MoneyAccountBalanceService', () => { }); it('uses the network client at defaultRpcEndpointIndex, not always index 0', async () => { + mockAccountantBase(); mockErc20BalanceOf('1000000'); const { service, mockGetNetworkConfig, mockGetNetworkClient } = createService(); @@ -846,42 +868,69 @@ describe('MoneyAccountBalanceService', () => { // ---------------------------------------------------------- describe('getMusdEquivalentValue', () => { - it('returns the vault share balance, exchange rate, and computed mUSD-equivalent value', async () => { - // balance = 2_000_000, rate = 1_100_000, decimals = 6 - // equivalent = (2_000_000 * 1_100_000) / 10^6 = 2_200_000 - mockContractsByAddress('2000000', '1100000'); + it('returns balanceOfInAssets from the Veda Lens contract', async () => { + mockLensBalanceOfInAssets('2200000'); const { service } = createService(); const result = await service.getMusdEquivalentValue(MOCK_ACCOUNT_ADDRESS); - expect(result).toStrictEqual({ - musdSHFvdBalance: '2000000', - exchangeRate: '1100000', - musdEquivalentValue: '2200000', - }); + expect(result).toStrictEqual({ balanceOfInAssets: '2200000' }); }); - it('returns zero musdEquivalentValue when the vault share balance is zero', async () => { - mockContractsByAddress('0', '1100000'); + it('returns zero balanceOfInAssets when the account holds no vault shares', async () => { + mockLensBalanceOfInAssets('0'); const { service } = createService(); const result = await service.getMusdEquivalentValue(MOCK_ACCOUNT_ADDRESS); - expect(result.musdEquivalentValue).toBe('0'); + expect(result).toStrictEqual({ balanceOfInAssets: '0' }); }); - it('truncates (floors) fractional mUSD when the product is not evenly divisible', async () => { - // balance = 7, rate = 1_500_000, decimals = 6 - // => (7 * 1_500_000) / 1_000_000 = 10_500_000 / 1_000_000 = 10 (BigInt floors) - mockContractsByAddress('7', '1500000'); + it('instantiates the Lens contract with lensAddress and calls balanceOfInAssets with (accountAddress, boringVault, accountantAddress)', async () => { + const mockBalanceOfInAssets = jest + .fn() + .mockResolvedValue({ toString: () => '1000000' }); + MockContract.mockImplementation( + () => + ({ + balanceOfInAssets: mockBalanceOfInAssets, + }) as unknown as Contract, + ); const { service } = createService(); - const result = await service.getMusdEquivalentValue(MOCK_ACCOUNT_ADDRESS); + await service.getMusdEquivalentValue(MOCK_ACCOUNT_ADDRESS); + + expect(MockContract).toHaveBeenCalledWith( + MOCK_LENS_ADDRESS, + expect.anything(), + expect.anything(), + ); + expect(mockBalanceOfInAssets).toHaveBeenCalledWith( + MOCK_ACCOUNT_ADDRESS, + MOCK_VAULT_ADDRESS, + MOCK_ACCOUNTANT_ADDRESS, + ); + }); - expect(result.musdEquivalentValue).toBe('10'); + it('throws if no network configuration is found for the vault chain', async () => { + const { service, mockGetNetworkConfig } = createService(); + mockGetNetworkConfig.mockReturnValue(undefined); + + await expect( + service.getMusdEquivalentValue(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow('No network configuration found for chain 0xa4b1'); + }); + + it('throws if the network client has no provider', async () => { + const { service, mockGetNetworkClient } = createService(); + mockGetNetworkClient.mockReturnValue({ provider: null }); + + await expect( + service.getMusdEquivalentValue(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow('No provider found for chain 0xa4b1'); }); }); @@ -1045,7 +1094,7 @@ describe('MoneyAccountBalanceService', () => { rffcFlags: { [VAULT_CONFIG_FEATURE_FLAG_KEY]: { ...MOCK_VAULT_CONFIG, - vaultChainId: '0x1', + chainId: '0x1', }, }, }); diff --git a/packages/money-account-balance-service/src/money-account-balance-service.ts b/packages/money-account-balance-service/src/money-account-balance-service.ts index 4d91052fe5..865c51b0ef 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.ts @@ -24,6 +24,7 @@ import { Duration, inMilliseconds } from '@metamask/utils'; import { ACCOUNTANT_ABI, + LENS_ABI, VAULT_CONFIG_FEATURE_FLAG_KEY, VEDA_API_NETWORK_NAMES, VEDA_PERFORMANCE_API_BASE_URL, @@ -214,6 +215,7 @@ export class MoneyAccountBalanceService extends BaseDataService< 'RemoteFeatureFlagController:getState', ); const flagValue = remoteFeatureFlags[VAULT_CONFIG_FEATURE_FLAG_KEY]; + if (flagValue === undefined) { configLogger( 'Init complete — no vault config flag present, awaiting remote flags', @@ -347,21 +349,19 @@ export class MoneyAccountBalanceService extends BaseDataService< * Resolves a Web3Provider for the given chain ID by looking up the network * configuration and client via the messenger. * - * @param vaultChainId - The chain ID to resolve a provider for. + * @param chainId - The chain ID to resolve a provider for. * @returns A Web3Provider connected to the given chain. * @throws If no network configuration exists for the chain, or if the * resolved network client has no provider. */ - #getProvider(vaultChainId: Hex): Web3Provider { + #getProvider(chainId: Hex): Web3Provider { const config = this.messenger.call( 'NetworkController:getNetworkConfigurationByChainId', - vaultChainId, + chainId, ); if (!config) { - throw new Error( - `No network configuration found for chain ${vaultChainId}`, - ); + throw new Error(`No network configuration found for chain ${chainId}`); } const { rpcEndpoints, defaultRpcEndpointIndex } = config; @@ -373,7 +373,7 @@ export class MoneyAccountBalanceService extends BaseDataService< ); if (!networkClient?.provider) { - throw new Error(`No provider found for chain ${vaultChainId}`); + throw new Error(`No provider found for chain ${chainId}`); } return new Web3Provider(networkClient.provider); @@ -384,20 +384,34 @@ export class MoneyAccountBalanceService extends BaseDataService< * * @param contractAddress - The address of the ERC-20 contract. * @param accountAddress - The address of the account. - * @param vaultChainId - The chain ID to use for the provider. + * @param chainId - The chain ID to use for the provider. * @returns The balance as a raw uint256 string. */ async #fetchErc20Balance( contractAddress: Hex, accountAddress: Hex, - vaultChainId: Hex, + chainId: Hex, ): Promise { - const provider = this.#getProvider(vaultChainId); + const provider = this.#getProvider(chainId); const contract = new Contract(contractAddress, abiERC20, provider); const balance = await contract.balanceOf(accountAddress); return balance.toString(); } + /** + * Fetches the underlying token address from the Accountant contract via RPC. + * + * @param chainId - The chain ID to use for the provider. + * @returns The underlying token address as a hex string. + */ + async #fetchUnderlyingTokenAddress(chainId: Hex): Promise { + const { accountantAddress } = this.#requireConfig(); + const provider = this.#getProvider(chainId); + const contract = new Contract(accountantAddress, ACCOUNTANT_ABI, provider); + const underlyingTokenAddress = await contract.base(); + return underlyingTokenAddress; + } + /** * Fetches the mUSD ERC-20 balance for the given account address via RPC. * @@ -409,11 +423,15 @@ export class MoneyAccountBalanceService extends BaseDataService< return this.fetchQuery({ queryKey: [`${this.name}:getMusdBalance`, accountAddress], queryFn: async () => { - const { underlyingTokenAddress, vaultChainId } = this.#requireConfig(); + const { chainId } = this.#requireConfig(); + + const underlyingTokenAddress = + await this.#fetchUnderlyingTokenAddress(chainId); + const balance = await this.#fetchErc20Balance( underlyingTokenAddress, accountAddress, - vaultChainId, + chainId, ); return { balance }; }, @@ -433,11 +451,11 @@ export class MoneyAccountBalanceService extends BaseDataService< return this.fetchQuery({ queryKey: [`${this.name}:getMusdSHFvdBalance`, accountAddress], queryFn: async () => { - const { vaultAddress, vaultChainId } = this.#requireConfig(); + const { boringVault, chainId } = this.#requireConfig(); const balance = await this.#fetchErc20Balance( - vaultAddress, + boringVault, accountAddress, - vaultChainId, + chainId, ); return { balance }; }, @@ -461,8 +479,8 @@ export class MoneyAccountBalanceService extends BaseDataService< return this.fetchQuery({ queryKey: [`${this.name}:getExchangeRate`], queryFn: async () => { - const { accountantAddress, vaultChainId } = this.#requireConfig(); - const provider = this.#getProvider(vaultChainId); + const { accountantAddress, chainId } = this.#requireConfig(); + const provider = this.#getProvider(chainId); const contract = new Contract( accountantAddress, ACCOUNTANT_ABI, @@ -489,27 +507,23 @@ export class MoneyAccountBalanceService extends BaseDataService< async getMusdEquivalentValue( accountAddress: Hex, ): Promise { - const [{ balance: musdSHFvdBalance }, { rate: exchangeRate }] = - await Promise.all([ - this.getMusdSHFvdBalance(accountAddress), - this.getExchangeRate(), - ]); - - const { underlyingTokenDecimals } = this.#requireConfig(); - - const balanceBigInt = BigInt(musdSHFvdBalance); - const rateBigInt = BigInt(exchangeRate); - - const musdEquivalentValue = ( - (balanceBigInt * rateBigInt) / - 10n ** BigInt(underlyingTokenDecimals) - ).toString(); - - return { - musdSHFvdBalance, - exchangeRate, - musdEquivalentValue, - }; + return this.fetchQuery({ + queryKey: [`${this.name}:getMusdEquivalentValue`, accountAddress], + queryFn: async () => { + const { lensAddress, boringVault, accountantAddress, chainId } = + this.#requireConfig(); + const provider = this.#getProvider(chainId); + const contract = new Contract(lensAddress, LENS_ABI, provider); + const balanceOfInAssets = await contract.balanceOfInAssets( + accountAddress, + boringVault, + accountantAddress, + ); + + return { balanceOfInAssets: balanceOfInAssets.toString() }; + }, + staleTime: inMilliseconds(30, Duration.Second), + }); } /** @@ -522,17 +536,17 @@ export class MoneyAccountBalanceService extends BaseDataService< return this.fetchQuery({ queryKey: [`${this.name}:getVaultApy`], queryFn: async () => { - const { vaultChainId, vaultAddress } = this.#requireConfig(); - const networkName = VEDA_API_NETWORK_NAMES[vaultChainId]; + const { chainId, boringVault } = this.#requireConfig(); + const networkName = VEDA_API_NETWORK_NAMES[chainId]; if (!networkName) { throw new Error( - `No Veda API network name found for chain ${vaultChainId}`, + `No Veda API network name found for chain ${chainId}`, ); } const url = new URL( - `/performance/${networkName}/${vaultAddress}`, + `/performance/${networkName}/${boringVault}`, VEDA_PERFORMANCE_API_BASE_URL, ); diff --git a/packages/money-account-balance-service/src/response.types.ts b/packages/money-account-balance-service/src/response.types.ts index 9f69321fbb..d8b13c80dd 100644 --- a/packages/money-account-balance-service/src/response.types.ts +++ b/packages/money-account-balance-service/src/response.types.ts @@ -8,13 +8,10 @@ export type ExchangeRateResponse = { /** * Response from {@link MoneyAccountBalanceService.getMusdEquivalentValue}. - * All values are raw uint256 strings. The `musdEquivalentValue` is - * `musdSHFvdBalance * exchangeRate / 10^underlyingTokenDecimals` (= 1e6 for mUSD). + * Balance of in assets is the raw uint256 string returned by the Lens's `balanceOfInAssets()`. */ export type MusdEquivalentValueResponse = { - musdSHFvdBalance: string; - exchangeRate: string; - musdEquivalentValue: string; + balanceOfInAssets: string; }; /** diff --git a/packages/money-account-balance-service/src/structs.ts b/packages/money-account-balance-service/src/structs.ts index 9eb56439b7..dbc1563e55 100644 --- a/packages/money-account-balance-service/src/structs.ts +++ b/packages/money-account-balance-service/src/structs.ts @@ -15,11 +15,11 @@ import { StrictHexStruct } from '@metamask/utils'; * flag in future do not break existing clients. */ export const VaultConfigStruct = type({ - vaultAddress: StrictHexStruct, - vaultChainId: StrictHexStruct, accountantAddress: StrictHexStruct, - underlyingTokenAddress: StrictHexStruct, - underlyingTokenDecimals: number(), + boringVault: StrictHexStruct, + lensAddress: StrictHexStruct, + tellerAddress: StrictHexStruct, + chainId: StrictHexStruct, }); /** diff --git a/packages/money-account-balance-service/src/types.ts b/packages/money-account-balance-service/src/types.ts index b367738ef5..eef9a84f33 100644 --- a/packages/money-account-balance-service/src/types.ts +++ b/packages/money-account-balance-service/src/types.ts @@ -5,9 +5,9 @@ import type { Hex } from '@metamask/utils'; * Runtime validation is performed by {@link VaultConfigStruct}. */ export type VaultConfig = { - vaultAddress: Hex; - vaultChainId: Hex; + boringVault: Hex; + tellerAddress: Hex; accountantAddress: Hex; - underlyingTokenAddress: Hex; - underlyingTokenDecimals: number; + lensAddress: Hex; + chainId: Hex; }; From 9ae531a6e26385a2171a0d14d090de9135af67c5 Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Fri, 8 May 2026 13:32:03 -0400 Subject: [PATCH 12/15] chore: update changelog --- packages/money-account-balance-service/CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/money-account-balance-service/CHANGELOG.md b/packages/money-account-balance-service/CHANGELOG.md index ffeb4bfca7..9179a9cdab 100644 --- a/packages/money-account-balance-service/CHANGELOG.md +++ b/packages/money-account-balance-service/CHANGELOG.md @@ -10,12 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `VaultConfigNotAvailableError` and `VaultConfigValidationError` error classes for typed consumer error handling ([#8742](https://github.com/MetaMask/core/pull/8742)) -- `MoneyAccountBalanceService` now subscribes to `RemoteFeatureFlagController:stateChange` and invalidates all cached queries when vault config changes ([#8742](https://github.com/MetaMask/core/pull/8742)) +- Add `LENS_ABI` constant for the Arctic Architecture Lens contract ([#8742](https://github.com/MetaMask/core/pull/8742)) ### Changed -- **BREAKING:** `MoneyAccountBalanceService` no longer accepts vault config via constructor (`vaultAddress`, `vaultChainId`, `accountantAddress`, `underlyingTokenAddress`, `underlyingTokenDecimals`). Vault config is now read from remote feature flag via `RemoteFeatureFlagController`. The service requires `RemoteFeatureFlagController` to be registered on the messenger. Methods throw `VaultConfigNotAvailableError` until a valid config has been loaded from remote flags. ([#8742](https://github.com/MetaMask/core/pull/8742)) - - Add `@metamask/remote-feature-flag-controller` as a dependency and ensure `RemoteFeatureFlagController:getState` and `RemoteFeatureFlagController:stateChange` are permitted on the messenger passed to `MoneyAccountBalanceService`. +- **BREAKING:** `MoneyAccountBalanceService` no longer accepts vault config via constructor. Vault config is now read from `RemoteFeatureFlagController` state. Add `@metamask/remote-feature-flag-controller` as a dependency and permit `RemoteFeatureFlagController:getState`action and `RemoteFeatureFlagController:stateChange` event on the service's messenger. Service methods throw `VaultConfigNotAvailableError` until a valid config is available. ([#8742](https://github.com/MetaMask/core/pull/8742)) +- **BREAKING:** `VaultConfig` fields have changed — `vaultAddress` → `boringVault`, `vaultChainId` → `chainId`; `underlyingTokenAddress` and `underlyingTokenDecimals` removed; `lensAddress` and `tellerAddress` added ([#8742](https://github.com/MetaMask/core/pull/8742)) +- **BREAKING:** `MusdEquivalentValueResponse` shape has changed — `musdSHFvdBalance`, `exchangeRate`, and `musdEquivalentValue` replaced by a single `balanceOfInAssets` field ([#8742](https://github.com/MetaMask/core/pull/8742)) +- Monad (`0x8f`) added to `VEDA_API_NETWORK_NAMES` and set as the new `DEFAULT_VEDA_API_NETWORK_NAME` ([#8742](https://github.com/MetaMask/core/pull/8742)) - Bump `@metamask/messenger` from `^1.1.1` to `^1.2.0` ([#8632](https://github.com/MetaMask/core/pull/8632)) - Bump `@metamask/network-controller` from `^30.0.1` to `^30.1.0` ([#8636](https://github.com/MetaMask/core/pull/8636)) From 1f84c7e9fb171f398f6334bc28d40e486c7e3c09 Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Fri, 8 May 2026 13:43:17 -0400 Subject: [PATCH 13/15] feat: resolve formatting issues --- .../src/money-account-balance-service.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/money-account-balance-service/src/money-account-balance-service.test.ts b/packages/money-account-balance-service/src/money-account-balance-service.test.ts index 6437b98380..be9778970c 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.test.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.test.ts @@ -286,9 +286,7 @@ function mockAccountantBase(): void { MockContract.mockImplementationOnce( () => ({ - base: jest - .fn() - .mockResolvedValue(MOCK_UNDERLYING_TOKEN_ADDRESS), + base: jest.fn().mockResolvedValue(MOCK_UNDERLYING_TOKEN_ADDRESS), }) as unknown as Contract, ); } From 4be34725520cf8909ebf8e957b89c19a47fbd84d Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Fri, 8 May 2026 13:47:59 -0400 Subject: [PATCH 14/15] feat: removed unused DEFAULT_VEDA_API_NETWORK_NAME constant --- packages/money-account-balance-service/CHANGELOG.md | 2 +- packages/money-account-balance-service/src/constants.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/money-account-balance-service/CHANGELOG.md b/packages/money-account-balance-service/CHANGELOG.md index 9179a9cdab..d884500b2a 100644 --- a/packages/money-account-balance-service/CHANGELOG.md +++ b/packages/money-account-balance-service/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** `MoneyAccountBalanceService` no longer accepts vault config via constructor. Vault config is now read from `RemoteFeatureFlagController` state. Add `@metamask/remote-feature-flag-controller` as a dependency and permit `RemoteFeatureFlagController:getState`action and `RemoteFeatureFlagController:stateChange` event on the service's messenger. Service methods throw `VaultConfigNotAvailableError` until a valid config is available. ([#8742](https://github.com/MetaMask/core/pull/8742)) - **BREAKING:** `VaultConfig` fields have changed — `vaultAddress` → `boringVault`, `vaultChainId` → `chainId`; `underlyingTokenAddress` and `underlyingTokenDecimals` removed; `lensAddress` and `tellerAddress` added ([#8742](https://github.com/MetaMask/core/pull/8742)) - **BREAKING:** `MusdEquivalentValueResponse` shape has changed — `musdSHFvdBalance`, `exchangeRate`, and `musdEquivalentValue` replaced by a single `balanceOfInAssets` field ([#8742](https://github.com/MetaMask/core/pull/8742)) -- Monad (`0x8f`) added to `VEDA_API_NETWORK_NAMES` and set as the new `DEFAULT_VEDA_API_NETWORK_NAME` ([#8742](https://github.com/MetaMask/core/pull/8742)) +- Monad (`0x8f`) added to `VEDA_API_NETWORK_NAMES` ([#8742](https://github.com/MetaMask/core/pull/8742)) - Bump `@metamask/messenger` from `^1.1.1` to `^1.2.0` ([#8632](https://github.com/MetaMask/core/pull/8632)) - Bump `@metamask/network-controller` from `^30.0.1` to `^30.1.0` ([#8636](https://github.com/MetaMask/core/pull/8636)) diff --git a/packages/money-account-balance-service/src/constants.ts b/packages/money-account-balance-service/src/constants.ts index f6197597b5..17524152c2 100644 --- a/packages/money-account-balance-service/src/constants.ts +++ b/packages/money-account-balance-service/src/constants.ts @@ -13,8 +13,6 @@ export const VEDA_API_NETWORK_NAMES: Record = { '0x8f': 'monad', }; -export const DEFAULT_VEDA_API_NETWORK_NAME = VEDA_API_NETWORK_NAMES['0x8f']; - /** * Minimal ABI for the Veda Accountant contract. Covers: * - base (0x5001f3b5) — the underlying ERC20 base asset address From 5e603a6c05a672f1ebde9d694f19e9f49a21e9e1 Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Fri, 8 May 2026 13:52:18 -0400 Subject: [PATCH 15/15] feat: clean up LENS_ABI outdated comment --- packages/money-account-balance-service/src/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/money-account-balance-service/src/constants.ts b/packages/money-account-balance-service/src/constants.ts index 17524152c2..d1050a064d 100644 --- a/packages/money-account-balance-service/src/constants.ts +++ b/packages/money-account-balance-service/src/constants.ts @@ -41,7 +41,6 @@ export const ACCOUNTANT_ABI = [ * Covers: * - balanceOf (0xf7888aec) — shares held by an account in a BoringVault * - balanceOfInAssets (0x789fd871) — share balance denominated in underlying assets - * - exchangeRate (0xdc3b7c8b) — current rate from an AccountantWithRateProviders */ export const LENS_ABI = [ {