Skip to content

Commit

Permalink
feat: ledger support
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Oct 6, 2024
1 parent 52f0c19 commit 3401d8d
Show file tree
Hide file tree
Showing 14 changed files with 2,070 additions and 2,904 deletions.
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ module.exports = {
"^.+\\.css": "<rootDir>/tests/mocks/StylesMock.tsx",
"^.+\\.scss": "<rootDir>/tests/mocks/StylesMock.tsx",
"boltz-bolt12": "<rootDir>/tests/mocks/bolt12.ts",
"@ledgerhq/hw-app-eth": "<rootDir>/tests/mocks/LedgerMock.ts",
"@ledgerhq/hw-transport-webhid": "<rootDir>/tests/mocks/LedgerMock.ts",
},
globals: {
Buffer: Buffer,
Expand Down
4,722 changes: 1,833 additions & 2,889 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
"@bitcoinerlab/secp256k1": "^1.1.1",
"@fontsource/noto-mono": "^5.0.11",
"@fontsource/noto-sans": "^5.0.22",
"@ledgerhq/hw-app-eth": "^6.38.2",
"@ledgerhq/hw-transport-webhid": "^6.29.4",
"@scure/base": "^1.1.7",
"@solid-primitives/i18n": "^2.1.1",
"@solid-primitives/storage": "^4.0.0",
Expand Down
3 changes: 3 additions & 0 deletions public/ledger.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 18 additions & 7 deletions src/components/ConnectWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,25 @@ const Modal = ({
return (
<div
class="provider-modal-entry-wrapper"
onClick={() => connect(notify, connectProvider, provider)}>
onClick={async () => {
if (provider.disabled) {
return;
}

await connect(notify, connectProvider, provider);
}}>
<hr />
<div class="provider-modal-entry">
<img
class="provider-modal-icon"
src={provider.icon}
alt={`${provider.name} icon`}
/>
<div
class="provider-modal-entry"
data-disabled={provider.disabled}>
<Show when={provider.icon !== undefined}>
<img
class="provider-modal-icon"
src={provider.icon}
alt={`${provider.name} icon`}
/>
</Show>

<h4>{provider.name}</h4>
</div>
</div>
Expand Down
7 changes: 6 additions & 1 deletion src/components/CreateButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,13 @@ export const CreateButton = () => {

await setSwapStorage({
...data,
signer: signer()?.address,
signer:
// We do not have to commit to a signer when creating submarine swaps
swapType() !== SwapType.Submarine
? signer()?.address
: undefined,
});

setInvoice("");
setInvoiceValid(false);
setOnchainAddress("");
Expand Down
1 change: 1 addition & 0 deletions src/components/LockupEvm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const LockupEvm = ({
);
const currentSwap = await getSwap(swapId);
currentSwap.lockupTx = tx.hash;
currentSwap.signer = signer().address;
await setSwapStorage(currentSwap);
}}
children={<ConnectWallet />}
Expand Down
22 changes: 20 additions & 2 deletions src/context/Web3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,27 @@ import {

import { config } from "../config";
import { RBTC } from "../consts/Assets";
import LedgerSigner from "../utils/LedgerSigner";
import { Contracts, getContracts } from "../utils/boltzClient";
import { useGlobalContext } from "./Global";
import LedgerIcon from "/ledger.svg";

declare global {
interface WindowEventMap {
"eip6963:announceProvider": CustomEvent;
}

interface Navigator {
hid: {};
}
}

export type EIP6963ProviderInfo = {
rdns: string;
uuid: string;
name: string;
icon: string;
icon?: string;
disabled?: boolean;
};

type EIP1193Provider = {
Expand Down Expand Up @@ -86,7 +93,18 @@ const Web3SignerProvider = (props: {

const [providers, setProviders] = createSignal<
Record<string, EIP6963ProviderDetail>
>({});
>({
ledger: {
provider: new LedgerSigner(),
info: {
name: "Ledger",
uuid: "ledger",
rdns: "ledger",
icon: LedgerIcon,
disabled: navigator.hid === undefined,
},
},
});
const [signer, setSigner] = createSignal<Signer | undefined>(undefined);
const [rawProvider, setRawProvider] = createSignal<
EIP1193Provider | undefined
Expand Down
1 change: 0 additions & 1 deletion src/rif/Signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const sign = async (signer: Signer, request: EnvelopingRequest) => {

export const relayClaimTransaction = async (
signer: Signer,
signerRns: string,
etherSwap: EtherSwap,
preimage: string,
amount: number,
Expand Down
1 change: 0 additions & 1 deletion src/status/TransactionConfirmed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const ClaimRsk = ({
if (useRif) {
transactionHash = await relayClaimTransaction(
signer(),
signer().rdns,
getEtherSwap(),
preimage,
amount,
Expand Down
5 changes: 5 additions & 0 deletions src/style/web3.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
cursor: pointer;
}

.provider-modal-entry[data-disabled="true"] {
opacity: 0.6;
cursor: not-allowed;
}

.provider-modal-entry h4 {
flex-grow: 1;
}
Expand Down
177 changes: 177 additions & 0 deletions src/utils/LedgerSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import Eth from "@ledgerhq/hw-app-eth";
import Transport from "@ledgerhq/hw-transport";
import TransportWebHID from "@ledgerhq/hw-transport-webhid";
import {
JsonRpcProvider,
Signature,
Transaction,
TransactionLike,
TypedDataEncoder,
} from "ethers";
import log from "loglevel";

import { config } from "../config";

class LedgerSigner {
private static readonly ethereumApp = "Ethereum";
private static readonly path = "44'/60'/0'/0/0";

private readonly provider: JsonRpcProvider;
private transport?: Transport;

constructor() {
this.provider = new JsonRpcProvider(
config.assets["RBTC"].network.rpcUrls[0],
);
}

public request = async (request: {
method: string;
params?: Array<unknown>;
}) => {
switch (request.method) {
case "eth_requestAccounts": {
log.debug("Getting Ledger accounts");

if (this.transport === undefined) {
this.transport = await TransportWebHID.create();
}

const openApp = (await this.getApp()).name;
log.debug(`Ledger has app open: ${openApp}`);
if (openApp !== LedgerSigner.ethereumApp) {
log.info(`Opening Ledger ${LedgerSigner.ethereumApp} app`);
await this.openApp(LedgerSigner.ethereumApp);
}

const eth = new Eth(this.transport);
const { address } = await eth.getAddress(LedgerSigner.path);

return [address];
}

case "eth_sendTransaction": {
log.debug("Signing transaction with Ledger");

const txParams = request.params[0] as TransactionLike;

const [nonce, network, feeData] = await Promise.all([
this.provider.getTransactionCount(txParams.from),
this.provider.getNetwork(),
this.provider.getFeeData(),
]);

const tx = Transaction.from({
...txParams,
nonce,
type: 0,
from: undefined,
chainId: network.chainId,
gasPrice: feeData.gasPrice,
gasLimit: (txParams as any).gas,
});

const eth = new Eth(this.transport);
const signature = await eth.clearSignTransaction(
LedgerSigner.path,
tx.unsignedSerialized.substring(2),
{},
);

tx.signature = this.serializeSignature(signature);
await this.provider.send("eth_sendRawTransaction", [
tx.serialized,
]);

return tx.hash;
}

case "eth_signTypedData_v4": {
log.debug("Signing EIP-712 message with Ledger");

const eth = new Eth(this.transport);
const message = JSON.parse(request.params[1] as string);

try {
const signature = await eth.signEIP712Message(
LedgerSigner.path,
message,
);
return this.serializeSignature(signature);
} catch (e) {
// Compatibility with Ledger Nano S
log.warn("Clear signing EIP-712 message failed", e);

const types = message.types;
delete types["EIP712Domain"];

const signature = await eth.signEIP712HashedMessage(
LedgerSigner.path,
TypedDataEncoder.hashDomain(message.domain),
TypedDataEncoder.hashStruct(
message.primaryType,
types,
message.message,
),
);
return this.serializeSignature(signature);
}
}
}

return this.provider.send(request.method, request.params);
};

public on = () => {};

public removeAllListeners = () => {};

private getApp = async (): Promise<{
name: string;
version: string;
flags: number | Buffer;
}> => {
const r = await this.transport!.send(0xb0, 0x01, 0x00, 0x00);
let i = 0;
const format = r[i++];

if (format !== 1) {
throw " format not supported";
}

const nameLength = r[i++];
const name = r.subarray(i, (i += nameLength)).toString("ascii");
const versionLength = r[i++];
const version = r.subarray(i, (i += versionLength)).toString("ascii");
const flagLength = r[i++];
const flags = r.subarray(i, i + flagLength);
return {
name,
version,
flags,
};
};

private openApp = async (name: string): Promise<void> => {
await this.transport!.send(
0xe0,
0xd8,
0x00,
0x00,
Buffer.from(name, "ascii"),
);
};

private serializeSignature = (signature: {
v: number | string;
r: string;
s: string;
}) =>
Signature.from({
v: signature.v,
r: BigInt(`0x${signature.r}`).toString(),
s: BigInt(`0x${signature.s}`).toString(),
}).serialized;
}

export default LedgerSigner;
Empty file added tests/mocks/LedgerMock.ts
Empty file.
6 changes: 3 additions & 3 deletions tests/pages/Refund.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe("Refund", () => {
const refundFrame = (await screen.findByTestId(
"refundFrame",
)) as HTMLDivElement;
expect(refundFrame.children.length).toEqual(5);
expect(refundFrame.children.length).toEqual(8);

const uploadInput = await screen.findByTestId("refundUpload");
const swapFile = new File(["{}"], "swap.json", {
Expand Down Expand Up @@ -85,7 +85,7 @@ describe("Refund", () => {
const refundFrame = (await screen.findByTestId(
"refundFrame",
)) as HTMLDivElement;
expect(refundFrame.children.length).toEqual(5);
expect(refundFrame.children.length).toEqual(8);

const uploadInput = await screen.findByTestId("refundUpload");
const swapFile = new File(["{}"], "swap.json", {
Expand Down Expand Up @@ -124,7 +124,7 @@ describe("Refund", () => {
const refundFrame = (await screen.findByTestId(
"refundFrame",
)) as HTMLDivElement;
expect(refundFrame.children.length).toEqual(5);
expect(refundFrame.children.length).toEqual(8);

const uploadInput = await screen.findByTestId("refundUpload");
const swapFile = new File(["{}"], "swap.json", {
Expand Down

0 comments on commit 3401d8d

Please sign in to comment.