Skip to content

Commit

Permalink
feat: add support for ledger & ledger dev tools
Browse files Browse the repository at this point in the history
  • Loading branch information
dawidsowardx committed Apr 14, 2023
1 parent 675b713 commit 3842323
Show file tree
Hide file tree
Showing 38 changed files with 2,060 additions and 16 deletions.
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,16 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@ledgerhq/hw-transport-webhid": "^6.27.12",
"@stitches/react": "^1.2.8",
"@types/blake2b": "^2.1.0",
"bech32": "^2.0.0",
"bip32": "^2.0.0",
"bip39": "^3.1.0",
"blake2b": "^2.1.4",
"buffer": "^6.0.3",
"ed25519-hd-key": "^1.3.0",
"elliptic": "^6.5.4",
"lodash.chunk": "^4.2.0",
"loglevel": "^1.8.0",
"neverthrow": "^5.0.1",
Expand All @@ -50,7 +56,7 @@
"@commitlint/cli": "^17.3.0",
"@commitlint/config-conventional": "^17.3.0",
"@crxjs/vite-plugin": "^1.0.14",
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
"@hirez_io/observer-spy": "^2.2.0",
"@koush/wrtc": "^0.5.3",
Expand All @@ -62,10 +68,12 @@
"@types/blake2b": "^2.1.0",
"@types/chai": "^4.3.3",
"@types/chrome": "^0.0.224",
"@types/elliptic": "^6.4.14",
"@types/jest": "^29.1.2",
"@types/lodash.chunk": "^4.2.7",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@types/w3c-web-hid": "^1.0.3",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"@vitejs/plugin-react": "^2.1.0",
Expand All @@ -90,6 +98,7 @@
"ts-node": "^10.9.1",
"typescript": "^4.8.4",
"vite": "^4.1.4",
"vite-compatible-readable-stream": "^3.6.1",
"vite-tsconfig-paths": "^4.0.7"
},
"repository": {
Expand Down
3 changes: 3 additions & 0 deletions src/buffer-shim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Buffer } from 'buffer'

globalThis.Buffer = Buffer
31 changes: 31 additions & 0 deletions src/chrome/background/background-with-dev-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import './background'
import { getExtensionTabsByUrl } from 'chrome/helpers/get-extension-tabs-by-url'
import { config } from 'config'

const openRadixDevToolsPage = async () => {
const devToolsUrl = chrome.runtime.getURL(config.devTools.url)

const result = await getExtensionTabsByUrl(config.devTools.url)

if (result.isErr()) return

const [devToolsTab] = result.value

if (devToolsTab?.id) {
await chrome.tabs.update(devToolsTab.id, { active: true })
} else {
await chrome.tabs.create({
url: devToolsUrl,
})
}
}

chrome.contextMenus.removeAll(() => {
chrome.contextMenus.create({
id: 'radix-dev-tools',
title: 'Radix Dev Tools',
contexts: ['all'],
})

chrome.contextMenus.onClicked.addListener(async () => openRadixDevToolsPage())
})
28 changes: 28 additions & 0 deletions src/chrome/background/message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import {
MessageHandlerOutput,
} from '../messages/_types'
import { getConnectionPassword as getConnectionPasswordFn } from '../helpers/get-connection-password'
import { config } from 'config'
import { createOrFocusTab } from 'chrome/helpers/create-or-focus-tab'
import { sendMessage } from 'chrome/messages/send-message'
import { createMessage } from 'chrome/messages/create-message'

export type BackgroundMessageHandler = ReturnType<
typeof BackgroundMessageHandler
Expand Down Expand Up @@ -66,6 +70,30 @@ export const BackgroundMessageHandler =
}))
}

case messageDiscriminator.walletToLedger:
return createOrFocusTab(config.popup.pages.ledger)
.map((tab) => {
const tabRemovedListener = (tabId: number) => {
if (tabId === tab.id) {
chrome.tabs.onRemoved.removeListener(tabRemovedListener)
sendMessage(
createMessage.confirmationError('ledger', message.messageId, {
reason: 'tabClosed',
})
)
}
}

chrome.tabs.onRemoved.addListener(tabRemovedListener)

return sendMessageWithConfirmation(
{ ...message, source: 'background' },
tab.id
)
})
.map(() => ({ sendConfirmation: true }))
.mapErr(() => ({ reason: 'failedToOpenLedgerTab' }))

