Skip to content

Commit 3be5fcc

Browse files
authored
feat(keyring-controller): allow non-evm addresses for non-evm methods/functions (#4282)
## Explanation This allows support of non-EVM addresses for most keyring methods. Right now, our addresses are just plain `string`, meaning we have not easy way to detect if an address is an ethereum one or not. So we use runtime check to verify this, and based on this we adapt some logic (mostly the address normalization right now). Also, some `Hex` were being dropped in favor of `string` type to make addresses a bit more "generic". ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? For example: * Fixes #12345 * Related to #67890 --> ## Changelog <!-- If you're making any consumer-facing changes, list those changes here as if you were updating a changelog, using the template below as a guide. (CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or FIXED. For security-related issues, follow the Security Advisory process.) Please take care to name the exact pieces of the API you've added or changed (e.g. types, interfaces, functions, or methods). If there are any breaking changes, make sure to offer a solution for consumers to follow once they upgrade to the changes. Finally, if you're only making changes to development scripts or tests, you may replace the template below with "None". --> ### `@metamask/keyring-controller` - **ADDED**: Basic support for non-EVM addresses ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate
1 parent 2752a88 commit 3be5fcc

File tree

2 files changed

+77
-41
lines changed

2 files changed

+77
-41
lines changed

packages/keyring-controller/src/KeyringController.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,7 @@ describe('KeyringController', () => {
657657
await expect(
658658
controller.exportAccount(password, ''),
659659
).rejects.toThrow(
660-
'KeyringController - No keyring found. Error info: The address passed in is invalid/empty',
660+
'KeyringController - No keyring found. Error info: There are keyrings, but none match the address',
661661
);
662662
});
663663
});
@@ -688,7 +688,7 @@ describe('KeyringController', () => {
688688

689689
describe('when the keyring for the given address does not support exportAccount', () => {
690690
it('should throw error', async () => {
691-
const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19';
691+
const address = '0x5AC6D462f054690a373FABF8CC28e161003aEB19';
692692
stubKeyringClassWithAccount(MockKeyring, address);
693693
await withController(
694694
{ keyringBuilders: [keyringBuilderFactory(MockKeyring)] },
@@ -737,7 +737,7 @@ describe('KeyringController', () => {
737737

738738
describe('when the keyring for the given address does not support getEncryptionPublicKey', () => {
739739
it('should throw error', async () => {
740-
const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19';
740+
const address = '0x5AC6D462f054690a373FABF8CC28e161003aEB19';
741741
stubKeyringClassWithAccount(MockKeyring, address);
742742
await withController(
743743
{ keyringBuilders: [keyringBuilderFactory(MockKeyring)] },
@@ -799,15 +799,15 @@ describe('KeyringController', () => {
799799
await expect(
800800
controller.decryptMessage(messageParams),
801801
).rejects.toThrow(
802-
'KeyringController - No keyring found. Error info: The address passed in is invalid/empty',
802+
'KeyringController - No keyring found. Error info: There are keyrings, but none match the address',
803803
);
804804
});
805805
});
806806
});
807807

808808
describe('when the keyring for the given address does not support decryptMessage', () => {
809809
it('should throw error', async () => {
810-
const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19';
810+
const address = '0x5AC6D462f054690a373FABF8CC28e161003aEB19';
811811
stubKeyringClassWithAccount(MockKeyring, address);
812812
await withController(
813813
{ keyringBuilders: [keyringBuilderFactory(MockKeyring)] },
@@ -1181,15 +1181,15 @@ describe('KeyringController', () => {
11811181
await expect(
11821182
controller.removeAccount('0xDUMMY_INPUT'),
11831183
).rejects.toThrow(
1184-
'KeyringController - No keyring found. Error info: The address passed in is invalid/empty',
1184+
'KeyringController - No keyring found. Error info: There are keyrings, but none match the address',
11851185
);
11861186
});
11871187
});
11881188
});
11891189

11901190
describe('when the keyring for the given address does not support removeAccount', () => {
11911191
it('should throw error', async () => {
1192-
const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19';
1192+
const address = '0x5AC6D462f054690a373FABF8CC28e161003aEB19';
11931193
stubKeyringClassWithAccount(MockKeyring, address);
11941194
await withController(
11951195
{ keyringBuilders: [keyringBuilderFactory(MockKeyring)] },
@@ -1239,15 +1239,15 @@ describe('KeyringController', () => {
12391239
from: '',
12401240
}),
12411241
).rejects.toThrow(
1242-
'KeyringController - No keyring found. Error info: The address passed in is invalid/empty',
1242+
'KeyringController - No keyring found. Error info: There are keyrings, but none match the address',
12431243
);
12441244
});
12451245
});
12461246
});
12471247

12481248
describe('when the keyring for the given address does not support signMessage', () => {
12491249
it('should throw error', async () => {
1250-
const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19';
1250+
const address = '0x5AC6D462f054690a373FABF8CC28e161003aEB19';
12511251
stubKeyringClassWithAccount(MockKeyring, address);
12521252
await withController(
12531253
{ keyringBuilders: [keyringBuilderFactory(MockKeyring)] },
@@ -1307,15 +1307,15 @@ describe('KeyringController', () => {
13071307
from: '',
13081308
}),
13091309
).rejects.toThrow(
1310-
'KeyringController - No keyring found. Error info: The address passed in is invalid/empty',
1310+
'KeyringController - No keyring found. Error info: There are keyrings, but none match the address',
13111311
);
13121312
});
13131313
});
13141314
});
13151315

13161316
describe('when the keyring for the given address does not support signPersonalMessage', () => {
13171317
it('should throw error', async () => {
1318-
const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19';
1318+
const address = '0x5AC6D462f054690a373FABF8CC28e161003aEB19';
13191319
stubKeyringClassWithAccount(MockKeyring, address);
13201320
await withController(
13211321
{ keyringBuilders: [keyringBuilderFactory(MockKeyring)] },
@@ -1577,7 +1577,7 @@ describe('KeyringController', () => {
15771577

15781578
describe('when the keyring for the given address does not support signTypedMessage', () => {
15791579
it('should throw error', async () => {
1580-
const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19';
1580+
const address = '0x5AC6D462f054690a373FABF8CC28e161003aEB19';
15811581
stubKeyringClassWithAccount(MockKeyring, address);
15821582
await withController(
15831583
{ keyringBuilders: [keyringBuilderFactory(MockKeyring)] },
@@ -1659,7 +1659,7 @@ describe('KeyringController', () => {
16591659
expect(unsignedEthTx.v).toBeUndefined();
16601660
await controller.signTransaction(unsignedEthTx, '');
16611661
}).rejects.toThrow(
1662-
'KeyringController - No keyring found. Error info: The address passed in is invalid/empty',
1662+
'KeyringController - No keyring found. Error info: There are keyrings, but none match the address',
16631663
);
16641664
});
16651665
});
@@ -1681,7 +1681,7 @@ describe('KeyringController', () => {
16811681

16821682
describe('when the keyring for the given address does not support signTransaction', () => {
16831683
it('should throw if the keyring for the given address does not support signTransaction', async () => {
1684-
const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19';
1684+
const address = '0x5AC6D462f054690a373FABF8CC28e161003aEB19';
16851685
stubKeyringClassWithAccount(MockKeyring, address);
16861686
await withController(
16871687
{ keyringBuilders: [keyringBuilderFactory(MockKeyring)] },

packages/keyring-controller/src/KeyringController.ts

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { RestrictedControllerMessenger } from '@metamask/base-controller';
88
import { BaseController } from '@metamask/base-controller';
99
import * as encryptorUtils from '@metamask/browser-passworder';
1010
import HDKeyring from '@metamask/eth-hd-keyring';
11-
import { normalize } from '@metamask/eth-sig-util';
11+
import { normalize as ethNormalize } from '@metamask/eth-sig-util';
1212
import SimpleKeyring from '@metamask/eth-simple-keyring';
1313
import type {
1414
EthBaseTransaction,
@@ -34,6 +34,7 @@ import {
3434
bytesToHex,
3535
hasProperty,
3636
isObject,
37+
isStrictHexString,
3738
isValidHexAddress,
3839
isValidJson,
3940
remove0x,
@@ -503,12 +504,44 @@ async function displayForKeyring(
503504

504505
return {
505506
type: keyring.type,
506-
// Cast to `Hex[]` here is safe here because `accounts` has no nullish
507-
// values, and `normalize` returns `Hex` unless given a nullish value
508-
accounts: accounts.map(normalize) as Hex[],
507+
// Cast to `string[]` here is safe here because `accounts` has no nullish
508+
// values, and `normalize` returns `string` unless given a nullish value
509+
accounts: accounts.map(normalize) as string[],
509510
};
510511
}
511512

513+
/**
514+
* Check if address is an ethereum address
515+
*
516+
* @param address - An address.
517+
* @returns Returns true if the address is an ethereum one, false otherwise.
518+
*/
519+
function isEthAddress(address: string): boolean {
520+
// We first check if it's a matching `Hex` string, so that is narrows down
521+
// `address` as an `Hex` type, allowing us to use `isValidHexAddress`
522+
return (
523+
// NOTE: This function only checks for lowercased strings
524+
isStrictHexString(address.toLowerCase()) &&
525+
// This checks for lowercased addresses and checksum addresses too
526+
isValidHexAddress(address as Hex)
527+
);
528+
}
529+
530+
/**
531+
* Normalize ethereum or non-EVM address.
532+
*
533+
* @param address - Ethereum or non-EVM address.
534+
* @returns The normalized address.
535+
*/
536+
function normalize(address: string): string | undefined {
537+
// Since the `KeyringController` is only dealing with address, we have
538+
// no other way to get the associated account type with this address. So we
539+
// are down to check the actual address format for now
540+
// TODO: Find a better way to not have those runtime checks based on the
541+
// address value!
542+
return isEthAddress(address) ? ethNormalize(address) : address;
543+
}
544+
512545
/**
513546
* Controller responsible for establishing and managing user identity.
514547
*
@@ -644,6 +677,10 @@ export class KeyringController extends BaseController<
644677
keyring: EthKeyring<Json>,
645678
accountCount?: number,
646679
): Promise<Hex> {
680+
// READ THIS CAREFULLY:
681+
// We still uses `Hex` here, since we are not using this method when creating
682+
// and account using a "Snap Keyring". This function assume the `keyring` is
683+
// ethereum compatible, but "Snap Keyring" might not be.
647684
return this.#persistOrRollback(async () => {
648685
const oldAccounts = await this.#getAccountsFromKeyrings();
649686

@@ -828,15 +865,15 @@ export class KeyringController extends BaseController<
828865
account: string,
829866
opts?: Record<string, unknown>,
830867
): Promise<string> {
831-
const normalizedAddress = normalize(account) as Hex;
868+
const address = ethNormalize(account) as Hex;
832869
const keyring = (await this.getKeyringForAccount(
833870
account,
834871
)) as EthKeyring<Json>;
835872
if (!keyring.getEncryptionPublicKey) {
836873
throw new Error(KeyringControllerError.UnsupportedGetEncryptionPublicKey);
837874
}
838875

839-
return await keyring.getEncryptionPublicKey(normalizedAddress, opts);
876+
return await keyring.getEncryptionPublicKey(address, opts);
840877
}
841878

842879
/**
@@ -851,7 +888,7 @@ export class KeyringController extends BaseController<
851888
from: string;
852889
data: Eip1024EncryptedData;
853890
}): Promise<string> {
854-
const address = normalize(messageParams.from) as Hex;
891+
const address = ethNormalize(messageParams.from) as Hex;
855892
const keyring = (await this.getKeyringForAccount(
856893
address,
857894
)) as EthKeyring<Json>;
@@ -873,9 +910,7 @@ export class KeyringController extends BaseController<
873910
* @returns Promise resolving to keyring of the `account` if one exists.
874911
*/
875912
async getKeyringForAccount(account: string): Promise<unknown> {
876-
// Cast to `Hex` here is safe here because `address` is not nullish.
877-
// `normalizeToHex` returns `Hex` unless given a nullish value.
878-
const hexed = normalize(account) as Hex;
913+
const address = normalize(account);
879914

880915
const candidates = await Promise.all(
881916
this.#keyrings.map(async (keyring) => {
@@ -885,7 +920,7 @@ export class KeyringController extends BaseController<
885920

886921
const winners = candidates.filter((candidate) => {
887922
const accounts = candidate[1].map(normalize);
888-
return accounts.includes(hexed);
923+
return accounts.includes(address);
889924
});
890925

891926
if (winners.length && winners[0]?.length) {
@@ -894,9 +929,7 @@ export class KeyringController extends BaseController<
894929

895930
// Adding more info to the error
896931
let errorInfo = '';
897-
if (!isValidHexAddress(hexed)) {
898-
errorInfo = 'The address passed in is invalid/empty';
899-
} else if (!candidates.length) {
932+
if (!candidates.length) {
900933
errorInfo = 'There are no keyrings';
901934
} else if (!winners.length) {
902935
errorInfo = 'There are keyrings, but none match the address';
@@ -999,7 +1032,7 @@ export class KeyringController extends BaseController<
9991032
* @fires KeyringController:accountRemoved
10001033
* @returns Promise resolving when the account is removed.
10011034
*/
1002-
async removeAccount(address: Hex): Promise<void> {
1035+
async removeAccount(address: string): Promise<void> {
10031036
await this.#persistOrRollback(async () => {
10041037
const keyring = (await this.getKeyringForAccount(
10051038
address,
@@ -1013,7 +1046,10 @@ export class KeyringController extends BaseController<
10131046
// The `removeAccount` method of snaps keyring is async. We have to update
10141047
// the interface of the other keyrings to be async as well.
10151048
// eslint-disable-next-line @typescript-eslint/await-thenable
1016-
await keyring.removeAccount(address);
1049+
// FIXME: We do cast to `Hex` to makes the type checker happy here, and
1050+
// because `Keyring<State>.removeAccount` requires address to be `Hex`. Those
1051+
// type would need to be updated for a full non-EVM support.
1052+
await keyring.removeAccount(address as Hex);
10171053

10181054
const accounts = await keyring.getAccounts();
10191055
// Check if this was the last/only account
@@ -1057,7 +1093,7 @@ export class KeyringController extends BaseController<
10571093
throw new Error("Can't sign an empty message");
10581094
}
10591095

1060-
const address = normalize(messageParams.from) as Hex;
1096+
const address = ethNormalize(messageParams.from) as Hex;
10611097
const keyring = (await this.getKeyringForAccount(
10621098
address,
10631099
)) as EthKeyring<Json>;
@@ -1075,7 +1111,7 @@ export class KeyringController extends BaseController<
10751111
* @returns Promise resolving to a signed message string.
10761112
*/
10771113
async signPersonalMessage(messageParams: PersonalMessageParams) {
1078-
const address = normalize(messageParams.from) as Hex;
1114+
const address = ethNormalize(messageParams.from) as Hex;
10791115
const keyring = (await this.getKeyringForAccount(
10801116
address,
10811117
)) as EthKeyring<Json>;
@@ -1113,7 +1149,7 @@ export class KeyringController extends BaseController<
11131149

11141150
// Cast to `Hex` here is safe here because `messageParams.from` is not nullish.
11151151
// `normalize` returns `Hex` unless given a nullish value.
1116-
const address = normalize(messageParams.from) as Hex;
1152+
const address = ethNormalize(messageParams.from) as Hex;
11171153
const keyring = (await this.getKeyringForAccount(
11181154
address,
11191155
)) as EthKeyring<Json>;
@@ -1147,7 +1183,7 @@ export class KeyringController extends BaseController<
11471183
from: string,
11481184
opts?: Record<string, unknown>,
11491185
): Promise<TxData> {
1150-
const address = normalize(from) as Hex;
1186+
const address = ethNormalize(from) as Hex;
11511187
const keyring = (await this.getKeyringForAccount(
11521188
address,
11531189
)) as EthKeyring<Json>;
@@ -1171,7 +1207,7 @@ export class KeyringController extends BaseController<
11711207
transactions: EthBaseTransaction[],
11721208
executionContext: KeyringExecutionContext,
11731209
): Promise<EthBaseUserOperation> {
1174-
const address = normalize(from) as Hex;
1210+
const address = ethNormalize(from) as Hex;
11751211
const keyring = (await this.getKeyringForAccount(
11761212
address,
11771213
)) as EthKeyring<Json>;
@@ -1201,7 +1237,7 @@ export class KeyringController extends BaseController<
12011237
userOp: EthUserOperation,
12021238
executionContext: KeyringExecutionContext,
12031239
): Promise<EthUserOperationPatch> {
1204-
const address = normalize(from) as Hex;
1240+
const address = ethNormalize(from) as Hex;
12051241
const keyring = (await this.getKeyringForAccount(
12061242
address,
12071243
)) as EthKeyring<Json>;
@@ -1226,7 +1262,7 @@ export class KeyringController extends BaseController<
12261262
userOp: EthUserOperation,
12271263
executionContext: KeyringExecutionContext,
12281264
): Promise<string> {
1229-
const address = normalize(from) as Hex;
1265+
const address = ethNormalize(from) as Hex;
12301266
const keyring = (await this.getKeyringForAccount(
12311267
address,
12321268
)) as EthKeyring<Json>;
@@ -1979,7 +2015,7 @@ export class KeyringController extends BaseController<
19792015
*
19802016
* @returns A promise resolving to an array of accounts.
19812017
*/
1982-
async #getAccountsFromKeyrings(): Promise<Hex[]> {
2018+
async #getAccountsFromKeyrings(): Promise<string[]> {
19832019
const keyrings = this.#keyrings;
19842020

19852021
const keyringArrays = await Promise.all(
@@ -1989,9 +2025,9 @@ export class KeyringController extends BaseController<
19892025
return res.concat(arr);
19902026
}, []);
19912027

1992-
// Cast to `Hex[]` here is safe here because `addresses` has no nullish
1993-
// values, and `normalize` returns `Hex` unless given a nullish value
1994-
return addresses.map(normalize) as Hex[];
2028+
// Cast to `string[]` here is safe here because `addresses` has no nullish
2029+
// values, and `normalize` returns `string` unless given a nullish value
2030+
return addresses.map(normalize) as string[];
19952031
}
19962032

19972033
/**

0 commit comments

Comments
 (0)