Skip to content

Commit 7b65fe8

Browse files
author
sim
committed
Support ledger
1 parent 23f316e commit 7b65fe8

18 files changed

Lines changed: 1093 additions & 301 deletions

package-lock.json

Lines changed: 604 additions & 245 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
},
1616
"dependencies": {
1717
"@anchor-protocol/anchor.js": "^3.0.1",
18+
"@ledgerhq/hw-transport-webhid": "^6.20.0",
19+
"@ledgerhq/hw-transport-webusb": "^6.20.0",
1820
"@mui/icons-material": "^5.2.5",
1921
"@mui/material": "^5.2.5",
22+
"@terra-money/ledger-terra-js": "^1.1.0",
2023
"@terra-money/log-finder-ruleset": "^3.0.0",
2124
"@terra-money/terra.js": "^3.0.2",
2225
"@terra-money/wallet-provider": "^3.6.0",
@@ -47,6 +50,8 @@
4750
"recharts": "^2.1.8",
4851
"recoil": "^0.5.2",
4952
"sass": "^1.45.1",
53+
"secp256k1": "^4.0.2",
54+
"semver": "^7.3.5",
5055
"sentence-case": "^3.0.4",
5156
"xss": "^1.0.10"
5257
},
@@ -66,6 +71,8 @@
6671
"@types/react-dom": "^17.0.11",
6772
"@types/react-modal": "^3.13.1",
6873
"@types/react-router-dom": "^5.3.2",
74+
"@types/secp256k1": "^4.0.3",
75+
"@types/semver": "^7.3.9",
6976
"husky": "^7.0.4",
7077
"lint-staged": "^12.1.4",
7178
"react-error-overlay": "6.0.9",

