Skip to content

Commit

Permalink
feat: ledger app gate event emitter (#7697)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaladinlight committed Sep 9, 2024
1 parent 28aea4f commit ce1ee51
Show file tree
Hide file tree
Showing 37 changed files with 260 additions and 484 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type {
SignTxInput,
} from '../../types'
import { ChainAdapterDisplayName, CONTRACT_INTERACTION } from '../../types'
import { bnOrZero, toAddressNList } from '../../utils'
import { bnOrZero, toAddressNList, verifyLedgerAppOpen } from '../../utils'
import { assertAddressNotSanctioned } from '../../utils/validateAddress'
import type { ChainAdapterArgs as BaseChainAdapterArgs } from '../CosmosSdkBaseAdapter'
import { assertIsValidatorAddress, CosmosSdkBaseAdapter, Denoms } from '../CosmosSdkBaseAdapter'
Expand Down Expand Up @@ -95,14 +95,18 @@ export class ChainAdapter extends CosmosSdkBaseAdapter<KnownChainIds.CosmosMainn

try {
if (supportsCosmos(wallet)) {
await verifyLedgerAppOpen(this.chainId, wallet)

const bip44Params = this.getBIP44Params({ accountNumber })
const cosmosAddress = await wallet.cosmosGetAddress({
addressNList: toAddressNList(bip44Params),
showDisplay: showOnDevice,
})

if (!cosmosAddress) {
throw new Error('Unable to generate Cosmos address.')
}

return cosmosAddress
} else {
throw new Error('Wallet does not support Cosmos.')
Expand Down Expand Up @@ -272,6 +276,8 @@ export class ChainAdapter extends CosmosSdkBaseAdapter<KnownChainIds.CosmosMainn
try {
const { txToSign, wallet } = signTxInput
if (supportsCosmos(wallet)) {
await verifyLedgerAppOpen(this.chainId, wallet)

const signedTx = await wallet.cosmosSignTx(txToSign)

if (!signedTx?.serialized) throw new Error('Error signing tx')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type {
ValidAddressResult,
} from '../../types'
import { ChainAdapterDisplayName, CONTRACT_INTERACTION, ValidAddressResultType } from '../../types'
import { toAddressNList } from '../../utils'
import { toAddressNList, verifyLedgerAppOpen } from '../../utils'
import { bnOrZero } from '../../utils/bignumber'
import { assertAddressNotSanctioned } from '../../utils/validateAddress'
import type { ChainAdapterArgs as BaseChainAdapterArgs } from '../CosmosSdkBaseAdapter'
Expand Down Expand Up @@ -104,13 +104,17 @@ export class ChainAdapter extends CosmosSdkBaseAdapter<KnownChainIds.ThorchainMa

try {
if (supportsThorchain(wallet)) {
await verifyLedgerAppOpen(this.chainId, wallet)

const address = await wallet.thorchainGetAddress({
addressNList: toAddressNList(bip44Params),
showDisplay: showOnDevice,
})

if (!address) {
throw new Error('Unable to generate Thorchain address.')
}

return address
} else {
throw new Error('Wallet does not support Thorchain.')
Expand Down Expand Up @@ -169,6 +173,8 @@ export class ChainAdapter extends CosmosSdkBaseAdapter<KnownChainIds.ThorchainMa
try {
const { txToSign, wallet } = signTxInput
if (supportsThorchain(wallet)) {
await verifyLedgerAppOpen(this.chainId, wallet)

const signedTx = await wallet.thorchainSignTx(txToSign)

if (!signedTx?.serialized) throw new Error('Error signing tx')
Expand Down
25 changes: 19 additions & 6 deletions packages/chain-adapters/src/evm/EvmBaseAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ import type {
ValidAddressResult,
} from '../types'
import { CONTRACT_INTERACTION, ValidAddressResultType } from '../types'
import { getAssetNamespace, toAddressNList, toRootDerivationPath } from '../utils'
import {
getAssetNamespace,
toAddressNList,
toRootDerivationPath,
verifyLedgerAppOpen,
} from '../utils'
import { bnOrZero } from '../utils/bignumber'
import { assertAddressNotSanctioned } from '../utils/validateAddress'
import type {
Expand Down Expand Up @@ -323,8 +328,6 @@ export abstract class EvmBaseAdapter<T extends EvmChainId> implements IChainAdap
throw new Error(`wallet does not support ${this.getDisplayName()}`)
}

await this.assertSwitchChain(input.wallet)

const from = await this.getAddress(input)
const txToSign = await this.buildSendApiTransaction({ ...input, from })

Expand Down Expand Up @@ -407,8 +410,11 @@ export abstract class EvmBaseAdapter<T extends EvmChainId> implements IChainAdap
try {
const { txToSign, wallet } = signTxInput

if (!this.supportsChain(wallet, txToSign.chainId))
if (!this.supportsChain(wallet, txToSign.chainId)) {
throw new Error(`wallet does not support chain reference: ${txToSign.chainId}`)
}

await verifyLedgerAppOpen(this.chainId, wallet)

const signedTx = await wallet.ethSignTx(txToSign)

Expand Down Expand Up @@ -468,6 +474,7 @@ export abstract class EvmBaseAdapter<T extends EvmChainId> implements IChainAdap
throw new Error(`wallet does not support ${this.getDisplayName()}`)

await this.assertSwitchChain(wallet)
await verifyLedgerAppOpen(this.chainId, wallet)

const signedMessage = await wallet.ethSignMessage(messageToSign)

Expand All @@ -492,6 +499,7 @@ export abstract class EvmBaseAdapter<T extends EvmChainId> implements IChainAdap
}

await this.assertSwitchChain(wallet)
await verifyLedgerAppOpen(this.chainId, wallet)

const result = await wallet.ethSignTypedData(typedDataToSign)

Expand All @@ -511,6 +519,13 @@ export abstract class EvmBaseAdapter<T extends EvmChainId> implements IChainAdap
return input.pubKey
}

if (!this.supportsChain(wallet)) {
throw new Error(`wallet does not support ${this.getDisplayName()}`)
}

await this.assertSwitchChain(wallet)
await verifyLedgerAppOpen(this.chainId, wallet)

const address = await (wallet as ETHWallet).ethGetAddress({
addressNList: toAddressNList(bip44Params),
showDisplay: showOnDevice,
Expand Down Expand Up @@ -602,8 +617,6 @@ export abstract class EvmBaseAdapter<T extends EvmChainId> implements IChainAdap
throw new Error(`wallet does not support ${this.getDisplayName()}`)
}

await this.assertSwitchChain(wallet)

const from = await this.getAddress({ accountNumber, wallet })
const txToSign = await this.buildCustomApiTx({ ...input, from })

Expand Down
1 change: 1 addition & 0 deletions packages/chain-adapters/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './bignumber'
export * from './bip44'
export * from './fees'
export * from './utxoUtils'
export * from './ledgerAppGate'

export const getAssetNamespace = (type: string): AssetNamespace => {
if (type === 'ERC20') return 'erc20'
Expand Down
121 changes: 121 additions & 0 deletions packages/chain-adapters/src/utils/ledgerAppGate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { ChainId } from '@shapeshiftoss/caip'
import type { HDWallet } from '@shapeshiftoss/hdwallet-core'
import { isLedger } from '@shapeshiftoss/hdwallet-ledger'
import { KnownChainIds } from '@shapeshiftoss/types'
import EventEmitter from 'events'

export const emitter = new EventEmitter()

export type LedgerOpenAppEventArgs = {
chainId: ChainId
reject: (reason?: any) => void
}

export const getLedgerAppName = (chainId: ChainId | KnownChainIds | undefined) => {
switch (chainId as KnownChainIds) {
case KnownChainIds.ArbitrumMainnet:
case KnownChainIds.AvalancheMainnet:
case KnownChainIds.ArbitrumNovaMainnet:
case KnownChainIds.BaseMainnet:
case KnownChainIds.BnbSmartChainMainnet:
case KnownChainIds.EthereumMainnet:
case KnownChainIds.GnosisMainnet:
case KnownChainIds.OptimismMainnet:
case KnownChainIds.PolygonMainnet:
return 'Ethereum'
case KnownChainIds.BitcoinCashMainnet:
return 'Bitcoin Cash'
case KnownChainIds.BitcoinMainnet:
return 'Bitcoin'
case KnownChainIds.CosmosMainnet:
return 'Cosmos'
case KnownChainIds.DogecoinMainnet:
return 'Dogecoin'
case KnownChainIds.LitecoinMainnet:
return 'Litecoin'
case KnownChainIds.ThorchainMainnet:
return 'THORChain'
default:
throw Error(`Unsupported chainId: ${chainId}`)
}
}

const getCoin = (chainId: ChainId | KnownChainIds) => {
switch (chainId as KnownChainIds) {
case KnownChainIds.BitcoinMainnet:
return 'Bitcoin'
case KnownChainIds.DogecoinMainnet:
return 'Dogecoin'
case KnownChainIds.BitcoinCashMainnet:
return 'BitcoinCash'
case KnownChainIds.LitecoinMainnet:
return 'Litecoin'
case KnownChainIds.EthereumMainnet:
return 'Ethereum'
case KnownChainIds.AvalancheMainnet:
return 'Avalanche'
case KnownChainIds.OptimismMainnet:
return 'Optimism'
case KnownChainIds.BnbSmartChainMainnet:
return 'BnbSmartChain'
case KnownChainIds.PolygonMainnet:
return 'Polygon'
case KnownChainIds.GnosisMainnet:
return 'Gnosis'
case KnownChainIds.ArbitrumMainnet:
return 'Arbitrum'
case KnownChainIds.ArbitrumNovaMainnet:
return 'ArbitrumNova'
case KnownChainIds.BaseMainnet:
return 'Base'
case KnownChainIds.ThorchainMainnet:
return 'Rune'
case KnownChainIds.CosmosMainnet:
return 'Atom'
default:
throw Error(`Unsupported chainId: ${chainId}`)
}
}

export const verifyLedgerAppOpen = async (chainId: ChainId | KnownChainIds, wallet: HDWallet) => {
const coin = getCoin(chainId)
const appName = getLedgerAppName(chainId)

if (!isLedger(wallet)) return

const isAppOpen = async () => {
try {
await wallet.validateCurrentApp(coin)
return true
} catch {
return false
}
}

if (await isAppOpen()) return

let intervalId: NodeJS.Timer | undefined

try {
await new Promise<void>((resolve, reject) => {
// emit event to trigger modal open
const args: LedgerOpenAppEventArgs = { chainId, reject }
emitter.emit('LedgerOpenApp', args)

// prompt user to open app on device
wallet.openApp(appName)

intervalId = setInterval(async () => {
if (!(await isAppOpen())) return

// emit event to trigger modal close
emitter.emit('LedgerAppOpened')
clearInterval(intervalId)
resolve()
}, 1000)
})
} catch {
clearInterval(intervalId)
throw new Error('Ledger app open cancelled')
}
}
9 changes: 8 additions & 1 deletion packages/chain-adapters/src/utxo/UtxoBaseAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
convertXpubVersion,
toAddressNList,
toRootDerivationPath,
verifyLedgerAppOpen,
} from '../utils'
import { bn, bnOrZero } from '../utils/bignumber'
import { assertAddressNotSanctioned } from '../utils/validateAddress'
Expand Down Expand Up @@ -240,9 +241,11 @@ export abstract class UtxoBaseAdapter<T extends UtxoChainId> implements IChainAd

const targetIndex = bip44Params.index ?? nextIndex ?? 0

const address = await (() => {
const address = await (async () => {
if (pubKey) return account?.chainSpecific.addresses?.[targetIndex]?.pubkey

await verifyLedgerAppOpen(this.chainId, wallet)

return wallet.btcGetAddress({
addressNList: toAddressNList({ ...bip44Params, index: targetIndex }),
coin: this.coinName,
Expand Down Expand Up @@ -435,6 +438,8 @@ export abstract class UtxoBaseAdapter<T extends UtxoChainId> implements IChainAd
throw new Error(`UtxoBaseAdapter: wallet does not support ${this.coinName}`)
}

await verifyLedgerAppOpen(this.chainId, wallet)

const signedTx = await wallet.btcSignTx(txToSign)

if (!signedTx?.serializedTx) throw new Error('UtxoBaseAdapter: error signing tx')
Expand Down Expand Up @@ -549,6 +554,8 @@ export abstract class UtxoBaseAdapter<T extends UtxoChainId> implements IChainAd
): Promise<PublicKey> {
this.assertIsAccountTypeSupported(accountType)

await verifyLedgerAppOpen(this.chainId, wallet)

const bip44Params = this.getBIP44Params({ accountNumber, accountType })
const path = toRootDerivationPath(bip44Params)
const publicKeys = await wallet.getPublicKeys([
Expand Down
2 changes: 1 addition & 1 deletion src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -1889,7 +1889,7 @@
"ledgerOpenApp": {
"title": "Open the %{appName} App",
"description": "To continue, you will need to open the %{appName} app on your Ledger device.",
"signingDescription": "Signing prompt will automatically pop-up in your device after opening the app"
"devicePrompt": "Your device will prompt you with any necessary steps required to complete your current action."
},
"loremIpsum": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sed lectus efficitur, iaculis sapien eu, luctus tellus. Maecenas eget sapien dignissim, finibus mauris nec, mollis ipsum. Donec sodales sit amet felis sagittis vestibulum. Ut in consectetur lacus. Suspendisse potenti. Aenean at massa consequat lectus semper pretium. Cras sed bibendum enim. Mauris euismod sit amet dolor in placerat.",
"transactionHistory": {
Expand Down
14 changes: 3 additions & 11 deletions src/components/ManageAccountsDrawer/ManageAccountsDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ChainId } from '@shapeshiftoss/caip'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useLedgerOpenApp } from 'hooks/useLedgerOpenApp/useLedgerOpenApp'
import { useWallet } from 'hooks/useWallet/useWallet'
import { assertUnreachable } from 'lib/utils'

Expand All @@ -25,8 +24,6 @@ export const ManageAccountsDrawer = ({
const [step, setStep] = useState<ManageAccountsStep>('selectChain')
const [selectedChainId, setSelectedChainId] = useState<ChainId | null>(null)

const checkLedgerAppOpenIfLedgerConnected = useLedgerOpenApp({ isSigning: false })

const handleClose = useCallback(() => {
setStep('selectChain')
onClose()
Expand Down Expand Up @@ -66,16 +63,11 @@ export const ManageAccountsDrawer = ({
}, [parentSelectedChainId])

const handleSelectChainId = useCallback(
async (chainId: ChainId) => {
(chainId: ChainId) => {
setSelectedChainId(chainId)

// Only proceed to next step if the promise is resolved, i.e the user has opened the Ledger
// app without cancelling
await checkLedgerAppOpenIfLedgerConnected(chainId)
.then(() => handleNext())
.catch(console.error)
handleNext()
},
[checkLedgerAppOpenIfLedgerConnected, handleNext],
[handleNext],
)

const drawerContent = useMemo(() => {
Expand Down
Loading

0 comments on commit ce1ee51

Please sign in to comment.