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

feat: add mobile bridge #198

Closed

Conversation

stanleyyconsensys
Copy link
Contributor

  • add mobile bridge
  • decouple the transport middleware from the bridge, for mobile, the transport middleware is a interface to connect to ledger eth app and the transport layer; for desktop, it is refer to iframe
  • adding setter / getter method for metadata from the bridge as a generic method to store different data on different bridge, hence the serialize and deserialize method from the highlevel module will serialize/deserialize the metadata together with the low level module (bridge)
  • move bridgeUrl into low level module (bridge)
  • enhance time complexity in deserialize, #migrateAccountDetails method by use Set, rather than array include
  • reduce the number of traversal in removeAccount
  • remove exportAccount due to eth keyring has throw an exception, should not create another exception type

- add mobile bridge
- decouple the transport middleware from the bridge, for mobile, the transport middleware is a interface to connect to ledger eth app and the transport layer; for desktop, it is refer to iframe
- adding setter / getter method for metadata from the bridge as a generic method to store different data on different bridge, hence the serialize and deserialize method from the highlevel module will serialize/deserialize the metadata together with the low level module (bridge)
- move bridgeUrl into low level module (bridge)
- enhance time complexity in deserialize, #migrateAccountDetails method by use Set, rather than array include
- reduce the number of traversal in  removeAccount
- remove exportAccount due to eth keyring has throw an exception, should not create another exception type
Copy link
Member

@mikesposito mikesposito left a comment

Choose a reason for hiding this comment

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

I think this solution of implementing a new bridge for mobile alongside the LedgerIframeBridge is very functional and practical. I left some minor comments for some API details, but it overall looks good

src/ledger-bridge.ts Outdated Show resolved Hide resolved
src/ledger-bridge.ts Outdated Show resolved Hide resolved
src/ledger-bridge.ts Outdated Show resolved Hide resolved
src/ledger-bridge.ts Outdated Show resolved Hide resolved
src/ledger-bridge.ts Show resolved Hide resolved
src/ledger-bridge.ts Outdated Show resolved Hide resolved
Comment on lines 137 to 139
setOptions(opts: LedgerIframeBridgeOptions): void {
this.bridgeUrl = opts.bridgeUrl ?? BRIDGE_URL;
}
Copy link
Member

Choose a reason for hiding this comment

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

When calling setOptions and changing the bridgeUrl we should do something to load another iframe, destroying the old one: this looks like destroying the bridge and re-initialising it (so that we don't end up having multiple event listeners that could lead to unpredictable results)

Copy link
Contributor Author

@stanleyyconsensys stanleyyconsensys Oct 5, 2023

Choose a reason for hiding this comment

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

i thought setOptions and getOption is mainly to support serialize deserialize, thats why i was using metadata, rather than option

if setOptions has to kick off some side action, it may has some unexpected behaviour in current implementation, which setOptions is couple with deserialize

one of the example
submitEncryptionKey -> unlockKeyrings -> #restoreKeyring-> #newKeyring -> deserialize + init
it will deserialize the keyring object , then trigger init
image

and some high level methods (verifySeedPhrase), they may just wanna to update the data for keyring via deserialize, but do nothing

may be we can just leave setOptions as a setter, for side action, just do it separately? (for iframe usecase, call init after setOptions) ?

or

we create methods deserializeData, serializeData in bridge to support high level module deserialize and serialize, and leave the setOptions and getOptions to something that fit to your idea?

the advantage is :

  • bridge can have custom data to serialize , rather than couple with its options
  • if configuration change will auto wire up with side action, i will prefer using setXXXX (e.g setBridgeUrl) rather than setOptions?
Suggested change
setOptions(opts: LedgerIframeBridgeOptions): void {
this.bridgeUrl = opts.bridgeUrl ?? BRIDGE_URL;
}
someOtherData = 0;
async deserializeData(serializeData: Record<string, unknown>) {
for (const key in serializeData) {
if (key === 'bridgeUrl') {
this.bridgeUrl = serializeData[key] as string;
}
if (key === 'someOtherData') {
this.someOtherData = serializeData[key] as number;
}
}
}
async serializeData(): Promise<Record<string, unknown>> {
return {
"someOtherData": this.someOtherData,
"bridgeUrl": this.bridgeUrl
}
}
async setBridgeUrl(bridgeUrl): Promise<void> {
if (bridgeUrl && this.bridgeUrl !== bridgeUrl) {
this.bridgeUrl = bridgeUrl
await this.destroy();
await this.init();
}
}

Comment on lines 197 to 199
setOptions(opts: LedgerMobileBridgeOptions): void {
this.deviceId = opts.deviceId ?? '';
}
Copy link
Member

Choose a reason for hiding this comment

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

what happens to the bridge is the deviceId here is changed to another value? do we need to take some side-actions or we are good to go?

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 leave it to setDeviceId, and setOption mainly set back the data from the deserialize

@stanleyyconsensys stanleyyconsensys marked this pull request as ready for review October 12, 2023 03:09
@stanleyyconsensys stanleyyconsensys requested a review from a team as a code owner October 12, 2023 03:09
@legobeat
Copy link
Contributor

legobeat commented Oct 26, 2023

@legobeat
Copy link
Contributor

legobeat commented Oct 26, 2023

  • move bridgeUrl into low level module (bridge)
  • enhance time complexity in deserialize, #migrateAccountDetails method by use Set, rather than array include
    reduce the number of traversal in removeAccount
    remove exportAccount due to eth keyring has throw an exception, should not create another exception type

These changes sound like they are orthogonal to adding the mobile bridge - would it be possible to break them out as separate PR(s) to keep the changes in this PR focused? :)

