Skip to content
This repository was archived by the owner on Oct 7, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 69 additions & 17 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ const { normalize: normalizeAddress } = require('eth-sig-util');
const SimpleKeyring = require('@metamask/eth-simple-keyring');
const HdKeyring = require('@metamask/eth-hd-keyring');

const keyringTypes = [SimpleKeyring, HdKeyring];
const defaultKeyringBuilders = [
keyringBuilderFactory(SimpleKeyring),
keyringBuilderFactory(HdKeyring),
];

const KEYRINGS_TYPE_MAP = {
HD_KEYRING: 'HD Key Tree',
Expand All @@ -35,13 +38,15 @@ class KeyringController extends EventEmitter {
constructor(opts) {
super();
const initState = opts.initState || {};
this.keyringTypes = opts.keyringTypes
? keyringTypes.concat(opts.keyringTypes)
: keyringTypes;
this.keyringBuilders = opts.keyringBuilders
? defaultKeyringBuilders.concat(opts.keyringBuilders)
: defaultKeyringBuilders;
Copy link
Contributor Author

@bergarces bergarces Dec 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it would be wise to rename this property as it will no longer hold class types, but builder functions.

I have checked within the extension code and it does not appear to be used, but worth thinking about any possible side effects as this can be a breaking change if external code tries to access this property.

keyringTypes in the store is left unchanged and it still stores the types as strings.

this.store = new ObservableStore(initState);
this.memStore = new ObservableStore({
isUnlocked: false,
keyringTypes: this.keyringTypes.map((keyringType) => keyringType.type),
keyringTypes: this.keyringBuilders.map(
(keyringBuilder) => keyringBuilder.type,
),
keyrings: [],
encryptionKey: null,
});
Expand Down Expand Up @@ -230,15 +235,15 @@ class KeyringController extends EventEmitter {
* and the current decrypted Keyrings array.
*
* All Keyring classes implement a unique `type` string,
* and this is used to retrieve them from the keyringTypes array.
* and this is used to retrieve them from the keyringBuilders array.
*
* @param {string} type - The type of keyring to add.
* @param {Object} opts - The constructor options for the keyring.
* @returns {Promise<Keyring>} The new keyring.
*/
async addNewKeyring(type, opts) {
const Keyring = this.getKeyringClassForType(type);
const keyring = new Keyring(opts);
const keyring = await this._newKeyring(type, opts);

if ((!opts || !opts.mnemonic) && type === KEYRINGS_TYPE_MAP.HD_KEYRING) {
keyring.generateRandomMnemonic();
keyring.addAccounts();
Expand Down Expand Up @@ -700,13 +705,12 @@ class KeyringController extends EventEmitter {
async _restoreKeyring(serialized) {
const { type, data } = serialized;

const Keyring = this.getKeyringClassForType(type);
if (!Keyring) {
const keyring = await this._newKeyring(type, data);
if (!keyring) {
this._unsupportedKeyrings.push(serialized);
return undefined;
}
const keyring = new Keyring();
await keyring.deserialize(data);

// getAccounts also validates the accounts for some keyrings
await keyring.getAccounts();
this.keyrings.push(keyring);
Expand All @@ -716,16 +720,18 @@ class KeyringController extends EventEmitter {
/**
* Get Keyring Class For Type
*
* Searches the current `keyringTypes` array
* for a Keyring class whose unique `type` property
* Searches the current `keyringBuilders` array
* for a Keyring builder whose unique `type` property
* matches the provided `type`,
* returning it if it exists.
*
* @param {string} type - The type whose class to get.
* @returns {Keyring|undefined} The class, if it exists.
*/
getKeyringClassForType(type) {
return this.keyringTypes.find((keyring) => keyring.type === type);
getKeyringBuilderForType(type) {
return this.keyringBuilders.find(
(keyringBuilder) => keyringBuilder.type === type,
);
}

/**
Expand Down Expand Up @@ -873,6 +879,52 @@ class KeyringController extends EventEmitter {
);
}
}

Copy link
Contributor Author

@bergarces bergarces Dec 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two methods in which a new keyring is instantiated: _restoreKeyring and addNewKeyring.

I've extracted the logic into a _newKeyring method so that we always instantiate, initialise (if available) and deserialize.

On the topic of deserialization, after looking at every single keyring I'm convinced that we don't have to pass the data at construction time as long as we call deserialize afterwards. deserialize is an asynchronous function, and we probably should stop calling it within the constructor (I have done so in the Trezor keyring refactor).

/**
* Instantiate, initialize and return a new keyring
*
* The keyring instantiated is of the given `type`.
*
* @param {string} type - The type of keyring to add.
* @param {Object} data - The data to restore a previously serialized keyring.
* @returns {Promise<Keyring>} The new keyring.
*/
async _newKeyring(type, data) {
const keyringBuilder = this.getKeyringBuilderForType(type);

if (!keyringBuilder) {
return undefined;
}

const keyring = keyringBuilder();

await keyring.deserialize(data);

if (keyring.init) {
await keyring.init();
}

return keyring;
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many instances in which we try to access the type from the static class. "Luckily", javascript allows a function to also have properties.

By exporting this helper method we can make sure that whenever we create a keyring builder function, the type is added as well.

Copy link
Contributor Author

@bergarces bergarces Dec 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, since we are always calling deserialize after instantiating the keyring, I'm just passing the bridge to the constructor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're changing the "Keyring" API in a non-backwards-compatible way, I'd prefer that we start using an object for constructor options rather than parameters (i.e. the "options bag" pattern, passing in { bridge } rather than bridge). This pattern is easier to read, and makes it easy to add additional options without breaking changes.

Copy link
Contributor Author

@bergarces bergarces Dec 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. I've made the change to the options bag pattern directly in eth-trezor-keyring PR. There's no longer any reference here as the new simple builder factory does not concern itself with bridges anymore.

The only argument I have in favour of passing dependencies without wrapping them in an object is that, if this ever moves to TypeScript, we could benefit from the syntactic sugar to declare them as class properties in the constructor, but that's a bit far-fetched.

constructor(
  private readonly bridge: KeyringBridge,
  )

module.exports = KeyringController;
/**
* Get builder function for `Keyring`
*
* Returns a builder function for `Keyring` with a `type` property.
*
* @param {typeof Keyring} Keyring - The Keyring class for the builder.
* @returns {function: Keyring} A builder function for the given Keyring.
*/
function keyringBuilderFactory(Keyring) {
const builder = () => new Keyring();

builder.type = Keyring.type;

return builder;
}

module.exports = {
KeyringController,
keyringBuilderFactory,
};
16 changes: 15 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const normalizeAddress = sigUtil.normalize;
const sinon = require('sinon');
const Wallet = require('ethereumjs-wallet').default;

const KeyringController = require('..');
const { KeyringController, keyringBuilderFactory } = require('..');
const { KeyringMockWithInit } = require('./lib/mock-keyring');
const mockEncryptor = require('./lib/mock-encryptor');

const password = 'password123';
Expand Down Expand Up @@ -33,6 +34,7 @@ describe('KeyringController', function () {
beforeEach(async function () {
keyringController = new KeyringController({
encryptor: mockEncryptor,
keyringBuilders: [keyringBuilderFactory(KeyringMockWithInit)],
});

await keyringController.createNewVaultAndKeychain(password);
Expand Down Expand Up @@ -240,6 +242,18 @@ describe('KeyringController', function () {
const allAccounts = await keyringController.getAccounts();
expect(allAccounts).toHaveLength(3);
});

it('should call init method if available', async function () {
const initSpy = sinon.spy(KeyringMockWithInit.prototype, 'init');

const keyring = await keyringController.addNewKeyring(
'Keyring Mock With Init',
);

expect(keyring).toBeInstanceOf(KeyringMockWithInit);

sinon.assert.calledOnce(initSpy);
});
});

describe('restoreKeyring', function () {
Expand Down
23 changes: 23 additions & 0 deletions test/lib/mock-keyring.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class KeyringMockWithInit {
init() {
return Promise.resolve();
}

getAccounts() {
return [];
}

serialize() {
return Promise.resolve({});
}

deserialize(_) {
return Promise.resolve();
}
}

KeyringMockWithInit.type = 'Keyring Mock With Init';

module.exports = {
KeyringMockWithInit,
};