Skip to content

Commit

Permalink
✨ (signer-btc) [DSDK-472]: Implement update PSBT and extract transact…
Browse files Browse the repository at this point in the history
…ion (#607)
  • Loading branch information
jdabbech-ledger authored Jan 17, 2025
2 parents 1efd75b + eae62c9 commit 40a001e
Show file tree
Hide file tree
Showing 30 changed files with 2,034 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/selfish-maps-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-signer-kit-bitcoin": minor
---

Sign transaction API
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type SignPsbtInputValuesType = {
descriptorTemplate: DefaultDescriptorTemplate;
};

const descriptorTemplateToDerivationPath: Record<
export const descriptorTemplateToDerivationPath: Record<
DefaultDescriptorTemplate,
string
> = {
Expand Down
41 changes: 40 additions & 1 deletion apps/sample/src/components/SignerBtcView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@ import {
type SignPsbtDAError,
type SignPsbtDAIntermediateValue,
type SignPsbtDAOutput,
type SignTransactionDAError,
type SignTransactionDAIntermediateValue,
type SignTransactionDAOutput,
} 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 {
descriptorTemplateToDerivationPath,
SignPsbtDAInputValuesForm,
} from "@/components/SignerBtcView/SignPsbtDAInputValusForm";
import { useDmk } from "@/providers/DeviceManagementKitProvider";

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

return signer.signTransaction(
new DefaultWallet(path, descriptorTemplate),
psbt,
);
},
InputValuesComponent: SignPsbtDAInputValuesForm,
initialValues: {
descriptorTemplate: DefaultDescriptorTemplate.NATIVE_SEGWIT,
psbt: "",
path: descriptorTemplateToDerivationPath[
DefaultDescriptorTemplate.NATIVE_SEGWIT
],
},
deviceModelId,
} satisfies DeviceActionProps<
SignTransactionDAOutput,
{
psbt: string;
path: string;
descriptorTemplate: DefaultDescriptorTemplate;
},
SignTransactionDAError,
SignTransactionDAIntermediateValue
>,
],
[deviceModelId, signer],
);
Expand Down
2 changes: 1 addition & 1 deletion packages/signer/signer-btc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"license": "Apache-2.0",
"main": "lib/cjs/index.js",
"types": "lib/cjs/index.d.ts",
"private": true,
"private": false,
"exports": {
".": {
"types": "./lib/types/index.d.ts",
Expand Down
3 changes: 2 additions & 1 deletion packages/signer/signer-btc/src/api/SignerBtc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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 SignTransactionDAReturnType } from "@api/app-binder/SignTransactionDeviceActionTypes";
import { type AddressOptions } from "@api/model/AddressOptions";
import { type Psbt } from "@api/model/Psbt";
import { type Wallet } from "@api/model/Wallet";
Expand All @@ -15,6 +16,6 @@ export interface SignerBtc {
message: string,
) => SignMessageDAReturnType;
signPsbt: (wallet: Wallet, psbt: Psbt) => SignPsbtDAReturnType;
signTransaction: (wallet: Wallet, psbt: Psbt) => SignTransactionDAReturnType;
// getAddress: (wallet: Wallet, options?: AddressOptions) => Promise<string>;
// signTransaction: (wallet: Wallet, psbt: Psbt) => Promise<Uint8Array>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export type SignPsbtDAError =
| OpenAppDAError
| CommandErrorResult<BtcErrorCodes>["error"];

type SignPsbtDARequiredInteraction =
export type SignPsbtDARequiredInteraction =
| OpenAppDARequiredInteraction
| UserInteractionRequired.SignTransaction;

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

import { type SignPsbtDARequiredInteraction } from "@api/app-binder/SignPsbtDeviceActionTypes";
import { type Psbt as ApiPsbt } from "@api/model/Psbt";
import { type PsbtSignature } from "@api/model/Signature";
import { type Wallet as ApiWallet } from "@api/model/Wallet";
import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors";
import { type DataStoreService } from "@internal/data-store/service/DataStoreService";
import { type Psbt as InternalPsbt } from "@internal/psbt/model/Psbt";
import { type PsbtMapper } from "@internal/psbt/service/psbt/PsbtMapper";
import { type ValueParser } from "@internal/psbt/service/value/ValueParser";
import { type WalletBuilder } from "@internal/wallet/service/WalletBuilder";
import { type WalletSerializer } from "@internal/wallet/service/WalletSerializer";

export type SignTransactionDAOutput = HexaString;

export type SignTransactionDAInput = {
psbt: ApiPsbt;
wallet: ApiWallet;
walletBuilder: WalletBuilder;
walletSerializer: WalletSerializer;
dataStoreService: DataStoreService;
psbtMapper: PsbtMapper;
valueParser: ValueParser;
};

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

type SignTransactionDARequiredInteraction =
| OpenAppDARequiredInteraction
| SignPsbtDARequiredInteraction;

export type SignTransactionDAIntermediateValue = {
requiredUserInteraction: SignTransactionDARequiredInteraction;
};

export type SignTransactionDAState = DeviceActionState<
SignTransactionDAOutput,
SignTransactionDAError,
SignTransactionDAIntermediateValue
>;

export type SignTransactionDAInternalState = {
readonly error: SignTransactionDAError | null;
readonly signatures: PsbtSignature[] | null;
readonly signedPsbt: InternalPsbt | null;
readonly transaction: HexaString | null;
};

export type SignTransactionDAReturnType = ExecuteDeviceActionReturnType<
SignTransactionDAOutput,
SignTransactionDAError,
SignTransactionDAIntermediateValue
>;
1 change: 1 addition & 0 deletions packages/signer/signer-btc/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type {
SignMessageDAState,
} from "@api/app-binder/SignMessageDeviceActionTypes";
export * from "@api/app-binder/SignPsbtDeviceActionTypes";
export * from "@api/app-binder/SignTransactionDeviceActionTypes";
export { DefaultDescriptorTemplate, DefaultWallet } from "@api/model/Wallet";
export * from "@api/SignerBtc";
export * from "@api/SignerBtcBuilder";
29 changes: 29 additions & 0 deletions packages/signer/signer-btc/src/internal/DefaultSignerBtc.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { type DeviceManagementKit } from "@ledgerhq/device-management-kit";

import { DefaultDescriptorTemplate, DefaultWallet } from "@api/model/Wallet";
import { DefaultSignerBtc } from "@internal/DefaultSignerBtc";
import { GetExtendedPublicKeyUseCase } from "@internal/use-cases/get-extended-public-key/GetExtendedPublicKeyUseCase";
import { SignPsbtUseCase } from "@internal/use-cases/sign-psbt/SignPsbtUseCase";
import { SignTransactionUseCase } from "@internal/use-cases/sign-transaction/SignTransactionUseCase";

import { SignMessageUseCase } from "./use-cases/sign-message/SignMessageUseCase";

Expand Down Expand Up @@ -39,4 +42,30 @@ describe("DefaultSignerBtc", () => {
signer.signMessage(derivationPath, message);
expect(SignMessageUseCase.prototype.execute).toHaveBeenCalled();
});
it("should call signPsbtUseCase", () => {
jest.spyOn(SignPsbtUseCase.prototype, "execute");
const sessionId = "session-id";
const dmk = {
executeDeviceAction: jest.fn(),
} as unknown as DeviceManagementKit;
const signer = new DefaultSignerBtc({ dmk, sessionId });
signer.signPsbt(
new DefaultWallet("44'/0'/0'", DefaultDescriptorTemplate.NATIVE_SEGWIT),
"",
);
expect(SignPsbtUseCase.prototype.execute).toHaveBeenCalled();
});
it("should call signTransactionUseCase", () => {
jest.spyOn(SignTransactionUseCase.prototype, "execute");
const sessionId = "session-id";
const dmk = {
executeDeviceAction: jest.fn(),
} as unknown as DeviceManagementKit;
const signer = new DefaultSignerBtc({ dmk, sessionId });
signer.signTransaction(
new DefaultWallet("44'/0'/0'", DefaultDescriptorTemplate.NATIVE_SEGWIT),
"",
);
expect(SignTransactionUseCase.prototype.execute).toHaveBeenCalled();
});
});
7 changes: 7 additions & 0 deletions packages/signer/signer-btc/src/internal/DefaultSignerBtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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 SignTransactionUseCase } from "@internal/use-cases/sign-transaction/SignTransactionUseCase";