default:
return errAsync({
reason: 'unhandledMessageDiscriminator',
Expand Down
213 changes: 213 additions & 0 deletions src/chrome/dev-tools/components/LedgerSimulator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { generateMnemonic } from 'bip39'
import { Box, Button, Header, Text } from 'components'
import {
createLedgerDeviceIdResponse,
createLedgerPublicKeyResponse,
createLedgerSignedTransactionResponse,
} from 'ledger/schemas'
import { useState } from 'react'
import { createRadixWallet } from '../hd-wallet/hd-wallet'
import { Curve } from '../hd-wallet/models'
import { ec as Elliptic } from 'elliptic'
import blake2b from 'blake2b'
import { logger } from 'utils/logger'
import { sendMessage } from 'chrome/messages/send-message'
import { createMessage } from 'chrome/messages/create-message'
import { compiledTxHex } from '../example'

const secp256k1 = new Elliptic('secp256k1')
const ed25519 = new Elliptic('ed25519')

const buf2hex = (buffer: any) =>
[...new Uint8Array(buffer)]
.map((x) => x.toString(16).padStart(2, '0'))
.join('')

export const LedgerSimulator = () => {
const [seed, setSeed] = useState<string>(
'equip will roof matter pink blind book anxiety banner elbow sun young'
)
const [interactionId, setInteractionId] = useState<string>(
crypto.randomUUID()
)
const [device, setDevice] = useState<string>('0')
const [curve, setCurve] = useState<keyof typeof Curve>('secp256k1')
const [txIntent, setTxIntent] = useState<string>(
compiledTxHex.createFungibleResourceWithInitialSupply
)
const [hdPath, setHdPath] = useState<string>(`m/44'/1022'/10'/525'/0'/1238'`)
const updateMnemonic = () => {
setSeed(generateMnemonic())
}

const sendDeviceIdResponse = async () => {
const wallet = createRadixWallet({ seed, curve: 'ed25519' })
const publicKey = wallet.derivePath(`365'`).publicKey.slice(2)
const hashed = await crypto.subtle.digest(
'SHA-256',
Buffer.from(publicKey, 'hex')
)
const hashed2 = await crypto.subtle.digest('SHA-256', hashed)
const response = createLedgerDeviceIdResponse(
{ interactionId, discriminator: 'getDeviceInfo' },
buf2hex(hashed2),
device
)
sendMessage(createMessage.ledgerResponse(response))
setInteractionId(crypto.randomUUID())
}

const sendPublicKeyResponse = async () => {
const wallet = createRadixWallet({ seed, curve })
const publicKey = wallet.deriveFullPath(hdPath).publicKey
const response = createLedgerPublicKeyResponse(
{ interactionId, discriminator: 'derivePublicKey' },
curve === 'ed25519' ? publicKey.slice(2) : publicKey
)
sendMessage(createMessage.ledgerResponse(response))
setInteractionId(crypto.randomUUID())
}

const signTx = async () => {
const wallet = createRadixWallet({ seed, curve })
const privateKey = wallet.deriveFullPath(hdPath).privateKey
const publicKey = wallet.deriveFullPath(hdPath).publicKey
const output = new Uint8Array(64)
const hash = blake2b(output.length)
.update(Buffer.from(txIntent, 'base64'))
.digest('hex')

logger.debug('TX intent blake hash', hash)

if (curve === Curve.ed25519) {
const pair = ed25519.keyFromPrivate(privateKey)
const signed = pair.sign(hash)
const signedTx = signed.r.toString(16, 32) + signed.s.toString(16, 32)
const response = createLedgerSignedTransactionResponse(
{ interactionId, discriminator: 'signTransaction' },
signedTx,
publicKey
)
sendMessage(createMessage.ledgerResponse(response))
setInteractionId(crypto.randomUUID())
} else {
const pair = secp256k1.keyFromPrivate(privateKey)
const signed = pair.sign(hash)
const signedTx =
signed.recoveryParam?.toString(16).padStart(2, '0') +
signed.r.toString(16, 32) +
signed.s.toString(16, 32)
const response = createLedgerSignedTransactionResponse(
{ interactionId, discriminator: 'signTransaction' },
signedTx,
publicKey
)
sendMessage(createMessage.ledgerResponse(response))
setInteractionId(crypto.randomUUID())
}
}

return (
<Box full p="medium">
<Header dark>Ledger Simulator</Header>
<Box flex="row" items="center">
<Text bold css={{ minWidth: '140px' }}>
Mnemonic / Seed
</Text>
<input
className="w-100"
value={seed}
onChange={(ev) => setSeed(ev.target.value)}
/>
<Button ml="small" onClick={updateMnemonic}>
Regenerate
</Button>
</Box>
<Box flex="row" items="center">
<Text bold css={{ minWidth: '140px' }}>
Interaction ID
</Text>
<input
className="w-100"
value={interactionId}
onChange={(ev) => setInteractionId(ev.target.value)}
/>
<Button
ml="small"
onClick={(ev) => setInteractionId(crypto.randomUUID())}
>
Regenerate
</Button>
</Box>
<Box flex="row" items="center">
<Text bold css={{ minWidth: '140px' }}>
Derivation Path
</Text>
<Box>
<input
className="w-100"
value={hdPath}
onChange={(ev) => setHdPath(ev.target.value)}
/>
<Text
muted
size="small"
>{`m/44'/<COIN_TYPE>'/<NETWORK_ID>'/<ENTITY_TYPE>'/<ENTITY_INDEX>'/<KEY_TYPE>'`}</Text>
</Box>
</Box>
<Box flex="row" items="center">
<Text bold css={{ minWidth: '140px' }}>
Compiled TxIntent
</Text>
<Box flex="row">
<textarea
name="compiled_intent"
cols={70}
rows={12}
value={txIntent}
onInput={(ev) => {
// @ts-ignore
setTxIntent(ev.target.value || '')
}}
/>
<Box flex="col">
{Object.keys(compiledTxHex).map((key) => (
<Button key={key} onClick={() => setTxIntent(compiledTxHex[key])}>
{key}
</Button>
))}
</Box>
</Box>
</Box>
<Box flex="row">
<Box>
<Text bold>Ledger model</Text>
<select onChange={(ev) => setDevice(ev.target.value)}>
<option value="0">Nano S</option>
<option value="1">Nano S Plus</option>
<option value="2">Nano X</option>
</select>
</Box>
<Box>
<Text bold>Curve</Text>
<select
onChange={(ev) => setCurve(ev.target.value as keyof typeof Curve)}
>
<option value="secp256k1">secp256k1</option>
<option value="curve25519">curve25519</option>
</select>
</Box>
</Box>
<Box>
<Text bold>Actions</Text>
<Button onClick={sendDeviceIdResponse}>Send Device ID Response</Button>
<Button onClick={sendPublicKeyResponse} ml="small">
Send Public Key Response
</Button>
<Button onClick={signTx} ml="small">
Sign Tx
</Button>
</Box>
</Box>
)
}
Loading

0 comments on commit 3842323

Please sign in to comment.