Skip to content

Commit 40a001e

Browse files
✨ (signer-btc) [DSDK-472]: Implement update PSBT and extract transaction (#607)
2 parents 1efd75b + eae62c9 commit 40a001e

30 files changed

+2034
-9
lines changed

.changeset/selfish-maps-rhyme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ledgerhq/device-signer-kit-bitcoin": minor
3+
---
4+
5+
Sign transaction API

apps/sample/src/components/SignerBtcView/SignPsbtDAInputValusForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type SignPsbtInputValuesType = {
1010
descriptorTemplate: DefaultDescriptorTemplate;
1111
};
1212

13-
const descriptorTemplateToDerivationPath: Record<
13+
export const descriptorTemplateToDerivationPath: Record<
1414
DefaultDescriptorTemplate,
1515
string
1616
> = {

apps/sample/src/components/SignerBtcView/index.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ import {
1212
type SignPsbtDAError,
1313
type SignPsbtDAIntermediateValue,
1414
type SignPsbtDAOutput,
15+
type SignTransactionDAError,
16+
type SignTransactionDAIntermediateValue,
17+
type SignTransactionDAOutput,
1518
} from "@ledgerhq/device-signer-kit-bitcoin";
1619

1720
import { DeviceActionsList } from "@/components/DeviceActionsView/DeviceActionsList";
1821
import { type DeviceActionProps } from "@/components/DeviceActionsView/DeviceActionTester";
19-
import { SignPsbtDAInputValuesForm } from "@/components/SignerBtcView/SignPsbtDAInputValusForm";
22+
import {
23+
descriptorTemplateToDerivationPath,
24+
SignPsbtDAInputValuesForm,
25+
} from "@/components/SignerBtcView/SignPsbtDAInputValusForm";
2026
import { useDmk } from "@/providers/DeviceManagementKitProvider";
2127

2228
const DEFAULT_DERIVATION_PATH = "84'/0'/0'";
@@ -115,6 +121,39 @@ export const SignerBtcView: React.FC<{ sessionId: string }> = ({
115121
SignPsbtDAError,
116122
SignPsbtDAIntermediateValue
117123
>,
124+
{
125+
title: "Sign transaction",
126+
description:
127+
"Perform all the actions necessary to sign a PSBT with the device and extract transaction",
128+
executeDeviceAction: ({ descriptorTemplate, psbt, path }) => {
129+
if (!signer) {
130+
throw new Error("Signer not initialized");
131+
}
132+
133+
return signer.signTransaction(
134+
new DefaultWallet(path, descriptorTemplate),
135+
psbt,
136+
);
137+
},
138+
InputValuesComponent: SignPsbtDAInputValuesForm,
139+
initialValues: {
140+
descriptorTemplate: DefaultDescriptorTemplate.NATIVE_SEGWIT,
141+
psbt: "",
142+
path: descriptorTemplateToDerivationPath[
143+
DefaultDescriptorTemplate.NATIVE_SEGWIT
144+
],
145+
},
146+
deviceModelId,
147+
} satisfies DeviceActionProps<
148+
SignTransactionDAOutput,
149+
{
150+
psbt: string;
151+
path: string;
152+
descriptorTemplate: DefaultDescriptorTemplate;
153+
},
154+
SignTransactionDAError,
155+
SignTransactionDAIntermediateValue
156+
>,
118157
],
119158
[deviceModelId, signer],
120159
);

packages/signer/signer-btc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"license": "Apache-2.0",
55
"main": "lib/cjs/index.js",
66
"types": "lib/cjs/index.d.ts",
7-
"private": true,
7+
"private": false,
88
"exports": {
99
".": {
1010
"types": "./lib/types/index.d.ts",

packages/signer/signer-btc/src/api/SignerBtc.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type GetExtendedPublicKeyDAReturnType } from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes";
22
import { type SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionTypes";
33
import { type SignPsbtDAReturnType } from "@api/app-binder/SignPsbtDeviceActionTypes";
4+
import { type SignTransactionDAReturnType } from "@api/app-binder/SignTransactionDeviceActionTypes";
45
import { type AddressOptions } from "@api/model/AddressOptions";
56
import { type Psbt } from "@api/model/Psbt";
67
import { type Wallet } from "@api/model/Wallet";
@@ -15,6 +16,6 @@ export interface SignerBtc {
1516
message: string,
1617
) => SignMessageDAReturnType;
1718
signPsbt: (wallet: Wallet, psbt: Psbt) => SignPsbtDAReturnType;
19+
signTransaction: (wallet: Wallet, psbt: Psbt) => SignTransactionDAReturnType;
1820
// getAddress: (wallet: Wallet, options?: AddressOptions) => Promise<string>;
19-
// signTransaction: (wallet: Wallet, psbt: Psbt) => Promise<Uint8Array>;
2021
}

packages/signer/signer-btc/src/api/app-binder/SignPsbtDeviceActionTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export type SignPsbtDAError =
3535
| OpenAppDAError
3636
| CommandErrorResult<BtcErrorCodes>["error"];
3737

38-
type SignPsbtDARequiredInteraction =
38+
export type SignPsbtDARequiredInteraction =
3939
| OpenAppDARequiredInteraction
4040
| UserInteractionRequired.SignTransaction;
4141

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {
2+
type CommandErrorResult,
3+
type DeviceActionState,
4+
type ExecuteDeviceActionReturnType,
5+
type HexaString,
6+
type OpenAppDAError,
7+
type OpenAppDARequiredInteraction,
8+
} from "@ledgerhq/device-management-kit";
9+
10+
import { type SignPsbtDARequiredInteraction } from "@api/app-binder/SignPsbtDeviceActionTypes";
11+
import { type Psbt as ApiPsbt } from "@api/model/Psbt";
12+
import { type PsbtSignature } from "@api/model/Signature";
13+
import { type Wallet as ApiWallet } from "@api/model/Wallet";
14+
import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors";
15+
import { type DataStoreService } from "@internal/data-store/service/DataStoreService";
16+
import { type Psbt as InternalPsbt } from "@internal/psbt/model/Psbt";
17+
import { type PsbtMapper } from "@internal/psbt/service/psbt/PsbtMapper";
18+
import { type ValueParser } from "@internal/psbt/service/value/ValueParser";
19+
import { type WalletBuilder } from "@internal/wallet/service/WalletBuilder";
20+
import { type WalletSerializer } from "@internal/wallet/service/WalletSerializer";
21+
22+
export type SignTransactionDAOutput = HexaString;
23+
24+
export type SignTransactionDAInput = {
25+
psbt: ApiPsbt;
26+
wallet: ApiWallet;
27+
walletBuilder: WalletBuilder;
28+
walletSerializer: WalletSerializer;
29+
dataStoreService: DataStoreService;
30+
psbtMapper: PsbtMapper;
31+
valueParser: ValueParser;
32+
};
33+
34+
export type SignTransactionDAError =
35+
| OpenAppDAError
36+
| CommandErrorResult<BtcErrorCodes>["error"];
37+
38+
type SignTransactionDARequiredInteraction =
39+
| OpenAppDARequiredInteraction
40+
| SignPsbtDARequiredInteraction;
41+
42+
export type SignTransactionDAIntermediateValue = {
43+
requiredUserInteraction: SignTransactionDARequiredInteraction;
44+
};
45+
46+
export type SignTransactionDAState = DeviceActionState<
47+
SignTransactionDAOutput,
48+
SignTransactionDAError,
49+
SignTransactionDAIntermediateValue
50+
>;
51+
52+
export type SignTransactionDAInternalState = {
53+
readonly error: SignTransactionDAError | null;
54+
readonly signatures: PsbtSignature[] | null;
55+
readonly signedPsbt: InternalPsbt | null;
56+
readonly transaction: HexaString | null;
57+
};
58+
59+
export type SignTransactionDAReturnType = ExecuteDeviceActionReturnType<
60+
SignTransactionDAOutput,
61+
SignTransactionDAError,
62+
SignTransactionDAIntermediateValue
63+
>;

packages/signer/signer-btc/src/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type {
99
SignMessageDAState,
1010
} from "@api/app-binder/SignMessageDeviceActionTypes";
1111
export * from "@api/app-binder/SignPsbtDeviceActionTypes";
12+
export * from "@api/app-binder/SignTransactionDeviceActionTypes";
1213
export { DefaultDescriptorTemplate, DefaultWallet } from "@api/model/Wallet";
1314
export * from "@api/SignerBtc";
1415
export * from "@api/SignerBtcBuilder";

packages/signer/signer-btc/src/internal/DefaultSignerBtc.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { type DeviceManagementKit } from "@ledgerhq/device-management-kit";
22

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

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

@@ -39,4 +42,30 @@ describe("DefaultSignerBtc", () => {
3942
signer.signMessage(derivationPath, message);
4043
expect(SignMessageUseCase.prototype.execute).toHaveBeenCalled();
4144
});
45+
it("should call signPsbtUseCase", () => {
46+
jest.spyOn(SignPsbtUseCase.prototype, "execute");
47+
const sessionId = "session-id";
48+
const dmk = {
49+
executeDeviceAction: jest.fn(),
50+
} as unknown as DeviceManagementKit;
51+
const signer = new DefaultSignerBtc({ dmk, sessionId });
52+
signer.signPsbt(
53+
new DefaultWallet("44'/0'/0'", DefaultDescriptorTemplate.NATIVE_SEGWIT),
54+
"",
55+
);
56+
expect(SignPsbtUseCase.prototype.execute).toHaveBeenCalled();
57+
});
58+
it("should call signTransactionUseCase", () => {
59+
jest.spyOn(SignTransactionUseCase.prototype, "execute");
60+
const sessionId = "session-id";
61+
const dmk = {
62+
executeDeviceAction: jest.fn(),
63+
} as unknown as DeviceManagementKit;
64+
const signer = new DefaultSignerBtc({ dmk, sessionId });
65+
signer.signTransaction(
66+
new DefaultWallet("44'/0'/0'", DefaultDescriptorTemplate.NATIVE_SEGWIT),
67+
"",
68+
);
69+
expect(SignTransactionUseCase.prototype.execute).toHaveBeenCalled();
70+
});
4271
});

packages/signer/signer-btc/src/internal/DefaultSignerBtc.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { type SignerBtc } from "@api/SignerBtc";
1212
import { useCasesTypes } from "@internal/use-cases/di/useCasesTypes";
1313
import { type GetExtendedPublicKeyUseCase } from "@internal/use-cases/get-extended-public-key/GetExtendedPublicKeyUseCase";
1414
import { type SignPsbtUseCase } from "@internal/use-cases/sign-psbt/SignPsbtUseCase";
15+
import { type SignTransactionUseCase } from "@internal/use-cases/sign-transaction/SignTransactionUseCase";
1516

1617
import { type SignMessageUseCase } from "./use-cases/sign-message/SignMessageUseCase";
1718
import { makeContainer } from "./di";
@@ -53,4 +54,10 @@ export class DefaultSignerBtc implements SignerBtc {
5354
.get<SignMessageUseCase>(useCasesTypes.SignMessageUseCase)
5455
.execute(_derivationPath, _message);
5556
}
57+
58+
signTransaction(wallet: Wallet, psbt: Psbt) {
59+
return this._container
60+
.get<SignTransactionUseCase>(useCasesTypes.SignTransactionUseCase)
61+
.execute(wallet, psbt);
62+
}
5663
}

packages/signer/signer-btc/src/internal/app-binder/BtcAppBinder.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import {
1212
} from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes";
1313
import { SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionTypes";
1414
import { SignPsbtDAReturnType } from "@api/app-binder/SignPsbtDeviceActionTypes";
15+
import { SignTransactionDAReturnType } from "@api/app-binder/SignTransactionDeviceActionTypes";
1516
import { Psbt } from "@api/model/Psbt";
1617
import { Wallet } from "@api/model/Wallet";
1718
import { GetExtendedPublicKeyCommand } from "@internal/app-binder/command/GetExtendedPublicKeyCommand";
1819
import { SignPsbtDeviceAction } from "@internal/app-binder/device-action/SignPsbt/SignPsbtDeviceAction";
20+
import { SignTransactionDeviceAction } from "@internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction";
1921
import { dataStoreTypes } from "@internal/data-store/di/dataStoreTypes";
2022
import type { DataStoreService } from "@internal/data-store/service/DataStoreService";
2123
import { externalTypes } from "@internal/externalTypes";
@@ -96,4 +98,24 @@ export class BtcAppBinder {
9698
}),
9799
});
98100
}
101+
102+
signTransaction(args: {
103+
psbt: Psbt;
104+
wallet: Wallet;
105+
}): SignTransactionDAReturnType {
106+
return this._dmk.executeDeviceAction({
107+
sessionId: this._sessionId,
108+
deviceAction: new SignTransactionDeviceAction({
109+
input: {
110+
psbt: args.psbt,
111+
wallet: args.wallet,
112+
walletBuilder: this._walletBuilder,
113+
walletSerializer: this._walletSerializer,
114+
dataStoreService: this._dataStoreService,
115+
psbtMapper: this._psbtMapper,
116+
valueParser: this._valueParser,
117+
},
118+
}),
119+
});
120+
}
99121
}

0 commit comments

Comments
 (0)