Could also consider breaking this apart in at least two major PRs:

  1. Refactor (decouple the transport middleware from the bridge, add getter/setter - maybe these should be separate as well?)
    • Apart from the changed behavior for bridgeUrl (which can be broken out), these should be non-breaking and not blocked by anything else from what I can see.
  2. Actually add mobile bridge, being enabled by 1.

@stanleyyconsensys
Copy link
Contributor Author

  • Apart

agree, i will create

  • 1 PR focus on refactor to make the bridgeUrl become a generic option parameter when init the ledger keyring for iframe
  • 1 PR focus on adding mobile as a bridge

but i have a doubt on 1 thing, is there any side affect if i change the keyring serialization structure?
from

{
      hdPath: string;
      accounts: string[];
      accountDetails: Record<string, AccountDetails>;
      accountIndexes: Record<string, number>;
      implementFullBIP44: boolean;
      bridgeUrl: string;
}

to

{
      hdPath: string;
      accounts: string[];
      accountDetails: Record<string, AccountDetails>;
      accountIndexes: Record<string, number>;
      implementFullBIP44: boolean;
      bridgeOptions: Record<string, unknown>;
}

@legobeat
Copy link
Contributor

legobeat commented Oct 26, 2023

@stanleyyconsensys As I read it here, that would be a breaking change (which in itself isn't necessarily a problem, just have to be more considerate about timing and ordering for merging and releasing such changes).

One way we could make it non-breaking would be something like:

{
  hdPath: string;
  accounts: string[];
  accountDetails: Record<string, AccountDetails>;
  accountIndexes: Record<string, number>;
  implementFullBIP44: boolean;
} & (
  {
    bridgeUrl: string;
  } | {
    bridgeOptions: SomeNewBridgeOptionsType; // we should be able to do stricter than Record<string, unknown>?
  }
)

...and then the implementation would do something like:

if(opts.bridgeOptions) {
  if (opts.bridgeUrl) {
    // error
  }
  // new behavior...
  return;
}
if (opts.bridgeUrl) {
  console.warn('some message indicating that this is now deprecated and will be removed in a future version, users should switch to bridgeOptions')
  // old behavior...
 return;
}
// error because missing conf...

that way we can allow users to stay on old behavior and migrate , by paying a price of increased complexity. Then the bridgeUrl path could be fully removed in a later semver-major release to simplify it again.

@stanleyyconsensys
Copy link
Contributor Author

stanleyyconsensys commented Oct 26, 2023

@legobeat 1 last question regarding on making changes non breaking

as i found that the current bridge implementation on extension is really old

extension yarn.log

"@metamask/eth-ledger-bridge-keyring@npm:^0.15.0":
  version: 0.15.0
  resolution: "@metamask/eth-ledger-bridge-keyring@npm:0.15.0"
  dependencies:
    "@ethereumjs/tx": "npm:^4.1.1"
    eth-sig-util: "npm:^2.0.0"
    ethereumjs-util: "npm:^7.0.9"
    hdkey: "npm:0.8.0"
  checksum: d856b820806bfd3a9a030b2116b082b5089531e21a5e4ca53e5af027d83b2935013a853a5c18bc6c374555467d4e8e9f1a1712c263e891a7cd3cf0a4388968ae
  languageName: node
  linkType: hard

it is still using a version that tight couple everything
extension version:

class LedgerBridgeKeyring extends EventEmitter {
    constructor (opts = {}) {
    }

current 1.0 version

class LedgerBridgeKeyring extends EventEmitter {
    constructor ({ bridge }: { bridge: LedgerBridge }) {
    }

the new version

class LedgerBridgeKeyring extends EventEmitter {
    constructor ({ bridge }: { bridge: LedgerBridge<Record<string, unknown>> }) {
    }

so is it still a case that we need to take care the breaking changes?

or becoz it is public repo, we assume every one is using 1.0, hence we have to make a transition step, and make sure the client who using the library should not necessary to change their 1.0 implementation?

just wanna confirm the reason, hence i can refactor the class in a correct way

1 more thing, if what we are referring on non breaking is something that i mentioned above, we should also taking care the use case of how client implementing the bridgeUrl?

e.g, in that case we have to keep the bridgeUrl in high level class

const ledger = new LedgerBridgeKeyring({bridge})
ledger.bridgeUrl = "https://some_custom_url"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants