Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor keyring to split bridge logic #156

Merged
merged 24 commits into from
Jun 15, 2023

Conversation

bergarces
Copy link
Contributor

@bergarces bergarces commented Nov 16, 2022

The goal of this PR is to split the logic that interacts directly with the Ledger device from the keyring logic that handles the interface with KeyringController.

  • BREAKING: Splits keyring and bridge logic into LedgerKeyring and LedgerIframeBridge classes.
  • ADDED: LedgerBridge interface to use in other implementations.
  • CHANGED: Removes initialisation code from the constructor and to the already supported init method.

@bergarces bergarces marked this pull request as ready for review November 17, 2022 13:05
@bergarces bergarces requested a review from a team as a code owner November 17, 2022 13:05
base-ledger-keyring.js Outdated Show resolved Hide resolved
throw new Error('Method init not implemented')
}

destroy () {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the trezor keyring there's a similar method called dispose. It'd be a breaking change, but we could rename this one to dispose and only call it if it exists, instead of calling one or the other on the extension depending on the keyring type.

this.hdPath = hdPath
}

async unlock (hdPath, updateHdk = true) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've done a simple refactor to methods that call the protected bridge methods.

Instead of returning an object with a success boolean and the payload. I'm just returning the payload and wrapping the call in a try/catch. It shouldn't be a breaking change as it's an internal implementation detail for now.

It made creating a different implementation (other than the existing MV2 one) easier, as I was able to just return the payload without wrapping it into an object with a boolean.

Opinions welcome.

base-ledger-keyring.js Outdated Show resolved Hide resolved
ledger-keyring-mv2.js Outdated Show resolved Hide resolved
ledger-keyring-mv2.js Outdated Show resolved Hide resolved
ledger-keyring-mv2.js Outdated Show resolved Hide resolved
ledger-keyring-mv2.js Outdated Show resolved Hide resolved
types/index.d.ts Outdated Show resolved Hide resolved
@bergarces bergarces marked this pull request as draft December 5, 2022 10:28
@bergarces
Copy link
Contributor Author

Moved to draft until there's an agreement about the changes needed for the Trezor keyring and Keyring controller.

ledger-keyring-mv2.js Outdated Show resolved Hide resolved
@bergarces bergarces force-pushed the bridge-refactor branch 2 times, most recently from e191c6e to 62ca07a Compare March 7, 2023 14:12
@bergarces bergarces marked this pull request as ready for review March 7, 2023 17:14
@bergarces
Copy link
Contributor Author

@bergarces any updates?

Hey @legobeat, thanks for the ping, I rebased the whole thing when the typescript support was added but then I completely forgot to follow up.

I'll get some time to solve conflicts and ping interested parties to get this and the trezor PRs merged so I can prepare one for extension and MV3 HW support.

@bergarces bergarces marked this pull request as draft April 26, 2023 07:57
async init(bridgeUrl: string) {
this.bridgeUrl = bridgeUrl;

this.#setupIframe();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now that this is not in the constructor, but on an async method, it should be perfectly possible to await this method by returning a promise that resolves on iframe.onLoad. That way we can remove all the iframeLoaded and delayedPromise references and code.

I chose not to do it in this PR so that it's easier to review, but it's a very simply change afterwards.

@bergarces bergarces marked this pull request as ready for review May 8, 2023 12:27
}

async destroy() {
window.removeEventListener('message', this.#eventListener.bind(this));
Copy link

Choose a reason for hiding this comment

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

Should we bind #eventListener in the constructor instead of re-binding it here? I'm wondering whether this would work, since in order to remove an event listener it has to match an event listener that was added exactly — but bind creates a new function.

Copy link
Contributor Author

@bergarces bergarces May 9, 2023

Choose a reason for hiding this comment

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

It's copied from the existing code, so I didn't consider the implications of calling bind in both instances. I agree that this approach probably does not work.

Before this library was changed to TypeScript, eventListener was a field within the class, which was initialised at constructor time, so I have pushed a minor refactor to replicate that behaviour (using init instead of the constructor). This guarantees that the event listener passed to addEventListener and removeEventListener is the same.

return this.#deviceActionMessage(
IFrameMessageAction.LedgerUnlock,
params,
) as Promise<GetPublicKeyResponse>;
Copy link

@mcmire mcmire May 8, 2023

Choose a reason for hiding this comment

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

Why do we have to typecast this? Is there any way to avoid this? (edit: see below)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed as suggested.

}

async #deviceActionMessage(
action:
Copy link

Choose a reason for hiding this comment

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

The types you've specified here would allow an invalid combination of arguments to be provided. For instance, you could pass an action of IFrameMessageAction.LedgerSignTransaction, but params of LedgerSignTypedDataParams. What do you think about using overloads to specify the desired combinations instead? That might prevent the typecasting above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. I was thinking so hard on how to make typescript infer the return type with generics that I completely overlooked using overloads, which should work perfectly in this case.

@bergarces bergarces requested review from mcmire and Gudahtt May 19, 2023 07:52
// If the iframe isn't loaded yet, let's store the desired transportType value and
// optimistically return a successful promise
if (!this.iframeLoaded) {
this.delayedPromise = {

Choose a reason for hiding this comment

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

Why do we need a delayedPromise for this? Couldn't we just persist the desiredTransportType in case the iframe is not loaded?

Choose a reason for hiding this comment

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

Ah I just noticed that we are just moving it from ledger-keyring.. nevermind then, let's keep this PR as simple as possible. Code improvements can be done in subsequent PRs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The delayed promise was needed because all that logic was in the constructor, which cannot be asynchronous.

Now that logic is moved to an asynchronous init method, it can be removed, but that should be done in a subsequent PR.

}

if (updateHdk && payload.chainCode) {
this.hdk.publicKey = Buffer.from(payload.publicKey, 'hex');

Choose a reason for hiding this comment

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

we have changed the payload.publicKey type checking from runtime to build time. Just want to make sure that that is enough.

Copy link
Contributor Author

@bergarces bergarces Jun 2, 2023

Choose a reason for hiding this comment

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

Should be if we trust the return types from ledger SDK.

Before, multiple checks where needed due to #sendMessage using an IFrameMessageResponse that would not consider the action, and therefore return just an union type. Now we have a more accurate IFrameMessageResponse<TAction extends IFrameMessageAction> that is based on the action sent to ledger.

Those checks were not there before this package was updated to Typescript, they were only added to avoid casting types.

Copy link

@cryptotavares cryptotavares left a comment

Choose a reason for hiding this comment

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

Left a very small comment but looks good to me!

}

beforeEach(async function () {
bridge = new LedgerIframeBridge();
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Generally we prefer to use helper functions and construct things per-test. This ensures the tests are readable individually without consulting the beforeEach, and it removes barriers to testing scenarios that have a different setup.

Copy link
Member

@Gudahtt Gudahtt left a comment

Choose a reason for hiding this comment

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

LGTM!

@bergarces bergarces merged commit c8f8a4e into MetaMask:main Jun 15, 2023
This was referenced Jun 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants