diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 450437656f3..e601069c671 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `withReadonlyKeyring` action ([#5727](https://github.com/MetaMask/core/pull/5727)) + ### Changed - Bump `@metamask/base-controller` from ^8.0.0 to ^8.0.1 ([#5722](https://github.com/MetaMask/core/pull/5722)) diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index f30a0a46387..d4bb6eeced4 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -192,6 +192,11 @@ export type KeyringControllerWithKeyringAction = { handler: KeyringController['withKeyring']; }; +export type KeyringControllerWithReadonlyKeyringAction = { + type: `${typeof name}:withReadonlyKeyring`; + handler: KeyringController['withReadonlyKeyring']; +}; + export type KeyringControllerStateChangeEvent = { type: `${typeof name}:stateChange`; payload: [KeyringControllerState, Patch[]]; @@ -233,7 +238,8 @@ export type KeyringControllerActions = | KeyringControllerPatchUserOperationAction | KeyringControllerSignUserOperationAction | KeyringControllerAddNewAccountAction - | KeyringControllerWithKeyringAction; + | KeyringControllerWithKeyringAction + | KeyringControllerWithReadonlyKeyringAction; export type KeyringControllerEvents = | KeyringControllerStateChangeEvent @@ -1465,6 +1471,74 @@ export class KeyringController extends BaseController< ); } + /** + * Execute an operation on the selected keyring. + * + * @param selector - Keyring selector object. + * @param operation - Function to execute with the selected keyring. + * @param options - Additional options. + * @returns Promise resolving to the result of the function execution. + * @template SelectedKeyring - The type of the selected keyring. + * @template CallbackResult - The type of the value resolved by the callback function. + */ + async #executeWithKeyring< + SelectedKeyring extends EthKeyring = EthKeyring, + CallbackResult = void, + >( + selector: KeyringSelector, + operation: ({ + keyring, + metadata, + }: { + keyring: SelectedKeyring; + metadata: KeyringMetadata; + }) => Promise, + + options: + | { createIfMissing?: false } + | { createIfMissing: true; createWithData?: unknown }, + ): Promise { + let keyring: SelectedKeyring | undefined; + + if ('address' in selector) { + keyring = (await this.getKeyringForAccount(selector.address)) as + | SelectedKeyring + | undefined; + } else if ('type' in selector) { + keyring = this.getKeyringsByType(selector.type)[selector.index || 0] as + | SelectedKeyring + | undefined; + + if (!keyring && options.createIfMissing) { + keyring = (await this.#newKeyring( + selector.type, + options.createWithData, + )) as SelectedKeyring; + } + } else if ('id' in selector) { + keyring = this.#getKeyringById(selector.id) as SelectedKeyring; + } + + if (!keyring) { + throw new Error(KeyringControllerError.KeyringNotFound); + } + + const result = await operation({ + keyring, + metadata: this.#getKeyringMetadata(keyring), + }); + + if (Object.is(result, keyring)) { + // Access to a keyring instance outside of controller safeguards + // should be discouraged, as it can lead to unexpected behavior. + // This error is thrown to prevent consumers using `withKeyring` + // as a way to get a reference to a keyring instance. + throw new Error(KeyringControllerError.UnsafeDirectKeyringAccess); + } + + return result; + } + /** * Select a keyring and execute the given operation with * the selected keyring, as a mutually exclusive atomic @@ -1552,45 +1626,60 @@ export class KeyringController extends BaseController< this.#assertIsUnlocked(); return this.#persistOrRollback(async () => { - let keyring: SelectedKeyring | undefined; - - if ('address' in selector) { - keyring = (await this.getKeyringForAccount(selector.address)) as - | SelectedKeyring - | undefined; - } else if ('type' in selector) { - keyring = this.getKeyringsByType(selector.type)[selector.index || 0] as - | SelectedKeyring - | undefined; - - if (!keyring && options.createIfMissing) { - keyring = (await this.#newKeyring( - selector.type, - options.createWithData, - )) as SelectedKeyring; - } - } else if ('id' in selector) { - keyring = this.#getKeyringById(selector.id) as SelectedKeyring; - } - - if (!keyring) { - throw new Error(KeyringControllerError.KeyringNotFound); - } + return await this.#executeWithKeyring(selector, operation, options); + }); + } - const result = await operation({ - keyring, - metadata: this.#getKeyringMetadata(keyring), - }); + /** + * Select a keyring and execute the given operation with + * the selected keyring, as a mutually exclusive atomic + * operation. + * + * The method won't persists changes at the end of the + * function execution. + * + * @param selector - Keyring selector object. + * @param operation - Function to execute with the selected keyring. + * @returns Promise resolving to the result of the function execution. + * @template SelectedKeyring - The type of the selected keyring. + * @template CallbackResult - The type of the value resolved by the callback function. + */ + async withReadonlyKeyring< + SelectedKeyring extends EthKeyring = EthKeyring, + CallbackResult = void, + >( + selector: KeyringSelector, + operation: ({ + keyring, + metadata, + }: { + keyring: Readonly; + metadata: KeyringMetadata; + }) => Promise, + ): Promise; - if (Object.is(result, keyring)) { - // Access to a keyring instance outside of controller safeguards - // should be discouraged, as it can lead to unexpected behavior. - // This error is thrown to prevent consumers using `withKeyring` - // as a way to get a reference to a keyring instance. - throw new Error(KeyringControllerError.UnsafeDirectKeyringAccess); - } + async withReadonlyKeyring< + SelectedKeyring extends EthKeyring = EthKeyring, + CallbackResult = void, + >( + selector: KeyringSelector, + operation: ({ + keyring, + metadata, + }: { + keyring: Readonly; + metadata: KeyringMetadata; + }) => Promise, + options: + | { createIfMissing?: false } + | { createIfMissing: true; createWithData?: unknown } = { + createIfMissing: false, + }, + ): Promise { + this.#assertIsUnlocked(); - return result; + return this.#withControllerLock(async () => { + return await this.#executeWithKeyring(selector, operation, options); }); } @@ -1913,6 +2002,11 @@ export class KeyringController extends BaseController< `${name}:withKeyring`, this.withKeyring.bind(this), ); + + this.messagingSystem.registerActionHandler( + `${name}:withReadonlyKeyring`, + this.withReadonlyKeyring.bind(this), + ); } /**