src/app/sections/ConnectWallet.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { ReactNode } from "react"
22
import { useTranslation } from "react-i18next"
3+
import UsbIcon from "@mui/icons-material/Usb"
34
import { ConnectType, useWallet } from "@terra-money/wallet-provider"
4-
import { EXTENSION } from "config/constants"
55
import { useAddress } from "data/wallet"
6-
import { Button, ExternalLink } from "components/general"
6+
import { Button } from "components/general"
77
import { Grid } from "components/layout"
88
import { List } from "components/display"
99
import { ModalButton } from "components/feedback"
10-
import { FormHelp } from "components/form"
1110
import { useAuth } from "auth"
1211
import SwitchWallet from "auth/modules/select/SwitchWallet"
1312
import Connected from "./Connected"
@@ -39,6 +38,11 @@ const ConnectWallet = ({ renderButton }: Props) => {
3938
children: type === ConnectType.EXTENSION ? t("Extension") : name,
4039
onClick: () => connect(type, identifier),
4140
})),
41+
{
42+
icon: <UsbIcon />,
43+
to: "/auth/ledger",
44+
children: t("Access with ledger"),
45+
},
4246
...availableInstallTypes
4347
.filter((type) => type === ConnectType.EXTENSION)
4448
.map((type) => ({
@@ -55,11 +59,6 @@ const ConnectWallet = ({ renderButton }: Props) => {
5559
<Grid gap={20}>
5660
<SwitchWallet />
5761
<List list={available.length ? available : list} />
58-
<FormHelp>
59-
Use{" "}
60-
<ExternalLink href={EXTENSION}>Terra Station Extension</ExternalLink>{" "}
61-
to access with Ledger wallet
62-
</FormHelp>
6362
</Grid>
6463
</ModalButton>
6564
)

src/app/sections/Connected.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ const Connected = () => {
6868
size="small"
6969
outline
7070
>
71-
{wallet?.name ?? truncate(address)}
71+
{wallet && "name" in wallet ? wallet.name : truncate(address)}
7272
</Button>
7373
</Popover>
7474
)

src/auth/auth.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ interface Wallet {
55
address: string
66
}
77

8+
interface LedgerWallet {
9+
address: string
10+
ledger: true
11+
}
12+
813
interface StoredWallet extends Wallet {
914
encrypted: string
1015
}

src/auth/hooks/useAuth.ts

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
import { useCallback } from "react"
1+
import { useCallback, useMemo } from "react"
22
import { atom, useRecoilState } from "recoil"
33
import { encode } from "js-base64"
4-
import { CreateTxOptions, RawKey } from "@terra-money/terra.js"
4+
import { AccAddress, CreateTxOptions } from "@terra-money/terra.js"
5+
import { PublicKey, RawKey, SignatureV2 } from "@terra-money/terra.js"
6+
import { useChainID } from "data/wallet"
57
import { useLCDClient } from "data/Terra/lcdClient"
68
import { PasswordError } from "../scripts/keystore"
79
import { getDecryptedKey, testPassword } from "../scripts/keystore"
810
import { getWallet, storeWallet, clearWallet } from "../scripts/keystore"
911
import { getStoredWallet, getStoredWallets } from "../scripts/keystore"
1012
import encrypt from "../scripts/encrypt"
13+
import * as ledger from "../ledger/ledger"
14+
import LedgerKey from "../ledger/LedgerKey"
1115
import useAvailable from "./useAvailable"
1216

1317
const walletState = atom({
@@ -33,22 +37,49 @@ const useAuth = () => {
3337
[setWallet]
3438
)
3539

40+
const connectLedger = useCallback(
41+
(address: AccAddress) => {
42+
const wallet = { address, ledger: true as const }
43+
storeWallet(wallet)
44+
setWallet(wallet)
45+
},
46+
[setWallet]
47+
)
48+
3649
const disconnect = useCallback(() => {
3750
clearWallet()
3851
setWallet(undefined)
3952
}, [setWallet])
4053

4154
/* helpers */
42-
const getConnectedWallet = () => {
43-
if (!wallet) throw new Error("Wallet is not connected")
55+
const connectedWallet = useMemo(() => {
56+
if (!(wallet && "name" in wallet)) return
4457
return wallet
58+
}, [wallet])
59+
60+
const getConnectedWallet = () => {
61+
if (!connectedWallet) throw new Error("Wallet is not defined")
62+
return connectedWallet
4563
}
4664

4765
const getKey = (password: string) => {
4866
const { name } = getConnectedWallet()
4967
return getDecryptedKey({ name, password })
5068
}
5169

70+
const getLedgerKey = async () => {
71+
const pk = await ledger.getPubKey()
72+
if (!pk) throw new Error("Public key is not defined")
73+
74+
const publicKey = PublicKey.fromAmino({
75+
type: "tendermint/PubKeySecp256k1",
76+
value: pk.toString("base64"),
77+
})
78+
79+
const key = new LedgerKey(publicKey)
80+
return key
81+
}
82+
5283
/* manage: export */
5384
const encodeEncryptedWallet = (password: string) => {
5485
const { name, address } = getConnectedWallet()
@@ -68,19 +99,38 @@ const useAuth = () => {
6899
}
69100

70101
/* tx */
71-
const post = async (txOptions: CreateTxOptions, password: string) => {
72-
const pk = getKey(password)
73-
if (!pk) throw new PasswordError("Incorrect password")
74-
const rk = new RawKey(Buffer.from(pk, "hex"))
75-
const wallet = lcd.wallet(rk)
76-
const signedTx = await wallet.createAndSignTx(txOptions)
77-
return { result: await lcd.tx.broadcastSync(signedTx) }
102+
const chainID = useChainID()
103+
const post = async (txOptions: CreateTxOptions, password = "") => {
104+
if (!wallet) throw new Error("Wallet is not defined")
105+
const { address } = wallet
106+
107+
if ("ledger" in wallet) {
108+
const key = await getLedgerKey()
109+
const wallet = lcd.wallet(key)
110+
const { account_number: accountNumber, sequence } =
111+
await wallet.accountNumberAndSequence()
112+
const signMode = SignatureV2.SignMode.SIGN_MODE_LEGACY_AMINO_JSON
113+
const unsignedTx = await lcd.tx.create([{ address }], txOptions)
114+
const options = { chainID, accountNumber, sequence, signMode }
115+
const signedTx = await key.signTx(unsignedTx, options)
116+
return { result: await lcd.tx.broadcastSync(signedTx) }
117+
} else {
118+
const pk = getKey(password)
119+
if (!pk) throw new PasswordError("Incorrect password")
120+
const key = new RawKey(Buffer.from(pk, "hex"))
121+
const wallet = lcd.wallet(key)
122+
const signedTx = await wallet.createAndSignTx(txOptions)
123+
return { result: await lcd.tx.broadcastSync(signedTx) }
124+
}
78125
}
79126

80127
return {
81128
wallet,
82129
wallets,
130+
getConnectedWallet,
131+
connectedWallet,
83132
connect,
133+
connectLedger,
84134
disconnect,
85135
available,
86136
encodeEncryptedWallet,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { useEffect, useState } from "react"
2+
import { useTranslation } from "react-i18next"
3+
import { useNavigate } from "react-router-dom"
4+
import UsbIcon from "@mui/icons-material/Usb"
5+
import { Button } from "components/general"
6+
import { Card, Grid, Page } from "components/layout"
7+
import { FormError } from "components/form"
8+
import useAuth from "../hooks/useAuth"
9+
import * as ledger from "./ledger"
10+
11+
const AccessWithLedger = () => {
12+
const { t } = useTranslation()
13+
const navigate = useNavigate()
14+
const { connectLedger, wallet } = useAuth()
15+
const [isLoading, setIsLoading] = useState(false)
16+
const [error, setError] = useState<Error>()
17+
18+
const connect = async () => {
19+
setIsLoading(true)
20+
setError(undefined)
21+
22+
try {
23+
const address = await ledger.getTerraAddress()
24+
connectLedger(address)
25+
} catch (error) {
26+
setError(error as Error)
27+
} finally {
28+
setIsLoading(false)
29+
}
30+
}
31+
32+
useEffect(() => {
33+
if (wallet) navigate("/wallet", { replace: true })
34+
}, [navigate, wallet])
35+
36+
return (
37+
<Page title={t("Access with ledger")} small>
38+
<Card>
39+
<Grid gap={20} className="center">
40+
<p className="center">
41+
<UsbIcon style={{ fontSize: 56 }} />
42+
</p>
43+
44+
{t("Plug in the Ledger Wallet")}
45+
46+
{error && <FormError>{error.message}</FormError>}
47+
48+
<Button onClick={connect} loading={isLoading} color="primary" block>
49+
{t("Connect")}
50+
</Button>
51+
</Grid>
52+
</Card>
53+
</Page>
54+
)
55+
}
56+
57+
export default AccessWithLedger

src/auth/ledger/LedgerKey.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Key, SignDoc, SignatureV2 } from "@terra-money/terra.js"
2+
import * as ledger from "./ledger"
3+
4+
class LedgerKey extends Key {
5+
public sign(): Promise<Buffer> {
6+
throw new Error(
7+
"LedgerKey does not use sign() -- use createSignature() directly."
8+
)
9+
}
10+
11+
public async createSignatureAmino(tx: SignDoc): Promise<SignatureV2> {
12+
const pubkeyBuffer = await ledger.getPubKey()
13+
14+
if (!pubkeyBuffer) {
15+
throw new Error("failed getting public key from ledger")
16+
}
17+
18+
const signatureBuffer = await ledger.sign(tx.toAminoJSON())
19+
20+
if (!signatureBuffer) {
21+
throw new Error("failed signing from ledger")
22+
}
23+
24+
if (!this.publicKey) {
25+
throw new Error("public key is undefined")
26+
}
27+
28+
return new SignatureV2(
29+
this.publicKey,
30+
new SignatureV2.Descriptor(
31+
new SignatureV2.Descriptor.Single(
32+
SignatureV2.SignMode.SIGN_MODE_LEGACY_AMINO_JSON,
33+
signatureBuffer.toString("base64")
34+
)
35+
),
36+
tx.sequence
37+
)
38+
}
39+
}
40+
41+
export default LedgerKey

0 commit comments

Comments
 (0)