Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Sign Tx] Add a sign with hardware wallets (trezor, ledger) #859

Merged
merged 18 commits into from
Jun 11, 2024
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
FROM node:20-alpine
FROM node:20

ENV NEXT_TELEMETRY_DISABLED 1
ENV PORT 80
WORKDIR /app
COPY . .

RUN yarn install
RUN yarn build

# Run on port 80 for compatibility with laboratory v1
EXPOSE 80
CMD ["npm", "start"]
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@
},
"dependencies": {
"@creit.tech/stellar-wallets-kit": "^0.8.2",
"@ledgerhq/hw-app-str": "^6.28.6",
"@ledgerhq/hw-transport-webusb": "^6.28.6",
"@stellar/design-system": "^2.0.0-beta.13",
"@stellar/stellar-sdk": "^11.3.0",
"@stellar/stellar-xdr-json-web": "^0.0.1",
"@tanstack/react-query": "^5.32.1",
"@tanstack/react-query-devtools": "^5.32.1",
"@trezor/connect-plugin-stellar": "^9.0.3",
"@trezor/connect-web": "^9.2.2",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"bignumber.js": "^9.1.2",
"dompurify": "^3.1.2",
Expand Down
184 changes: 155 additions & 29 deletions src/app/(sidebar)/transaction/sign/components/Overview.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
"use client";

import { useEffect, useState } from "react";
import { Card, Icon, Text, Button } from "@stellar/design-system";
import { FeeBumpTransaction, TransactionBuilder } from "@stellar/stellar-sdk";
import { Card, Icon, Text, Button, Select } from "@stellar/design-system";
import {
FeeBumpTransaction,
Transaction,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";

import { FEE_BUMP_TX_FIELDS, TX_FIELDS } from "@/constants/signTransactionPage";

import { useStore } from "@/store/useStore";

import { transactionSigner } from "@/helpers/transactionSigner";
import { txSigner } from "@/helpers/txSigner";

import { validate } from "@/validate";

Expand All @@ -27,25 +32,37 @@ export const Overview = () => {
const { network, transaction } = useStore();
const {
sign,
updateHardWalletSigs,
updateSignActiveView,
updateSignImportTx,
updateSignedTx,
updateBipPath,
resetSign,
resetSignHardWalletSigs,
} = transaction;

const [secretInputs, setSecretInputs] = useState<string[]>([""]);

// Adding hardware wallets sig (signatures) related
const [bipPathErrorMsg, setBipPathErrorMsg] = useState<string>("");
const [hardwareSigSuccess, setHardwareSigSuccess] = useState<boolean>(false);
const [hardwareSigErrorMsg, setHardwareSigErrorMsg] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);

const [selectedHardware, setSelectedHardware] = useState<string>("");

// Sign tx status related
const [signedTxSuccessMsg, setSignedTxSuccessMsg] = useState<string>("");
const [signedTxErrorMsg, setSignedTxErrorMsg] = useState<string>("");
const [signError, setSignError] = useState<string>("");

// @TODO bip path
const [bipPath, setBipPath] = useState<string>("");
const [bipPathErrorMsg, setBipPathErrorMsg] = useState<string>("");

const HAS_SECRET_KEYS = secretInputs.some((input) => input !== "");
const HAS_INVALID_SECRET_KEYS = secretInputs.some((input) =>
validate.secretKey(input),
);
const HAS_INVALID_SECRET_KEYS = secretInputs.some((input) => {
if (input.length) {
return validate.secretKey(input);
}
return false;
});

useEffect(() => {
if (!sign.importTx) {
Expand Down Expand Up @@ -78,15 +95,17 @@ export const Overview = () => {
setSecretInputs([...secretInputs, ""]);
};

const signTxWithSecretKeys = (
const signTransaction = (
txXdr: string,
signers: string[],
networkPassphrase: string,
hardWalletSigs: xdr.DecoratedSignature[],
) => {
const { xdr, message } = transactionSigner.secretKeys({
const { xdr, message } = txSigner.signTx({
txXdr,
signers,
networkPassphrase,
hardWalletSigs: hardWalletSigs || [],
});

if (xdr && message) {
Expand All @@ -97,6 +116,64 @@ export const Overview = () => {
}
};

const signWithHardware = async () => {
setHardwareSigSuccess(false);
updateSignedTx("");

setIsLoading(true);

let hardwareSign;
let hardwareSignError;

try {
if (selectedHardware === "ledger") {
const { signature, error } = await txSigner.signWithLedger({
bipPath: sign.bipPath,
transaction: sign.importTx as FeeBumpTransaction | Transaction,
isHash: false,
});

hardwareSign = signature;
hardwareSignError = error;
}

if (selectedHardware === "ledger_hash") {
const { signature, error } = await txSigner.signWithLedger({
bipPath: sign.bipPath,
transaction: sign.importTx as FeeBumpTransaction | Transaction,
isHash: true,
});

hardwareSign = signature;
hardwareSignError = error;
}

if (selectedHardware === "trezor") {
const path = `m/${sign.bipPath}`;

const { signature, error } = await txSigner.signWithTrezor({
bipPath: path,
transaction: sign.importTx as Transaction,
});

hardwareSign = signature;
hardwareSignError = error;
}

setIsLoading(false);

if (hardwareSign) {
updateHardWalletSigs(hardwareSign);
setHardwareSigSuccess(true);
} else if (hardwareSignError) {
setHardwareSigErrorMsg(hardwareSignError);
}
} catch (err) {
setIsLoading(false);
setHardwareSigErrorMsg(`An unexpected error occurred: ${err}`);
}
};

const REQUIRED_FIELDS = [
{
label: "Signing for",
Expand All @@ -120,6 +197,12 @@ export const Overview = () => {
mergedFields = [...REQUIRED_FIELDS, ...TX_FIELDS(sign.importTx)];
}

const resetHardwareSign = () => {
resetSignHardWalletSigs();
setHardwareSigSuccess(false);
setHardwareSigErrorMsg("");
};

return (
<>
<Box gap="md">
Expand Down Expand Up @@ -202,56 +285,99 @@ export const Overview = () => {
placeholder="Secret key (starting with S) or hash preimage (in hex)"
/>
</div>

<div className="full-width">
<div>
<Button
size="md"
variant="tertiary"
onClick={() => addSignature()}
>
Add signature
</Button>
</div>
<div className="Input__buttons full-width">
<TextPicker
id="bip-path"
label="BIP Path"
placeholder="BIP path in format: 44'/148'/0'"
onChange={(e) => {
setBipPath(e.target.value);
updateBipPath(e.target.value);

const error = validate.bipPath(e.target.value);

if (error) {
setBipPathErrorMsg(error);
} else {
setBipPathErrorMsg("");
}
}}
error={bipPathErrorMsg}
value={bipPath}
error={bipPathErrorMsg || hardwareSigErrorMsg}
value={sign.bipPath}
success={
hardwareSigSuccess
? "Successfully added a hardware wallet signature"
: ""
}
note="Note: Trezor devices require upper time bounds to be set (non-zero), otherwise the signature will not be verified"
rightElement={
<>
<div className="hardware-button">
<Select
fieldSize="md"
id="hardware-wallet-select"
onChange={(
event: React.ChangeEvent<HTMLSelectElement>,
) => {
resetHardwareSign();
setSelectedHardware(event.target.value);
}}
>
<option value="">Select operation type</option>
<option value="ledger">Ledger</option>
<option value="ledger_hash">Hash with Ledger</option>
<option value="trezor">Trezor</option>
</Select>
</div>

<div className="hardware-button">
<Button
disabled={!selectedHardware || !sign.bipPath}
isLoading={isLoading}
onClick={signWithHardware}
size="md"
variant="tertiary"
>
Sign
</Button>
</div>
</>
}
/>
</div>

<Box gap="xs" addlClassName="full-width">
<div className="SignTx__Buttons">
<div>
<Button
disabled={!HAS_SECRET_KEYS || HAS_INVALID_SECRET_KEYS}
disabled={
(!HAS_SECRET_KEYS || HAS_INVALID_SECRET_KEYS) &&
!sign.hardWalletSigs?.length
}
size="md"
variant="secondary"
onClick={() =>
signTxWithSecretKeys(
signTransaction(
sign.importXdr,
secretInputs,
network.passphrase,
sign.hardWalletSigs,
)
}
>
Sign with secret key
Sign transaction
</Button>

<SignWithWallet setSignError={setSignError} />
</div>
<div>
<Button
size="md"
variant="tertiary"
onClick={() => addSignature()}
>
Add signature
</Button>
</div>
</div>
<div>
{signError ? (
Expand Down
65 changes: 0 additions & 65 deletions src/helpers/transactionSigner.ts

This file was deleted.

Loading
Loading