diff --git a/.changeset/fuzzy-eagles-cough.md b/.changeset/fuzzy-eagles-cough.md new file mode 100644 index 000000000..07cf2f449 --- /dev/null +++ b/.changeset/fuzzy-eagles-cough.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-signer-kit-bitcoin": minor +--- + +Implement get wallet device action and sample app getWalletAddress diff --git a/apps/sample/src/components/SignerBtcView/index.tsx b/apps/sample/src/components/SignerBtcView/index.tsx index a3746963b..11fb31221 100644 --- a/apps/sample/src/components/SignerBtcView/index.tsx +++ b/apps/sample/src/components/SignerBtcView/index.tsx @@ -5,6 +5,9 @@ import { type GetExtendedDAIntermediateValue, type GetExtendedPublicKeyDAError, type GetExtendedPublicKeyDAOutput, + type GetWalletAddressDAError, + type GetWalletAddressDAIntermediateValue, + type GetWalletAddressDAOutput, SignerBtcBuilder, type SignMessageDAError, type SignMessageDAIntermediateValue, @@ -114,6 +117,47 @@ export const SignerBtcView: React.FC<{ sessionId: string }> = ({ SignPsbtDAError, SignPsbtDAIntermediateValue >, + { + title: "Get wallet address", + description: + "Perform all the actions necessary to get the device's Bitcoin wallet address", + executeDeviceAction: ({ + checkOnDevice, + change, + addressIndex, + derivationPath, + }) => { + if (!signer) { + throw new Error("Signer not initialized"); + } + + return signer.getWalletAddress( + new DefaultWallet( + derivationPath, + DefaultDescriptorTemplate.NATIVE_SEGWIT, + ), + addressIndex, + { checkOnDevice, change }, + ); + }, + initialValues: { + checkOnDevice: false, + change: false, + derivationPath: DEFAULT_DERIVATION_PATH, + addressIndex: 0, + }, + deviceModelId, + } satisfies DeviceActionProps< + GetWalletAddressDAOutput, + { + checkOnDevice: boolean; + change: boolean; + addressIndex: number; + derivationPath: string; + }, + GetWalletAddressDAError, + GetWalletAddressDAIntermediateValue + >, ], [deviceModelId, signer], ); diff --git a/packages/signer/signer-btc/src/api/SignerBtc.ts b/packages/signer/signer-btc/src/api/SignerBtc.ts index c955bcd56..82d3693a5 100644 --- a/packages/signer/signer-btc/src/api/SignerBtc.ts +++ b/packages/signer/signer-btc/src/api/SignerBtc.ts @@ -5,6 +5,9 @@ import { type AddressOptions } from "@api/model/AddressOptions"; import { type Psbt } from "@api/model/Psbt"; import { type Wallet } from "@api/model/Wallet"; +import { type GetWalletAddressDAReturnType } from "./app-binder/GetWalletAddressDeviceActionTypes"; +import { WalletAddressOptions } from "./model/WalletAddressOptions"; + export interface SignerBtc { getExtendedPublicKey: ( derivationPath: string, @@ -15,6 +18,10 @@ export interface SignerBtc { message: string, ) => SignMessageDAReturnType; signPsbt: (wallet: Wallet, psbt: Psbt) => SignPsbtDAReturnType; - // getAddress: (wallet: Wallet, options?: AddressOptions) => Promise; + getWalletAddress: ( + wallet: Wallet, + addressIndex: number, + options: WalletAddressOptions, + ) => GetWalletAddressDAReturnType; // signTransaction: (wallet: Wallet, psbt: Psbt) => Promise; } diff --git a/packages/signer/signer-btc/src/api/app-binder/GetWalletAddressDeviceActionTypes.ts b/packages/signer/signer-btc/src/api/app-binder/GetWalletAddressDeviceActionTypes.ts new file mode 100644 index 000000000..10c49c6a8 --- /dev/null +++ b/packages/signer/signer-btc/src/api/app-binder/GetWalletAddressDeviceActionTypes.ts @@ -0,0 +1,47 @@ +import { + type CommandErrorResult, + type DeviceActionState, + type ExecuteDeviceActionReturnType, + type OpenAppDAError, + type OpenAppDARequiredInteraction, +} from "@ledgerhq/device-management-kit"; + +import { type WalletAddress } from "@api/model/Wallet"; +import { type Wallet as ApiWallet } from "@api/model/Wallet"; +import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors"; + +export type GetWalletAddressDAOutput = WalletAddress; + +export type GetWalletAddressDAInput = { + checkOnDevice: boolean; + wallet: ApiWallet; + change: boolean; + addressIndex: number; +}; + +export type GetWalletAddressDAError = + | OpenAppDAError + | CommandErrorResult["error"]; + +type GetWalletAddressDARequiredInteraction = OpenAppDARequiredInteraction; + +export type GetWalletAddressDAIntermediateValue = { + requiredUserInteraction: GetWalletAddressDARequiredInteraction; +}; + +export type GetWalletAddressDAState = DeviceActionState< + GetWalletAddressDAOutput, + GetWalletAddressDAError, + GetWalletAddressDAIntermediateValue +>; + +export type GetWalletAddressDAInternalState = { + readonly error: GetWalletAddressDAError | null; + readonly walletAddress: WalletAddress | null; +}; + +export type GetWalletAddressDAReturnType = ExecuteDeviceActionReturnType< + GetWalletAddressDAOutput, + GetWalletAddressDAError, + GetWalletAddressDAIntermediateValue +>; diff --git a/packages/signer/signer-btc/src/api/index.ts b/packages/signer/signer-btc/src/api/index.ts index a1bdfb302..2e9dce71d 100644 --- a/packages/signer/signer-btc/src/api/index.ts +++ b/packages/signer/signer-btc/src/api/index.ts @@ -1,6 +1,7 @@ export { type SignerBtc } from "./SignerBtc"; export { SignerBtcBuilder } from "./SignerBtcBuilder"; export * from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes"; +export * from "@api/app-binder/GetWalletAddressDeviceActionTypes"; export type { SignMessageDAError, SignMessageDAInput, diff --git a/packages/signer/signer-btc/src/api/model/WalletAddressOptions.ts b/packages/signer/signer-btc/src/api/model/WalletAddressOptions.ts new file mode 100644 index 000000000..b072be698 --- /dev/null +++ b/packages/signer/signer-btc/src/api/model/WalletAddressOptions.ts @@ -0,0 +1,4 @@ +export type WalletAddressOptions = { + checkOnDevice?: boolean; + change?: boolean; +}; diff --git a/packages/signer/signer-btc/src/internal/DefaultSignerBtc.ts b/packages/signer/signer-btc/src/internal/DefaultSignerBtc.ts index 8d48d8121..42c07d62c 100644 --- a/packages/signer/signer-btc/src/internal/DefaultSignerBtc.ts +++ b/packages/signer/signer-btc/src/internal/DefaultSignerBtc.ts @@ -8,11 +8,13 @@ import { type SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceA import { type AddressOptions } from "@api/model/AddressOptions"; import { type Psbt } from "@api/model/Psbt"; import { type Wallet } from "@api/model/Wallet"; +import { type WalletAddressOptions } from "@api/model/WalletAddressOptions"; 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 GetWalletAddressUseCase } from "./use-cases/get-wallet-address/GetWalletAddressUseCase"; import { type SignMessageUseCase } from "./use-cases/sign-message/SignMessageUseCase"; import { makeContainer } from "./di"; @@ -28,6 +30,16 @@ export class DefaultSignerBtc implements SignerBtc { this._container = makeContainer({ dmk, sessionId }); } + getWalletAddress( + wallet: Wallet, + addressIndex: number, + { checkOnDevice = false, change = false }: WalletAddressOptions, + ) { + return this._container + .get(useCasesTypes.GetWalletAddressUseCase) + .execute(checkOnDevice, wallet, change, addressIndex); + } + signPsbt(wallet: Wallet, psbt: Psbt) { return this._container .get(useCasesTypes.SignPsbtUseCase) @@ -46,11 +58,11 @@ export class DefaultSignerBtc implements SignerBtc { } signMessage( - _derivationPath: string, - _message: string, + derivationPath: string, + message: string, ): SignMessageDAReturnType { return this._container .get(useCasesTypes.SignMessageUseCase) - .execute(_derivationPath, _message); + .execute(derivationPath, message); } } diff --git a/packages/signer/signer-btc/src/internal/app-binder/BtcAppBinder.ts b/packages/signer/signer-btc/src/internal/app-binder/BtcAppBinder.ts index ef70b3fed..d403b80c0 100644 --- a/packages/signer/signer-btc/src/internal/app-binder/BtcAppBinder.ts +++ b/packages/signer/signer-btc/src/internal/app-binder/BtcAppBinder.ts @@ -10,14 +10,23 @@ import { GetExtendedPublicKeyDAInput, GetExtendedPublicKeyDAReturnType, } from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes"; -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 { + GetWalletAddressDAInput, + GetWalletAddressDAReturnType, +} from "@api/app-binder/GetWalletAddressDeviceActionTypes"; +import { + SignMessageDAInput, + SignMessageDAReturnType, +} from "@api/app-binder/SignMessageDeviceActionTypes"; +import { + SignPsbtDAInput, + SignPsbtDAReturnType, +} from "@api/app-binder/SignPsbtDeviceActionTypes"; import { GetExtendedPublicKeyCommand } from "@internal/app-binder/command/GetExtendedPublicKeyCommand"; import { SignPsbtDeviceAction } from "@internal/app-binder/device-action/SignPsbt/SignPsbtDeviceAction"; import { externalTypes } from "@internal/externalTypes"; +import { GetWalletAddressDeviceAction } from "./device-action/GetWalletAddress/GetWalletAddressDeviceAction"; import { SignMessageDeviceAction } from "./device-action/SignMessage/SignMessageDeviceAction"; @injectable() @@ -44,10 +53,7 @@ export class BtcAppBinder { }); } - signMessage(args: { - derivationPath: string; - message: string; - }): SignMessageDAReturnType { + signMessage(args: SignMessageDAInput): SignMessageDAReturnType { return this.dmk.executeDeviceAction({ sessionId: this.sessionId, deviceAction: new SignMessageDeviceAction({ @@ -59,7 +65,7 @@ export class BtcAppBinder { }); } - signPsbt(args: { psbt: Psbt; wallet: Wallet }): SignPsbtDAReturnType { + signPsbt(args: SignPsbtDAInput): SignPsbtDAReturnType { return this.dmk.executeDeviceAction({ sessionId: this.sessionId, deviceAction: new SignPsbtDeviceAction({ @@ -70,4 +76,20 @@ export class BtcAppBinder { }), }); } + + getWalletAddress( + args: GetWalletAddressDAInput, + ): GetWalletAddressDAReturnType { + return this.dmk.executeDeviceAction({ + sessionId: this.sessionId, + deviceAction: new GetWalletAddressDeviceAction({ + input: { + wallet: args.wallet, + checkOnDevice: args.checkOnDevice, + change: args.change, + addressIndex: args.addressIndex, + }, + }), + }); + } } diff --git a/packages/signer/signer-btc/src/internal/app-binder/command/GetExtendedPublicKeyCommand.test.ts b/packages/signer/signer-btc/src/internal/app-binder/command/GetExtendedPublicKeyCommand.test.ts index 44bfda957..8ed194e14 100644 --- a/packages/signer/signer-btc/src/internal/app-binder/command/GetExtendedPublicKeyCommand.test.ts +++ b/packages/signer/signer-btc/src/internal/app-binder/command/GetExtendedPublicKeyCommand.test.ts @@ -14,12 +14,12 @@ import { type GetExtendedPublicKeyCommandArgs, } from "./GetExtendedPublicKeyCommand"; -const GET_EXTENDED_PUBLIC_KEY_APDU_WITH_DISPLAY = new Uint8Array([ +const GET_EXTENDED_PUBLIC_KEY_APDU_WITH_checkOnDevice = new Uint8Array([ 0xe1, 0x00, 0x00, 0x00, 0x0e, 0x01, 0x03, 0x80, 0x00, 0x00, 0x54, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, ]); -const GET_EXTENDED_PUBLIC_KEY_APDU_WITHOUT_DISPLAY = new Uint8Array([ +const GET_EXTENDED_PUBLIC_KEY_APDU_WITHOUT_checkOnDevice = new Uint8Array([ 0xe1, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x03, 0x80, 0x00, 0x00, 0x54, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, ]); @@ -60,11 +60,11 @@ describe("GetExtendedPublicKeyCommand", () => { //THEN expect(apdu.getRawApdu()).toEqual( - GET_EXTENDED_PUBLIC_KEY_APDU_WITH_DISPLAY, + GET_EXTENDED_PUBLIC_KEY_APDU_WITH_checkOnDevice, ); }); - it("should return the correct APDU without display", () => { + it("should return the correct APDU without checkOnDevice", () => { // GIVEN command = new GetExtendedPublicKeyCommand({ ...defaultArgs, @@ -76,7 +76,7 @@ describe("GetExtendedPublicKeyCommand", () => { //THEN expect(apdu.getRawApdu()).toEqual( - GET_EXTENDED_PUBLIC_KEY_APDU_WITHOUT_DISPLAY, + GET_EXTENDED_PUBLIC_KEY_APDU_WITHOUT_checkOnDevice, ); }); diff --git a/packages/signer/signer-btc/src/internal/app-binder/command/GetWalletAddressCommand.test.ts b/packages/signer/signer-btc/src/internal/app-binder/command/GetWalletAddressCommand.test.ts index 908861b67..dcf3abcc7 100644 --- a/packages/signer/signer-btc/src/internal/app-binder/command/GetWalletAddressCommand.test.ts +++ b/packages/signer/signer-btc/src/internal/app-binder/command/GetWalletAddressCommand.test.ts @@ -16,7 +16,7 @@ const USER_DENIED_STATUS = new Uint8Array([0x69, 0x85]); describe("GetWalletAddressCommand", () => { let command: GetWalletAddressCommand; const defaultArgs: GetWalletAddressCommandArgs = { - display: true, + checkOnDevice: true, walletId: Uint8Array.from("walletIdBuffer", (c) => c.charCodeAt(0)), walletHmac: Uint8Array.from("walletHmacBuffer", (c) => c.charCodeAt(0)), change: false, @@ -38,7 +38,7 @@ describe("GetWalletAddressCommand", () => { 0x00, // P1 0x01, // P2 0x24, // Length of data: 36 bytes - 0x01, // display: true + 0x01, // checkOnDevice: true ...Uint8Array.from("walletIdBuffer", (c) => c.charCodeAt(0)), ...Uint8Array.from("walletHmacBuffer", (c) => c.charCodeAt(0)), 0x00, // change: false @@ -52,7 +52,7 @@ describe("GetWalletAddressCommand", () => { it("should return correct APDU for different arguments", () => { const args: GetWalletAddressCommandArgs = { - display: false, + checkOnDevice: false, walletId: Uint8Array.from("anotherWalletId", (c) => c.charCodeAt(0)), walletHmac: Uint8Array.from("anotherWalletHmac", (c) => c.charCodeAt(0), @@ -68,7 +68,7 @@ describe("GetWalletAddressCommand", () => { 0x00, // P1 0x01, // P2 0x26, // Length of data - 0x00, // display: false + 0x00, // checkOnDevice: false ...Uint8Array.from("anotherWalletId", (c) => c.charCodeAt(0)), ...Uint8Array.from("anotherWalletHmac", (c) => c.charCodeAt(0)), 0x01, // change: true diff --git a/packages/signer/signer-btc/src/internal/app-binder/command/GetWalletAddressCommand.ts b/packages/signer/signer-btc/src/internal/app-binder/command/GetWalletAddressCommand.ts index e8eb97935..4ee536851 100644 --- a/packages/signer/signer-btc/src/internal/app-binder/command/GetWalletAddressCommand.ts +++ b/packages/signer/signer-btc/src/internal/app-binder/command/GetWalletAddressCommand.ts @@ -21,7 +21,7 @@ import { export type GetWalletAddressCommandResponse = ApduResponse; export type GetWalletAddressCommandArgs = { - readonly display: boolean; + readonly checkOnDevice: boolean; readonly walletId: Uint8Array; readonly walletHmac: Uint8Array; readonly change: boolean; @@ -55,7 +55,7 @@ export class GetWalletAddressCommand p1: 0x00, p2: PROTOCOL_VERSION, }) - .addBufferToData(Uint8Array.from([this.args.display ? 1 : 0])) + .addBufferToData(Uint8Array.from([this.args.checkOnDevice ? 1 : 0])) .addBufferToData(this.args.walletId) .addBufferToData(this.args.walletHmac) .addBufferToData(Uint8Array.from([this.args.change ? 1 : 0])) diff --git a/packages/signer/signer-btc/src/internal/app-binder/device-action/GetWalletAddress/GetWalletAddressDeviceAction.test.ts b/packages/signer/signer-btc/src/internal/app-binder/device-action/GetWalletAddress/GetWalletAddressDeviceAction.test.ts new file mode 100644 index 000000000..2bd11454b --- /dev/null +++ b/packages/signer/signer-btc/src/internal/app-binder/device-action/GetWalletAddress/GetWalletAddressDeviceAction.test.ts @@ -0,0 +1,325 @@ +import { + CommandResultFactory, + DeviceActionStatus, + UnknownDeviceExchangeError, + UserInteractionRequired, +} from "@ledgerhq/device-management-kit"; +import { UnknownDAError } from "@ledgerhq/device-management-kit"; +import { InvalidStatusWordError } from "@ledgerhq/device-management-kit"; + +import { type GetWalletAddressDAState } from "@api/app-binder/GetWalletAddressDeviceActionTypes"; +import { type RegisteredWallet, type WalletAddress } from "@api/model/Wallet"; +import { makeDeviceActionInternalApiMock } from "@internal/app-binder/device-action/__test-utils__/makeInternalApi"; +import { setupOpenAppDAMock } from "@internal/app-binder/device-action/__test-utils__/setupOpenAppDAMock"; +import { testDeviceActionStates } from "@internal/app-binder/device-action/__test-utils__/testDeviceActionStates"; + +import { GetWalletAddressDeviceAction as DeviceAction } from "./GetWalletAddressDeviceAction"; + +jest.mock( + "@ledgerhq/device-management-kit", + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + () => ({ + ...jest.requireActual("@ledgerhq/device-management-kit"), + OpenAppDeviceAction: jest.fn(() => ({ + makeStateMachine: jest.fn(), + })), + }), +); + +describe("GetWalletAddressDeviceAction", () => { + const getWalletAddressMock = jest.fn(); + + function extractDependenciesMock() { + return { + getWalletAddress: getWalletAddressMock, + }; + } + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe("Success cases", () => { + it("should call external dependencies with the correct parameters and complete successfully", (done) => { + setupOpenAppDAMock(); + + const deviceAction = new DeviceAction({ + input: { + checkOnDevice: true, + wallet: {} as unknown as RegisteredWallet, + change: false, + addressIndex: 0, + }, + }); + + jest + .spyOn(deviceAction, "extractDependencies") + .mockReturnValue(extractDependenciesMock()); + const mockWalletAddress: WalletAddress = { + address: "mocked_address", + }; + getWalletAddressMock.mockResolvedValueOnce( + CommandResultFactory({ + data: mockWalletAddress, + }), + ); + + // Initial -> OpenApp -> CheckResult -> GetWalletAddress -> CheckResult -> Success + const expectedStates: Array = [ + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.ConfirmOpenApp, + }, + status: DeviceActionStatus.Pending, + }, + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + { + output: mockWalletAddress, + status: DeviceActionStatus.Completed, + }, + ]; + + const { observable } = testDeviceActionStates( + deviceAction, + expectedStates, + makeDeviceActionInternalApiMock(), + done, + ); + + observable.subscribe({ + complete: () => { + expect(getWalletAddressMock).toHaveBeenCalledWith( + expect.objectContaining({ + input: { + checkOnDevice: true, + wallet: {} as unknown as RegisteredWallet, + change: false, + addressIndex: 0, + }, + }), + ); + }, + }); + }); + }); + + describe("Error cases", () => { + it("should handle error if the open app fails", (done) => { + setupOpenAppDAMock( + new UnknownDeviceExchangeError("Mocked open app error"), + ); + + const deviceAction = new DeviceAction({ + input: { + checkOnDevice: true, + wallet: {} as unknown as RegisteredWallet, + change: false, + addressIndex: 0, + }, + }); + + const expectedStates: Array = [ + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + }, + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.ConfirmOpenApp, + }, + }, + { + status: DeviceActionStatus.Error, + error: new UnknownDeviceExchangeError("Mocked open app error"), + }, + ]; + + testDeviceActionStates( + deviceAction, + expectedStates, + makeDeviceActionInternalApiMock(), + done, + ); + }); + + it("should handle error if getWalletAddress fails with CommandErrorResult", (done) => { + setupOpenAppDAMock(); + + const deviceAction = new DeviceAction({ + input: { + checkOnDevice: true, + wallet: {} as unknown as RegisteredWallet, + change: false, + addressIndex: 0, + }, + }); + + jest + .spyOn(deviceAction, "extractDependencies") + .mockReturnValue(extractDependenciesMock()); + getWalletAddressMock.mockResolvedValueOnce( + CommandResultFactory({ + error: new UnknownDeviceExchangeError( + "Mocked getWalletAddress error", + ), + }), + ); + + const expectedStates: Array = [ + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + }, + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.ConfirmOpenApp, + }, + }, + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + }, + { + status: DeviceActionStatus.Error, + error: new UnknownDeviceExchangeError( + "Mocked getWalletAddress error", + ), + }, + ]; + + testDeviceActionStates( + deviceAction, + expectedStates, + makeDeviceActionInternalApiMock(), + done, + ); + }); + + it("should handle error if getWalletAddress throws an exception", (done) => { + setupOpenAppDAMock(); + + const deviceAction = new DeviceAction({ + input: { + checkOnDevice: true, + wallet: {} as unknown as RegisteredWallet, + change: false, + addressIndex: 0, + }, + }); + + jest + .spyOn(deviceAction, "extractDependencies") + .mockReturnValue(extractDependenciesMock()); + getWalletAddressMock.mockRejectedValueOnce( + new InvalidStatusWordError("Mocked exception from getWalletAddress"), + ); + + const expectedStates: Array = [ + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + }, + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.ConfirmOpenApp, + }, + }, + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + }, + { + status: DeviceActionStatus.Error, + error: new InvalidStatusWordError( + "Mocked exception from getWalletAddress", + ), + }, + ]; + + testDeviceActionStates( + deviceAction, + expectedStates, + makeDeviceActionInternalApiMock(), + done, + ); + }); + + it("should return a Left if the final state has no walletAddress", (done) => { + setupOpenAppDAMock(); + + const deviceAction = new DeviceAction({ + input: { + checkOnDevice: true, + wallet: {} as unknown as RegisteredWallet, + change: false, + addressIndex: 0, + }, + }); + + jest + .spyOn(deviceAction, "extractDependencies") + .mockReturnValue(extractDependenciesMock()); + getWalletAddressMock.mockResolvedValueOnce( + CommandResultFactory({ + data: undefined, + }), + ); + + const expectedStates: Array = [ + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + }, + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.ConfirmOpenApp, + }, + }, + { + status: DeviceActionStatus.Pending, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + }, + { + status: DeviceActionStatus.Error, + error: new UnknownDAError("No error in final state"), + }, + ]; + + testDeviceActionStates( + deviceAction, + expectedStates, + makeDeviceActionInternalApiMock(), + done, + ); + }); + }); +}); diff --git a/packages/signer/signer-btc/src/internal/app-binder/device-action/GetWalletAddress/GetWalletAddressDeviceAction.ts b/packages/signer/signer-btc/src/internal/app-binder/device-action/GetWalletAddress/GetWalletAddressDeviceAction.ts new file mode 100644 index 000000000..8ae700cd9 --- /dev/null +++ b/packages/signer/signer-btc/src/internal/app-binder/device-action/GetWalletAddress/GetWalletAddressDeviceAction.ts @@ -0,0 +1,231 @@ +import { + type CommandResult, + type DeviceActionStateMachine, + type InternalApi, + isSuccessCommandResult, + OpenAppDeviceAction, + type StateMachineTypes, + UnknownDAError, + UserInteractionRequired, + XStateDeviceAction, +} from "@ledgerhq/device-management-kit"; +import { Left, Right } from "purify-ts"; +import { assign, fromPromise, setup } from "xstate"; + +import { + type GetWalletAddressDAError, + type GetWalletAddressDAInput, + type GetWalletAddressDAIntermediateValue, + type GetWalletAddressDAInternalState, + type GetWalletAddressDAOutput, +} from "@api/app-binder/GetWalletAddressDeviceActionTypes"; +import { type WalletAddress } from "@api/model/Wallet"; +import { type Wallet as ApiWallet } from "@api/model/Wallet"; +import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors"; +import { GetWalletAddressTask } from "@internal/app-binder/task/GetWalletAddressTask"; + +export type GetWalletAddressMachineDependencies = { + readonly getWalletAddress: (arg0: { + input: { + checkOnDevice: boolean; + wallet: ApiWallet; + change: boolean; + addressIndex: number; + }; + }) => Promise>; +}; + +export type ExtractGetWalletAddressMachineDependencies = ( + internalApi: InternalApi, +) => GetWalletAddressMachineDependencies; + +export class GetWalletAddressDeviceAction extends XStateDeviceAction< + GetWalletAddressDAOutput, + GetWalletAddressDAInput, + GetWalletAddressDAError, + GetWalletAddressDAIntermediateValue, + GetWalletAddressDAInternalState +> { + constructor(args: { input: GetWalletAddressDAInput; inspect?: boolean }) { + super(args); + } + + makeStateMachine( + internalApi: InternalApi, + ): DeviceActionStateMachine< + GetWalletAddressDAOutput, + GetWalletAddressDAInput, + GetWalletAddressDAError, + GetWalletAddressDAIntermediateValue, + GetWalletAddressDAInternalState + > { + type types = StateMachineTypes< + GetWalletAddressDAOutput, + GetWalletAddressDAInput, + GetWalletAddressDAError, + GetWalletAddressDAIntermediateValue, + GetWalletAddressDAInternalState + >; + + const { getWalletAddress } = this.extractDependencies(internalApi); + + return setup({ + types: { + input: {} as types["input"], + context: {} as types["context"], + output: {} as types["output"], + }, + + actors: { + openAppStateMachine: new OpenAppDeviceAction({ + input: { appName: "Bitcoin" }, + }).makeStateMachine(internalApi), + getWalletAddress: fromPromise(getWalletAddress), + }, + guards: { + noInternalError: ({ context }) => context._internalState.error === null, + }, + actions: { + assignErrorFromEvent: assign({ + _internalState: (_) => ({ + ..._.context._internalState, + error: _.event["error"], // NOTE: it should never happen, the error is not typed anymore here + }), + }), + }, + }).createMachine({ + /** @xstate-layout N4IgpgJg5mDOIC5QGUCWUB2AFMAnWA9hgIYA2AsnLMTACJgBuqAxmAILMAuqRAdAPIAHMBjaDB9Jqw7ciAYghEwvVBgYEA1soLDR45J2Kcw5YswAWqsAG0ADAF1EoQQVipZGJyAAeiACwAzADsvACcABwAjKGRtuG2AGyRSX4ANCAAnoiRAEwArLy2uX6RQUGRpeHhfgC+NelomDj4RGSUsNR0jCzsXDwYvADC5mDMGkIiYhLd0n1EAEpwAK6knHJ2jkggLm4eXr4IoRG8eTlFOaWhQX45QaHpWQiRN4UBfu8JeaE3obY5OXUGuhsHhCCQKFQaGBJD0ZP0hiMxhM9NMpL0PItYCs1tZIptnK53P19ogjuETmdcpdrrd7pl-M8wnkgvkgvFbH48n4EkFASBGiCWuD2p1oTN0fCBc0wW1ITAFEoVGpNMo3E1Qa0IR0oRsvDsiUQSU88tVeOEktEEuE8m8cjcHqScicgokTXk8rZygFQgD6vzgdLNSKoTDZh5eFKNcK5WA5HhcARcLxBKQjAAzRMAW14asFMq1ot1W31ey2B0iJr8ZotoStNpu9vpCDtoTNHr8PoizyKvL9kaFsu1XTRcL4-fzwZgmOxw1GGnWDj1hNLoAOFwCBWC5u5RySEQdT1sra+OSt1wCAQuzICfPHQZjoYlY4DUcHounq1nY3WeKXu2JZaIOum5sgkO61tE4QHhWBS2HkCT-G8R5wc8N58hgBAQHAXh3tGQ5iiOcyeMWy4AauiAALQJAeVGFLY9EMYxDG9kC6oDgWIbiqOAzIlMj7cX+BrEeRCCNo8sS2CcRz-B6zIsnBaGsXm974fxREInOvHiGpGLLKsgkrj4iBnrwFwVgkCHfEeCQBNB3K8IEpxBO6ySep8in+mxE4Plx6m4W+UIGWRRlPJEVRmncOTmhyLI2QeUS8GFQRvPBXZRLWt4vuxk4EbCflZd5+EfpwX4aEFhqAU84RRYUpyXmBATJBeQTQZEARhKEG5Ne69GUplXkqaKOmSkszCsB05XCSFOSXgUyVshUdoJLYF7QYkvAXkUER3H45q1qE-XKXhQ2+eGACiuAJrgk1GjN+S8PNUTFMtq1Nrckl-O6wTct6KF5HUdRAA */ + id: "GetWalletAddressDeviceAction", + initial: "OpenAppDeviceAction", + context: ({ input }) => { + return { + input, + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + _internalState: { + error: null, + walletAddress: null, + }, + }; + }, + states: { + OpenAppDeviceAction: { + exit: assign({ + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + }), + invoke: { + id: "openAppStateMachine", + input: { appName: "Bitcoin" }, + src: "openAppStateMachine", + onSnapshot: { + actions: assign({ + intermediateValue: (_) => + _.event.snapshot.context.intermediateValue, + }), + }, + onDone: { + actions: assign({ + _internalState: (_) => { + return _.event.output.caseOf( + { + Right: () => _.context._internalState, + Left: (error) => ({ + ..._.context._internalState, + error, + }), + }, + ); + }, + }), + target: "CheckOpenAppDeviceActionResult", + }, + }, + }, + CheckOpenAppDeviceActionResult: { + always: [ + { + target: "GetWalletAddress", + guard: "noInternalError", + }, + "Error", + ], + }, + GetWalletAddress: { + invoke: { + id: "getWalletAddress", + src: "getWalletAddress", + input: ({ context }) => ({ + checkOnDevice: context.input.checkOnDevice, + wallet: context.input.wallet, + change: context.input.change, + addressIndex: context.input.addressIndex, + }), + onDone: { + target: "GetWalletAddressResultCheck", + actions: [ + assign({ + _internalState: ({ event, context }) => { + if (isSuccessCommandResult(event.output)) { + return { + ...context._internalState, + walletAddress: event.output.data, + }; + } + return { + ...context._internalState, + error: event.output.error, + }; + }, + }), + ], + }, + onError: { + target: "Error", + actions: "assignErrorFromEvent", + }, + }, + }, + GetWalletAddressResultCheck: { + always: [ + { guard: "noInternalError", target: "Success" }, + { target: "Error" }, + ], + }, + Success: { + type: "final", + }, + Error: { + type: "final", + }, + }, + output: ({ context }) => + context._internalState.walletAddress + ? Right(context._internalState.walletAddress) + : Left( + context._internalState.error || + new UnknownDAError("No error in final state"), + ), + }); + } + + extractDependencies( + internalApi: InternalApi, + ): GetWalletAddressMachineDependencies { + const getWalletAddress = async (arg0: { + input: { + checkOnDevice: boolean; + wallet: ApiWallet; + change: boolean; + addressIndex: number; + }; + }): Promise> => { + return await new GetWalletAddressTask(internalApi, arg0.input).run(); + }; + return { + getWalletAddress, + }; + } +} diff --git a/packages/signer/signer-btc/src/internal/app-binder/task/GetWalletAddressTask.test.ts b/packages/signer/signer-btc/src/internal/app-binder/task/GetWalletAddressTask.test.ts index 8e58dfbf5..ede94be25 100644 --- a/packages/signer/signer-btc/src/internal/app-binder/task/GetWalletAddressTask.test.ts +++ b/packages/signer/signer-btc/src/internal/app-binder/task/GetWalletAddressTask.test.ts @@ -6,10 +6,8 @@ import { type InternalApi, InvalidStatusWordError, } from "@ledgerhq/device-management-kit"; -import { Left, Right } from "purify-ts"; +import { Right } from "purify-ts"; -import { ClientCommandHandlerError } from "@internal/app-binder/command/client-command-handlers/Errors"; -import { ContinueCommand } from "@internal/app-binder/command/ContinueCommand"; import { GetWalletAddressCommand } from "@internal/app-binder/command/GetWalletAddressCommand"; import { ClientCommandInterpreter } from "@internal/app-binder/command/service/ClientCommandInterpreter"; import { @@ -21,7 +19,7 @@ import { DefaultWalletSerializer } from "@internal/wallet/service/DefaultWalletS import { GetWalletAddressTask } from "./GetWalletAddressTask"; -const DISPLAY = true; +const checkOnDevice = true; const CHANGE = false; const ADDRESS_INDEX = 0; const TEST_ADDRESS = "bc1qexampleaddress"; @@ -64,7 +62,7 @@ describe("GetWalletAddressTask", () => { // when const result = await new GetWalletAddressTask(apiMock, { - display: DISPLAY, + checkOnDevice: checkOnDevice, wallet: MOCK_WALLET, change: CHANGE, addressIndex: ADDRESS_INDEX, @@ -82,47 +80,26 @@ describe("GetWalletAddressTask", () => { it("should handle interactive requests after an interrupted execution", async () => { // given - (apiMock.sendCommand as jest.Mock) - .mockResolvedValueOnce( - CommandResultFactory({ - data: { - statusCode: SW_INTERRUPTED_EXECUTION, - data: new Uint8Array([ClientCommandCodes.YIELD]), - }, - }), - ) // first GET_WALLET_ADDRESS - .mockResolvedValueOnce( - CommandResultFactory({ data: APDU_SUCCESS_RESPONSE }), - ); // after CONTINUE + (apiMock.sendCommand as jest.Mock).mockResolvedValueOnce( + CommandResultFactory({ data: APDU_SUCCESS_RESPONSE }), + ); jest .spyOn(DefaultWalletSerializer.prototype, "serialize") .mockReturnValue(REGISTERED_WALLET_ID); - jest - .spyOn(ClientCommandInterpreter.prototype, "getClientCommandPayload") - .mockImplementation((request: Uint8Array, context: any) => { - // YIELD command - if (request[0] === ClientCommandCodes.YIELD) { - context.yieldedResults.push(new Uint8Array([])); - return Right(new Uint8Array([0x00])); - } - return Left(new ClientCommandHandlerError("Unexpected command")); - }); - // when const result = await new GetWalletAddressTask(apiMock, { - display: DISPLAY, + checkOnDevice: checkOnDevice, wallet: MOCK_WALLET, change: CHANGE, addressIndex: ADDRESS_INDEX, }).run(); // then - expect(apiMock.sendCommand).toHaveBeenCalledTimes(2); - expect(apiMock.sendCommand).toHaveBeenNthCalledWith( - 2, - expect.any(ContinueCommand), + expect(apiMock.sendCommand).toHaveBeenCalledTimes(1); + expect(apiMock.sendCommand).toHaveBeenCalledWith( + expect.any(GetWalletAddressCommand), ); expect(result).toStrictEqual( CommandResultFactory({ data: { address: TEST_ADDRESS } }), @@ -139,7 +116,7 @@ describe("GetWalletAddressTask", () => { // when const result = await new GetWalletAddressTask(apiMock, { - display: DISPLAY, + checkOnDevice: checkOnDevice, wallet: MOCK_WALLET, change: CHANGE, addressIndex: ADDRESS_INDEX, @@ -157,7 +134,6 @@ describe("GetWalletAddressTask", () => { it("should fail if no address is extracted after all continuations", async () => { // given - // continue response but never a final address const continueResponse: ApduResponse = { statusCode: SW_INTERRUPTED_EXECUTION, data: new Uint8Array([ClientCommandCodes.YIELD]), @@ -178,7 +154,7 @@ describe("GetWalletAddressTask", () => { // when const result = await new GetWalletAddressTask(apiMock, { - display: DISPLAY, + checkOnDevice: checkOnDevice, wallet: MOCK_WALLET, change: CHANGE, addressIndex: ADDRESS_INDEX, diff --git a/packages/signer/signer-btc/src/internal/app-binder/task/GetWalletAddressTask.ts b/packages/signer/signer-btc/src/internal/app-binder/task/GetWalletAddressTask.ts index f92c5530d..92d28e3a1 100644 --- a/packages/signer/signer-btc/src/internal/app-binder/task/GetWalletAddressTask.ts +++ b/packages/signer/signer-btc/src/internal/app-binder/task/GetWalletAddressTask.ts @@ -7,12 +7,17 @@ import { import { type InternalApi } from "@ledgerhq/device-management-kit"; import { type WalletAddress } from "@api/model/Wallet"; +import { type Wallet as ApiWallet } from "@api/model/Wallet"; import { GetWalletAddressCommand } from "@internal/app-binder/command/GetWalletAddressCommand"; import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors"; import { DataStore } from "@internal/data-store/model/DataStore"; +import { type DataStoreService } from "@internal/data-store/service/DataStoreService"; +import { DefaultDataStoreService } from "@internal/data-store/service/DefaultDataStoreService"; +import { MerkleMapBuilder } from "@internal/merkle-tree/service/MerkleMapBuilder"; +import { MerkleTreeBuilder } from "@internal/merkle-tree/service/MerkleTreeBuilder"; import { Sha256HasherService } from "@internal/merkle-tree/service/Sha256HasherService"; import { BtcCommandUtils } from "@internal/utils/BtcCommandUtils"; -import { type Wallet } from "@internal/wallet/model/Wallet"; +import { type Wallet as InternalWallet } from "@internal/wallet/model/Wallet"; import { DefaultWalletSerializer } from "@internal/wallet/service/DefaultWalletSerializer"; import { type WalletSerializer } from "@internal/wallet/service/WalletSerializer"; @@ -20,53 +25,67 @@ import { ContinueTask } from "./ContinueTask"; import { PrepareWalletPolicyTask } from "./PrepareWalletPolicyTask"; export type SendGetWalletAddressTaskArgs = { - display: boolean; - wallet: Wallet; + checkOnDevice: boolean; + wallet: ApiWallet; change: boolean; addressIndex: number; }; export class GetWalletAddressTask { - private readonly _walletSerializer: WalletSerializer; - private readonly _dataStore: DataStore; + private readonly walletSerializer: WalletSerializer; + private readonly dataStoreService: DataStoreService; constructor( - private api: InternalApi, - private args: SendGetWalletAddressTaskArgs, - walletSerializer?: WalletSerializer, - dataStore?: DataStore, + private readonly api: InternalApi, + private readonly args: SendGetWalletAddressTaskArgs, + walletSerializerFromArg?: WalletSerializer, ) { - this._walletSerializer = - walletSerializer || + this.walletSerializer = + walletSerializerFromArg || new DefaultWalletSerializer(new Sha256HasherService()); - this._dataStore = dataStore || new DataStore(); + + const merkleTreeBuilder = new MerkleTreeBuilder(new Sha256HasherService()); + const merkleMapBuilder = new MerkleMapBuilder(merkleTreeBuilder); + const walletSerializer = new DefaultWalletSerializer( + new Sha256HasherService(), + ); + + this.dataStoreService = new DefaultDataStoreService( + merkleTreeBuilder, + merkleMapBuilder, + walletSerializer, + new Sha256HasherService(), + ); } - private async runPrepareWalletPolicy() { + private async prepareWalletPolicy() { return new PrepareWalletPolicyTask(this.api, { wallet: this.args.wallet, }).run(); } private async runGetWalletAddressTask( - wallet: Wallet, + wallet: InternalWallet, + dataStore: DataStore, ): Promise> { - const { display, change, addressIndex } = this.args; + const { checkOnDevice, change, addressIndex } = this.args; - const walletId = this._walletSerializer.serialize(wallet); + const walletId = this.walletSerializer.getId(wallet); const getWalletAddressInitialResponse = await this.api.sendCommand( new GetWalletAddressCommand({ - display, + checkOnDevice, walletId, walletHmac: wallet.hmac, change, addressIndex, }), ); - const response = await new ContinueTask(this.api, this._dataStore).run( + + const response = await new ContinueTask(this.api, dataStore).run( getWalletAddressInitialResponse, ); + if (isSuccessCommandResult(response)) { return BtcCommandUtils.getAddress(response); } @@ -77,11 +96,16 @@ export class GetWalletAddressTask { } async run(): Promise> { - const walletPolicyResult = await this.runPrepareWalletPolicy(); + const walletPolicyResult = await this.prepareWalletPolicy(); + if (!isSuccessCommandResult(walletPolicyResult)) { return walletPolicyResult; } - return this.runGetWalletAddressTask(walletPolicyResult.data); + const dataStore = new DataStore(); + + this.dataStoreService.merklizeWallet(dataStore, walletPolicyResult.data); + + return this.runGetWalletAddressTask(walletPolicyResult.data, dataStore); } } diff --git a/packages/signer/signer-btc/src/internal/use-cases/di/useCasesModule.test.ts b/packages/signer/signer-btc/src/internal/use-cases/di/useCasesModule.test.ts index fa1619860..23f919d38 100644 --- a/packages/signer/signer-btc/src/internal/use-cases/di/useCasesModule.test.ts +++ b/packages/signer/signer-btc/src/internal/use-cases/di/useCasesModule.test.ts @@ -26,5 +26,11 @@ describe("useCasesModuleFactory", () => { it("should bind SignMessageUseCase", () => { expect(container.isBound(useCasesTypes.SignMessageUseCase)).toBeTruthy(); }); + + it("should bind GetWalletAddressUseCase", () => { + expect( + container.isBound(useCasesTypes.GetWalletAddressUseCase), + ).toBeTruthy(); + }); }); }); diff --git a/packages/signer/signer-btc/src/internal/use-cases/di/useCasesModule.ts b/packages/signer/signer-btc/src/internal/use-cases/di/useCasesModule.ts index b66e24168..c4331b460 100644 --- a/packages/signer/signer-btc/src/internal/use-cases/di/useCasesModule.ts +++ b/packages/signer/signer-btc/src/internal/use-cases/di/useCasesModule.ts @@ -2,6 +2,7 @@ import { ContainerModule } from "inversify"; import { useCasesTypes } from "@internal/use-cases/di/useCasesTypes"; import { GetExtendedPublicKeyUseCase } from "@internal/use-cases/get-extended-public-key/GetExtendedPublicKeyUseCase"; +import { GetWalletAddressUseCase } from "@internal/use-cases/get-wallet-address/GetWalletAddressUseCase"; import { SignMessageUseCase } from "@internal/use-cases/sign-message/SignMessageUseCase"; import { SignPsbtUseCase } from "@internal/use-cases/sign-psbt/SignPsbtUseCase"; @@ -21,5 +22,6 @@ export const useCasesModuleFactory = () => ); bind(useCasesTypes.SignMessageUseCase).to(SignMessageUseCase); bind(useCasesTypes.SignPsbtUseCase).to(SignPsbtUseCase); + bind(useCasesTypes.GetWalletAddressUseCase).to(GetWalletAddressUseCase); }, ); diff --git a/packages/signer/signer-btc/src/internal/use-cases/di/useCasesTypes.ts b/packages/signer/signer-btc/src/internal/use-cases/di/useCasesTypes.ts index e2ebd8142..b38be385b 100644 --- a/packages/signer/signer-btc/src/internal/use-cases/di/useCasesTypes.ts +++ b/packages/signer/signer-btc/src/internal/use-cases/di/useCasesTypes.ts @@ -2,4 +2,5 @@ export const useCasesTypes = { GetExtendedPublicKeyUseCase: Symbol.for("GetExtendedPublicKeyUseCase"), SignMessageUseCase: Symbol.for("SignMessageUseCase"), SignPsbtUseCase: Symbol.for("SignPsbtUseCase"), + GetWalletAddressUseCase: Symbol.for("GetWalletAddressUseCase"), }; diff --git a/packages/signer/signer-btc/src/internal/use-cases/get-wallet-address/GetWalletAddressUseCase.test.ts b/packages/signer/signer-btc/src/internal/use-cases/get-wallet-address/GetWalletAddressUseCase.test.ts new file mode 100644 index 000000000..66372e49a --- /dev/null +++ b/packages/signer/signer-btc/src/internal/use-cases/get-wallet-address/GetWalletAddressUseCase.test.ts @@ -0,0 +1,48 @@ +import { type BtcAppBinder } from "@internal/app-binder/BtcAppBinder"; +import { Leaf } from "@internal/merkle-tree/model/Leaf"; +import { MerkleTree } from "@internal/merkle-tree/model/MerkleTree"; + +import { GetWalletAddressUseCase } from "./GetWalletAddressUseCase"; + +describe("GetWalletAddressUseCase", () => { + it("should call getWalletAddress on appBinder with the correct arguments", () => { + // given + const wallet = { + name: "wallet-name", + descriptorTemplate: "wpkh(@0/**)", + keys: ["key1", "key2"], + hmac: new Uint8Array(32).fill(42), + keysTree: new MerkleTree( + new Leaf(new Uint8Array(), new Uint8Array(32).fill(7)), + [], + ), + descriptorBuffer: new Uint8Array(31).fill(16), + }; + const checkOnDevice = false; + const change = false; + const addressIndex = 0; + + const appBinder = { + getWalletAddress: jest.fn(), + }; + const getWalletAddressUseCase = new GetWalletAddressUseCase( + appBinder as unknown as BtcAppBinder, + ); + + // when + getWalletAddressUseCase.execute( + checkOnDevice, + wallet, + change, + addressIndex, + ); + + // then + expect(appBinder.getWalletAddress).toHaveBeenCalledWith({ + wallet, + checkOnDevice, + change, + addressIndex, + }); + }); +}); diff --git a/packages/signer/signer-btc/src/internal/use-cases/get-wallet-address/GetWalletAddressUseCase.ts b/packages/signer/signer-btc/src/internal/use-cases/get-wallet-address/GetWalletAddressUseCase.ts new file mode 100644 index 000000000..371321026 --- /dev/null +++ b/packages/signer/signer-btc/src/internal/use-cases/get-wallet-address/GetWalletAddressUseCase.ts @@ -0,0 +1,32 @@ +import { inject, injectable } from "inversify"; + +import { GetWalletAddressDAReturnType } from "@api/app-binder/GetWalletAddressDeviceActionTypes"; +import { Wallet } from "@api/model/Wallet"; +import { BtcAppBinder } from "@internal/app-binder/BtcAppBinder"; +import { appBinderTypes } from "@internal/app-binder/di/appBinderTypes"; + +@injectable() +export class GetWalletAddressUseCase { + private _appBinder: BtcAppBinder; + + constructor( + @inject(appBinderTypes.AppBinder) + appBinding: BtcAppBinder, + ) { + this._appBinder = appBinding; + } + + execute( + checkOnDevice: boolean, + wallet: Wallet, + change: boolean, + addressIndex: number, + ): GetWalletAddressDAReturnType { + return this._appBinder.getWalletAddress({ + wallet, + checkOnDevice, + change, + addressIndex, + }); + } +}