Skip to content

Commit

Permalink
Merge pull request #655 from threshold-network/ledger-live-app
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
r-czajkowski authored Dec 14, 2023
2 parents 7ea372b + a993ab7 commit a3352e0
Show file tree
Hide file tree
Showing 43 changed files with 793 additions and 114 deletions.
40 changes: 40 additions & 0 deletions manifest-ledger-live-app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"id": "threshold",
"name": "Threshold",
"url": "http://localhost:3000?embed=true",
"homepageUrl": "http://localhost:3000",
"icon": "https://dashboard.threshold.network/favicon-T.png",
"platform": "all",
"apiVersion": "2.0.0",
"manifestVersion": "1",
"branch": "stable",
"categories": ["tbtc", "bitocin", "bridge"],
"currencies": ["bitcoin", "ethereum", "bitcoin_testnet", "ethereum_goerli"],
"content": {
"shortDescription": {
"en": "Threshold's tBTC is a truly decentralized bridge between Bitcoin and Ethereum."
},
"description": {
"en": "tBTC is Threshold’s decentralized bridge to bring BTC to the Ethereum network; the only permissionless solution on the market today. Bridge your Bitcoin to Ethereum in a secure and trustless way to participate in DeFi."
}
},
"permissions": [
"account.list",
"account.receive",
"account.request",
"currency.list",
"device.close",
"device.exchange",
"device.transport",
"message.sign",
"transaction.sign",
"transaction.signAndBroadcast",
"storage.set",
"storage.get",
"bitcoin.getXPub",
"wallet.capabilities",
"wallet.userId",
"wallet.info"
],
"domains": ["https://*", "wss://*"]
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"@keep-network/tbtc": "development",
"@keep-network/tbtc-v2.ts": "^2.3.0",
"@ledgerhq/connect-kit-loader": "^1.1.2",
"@ledgerhq/wallet-api-client": "^1.2.0",
"@ledgerhq/wallet-api-client-react": "^1.1.1",
"@reduxjs/toolkit": "^1.6.1",
"@rehooks/local-storage": "^2.4.4",
"@sentry/react": "^7.33.0",
Expand All @@ -44,6 +46,7 @@
"@web3-react/types": "^6.0.7",
"@web3-react/walletlink-connector": "^6.2.13",
"axios": "^0.24.0",
"bignumber.js": "^9.1.2",
"bitcoin-address-validation": "^2.2.1",
"crypto-js": "^4.1.1",
"ethereum-blockies-base64": "^1.0.2",
Expand Down
43 changes: 27 additions & 16 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { useCheckBonusEligibility } from "./hooks/useCheckBonusEligibility"
import { useFetchStakingRewards } from "./hooks/useFetchStakingRewards"
import { isSameETHAddress } from "./web3/utils"
import { ThresholdProvider } from "./contexts/ThresholdContext"
import { LedgerLiveAppProvider } from "./contexts/LedgerLiveAppContext"
import {
useSubscribeToAuthorizationIncreasedEvent,
useSubscribeToAuthorizationDecreaseApprovedEvent,
Expand All @@ -58,6 +59,9 @@ import {
useSubscribeToRedemptionRequestedEvent,
} from "./hooks/tbtc"
import { useSentry } from "./hooks/sentry"
import { useIsEmbed } from "./hooks/useIsEmbed"
import TBTC from "./pages/tBTC"
import { useDetectIfEmbed } from "./hooks/useDetectIfEmbed"

const Web3EventHandlerComponent = () => {
useSubscribeToVendingMachineContractEvents()
Expand Down Expand Up @@ -170,6 +174,7 @@ const AppBody = () => {
dispatch(fetchETHPriceUSD())
}, [dispatch])

useDetectIfEmbed()
usePosthog()
useCheckBonusEligibility()
useFetchStakingRewards()
Expand All @@ -180,12 +185,13 @@ const AppBody = () => {
}

const Layout = () => {
const { isEmbed } = useIsEmbed()
return (
<Box display="flex">
<Sidebar />
{!isEmbed && <Sidebar />}
<Box
// 100% - 80px is to account for the sidebar
w={{ base: "100%", md: "calc(100% - 80px)" }}
w={{ base: "100%", md: isEmbed ? "100%" : "calc(100% - 80px)" }}
bg={useColorModeValue("transparent", "gray.900")}
>
<Navbar />
Expand All @@ -199,12 +205,15 @@ const Layout = () => {
}

const Routing = () => {
const { isEmbed } = useIsEmbed()
const finalPages = isEmbed ? [TBTC] : pages
const to = isEmbed ? "tBTC" : "overview"
return (
<Routes>
<Route path="*" element={<Layout />}>
<Route index element={<Navigate to="overview" />} />
{pages.map(renderPageComponent)}
<Route path="*" element={<Navigate to="overview" />} />
<Route index element={<Navigate to={to} />} />
{finalPages.map(renderPageComponent)}
<Route path="*" element={<Navigate to={to} />} />
</Route>
</Routes>
)
Expand Down Expand Up @@ -248,17 +257,19 @@ const App: FC = () => {
return (
<Router basename={`${process.env.PUBLIC_URL}`}>
<Web3ReactProvider getLibrary={getLibrary}>
<ThresholdProvider>
<ReduxProvider store={reduxStore}>
<ChakraProvider theme={theme}>
<TokenContextProvider>
<Web3EventHandlerComponent />
<ModalRoot />
<AppBody />
</TokenContextProvider>
</ChakraProvider>
</ReduxProvider>
</ThresholdProvider>
<LedgerLiveAppProvider>
<ThresholdProvider>
<ReduxProvider store={reduxStore}>
<ChakraProvider theme={theme}>
<TokenContextProvider>
<Web3EventHandlerComponent />
<ModalRoot />
<AppBody />
</TokenContextProvider>
</ChakraProvider>
</ReduxProvider>
</ThresholdProvider>
</LedgerLiveAppProvider>
</Web3ReactProvider>
</Router>
)
Expand Down
4 changes: 2 additions & 2 deletions src/components/Modal/tBTC/InitiateUnminting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
ModalHeader,
Skeleton,
} from "@threshold-network/components"
import { useWeb3React } from "@web3-react/core"
import { useIsActive } from "../../../hooks/useIsActive"
import { FC } from "react"
import { useNavigate } from "react-router-dom"
import { useThreshold } from "../../../contexts/ThresholdContext"
Expand Down Expand Up @@ -43,7 +43,7 @@ const InitiateUnmintingBase: FC<InitiateUnmintingProps> = ({
btcAddress,
}) => {
const navigate = useNavigate()
const { account } = useWeb3React()
const { account } = useIsActive()
const { estimatedBTCAmount, thresholdNetworkFee } =
useRedemptionEstimatedFees(unmintAmount)
const threshold = useThreshold()
Expand Down
32 changes: 24 additions & 8 deletions src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
import { FC } from "react"
import { useWeb3React } from "@web3-react/core"
import { useModal } from "../../hooks/useModal"
import { FC, useEffect } from "react"
import NavbarComponent from "./NavbarComponent"
import { ModalType } from "../../enums"
import { useAppDispatch } from "../../hooks/store"
import { walletConnected } from "../../store/account"
import { useRequestEthereumAccount } from "../../hooks/ledger-live-app"
import { useIsActive } from "../../hooks/useIsActive"
import { useConnectWallet } from "../../hooks/useConnectWallet"

const Navbar: FC = () => {
const { openModal } = useModal()
const { account, active, chainId, deactivate } = useWeb3React()
const openWalletModal = () => openModal(ModalType.SelectWallet)
const { isActive, account, chainId, deactivate } = useIsActive()
const dispatch = useAppDispatch()
const connectWallet = useConnectWallet()

const { account: ledgerLiveAccount, requestAccount } =
useRequestEthereumAccount()
const ledgerLiveAccountAddress = ledgerLiveAccount?.address

const openWalletModal = () => {
connectWallet()
}

useEffect(() => {
if (ledgerLiveAccountAddress) {
dispatch(walletConnected(ledgerLiveAccountAddress))
}
}, [ledgerLiveAccountAddress, dispatch])

return (
<NavbarComponent
{...{
active,
active: isActive,
account,
chainId,
openWalletModal,
Expand Down
19 changes: 11 additions & 8 deletions src/components/SubmitTxButton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React, { FC } from "react"
import { FC } from "react"
import { Button, ButtonProps } from "@chakra-ui/react"
import { ModalType } from "../enums"
import { useWeb3React } from "@web3-react/core"
import { useModal } from "../hooks/useModal"
import { useIsActive } from "../hooks/useIsActive"
import { useIsTbtcSdkInitializing } from "../contexts/ThresholdContext"
import { useConnectWallet } from "../hooks/useConnectWallet"

interface Props extends ButtonProps {
onSubmit?: () => void
Expand All @@ -15,11 +14,15 @@ const SubmitTxButton: FC<Props> = ({
submitText = "Upgrade",
...buttonProps
}) => {
const { active } = useWeb3React()
const { openModal } = useModal()
const { isActive } = useIsActive()
const { isSdkInitializedWithSigner } = useIsTbtcSdkInitializing()
const connectWallet = useConnectWallet()

if (active && isSdkInitializedWithSigner) {
const onConnectWalletClick = () => {
connectWallet()
}

if (isActive && isSdkInitializedWithSigner) {
return (
<Button mt={6} isFullWidth onClick={onSubmit} {...buttonProps}>
{submitText}
Expand All @@ -31,7 +34,7 @@ const SubmitTxButton: FC<Props> = ({
<Button
mt={6}
isFullWidth
onClick={() => openModal(ModalType.SelectWallet)}
onClick={onConnectWalletClick}
{...buttonProps}
type="button"
isDisabled={false}
Expand Down
5 changes: 2 additions & 3 deletions src/components/TokenBalance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import {
BoxProps,
Icon,
} from "@threshold-network/components"
import { useWeb3React } from "@web3-react/core"
import { formatTokenAmount } from "../utils/formatAmount"
import tokenIconMap, { TokenIcon } from "../static/icons/tokenIconMap"
import { useIsActive } from "../hooks/useIsActive"

export interface TokenBalanceProps {
tokenAmount: string | number
Expand Down Expand Up @@ -95,8 +95,7 @@ const TokenBalance: FC<TokenBalanceProps & TextProps> = ({
withHigherPrecision = false,
...restProps
}) => {
const { active } = useWeb3React()
const shouldRenderTokenAmount = active
const { isActive: shouldRenderTokenAmount } = useIsActive()

const _tokenAmount = useMemo(() => {
return formatTokenAmount(tokenAmount || 0, tokenFormat, tokenDecimals)
Expand Down
18 changes: 7 additions & 11 deletions src/components/TokenBalanceInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface TokenBalanceInputProps
hasError?: boolean
errorMsgText?: string | JSX.Element
helperText?: string | JSX.Element
tokenDecimals?: number
}

const TokenBalanceInput: FC<TokenBalanceInputProps> = ({
Expand All @@ -42,6 +43,7 @@ const TokenBalanceInput: FC<TokenBalanceInputProps> = ({
errorMsgText,
helperText,
hasError = false,
tokenDecimals = web3Constants.STANDARD_ERC20_DECIMALS,
...inputProps
}) => {
const inputRef = useRef<HTMLInputElement>()
Expand All @@ -64,24 +66,18 @@ const TokenBalanceInput: FC<TokenBalanceInputProps> = ({
const setToMax = () => {
let remainder = Zero
const { decimalScale } = inputProps
if (
decimalScale &&
decimalScale > 0 &&
decimalScale < web3Constants.STANDARD_ERC20_DECIMALS
) {
if (decimalScale && decimalScale > 0 && decimalScale < tokenDecimals) {
remainder = BigNumber.from(max).mod(
BigNumber.from(10).pow(
web3Constants.STANDARD_ERC20_DECIMALS - decimalScale
)
BigNumber.from(10).pow(tokenDecimals - decimalScale)
)
}
_setAmount(formatUnits(BigNumber.from(max).sub(remainder)))
_setAmount(formatUnits(BigNumber.from(max).sub(remainder), tokenDecimals))
setAmount(valueRef.current)
}

const _setAmount = (value: string | number) => {
valueRef.current = value
? parseUnits(value.toString()).toString()
? parseUnits(value.toString(), tokenDecimals).toString()
: undefined
}

Expand All @@ -106,7 +102,7 @@ const TokenBalanceInput: FC<TokenBalanceInputProps> = ({
onValueChange={(values: NumberFormatInputValues) =>
_setAmount(values.value)
}
value={amount ? formatUnits(amount) : undefined}
value={amount ? formatUnits(amount, tokenDecimals) : undefined}
onChange={() => {
setAmount(valueRef.current)
}}
Expand Down
4 changes: 2 additions & 2 deletions src/components/tBTC/BridgeActivity.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { FC, createContext, useContext, ReactElement } from "react"
import { useWeb3React } from "@web3-react/core"
import {
Badge,
BodyMd,
Expand Down Expand Up @@ -27,6 +26,7 @@ import { InlineTokenBalance } from "../TokenBalance"
import Link from "../Link"
import { OutlineListItem } from "../OutlineListItem"
import { RedemptionDetailsLinkBuilder } from "../../utils/tBTC"
import { useIsActive } from "../../hooks/useIsActive"

export type BridgeActivityProps = {
data: BridgeActivityType[]
Expand Down Expand Up @@ -105,7 +105,7 @@ const ActivityItem: FC<BridgeActivityType> = ({
additionalData,
txHash,
}) => {
const { account } = useWeb3React()
const { account } = useIsActive()

const link =
bridgeProcess === "unmint"
Expand Down
Loading

0 comments on commit a3352e0

Please sign in to comment.