diff --git a/package.json b/package.json index 96be63eac..319643d59 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@devexperts/remote-data-ts": "^2.0.5", "@devexperts/rx-utils": "^1.0.0-alpha.14", "@devexperts/utils": "^1.0.0-alpha.14", + "@ledgerhq/hw-transport-webusb": "^6.1.0", "@openapitools/openapi-generator-cli": "^2.3.5", "@psf/bitcoincashjs-lib": "^4.0.2", "@thorchain/asgardex-midgard": "^1.1.0", diff --git a/src/main/api/hdwallet.ts b/src/main/api/hdwallet.ts index 436241538..ac8527aed 100644 --- a/src/main/api/hdwallet.ts +++ b/src/main/api/hdwallet.ts @@ -9,5 +9,6 @@ export const apiHDWallet: ApiHDWallet = { getLedgerAddress: (chain: Chain, network: Network) => ipcRenderer.invoke(IPCMessages.GET_LEDGER_ADDRESS, chain, network), sendTxInLedger: (chain: Chain, network: Network, txInfo: LedgerTxInfo) => - ipcRenderer.invoke(IPCMessages.SEND_LEDGER_TX, chain, network, txInfo) + ipcRenderer.invoke(IPCMessages.SEND_LEDGER_TX, chain, network, txInfo), + getTransport: () => ipcRenderer.invoke(IPCMessages.GET_TRANSPORT) } diff --git a/src/main/api/ledger/address.ts b/src/main/api/ledger/address.ts index abba0ce92..c1f252898 100644 --- a/src/main/api/ledger/address.ts +++ b/src/main/api/ledger/address.ts @@ -1,32 +1,27 @@ -// import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' -// import { BNBChain, BTCChain, Chain } from '@xchainjs/xchain-util' -import { Chain } from '@xchainjs/xchain-util' -// import * as E from 'fp-ts/Either' +import TransportWebUSB from '@ledgerhq/hw-transport-webusb' +import { Chain, THORChain } from '@xchainjs/xchain-util' +import * as E from 'fp-ts/Either' -import { Network } from '../../../shared/api/types' -// import { LedgerErrorId, Network } from '../../../shared/api/types' +import { LedgerErrorId, Network } from '../../../shared/api/types' // import { getAddress as getBNBAddress } from './binance' // import { getAddress as getBTCAddress } from './bitcoin' -// import { getErrorId } from './utils' +import { getAddress as getTHORAddress } from './thorchain' +import { getErrorId } from './utils' export const getAddress = async (chain: Chain, network: Network) => { - const _disabled = { chain, network } - // try { - // const transport = await TransportNodeHid.open('') - // let res: E.Either - // switch (chain) { - // case BNBChain: - // res = await getBNBAddress(transport, network) - // break - // case BTCChain: - // res = await getBTCAddress(transport, network) - // break - // default: - // res = E.left(LedgerErrorId.NO_APP) - // } - // await transport.close() - // return res - // } catch (error) { - // return E.left(getErrorId(error.toString())) - // } + try { + const transport = await TransportWebUSB.create() + let res: E.Either + switch (chain) { + case THORChain: + res = await getTHORAddress(transport, network) + break + default: + res = E.left(LedgerErrorId.NO_APP) + } + await transport.close() + return res + } catch (error) { + return E.left(getErrorId(error.toString())) + } } diff --git a/src/main/api/ledger/common.ts b/src/main/api/ledger/common.ts new file mode 100644 index 000000000..abe0112b7 --- /dev/null +++ b/src/main/api/ledger/common.ts @@ -0,0 +1,3 @@ +import TransportWebUSB from '@ledgerhq/hw-transport-webusb' + +export const getTransport = async () => await TransportWebUSB.create() diff --git a/src/main/api/ledger/index.ts b/src/main/api/ledger/index.ts index f4950a34c..c2f7f4c4f 100644 --- a/src/main/api/ledger/index.ts +++ b/src/main/api/ledger/index.ts @@ -1,2 +1,3 @@ export * from './address' export * from './transaction' +export * from './common' diff --git a/src/main/api/ledger/thorchain.ts b/src/main/api/ledger/thorchain.ts new file mode 100644 index 000000000..f56305430 --- /dev/null +++ b/src/main/api/ledger/thorchain.ts @@ -0,0 +1,30 @@ +import Transport from '@ledgerhq/hw-transport' +import THORChainApp from '@thorchain/ledger-thorchain' +import * as Client from '@xchainjs/xchain-client' +import { getPrefix } from '@xchainjs/xchain-thorchain' +import * as E from 'fp-ts/Either' + +import { LedgerErrorId, Network } from '../../../shared/api/types' +import { getErrorId } from './utils' + +// TODO(@Veado) Move `toClientNetwork` from `renderer/services/clients` to `main/util` or so +const toClientNetwork = (network: Network): Client.Network => + network === 'mainnet' ? Client.Network.Mainnet : Client.Network.Testnet +// TODO(@veado) Get path by using `xchain-thorchain` +const PATH = [44, 931, 0, 0, 0] + +export const getAddress = async (transport: Transport, network: Network) => { + try { + const app = new THORChainApp(transport) + const clientNetwork = toClientNetwork(network) + const response = await app.getAddressAndPubKey(PATH, getPrefix(clientNetwork)) + if (response.return_code !== 0x9000) { + // TODO(@Veado) get address from pubkey + return E.right('my-address') + } else { + return E.left(LedgerErrorId.UNKNOWN) + } + } catch (error) { + return E.left(getErrorId(error.toString())) + } +} diff --git a/src/main/api/ledger/transaction.ts b/src/main/api/ledger/transaction.ts index 506f89a2b..52acc182d 100644 --- a/src/main/api/ledger/transaction.ts +++ b/src/main/api/ledger/transaction.ts @@ -5,7 +5,7 @@ import { Chain } from '@xchainjs/xchain-util' import * as E from 'fp-ts/Either' -import { LedgerErrorId, LedgerTxInfo, Network } from '../../../shared/api/types' +import { /* LedgerErrorId, */ LedgerTxInfo, Network } from '../../../shared/api/types' // import { LedgerBNCTxInfo, LedgerBTCTxInfo, LedgerErrorId, LedgerTxInfo, Network } from '../../../shared/api/types' // import { sendTx as sendBNCTx } from './binance' // import { sendTx as sendBTCTx } from './bitcoin' @@ -15,9 +15,9 @@ export const sendTx = async ( chain: Chain, network: Network, txInfo: LedgerTxInfo -): Promise> => { +): Promise> => { const _disabled = { chain, network, txInfo } - return Promise.reject(Error('sendTx for Ledger is disabled temporary')) + return E.left(Error('sendTx for Ledger is disabled temporary')) // try { // const transport = await TransportNodeHid.open('') // let res: E.Either diff --git a/src/main/electron.ts b/src/main/electron.ts index 0c00ba159..3c25e45e3 100644 --- a/src/main/electron.ts +++ b/src/main/electron.ts @@ -15,7 +15,11 @@ import { Locale } from '../shared/i18n/types' import { registerAppCheckUpdatedHandler } from './api/appUpdate' import { getFileStoreService } from './api/fileStore' import { saveKeystore, removeKeystore, getKeystore, keystoreExist, exportKeystore, loadKeystore } from './api/keystore' -import { getAddress, sendTx } from './api/ledger' +import { + getAddress as getLedgerAddress, + sendTx as sendLedgerTx, + getTransport as getLedgerTransport +} from './api/ledger' import IPCMessages from './ipc/messages' import { setMenu } from './menu' @@ -121,7 +125,9 @@ const langChangeHandler = (locale: Locale) => { } const initIPC = () => { + // Lang ipcMain.on(IPCMessages.UPDATE_LANG, (_, locale: Locale) => langChangeHandler(locale)) + // Keystore ipcMain.handle(IPCMessages.SAVE_KEYSTORE, (_, keystore: Keystore) => saveKeystore(keystore)) ipcMain.handle(IPCMessages.REMOVE_KEYSTORE, () => removeKeystore()) ipcMain.handle(IPCMessages.GET_KEYSTORE, () => getKeystore()) @@ -130,11 +136,15 @@ const initIPC = () => { exportKeystore(defaultFileName, keystore) ) ipcMain.handle(IPCMessages.LOAD_KEYSTORE, () => loadKeystore()) - ipcMain.handle(IPCMessages.GET_LEDGER_ADDRESS, (_, chain: Chain, network: Network) => getAddress(chain, network)) + // Ledger + ipcMain.handle(IPCMessages.GET_LEDGER_ADDRESS, (_, chain: Chain, network: Network) => + getLedgerAddress(chain, network) + ) ipcMain.handle(IPCMessages.SEND_LEDGER_TX, (_, chain: Chain, network: Network, txInfo: LedgerTxInfo) => - sendTx(chain, network, txInfo) + sendLedgerTx(chain, network, txInfo) ) - + ipcMain.handle(IPCMessages.GET_TRANSPORT, () => getLedgerTransport()) + // Update registerAppCheckUpdatedHandler(IS_DEV) // Register all file-stored data services Object.entries(DEFAULT_STORAGES).forEach(([name, defaultValue]) => { diff --git a/src/main/ipc/messages.ts b/src/main/ipc/messages.ts index f524fac41..62cee07d4 100644 --- a/src/main/ipc/messages.ts +++ b/src/main/ipc/messages.ts @@ -8,6 +8,7 @@ enum IPCMessages { LOAD_KEYSTORE = 'LOAD_KEYSTORE', GET_LEDGER_ADDRESS = 'GET_LEDGER_ADDRESS', SEND_LEDGER_TX = 'SEND_LEDGER_TX', + GET_TRANSPORT = 'GET_TRANSPORT', UPDATE_AVAILABLE = 'UPDATE_AVAILABLE', APP_CHECK_FOR_UPDATE = 'APP_CHECK_FOR_UPDATE', /** diff --git a/src/renderer/hooks/useAppUpdate.ts b/src/renderer/hooks/useAppUpdate.ts index 581183316..7e88aa2bb 100644 --- a/src/renderer/hooks/useAppUpdate.ts +++ b/src/renderer/hooks/useAppUpdate.ts @@ -15,11 +15,15 @@ export const useAppUpdate = () => { const resetAppUpdater = () => setAppUpdater(RD.initial) const checkForUpdates = useCallback(() => { - FP.pipe( + const subscription = FP.pipe( Rx.from(window.apiAppUpdate.checkForAppUpdates()), RxOp.catchError((e) => Rx.of(RD.failure(new Error(e.message)))), RxOp.startWith(RD.pending) ).subscribe(setAppUpdater) + + return () => { + subscription.unsubscribe() + } }, [setAppUpdater]) return { diff --git a/src/renderer/hooks/useLedger.ts b/src/renderer/hooks/useLedger.ts new file mode 100644 index 000000000..9528dfc31 --- /dev/null +++ b/src/renderer/hooks/useLedger.ts @@ -0,0 +1,77 @@ +import { useCallback, useMemo } from 'react' + +import * as RD from '@devexperts/remote-data-ts' +import Transport from '@ledgerhq/hw-transport' +import THORChainApp from '@thorchain/ledger-thorchain' +import { getPrefix as getTHORPrefix } from '@xchainjs/xchain-thorchain' +import { Chain } from '@xchainjs/xchain-util' +import * as E from 'fp-ts/Either' +import * as FP from 'fp-ts/function' +import { useObservableState } from 'observable-hooks' +import * as Rx from 'rxjs' +import * as RxOp from 'rxjs/operators' + +import { LedgerErrorId, Network } from '../../shared/api/types' +import { useAppContext } from '../contexts/AppContext' +import { observableState } from '../helpers/stateHelper' +import { toClientNetwork } from '../services/clients' +import { DEFAULT_NETWORK } from '../services/const' +import { LedgerAddressRD } from '../services/wallet/types' + +// TODO(@veado) Get path by using `xchain-thorchain` +const PATH = [44, 931, 0, 0, 0] + +export const getTHORAddress = async (transport: Transport, network: Network) => { + try { + const app = new THORChainApp(transport) + const clientNetwork = toClientNetwork(network) + const response = await app.getAddressAndPubKey(PATH, getTHORPrefix(clientNetwork)) + if (response.return_code !== 0x9000) { + // TODO(@Veado) get address from pubkey + return E.right('my-address') + } else { + return E.left(LedgerErrorId.UNKNOWN) + } + } catch (error) { + return E.left(LedgerErrorId.WRONG_APP) + } +} + +export const useLedger = () => { + const { network$ } = useAppContext() + const network = useObservableState(network$, DEFAULT_NETWORK) + + const { get$: address$, set: setAddress } = useMemo(() => observableState(RD.initial), []) + + const addressRD = useObservableState(FP.pipe(address$, RxOp.shareReplay(1)), RD.initial) + + const getAddress = useCallback( + (_chain: Chain) => { + FP.pipe( + Rx.from(window.apiHDWallet.getTransport()), + RxOp.switchMap((transport) => Rx.from(getTHORAddress(transport, network))), + RxOp.map(RD.fromEither), + RxOp.startWith(RD.pending), + RxOp.catchError((error) => Rx.of(RD.failure(error))) + ).subscribe(setAddress) + }, + [network, setAddress] + ) + + // const getAddress = useCallback( + // (chain: Chain) => { + // FP.pipe( + // Rx.from(window.apiHDWallet.getLedgerAddress(chain, network)), + // RxOp.map(RD.fromEither), + // RxOp.startWith(RD.pending), + // RxOp.catchError((error) => Rx.of(RD.failure(error))) + // ).subscribe(setAddress) + // }, + // [network, setAddress] + // ) + + return { + getAddress, + address: addressRD + } +} diff --git a/src/renderer/views/wallet/SettingsView.tsx b/src/renderer/views/wallet/SettingsView.tsx index 36347d4ab..c6fdd1f5d 100644 --- a/src/renderer/views/wallet/SettingsView.tsx +++ b/src/renderer/views/wallet/SettingsView.tsx @@ -12,6 +12,7 @@ import * as Rx from 'rxjs' import * as RxOp from 'rxjs/operators' import { Network } from '../../../shared/api/types' +import { Button } from '../../components/uielements/button' import { Settings } from '../../components/wallet/settings' import { useAppContext } from '../../contexts/AppContext' import { useBinanceContext } from '../../contexts/BinanceContext' @@ -24,6 +25,7 @@ import { useThorchainContext } from '../../contexts/ThorchainContext' import { useWalletContext } from '../../contexts/WalletContext' import { filterEnabledChains } from '../../helpers/chainHelper' import { sequenceTOptionFromArray } from '../../helpers/fpHelpers' +import { useLedger } from '../../hooks/useLedger' import { DEFAULT_NETWORK } from '../../services/const' import { getPhrase } from '../../services/wallet/util' import { UserAccountType } from '../../types/wallet' @@ -260,22 +262,32 @@ export const SettingsView: React.FC = (): JSX.Element => { } } + const { getAddress: getLedgerAddress, address: ledgerAddressRD } = useLedger() + return ( - - - - - + <> + + + + + ledgerAddressRD: {JSON.stringify(ledgerAddressRD)} + + + + + + + ) } diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index 87847c6c5..d3edd5970 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -1,4 +1,5 @@ import * as RD from '@devexperts/remote-data-ts' +import Transport from '@ledgerhq/hw-transport' import { Address, FeeRate, TxParams } from '@xchainjs/xchain-client' import { Keystore } from '@xchainjs/xchain-crypto' import { Chain } from '@xchainjs/xchain-util' @@ -91,6 +92,7 @@ export type LedgerTxInfo = LedgerBTCTxInfo | LedgerBNCTxInfo export type ApiHDWallet = { getLedgerAddress: (chain: Chain, network: Network) => Promise> sendTxInLedger: (chain: Chain, network: Network, txInfo: LedgerTxInfo) => Promise> + getTransport: () => Promise } declare global { diff --git a/src/shared/mock/api.ts b/src/shared/mock/api.ts index 0b0a7d5d9..9d419693e 100644 --- a/src/shared/mock/api.ts +++ b/src/shared/mock/api.ts @@ -1,3 +1,4 @@ +import Transport from '@ledgerhq/hw-transport' import { Keystore } from '@xchainjs/xchain-crypto' import * as E from 'fp-ts/Either' @@ -77,5 +78,6 @@ export const apiUrl: ApiUrl = { // Mock `apiHDWallet` export const apiHDWallet: ApiHDWallet = { getLedgerAddress: () => Promise.resolve(E.right('ledger_address')), - sendTxInLedger: () => Promise.resolve(E.right('tx_hash')) + sendTxInLedger: () => Promise.resolve(E.right('tx_hash')), + getTransport: () => Promise.resolve({} as Transport) } diff --git a/src/shared/thorchain-ledger.d.ts b/src/shared/thorchain-ledger.d.ts new file mode 100644 index 000000000..8df935396 --- /dev/null +++ b/src/shared/thorchain-ledger.d.ts @@ -0,0 +1 @@ +declare module '@thorchain/ledger-thorchain' diff --git a/yarn.lock b/yarn.lock index a8c33da2b..810876fcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2251,11 +2251,26 @@ rxjs "6" semver "^7.3.5" +"@ledgerhq/devices@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-6.1.0.tgz#54963409011c0bb83e2cc3d4d55ad9b905e296ff" + integrity sha512-Swl08sVuvx7IL9yx9P0ZzvwjIl4JXl51X34Po3pT2uRRaLnh/fRRSNe9tSC1gFMioviiLJlkmO+yydE4XeV+Jg== + dependencies: + "@ledgerhq/errors" "^6.0.2" + "@ledgerhq/logs" "^6.0.2" + rxjs "6" + semver "^7.3.5" + "@ledgerhq/errors@^5.34.0", "@ledgerhq/errors@^5.50.0": version "5.50.0" resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-5.50.0.tgz#e3a6834cb8c19346efca214c1af84ed28e69dad9" integrity sha512-gu6aJ/BHuRlpU7kgVpy2vcYk6atjB4iauP2ymF7Gk0ez0Y/6VSMVSJvubeEQN+IV60+OBK0JgeIZG7OiHaw8ow== +"@ledgerhq/errors@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.0.2.tgz#7c88d16620db08c96de6a2636440db1c0e541da1" + integrity sha512-m42ZMzR/EKpOrZfPR3DzusE98DoF3d03cbBkQG6ddm6diwVXFSa7MabaKzgD+41EYQ+hrCGOEZK1K0kosX1itg== + "@ledgerhq/hw-transport-node-hid-noevents@^5.51.1": version "5.51.1" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-5.51.1.tgz#71f37f812e448178ad0bcc2258982150d211c1ab" @@ -2302,6 +2317,16 @@ "@ledgerhq/logs" "^5.50.0" rxjs "6" +"@ledgerhq/hw-transport-webusb@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.1.0.tgz#47f779c000de6b1f6d7e3b44805bcc18beea416c" + integrity sha512-UsfB9UcDgAo6ykO40Ai8qimv/VYvGwZVIe65nUGs6JK+2PRnkuk2i00FTK3NTgmvIeX4cOIlkg8+ZmGQuB8Evw== + dependencies: + "@ledgerhq/devices" "^6.1.0" + "@ledgerhq/errors" "^6.0.2" + "@ledgerhq/hw-transport" "^6.1.0" + "@ledgerhq/logs" "^6.0.2" + "@ledgerhq/hw-transport@^5.34.0", "@ledgerhq/hw-transport@^5.51.1": version "5.51.1" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-5.51.1.tgz#8dd14a8e58cbee4df0c29eaeef983a79f5f22578" @@ -2311,11 +2336,25 @@ "@ledgerhq/errors" "^5.50.0" events "^3.3.0" +"@ledgerhq/hw-transport@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.1.0.tgz#b50151c6199e9ad6d3ab9e447532a22170c9f3b7" + integrity sha512-j9IyvksI9PjFoFrk/B3p8wCXWRWc8uK24gc20pAaXQiDtqMkWqEge8iZyPKWBVIv69vDQF3LE3Y6EeRwwA7wJA== + dependencies: + "@ledgerhq/devices" "^6.1.0" + "@ledgerhq/errors" "^6.0.2" + events "^3.3.0" + "@ledgerhq/logs@^5.30.0", "@ledgerhq/logs@^5.50.0": version "5.50.0" resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-5.50.0.tgz#29c6419e8379d496ab6d0426eadf3c4d100cd186" integrity sha512-swKHYCOZUGyVt4ge0u8a7AwNcA//h4nx5wIi0sruGye1IJ5Cva0GyK9L2/WdX+kWVTKp92ZiEo1df31lrWGPgA== +"@ledgerhq/logs@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.0.2.tgz#a6063ed99461c0d2c36a48de89ed0283f13cb908" + integrity sha512-4lU3WBwugG+I/dv/qE8HQ2f7MNsKfU58FEzSE1PAELvW96umrlO4ogwuO1tRCPmrtOo9ssam1QVYotwELY8zvw== + "@malept/cross-spawn-promise@^1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d"