import { type SignMessageUseCase } from "./use-cases/sign-message/SignMessageUseCase";
import { makeContainer } from "./di";
Expand Down Expand Up @@ -53,4 +54,10 @@ export class DefaultSignerBtc implements SignerBtc {
.get<SignMessageUseCase>(useCasesTypes.SignMessageUseCase)
.execute(_derivationPath, _message);
}

signTransaction(wallet: Wallet, psbt: Psbt) {
return this._container
.get<SignTransactionUseCase>(useCasesTypes.SignTransactionUseCase)
.execute(wallet, psbt);
}
}
22 changes: 22 additions & 0 deletions packages/signer/signer-btc/src/internal/app-binder/BtcAppBinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import {
} from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes";
import { SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionTypes";
import { SignPsbtDAReturnType } from "@api/app-binder/SignPsbtDeviceActionTypes";
import { SignTransactionDAReturnType } from "@api/app-binder/SignTransactionDeviceActionTypes";
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 { SignTransactionDeviceAction } from "@internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction";
import { dataStoreTypes } from "@internal/data-store/di/dataStoreTypes";
import type { DataStoreService } from "@internal/data-store/service/DataStoreService";
import { externalTypes } from "@internal/externalTypes";
Expand Down Expand Up @@ -96,4 +98,24 @@ export class BtcAppBinder {
}),
});
}

signTransaction(args: {
psbt: Psbt;
wallet: Wallet;
}): SignTransactionDAReturnType {
return this._dmk.executeDeviceAction({
sessionId: this._sessionId,
deviceAction: new SignTransactionDeviceAction({
input: {
psbt: args.psbt,
wallet: args.wallet,
walletBuilder: this._walletBuilder,
walletSerializer: this._walletSerializer,
dataStoreService: this._dataStoreService,
psbtMapper: this._psbtMapper,
valueParser: this._valueParser,
},
}),
});
}
}
Loading

0 comments on commit 40a001e

Please sign in to comment.