Skip to content

Commit

Permalink
✨ (signer-btc) [DSDK-471]: SignPsbt task (#581)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdabbech-ledger authored Jan 14, 2025
2 parents 1929a32 + 1fdb8fa commit 1c2a424
Show file tree
Hide file tree
Showing 70 changed files with 3,566 additions and 1,162 deletions.
5 changes: 5 additions & 0 deletions .changeset/blue-lemons-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-management-kit": patch
---

Fix CommandUtils static calls
5 changes: 5 additions & 0 deletions .changeset/breezy-plums-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/signer-utils": patch
---

Create CommandErrorHelper to handle command errors
5 changes: 5 additions & 0 deletions .changeset/nervous-points-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-signer-kit-bitcoin": minor
---

Create SignPsbt API
5 changes: 5 additions & 0 deletions .changeset/odd-spies-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-management-kit": patch
---

Expose CommandSuccessResult
5 changes: 5 additions & 0 deletions .changeset/young-horses-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-signer-kit-bitcoin": patch
---

Use CommandErrorHelper in BTC commands
2 changes: 1 addition & 1 deletion apps/sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
"@ledgerhq/device-management-kit": "workspace:*",
"@ledgerhq/device-management-kit-flipper-plugin-client": "workspace:*",
"@ledgerhq/device-mockserver-client": "workspace:*",
"@ledgerhq/device-signer-kit-bitcoin": "workspace:*",
"@ledgerhq/device-signer-kit-ethereum": "workspace:*",
"@ledgerhq/device-signer-kit-solana": "workspace:*",
"@ledgerhq/device-signer-kit-bitcoin": "workspace:*",
"@ledgerhq/device-transport-kit-mockserver": "workspace:*",
"@ledgerhq/device-transport-kit-web-ble": "workspace:*",
"@ledgerhq/device-transport-kit-web-hid": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "@ledgerhq/device-management-kit";
import { Flex, Icons, Tag, Text, Tooltip } from "@ledgerhq/react-ui";
import styled from "styled-components";
import { inspect } from "util";

import { type FieldType } from "@/hooks/useForm";

Expand Down Expand Up @@ -91,11 +92,11 @@ export function DeviceActionResponse<
wordBreak: "break-word",
}}
>
{JSON.stringify(
isError ? props.error : props.deviceActionState,
null,
2,
)}
{isError
? inspect(props.error, { depth: null })
: props.deviceActionState.status === DeviceActionStatus.Error
? inspect(props.deviceActionState.error, { depth: null })
: JSON.stringify(props.deviceActionState, null, 2)}
</Text>
{!isError &&
props.deviceActionState.status === DeviceActionStatus.Pending ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export type DeviceActionProps<
debug?: boolean,
) => ExecuteDeviceActionReturnType<Output, Error, IntermediateValue>;
initialValues: Input;
InputValuesComponent?: React.FC<{
initialValues: Input;
onChange: (values: Input) => void;
valueSelector?: ValueSelector<FieldType>;
disabled?: boolean;
}>;
validateValues?: (args: Input) => boolean;
valueSelector?: ValueSelector<FieldType>;
deviceModelId: DeviceModelId;
Expand Down Expand Up @@ -94,6 +100,7 @@ export function DeviceActionTester<
executeDeviceAction,
valueSelector,
validateValues,
InputValuesComponent,
} = props;

const nonce = useRef(-1);
Expand Down Expand Up @@ -204,12 +211,21 @@ export function DeviceActionTester<
rowGap={3}
pointerEvents={loading ? "none" : "auto"}
>
<CommandForm
initialValues={values}
onChange={setValues}
valueSelector={valueSelector}
disabled={loading}
/>
{InputValuesComponent ? (
<InputValuesComponent
initialValues={values}
onChange={setValues}
valueSelector={valueSelector}
disabled={loading}
/>
) : (
<CommandForm
initialValues={values}
onChange={setValues}
valueSelector={valueSelector}
disabled={loading}
/>
)}
<Divider />
<Switch
checked={inspect}
Expand Down
100 changes: 100 additions & 0 deletions apps/sample/src/components/SignerBtcView/SignPsbtDAInputValusForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useCallback, useEffect } from "react";
import { DefaultDescriptorTemplate } from "@ledgerhq/device-signer-kit-bitcoin";
import { Flex, Input, SelectInput, Text } from "@ledgerhq/react-ui";

import { useForm } from "@/hooks/useForm";

type SignPsbtInputValuesType = {
psbt: string;
path: string;
descriptorTemplate: DefaultDescriptorTemplate;
};

