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

✨ (signer-btc) [DSDK-471]: SignPsbt task #581

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
Open
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
36 changes: 36 additions & 0 deletions apps/sample/src/components/SignerBtcView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
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";
Expand Down Expand Up @@ -78,6 +83,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: ({ derivationPath, psbt }) => {
if (!signer) {
throw new Error("Signer not initialized");
}

return signer.signPsbt(
new DefaultWallet(
derivationPath,
DefaultDescriptorTemplate.NATIVE_SEGWIT,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally we should be able to select the template we want to test. BOr another possibility would be to deduce it from the derivation path since we only support standard wallets for now, and derivation types also follow the standard as described here: https://learnmeabitcoin.com/technical/keys/hd-wallets/derivation-paths/

  • m/44'/... -> legacy
  • m/49'/... -> nested segwit
  • m/84'/... -> native segwit
  • m/86'/... -> taproot

(also note that if derivation path and descriptor template are not consistent, it will be rejected by the device anyway)

Copy link
Contributor Author

@jdabbech-ledger jdabbech-ledger Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok I'll update the device action command form with a render prop to allow custom inputs

),
psbt,
);
},
initialValues: {
derivationPath: DEFAULT_DERIVATION_PATH,
psbt: "70736274ff0104010101fb0402000000010204020000000105010100011004000000000103040100000001007102000000013daeeb9a92e7b5af90c787d53f0e60d2cf4cfd47bca9a0d8bc77a7464b024c0b00000000000000000002ff0300000000000016001402fe597c6ec0e2982712929bcf079a4e11d37e8d950b0000000000001600144dc432cb6a26c52a1e6ddd2bcf0ee49199fae0cc000000002206031869567d5e88d988ff7baf6827983f89530ddd79dbaeadaa6ec538a8f03dea8b18f5acc2fd540000800000008000000080000000000000000001011fff0300000000000016001402fe597c6ec0e2982712929bcf079a4e11d37e8d010e200cf08d04fa11ff024d5a50165ba65e495409b50ba6657788dfa15274adb682df010f0400000000000103086b01000000000000010416001429159115f12bb6a7e977439c83d3f8d555d72d5f00",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[COULD] Remove the default psbt here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, it's a heartache after so much difficulties to create it 😿

},
deviceModelId,
} satisfies DeviceActionProps<
SignPsbtDAOutput,
{
psbt: string;
derivationPath: string;
},
SignPsbtDAError,
SignPsbtDAIntermediateValue
>,
],
[deviceModelId, signer],
);
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@ledgerhq/device-management-kit";

import { type Signature } from "@api/model/Signature";
import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors";

export type SignMessageDAOutput = Signature;

Expand All @@ -16,7 +17,9 @@ export type SignMessageDAInput = {
readonly message: string;
};

export type SignMessageDAError = OpenAppDAError | CommandErrorResult["error"];
export type SignMessageDAError =
| OpenAppDAError
| CommandErrorResult<BtcErrorCodes>["error"];

type SignMessageDARequiredInteraction =
| OpenAppDARequiredInteraction
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
type CommandErrorResult,
type DeviceActionState,
type ExecuteDeviceActionReturnType,
type OpenAppDAError,
type OpenAppDARequiredInteraction,
} from "@ledgerhq/device-management-kit";

import { type Psbt } from "@api/model/Psbt";
import { type Wallet } from "@api/model/Wallet";
import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors";

// @toDo Update this return value to Psbt once it would be updated in SignPsbtTask
export type SignPsbtDAOutput = Uint8Array[];

export type SignPsbtDAInput = {
psbt: Psbt;
wallet: Wallet;
};

export type SignPsbtDAError =
| OpenAppDAError
| CommandErrorResult<BtcErrorCodes>["error"];

type SignPsbtDARequiredInteraction = OpenAppDARequiredInteraction;

export type SignPsbtDAIntermediateValue = {
requiredUserInteraction: SignPsbtDARequiredInteraction;
};

export type SignPsbtDAState = DeviceActionState<
SignPsbtDAOutput,
SignPsbtDAError,
SignPsbtDAIntermediateValue
>;

export type SignPsbtDAInternalState = {
readonly error: SignPsbtDAError | null;
// [SHOULD] be psbt instead of signature
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ASK] What to do with this comment ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next task I'm working on, UpdatePsbtTask, will modify this device action to return a psbt instead of a Signature

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually as discussed, we won't return a PSBT but the list of signature, with according index/tag/pubkey_augm (typing to be refined)

readonly signature: Uint8Array[] | null;
};

