Skip to content

Commit

Permalink
♻️ (signer-btc): Create ContinueTask & use it for SignMessage
Browse files Browse the repository at this point in the history
  • Loading branch information
jdabbech-ledger committed Dec 27, 2024
1 parent 17aaee3 commit c3296da
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 412 deletions.
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
Expand Up @@ -18,8 +18,9 @@ import {
type SignMessageDAIntermediateValue,
type SignMessageDAInternalState,
type SignMessageDAOutput,
} from "@api/app-binder/SignMessageDeviceActionType";
} from "@api/app-binder/SignMessageDeviceActionTypes";
import { type Signature } from "@api/model/Signature";
import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors";
import {
SendSignMessageTask,
type SendSignMessageTaskArgs,
Expand All @@ -28,7 +29,7 @@ import {
export type MachineDependencies = {
readonly signMessage: (arg0: {
input: SendSignMessageTaskArgs;
}) => Promise<CommandResult<Signature>>;
}) => Promise<CommandResult<Signature, BtcErrorCodes>>;
};

export type ExtractMachineDependencies = (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
ApduResponse,
CommandResultFactory,
type DmkError,
type InternalApi,
UnknownDeviceExchangeError,
} from "@ledgerhq/device-management-kit";
import { type Either, Left, Right } from "purify-ts";

import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors";
import { ContinueTask } from "@internal/app-binder/task/ContinueTask";
import { type DataStore } from "@internal/data-store/model/DataStore";

describe("ContinueTask", () => {
const clientCommandInterpreter = {
getClientCommandPayload: jest.fn(
() => Right(Uint8Array.from([])) as Either<DmkError, Uint8Array>,
),
};
const api = {
sendCommand: jest.fn(),
};
const randomNumberOfClientCalls = Math.floor(Math.random() * 10 + 2);

afterEach(() => {
jest.clearAllMocks();
});

it(`should call ${randomNumberOfClientCalls} times client interpreter and return success`, async () => {
// given
new Array(randomNumberOfClientCalls).fill(0).forEach((_) => {
api.sendCommand.mockReturnValueOnce(
CommandResultFactory({
data: new ApduResponse({
statusCode: Uint8Array.from([0xe0, 0x00]),
data: Uint8Array.from([]),
}),
}),
);
});
api.sendCommand.mockReturnValueOnce(
CommandResultFactory({
data: new ApduResponse({
statusCode: Uint8Array.from([0x90, 0x00]),
data: Uint8Array.from([]),
}),
}),
);
const fromResult = CommandResultFactory<ApduResponse, BtcErrorCodes>({
data: new ApduResponse({
statusCode: Uint8Array.from([0xe0, 0x00]),
data: Uint8Array.from([]),
}),
});
// when
const task = new ContinueTask(
api as unknown as InternalApi,
clientCommandInterpreter,
);
await task.run({} as DataStore, fromResult);
// then
expect(
clientCommandInterpreter.getClientCommandPayload,
).toHaveBeenCalledTimes(randomNumberOfClientCalls + 1);
});

it("should return an error if the client interpreter fails", async () => {
// given
const error = new UnknownDeviceExchangeError("Failed");
clientCommandInterpreter.getClientCommandPayload.mockReturnValueOnce(
Left(error),
);
const fromResult = CommandResultFactory<ApduResponse, BtcErrorCodes>({
data: new ApduResponse({
statusCode: Uint8Array.from([0xe0, 0x00]),
data: Uint8Array.from([]),
}),
});
// when
const task = new ContinueTask(
api as unknown as InternalApi,
clientCommandInterpreter,
);
const result = await task.run({} as DataStore, fromResult);
// then
expect(api.sendCommand).toHaveBeenCalledTimes(0);
expect(result).toStrictEqual(
CommandResultFactory({ error: new UnknownDeviceExchangeError(error) }),
);
});
it("should return an error if send command fails", async () => {
// given
const error = new UnknownDeviceExchangeError("Failed");
api.sendCommand.mockReturnValueOnce(CommandResultFactory({ error }));
const fromResult = CommandResultFactory<ApduResponse, BtcErrorCodes>({
data: new ApduResponse({
statusCode: Uint8Array.from([0xe0, 0x00]),
data: Uint8Array.from([]),
}),
});
// when
const task = new ContinueTask(
api as unknown as InternalApi,
clientCommandInterpreter,
);
const result = await task.run({} as DataStore, fromResult);
// then
expect(
clientCommandInterpreter.getClientCommandPayload,
).toHaveBeenCalledTimes(1);
expect(result).toStrictEqual(CommandResultFactory({ error }));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
type ApduResponse,
type CommandResult,
CommandResultFactory,
type CommandSuccessResult,
type InternalApi,
isSuccessCommandResult,
UnknownDeviceExchangeError,
} from "@ledgerhq/device-management-kit";

import { type ClientCommandContext } from "@internal/app-binder/command/client-command-handlers/ClientCommandHandlersTypes";
import {
ContinueCommand,
type ContinueCommandResponse,
} from "@internal/app-binder/command/ContinueCommand";
import { ClientCommandInterpreter } from "@internal/app-binder/command/service/ClientCommandInterpreter";
import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors";
import { type DataStore } from "@internal/data-store/model/DataStore";
import { BtcCommandUtils } from "@internal/utils/BtcCommandUtils";

export class ContinueTask {
private readonly _clientCommandInterpreter: ClientCommandInterpreter;

constructor(
private readonly _api: InternalApi,
clientCommandInterpreter?: ClientCommandInterpreter,
) {
this._clientCommandInterpreter =
clientCommandInterpreter || new ClientCommandInterpreter();
}

async run(
dataStore: DataStore,
fromResult: CommandResult<ApduResponse, BtcErrorCodes>,
): Promise<CommandResult<ContinueCommandResponse, BtcErrorCodes>> {
let currentResponse: CommandResult<ContinueCommandResponse, BtcErrorCodes> =
fromResult;
const commandHandlersContext: ClientCommandContext = {
dataStore,
queue: [],
yieldedResults: [],
};

while (
this.isApduResult(currentResponse) &&
BtcCommandUtils.isContinueResponse(currentResponse.data)
) {
currentResponse = await this._clientCommandInterpreter
.getClientCommandPayload(
currentResponse.data.data,
commandHandlersContext,
)
.caseOf({
Left: (error) =>
Promise.resolve(
CommandResultFactory({
error: new UnknownDeviceExchangeError(error),
}),
),
Right: (payload) =>
this._api.sendCommand(
new ContinueCommand({
payload,
}),
),
});
}
return currentResponse;
}
private isApduResult = (
response: CommandResult<ContinueCommandResponse, BtcErrorCodes>,
): response is CommandSuccessResult<ApduResponse> => {
return (
isSuccessCommandResult(response) &&
typeof response.data === "object" &&
response.data !== null &&
"statusCode" in response.data &&
"data" in response.data
);
};
}
Loading

0 comments on commit c3296da

Please sign in to comment.