Skip to content

Commit

Permalink
Support ledger
Browse files Browse the repository at this point in the history
  • Loading branch information
sim committed Dec 27, 2021
1 parent 23f316e commit 7b65fe8
Show file tree
Hide file tree
Showing 18 changed files with 1,093 additions and 301 deletions.
849 changes: 604 additions & 245 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
},
"dependencies": {
"@anchor-protocol/anchor.js": "^3.0.1",
"@ledgerhq/hw-transport-webhid": "^6.20.0",
"@ledgerhq/hw-transport-webusb": "^6.20.0",
"@mui/icons-material": "^5.2.5",
"@mui/material": "^5.2.5",
"@terra-money/ledger-terra-js": "^1.1.0",
"@terra-money/log-finder-ruleset": "^3.0.0",
"@terra-money/terra.js": "^3.0.2",
"@terra-money/wallet-provider": "^3.6.0",
Expand Down Expand Up @@ -47,6 +50,8 @@
"recharts": "^2.1.8",
"recoil": "^0.5.2",
"sass": "^1.45.1",
"secp256k1": "^4.0.2",
"semver": "^7.3.5",
"sentence-case": "^3.0.4",
"xss": "^1.0.10"
},
Expand All @@ -66,6 +71,8 @@
"@types/react-dom": "^17.0.11",
"@types/react-modal": "^3.13.1",
"@types/react-router-dom": "^5.3.2",
"@types/secp256k1": "^4.0.3",
"@types/semver": "^7.3.9",
"husky": "^7.0.4",
"lint-staged": "^12.1.4",
"react-error-overlay": "6.0.9",
Expand Down
15 changes: 7 additions & 8 deletions src/app/sections/ConnectWallet.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { ReactNode } from "react"
import { useTranslation } from "react-i18next"
import UsbIcon from "@mui/icons-material/Usb"
import { ConnectType, useWallet } from "@terra-money/wallet-provider"
import { EXTENSION } from "config/constants"
import { useAddress } from "data/wallet"
import { Button, ExternalLink } from "components/general"
import { Button } from "components/general"
import { Grid } from "components/layout"
import { List } from "components/display"
import { ModalButton } from "components/feedback"
import { FormHelp } from "components/form"
import { useAuth } from "auth"
import SwitchWallet from "auth/modules/select/SwitchWallet"
import Connected from "./Connected"
Expand Down Expand Up @@ -39,6 +38,11 @@ const ConnectWallet = ({ renderButton }: Props) => {
children: type === ConnectType.EXTENSION ? t("Extension") : name,
onClick: () => connect(type, identifier),
})),
{
icon: <UsbIcon />,
to: "/auth/ledger",
children: t("Access with ledger"),
},
...availableInstallTypes
.filter((type) => type === ConnectType.EXTENSION)
.map((type) => ({
Expand All @@ -55,11 +59,6 @@ const ConnectWallet = ({ renderButton }: Props) => {
<Grid gap={20}>
<SwitchWallet />
<List list={available.length ? available : list} />
<FormHelp>
Use{" "}
<ExternalLink href={EXTENSION}>Terra Station Extension</ExternalLink>{" "}
to access with Ledger wallet
</FormHelp>
</Grid>
</ModalButton>
)
Expand Down
2 changes: 1 addition & 1 deletion src/app/sections/Connected.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const Connected = () => {
size="small"
outline
>
{wallet?.name ?? truncate(address)}
{wallet && "name" in wallet ? wallet.name : truncate(address)}
</Button>
</Popover>
)
Expand Down
5 changes: 5 additions & 0 deletions src/auth/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ interface Wallet {
address: string
}

interface LedgerWallet {
address: string
ledger: true
}

interface StoredWallet extends Wallet {
encrypted: string
}
Expand Down
72 changes: 61 additions & 11 deletions src/auth/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { useCallback } from "react"
import { useCallback, useMemo } from "react"
import { atom, useRecoilState } from "recoil"
import { encode } from "js-base64"
import { CreateTxOptions, RawKey } from "@terra-money/terra.js"
import { AccAddress, CreateTxOptions } from "@terra-money/terra.js"
import { PublicKey, RawKey, SignatureV2 } from "@terra-money/terra.js"
import { useChainID } from "data/wallet"
import { useLCDClient } from "data/Terra/lcdClient"
import { PasswordError } from "../scripts/keystore"
import { getDecryptedKey, testPassword } from "../scripts/keystore"
import { getWallet, storeWallet, clearWallet } from "../scripts/keystore"
import { getStoredWallet, getStoredWallets } from "../scripts/keystore"
import encrypt from "../scripts/encrypt"
import * as ledger from "../ledger/ledger"
import LedgerKey from "../ledger/LedgerKey"
import useAvailable from "./useAvailable"

const walletState = atom({
Expand All @@ -33,22 +37,49 @@ const useAuth = () => {
[setWallet]
)

const connectLedger = useCallback(
(address: AccAddress) => {
const wallet = { address, ledger: true as const }
storeWallet(wallet)
setWallet(wallet)
},
[setWallet]
)

const disconnect = useCallback(() => {
clearWallet()
setWallet(undefined)
}, [setWallet])

/* helpers */
const getConnectedWallet = () => {
if (!wallet) throw new Error("Wallet is not connected")
const connectedWallet = useMemo(() => {
if (!(wallet && "name" in wallet)) return
return wallet
}, [wallet])

const getConnectedWallet = () => {
if (!connectedWallet) throw new Error("Wallet is not defined")
return connectedWallet
}

const getKey = (password: string) => {
const { name } = getConnectedWallet()
return getDecryptedKey({ name, password })
}

const getLedgerKey = async () => {
const pk = await ledger.getPubKey()
if (!pk) throw new Error("Public key is not defined")

const publicKey = PublicKey.fromAmino({
type: "tendermint/PubKeySecp256k1",
value: pk.toString("base64"),
})

const key = new LedgerKey(publicKey)
return key
}

/* manage: export */
const encodeEncryptedWallet = (password: string) => {
const { name, address } = getConnectedWallet()
Expand All @@ -68,19 +99,38 @@ const useAuth = () => {
}

/* tx */
const post = async (txOptions: CreateTxOptions, password: string) => {
const pk = getKey(password)
if (!pk) throw new PasswordError("Incorrect password")
const rk = new RawKey(Buffer.from(pk, "hex"))
const wallet = lcd.wallet(rk)
const signedTx = await wallet.createAndSignTx(txOptions)
return { result: await lcd.tx.broadcastSync(signedTx) }
const chainID = useChainID()
const post = async (txOptions: CreateTxOptions, password = "") => {
if (!wallet) throw new Error("Wallet is not defined")
const { address } = wallet

if ("ledger" in wallet) {
const key = await getLedgerKey()
const wallet = lcd.wallet(key)
const { account_number: accountNumber, sequence } =
await wallet.accountNumberAndSequence()
const signMode = SignatureV2.SignMode.SIGN_MODE_LEGACY_AMINO_JSON
const unsignedTx = await lcd.tx.create([{ address }], txOptions)
const options = { chainID, accountNumber, sequence, signMode }
const signedTx = await key.signTx(unsignedTx, options)
return { result: await lcd.tx.broadcastSync(signedTx) }
} else {
const pk = getKey(password)
if (!pk) throw new PasswordError("Incorrect password")
const key = new RawKey(Buffer.from(pk, "hex"))
const wallet = lcd.wallet(key)
const signedTx = await wallet.createAndSignTx(txOptions)
return { result: await lcd.tx.broadcastSync(signedTx) }
}
}

return {
wallet,
wallets,
getConnectedWallet,
connectedWallet,
connect,
connectLedger,
disconnect,
available,
encodeEncryptedWallet,
Expand Down
57 changes: 57 additions & 0 deletions src/auth/ledger/AccessWithLedger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import UsbIcon from "@mui/icons-material/Usb"
import { Button } from "components/general"
import { Card, Grid, Page } from "components/layout"
import { FormError } from "components/form"
import useAuth from "../hooks/useAuth"
import * as ledger from "./ledger"

const AccessWithLedger = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const { connectLedger, wallet } = useAuth()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<Error>()

const connect = async () => {
setIsLoading(true)
setError(undefined)

try {
const address = await ledger.getTerraAddress()
connectLedger(address)
} catch (error) {
setError(error as Error)
} finally {
setIsLoading(false)
}
}

useEffect(() => {
if (wallet) navigate("/wallet", { replace: true })
}, [navigate, wallet])

return (
<Page title={t("Access with ledger")} small>
<Card>
<Grid gap={20} className="center">
<p className="center">
<UsbIcon style={{ fontSize: 56 }} />
</p>

{t("Plug in the Ledger Wallet")}

{error && <FormError>{error.message}</FormError>}

<Button onClick={connect} loading={isLoading} color="primary" block>
{t("Connect")}
</Button>
</Grid>
</Card>
</Page>
)
}

export default AccessWithLedger
41 changes: 41 additions & 0 deletions src/auth/ledger/LedgerKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Key, SignDoc, SignatureV2 } from "@terra-money/terra.js"
import * as ledger from "./ledger"

class LedgerKey extends Key {
public sign(): Promise<Buffer> {
throw new Error(
"LedgerKey does not use sign() -- use createSignature() directly."
)
}

public async createSignatureAmino(tx: SignDoc): Promise<SignatureV2> {
const pubkeyBuffer = await ledger.getPubKey()

if (!pubkeyBuffer) {
throw new Error("failed getting public key from ledger")
}

const signatureBuffer = await ledger.sign(tx.toAminoJSON())

if (!signatureBuffer) {
throw new Error("failed signing from ledger")
}

if (!this.publicKey) {
throw new Error("public key is undefined")
}

return new SignatureV2(
this.publicKey,
new SignatureV2.Descriptor(
new SignatureV2.Descriptor.Single(
SignatureV2.SignMode.SIGN_MODE_LEGACY_AMINO_JSON,
signatureBuffer.toString("base64")
)
),
tx.sequence
)
}
}

export default LedgerKey
Loading

0 comments on commit 7b65fe8

Please sign in to comment.