diff --git a/packages/snap-networks-utils/CHANGELOG.md b/packages/snap-networks-utils/CHANGELOG.md index a8e57931a..2c00c4487 100644 --- a/packages/snap-networks-utils/CHANGELOG.md +++ b/packages/snap-networks-utils/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Export namespace-agnostic `MessengerCaller` / `CanCall` types for Snap Core messenger endowments ([#138](https://github.com/MetaMask/internal-snaps/pull/138)) + +### Changed + +- Type `AssetsProviderMessenger` and `RemoteFeatureFlagsProviderMessenger` as `MessengerCaller`s so a Snap Core `getMessenger` result (Snap namespace + action superset) is assignable without casts ([#138](https://github.com/MetaMask/internal-snaps/pull/138)) + ## [1.0.0] ### Added diff --git a/packages/snap-networks-utils/README.md b/packages/snap-networks-utils/README.md index 4f615c52d..19ab6404a 100644 --- a/packages/snap-networks-utils/README.md +++ b/packages/snap-networks-utils/README.md @@ -30,7 +30,10 @@ snapLogger.info('account synced'); ### Core AssetsController reads -Wire the Snap messenger endowment, then pass it to `AssetsProvider`: +Wire the Snap messenger endowment, then pass it to `AssetsProvider`. Provider +messenger types are namespace-agnostic callers, so the Snap Core messenger (which +uses the Snap's own namespace and may expose additional actions) is assignable +without casts: ```typescript import type { Messenger } from '@metamask/messenger'; @@ -48,7 +51,9 @@ type CoreMessengerActions = | AssetsControllerGetAccountAssetsByIDsAction | AssetsControllerGetAccountAssetsByScopeAction; -const messenger = getMessenger>(); +type CoreMessengerConstraint = Messenger<'ExampleSnap', CoreMessengerActions>; + +const messenger = getMessenger(); const assetsProvider = new AssetsProvider({ messenger }); const accountId: AccountId = '550e8400-e29b-41d4-a716-446655440000'; diff --git a/packages/snap-networks-utils/src/index.ts b/packages/snap-networks-utils/src/index.ts index 421763112..e97fb5afd 100644 --- a/packages/snap-networks-utils/src/index.ts +++ b/packages/snap-networks-utils/src/index.ts @@ -10,3 +10,4 @@ export { RemoteFeatureFlagsProvider, type RemoteFeatureFlagsProviderMessenger, } from './providers/remote-feature-flags/RemoteFeatureFlagsProvider'; +export type { CanCall, MessengerCaller } from './types/messenger-caller'; diff --git a/packages/snap-networks-utils/src/providers/assets/AssetsProvider.ts b/packages/snap-networks-utils/src/providers/assets/AssetsProvider.ts index 25f22abd7..9354bc0ff 100644 --- a/packages/snap-networks-utils/src/providers/assets/AssetsProvider.ts +++ b/packages/snap-networks-utils/src/providers/assets/AssetsProvider.ts @@ -5,10 +5,10 @@ import type { AssetsControllerGetAccountAssetsByScopeAction, Caip19AssetId, } from '@metamask/assets-controller'; -import type { Messenger } from '@metamask/messenger'; -import { AsyncMessenger } from '@metamask/snaps-sdk'; import type { CaipChainId } from '@metamask/utils'; +import type { MessengerCaller } from '../../types/messenger-caller'; + /** * Namespace for the {@link AssetsProvider} messenger. */ @@ -23,11 +23,14 @@ export type AssetsProviderAllowedActions = | AssetsControllerGetAccountAssetsByScopeAction; /** - * Messenger restricted to actions consumed by {@link AssetsProvider}. + * Messenger caller for actions consumed by {@link AssetsProvider}. + * + * Namespace-agnostic so a Snap Core `getMessenger` endowment (which uses the + * Snap's own namespace and may expose additional actions) is assignable without + * casts. */ -export type AssetsProviderMessenger = AsyncMessenger< - Messenger ->; +export type AssetsProviderMessenger = + MessengerCaller; export class AssetsProvider { readonly #messenger: AssetsProviderMessenger; diff --git a/packages/snap-networks-utils/src/providers/remote-feature-flags/RemoteFeatureFlagsProvider.ts b/packages/snap-networks-utils/src/providers/remote-feature-flags/RemoteFeatureFlagsProvider.ts index f73265f03..c5a3e576c 100644 --- a/packages/snap-networks-utils/src/providers/remote-feature-flags/RemoteFeatureFlagsProvider.ts +++ b/packages/snap-networks-utils/src/providers/remote-feature-flags/RemoteFeatureFlagsProvider.ts @@ -1,8 +1,8 @@ -import type { Messenger } from '@metamask/messenger'; import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; -import { AsyncMessenger } from '@metamask/snaps-sdk'; import type { Json } from '@metamask/utils'; +import type { MessengerCaller } from '../../types/messenger-caller'; + /** * Namespace for the {@link RemoteFeatureFlagsProvider} messenger. */ @@ -16,14 +16,14 @@ export type RemoteFeatureFlagsProviderAllowedActions = RemoteFeatureFlagControllerGetStateAction; /** - * Messenger restricted to actions consumed by {@link RemoteFeatureFlagsProvider}. + * Messenger caller for actions consumed by {@link RemoteFeatureFlagsProvider}. + * + * Namespace-agnostic so a Snap Core `getMessenger` endowment (which uses the + * Snap's own namespace and may expose additional actions) is assignable without + * casts. */ -export type RemoteFeatureFlagsProviderMessenger = AsyncMessenger< - Messenger< - typeof REMOTE_FEATURE_FLAGS_PROVIDER_NAME, - RemoteFeatureFlagsProviderAllowedActions - > ->; +export type RemoteFeatureFlagsProviderMessenger = + MessengerCaller; export class RemoteFeatureFlagsProvider { readonly #messenger: RemoteFeatureFlagsProviderMessenger; diff --git a/packages/snap-networks-utils/src/types/messenger-caller.test.ts b/packages/snap-networks-utils/src/types/messenger-caller.test.ts new file mode 100644 index 000000000..cee8f6897 --- /dev/null +++ b/packages/snap-networks-utils/src/types/messenger-caller.test.ts @@ -0,0 +1,51 @@ +import type { + AssetsControllerGetAccountAssetByIDAction, + AssetsControllerGetAccountAssetsByIDsAction, + AssetsControllerGetAccountAssetsByScopeAction, +} from '@metamask/assets-controller'; +import type { Messenger } from '@metamask/messenger'; +import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; +import type { AsyncMessenger } from '@metamask/snaps-sdk'; + +import type { MessengerCaller } from './messenger-caller'; + +type AssetsActions = + | AssetsControllerGetAccountAssetByIDAction + | AssetsControllerGetAccountAssetsByIDsAction + | AssetsControllerGetAccountAssetsByScopeAction; + +type CoreActions = AssetsActions | RemoteFeatureFlagControllerGetStateAction; + +type CoreAsyncMessenger = AsyncMessenger< + Messenger<'TestSnapMessenger', CoreActions> +>; + +/** + * Compile-time assertion helper. If `Value` is not assignable to `Target`, + * TypeScript fails this file. + * + * @param value - Value that must be assignable to `Target`. + * @returns The same value. + */ +function assertAssignable(value: Target): Target { + return value; +} + +describe('MessengerCaller', () => { + it('accepts a Core AsyncMessenger that exposes a superset of actions', () => { + const coreMessenger = { + call: jest.fn(), + } as unknown as CoreAsyncMessenger; + + // These assignments must typecheck without casts. + const assetsCaller: MessengerCaller = + assertAssignable>(coreMessenger); + const flagsCaller: MessengerCaller = + assertAssignable< + MessengerCaller + >(coreMessenger); + + expect(assetsCaller).toBe(coreMessenger); + expect(flagsCaller).toBe(coreMessenger); + }); +}); diff --git a/packages/snap-networks-utils/src/types/messenger-caller.ts b/packages/snap-networks-utils/src/types/messenger-caller.ts new file mode 100644 index 000000000..0ccdf6d5c --- /dev/null +++ b/packages/snap-networks-utils/src/types/messenger-caller.ts @@ -0,0 +1,45 @@ +import type { + ActionConstraint, + ExtractActionParameters, + ExtractActionResponse, +} from '@metamask/messenger'; + +/** + * Ability to call a single messenger action through an async Snap messenger. + * + * Prefer composing these with {@link MessengerCaller} rather than using + * `AsyncMessenger>` directly: Snap + * `getMessenger` endowments use the Snap's own namespace and often expose a + * superset of actions, so namespace-bound `AsyncMessenger` types force unsafe + * casts at the call site. + */ +export type CanCall = { + call( + actionType: Action['type'], + ...params: ExtractActionParameters + ): Promise>>; +}; + +/** + * Convert a union to an intersection (distributive). + * + * @template Union - Union to convert. + */ +type UnionToIntersection = ( + Union extends unknown ? (value: Union) => void : never +) extends (value: infer Intersection) => void + ? Intersection + : never; + +/** + * Namespace-agnostic caller for a fixed set of messenger actions. + * + * Distributes {@link CanCall} over an action union so a Snap Core messenger + * that exposes a superset of actions remains assignable without casts. + * + * @template Actions - Action constraint union the caller must support. + */ +export type MessengerCaller = + UnionToIntersection< + Actions extends ActionConstraint ? CanCall : never + >; diff --git a/packages/tron-wallet-snap/CHANGELOG.md b/packages/tron-wallet-snap/CHANGELOG.md index 2509a5c7a..f54ec3c66 100644 --- a/packages/tron-wallet-snap/CHANGELOG.md +++ b/packages/tron-wallet-snap/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Scope `bip44:discover` activity checks and account creation to the networks declared in the snap manifest, preventing unnecessary calls to testnet APIs during discovery ([#135](https://github.com/MetaMask/internal-snaps/pull/135)) +- Type Core messenger as `AsyncMessenger` and pass it to shared providers without casts ([#138](https://github.com/MetaMask/internal-snaps/pull/138)) ## [3.0.0] diff --git a/packages/tron-wallet-snap/src/context.ts b/packages/tron-wallet-snap/src/context.ts index 278d549be..57a1089d2 100644 --- a/packages/tron-wallet-snap/src/context.ts +++ b/packages/tron-wallet-snap/src/context.ts @@ -2,10 +2,6 @@ import { AssetsProvider, RemoteFeatureFlagsProvider, } from '@metamask/snap-networks-utils'; -import type { - AssetsProviderMessenger, - RemoteFeatureFlagsProviderMessenger, -} from '@metamask/snap-networks-utils'; import { getMessenger } from '@metamask/snaps-sdk'; import { InMemoryCache } from './caching/InMemoryCache'; @@ -39,7 +35,10 @@ import { TransactionScanService } from './services/transaction-scan/TransactionS import { TransactionsRepository } from './services/transactions/TransactionsRepository'; import { TransactionsService } from './services/transactions/TransactionsService'; import { WalletService } from './services/wallet/WalletService'; -import type { CoreMessenger } from './types/core-messenger'; +import type { + CoreMessenger, + CoreMessengerConstraint, +} from './types/core-messenger'; import logger, { noOpLogger } from './utils/logger'; /** @@ -96,12 +95,12 @@ const tokenApiClient = new TokenApiClient(configProvider); /** * Core controllers plumbing */ -const coreMessenger = getMessenger(); +const coreMessenger = getMessenger(); const remoteFeatureFlagsProvider = new RemoteFeatureFlagsProvider({ - messenger: coreMessenger as RemoteFeatureFlagsProviderMessenger, + messenger: coreMessenger, }); const assetsProvider = new AssetsProvider({ - messenger: coreMessenger as AssetsProviderMessenger, + messenger: coreMessenger, }); // Security Alerts API client diff --git a/packages/tron-wallet-snap/src/types/core-messenger.ts b/packages/tron-wallet-snap/src/types/core-messenger.ts index 7c826d766..b80a28087 100644 --- a/packages/tron-wallet-snap/src/types/core-messenger.ts +++ b/packages/tron-wallet-snap/src/types/core-messenger.ts @@ -5,6 +5,7 @@ import type { } from '@metamask/assets-controller'; import type { Messenger } from '@metamask/messenger'; import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; +import type { AsyncMessenger } from '@metamask/snaps-sdk'; /** * Namespace for this Snap's Core messenger endowment. @@ -18,9 +19,15 @@ export type CoreMessengerActions = | AssetsControllerGetAccountAssetsByScopeAction; /** - * Messenger type passed to `getMessenger` for Core controller actions. + * Sync messenger constraint passed to `getMessenger` for Core controller + * actions. Runtime value is {@link CoreMessenger}. */ -export type CoreMessenger = Messenger< +export type CoreMessengerConstraint = Messenger< typeof TRON_WALLET_SNAP_MESSENGER_NAMESPACE, CoreMessengerActions >; + +/** + * Async messenger returned by `getMessenger` for Core controller actions. + */ +export type CoreMessenger = AsyncMessenger;