const descriptorTemplateToDerivationPath: Record<
DefaultDescriptorTemplate,
string
> = {
[DefaultDescriptorTemplate.TAPROOT]: "86'/0'/0'",
[DefaultDescriptorTemplate.NATIVE_SEGWIT]: "84'/0'/0'",
[DefaultDescriptorTemplate.NESTED_SEGWIT]: "49'/0'/0'",
[DefaultDescriptorTemplate.LEGACY]: "44'/0'/0'",
};

const descriptorTemplateToLabel = {
[DefaultDescriptorTemplate.TAPROOT]: "Taproot",
[DefaultDescriptorTemplate.NATIVE_SEGWIT]: "Native Segwit",
[DefaultDescriptorTemplate.NESTED_SEGWIT]: "Nested Segwit",
[DefaultDescriptorTemplate.LEGACY]: "Legacy",
};

export const SignPsbtDAInputValuesForm: React.FC<{
initialValues: SignPsbtInputValuesType;
onChange: (values: SignPsbtInputValuesType) => void;
disabled?: boolean;
}> = ({ initialValues, onChange, disabled }) => {
const { formValues, setFormValues, setFormValue } = useForm(initialValues);

useEffect(() => {
onChange(formValues);
}, [formValues, onChange]);

const onWalletDescriptorTemplateChange = useCallback(
(value: DefaultDescriptorTemplate) => {
const newValues = {
path: descriptorTemplateToDerivationPath[value],
descriptorTemplate: value,
};
setFormValues((prev) => ({ ...prev, ...newValues }));
},
[setFormValues],
);

return (
<Flex
flexDirection="column"
flex={1}
rowGap={6}
columnGap={6}
flexWrap="wrap"
>
<Flex flexDirection="row" alignItems="center" mb={4}>
<Text style={{ marginRight: 8 }}>Wallet address type</Text>
<SelectInput
options={Object.entries(DefaultDescriptorTemplate).map(
([_key, value]) => ({
label: descriptorTemplateToLabel[value],
value,
}),
)}
value={{
label: descriptorTemplateToLabel[formValues.descriptorTemplate],
value: formValues.descriptorTemplate,
}}
isMulti={false}
isSearchable={false}
onChange={(newVal) =>
newVal && onWalletDescriptorTemplateChange(newVal.value)
}
/>
</Flex>

<Input
id="path"
value={formValues.path}
placeholder="path"
onChange={(newVal) => setFormValue("path", newVal)}
disabled={disabled}
data-testid="input-text_path"
/>

<Input
id="psbt"
value={formValues.psbt}
placeholder="psbt"
onChange={(newVal) => setFormValue("psbt", newVal)}
disabled={disabled}
data-testid="input-text_psbt"
/>
</Flex>
);
};
37 changes: 37 additions & 0 deletions apps/sample/src/components/SignerBtcView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import React, { useMemo } from "react";
import {
DefaultDescriptorTemplate,
DefaultWallet,
type GetExtendedDAIntermediateValue,
type GetExtendedPublicKeyDAError,
type GetExtendedPublicKeyDAOutput,
SignerBtcBuilder,
type SignMessageDAError,
type SignMessageDAIntermediateValue,
type SignMessageDAOutput,
type SignPsbtDAError,
type SignPsbtDAIntermediateValue,
type SignPsbtDAOutput,
} from "@ledgerhq/device-signer-kit-bitcoin";

import { DeviceActionsList } from "@/components/DeviceActionsView/DeviceActionsList";
import { type DeviceActionProps } from "@/components/DeviceActionsView/DeviceActionTester";
import { SignPsbtDAInputValuesForm } from "@/components/SignerBtcView/SignPsbtDAInputValusForm";
import { useDmk } from "@/providers/DeviceManagementKitProvider";

