diff --git a/.changeset/fine-colts-battle.md b/.changeset/fine-colts-battle.md new file mode 100644 index 000000000..4b5947524 --- /dev/null +++ b/.changeset/fine-colts-battle.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-signer-kit-aleo": minor +--- + +Add aleo-signer module with get app config command support diff --git a/apps/docs/pages/docs/references/signers/_meta.js b/apps/docs/pages/docs/references/signers/_meta.js index 8ee823f71..e2424c0ce 100644 --- a/apps/docs/pages/docs/references/signers/_meta.js +++ b/apps/docs/pages/docs/references/signers/_meta.js @@ -4,4 +4,5 @@ export default { btc: "Signer Bitcoin", hyperliquid: "Signer Hyperliquid", cosmos: "Signer Cosmos", + aleo: "Signer Aleo", }; diff --git a/apps/docs/pages/docs/references/signers/aleo.mdx b/apps/docs/pages/docs/references/signers/aleo.mdx new file mode 100644 index 000000000..48cd85460 --- /dev/null +++ b/apps/docs/pages/docs/references/signers/aleo.mdx @@ -0,0 +1,314 @@ +# Aleo Signer Kit + +This module provides the implementation of the Ledger aleo signer of the Device Management Kit. It enables interaction with the aleo application on a Ledger device including: + +- Retrieving the aleo address using a given derivation path +- Signing a aleo transaction +- Signing a message displayed on a Ledger device +- Retrieving the app configuration + +## 🔹 Index + +1. [How it works](#-how-it-works) +2. [Installation](#-installation) +3. [Initialisation](#-initialisation) +4. [Use Cases](#-use-cases) + - [Get App Configuration](#use-case-1-get-app-configuration) + - [Get Address](#use-case-2-get-address) + - [Sign Transaction](#use-case-3-sign-transaction) + - [Sign Message](#use-case-4-sign-message) +5. [Observable Behavior](#-observable-behavior) +6. [Example](#-example) + +## 🔹 How it works + +The Ledger Aleo Signer utilizes the advanced capabilities of the Ledger device to provide secure operations for end users. It takes advantage of the interface provided by the Device Management Kit to establish communication with the Ledger device and execute various operations. The communication with the Ledger device is performed using [APDU](https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit)s (Application Protocol Data Units), which are encapsulated within the `Command` object. These commands are then organized into tasks, allowing for the execution of complex operations with one or more APDUs. The tasks are further encapsulated within `DeviceAction` objects to handle different real-world scenarios. Finally, the Signer exposes dedicated and independent use cases that can be directly utilized by end users. + +## 🔹 Installation + +> **Note:** This module is not standalone; it depends on the [@ledgerhq/device-management-kit](https://github.com/LedgerHQ/device-sdk-ts/tree/develop/packages/device-management-kit) package, so you need to install it first. + +To install the `device-signer-kit-aleo` package, run the following command: + +```sh +npm install @ledgerhq/device-signer-kit-aleo +``` + +## 🔹 Initialisation + +To initialise a Aleo signer instance, you need a Ledger Device Management Kit instance and the ID of the session of the connected device. Use the `SignerAleoBuilder`: + +```typescript +const signerAleo = new SignerAleoBuilder({ dmk, sessionId }).build(); +``` + +## 🔹 Use Cases + +The `SignerAleoBuilder.build()` method will return a `SignerAleo` instance that exposes 4 dedicated methods, each of which calls an independent use case. Each use case will return an object that contains an observable and a method called `cancel`. + +--- + +### Use Case 1: Get App Configuration + +This method allows users to retrieve the app configuration from the Ledger device. + +```typescript +const { observable, cancel } = signerAleo.getAppConfig(); +``` + +#### **Returns** + +- `observable` Emits DeviceActionState updates, including the following details: + +```typescript +type GetAppConfigCommandResponse = { + // TODO: Define the app configuration response type + // Example: + // version: string; + // flags: number; +}; +``` + +- `cancel` A function to cancel the action on the Ledger device. + +--- + +### Use Case 2: Get Address + +This method allows users to retrieve the aleo address based on a given `derivationPath`. + +```typescript +const { observable, cancel } = signerAleo.getAddress(derivationPath, options); +``` + +#### **Parameters** + +- `derivationPath` + + - **Required** + - **Type:** `string` (e.g., `"m/44'/0'/0'/0/0"`) + - The derivation path used for the aleo address. See [here](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information. + +- `options` + + - Optional + - Type: `AddressOptions` + + ```typescript + type AddressOptions = { + checkOnDevice?: boolean; + skipOpenApp?: boolean; + }; + ``` + + - `checkOnDevice`: An optional boolean indicating whether user confirmation on the device is required (`true`) or not (`false`). + - `skipOpenApp`: An optional boolean indicating whether to skip opening the aleo app automatically (`true`) or not (`false`). + +#### **Returns** + +- `observable` Emits DeviceActionState updates, including the following details: + +```typescript +type GetAddressCommandResponse = { + publicKey: Uint8Array; + chainCode?: Uint8Array; +}; +``` + +- `cancel` A function to cancel the action on the Ledger device. + +--- + +### Use Case 3: Sign Transaction + +This method allows users to sign a aleo transaction. + +```typescript +const { observable, cancel } = signerAleo.signTransaction( + derivationPath, + transaction, + options, +); +``` + +#### **Parameters** + +- `derivationPath` + + - **Required** + - **Type:** `string` (e.g., `"m/44'/0'/0'/0/0"`) + - The derivation path used for the aleo transaction. See [here](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information. + +- `transaction` + + - **Required** + - **Type:** `Uint8Array` + - The serialized transaction to be signed. + +- `options` + + - Optional + - Type: `TransactionOptions` + + ```typescript + type TransactionOptions = { + skipOpenApp?: boolean; + }; + ``` + + - `skipOpenApp`: An optional boolean indicating whether to skip opening the aleo app automatically (`true`) or not (`false`). + +#### **Returns** + +- `observable` Emits DeviceActionState updates, including the following details: + +```typescript +type Signature = { + r: string; + s: string; + v?: number; +}; +``` + +- `cancel` A function to cancel the action on the Ledger device. + +--- + +### Use Case 4: Sign Message + +This method allows users to sign a text string that is displayed on Ledger devices. + +```typescript +const { observable, cancel } = signerAleo.signMessage(derivationPath, message); +``` + +#### **Parameters** + +- `derivationPath` + + - **Required** + - **Type:** `string` (e.g., `"m/44'/0'/0'/0/0"`) + - The derivation path used for the aleo message. See [here](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information. + +- `message` + + - **Required** + - **Type:** `string | Uint8Array` + - The message to be signed, which will be displayed on the Ledger device. + +#### **Returns** + +- `observable` Emits DeviceActionState updates, including the following details: + +```typescript +type Signature = { + r: string; + s: string; + v?: number; +}; +``` + +- `cancel` A function to cancel the action on the Ledger device. + +--- + +## 🔹 Observable Behavior + +Each method returns an [Observable](https://rxjs.dev/guide/observable) emitting updates structured as [`DeviceActionState`](https://github.com/LedgerHQ/device-sdk-ts/blob/develop/packages/device-management-kit/src/api/device-action/model/DeviceActionState.ts). These updates reflect the operation's progress and status: + +- **NotStarted**: The operation hasn't started. +- **Pending**: The operation is in progress and may require user interaction. +- **Stopped**: The operation was canceled or stopped. +- **Completed**: The operation completed successfully, with results available. +- **Error**: An error occurred. + +**Example Observable Subscription:** + +```typescript +observable.subscribe({ + next: (state: DeviceActionState) => { + switch (state.status) { + case DeviceActionStatus.NotStarted: { + console.log("The action is not started yet."); + break; + } + case DeviceActionStatus.Pending: { + const { + intermediateValue: { requiredUserInteraction }, + } = state; + // Access the intermediate value here, explained below + console.log( + "The action is pending and the intermediate value is: ", + intermediateValue, + ); + break; + } + case DeviceActionStatus.Stopped: { + console.log("The action has been stopped."); + break; + } + case DeviceActionStatus.Completed: { + const { output } = state; + // Access the output of the completed action here + console.log("The action has been completed: ", output); + break; + } + case DeviceActionStatus.Error: { + const { error } = state; + // Access the error here if occurred + console.log("An error occurred during the action: ", error); + break; + } + } + }, +}); +``` + +**Intermediate Values in Pending Status:** + +When the status is DeviceActionStatus.Pending, the state will include an `intermediateValue` object that provides useful information for interaction: + +```typescript +const { requiredUserInteraction } = intermediateValue; + +switch (requiredUserInteraction) { + case UserInteractionRequired.VerifyAddress: { + // User needs to verify the address displayed on the device + console.log("User needs to verify the address displayed on the device."); + break; + } + case UserInteractionRequired.SignTransaction: { + // User needs to sign the transaction displayed on the device + console.log("User needs to sign the transaction displayed on the device."); + break; + } + case UserInteractionRequired.SignPersonalMessage: { + // User needs to sign the message displayed on the device + console.log("User needs to sign the message displayed on the device."); + break; + } + case UserInteractionRequired.None: { + // No user action required + console.log("No user action needed."); + break; + } + case UserInteractionRequired.UnlockDevice: { + // User needs to unlock the device + console.log("The user needs to unlock the device."); + break; + } + case UserInteractionRequired.ConfirmOpenApp: { + // User needs to confirm on the device to open the app + console.log("The user needs to confirm on the device to open the app."); + break; + } + default: + // Type guard to ensure all cases are handled + const uncaughtUserInteraction: never = requiredUserInteraction; + console.error("Unhandled user interaction case:", uncaughtUserInteraction); +} +``` + +## 🔹 Example + +We encourage you to explore the Aleo Signer by trying it out in our online [sample application](https://app.devicesdk.ledger-test.com/). Experience how it works and see its capabilities in action. Of course, you will need a Ledger device connected. diff --git a/apps/sample/package.json b/apps/sample/package.json index 6d74a05cf..92c610fa9 100644 --- a/apps/sample/package.json +++ b/apps/sample/package.json @@ -25,6 +25,7 @@ "@ledgerhq/device-management-kit-devtools-websocket-common": "workspace:^", "@ledgerhq/device-management-kit-devtools-websocket-connector": "workspace:^", "@ledgerhq/device-mockserver-client": "workspace:^", + "@ledgerhq/device-signer-kit-aleo": "workspace:^", "@ledgerhq/device-signer-kit-bitcoin": "workspace:^", "@ledgerhq/device-signer-kit-cosmos": "workspace:^", "@ledgerhq/device-signer-kit-ethereum": "workspace:^", diff --git a/apps/sample/src/app/client-layout.tsx b/apps/sample/src/app/client-layout.tsx index 62c1dbddd..dd088a1eb 100644 --- a/apps/sample/src/app/client-layout.tsx +++ b/apps/sample/src/app/client-layout.tsx @@ -23,6 +23,7 @@ import { CalInterceptorProvider } from "@/providers/CalInterceptorProvider"; import { DmkProvider } from "@/providers/DeviceManagementKitProvider"; import { LedgerKeyringProtocolProvider } from "@/providers/LedgerKeyringProvider"; import { SettingsGate } from "@/providers/SettingsGate"; +import { SignerAleoProvider } from "@/providers/SignerAleoProvider"; import { SignerCosmosProvider } from "@/providers/SignerCosmosProvider"; import { SignerEthProvider } from "@/providers/SignerEthProvider"; import { store } from "@/state/store"; @@ -74,17 +75,19 @@ const ClientRootLayout: React.FC = ({ children }) => { - - - - - - - - {children} - - - + + + + + + + + + {children} + + + + diff --git a/apps/sample/src/app/signers/aleo/page.tsx b/apps/sample/src/app/signers/aleo/page.tsx new file mode 100644 index 000000000..2ae2a55ed --- /dev/null +++ b/apps/sample/src/app/signers/aleo/page.tsx @@ -0,0 +1,11 @@ +"use client"; +import React from "react"; + +import { SessionIdWrapper } from "@/components/SessionIdWrapper"; +import { SignerAleoView } from "@/components/SignerAleoView"; + +const Signer: React.FC = () => { + return ; +}; + +export default Signer; diff --git a/apps/sample/src/components/SignerAleoView/index.tsx b/apps/sample/src/components/SignerAleoView/index.tsx new file mode 100644 index 000000000..3bb806fd8 --- /dev/null +++ b/apps/sample/src/components/SignerAleoView/index.tsx @@ -0,0 +1,128 @@ +import React, { useMemo } from "react"; +import { + type GetAddressDAError, + type GetAddressDAIntermediateValue, + type GetAddressDAOutput, + type GetAppConfigDAError, + type GetAppConfigDAIntermediateValue, + type GetAppConfigDAOutput, + type SignTransactionDAError, + type SignTransactionDAIntermediateValue, + type SignTransactionDAOutput, +} from "@ledgerhq/device-signer-kit-aleo"; + +import { DeviceActionsList } from "@/components/DeviceActionsView/DeviceActionsList"; +import { type DeviceActionProps } from "@/components/DeviceActionsView/DeviceActionTester"; +import { useDmk } from "@/providers/DeviceManagementKitProvider"; +import { useSignerAleo } from "@/providers/SignerAleoProvider"; + +export const SignerAleoView: React.FC<{ sessionId: string }> = ({ + sessionId, +}) => { + const dmk = useDmk(); + const signer = useSignerAleo(); + + const deviceModelId = dmk.getConnectedDevice({ + sessionId, + }).modelId; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const deviceActions: DeviceActionProps[] = useMemo( + () => [ + { + title: "Get App Config", + description: "Get the app configuration from the device", + executeDeviceAction: () => { + if (!signer) { + throw new Error("Signer not initialized"); + } + return signer.getAppConfig(); + }, + initialValues: {}, + deviceModelId, + } satisfies DeviceActionProps< + GetAppConfigDAOutput, + Record, + GetAppConfigDAError, + GetAppConfigDAIntermediateValue + >, + { + title: "Get Address", + description: "Get an address from the device", + executeDeviceAction: ({ + derivationPath, + checkOnDevice, + skipOpenApp, + }) => { + if (!signer) { + throw new Error("Signer not initialized"); + } + return signer.getAddress(derivationPath, { + checkOnDevice, + skipOpenApp, + }); + }, + initialValues: { + derivationPath: "44'/0'/0'/0/0", + checkOnDevice: false, + skipOpenApp: false, + }, + deviceModelId, + } satisfies DeviceActionProps< + GetAddressDAOutput, + { + derivationPath: string; + checkOnDevice?: boolean; + skipOpenApp?: boolean; + }, + GetAddressDAError, + GetAddressDAIntermediateValue + >, + { + title: "Sign Transaction", + description: "Sign a transaction with the device", + executeDeviceAction: ({ derivationPath, transaction, skipOpenApp }) => { + if (!signer) { + throw new Error("Signer not initialized"); + } + // Convert hex string to Uint8Array + const txBytes = transaction.startsWith("0x") + ? new Uint8Array( + transaction + .slice(2) + .match(/.{1,2}/g) + ?.map((byte) => parseInt(byte, 16)) ?? [], + ) + : new Uint8Array( + transaction + .match(/.{1,2}/g) + ?.map((byte) => parseInt(byte, 16)) ?? [], + ); + return signer.signTransaction(derivationPath, txBytes, { + skipOpenApp, + }); + }, + initialValues: { + derivationPath: "44'/0'/0'/0/0", + transaction: "", + skipOpenApp: false, + }, + deviceModelId, + } satisfies DeviceActionProps< + SignTransactionDAOutput, + { + derivationPath: string; + transaction: string; + skipOpenApp?: boolean; + }, + SignTransactionDAError, + SignTransactionDAIntermediateValue + >, + ], + [deviceModelId, signer], + ); + + return ( + + ); +}; diff --git a/apps/sample/src/components/SignerView/index.tsx b/apps/sample/src/components/SignerView/index.tsx index d6c639ef5..04f2111eb 100644 --- a/apps/sample/src/components/SignerView/index.tsx +++ b/apps/sample/src/components/SignerView/index.tsx @@ -36,6 +36,11 @@ const SUPPORTED_SIGNERS = [ description: "Access Cosmos signer functionality", icon: , }, + { + title: "Aleo", + description: "Access Aleo signer functionality", + icon: , + }, ]; export const SignerView = () => { diff --git a/apps/sample/src/providers/SignerAleoProvider/index.tsx b/apps/sample/src/providers/SignerAleoProvider/index.tsx new file mode 100644 index 000000000..2addf7890 --- /dev/null +++ b/apps/sample/src/providers/SignerAleoProvider/index.tsx @@ -0,0 +1,63 @@ +"use client"; + +import React, { + createContext, + type PropsWithChildren, + useContext, + useEffect, + useState, +} from "react"; +import { useSelector } from "react-redux"; +import { + type SignerAleo, + SignerAleoBuilder, +} from "@ledgerhq/device-signer-kit-aleo"; + +import { useDmk } from "@/providers/DeviceManagementKitProvider"; +import { selectSelectedSessionId } from "@/state/sessions/selectors"; + +type SignerAleoContextType = { + signer: SignerAleo | null; +}; + +const initialState: SignerAleoContextType = { + signer: null, +}; + +const SignerAleoContext = createContext(initialState); + +export const SignerAleoProvider: React.FC = ({ + children, +}) => { + const dmk = useDmk(); + const sessionId = useSelector(selectSelectedSessionId); + + const [signer, setSigner] = useState(null); + + useEffect(() => { + if (!sessionId || !dmk) { + setSigner(null); + return; + } + + const newSigner = new SignerAleoBuilder({ + dmk, + sessionId, + }).build(); + setSigner(newSigner); + }, [dmk, sessionId]); + + return ( + + {children} + + ); +}; + +export const useSignerAleo = (): SignerAleo | null => { + return useContext(SignerAleoContext).signer; +}; diff --git a/package.json b/package.json index 6b2086666..3ddd921b6 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "devtools": "pnpm --filter @ledgerhq/device-management-kit-devtools", "speculos-device-controller": "pnpm --filter @ledgerhq/speculos-device-controller", "context-module": "pnpm --filter @ledgerhq/context-module", + "signer-aleo": "pnpm --filter @ledgerhq/device-signer-kit-aleo", "signer-btc": "pnpm --filter @ledgerhq/device-signer-kit-bitcoin", "signer-cosmos": "pnpm --filter @ledgerhq/device-signer-kit-cosmos", "signer-eth": "pnpm --filter @ledgerhq/device-signer-kit-ethereum", diff --git a/packages/signer/signer-aleo/.prettierignore b/packages/signer/signer-aleo/.prettierignore new file mode 100644 index 000000000..3b4e8dcf9 --- /dev/null +++ b/packages/signer/signer-aleo/.prettierignore @@ -0,0 +1,2 @@ +lib/ +node_modules/ diff --git a/packages/signer/signer-aleo/.prettierrc.js b/packages/signer/signer-aleo/.prettierrc.js new file mode 100644 index 000000000..98bb39000 --- /dev/null +++ b/packages/signer/signer-aleo/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require("@ledgerhq/prettier-config-dsdk"); diff --git a/packages/signer/signer-aleo/CHANGELOG.md b/packages/signer/signer-aleo/CHANGELOG.md new file mode 100644 index 000000000..a713356b9 --- /dev/null +++ b/packages/signer/signer-aleo/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.1.0] - 2026-02-05 + +### Added + +- Initial signer implementation for aleo diff --git a/packages/signer/signer-aleo/README.md b/packages/signer/signer-aleo/README.md new file mode 100644 index 000000000..db9d16bab --- /dev/null +++ b/packages/signer/signer-aleo/README.md @@ -0,0 +1,42 @@ +# Signer aleo + +This package provides a signer implementation for aleo. + +## Installation + +```bash +pnpm add @ledgerhq/device-signer-kit-aleo +``` + +## Usage + +```typescript +import { SignerAleoBuilder } from "@ledgerhq/device-signer-kit-aleo"; + +const signer = new SignerAleoBuilder({ dmk, sessionId }).build(); + +// Get address +const address = await signer.getAddress("m/44'/0'/0'/0/0"); + +// Sign transaction +const signature = await signer.signTransaction( + "m/44'/0'/0'/0/0", + transactionBytes, +); +``` + +## Development + +```bash +# Install dependencies +pnpm install + +# Build +pnpm build + +# Test +pnpm test + +# Lint +pnpm lint +``` diff --git a/packages/signer/signer-aleo/eslint.config.mjs b/packages/signer/signer-aleo/eslint.config.mjs new file mode 100644 index 000000000..40734f180 --- /dev/null +++ b/packages/signer/signer-aleo/eslint.config.mjs @@ -0,0 +1,13 @@ +import config from "@ledgerhq/eslint-config-dsdk"; + +export default [ + ...config, + { + ignores: ["eslint.config.mjs", "lib", "vitest.*.mjs"], + languageOptions: { + parserOptions: { + project: "./tsconfig.json", + }, + }, + }, +]; diff --git a/packages/signer/signer-aleo/package.json b/packages/signer/signer-aleo/package.json new file mode 100644 index 000000000..5f1881d0f --- /dev/null +++ b/packages/signer/signer-aleo/package.json @@ -0,0 +1,63 @@ +{ + "name": "@ledgerhq/device-signer-kit-aleo", + "version": "0.1.0", + "private": false, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/LedgerHQ/device-sdk-ts.git" + }, + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "import": "./lib/esm/index.js", + "require": "./lib/cjs/index.js" + }, + "./*": { + "types": "./lib/types/*", + "import": "./lib/esm/*", + "require": "./lib/cjs/*" + } + }, + "files": [ + "./lib" + ], + "scripts": { + "prebuild": "rimraf lib", + "build": "pnpm ldmk-tool build --entryPoints src/index.ts,src/**/*.ts --tsconfig tsconfig.prod.json", + "dev": "concurrently \"pnpm watch:builds\" \"pnpm watch:types\"", + "watch:builds": "pnpm ldmk-tool watch --entryPoints src/index.ts,src/**/*.ts --tsconfig tsconfig.prod.json", + "watch:types": "concurrently \"tsc --watch -p tsconfig.prod.json\" \"tsc-alias --watch -p tsconfig.prod.json\"", + "lint": "eslint", + "lint:fix": "pnpm lint --fix", + "prettier": "prettier . --check", + "prettier:fix": "prettier . --write", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" + }, + "dependencies": { + "@ledgerhq/signer-utils": "workspace:^", + "inversify": "catalog:", + "purify-ts": "catalog:", + "reflect-metadata": "catalog:", + "xstate": "catalog:", + "@ledgerhq/context-module": "workspace:^" + }, + "devDependencies": { + "@ledgerhq/device-management-kit": "workspace:^", + "@ledgerhq/ldmk-tool": "workspace:^", + "@ledgerhq/eslint-config-dsdk": "workspace:^", + "@ledgerhq/prettier-config-dsdk": "workspace:^", + "@ledgerhq/tsconfig-dsdk": "workspace:^", + "@ledgerhq/vitest-config-dmk": "workspace:^", + "rxjs": "catalog:", + "ts-node": "catalog:", + "@ledgerhq/context-module": "workspace:^" + }, + "peerDependencies": { + "@ledgerhq/device-management-kit": "workspace:^", + "@ledgerhq/context-module": "workspace:^" + } +} diff --git a/packages/signer/signer-aleo/src/api/SignerAleo.ts b/packages/signer/signer-aleo/src/api/SignerAleo.ts new file mode 100644 index 000000000..13228dccb --- /dev/null +++ b/packages/signer/signer-aleo/src/api/SignerAleo.ts @@ -0,0 +1,26 @@ +import { type GetAddressDAReturnType } from "@api/app-binder/GetAddressDeviceActionTypes"; +import { type GetAppConfigDAReturnType } from "@api/app-binder/GetAppConfigDeviceActionTypes"; +import { type SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionTypes"; +import { type SignTransactionDAReturnType } from "@api/app-binder/SignTransactionDeviceActionTypes"; +import { type AddressOptions } from "@api/model/AddressOptions"; +import { type TransactionOptions } from "@api/model/TransactionOptions"; + +export interface SignerAleo { + getAppConfig: () => GetAppConfigDAReturnType; + + getAddress: ( + derivationPath: string, + options?: AddressOptions, + ) => GetAddressDAReturnType; + + signTransaction: ( + derivationPath: string, + transaction: Uint8Array, + options?: TransactionOptions, + ) => SignTransactionDAReturnType; + + signMessage: ( + derivationPath: string, + message: string | Uint8Array, + ) => SignMessageDAReturnType; +} diff --git a/packages/signer/signer-aleo/src/api/SignerAleoBuilder.ts b/packages/signer/signer-aleo/src/api/SignerAleoBuilder.ts new file mode 100644 index 000000000..af1d3362d --- /dev/null +++ b/packages/signer/signer-aleo/src/api/SignerAleoBuilder.ts @@ -0,0 +1,36 @@ +import { + type DeviceManagementKit, + type DeviceSessionId, +} from "@ledgerhq/device-management-kit"; + +import { DefaultSignerAleo } from "@internal/DefaultSignerAleo"; + +type SignerAleoBuilderConstructorArgs = { + dmk: DeviceManagementKit; + sessionId: DeviceSessionId; +}; + +/** + * Builder for the `SignerAleo` class. + */ +export class SignerAleoBuilder { + private readonly _dmk: DeviceManagementKit; + private readonly _sessionId: DeviceSessionId; + + constructor({ dmk, sessionId }: SignerAleoBuilderConstructorArgs) { + this._dmk = dmk; + this._sessionId = sessionId; + } + + /** + * Build the signer instance + * + * @returns the signer instance + */ + public build() { + return new DefaultSignerAleo({ + dmk: this._dmk, + sessionId: this._sessionId, + }); + } +} diff --git a/packages/signer/signer-aleo/src/api/app-binder/GetAddressDeviceActionTypes.ts b/packages/signer/signer-aleo/src/api/app-binder/GetAddressDeviceActionTypes.ts new file mode 100644 index 000000000..34a4acbf7 --- /dev/null +++ b/packages/signer/signer-aleo/src/api/app-binder/GetAddressDeviceActionTypes.ts @@ -0,0 +1,31 @@ +import { + type CommandErrorResult, + type ExecuteDeviceActionReturnType, + type OpenAppDAError, + type SendCommandInAppDAIntermediateValue, + type SendCommandInAppDAOutput, + type UserInteractionRequired, +} from "@ledgerhq/device-management-kit"; + +import { type GetAddressCommandResponse } from "@internal/app-binder/command/GetAddressCommand"; +import { type AleoErrorCodes } from "@internal/app-binder/command/utils/aleoApplicationErrors"; + +type GetAddressDAUserInteractionRequired = + | UserInteractionRequired.None + | UserInteractionRequired.VerifyAddress; + +export type GetAddressDAOutput = + SendCommandInAppDAOutput; + +export type GetAddressDAError = + | OpenAppDAError + | CommandErrorResult["error"]; + +export type GetAddressDAIntermediateValue = + SendCommandInAppDAIntermediateValue; + +export type GetAddressDAReturnType = ExecuteDeviceActionReturnType< + GetAddressDAOutput, + GetAddressDAError, + GetAddressDAIntermediateValue +>; diff --git a/packages/signer/signer-aleo/src/api/app-binder/GetAppConfigDeviceActionTypes.ts b/packages/signer/signer-aleo/src/api/app-binder/GetAppConfigDeviceActionTypes.ts new file mode 100644 index 000000000..31a72d9e2 --- /dev/null +++ b/packages/signer/signer-aleo/src/api/app-binder/GetAppConfigDeviceActionTypes.ts @@ -0,0 +1,28 @@ +import { + type CommandErrorResult, + type ExecuteDeviceActionReturnType, + type OpenAppDAError, + type SendCommandInAppDAIntermediateValue, + type SendCommandInAppDAOutput, + type UserInteractionRequired, +} from "@ledgerhq/device-management-kit"; + +import type { AppConfig } from "@api/model/AppConfig"; +import { type AleoErrorCodes } from "@internal/app-binder/command/utils/aleoApplicationErrors"; + +type GetAppConfigDAUserInteractionRequired = UserInteractionRequired.None; + +export type GetAppConfigDAOutput = SendCommandInAppDAOutput; + +export type GetAppConfigDAError = + | OpenAppDAError + | CommandErrorResult["error"]; + +export type GetAppConfigDAIntermediateValue = + SendCommandInAppDAIntermediateValue; + +export type GetAppConfigDAReturnType = ExecuteDeviceActionReturnType< + GetAppConfigDAOutput, + GetAppConfigDAError, + GetAppConfigDAIntermediateValue +>; diff --git a/packages/signer/signer-aleo/src/api/app-binder/SignMessageDeviceActionTypes.ts b/packages/signer/signer-aleo/src/api/app-binder/SignMessageDeviceActionTypes.ts new file mode 100644 index 000000000..896d136b4 --- /dev/null +++ b/packages/signer/signer-aleo/src/api/app-binder/SignMessageDeviceActionTypes.ts @@ -0,0 +1,31 @@ +import { + type CommandErrorResult, + type ExecuteDeviceActionReturnType, + type OpenAppDAError, + type SendCommandInAppDAIntermediateValue, + type SendCommandInAppDAOutput, + type UserInteractionRequired, +} from "@ledgerhq/device-management-kit"; + +import { type SignMessageCommandResponse } from "@internal/app-binder/command/SignMessageCommand"; +import { type AleoErrorCodes } from "@internal/app-binder/command/utils/aleoApplicationErrors"; + +type SignMessageDAUserInteractionRequired = + | UserInteractionRequired.None + | UserInteractionRequired.SignPersonalMessage; + +export type SignMessageDAOutput = + SendCommandInAppDAOutput; + +export type SignMessageDAError = + | OpenAppDAError + | CommandErrorResult["error"]; + +export type SignMessageDAIntermediateValue = + SendCommandInAppDAIntermediateValue; + +export type SignMessageDAReturnType = ExecuteDeviceActionReturnType< + SignMessageDAOutput, + SignMessageDAError, + SignMessageDAIntermediateValue +>; diff --git a/packages/signer/signer-aleo/src/api/app-binder/SignTransactionDeviceActionTypes.ts b/packages/signer/signer-aleo/src/api/app-binder/SignTransactionDeviceActionTypes.ts new file mode 100644 index 000000000..8d9fc4fdf --- /dev/null +++ b/packages/signer/signer-aleo/src/api/app-binder/SignTransactionDeviceActionTypes.ts @@ -0,0 +1,30 @@ +import { + type CommandErrorResult, + type ExecuteDeviceActionReturnType, + type OpenAppDAError, + type OpenAppDARequiredInteraction, + type UserInteractionRequired, +} from "@ledgerhq/device-management-kit"; + +import { type Signature } from "@api/model/Signature"; +import { type AleoErrorCodes } from "@internal/app-binder/command/utils/aleoApplicationErrors"; + +export type SignTransactionDAOutput = Signature; + +export type SignTransactionDAError = + | OpenAppDAError + | CommandErrorResult["error"]; + +type SignTransactionDARequiredInteraction = + | OpenAppDARequiredInteraction + | UserInteractionRequired.SignTransaction; + +export type SignTransactionDAIntermediateValue = { + requiredUserInteraction: SignTransactionDARequiredInteraction; +}; + +export type SignTransactionDAReturnType = ExecuteDeviceActionReturnType< + SignTransactionDAOutput, + SignTransactionDAError, + SignTransactionDAIntermediateValue +>; diff --git a/packages/signer/signer-aleo/src/api/index.ts b/packages/signer/signer-aleo/src/api/index.ts new file mode 100644 index 000000000..8f0dfd6aa --- /dev/null +++ b/packages/signer/signer-aleo/src/api/index.ts @@ -0,0 +1,18 @@ +export type { + GetAddressDAError, + GetAddressDAIntermediateValue, + GetAddressDAOutput, +} from "@api/app-binder/GetAddressDeviceActionTypes"; +export type { + GetAppConfigDAError, + GetAppConfigDAIntermediateValue, + GetAppConfigDAOutput, +} from "@api/app-binder/GetAppConfigDeviceActionTypes"; +export type { + SignTransactionDAError, + SignTransactionDAIntermediateValue, + SignTransactionDAOutput, +} from "@api/app-binder/SignTransactionDeviceActionTypes"; +export * from "@api/SignerAleo"; +export * from "@api/SignerAleoBuilder"; +// Export other types as needed diff --git a/packages/signer/signer-aleo/src/api/model/AddressOptions.ts b/packages/signer/signer-aleo/src/api/model/AddressOptions.ts new file mode 100644 index 000000000..0a7f9c75e --- /dev/null +++ b/packages/signer/signer-aleo/src/api/model/AddressOptions.ts @@ -0,0 +1,4 @@ +export type AddressOptions = { + checkOnDevice?: boolean; + skipOpenApp?: boolean; +}; diff --git a/packages/signer/signer-aleo/src/api/model/AppConfig.ts b/packages/signer/signer-aleo/src/api/model/AppConfig.ts new file mode 100644 index 000000000..b7b6799f7 --- /dev/null +++ b/packages/signer/signer-aleo/src/api/model/AppConfig.ts @@ -0,0 +1,3 @@ +export type AppConfig = { + version: string; +}; diff --git a/packages/signer/signer-aleo/src/api/model/Signature.ts b/packages/signer/signer-aleo/src/api/model/Signature.ts new file mode 100644 index 000000000..d534ab5c2 --- /dev/null +++ b/packages/signer/signer-aleo/src/api/model/Signature.ts @@ -0,0 +1,6 @@ +export type Signature = { + r: string; + s: string; + v?: number; + // Adjust based on your blockchain's signature format +}; diff --git a/packages/signer/signer-aleo/src/api/model/TransactionOptions.ts b/packages/signer/signer-aleo/src/api/model/TransactionOptions.ts new file mode 100644 index 000000000..f7ae2dc93 --- /dev/null +++ b/packages/signer/signer-aleo/src/api/model/TransactionOptions.ts @@ -0,0 +1,4 @@ +export type TransactionOptions = { + skipOpenApp?: boolean; + // Add other options as needed +}; diff --git a/packages/signer/signer-aleo/src/index.ts b/packages/signer/signer-aleo/src/index.ts new file mode 100644 index 000000000..d1f800da1 --- /dev/null +++ b/packages/signer/signer-aleo/src/index.ts @@ -0,0 +1,4 @@ +// inversify requirement +import "reflect-metadata"; + +export * from "@api/index"; diff --git a/packages/signer/signer-aleo/src/internal/DefaultSignerAleo.ts b/packages/signer/signer-aleo/src/internal/DefaultSignerAleo.ts new file mode 100644 index 000000000..f983c4be3 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/DefaultSignerAleo.ts @@ -0,0 +1,69 @@ +import { + type DeviceManagementKit, + type DeviceSessionId, +} from "@ledgerhq/device-management-kit"; +import { type Container } from "inversify"; + +import { type GetAddressDAReturnType } from "@api/app-binder/GetAddressDeviceActionTypes"; +import { type GetAppConfigDAReturnType } from "@api/app-binder/GetAppConfigDeviceActionTypes"; +import { type SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionTypes"; +import { type SignTransactionDAReturnType } from "@api/app-binder/SignTransactionDeviceActionTypes"; +import { type AddressOptions } from "@api/model/AddressOptions"; +import { type TransactionOptions } from "@api/model/TransactionOptions"; +import { type SignerAleo } from "@api/SignerAleo"; +import { makeContainer } from "@internal/di"; +import { addressTypes } from "@internal/use-cases/address/di/addressTypes"; +import { type GetAddressUseCase } from "@internal/use-cases/address/GetAddressUseCase"; +import { configTypes } from "@internal/use-cases/config/di/configTypes"; +import { type GetAppConfigUseCase } from "@internal/use-cases/config/GetAppConfigUseCase"; +import { messageTypes } from "@internal/use-cases/message/di/messageTypes"; +import { type SignMessageUseCase } from "@internal/use-cases/message/SignMessageUseCase"; +import { transactionTypes } from "@internal/use-cases/transaction/di/transactionTypes"; +import { type SignTransactionUseCase } from "@internal/use-cases/transaction/SignTransactionUseCase"; + +type DefaultSignerAleoConstructorArgs = { + dmk: DeviceManagementKit; + sessionId: DeviceSessionId; +}; + +export class DefaultSignerAleo implements SignerAleo { + private readonly _container: Container; + + constructor({ dmk, sessionId }: DefaultSignerAleoConstructorArgs) { + this._container = makeContainer({ dmk, sessionId }); + } + + getAppConfig(): GetAppConfigDAReturnType { + return this._container + .get(configTypes.GetAppConfigUseCase) + .execute(); + } + + getAddress( + derivationPath: string, + options?: AddressOptions, + ): GetAddressDAReturnType { + return this._container + .get(addressTypes.GetAddressUseCase) + .execute(derivationPath, options); + } + + signTransaction( + derivationPath: string, + transaction: Uint8Array, + options?: TransactionOptions, + ): SignTransactionDAReturnType { + return this._container + .get(transactionTypes.SignTransactionUseCase) + .execute(derivationPath, transaction, options); + } + + signMessage( + derivationPath: string, + message: string | Uint8Array, + ): SignMessageDAReturnType { + return this._container + .get(messageTypes.SignMessageUseCase) + .execute(derivationPath, message); + } +} diff --git a/packages/signer/signer-aleo/src/internal/app-binder/AleoAppBinder.ts b/packages/signer/signer-aleo/src/internal/app-binder/AleoAppBinder.ts new file mode 100644 index 000000000..70ed2ef5f --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/AleoAppBinder.ts @@ -0,0 +1,98 @@ +import { + CallTaskInAppDeviceAction, + type DeviceManagementKit, + type DeviceSessionId, + SendCommandInAppDeviceAction, + UserInteractionRequired, +} from "@ledgerhq/device-management-kit"; +import { inject, injectable } from "inversify"; + +import { type GetAddressDAReturnType } from "@api/app-binder/GetAddressDeviceActionTypes"; +import { type GetAppConfigDAReturnType } from "@api/app-binder/GetAppConfigDeviceActionTypes"; +import { type SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionTypes"; +import { type SignTransactionDAReturnType } from "@api/app-binder/SignTransactionDeviceActionTypes"; +import { externalTypes } from "@internal/externalTypes"; + +import { GetAddressCommand } from "./command/GetAddressCommand"; +import { GetAppConfigCommand } from "./command/GetAppConfigCommand"; +import { SignMessageCommand } from "./command/SignMessageCommand"; +import { SignTransactionTask } from "./task/SignTransactionTask"; + +@injectable() +export class AleoAppBinder { + constructor( + @inject(externalTypes.Dmk) private dmk: DeviceManagementKit, + @inject(externalTypes.SessionId) private sessionId: DeviceSessionId, + ) {} + + getAppConfig(args: { skipOpenApp: boolean }): GetAppConfigDAReturnType { + return this.dmk.executeDeviceAction({ + sessionId: this.sessionId, + deviceAction: new SendCommandInAppDeviceAction({ + input: { + command: new GetAppConfigCommand(), + appName: "Aleo", + requiredUserInteraction: UserInteractionRequired.None, + skipOpenApp: args.skipOpenApp, + }, + }), + }); + } + + getAddress(args: { + derivationPath: string; + checkOnDevice: boolean; + skipOpenApp: boolean; + }): GetAddressDAReturnType { + return this.dmk.executeDeviceAction({ + sessionId: this.sessionId, + deviceAction: new SendCommandInAppDeviceAction({ + input: { + command: new GetAddressCommand(args), + appName: "Aleo", + requiredUserInteraction: args.checkOnDevice + ? UserInteractionRequired.VerifyAddress + : UserInteractionRequired.None, + skipOpenApp: args.skipOpenApp, + }, + }), + }); + } + + signTransaction(args: { + derivationPath: string; + transaction: Uint8Array; + skipOpenApp?: boolean; + }): SignTransactionDAReturnType { + return this.dmk.executeDeviceAction({ + sessionId: this.sessionId, + deviceAction: new CallTaskInAppDeviceAction({ + input: { + task: async (internalApi) => + new SignTransactionTask(internalApi, args).run(), + appName: "Aleo", + requiredUserInteraction: UserInteractionRequired.SignTransaction, + skipOpenApp: args.skipOpenApp ?? false, + }, + }), + }); + } + + signMessage(args: { + derivationPath: string; + message: string | Uint8Array; + skipOpenApp: boolean; + }): SignMessageDAReturnType { + return this.dmk.executeDeviceAction({ + sessionId: this.sessionId, + deviceAction: new SendCommandInAppDeviceAction({ + input: { + command: new SignMessageCommand(args), + appName: "Aleo", + requiredUserInteraction: UserInteractionRequired.SignPersonalMessage, + skipOpenApp: args.skipOpenApp, + }, + }), + }); + } +} diff --git a/packages/signer/signer-aleo/src/internal/app-binder/command/GetAddressCommand.ts b/packages/signer/signer-aleo/src/internal/app-binder/command/GetAddressCommand.ts new file mode 100644 index 000000000..ab39763e5 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/command/GetAddressCommand.ts @@ -0,0 +1,43 @@ +import { + type Apdu, + type ApduResponse, + type Command, + type CommandResult, +} from "@ledgerhq/device-management-kit"; + +import { type AleoErrorCodes } from "./utils/aleoApplicationErrors"; + +export type GetAddressCommandArgs = { + readonly derivationPath: string; + readonly checkOnDevice?: boolean; +}; + +export type GetAddressCommandResponse = { + readonly publicKey: Uint8Array; + readonly chainCode?: Uint8Array; +}; + +export class GetAddressCommand + implements + Command +{ + readonly name = "GetAddress"; + + private readonly args: GetAddressCommandArgs; + + constructor(args: GetAddressCommandArgs) { + this.args = args; + } + + getApdu(): Apdu { + throw new Error( + `GetAddressCommand.getApdu() not implemented (args: ${JSON.stringify(this.args)})`, + ); + } + + parseResponse( + _apduResponse: ApduResponse, + ): CommandResult { + throw new Error("GetAddressCommand.parseResponse() not implemented"); + } +} diff --git a/packages/signer/signer-aleo/src/internal/app-binder/command/GetAppConfigCommand.test.ts b/packages/signer/signer-aleo/src/internal/app-binder/command/GetAppConfigCommand.test.ts new file mode 100644 index 000000000..061892121 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/command/GetAppConfigCommand.test.ts @@ -0,0 +1,74 @@ +import { + ApduResponse, + CommandResultFactory, + isSuccessCommandResult, +} from "@ledgerhq/device-management-kit"; + +import { GetAppConfigCommand } from "@internal/app-binder/command/GetAppConfigCommand"; + +const GET_APP_CONFIG_APDU = new Uint8Array([0xe0, 0x03, 0x00, 0x00, 0x00]); + +const GET_APP_CONFIG_RESPONSE_DATA = new Uint8Array([0x00, 0x01, 0x02]); + +const GET_APP_CONFIG_RESPONSE = new ApduResponse({ + statusCode: Uint8Array.from([0x90, 0x00]), + data: GET_APP_CONFIG_RESPONSE_DATA, +}); + +describe("GetAppConfigurationCommand", () => { + let command: GetAppConfigCommand; + + beforeEach(() => { + command = new GetAppConfigCommand(); + vi.clearAllMocks(); + vi.importActual("@ledgerhq/device-management-kit"); + }); + + describe("name", () => { + it("should be 'getAppConfiguration'", () => { + expect(command.name).toBe("GetAppConfig"); + }); + }); + + describe("getApdu", () => { + it("should return the correct APDU", () => { + const apdu = command.getApdu(); + expect(apdu.getRawApdu()).toEqual(GET_APP_CONFIG_APDU); + }); + }); + + describe("parseResponse", () => { + it("should parse the response correctly", () => { + const parsed = command.parseResponse(GET_APP_CONFIG_RESPONSE); + expect(parsed).toStrictEqual( + CommandResultFactory({ + data: { + version: "0.1.2", + }, + }), + ); + }); + + describe("error handling", () => { + it("should return error if response is not success", () => { + const response = new ApduResponse({ + statusCode: Uint8Array.from([0x6a, 0x86]), + data: new Uint8Array(0), + }); + const result = command.parseResponse(response); + expect(isSuccessCommandResult(result)).toBe(false); + if (!isSuccessCommandResult(result)) { + expect(result.error).toEqual( + expect.objectContaining({ + _tag: "AleoAppCommandError", + errorCode: "6a86", + message: "Incorrect P1 or P2", + }), + ); + } else { + assert.fail("Expected error"); + } + }); + }); + }); +}); diff --git a/packages/signer/signer-aleo/src/internal/app-binder/command/GetAppConfigCommand.ts b/packages/signer/signer-aleo/src/internal/app-binder/command/GetAppConfigCommand.ts new file mode 100644 index 000000000..8c9138aaf --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/command/GetAppConfigCommand.ts @@ -0,0 +1,72 @@ +import { + type Apdu, + ApduBuilder, + type ApduBuilderArgs, + ApduParser, + type ApduResponse, + type Command, + type CommandResult, + CommandResultFactory, + InvalidStatusWordError, +} from "@ledgerhq/device-management-kit"; +import { CommandErrorHelper } from "@ledgerhq/signer-utils"; +import { Maybe } from "purify-ts"; + +import { type AppConfig } from "@api/model/AppConfig"; + +import { + ALEO_APP_ERRORS, + AleoAppCommandErrorFactory, + type AleoErrorCodes, +} from "./utils/aleoApplicationErrors"; + +const EXPECTED_DATA_LENGTH = 3; + +export class GetAppConfigCommand + implements Command +{ + readonly name = "GetAppConfig"; + private readonly errorHelper = new CommandErrorHelper< + AppConfig, + AleoErrorCodes + >(ALEO_APP_ERRORS, AleoAppCommandErrorFactory); + + getApdu(): Apdu { + const getEthConfigArgs: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x03, + p1: 0x00, + p2: 0x00, + }; + const builder = new ApduBuilder(getEthConfigArgs); + return builder.build(); + } + + parseResponse( + response: ApduResponse, + ): CommandResult { + return Maybe.fromNullable( + this.errorHelper.getError(response), + ).orDefaultLazy(() => { + const parser = new ApduParser(response); + const buffer = parser.extractFieldByLength(EXPECTED_DATA_LENGTH); + if ( + !buffer || + buffer.length !== EXPECTED_DATA_LENGTH || + buffer.some((element) => element === undefined) + ) { + return CommandResultFactory({ + error: new InvalidStatusWordError("Invalid response"), + }); + } + + const config: AppConfig = { + version: `${buffer[0]}.${buffer[1]}.${buffer[2]}`, + }; + + return CommandResultFactory({ + data: config, + }); + }); + } +} diff --git a/packages/signer/signer-aleo/src/internal/app-binder/command/SignMessageCommand.ts b/packages/signer/signer-aleo/src/internal/app-binder/command/SignMessageCommand.ts new file mode 100644 index 000000000..a9b86e0b4 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/command/SignMessageCommand.ts @@ -0,0 +1,42 @@ +import { + type Apdu, + type ApduResponse, + type Command, + type CommandResult, +} from "@ledgerhq/device-management-kit"; + +import { type Signature } from "@api/model/Signature"; + +import { type AleoErrorCodes } from "./utils/aleoApplicationErrors"; + +export type SignMessageCommandArgs = { + derivationPath: string; + message: string | Uint8Array; +}; + +export type SignMessageCommandResponse = Signature; + +export class SignMessageCommand + implements + Command +{ + readonly name = "SignMessage"; + + private readonly args: SignMessageCommandArgs; + + constructor(args: SignMessageCommandArgs) { + this.args = args; + } + + getApdu(): Apdu { + throw new Error( + `SignMessageCommand.getApdu() not implemented (args: ${JSON.stringify(this.args)})`, + ); + } + + parseResponse( + _apduResponse: ApduResponse, + ): CommandResult { + throw new Error("SignMessageCommand.parseResponse() not implemented"); + } +} diff --git a/packages/signer/signer-aleo/src/internal/app-binder/command/SignTransactionCommand.ts b/packages/signer/signer-aleo/src/internal/app-binder/command/SignTransactionCommand.ts new file mode 100644 index 000000000..0b5ad8b5f --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/command/SignTransactionCommand.ts @@ -0,0 +1,50 @@ +import { + type Apdu, + type ApduResponse, + type Command, + type CommandResult, +} from "@ledgerhq/device-management-kit"; + +import { type AleoErrorCodes } from "./utils/aleoApplicationErrors"; + +export type SignTransactionCommandArgs = { + derivationPath: string; + transaction: Uint8Array; +}; + +export type SignTransactionCommandResponse = { + signature: { + r: string; + s: string; + v?: number; + }; +}; + +export class SignTransactionCommand + implements + Command< + SignTransactionCommandResponse, + SignTransactionCommandArgs, + AleoErrorCodes + > +{ + readonly name = "SignTransaction"; + + private readonly args: SignTransactionCommandArgs; + + constructor(args: SignTransactionCommandArgs) { + this.args = args; + } + + getApdu(): Apdu { + throw new Error( + `SignTransactionCommand.getApdu() not implemented (args: ${JSON.stringify(this.args)})`, + ); + } + + parseResponse( + _apduResponse: ApduResponse, + ): CommandResult { + throw new Error("SignTransactionCommand.parseResponse() not implemented"); + } +} diff --git a/packages/signer/signer-aleo/src/internal/app-binder/command/utils/aleoApplicationErrors.test.ts b/packages/signer/signer-aleo/src/internal/app-binder/command/utils/aleoApplicationErrors.test.ts new file mode 100644 index 000000000..e6b9e23e5 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/command/utils/aleoApplicationErrors.test.ts @@ -0,0 +1,90 @@ +import { DeviceExchangeError } from "@ledgerhq/device-management-kit"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; + +import { + ALEO_APP_ERRORS, + AleoAppCommandError, + AleoAppCommandErrorFactory, + type AleoErrorCodes, +} from "./aleoApplicationErrors"; + +describe("AleoAppCommandError", () => { + afterEach(() => { + vi.resetAllMocks(); + }); + + afterAll(() => { + vi.resetModules(); + }); + + it("should be an instance of DeviceExchangeError", () => { + const error = new AleoAppCommandError({ + message: "Test error message", + errorCode: "6985", + }); + + expect(error).toBeInstanceOf(DeviceExchangeError); + }); + + it("should set the correct message when provided", () => { + const customMessage = "Custom error message"; + const error = new AleoAppCommandError({ + message: customMessage, + errorCode: "6985", + }); + + expect(error.message).toBe(customMessage); + }); + + it("should set the correct customErrorCode", () => { + const errorCode: AleoErrorCodes = "6a86"; + const error = new AleoAppCommandError({ + message: "Incorrect P1 or P2", + errorCode, + }); + + expect(error.errorCode).toBe(errorCode); + }); + + it("should correlate error codes with messages from aleoApplicationErrors", () => { + const errorCode: AleoErrorCodes = "6e00"; + const expectedMessage = ALEO_APP_ERRORS[errorCode].message; + + const error = new AleoAppCommandError({ + message: expectedMessage, + errorCode, + }); + + expect(error.errorCode).toBe(errorCode); + expect(error.message).toBe(expectedMessage); + + expect(error).toBeInstanceOf(DeviceExchangeError); + }); + + it("should handle unknown error codes gracefully", () => { + const unknownErrorCode = "9999" as AleoErrorCodes; + const customMessage = "Unknown error occurred"; + + const error = new AleoAppCommandError({ + message: customMessage, + errorCode: unknownErrorCode, + }); + + expect(error.errorCode).toBe(unknownErrorCode); + expect(error.message).toBe(customMessage); + expect(error).toBeInstanceOf(DeviceExchangeError); + }); + + describe("AleoAppCommandErrorFactory", () => { + it("should create an instance of AleoAppCommandError", () => { + const error = AleoAppCommandErrorFactory({ + message: "Test error message", + errorCode: "6985", + }); + + expect(error).toBeInstanceOf(AleoAppCommandError); + expect(error.message).toBe("Test error message"); + expect(error.errorCode).toBe("6985"); + }); + }); +}); diff --git a/packages/signer/signer-aleo/src/internal/app-binder/command/utils/aleoApplicationErrors.ts b/packages/signer/signer-aleo/src/internal/app-binder/command/utils/aleoApplicationErrors.ts new file mode 100644 index 000000000..acc425277 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/command/utils/aleoApplicationErrors.ts @@ -0,0 +1,50 @@ +import { + type CommandErrorArgs, + type CommandErrors, + DeviceExchangeError, +} from "@ledgerhq/device-management-kit"; + +export type AleoErrorCodes = + | "6985" + | "6a86" + | "6a87" + | "6d00" + | "6e00" + | "b000" + | "b001" + | "b002" + | "b003" + | "b004" + | "b005" + | "b006" + | "b007" + | "b008" + | "c000"; + +export const ALEO_APP_ERRORS: CommandErrors = { + "6985": { message: "Denied by user" }, + "6a86": { message: "Incorrect P1 or P2" }, + "6a87": { message: "Wrong LC or length of APDU command less than 5" }, + "6d00": { message: "Unknown command with this INS" }, + "6e00": { message: "Instruction class is different than CLA" }, + b000: { message: "Wrong response length (buffer too small or too big)" }, + b001: { message: "Fail to display BIP32 path" }, + b002: { message: "Fail to display address" }, + b003: { message: "Fail to display amount" }, + b004: { message: "Wrong transaction length" }, + b005: { message: "Fail of transaction parsing" }, + b006: { message: "Fail of transaction hash" }, + b007: { message: "Bad state" }, + b008: { message: "Signature fail" }, + c000: { message: "Swap failure" }, +}; + +export class AleoAppCommandError extends DeviceExchangeError { + constructor(args: CommandErrorArgs) { + super({ tag: "AleoAppCommandError", ...args }); + } +} + +export const AleoAppCommandErrorFactory = ( + args: CommandErrorArgs, +) => new AleoAppCommandError(args); diff --git a/packages/signer/signer-aleo/src/internal/app-binder/device-action/GetAddress/GetAddressDeviceAction.ts b/packages/signer/signer-aleo/src/internal/app-binder/device-action/GetAddress/GetAddressDeviceAction.ts new file mode 100644 index 000000000..580962828 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/device-action/GetAddress/GetAddressDeviceAction.ts @@ -0,0 +1,3 @@ +// TODO: Implement GetAddressDeviceAction if needed +// This is a placeholder - you may not need a custom device action for getAddress +// if SendCommandInAppDeviceAction is sufficient diff --git a/packages/signer/signer-aleo/src/internal/app-binder/device-action/GetAppConfig/GetAppConfigDeviceAction.ts b/packages/signer/signer-aleo/src/internal/app-binder/device-action/GetAppConfig/GetAppConfigDeviceAction.ts new file mode 100644 index 000000000..2dd21ceaf --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/device-action/GetAppConfig/GetAppConfigDeviceAction.ts @@ -0,0 +1,3 @@ +// TODO: Implement GetAppConfigDeviceAction if needed +// This is a placeholder - you may not need a custom device action for getAppConfig +// if SendCommandInAppDeviceAction is sufficient diff --git a/packages/signer/signer-aleo/src/internal/app-binder/device-action/SignMessage/SignMessageDeviceAction.ts b/packages/signer/signer-aleo/src/internal/app-binder/device-action/SignMessage/SignMessageDeviceAction.ts new file mode 100644 index 000000000..2251573e0 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/device-action/SignMessage/SignMessageDeviceAction.ts @@ -0,0 +1,3 @@ +// TODO: Implement SignMessageDeviceAction if needed +// This is a placeholder - you may not need a custom device action for signMessage +// if SendCommandInAppDeviceAction is sufficient diff --git a/packages/signer/signer-aleo/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.ts b/packages/signer/signer-aleo/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.ts new file mode 100644 index 000000000..2dcd298dd --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.ts @@ -0,0 +1,3 @@ +// TODO: Implement SignTransactionDeviceAction if needed +// This is a placeholder - you may not need a custom device action for signTransaction +// if CallTaskInAppDeviceAction with SignTransactionTask is sufficient diff --git a/packages/signer/signer-aleo/src/internal/app-binder/di/appBinderModule.ts b/packages/signer/signer-aleo/src/internal/app-binder/di/appBinderModule.ts new file mode 100644 index 000000000..66f0bc080 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/di/appBinderModule.ts @@ -0,0 +1,9 @@ +import { ContainerModule } from "inversify"; + +import { AleoAppBinder } from "@internal/app-binder/AleoAppBinder"; +import { appBinderTypes } from "@internal/app-binder/di/appBinderTypes"; + +export const appBindingModuleFactory = () => + new ContainerModule(({ bind }) => { + bind(appBinderTypes.AppBinding).to(AleoAppBinder); + }); diff --git a/packages/signer/signer-aleo/src/internal/app-binder/di/appBinderTypes.ts b/packages/signer/signer-aleo/src/internal/app-binder/di/appBinderTypes.ts new file mode 100644 index 000000000..565f9585a --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/di/appBinderTypes.ts @@ -0,0 +1,3 @@ +export const appBinderTypes = { + AppBinding: Symbol.for("AppBinding"), +} as const; diff --git a/packages/signer/signer-aleo/src/internal/app-binder/task/SignTransactionTask.ts b/packages/signer/signer-aleo/src/internal/app-binder/task/SignTransactionTask.ts new file mode 100644 index 000000000..4a5a786ad --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/app-binder/task/SignTransactionTask.ts @@ -0,0 +1,45 @@ +import { + type CommandResult, + CommandResultFactory, + type InternalApi, + isSuccessCommandResult, +} from "@ledgerhq/device-management-kit"; + +import { type Signature } from "@api/model/Signature"; +import { SignTransactionCommand } from "@internal/app-binder/command/SignTransactionCommand"; +import { type AleoErrorCodes } from "@internal/app-binder/command/utils/aleoApplicationErrors"; + +type SignTransactionTaskArgs = { + derivationPath: string; + transaction: Uint8Array; +}; + +export class SignTransactionTask { + constructor( + private api: InternalApi, + private args: SignTransactionTaskArgs, + ) {} + + async run(): Promise> { + // TODO: Adapt this implementation to your blockchain's signing protocol + // For transactions larger than a single APDU, you may need to: + // 1. Split the transaction into chunks + // 2. Send each chunk with appropriate first/continue flags + // 3. Collect the final signature from the last response + + const result = await this.api.sendCommand( + new SignTransactionCommand({ + derivationPath: this.args.derivationPath, + transaction: this.args.transaction, + }), + ); + + if (!isSuccessCommandResult(result)) { + return result; + } + + return CommandResultFactory({ + data: result.data.signature, + }); + } +} diff --git a/packages/signer/signer-aleo/src/internal/di.ts b/packages/signer/signer-aleo/src/internal/di.ts new file mode 100644 index 000000000..6e00e871f --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/di.ts @@ -0,0 +1,36 @@ +import { + type DeviceManagementKit, + type DeviceSessionId, +} from "@ledgerhq/device-management-kit"; +import { Container } from "inversify"; + +import { appBindingModuleFactory } from "@internal/app-binder/di/appBinderModule"; +import { externalTypes } from "@internal/externalTypes"; +import { addressModuleFactory } from "@internal/use-cases/address/di/addressModule"; +import { configModuleFactory } from "@internal/use-cases/config/di/configModule"; +import { messageModuleFactory } from "@internal/use-cases/message/di/messageModule"; +import { transactionModuleFactory } from "@internal/use-cases/transaction/di/transactionModule"; + +type MakeContainerProps = { + dmk: DeviceManagementKit; + sessionId: DeviceSessionId; +}; + +export const makeContainer = ({ dmk, sessionId }: MakeContainerProps) => { + const container = new Container(); + + container.bind(externalTypes.Dmk).toConstantValue(dmk); + container + .bind(externalTypes.SessionId) + .toConstantValue(sessionId); + + container.loadSync( + appBindingModuleFactory(), + configModuleFactory(), + addressModuleFactory(), + transactionModuleFactory(), + messageModuleFactory(), + ); + + return container; +}; diff --git a/packages/signer/signer-aleo/src/internal/externalTypes.ts b/packages/signer/signer-aleo/src/internal/externalTypes.ts new file mode 100644 index 000000000..6ccea36a4 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/externalTypes.ts @@ -0,0 +1,5 @@ +export const externalTypes = { + Dmk: Symbol.for("Dmk"), + SessionId: Symbol.for("SessionId"), + ContextModule: Symbol.for("ContextModule"), +} as const; diff --git a/packages/signer/signer-aleo/src/internal/use-cases/address/GetAddressUseCase.ts b/packages/signer/signer-aleo/src/internal/use-cases/address/GetAddressUseCase.ts new file mode 100644 index 000000000..af32ca63e --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/use-cases/address/GetAddressUseCase.ts @@ -0,0 +1,26 @@ +import { inject, injectable } from "inversify"; + +import { type GetAddressDAReturnType } from "@api/app-binder/GetAddressDeviceActionTypes"; +import { type AddressOptions } from "@api/model/AddressOptions"; +import { AleoAppBinder } from "@internal/app-binder/AleoAppBinder"; +import { appBinderTypes } from "@internal/app-binder/di/appBinderTypes"; + +@injectable() +export class GetAddressUseCase { + private readonly _appBinder: AleoAppBinder; + + constructor(@inject(appBinderTypes.AppBinding) appBinder: AleoAppBinder) { + this._appBinder = appBinder; + } + + execute( + derivationPath: string, + options?: AddressOptions, + ): GetAddressDAReturnType { + return this._appBinder.getAddress({ + derivationPath, + checkOnDevice: options?.checkOnDevice ?? false, + skipOpenApp: options?.skipOpenApp ?? false, + }); + } +} diff --git a/packages/signer/signer-aleo/src/internal/use-cases/address/di/addressModule.ts b/packages/signer/signer-aleo/src/internal/use-cases/address/di/addressModule.ts new file mode 100644 index 000000000..9d769f0f7 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/use-cases/address/di/addressModule.ts @@ -0,0 +1,9 @@ +import { ContainerModule } from "inversify"; + +import { addressTypes } from "@internal/use-cases/address/di/addressTypes"; +import { GetAddressUseCase } from "@internal/use-cases/address/GetAddressUseCase"; + +export const addressModuleFactory = () => + new ContainerModule(({ bind }) => { + bind(addressTypes.GetAddressUseCase).to(GetAddressUseCase); + }); diff --git a/packages/signer/signer-aleo/src/internal/use-cases/address/di/addressTypes.ts b/packages/signer/signer-aleo/src/internal/use-cases/address/di/addressTypes.ts new file mode 100644 index 000000000..671b42967 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/use-cases/address/di/addressTypes.ts @@ -0,0 +1,3 @@ +export const addressTypes = { + GetAddressUseCase: Symbol.for("GetAddressUseCase"), +} as const; diff --git a/packages/signer/signer-aleo/src/internal/use-cases/config/GetAppConfigUseCase.ts b/packages/signer/signer-aleo/src/internal/use-cases/config/GetAppConfigUseCase.ts new file mode 100644 index 000000000..8e75af790 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/use-cases/config/GetAppConfigUseCase.ts @@ -0,0 +1,20 @@ +import { inject, injectable } from "inversify"; + +import { type GetAppConfigDAReturnType } from "@api/app-binder/GetAppConfigDeviceActionTypes"; +import { AleoAppBinder } from "@internal/app-binder/AleoAppBinder"; +import { appBinderTypes } from "@internal/app-binder/di/appBinderTypes"; + +@injectable() +export class GetAppConfigUseCase { + private readonly _appBinder: AleoAppBinder; + + constructor(@inject(appBinderTypes.AppBinding) appBinder: AleoAppBinder) { + this._appBinder = appBinder; + } + + execute(): GetAppConfigDAReturnType { + return this._appBinder.getAppConfig({ + skipOpenApp: false, + }); + } +} diff --git a/packages/signer/signer-aleo/src/internal/use-cases/config/di/configModule.ts b/packages/signer/signer-aleo/src/internal/use-cases/config/di/configModule.ts new file mode 100644 index 000000000..9ea31d222 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/use-cases/config/di/configModule.ts @@ -0,0 +1,9 @@ +import { ContainerModule } from "inversify"; + +import { configTypes } from "@internal/use-cases/config/di/configTypes"; +import { GetAppConfigUseCase } from "@internal/use-cases/config/GetAppConfigUseCase"; + +export const configModuleFactory = () => + new ContainerModule(({ bind }) => { + bind(configTypes.GetAppConfigUseCase).to(GetAppConfigUseCase); + }); diff --git a/packages/signer/signer-aleo/src/internal/use-cases/config/di/configTypes.ts b/packages/signer/signer-aleo/src/internal/use-cases/config/di/configTypes.ts new file mode 100644 index 000000000..1f588c1fb --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/use-cases/config/di/configTypes.ts @@ -0,0 +1,3 @@ +export const configTypes = { + GetAppConfigUseCase: Symbol.for("GetAppConfigUseCase"), +} as const; diff --git a/packages/signer/signer-aleo/src/internal/use-cases/message/SignMessageUseCase.ts b/packages/signer/signer-aleo/src/internal/use-cases/message/SignMessageUseCase.ts new file mode 100644 index 000000000..fbe17f20a --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/use-cases/message/SignMessageUseCase.ts @@ -0,0 +1,25 @@ +import { inject, injectable } from "inversify"; + +import { type SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionTypes"; +import { AleoAppBinder } from "@internal/app-binder/AleoAppBinder"; +import { appBinderTypes } from "@internal/app-binder/di/appBinderTypes"; + +@injectable() +export class SignMessageUseCase { + private readonly _appBinder: AleoAppBinder; + + constructor(@inject(appBinderTypes.AppBinding) appBinder: AleoAppBinder) { + this._appBinder = appBinder; + } + + execute( + derivationPath: string, + message: string | Uint8Array, + ): SignMessageDAReturnType { + return this._appBinder.signMessage({ + derivationPath, + message, + skipOpenApp: false, + }); + } +} diff --git a/packages/signer/signer-aleo/src/internal/use-cases/message/di/messageModule.ts b/packages/signer/signer-aleo/src/internal/use-cases/message/di/messageModule.ts new file mode 100644 index 000000000..46de75518 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/use-cases/message/di/messageModule.ts @@ -0,0 +1,9 @@ +import { ContainerModule } from "inversify"; + +import { messageTypes } from "@internal/use-cases/message/di/messageTypes"; +import { SignMessageUseCase } from "@internal/use-cases/message/SignMessageUseCase"; + +export const messageModuleFactory = () => + new ContainerModule(({ bind }) => { + bind(messageTypes.SignMessageUseCase).to(SignMessageUseCase); + }); diff --git a/packages/signer/signer-aleo/src/internal/use-cases/message/di/messageTypes.ts b/packages/signer/signer-aleo/src/internal/use-cases/message/di/messageTypes.ts new file mode 100644 index 000000000..04dc51d7b --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/use-cases/message/di/messageTypes.ts @@ -0,0 +1,3 @@ +export const messageTypes = { + SignMessageUseCase: Symbol.for("SignMessageUseCase"), +} as const; diff --git a/packages/signer/signer-aleo/src/internal/use-cases/transaction/SignTransactionUseCase.ts b/packages/signer/signer-aleo/src/internal/use-cases/transaction/SignTransactionUseCase.ts new file mode 100644 index 000000000..7e81c1fb2 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/use-cases/transaction/SignTransactionUseCase.ts @@ -0,0 +1,27 @@ +import { inject, injectable } from "inversify"; + +import { type SignTransactionDAReturnType } from "@api/app-binder/SignTransactionDeviceActionTypes"; +import { type TransactionOptions } from "@api/model/TransactionOptions"; +import { AleoAppBinder } from "@internal/app-binder/AleoAppBinder"; +import { appBinderTypes } from "@internal/app-binder/di/appBinderTypes"; + +@injectable() +export class SignTransactionUseCase { + private readonly _appBinder: AleoAppBinder; + + constructor(@inject(appBinderTypes.AppBinding) appBinder: AleoAppBinder) { + this._appBinder = appBinder; + } + + execute( + derivationPath: string, + transaction: Uint8Array, + options?: TransactionOptions, + ): SignTransactionDAReturnType { + return this._appBinder.signTransaction({ + derivationPath, + transaction, + skipOpenApp: options?.skipOpenApp, + }); + } +} diff --git a/packages/signer/signer-aleo/src/internal/use-cases/transaction/di/transactionModule.ts b/packages/signer/signer-aleo/src/internal/use-cases/transaction/di/transactionModule.ts new file mode 100644 index 000000000..a749f88d8 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/use-cases/transaction/di/transactionModule.ts @@ -0,0 +1,9 @@ +import { ContainerModule } from "inversify"; + +import { transactionTypes } from "@internal/use-cases/transaction/di/transactionTypes"; +import { SignTransactionUseCase } from "@internal/use-cases/transaction/SignTransactionUseCase"; + +export const transactionModuleFactory = () => + new ContainerModule(({ bind }) => { + bind(transactionTypes.SignTransactionUseCase).to(SignTransactionUseCase); + }); diff --git a/packages/signer/signer-aleo/src/internal/use-cases/transaction/di/transactionTypes.ts b/packages/signer/signer-aleo/src/internal/use-cases/transaction/di/transactionTypes.ts new file mode 100644 index 000000000..641d0e950 --- /dev/null +++ b/packages/signer/signer-aleo/src/internal/use-cases/transaction/di/transactionTypes.ts @@ -0,0 +1,3 @@ +export const transactionTypes = { + SignTransactionUseCase: Symbol.for("SignTransactionUseCase"), +} as const; diff --git a/packages/signer/signer-aleo/tsconfig.json b/packages/signer/signer-aleo/tsconfig.json new file mode 100644 index 000000000..d9a429203 --- /dev/null +++ b/packages/signer/signer-aleo/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "@ledgerhq/tsconfig-dsdk/tsconfig.sdk", + "compilerOptions": { + "baseUrl": ".", + "outDir": "./lib/types", + "module": "ES2022", + "target": "ES2022", + "moduleResolution": "bundler", + "emitDeclarationOnly": true, + "paths": { + "@api/*": ["./src/api/*"], + "@internal/*": ["./src/internal/*"], + "@root/*": ["./*"] + }, + "resolveJsonModule": true, + "types": ["vitest/globals", "node"] + }, + "include": ["src", "vitest.*.mjs"] +} diff --git a/packages/signer/signer-aleo/tsconfig.prod.json b/packages/signer/signer-aleo/tsconfig.prod.json new file mode 100644 index 000000000..b90fc83e0 --- /dev/null +++ b/packages/signer/signer-aleo/tsconfig.prod.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"] +} diff --git a/packages/signer/signer-aleo/vitest.config.mjs b/packages/signer/signer-aleo/vitest.config.mjs new file mode 100644 index 000000000..45f86746d --- /dev/null +++ b/packages/signer/signer-aleo/vitest.config.mjs @@ -0,0 +1,23 @@ +import { defineConfig } from "vitest/config"; +import baseConfig from "@ledgerhq/vitest-config-dmk"; +import path from "path"; + +export default defineConfig({ + ...baseConfig, + test: { + ...baseConfig.test, + include: ["src/**/*.test.ts"], + setupFiles: ["./vitest.setup.mjs"], + coverage: { + provider: "istanbul", + reporter: ["lcov", "text"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.stub.ts", "src/index.ts", "src/api/index.ts"], + }, + }, + resolve: { + alias: { + "@internal": path.resolve(__dirname, "src/internal"), + }, + }, +}); diff --git a/packages/signer/signer-aleo/vitest.setup.mjs b/packages/signer/signer-aleo/vitest.setup.mjs new file mode 100644 index 000000000..fded23ac1 --- /dev/null +++ b/packages/signer/signer-aleo/vitest.setup.mjs @@ -0,0 +1 @@ +import "reflect-metadata"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f51d725c8..2b21989e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -877,6 +877,9 @@ importers: '@ledgerhq/device-mockserver-client': specifier: workspace:^ version: link:../../packages/mockserver-client + '@ledgerhq/device-signer-kit-aleo': + specifier: workspace:^ + version: link:../../packages/signer/signer-aleo '@ledgerhq/device-signer-kit-bitcoin': specifier: workspace:^ version: link:../../packages/signer/signer-btc @@ -1431,6 +1434,52 @@ importers: specifier: 'catalog:' version: 10.9.2(@types/node@24.3.0)(typescript@5.9.2) + packages/signer/signer-aleo: + dependencies: + '@ledgerhq/context-module': + specifier: workspace:^ + version: link:../context-module + '@ledgerhq/signer-utils': + specifier: workspace:^ + version: link:../signer-utils + inversify: + specifier: 'catalog:' + version: 7.5.1(reflect-metadata@0.2.2) + purify-ts: + specifier: 'catalog:' + version: 2.1.0 + reflect-metadata: + specifier: 'catalog:' + version: 0.2.2 + xstate: + specifier: 'catalog:' + version: 5.19.2 + devDependencies: + '@ledgerhq/device-management-kit': + specifier: workspace:^ + version: link:../../device-management-kit + '@ledgerhq/eslint-config-dsdk': + specifier: workspace:^ + version: link:../../config/eslint + '@ledgerhq/ldmk-tool': + specifier: workspace:^ + version: link:../../tools/ldmk-tool + '@ledgerhq/prettier-config-dsdk': + specifier: workspace:^ + version: link:../../config/prettier + '@ledgerhq/tsconfig-dsdk': + specifier: workspace:^ + version: link:../../config/typescript + '@ledgerhq/vitest-config-dmk': + specifier: workspace:^ + version: link:../../config/vitest + rxjs: + specifier: 'catalog:' + version: 7.8.2 + ts-node: + specifier: 'catalog:' + version: 10.9.2(@types/node@24.3.0)(typescript@5.9.2) + packages/signer/signer-btc: dependencies: '@types/crypto-js':