export type SignPsbtDAReturnType = ExecuteDeviceActionReturnType<
SignPsbtDAOutput,
SignPsbtDAError,
SignPsbtDAIntermediateValue
>;
5 changes: 3 additions & 2 deletions packages/signer/signer-btc/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export type {
SignMessageDAIntermediateValue,
SignMessageDAOutput,
SignMessageDAState,
} from "@api/app-binder/SignMessageDeviceActionType";
export * from "@api/app-binder/SignMessageDeviceActionType";
} from "@api/app-binder/SignMessageDeviceActionTypes";
export * from "@api/app-binder/SignPsbtDeviceActionTypes";
export { DefaultDescriptorTemplate, DefaultWallet } from "@api/model/Wallet";
export * from "@api/SignerBtc";
export * from "@api/SignerBtcBuilder";
11 changes: 10 additions & 1 deletion packages/signer/signer-btc/src/internal/DefaultSignerBtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import {
} from "@ledgerhq/device-management-kit";
import { type Container } from "inversify";

import { type SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionType";
import { type SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionTypes";
import { type AddressOptions } from "@api/model/AddressOptions";
import { type Psbt } from "@api/model/Psbt";
import { type Wallet } from "@api/model/Wallet";
import { type SignerBtc } from "@api/SignerBtc";
import { useCasesTypes } from "@internal/use-cases/di/useCasesTypes";
import { type GetExtendedPublicKeyUseCase } from "@internal/use-cases/get-extended-public-key/GetExtendedPublicKeyUseCase";
import { type SignPsbtUseCase } from "@internal/use-cases/sign-psbt/SignPsbtUseCase";

import { type SignMessageUseCase } from "./use-cases/sign-message/SignMessageUseCase";
import { makeContainer } from "./di";
Expand All @@ -25,6 +28,12 @@ export class DefaultSignerBtc implements SignerBtc {
this._container = makeContainer({ dmk, sessionId });
}

signPsbt(wallet: Wallet, psbt: Psbt) {
return this._container
.get<SignPsbtUseCase>(useCasesTypes.SignPsbtUseCase)
.execute(wallet, psbt);
}

getExtendedPublicKey(
derivationPath: string,
{ checkOnDevice = false }: AddressOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ import { inject, injectable } from "inversify";

import {
GetExtendedPublicKeyDAInput,
GetExtendedPublicKeyReturnType,
GetExtendedPublicKeyDAReturnType,
} from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes";
import { SignMessageDAReturnType } from "@api/index";
import { SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionTypes";
import { SignPsbtDAReturnType } from "@api/app-binder/SignPsbtDeviceActionTypes";
import { Psbt } from "@api/model/Psbt";
import { Wallet } from "@api/model/Wallet";
import { GetExtendedPublicKeyCommand } from "@internal/app-binder/command/GetExtendedPublicKeyCommand";
import { SignPsbtDeviceAction } from "@internal/app-binder/device-action/SignPsbt/SignPsbtDeviceAction";
import { externalTypes } from "@internal/externalTypes";

import { SignMessageDeviceAction } from "./device-action/SignMessage/SignMessageDeviceAction";
Expand All @@ -25,7 +29,7 @@ export class BtcAppBinder {

getExtendedPublicKey(
args: GetExtendedPublicKeyDAInput,
): GetExtendedPublicKeyReturnType {
): GetExtendedPublicKeyDAReturnType {
return this.dmk.executeDeviceAction({
sessionId: this.sessionId,
deviceAction: new SendCommandInAppDeviceAction({
Expand Down Expand Up @@ -54,4 +58,16 @@ export class BtcAppBinder {
}),
});
}

signPsbt(args: { psbt: Psbt; wallet: Wallet }): SignPsbtDAReturnType {
return this.dmk.executeDeviceAction({
sessionId: this.sessionId,
deviceAction: new SignPsbtDeviceAction({
input: {
psbt: args.psbt,
wallet: args.wallet,
},
}),
});
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import {
ApduResponse,
CommandResultFactory,
GlobalCommandErrorHandler,
} from "@ledgerhq/device-management-kit";

import { SW_INTERRUPTED_EXECUTION } from "@internal/app-binder/command/utils/constants";
import { CommandUtils as BtcCommandUtils } from "@internal/utils/CommandUtils";

import { ContinueCommand } from "./ContinueCommand";

Expand All @@ -26,21 +24,10 @@ describe("ContinueCommand", (): void => {
0xef, // Payload data
]);

const parser = (response: ApduResponse) => {
if (BtcCommandUtils.isContinueResponse(response)) {
return CommandResultFactory({
data: response,
});
}
return CommandResultFactory({
error: GlobalCommandErrorHandler.handle(response),
});
};

describe("getApdu", () => {
it("should return correct APDU for given payload", () => {
// given
const command = new ContinueCommand(defaultArgs, parser);
const command = new ContinueCommand(defaultArgs);
// when
const apdu = command.getApdu();
// then
Expand All @@ -51,7 +38,7 @@ describe("ContinueCommand", (): void => {
describe("parseResponse", () => {
it("should return the APDU response if it's a continue response", () => {
// given
const command = new ContinueCommand(defaultArgs, parser);
const command = new ContinueCommand(defaultArgs);
const continueResponseData = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
const apduResponse = new ApduResponse({
statusCode: SW_INTERRUPTED_EXECUTION,
Expand Down
Loading