const DEFAULT_DERIVATION_PATH = "84'/0'/0'";
Expand Down Expand Up @@ -78,6 +84,37 @@ export const SignerBtcView: React.FC<{ sessionId: string }> = ({
SignMessageDAError,
SignMessageDAIntermediateValue
>,
{
title: "Sign psbt",
description:
"Perform all the actions necessary to sign a PSBT with the device",
executeDeviceAction: ({ descriptorTemplate, psbt, path }) => {
if (!signer) {
throw new Error("Signer not initialized");
}

return signer.signPsbt(
new DefaultWallet(path, descriptorTemplate),
psbt,
);
},
InputValuesComponent: SignPsbtDAInputValuesForm,
initialValues: {
descriptorTemplate: DefaultDescriptorTemplate.NATIVE_SEGWIT,
psbt: "",
path: DEFAULT_DERIVATION_PATH,
},
deviceModelId,
} satisfies DeviceActionProps<
SignPsbtDAOutput,
{
psbt: string;
path: string;
descriptorTemplate: DefaultDescriptorTemplate;
},
SignPsbtDAError,
SignPsbtDAIntermediateValue
>,
],
[deviceModelId, signer],
);
Expand Down
1 change: 1 addition & 0 deletions apps/sample/src/hooks/useForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export function useForm<T extends Record<string, string | boolean | number>>(
return {
formValues,
setFormValue,
setFormValues,
};
}
2 changes: 1 addition & 1 deletion apps/sample/src/styles/globalstyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const GlobalStyle = createGlobalStyle`
background-color: #000000;
}
body {
user-select: none;
user-select: text;
}
.no-scrollbar {
&::-webkit-scrollbar {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export enum CommandResultStatus {
Error = "ERROR",
Success = "SUCCESS",
}
type CommandSuccessResult<Data> = {
export type CommandSuccessResult<Data> = {
status: CommandResultStatus.Success;
data: Data;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ export class CommandUtils {
}

static isSuccessResponse({ statusCode }: ApduResponse) {
if (!this.isValidStatusCode(statusCode)) {
if (!CommandUtils.isValidStatusCode(statusCode)) {
return false;
}

return statusCode[0] === 0x90 && statusCode[1] === 0x00;
}

static isLockedDeviceResponse({ statusCode }: ApduResponse) {
if (!this.isValidStatusCode(statusCode)) {
if (!CommandUtils.isValidStatusCode(statusCode)) {
return false;
}

Expand Down
1 change: 1 addition & 0 deletions packages/device-management-kit/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type { Command } from "@api/command/Command";
export type {
CommandErrorResult,
CommandResult,
CommandSuccessResult,
} from "@api/command/model/CommandResult";
export type { SendCommandUseCaseArgs } from "@api/command/use-case/SendCommandUseCase";
export type { DeviceModelId } from "@api/device/DeviceModel";
Expand Down
17 changes: 7 additions & 10 deletions packages/signer/signer-btc/src/api/SignerBtc.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
// import { type AddressOptions } from "@api/model/AddressOptions";
// import { type Psbt } from "@api/model/Psbt";
// import { type Signature } from "@api/model/Signature";
// import { type Wallet } from "@api/model/Wallet";
import { type GetExtendedPublicKeyDAReturnType } from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes";
import { type SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionTypes";
import { type SignPsbtDAReturnType } from "@api/app-binder/SignPsbtDeviceActionTypes";
import { type AddressOptions } from "@api/model/AddressOptions";
import {
type GetExtendedPublicKeyReturnType,
type SignMessageDAReturnType,
} from "@root/src";
import { type Psbt } from "@api/model/Psbt";
import { type Wallet } from "@api/model/Wallet";

export interface SignerBtc {
getExtendedPublicKey: (
derivationPath: string,
options: AddressOptions,
) => GetExtendedPublicKeyReturnType;
) => GetExtendedPublicKeyDAReturnType;
signMessage: (
derivationPath: string,
message: string,
) => SignMessageDAReturnType;
signPsbt: (wallet: Wallet, psbt: Psbt) => SignPsbtDAReturnType;
// getAddress: (wallet: Wallet, options?: AddressOptions) => Promise<string>;
// signPsbt: (wallet: Wallet, psbt: Psbt) => Promise<Psbt>;
// signTransaction: (wallet: Wallet, psbt: Psbt) => Promise<Uint8Array>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type GetExtendedPublicKeyCommandArgs,
type GetExtendedPublicKeyCommandResponse,
} from "@internal/app-binder/command/GetExtendedPublicKeyCommand";
import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors";

type GetExtendedPublicKeyDARequiredInteraction =
| UserInteractionRequired.None
Expand All @@ -18,14 +19,15 @@ type GetExtendedPublicKeyDARequiredInteraction =
export type GetExtendedPublicKeyDAOutput =
SendCommandInAppDAOutput<GetExtendedPublicKeyCommandResponse>;

export type GetExtendedPublicKeyDAError = SendCommandInAppDAError;
export type GetExtendedPublicKeyDAError =
SendCommandInAppDAError<BtcErrorCodes>;

export type GetExtendedDAIntermediateValue =
SendCommandInAppDAIntermediateValue<GetExtendedPublicKeyDARequiredInteraction>;

export type GetExtendedPublicKeyDAInput = GetExtendedPublicKeyCommandArgs;

export type GetExtendedPublicKeyReturnType = ExecuteDeviceActionReturnType<
export type GetExtendedPublicKeyDAReturnType = ExecuteDeviceActionReturnType<
GetExtendedPublicKeyDAOutput,
GetExtendedPublicKeyDAError,
GetExtendedDAIntermediateValue
Expand Down
Loading

0 comments on commit 1c2a424

Please sign in to comment.