-
Notifications
You must be signed in to change notification settings - Fork 38
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
Add signer for ledger live app #743
Merged
Merged
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
In the T dashboard repo we've created an ethereum signer for Ledger Live App. We've decided to move it here. The LedgerLiveAppEthereumSigner extends the `Signer` class from `ethers`. It implements all the needed methods, like `getAddress`, `signMessage`, `signTransaction` and `connect`. Additionaly I've added `sendTransaction` method that will be mainly used to communicate with contracts. All the methods use `ledgergq/wallet-api-client` under the hood. -Constructor- The `Signer` class has `provider` property tha we have to define in the constructor - that's why this is the argument needed to create an instance of our ledger live app ehtereum signer. Besides that we will also have `_walletApiClient`, `_windowMessageTransport` and `_account` properties in our class. The first two will be initialized in the contructor, and in the future commits I might also add the possibility to pass them via the constructor. The real initialization of those two is happening in `src/lib/utils/ledger/wallet-api.ts`. This is also the reason why I've decided to create a separate `ledger` folder in `utils`. -Account- As i mentioned earlier, we also have `_account` property which will store an `Account` object (from `@ledgerhw/wallet-api-client`). This will be helpful when doing a transaction, because for that we will have to use account id, so storing only the address would not work. The `_account` is managed trough getter and setter, so the user can request an account with either `wallet-api-client` or `wallet-api-client-react` and use `setAccount` setter to store it in our signer. The account MUST be set before doing transaction or signing a message. If it's not set, then the proper error will be thrown. We can also get only the address through the `getAddress` method (required from `Signer` abstract class) and `getAccountId`, which is not really used anywhere, but might be helpful in some cases. I've also created a `requestAccount` method if we would want to trigger the request account, but it will also not be used in our dApp, since we will be requesting an account through a hook from `wallet-api-client-react` and just setting the account with `setAccount`. I've decided to keep it there anyway, as it might be helpful for someone, who decides to use it in his app. -Sending transaction- Sending transaction uses wallet-api under the hood. An example ethereum transaction object, that we will be using with that library, can look like this: ``` const ethereumTransaction = { family: "ethereum", amount: new BigNumber(100000000000000), recipient: "0xRecipientAddress", nonce: 2, data: Buffer.from("SomeDataInHex", "hex"), gasPrice: new BigNumber(20000000000), gasLimit: new BigNumber(21000), }; ``` It is worth noticing that calling a contract method requires to: 1) Pass `0` as the amount, 2) Pass contract address as the `recipient` 3) Pass the hex data related to calling that method 4) Set the `family` to `ethereum` The rest of the things are optional. It is also worht noticing that the lib uses `BigNumber` from `bignumber.js` library. -Connecting and disconnecting the transport- When doing any operation (like sending transaction or requesting an account) with `wallet-api` we have to connect the `_windowMessageTransport` just before doing that, and disconnect it just after to avoid some unexpected issues, It is as simple as calling `this._walletMessageTransport.connect()` and `this._walletMessageTransport.disconnect()` methods.
In the past there was an issue with recognizing the `Signer` in the tbtc-v2.ts lib. As a workaround I've used `Signer` from `@ethersproject/abstract-signer`. The issue was fixed in `25d2de2ab89ee3c8428d7044f4f80fe53b15f750` so we can now safely import SIgner from `ethers`.
Add additional arguments - `windowsMessageTransport` and `walletApiClient` - that user can pass through argument. They are optional so if the user doesn't pass anything it will automatically initialize it using the methods from `src/lib/utils/ledger/wallet-api.ts`.
michalsmiarowski
force-pushed
the
add-signer-for-ledger-live-app
branch
from
November 9, 2023 09:58
bfc9182
to
1f8ca9e
Compare
Checked it out locally with the dApp and all works good (but had to update the |
Removes `src/lib/utils/ledger` folder and moves everything to `src/lib/utils/ledger.ts` file.
Merge `ethers` and `Signer` imports.
The "Account not found" error is repeated several times in the class, so I'm extracting it to a separate `AccountNotFoundError` class that extends the base `Error` class.
The result of running `yarn docs` command. The `src` is added at the beginning to most of the paths because we now have ethers Signer in the hierarchy so typedoc must differentiate our own code (hence src prefix) from 3rd party code (node_modules prefix).
20 tasks
`LedgerLiveAppEthereumSigner` -> `LedgerLiveEthereumSigner`. I've added a docstring above the class explaining why and where to use that signer.
So that `AccountNotFoundError` is not glued to them.
Removes exports from `getWindowMessageTransport` and `getWalletAPIClient` functions as they will only be used inside our LedgerLiveAppEthereumSigner.
Removes `windowMessageTransport` and `walletApiClient` arguments from `LedgerLiveEthereumSigner` constructor.
Sets current account in the new instance of `LedgerLiveAppEthereumSigner` that is returned from `connect` method.
Creates two new private methods: `_checkAccount()` and `_checkProviderAndAccount()`. The first one checks if account object is not null and have id. If not, then it throws an error. The second one compines `_checkProvider()` method from ethers signer and our new `_checkAccount()` method.
Makes `provider` argument, that we pass to `LedgerLiveEthereumSigner` constructor, optional.
Based on ethers implementation of abstract signer, the `sendTransaction` method should populate the transaction using `this.populateTransaction(transaction)`. We are doing exactly that in this commit, and thanks to that additional data is returned. The `transaction` argument passed to `sendTransaction` had only `data`, `from` and `to` arguments defined, while the transaction returned from `this.populateTransaction` additionally returns `nonce`, `gasLimit`, `gasPrice`, `maxFeePerGas` an `maxPriorityFeePerGas`, which we can then use to construct wallet-api's EthereumTransaction object. Since we use similar code to create that object in both `singTransaction` and `sendTransaction` I've decided to extract it to separate private method `_getWalletApiEthereumTransaction`. Finally, I've had to add `bignumber.js` to a `package.json` through `yarn add bignumber.js`. The `yarn.lock` was not updated, because it already installed the neede bignumber version from the dependency of `wallet-api`. Without it, the error occured when I used `EthereumTransaction` type for `ethereumTransaction` variable: ``` Type 'import("/Users/michalsmiarowski/Projects/threshold-network/tbtc-v2_4_sdk_integration_buts/typescript/node_modules/bignumber.js/bignumber").default' is not assignable to type 'import("/Users/michalsmiarowski/Projects/threshold-network/tbtc-v2_4_sdk_integration_buts/typescript/node_modules/@ledgerhq/wallet-api-core/node_modules/bignumber.js/bignumber").BigNumber'. ```
As you know before any wallet-api call we first have to connect() our `windowsMessageTransport` and `disconnect()` it right after to avoid any potential problems. However, there might be a situation when something goes wrong with the call and the next line, which is transport disconnect, does not execute, which may leas to some issues later on. This is why we additionaly wrap those calls calls try and cattch block, where we disconnect the `windowMessageTransport` if an error occures.
lukasz-zimnoch
approved these changes
Nov 14, 2023
r-czajkowski
added a commit
to threshold-network/token-dashboard
that referenced
this pull request
Dec 14, 2023
Ledger Live App <h1 align="center" fontSize="30">Ledger Live App</h1> Closes: #649 ~Blocked by: #654~ This PR allows to run our dApp as Live App withing Ledger Live. The Live Apps are displayed in the Discover section of Ledger Live on Desktop (Windows, Mac, Linux) and mobile (Android and iOS). The main purpose of it would be to complete the whole Mint & Unmint flow, without the need to leave the Ledger Live application and do a bitcoin transaction to generated deposit address. All transactions are done within the application. # Overall Description When running as Ledger Live App, our Token Dashboard is embedded into it and displayet differently than in the website. We are checking that with our `isEmbed` query parameter, that I've put in the manifest file. Only tbtc section is needed for this, so that's why onli this section is displayed and the rest are hidden. The user can connect his ethereum account from Ledger to communicate with eth contracts. He can also choose which of his bitcoin addresses he wants to use to send the bitcoins from. # Technical Details ### Overview The code was written based on the [Ledger Live App documentations](https://developers.ledger.com/docs/live-app/start-here/). As you can see there are two sections in the documentation: [DApp](https://developers.ledger.com/docs/dapp/process/) and [Non-DApp](https://developers.ledger.com/docs/non-dapp/introduction/) - both describe two different ways of embedding an application into the Ledger Live Discover section. A first natural choice in our case would be the `DApp` section, since our website is a Dapp. Unfortunately, that is not the case, because from my experience and research it looks like it was not possible to do a bitcoin transaction there. This is why we choose the second option, which allows to use [Wallet-API](https://wallet.api.live.ledger.com/). With the help of this API we are able to do bitcoin and eth transactions, and also interact with eth contracts. The Wallet-API also has two sections in the docs: [Core-API](https://wallet.api.live.ledger.com/core) and [React-API](https://wallet.api.live.ledger.com/react), that uses Core-API under the hood. In our case we actually use both: React-API for connecting the eth/btc accounts and sending bitcoin transactions from one account to another (in our case to deposit address) and Core-Api to interact with eth contracts. Why? The answer is that using only React-API would require us to reorganize [tBTC v2 SDK](https://github.com/keep-network/tbtc-v2/tree/main/typescript) just for the Ledger Live App functionality. The API for reacts needs raw data format of the ethereum transaction when we interact with the contract, and that can be obtained using [populateTransaction method](https://docs.ethers.org/v5/api/contract/contract/#contract-populateTransaction) from `ethers` lib, but we are not returning it in such form in our SDK. This is why we've decided to create a separate signer for this purpose - to avoid doing any changes in the SDK just for that feature and to not unnecessarily extend SDK responsibility. ### Ledger Live Ethereum Signer (wallet-api-core) TBTC v2 SDK allows us to pass signer when initiating it. The signer must extend the `Signer` class from `ethers` lib and this is exactly what our Ledger Live Ethereum Signer do. It uses `wallet-api-core` lib under the hood. The signer [was placed in tbtc-v2 repo](https://github.com/keep-network/tbtc-v2/blob/releases/mainnet/typescript/v2.3.0/typescript/src/lib/utils/ledger.ts) You can see a more detailed description of that signer, its purpose and explanation of how it works in keep-network/tbtc-v2#743. In our dApp we are requesting an eth account using `wallet-api-core-react` (see the subsection below) and then pass the account to the signer using [`setAccount` method](https://github.com/keep-network/tbtc-v2/blob/releases/mainnet/typescript/v2.3.0/typescript/src/lib/utils/ledger.ts#L65-L67). ### Connecting wallets and doing simple transactions (wallet-api-core-react) The Ledger Live Ethereum Signer is used to integrate with eth contracts, but what about connecting the account to our dApp and sending some tokens from one account to another? This is where we use `wallet-api-core-react` and it's hooks. In our dApp we have three custom hooks that use hooks from `wallet-api-core-react` under the hood: - `useRequestBitcoinAccount`, - `useRequestEthereumAccount`, - `useSendBitcoinTransaction`. The first two are pretty similar to the original ones (from the lib), but I've had to write a wrapper to it so that I can connect and disconnect `walletApiReactTransport` there. This is needed because our Ledger Live Ethereum Signer uses different instance of the transport there, so if we won't disconnect one or another, a `no ongoing request` error might occur. Based on [the dosc](https://wallet.api.live.ledger.com/core/configuration#initializing-the-wallet-api-client) the transport should be disconnected when we are done to ensure the communication is properly closed. The third one, `useSendBitcoinTransaction`, is used to create a bitcoin transaction in a proper format that is required by `wallet-api-core-react`. The format for our bitcoin transaction looks like this: ``` const bitcoinTransaction = { family: "bitcoin", amount: new BigNumber(100000000000000), recipient: "<bitcoin_address>", }; ``` Fields: - `family` (string): The cryptocurrency family to which the transaction belongs. This could be 'ethereum', 'bitcoin', etc. - `amount` (BigNumber): The amount of cryptocurrency to be sent in the transaction, represented in the smallest unit of the currency. For instance, in Bitcoin, an amount of 1 represents 0.00000001 BTC. - `recipient` (string): The address of the recipient of the transaction. - `nonce` (number, optional): This is the number of transactions sent from the sender's address. - `data` (Buffer, optional): Input data of the transaction. This is often used for contract interactions. - `gasPrice` (BigNumber, optional): The price per gas in wei. - `gasLimit` (BigNumber, optional): The maximum amount of gas provided for the transaction. - `maxPriorityFeePerGas `(BigNumber, optional): Maximum fee per gas to be paid for a transaction to be included in a block. - `maxFeePerGas` (BigNumber, optional): Maximum fee per gas willing to be paid for a transaction. _Source: https://wallet.api.live.ledger.com/appendix/transaction_ In our case, for our bitcoin transaction, we only need `family`, `amount` and `recipient`. We only use that to send bitcoins to deposit address, so we will use the deposit address as a `recipient` here. Finally, to execute the transaction, we just pass the transaction object and id of the connected bitcoin account to [`useSignAndBroadcastTransaction` hook](https://wallet.api.live.ledger.com/react/hooks/useSignAndBroadcastTransaction). ### LedgerLiveAppContext Connecting account in Ledger Live App is quite different than our actual one in the website. Normally, we use `web3react` for that, but in this case we need to use [`useRequestAccount` hook](https://wallet.api.live.ledger.com/react/hooks/useRequestAccount) form `wallet-api-client-react`. Because of that we need to store those accounts somewhere in our dApp, so I decided to create a `LedgerLiveAppContext` for that. The context contain 5 properties: ``` interface LedgerLiveAppContextState { ethAccount: Account | undefined btcAccount: Account | undefined setEthAccount: (ethAccount: Account | undefined) => void setBtcAccount: (btcAccount: Account | undefined) => void ledgerLiveAppEthereumSigner: LedgerLiveEthereumSigner | undefined } ``` As you can see we have `ethAccount` and `btcAccount` to store the connected accounts there. We can also set those account using `setEthAccount` and `setBtcAccount` methods, after we request it using our hook. The `ledgerLiveAppEthereumSigner` is an additional property that contains our signer for Ledger Live App. This way we will be able to set the account also in the signer. ### `useIsEmbed` hook Like I said earlier, we use `isEmbed` query parameter to determine if the dApp is used in Ledger Live or not. I've created an `useIsEmbed` hook that saves that query parameter to local storage and the use it to detect if we should use all the functionalities for Ledger Live App or not. ### `useIsActive` hook This is also a new hook here. His main purpose is to determine if, and what account is active. Up to this point we've used `useWeb3React` hook for that purpose, but in this case it won't work. So, under the hook, the `useIsActive` returns similar values to `useWeb3React` hook if the app is not embed, but if it is, then we return proper values based on the `LedgerLiveAppContext`. ### How it works with `threshold-ts` lib I've actually manage to not do any changes in our `threshold-ts` lib. The way it works now is that when the `isEmbed` flag is set to true, we pass the Ledger Live Ethereum Signer as a `providerOrSigner` property. This required me to change `getContract` and `getBlock` method though, so that they return the proper values when tthe `providerOrSigner` is and instance of `LedgerLiveEthereumSigner`. # Read More - [Ledger Live App documentation](https://developers.ledger.com/docs/live-app/start-here/) - [Wallet-Api documentation](https://wallet.api.live.ledger.com/) # How To Test Steps to Run it in as Ledger Live App: 1. Pull the newest changes from this branch 2. Run Ledger Live on your device 3. [Enable the developer mode](https://developers.ledger.com/docs/live-app/developer-mode/) 4. Go to Settings -> Developer 5. Go to `Add a local app` row and click `Browse` 6. Got to your project directory and choose [manifest-ledger-live-app.json](https://github.com/threshold-network/token-dashboard/blob/ledger-live-app/manifest-ledger-live-app.json) 6. Click `Open` In the future: - [ ] Write [Ledger Live App Plugin](https://developers.ledger.com/docs/dapp/requirements/) so we can display proper information on the Ledger device when revealing a deposit or requesting a redemption - [ ] Implement/check if the plugin works on Sepolia. It's currently [under development](LedgerHQ/ledger-live#5722).
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Refs: threshold-network/token-dashboard#649
In the T dashboard repo we've created an ethereum signer for Ledger Live App (ref threshold-network/token-dashboard#655). We've decided to move it here.
The LedgerLiveAppEthereumSigner extends the
Signer
class fromethers
. It implements all the needed methods, likegetAddress
,signMessage
,signTransaction
andconnect
. Additionaly I've addedsendTransaction
method that will be mainly used to communicate with contracts. All the methods useledgergq/wallet-api-client
under the hood.Constructor
The
Signer
class hasprovider
property tha we have to define in the constructor - that's why this is the argument needed to create an instance of our ledger live app ehtereum signer. Besides that we will also have_walletApiClient
,_windowMessageTransport
and_account
properties in our class. The first two can be also passed through a constructor, but if they not they will be initialized automatically using our methods insrc/lib/utils/ledger/wallet-api.ts
.This is also the reason why I've decided to create a separate(EDIT: I've actually moved everything to oneledger
folder inutils
.ledger.ts
file. See #743 (comment)).Account
As i mentioned earlier, we also have
_account
property which will store anAccount
object (from@ledgerhw/wallet-api-client
). This will be helpful whenn doing a transaction, because for that we will have to use account id, so storing only the address would not work.The
_account
is managed trough getter and setter, so the user can request an account with eitherwallet-api-client
orwallet-api-client-react
and usesetAccount
setter to store it in our signer. The account MUST be set before doing transaction or signing a message. If it's not set, then the proper error will be thrown.We can also get only the address through the
getAddress
method (required fromSigner
abstract class) andgetAccountId
, which is not really used anywhere, but might be helpful in some cases.I've also created a
requestAccount
method if we would want to trigger the request account, but it will also not be used in our dApp, since we will be requesting an account through a hook fromwallet-api-client-react
and just setting the account withsetAccount
. I've decided to keep it there anyway, as it might be helpful for someone, who decides to use it in his app.Sending transaction
Sending transaction uses wallet-api under the hood. An example ethereum transaction object, that we will be using with that library, can look like this:
It is worth noticing that calling a contract method requires to:
0
as the amount,recipient
family
toethereum
The rest of the things are optional. It is also worht noticing that the lib uses
BigNumber
frombignumber.js
library.Connecting and disconnecting the transport
When doing any operation (like sending transaction or requesting an account) with
wallet-api
we have to connect the_windowMessageTransport
just before doing that, and disconnect it just after to avoid some unexpected issues. It is as simple as callingthis._walletMessageTransport.connect()
andthis._walletMessageTransport.disconnect()
methods.