diff --git a/.changeset/pretty-wombats-know.md b/.changeset/pretty-wombats-know.md new file mode 100644 index 000000000..53e9186a1 --- /dev/null +++ b/.changeset/pretty-wombats-know.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-signer-kit-bitcoin": minor +--- + +PrepareWalletPolicy task diff --git a/packages/signer/signer-btc/src/internal/app-binder/command/GetMasterFingerprintCommand.test.ts b/packages/signer/signer-btc/src/internal/app-binder/command/GetMasterFingerprintCommand.test.ts index 5b33a4a5f..f30b2c6e1 100644 --- a/packages/signer/signer-btc/src/internal/app-binder/command/GetMasterFingerprintCommand.test.ts +++ b/packages/signer/signer-btc/src/internal/app-binder/command/GetMasterFingerprintCommand.test.ts @@ -47,7 +47,7 @@ describe("GetMasterFingerprintCommand", () => { expect(result).toEqual( CommandResultFactory({ data: { - masterFingerprint: "828dc2f3", + masterFingerprint: Uint8Array.from([0x82, 0x8d, 0xc2, 0xf3]), }, }), ); diff --git a/packages/signer/signer-btc/src/internal/app-binder/command/GetMasterFingerprintCommand.ts b/packages/signer/signer-btc/src/internal/app-binder/command/GetMasterFingerprintCommand.ts index 14796590e..ce3a902fb 100644 --- a/packages/signer/signer-btc/src/internal/app-binder/command/GetMasterFingerprintCommand.ts +++ b/packages/signer/signer-btc/src/internal/app-binder/command/GetMasterFingerprintCommand.ts @@ -16,7 +16,7 @@ import { const MASTER_FINGERPRINT_LENGTH = 4; type GetMasterFingerprintCommandResponse = { - masterFingerprint: string; + masterFingerprint: Uint8Array; }; export class GetMasterFingerprintCommand @@ -43,16 +43,16 @@ export class GetMasterFingerprintCommand }); } - if (!parser.testMinimalLength(MASTER_FINGERPRINT_LENGTH)) { + const masterFingerprint = parser.extractFieldByLength( + MASTER_FINGERPRINT_LENGTH, + ); + + if (!masterFingerprint) { return CommandResultFactory({ error: new InvalidStatusWordError("Master fingerprint is missing"), }); } - const masterFingerprint = parser.encodeToHexaString( - parser.extractFieldByLength(MASTER_FINGERPRINT_LENGTH), - ); - return CommandResultFactory({ data: { masterFingerprint, diff --git a/packages/signer/signer-btc/src/internal/app-binder/task/PrepareWalletPolicyTask.test.ts b/packages/signer/signer-btc/src/internal/app-binder/task/PrepareWalletPolicyTask.test.ts new file mode 100644 index 000000000..c17b37162 --- /dev/null +++ b/packages/signer/signer-btc/src/internal/app-binder/task/PrepareWalletPolicyTask.test.ts @@ -0,0 +1,150 @@ +import { + CommandResultFactory, + type InternalApi, + isSuccessCommandResult, + UnknownDeviceExchangeError, +} from "@ledgerhq/device-management-kit"; + +import { + DefaultDescriptorTemplate, + DefaultWallet, + type Wallet, +} from "@api/model/Wallet"; +import { PrepareWalletPolicyTask } from "@internal/app-binder/task/PrepareWalletPolicyTask"; +import { DataStore } from "@internal/data-store/model/DataStore"; +import { type DataStoreService } from "@internal/data-store/service/DataStoreService"; +import { type WalletBuilder } from "@internal/wallet/service/WalletBuilder"; +const fromDefaultWalletMock = jest.fn(); +const merklizeWalletMock = jest.fn(); + +describe("PrepareWalletPolicyTask", () => { + let internalApi: { sendCommand: jest.Mock }; + const walletBuilder = { + fromDefaultWallet: fromDefaultWalletMock, + } as unknown as WalletBuilder; + const dataStoreService = { + merklizeWallet: merklizeWalletMock, + } as unknown as DataStoreService; + beforeEach(() => { + internalApi = { + sendCommand: jest.fn(), + }; + }); + afterEach(() => { + jest.resetAllMocks(); + }); + + it("should return a filled data store", async () => { + // given + const defaultWallet = new DefaultWallet( + "49'/0'/0'", + DefaultDescriptorTemplate.LEGACY, + ); + const task = new PrepareWalletPolicyTask( + internalApi as unknown as InternalApi, + walletBuilder, + dataStoreService, + ); + internalApi.sendCommand.mockResolvedValueOnce( + Promise.resolve( + CommandResultFactory({ + data: { + masterFingerprint: Uint8Array.from([0x42, 0x21, 0x12, 0x24]), + }, + }), + ), + ); + internalApi.sendCommand.mockResolvedValueOnce( + Promise.resolve( + CommandResultFactory({ + data: { + extendedPublicKey: "xPublicKey", + }, + }), + ), + ); + const wallet = {} as Wallet; + fromDefaultWalletMock.mockReturnValue(wallet); + // when + const result = await task.run(defaultWallet); + // then + if (isSuccessCommandResult(result)) { + expect(fromDefaultWalletMock).toHaveBeenCalledWith( + Uint8Array.from([0x42, 0x21, 0x12, 0x24]), + "xPublicKey", + defaultWallet, + ); + expect(merklizeWalletMock).toHaveBeenCalledWith(new DataStore(), wallet); + expect(result.data).toBeInstanceOf(DataStore); + } else { + fail("Expected a success result, but the result was an error"); + } + }); + + it("should return an error if getMasterFingerprint failed", async () => { + // given + const defaultWallet = new DefaultWallet( + "49'/0'/0'", + DefaultDescriptorTemplate.LEGACY, + ); + const task = new PrepareWalletPolicyTask( + internalApi as unknown as InternalApi, + walletBuilder, + dataStoreService, + ); + const error = new UnknownDeviceExchangeError("Failed"); + internalApi.sendCommand.mockResolvedValueOnce( + Promise.resolve( + CommandResultFactory({ + error, + }), + ), + ); + // when + const result = await task.run(defaultWallet); + // then + if (isSuccessCommandResult(result) === false) { + expect(result.error).toStrictEqual(error); + } else { + fail("Expected an error, but the result was successful"); + } + }); + + it("should return an error if getExtendedPublicKey failed", async () => { + // given + const defaultWallet = new DefaultWallet( + "49'/0'/0'", + DefaultDescriptorTemplate.LEGACY, + ); + const task = new PrepareWalletPolicyTask( + internalApi as unknown as InternalApi, + walletBuilder, + dataStoreService, + ); + const error = new UnknownDeviceExchangeError("Failed"); + internalApi.sendCommand.mockResolvedValueOnce( + Promise.resolve( + CommandResultFactory({ + data: { + masterFingerprint: Uint8Array.from([0x42, 0x21, 0x12, 0x24]), + }, + }), + ), + ); + internalApi.sendCommand.mockResolvedValueOnce( + Promise.resolve( + CommandResultFactory({ + error, + }), + ), + ); + // when + const result = await task.run(defaultWallet); + // then + if (isSuccessCommandResult(result) === false) { + expect(result.error).toStrictEqual(error); + } else { + fail("Expected an error, but the result was successful"); + } + }); +}); diff --git a/packages/signer/signer-btc/src/internal/app-binder/task/PrepareWalletPolicyTask.ts b/packages/signer/signer-btc/src/internal/app-binder/task/PrepareWalletPolicyTask.ts new file mode 100644 index 000000000..cd57a396e --- /dev/null +++ b/packages/signer/signer-btc/src/internal/app-binder/task/PrepareWalletPolicyTask.ts @@ -0,0 +1,55 @@ +import { + type CommandResult, + CommandResultFactory, + type InternalApi, + isSuccessCommandResult, +} from "@ledgerhq/device-management-kit"; + +import { type DefaultWallet } from "@api/model/Wallet"; +import { GetExtendedPublicKeyCommand } from "@internal/app-binder/command/GetExtendedPublicKeyCommand"; +import { GetMasterFingerprintCommand } from "@internal/app-binder/command/GetMasterFingerprintCommand"; +import { DataStore } from "@internal/data-store/model/DataStore"; +import { type DataStoreService } from "@internal/data-store/service/DataStoreService"; +import { type WalletBuilder } from "@internal/wallet/service/WalletBuilder"; + +export class PrepareWalletPolicyTask { + constructor( + private readonly _api: InternalApi, + private readonly _walletBuilder: WalletBuilder, + private readonly _dataStoreService: DataStoreService, + ) {} + + async run(defaultWallet: DefaultWallet): Promise> { + // request master fingerprint + const getMasterFingerPrintResult = await this._api.sendCommand( + new GetMasterFingerprintCommand(), + ); + if (!isSuccessCommandResult(getMasterFingerPrintResult)) { + return getMasterFingerPrintResult; + } + + // request extended public key for derivation path + const getExtendedPublicKeyResult = await this._api.sendCommand( + new GetExtendedPublicKeyCommand({ + derivationPath: defaultWallet.derivationPath, + checkOnDevice: false, + }), + ); + if (!isSuccessCommandResult(getExtendedPublicKeyResult)) { + return getExtendedPublicKeyResult; + } + // create default wallet with wallet policy service + const wallet = this._walletBuilder.fromDefaultWallet( + getMasterFingerPrintResult.data.masterFingerprint, + getExtendedPublicKeyResult.data.extendedPublicKey, + defaultWallet, + ); + // feed the data store + const store = new DataStore(); + this._dataStoreService.merklizeWallet(store, wallet); + + return CommandResultFactory({ + data: store, + }); + } +}