From 5dd0e19d69c58034c472ff9324fd9cee9ec7cccc Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Tue, 20 Jan 2026 18:26:52 +0530 Subject: [PATCH 01/15] feat(utils): introduce bech32 and blakejs deps Signed-off-by: Prashant Rahul --- packages/util/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/util/package.json b/packages/util/package.json index ecca5109..f1875c38 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -33,6 +33,8 @@ "dependencies": { "@aws-crypto/sha256-browser": "^4.0.0", "@cypherock/sdk-interfaces": "workspace:^0.0.16", + "bech32": "^2.0.0", + "blakejs": "^1.2.1", "compare-versions": "6.0.0-rc.1", "protobufjs": "^7.2.2", "semver": "^7.5.2", From 4f216f45cdf839e99f4a573ff639fbf844a32f57 Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Tue, 20 Jan 2026 18:29:12 +0530 Subject: [PATCH 02/15] feat(utils): add convert_bits, wrappers for blake2b, bech32 Signed-off-by: Prashant Rahul --- packages/util/src/utils/crypto.ts | 69 +++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/packages/util/src/utils/crypto.ts b/packages/util/src/utils/crypto.ts index 8b902b97..d8049e2e 100644 --- a/packages/util/src/utils/crypto.ts +++ b/packages/util/src/utils/crypto.ts @@ -1,4 +1,6 @@ import { Sha256 } from '@aws-crypto/sha256-browser'; +import { blake2b as internalBlake2b } from 'blakejs'; +import { bech32 } from 'bech32'; import { assert } from './assert'; const updateCRC16 = (crcParam: number, byte: number) => { @@ -208,3 +210,70 @@ export const hexToBase64 = (hex: string): string => { const bytes = hexToUint8Array(hex); return uint8ArrayToBase64(bytes); }; + +/** + * Hash given Uint8Array using blake2b + * + * @param input input as Uint8Array or string + * @param key Optional key + * @param outlen Optional out len + */ +export const blake2b = ( + input: Uint8Array | string, + key?: Uint8Array, + outlen?: number, +): Uint8Array => internalBlake2b(input, key, outlen); + +/** + * bech32 encoding + * + * @param prefix bech32 encoding prefix + * @param input input bytes + * @param LIMIT set max LIMIT for bech32 encoding + */ +export const bech32Encode = ( + prefix: string, + input: ArrayLike, + LIMIT?: number, +): string => bech32.encode(prefix, input, LIMIT); + +/** + * Bit group conversion from fixed width `inbits` to symbols of width `outbits` + * + * @param outBits Out stream byte width + * @param inBits In stream byte width + * @param input Input stream + * @param pad Optional padding + * + */ +export const convertBits = ( + outBits: number, + inBits: number, + input: Uint8Array, + pad: boolean, +) => { + let val = 0; + let bits = 0; + const maxv = (1 << outBits) - 1; + const output = []; + + for (let i = 0; i < input.length; i += 1) { + val = (val << inBits) | input[i]; + bits += inBits; + + while (bits >= outBits) { + bits -= outBits; + output.push((val >> bits) & maxv); + } + } + + if (pad) { + if (bits > 0) { + output.push((val << (outBits - bits)) & maxv); + } + } else if (bits >= inBits || (val << (outBits - bits)) & maxv) { + return null; + } + + return output; +}; From 6c640f280d9006f330a6b74657ba06b55d020a5c Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Tue, 20 Jan 2026 18:32:37 +0530 Subject: [PATCH 03/15] feat(cardano): boilerplate for cardano app Signed-off-by: Prashant Rahul --- packages/app-cardano/.eslintrc.js | 8 ++ packages/app-cardano/.gitignore | 7 + packages/app-cardano/.prettierignore | 1 + packages/app-cardano/.prettierrc | 1 + packages/app-cardano/CHANGELOG.md | 3 + packages/app-cardano/docs/index.md | 3 + packages/app-cardano/jest.config.js | 22 +++ packages/app-cardano/jest.stryker.config.js | 21 +++ packages/app-cardano/mkdocs.yml | 1 + packages/app-cardano/package.json | 53 ++++++++ packages/app-cardano/scripts/prebuild.sh | 16 +++ packages/app-cardano/src/__mocks__/sdk.ts | 60 +++++++++ packages/app-cardano/src/app.ts | 41 ++++++ packages/app-cardano/src/constants/appId.ts | 4 + packages/app-cardano/src/index.ts | 5 + packages/app-cardano/src/operations/index.ts | 3 + packages/app-cardano/src/operations/types.ts | 2 + packages/app-cardano/src/types.ts | 2 + packages/app-cardano/src/utils/asserts.ts | 35 +++++ packages/app-cardano/src/utils/index.ts | 4 + packages/app-cardano/src/utils/logger.ts | 22 +++ .../app-cardano/src/utils/operationHelper.ts | 126 ++++++++++++++++++ packages/app-cardano/stryker.conf.mjs | 24 ++++ packages/app-cardano/tsconfig.eslint.json | 9 ++ packages/app-cardano/tsconfig.json | 15 +++ 25 files changed, 488 insertions(+) create mode 100644 packages/app-cardano/.eslintrc.js create mode 100644 packages/app-cardano/.gitignore create mode 100644 packages/app-cardano/.prettierignore create mode 100644 packages/app-cardano/.prettierrc create mode 100644 packages/app-cardano/CHANGELOG.md create mode 100644 packages/app-cardano/docs/index.md create mode 100644 packages/app-cardano/jest.config.js create mode 100644 packages/app-cardano/jest.stryker.config.js create mode 100644 packages/app-cardano/mkdocs.yml create mode 100644 packages/app-cardano/package.json create mode 100755 packages/app-cardano/scripts/prebuild.sh create mode 100644 packages/app-cardano/src/__mocks__/sdk.ts create mode 100644 packages/app-cardano/src/app.ts create mode 100644 packages/app-cardano/src/constants/appId.ts create mode 100644 packages/app-cardano/src/index.ts create mode 100644 packages/app-cardano/src/operations/index.ts create mode 100644 packages/app-cardano/src/operations/types.ts create mode 100644 packages/app-cardano/src/types.ts create mode 100644 packages/app-cardano/src/utils/asserts.ts create mode 100644 packages/app-cardano/src/utils/index.ts create mode 100644 packages/app-cardano/src/utils/logger.ts create mode 100644 packages/app-cardano/src/utils/operationHelper.ts create mode 100644 packages/app-cardano/stryker.conf.mjs create mode 100644 packages/app-cardano/tsconfig.eslint.json create mode 100644 packages/app-cardano/tsconfig.json diff --git a/packages/app-cardano/.eslintrc.js b/packages/app-cardano/.eslintrc.js new file mode 100644 index 00000000..8754a051 --- /dev/null +++ b/packages/app-cardano/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + root: true, + extends: ['@cypherock/eslint-config'], + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.eslint.json'], + }, +}; diff --git a/packages/app-cardano/.gitignore b/packages/app-cardano/.gitignore new file mode 100644 index 00000000..ddd43361 --- /dev/null +++ b/packages/app-cardano/.gitignore @@ -0,0 +1,7 @@ +.turbo +dist + +# stryker temp files +.stryker-tmp +reports +src/proto \ No newline at end of file diff --git a/packages/app-cardano/.prettierignore b/packages/app-cardano/.prettierignore new file mode 100644 index 00000000..8048b417 --- /dev/null +++ b/packages/app-cardano/.prettierignore @@ -0,0 +1 @@ +src/proto/generated diff --git a/packages/app-cardano/.prettierrc b/packages/app-cardano/.prettierrc new file mode 100644 index 00000000..3add8a13 --- /dev/null +++ b/packages/app-cardano/.prettierrc @@ -0,0 +1 @@ +"@cypherock/prettier-config" diff --git a/packages/app-cardano/CHANGELOG.md b/packages/app-cardano/CHANGELOG.md new file mode 100644 index 00000000..59164443 --- /dev/null +++ b/packages/app-cardano/CHANGELOG.md @@ -0,0 +1,3 @@ +# @cypherock/sdk-app-cardano + +## 1.0.0 diff --git a/packages/app-cardano/docs/index.md b/packages/app-cardano/docs/index.md new file mode 100644 index 00000000..4298f4d0 --- /dev/null +++ b/packages/app-cardano/docs/index.md @@ -0,0 +1,3 @@ +# Introduction + +Documentation pending diff --git a/packages/app-cardano/jest.config.js b/packages/app-cardano/jest.config.js new file mode 100644 index 00000000..58ff4e67 --- /dev/null +++ b/packages/app-cardano/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + collectCoverage: true, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.test.ts', + '!src/**/__fixtures__/*', + '!src/proto/generated/**/*', + ], + testTimeout: 500, + preset: 'ts-jest', + testEnvironment: 'node', + rootDir: '.', + testPathIgnorePatterns: ['/node_modules/', '/__fixtures__/', '/dist/'], + testMatch: [ + '**/tests/**/*.[jt]s?(x)', + '**/__tests__/**/*.[jt]s?(x)', + '!**/__mocks__/**/*.[jt]s?(x)', + '!**/__helpers__/**/*.[jt]s?(x)', + '!**/.stryker-tmp/**/*.[jt]s?(x)', + ], + modulePathIgnorePatterns: ['/.stryker-tmp'], +}; diff --git a/packages/app-cardano/jest.stryker.config.js b/packages/app-cardano/jest.stryker.config.js new file mode 100644 index 00000000..6e650339 --- /dev/null +++ b/packages/app-cardano/jest.stryker.config.js @@ -0,0 +1,21 @@ +module.exports = { + collectCoverage: true, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.test.ts', + '!src/**/__fixtures__/*', + '!src/proto/generated/**/*', + ], + testTimeout: 500, + preset: 'ts-jest', + testEnvironment: 'node', + rootDir: '.', + testPathIgnorePatterns: ['/node_modules/', '/__fixtures__/', '/dist/'], + testMatch: [ + '**/tests/**/*.[jt]s?(x)', + '**/__tests__/**/*.[jt]s?(x)', + '!**/__mocks__/**/*.[jt]s?(x)', + '!**/__helpers__/**/*.[jt]s?(x)', + ], + modulePathIgnorePatterns: ['/.stryker-tmp'], +}; diff --git a/packages/app-cardano/mkdocs.yml b/packages/app-cardano/mkdocs.yml new file mode 100644 index 00000000..fec904d2 --- /dev/null +++ b/packages/app-cardano/mkdocs.yml @@ -0,0 +1 @@ +site_name: '@cypherock/sdk-app-cardano' diff --git a/packages/app-cardano/package.json b/packages/app-cardano/package.json new file mode 100644 index 00000000..a7797436 --- /dev/null +++ b/packages/app-cardano/package.json @@ -0,0 +1,53 @@ +{ + "name": "@cypherock/sdk-app-cardano", + "version": "1.0.1", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "license": "MIT", + "scripts": { + "lint": "eslint src/ tests/ --fix", + "lint:check": "eslint --ext .ts,tsx,js,jsx,js src/", + "pretty": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'", + "pretty:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts'", + "prebuild": "bash ./scripts/prebuild.sh", + "build": "rimraf dist && tsc -p tsconfig.json", + "test": "cross-env LOG_LEVEL=error jest", + "test:mutation": "stryker run", + "pre-commit": "lint-staged" + }, + "devDependencies": { + "@cypherock/eslint-config": "workspace:*", + "@cypherock/prettier-config": "workspace:^0.0.8", + "@cypherock/tsconfig": "workspace:*", + "@jest/globals": "^29.4.1", + "@stryker-mutator/core": "^6.4.1", + "@stryker-mutator/jest-runner": "^6.4.1", + "@stryker-mutator/typescript-checker": "^6.4.1", + "@types/jest": "^29.4.0", + "@types/node": "18.11.18", + "cross-env": "^7.0.3", + "eslint": "^7.32.0", + "jest": "^29.4.1", + "lint-staged": "^13.2.0", + "rimraf": "^4.1.2", + "ts-jest": "^29.0.5", + "ts-proto": "^1.139.0", + "typescript": "^4.5.2" + }, + "dependencies": { + "@cypherock/sdk-core": "workspace:^0.1.2", + "@cypherock/sdk-interfaces": "workspace:^0.0.16", + "@cypherock/sdk-utils": "workspace:^0.0.21", + "long": "^5.2.1", + "protobufjs": "^7.2.2" + }, + "lint-staged": { + "*.{ts,tsx}": [ + "eslint --ext ts,tsx --quiet --fix --", + "prettier --write" + ], + "*.{js,jsx,md,mdx,mjs,yml,yaml,css,json}": [ + "prettier --write" + ] + } +} diff --git a/packages/app-cardano/scripts/prebuild.sh b/packages/app-cardano/scripts/prebuild.sh new file mode 100755 index 00000000..acce802d --- /dev/null +++ b/packages/app-cardano/scripts/prebuild.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +rm -rf ./src/proto/generated/*.ts || true +rm -rf ./src/proto/generated/cardano || true + +mkdir -p src/proto/generated + +if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then + protoc --plugin=protoc-gen-ts_proto=$(pwd)/node_modules/.bin/protoc-gen-ts_proto.cmd --ts_proto_out=./src/proto/generated ../../submodules/common/proto/cardano/*.proto ../../submodules/common/proto/common.proto -I../../submodules/common/proto --ts_proto_opt=forceLong=string --ts_proto_opt=esModuleInterop=true +else + protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/proto/generated ../../submodules/common/proto/cardano/*.proto ../../submodules/common/proto/common.proto -I../../submodules/common/proto --ts_proto_opt=forceLong=string --ts_proto_opt=esModuleInterop=true +fi + +node ../../scripts/extractTypes/index.js ./src/proto/generated ./src/proto/generated/types.ts diff --git a/packages/app-cardano/src/__mocks__/sdk.ts b/packages/app-cardano/src/__mocks__/sdk.ts new file mode 100644 index 00000000..9f56ef61 --- /dev/null +++ b/packages/app-cardano/src/__mocks__/sdk.ts @@ -0,0 +1,60 @@ +import { ISDK } from '@cypherock/sdk-core'; +import { DeviceState } from '@cypherock/sdk-interfaces'; +import { jest } from '@jest/globals'; + +let sequenceNumber = 0; + +export const getStatus = jest.fn(); +export const sendAbort = jest.fn(); +export const getResult = jest.fn(); +export const sendQuery = jest.fn(); + +export const configureAppletId = jest.fn(); +export const checkAppCompatibility = jest.fn(); + +export const waitForResult = jest.fn(); +export const getSequenceNumber = jest.fn( + async () => sequenceNumber, +); +export const getNewSequenceNumber = jest.fn( + async () => { + sequenceNumber += 1; + return sequenceNumber; + }, +); + +export const runOperation = jest.fn(func => func()); + +export const destroy = jest.fn(); +export const getDeviceState = jest.fn( + async () => DeviceState.MAIN, +); + +export const create = jest.fn(async () => + Promise.resolve({ + configureAppletId, + checkAppCompatibility, + sendAbort, + getResult, + getStatus, + sendQuery, + waitForResult, + getSequenceNumber, + getNewSequenceNumber, + runOperation, + destroy, + getDeviceState, + }), +); + +jest.mock('@cypherock/sdk-core', () => { + const originalModule: any = jest.requireActual('@cypherock/sdk-core'); + + return { + __esModule: true, + ...originalModule, + SDK: { + create, + }, + }; +}); diff --git a/packages/app-cardano/src/app.ts b/packages/app-cardano/src/app.ts new file mode 100644 index 00000000..a85e1184 --- /dev/null +++ b/packages/app-cardano/src/app.ts @@ -0,0 +1,41 @@ +import { IDeviceConnection } from '@cypherock/sdk-interfaces'; +import { SDK } from '@cypherock/sdk-core'; + +import * as operations from './operations'; + +export class CardanoApp { + private readonly sdk: SDK; + + public static readonly APPLET_ID = 28; + + private constructor(sdk: SDK) { + this.sdk = sdk; + } + + public static async create(connection: IDeviceConnection) { + const sdk = await SDK.create(connection, CardanoApp.APPLET_ID); + return new CardanoApp(sdk); + } + + public async getPublicKeys(params: operations.IGetPublicKeysParams) { + return this.sdk.runOperation(() => + operations.getPublicKeys(this.sdk, params), + ); + } + + public async getUserVerifiedPublicKey( + params: operations.IGetUserVerifiedPublicKeyParams, + ) { + return this.sdk.runOperation(() => + operations.getUserVerifiedPublicKey(this.sdk, params), + ); + } + + public async destroy() { + return this.sdk.destroy(); + } + + public async abort() { + await this.sdk.sendAbort(); + } +} diff --git a/packages/app-cardano/src/constants/appId.ts b/packages/app-cardano/src/constants/appId.ts new file mode 100644 index 00000000..68702951 --- /dev/null +++ b/packages/app-cardano/src/constants/appId.ts @@ -0,0 +1,4 @@ +export const APP_VERSION = { + from: '1.0.0', + to: '2.0.0', +}; diff --git a/packages/app-cardano/src/index.ts b/packages/app-cardano/src/index.ts new file mode 100644 index 00000000..e1c20d43 --- /dev/null +++ b/packages/app-cardano/src/index.ts @@ -0,0 +1,5 @@ +export { + updateLogger, + derivePaymentAddress, + deriveStakeAddress, +} from './utils'; diff --git a/packages/app-cardano/src/operations/index.ts b/packages/app-cardano/src/operations/index.ts new file mode 100644 index 00000000..ac301862 --- /dev/null +++ b/packages/app-cardano/src/operations/index.ts @@ -0,0 +1,3 @@ +export * from './getPublicKeys'; +export * from './getUserVerifiedPublicKey'; +export * from './runGetPublicKeys'; diff --git a/packages/app-cardano/src/operations/types.ts b/packages/app-cardano/src/operations/types.ts new file mode 100644 index 00000000..170894c2 --- /dev/null +++ b/packages/app-cardano/src/operations/types.ts @@ -0,0 +1,2 @@ +export * from './runGetPublicKeys/types'; +export * from './getUserVerifiedPublicKey/types'; diff --git a/packages/app-cardano/src/types.ts b/packages/app-cardano/src/types.ts new file mode 100644 index 00000000..ad220967 --- /dev/null +++ b/packages/app-cardano/src/types.ts @@ -0,0 +1,2 @@ +export * from './operations/types'; +export * from './proto/generated/types'; diff --git a/packages/app-cardano/src/utils/asserts.ts b/packages/app-cardano/src/utils/asserts.ts new file mode 100644 index 00000000..af738810 --- /dev/null +++ b/packages/app-cardano/src/utils/asserts.ts @@ -0,0 +1,35 @@ +import { DeviceAppError, DeviceAppErrorType } from '@cypherock/sdk-interfaces'; +import { assert } from '@cypherock/sdk-utils'; +import { ICommonError } from '../proto/generated/types'; + +export function assertOrThrowInvalidResult( + condition: T, +): asserts condition is Exclude { + assert( + condition, + new DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE), + ); +} + +export function parseCommonError(error?: ICommonError) { + if (error === undefined) return; + + type CommonErrorKey = keyof ICommonError; + const keys = Object.keys(error) as CommonErrorKey[]; + + const errorTypesMap: Record = { + unknownError: DeviceAppErrorType.UNKNOWN_ERROR, + deviceSetupRequired: DeviceAppErrorType.DEVICE_SETUP_REQUIRED, + walletNotFound: DeviceAppErrorType.WALLET_NOT_FOUND, + walletPartialState: DeviceAppErrorType.WALLET_PARTIAL_STATE, + cardError: DeviceAppErrorType.CARD_OPERATION_FAILED, + userRejection: DeviceAppErrorType.USER_REJECTION, + corruptData: DeviceAppErrorType.CORRUPT_DATA, + }; + + for (const key of keys) { + if (error[key] !== undefined) { + throw new DeviceAppError(errorTypesMap[key], error[key]); + } + } +} diff --git a/packages/app-cardano/src/utils/index.ts b/packages/app-cardano/src/utils/index.ts new file mode 100644 index 00000000..c0689681 --- /dev/null +++ b/packages/app-cardano/src/utils/index.ts @@ -0,0 +1,4 @@ +export * from './operationHelper'; +export * from './asserts'; +export * from './logger'; +export * from './publicKeys'; diff --git a/packages/app-cardano/src/utils/logger.ts b/packages/app-cardano/src/utils/logger.ts new file mode 100644 index 00000000..550a3891 --- /dev/null +++ b/packages/app-cardano/src/utils/logger.ts @@ -0,0 +1,22 @@ +import { updateLogger as updateLoggerCore } from '@cypherock/sdk-core'; +import { ILogger, LogCreator } from '@cypherock/sdk-interfaces'; +import { + createDefaultConsoleLogger, + updateLoggerObject, +} from '@cypherock/sdk-utils'; + +export const loggerServiceName = 'sdk-app-canton'; + +export const logger: ILogger = { + ...createDefaultConsoleLogger(loggerServiceName), +}; + +export const updateLogger = (createLogger: LogCreator) => { + updateLoggerCore(createLogger); + updateLoggerObject({ + currentLogger: logger, + newLogger: createLogger(loggerServiceName), + }); +}; + +export default logger; diff --git a/packages/app-cardano/src/utils/operationHelper.ts b/packages/app-cardano/src/utils/operationHelper.ts new file mode 100644 index 00000000..e6689950 --- /dev/null +++ b/packages/app-cardano/src/utils/operationHelper.ts @@ -0,0 +1,126 @@ +import { ISDK } from '@cypherock/sdk-core'; +import { DeviceAppError, DeviceAppErrorType } from '@cypherock/sdk-interfaces'; +import { OnStatus } from '@cypherock/sdk-utils'; +import { ChunkPayload, ChunkAck } from '../proto/generated/common'; +import { + DeepPartial, + Exact, + Query, + Result, +} from '../proto/generated/cardano/core'; +import { assertOrThrowInvalidResult, parseCommonError } from './asserts'; + +export function decodeResult(data: Uint8Array) { + let result: Result; + + try { + result = Result.decode(data); + } catch (error) { + throw new DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE); + } + + return result; +} + +export function encodeQuery, I>>(query: I) { + return Uint8Array.from(Query.encode(Query.create(query)).finish()); +} + +type QueryKey = keyof Query; + +type ResultKey = keyof Result; + +export class OperationHelper { + public readonly sdk: ISDK; + + private readonly queryKey: QueryKey; + + private readonly resultKey: ResultKey; + + private readonly onStatus?: OnStatus; + + private static readonly CHUNK_SIZE = 2048; + + constructor(params: { + sdk: ISDK; + queryKey: Q; + resultKey: R; + onStatus?: OnStatus; + }) { + this.sdk = params.sdk; + + this.queryKey = params.queryKey; + + this.resultKey = params.resultKey; + + this.onStatus = params.onStatus; + } + + public async sendQuery(query: I) { + return this.sdk.sendQuery(encodeQuery({ [this.queryKey]: query } as any)); + } + + public async waitForResult() { + const result = decodeResult( + await this.sdk.waitForResult({ onStatus: this.onStatus }), + ); + + const resultData = result[this.resultKey] as Result[R]; + parseCommonError(result.commonError); + assertOrThrowInvalidResult(resultData); + parseCommonError((result[this.resultKey] as any).commonError); + + return resultData; + } + + private static splitIntoChunks(txn: Uint8Array): Uint8Array[] { + const chunks: Uint8Array[] = []; + const totalChunks = Math.ceil(txn.length / OperationHelper.CHUNK_SIZE); + + for (let i = 0; i < totalChunks; i += 1) { + const chunk = txn.slice( + i * OperationHelper.CHUNK_SIZE, + i * OperationHelper.CHUNK_SIZE + OperationHelper.CHUNK_SIZE, + ); + chunks.push(chunk); + } + + return chunks; + } + + public async sendInChunks< + RK extends keyof Exclude, + QK extends keyof Exclude, + >(data: Uint8Array, queryKey: QK, resultKey: RK) { + const chunks = OperationHelper.splitIntoChunks(data); + let remainingSize = data.length; + + for (let i = 0; i < chunks.length; i += 1) { + const chunk = chunks[i]; + remainingSize -= chunk.length; + + const chunkPayload: ChunkPayload = { + chunk, + chunkIndex: i, + totalChunks: chunks.length, + remainingSize, + }; + + await this.sendQuery({ + [queryKey]: { + chunkPayload, + }, + }); + + const result = await this.waitForResult(); + assertOrThrowInvalidResult(result[resultKey]); + + const { chunkAck } = result[resultKey] as { + chunkAck: ChunkAck; + }; + + assertOrThrowInvalidResult(chunkAck); + assertOrThrowInvalidResult(chunkAck.chunkIndex === i); + } + } +} diff --git a/packages/app-cardano/stryker.conf.mjs b/packages/app-cardano/stryker.conf.mjs new file mode 100644 index 00000000..9bacedf1 --- /dev/null +++ b/packages/app-cardano/stryker.conf.mjs @@ -0,0 +1,24 @@ +const config = { + mutate: [ + 'src/**/*.ts', + '!src/**/*.test.ts', + '!**/__fixtures__/*', + '!**/__mocks__/*', + '!**/__helpers__/*', + '!src/proto/generated/**/*', + ], + tempDirName: '.stryker-tmp', + packageManager: 'pnpm', + reporters: ['html', 'clear-text', 'progress'], + testRunner: 'jest', + coverageAnalysis: 'perTest', + plugins: ['@stryker-mutator/jest-runner'], + disableTypeChecks: true, + tsconfigFile: 'tsconfig.json', + jest: { + projectType: 'custom', + configFile: './jest.stryker.config.js', + }, +}; + +export default config; diff --git a/packages/app-cardano/tsconfig.eslint.json b/packages/app-cardano/tsconfig.eslint.json new file mode 100644 index 00000000..95386ab1 --- /dev/null +++ b/packages/app-cardano/tsconfig.eslint.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "dist", + "lib": ["ES2015"] + }, + "extends": "./tsconfig.json", + "include": ["./src/**/*.ts", "./tests/**/*.ts"], + "exclude": ["node_modules", "src/coverage"] +} diff --git a/packages/app-cardano/tsconfig.json b/packages/app-cardano/tsconfig.json new file mode 100644 index 00000000..db950607 --- /dev/null +++ b/packages/app-cardano/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "outDir": "dist", + "lib": ["ES2015"], + }, + "extends": "@cypherock/tsconfig/base.json", + "include": ["./src/**/*.ts"], + "exclude": [ + "node_modules", + "src/coverage", + "src/**/__fixtures__/*.ts", + "src/**/__tests__/*.ts", + "src/**/__mocks__/*.ts", + ], +} From 51eb1a9f371015c34e58a71558b993a310d23238 Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Tue, 20 Jan 2026 18:40:23 +0530 Subject: [PATCH 04/15] feat(cardano): add address derivation functions Signed-off-by: Prashant Rahul --- packages/app-cardano/src/utils/publicKeys.ts | 57 ++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 packages/app-cardano/src/utils/publicKeys.ts diff --git a/packages/app-cardano/src/utils/publicKeys.ts b/packages/app-cardano/src/utils/publicKeys.ts new file mode 100644 index 00000000..8d380fda --- /dev/null +++ b/packages/app-cardano/src/utils/publicKeys.ts @@ -0,0 +1,57 @@ +import { + assert, + convertBits, + blake2b, + bech32Encode, +} from '@cypherock/sdk-utils'; + +const NETWORK_ID = 0x01; +const STAKE_BECH32_PREFIX = 'stake'; +const PAYMENT_BECH32_PREFIX = 'addr'; + +/** + * @brief Derive payment address for cardano network + * + * @param paymentPublicKey Payment derivation public key + * @param stakePublicKey Stake derivation public key + * + * @returns Payment address + */ +export const derivePaymentAddress = ( + paymentPublicKey: Uint8Array, + stakePublicKey: Uint8Array, +): string => { + const rawPaymentAddr = new Uint8Array(1 + 28 + 28); + rawPaymentAddr.set(Uint8Array.from([0x01]), 0); + + const blake2bPaymentPubKey = blake2b(paymentPublicKey, undefined, 28); + rawPaymentAddr.set(blake2bPaymentPubKey, 1); + + const blake2bStakePubKey = blake2b(stakePublicKey, undefined, 28); + rawPaymentAddr.set(blake2bStakePubKey, 1 + 28); + + const rawPaymentAddr5bit = convertBits(5, 8, rawPaymentAddr, true); + assert(rawPaymentAddr5bit, 'rawPaymentAddr5bit is null'); + + return bech32Encode(PAYMENT_BECH32_PREFIX, rawPaymentAddr5bit, 180); +}; + +/** + * @brief Derive stake address for cardano network + * + * @param stakePublicKey Stake derivation public key + * + * @returns Stake address + */ +export const deriveStakeAddress = (stakePublicKey: Uint8Array): string => { + const rawStakeAddr = new Uint8Array(1 + 28); + rawStakeAddr.set(Uint8Array.from([0xe0 | NETWORK_ID]), 0); // eslint-disable-line no-bitwise + + const blake2bHashedPubKey = blake2b(stakePublicKey, undefined, 28); + rawStakeAddr.set(blake2bHashedPubKey, 1); + + const rawStakeAddr5bit = convertBits(5, 8, rawStakeAddr, true); + assert(rawStakeAddr5bit, 'rawStakeAddr5bit is null'); + + return bech32Encode(STAKE_BECH32_PREFIX, rawStakeAddr5bit, 180); +}; From 351d511a6e17e6412b20ab05fa237d50120b07dc Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Tue, 20 Jan 2026 18:42:59 +0530 Subject: [PATCH 05/15] feat(cardano): flow to retrieve public keys, addresses Signed-off-by: Prashant Rahul --- .../src/operations/runGetPublicKeys/index.ts | 71 +++++++++++++++++++ .../src/operations/runGetPublicKeys/types.ts | 29 ++++++++ 2 files changed, 100 insertions(+) create mode 100644 packages/app-cardano/src/operations/runGetPublicKeys/index.ts create mode 100644 packages/app-cardano/src/operations/runGetPublicKeys/types.ts diff --git a/packages/app-cardano/src/operations/runGetPublicKeys/index.ts b/packages/app-cardano/src/operations/runGetPublicKeys/index.ts new file mode 100644 index 00000000..008dc9e3 --- /dev/null +++ b/packages/app-cardano/src/operations/runGetPublicKeys/index.ts @@ -0,0 +1,71 @@ +import { uint8ArrayToHex } from '@cypherock/sdk-utils'; +import { + OperationHelper, + assertOrThrowInvalidResult, + derivePaymentAddress, + deriveStakeAddress, +} from '../../utils'; +import { IGetPublicKeysParams, GetPublicKeysEvent } from './types'; + +export * from './types'; + +export const runGetPublicKeysOnDevice = async ( + helper: + | OperationHelper<'getPublicKeys', 'getPublicKeys'> + | OperationHelper<'getUserVerifiedPublicKey', 'getUserVerifiedPublicKey'>, + params: IGetPublicKeysParams, + forceStatusUpdate: (flowStatus: number) => void, +) => { + await helper.sendQuery({ + initiate: { + walletId: params.walletId, + derivationPaths: params.derivationPaths, + }, + }); + + const publicKeys: { paymentKeys: Uint8Array[]; stakeKeys: Uint8Array[] } = { + paymentKeys: [], + stakeKeys: [], + }; + const hasMore = () => + publicKeys.paymentKeys.length !== params.derivationPaths.length; + do { + const result = await helper.waitForResult(); + assertOrThrowInvalidResult(result.result); + publicKeys.paymentKeys = [ + ...publicKeys.paymentKeys, + ...result.result.paymentPubKeys, + ]; + publicKeys.stakeKeys = [ + ...publicKeys.stakeKeys, + ...result.result.stakePubKeys, + ]; + forceStatusUpdate(GetPublicKeysEvent.PIN_CARD); + if (hasMore()) { + await helper.sendQuery({ + fetchNext: {}, + }); + } + } while (hasMore()); + + forceStatusUpdate(GetPublicKeysEvent.VERIFY); + + const addresses: { stake: string[]; payment: string[] } = { + stake: [], + payment: [], + }; + for (let i = 0; i < publicKeys.paymentKeys.length; i += 1) { + const stakeKey = publicKeys.stakeKeys[i]; + const paymentKey = publicKeys.paymentKeys[i]; + addresses.stake.push(deriveStakeAddress(stakeKey)); + addresses.payment.push(derivePaymentAddress(paymentKey, stakeKey)); + } + + return { + publicKeys: { + payment: publicKeys.paymentKeys.map(e => uint8ArrayToHex(e)), + stake: publicKeys.stakeKeys.map(e => uint8ArrayToHex(e)), + }, + addresses, + }; +}; diff --git a/packages/app-cardano/src/operations/runGetPublicKeys/types.ts b/packages/app-cardano/src/operations/runGetPublicKeys/types.ts new file mode 100644 index 00000000..43818cd0 --- /dev/null +++ b/packages/app-cardano/src/operations/runGetPublicKeys/types.ts @@ -0,0 +1,29 @@ +import { IGetPublicKeysDerivationPath } from '../../proto/generated/types'; + +export enum GetPublicKeysEvent { + INIT = 0, + CONFIRM = 1, + PASSPHRASE = 2, + PIN_CARD = 3, + VERIFY = 4, +} + +export type GetPublicKeysEventHandler = (event: GetPublicKeysEvent) => void; + +export interface IGetPublicKeysParams { + onEvent?: GetPublicKeysEventHandler; + + walletId: Uint8Array; + derivationPaths: IGetPublicKeysDerivationPath[]; +} + +export interface IGetPublicKeysResult { + publicKeys: { + payment: string[]; + stake: string[]; + }; + addresses: { + stake: string[]; + payment: string[]; + }; +} From b650d42968a27040d9719213df13b2d5956c1b25 Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Tue, 20 Jan 2026 18:47:11 +0530 Subject: [PATCH 06/15] feat(cardano): wrapper public key, address retrieval flows Signed-off-by: Prashant Rahul --- .../src/operations/getPublicKeys/index.ts | 59 +++++++++++++++ .../getUserVerifiedPublicKey/index.ts | 73 +++++++++++++++++++ .../getUserVerifiedPublicKey/types.ts | 19 +++++ 3 files changed, 151 insertions(+) create mode 100644 packages/app-cardano/src/operations/getPublicKeys/index.ts create mode 100644 packages/app-cardano/src/operations/getUserVerifiedPublicKey/index.ts create mode 100644 packages/app-cardano/src/operations/getUserVerifiedPublicKey/types.ts diff --git a/packages/app-cardano/src/operations/getPublicKeys/index.ts b/packages/app-cardano/src/operations/getPublicKeys/index.ts new file mode 100644 index 00000000..f6570bea --- /dev/null +++ b/packages/app-cardano/src/operations/getPublicKeys/index.ts @@ -0,0 +1,59 @@ +import { ISDK } from '@cypherock/sdk-core'; +import { + createStatusListener, + assert, + createLoggerWithPrefix, +} from '@cypherock/sdk-utils'; +import { APP_VERSION } from '../../constants/appId'; +import { + GetPublicKeysStatus, + SeedGenerationStatus, +} from '../../proto/generated/types'; +import { OperationHelper, logger as rootLogger } from '../../utils'; +import { runGetPublicKeysOnDevice } from '../runGetPublicKeys'; +import { + IGetPublicKeysParams, + IGetPublicKeysResult, + GetPublicKeysEvent, +} from '../types'; + +const logger = createLoggerWithPrefix(rootLogger, 'GetPublicKeys'); + +export const getPublicKeys = async ( + sdk: ISDK, + params: IGetPublicKeysParams, +): Promise => { + assert(params, 'Params should be defined'); + assert(params.walletId, 'walletId should be defined'); + assert(params.derivationPaths, 'derivationPaths should be defined'); + assert( + params.derivationPaths.length > 0, + 'derivationPaths should not be empty', + ); + assert( + params.derivationPaths.reduce( + (acc, path) => acc && path.path.length === 5, + true, + ), + 'derivationPaths should be equal to 5', + ); + + await sdk.checkAppCompatibility(APP_VERSION); + + const { onStatus, forceStatusUpdate } = createStatusListener({ + enums: GetPublicKeysEvent, + operationEnums: GetPublicKeysStatus, + seedGenerationEnums: SeedGenerationStatus, + onEvent: params.onEvent, + logger, + }); + + const helper = new OperationHelper({ + sdk, + queryKey: 'getPublicKeys', + resultKey: 'getPublicKeys', + onStatus, + }); + + return runGetPublicKeysOnDevice(helper, params, forceStatusUpdate); +}; diff --git a/packages/app-cardano/src/operations/getUserVerifiedPublicKey/index.ts b/packages/app-cardano/src/operations/getUserVerifiedPublicKey/index.ts new file mode 100644 index 00000000..457c27c9 --- /dev/null +++ b/packages/app-cardano/src/operations/getUserVerifiedPublicKey/index.ts @@ -0,0 +1,73 @@ +import { ISDK } from '@cypherock/sdk-core'; +import { + createStatusListener, + assert, + createLoggerWithPrefix, +} from '@cypherock/sdk-utils'; +import { APP_VERSION } from '../../constants/appId'; +import { + GetPublicKeysStatus, + SeedGenerationStatus, +} from '../../proto/generated/types'; +import { OperationHelper, logger as rootLogger } from '../../utils'; +import { runGetPublicKeysOnDevice } from '../runGetPublicKeys'; +import { GetPublicKeysEvent } from '../types'; +import { + IGetUserVerifiedPublicKeyParams, + IGetUserVerifiedPublicKeyResult, +} from './types'; + +export * from './types'; + +const logger = createLoggerWithPrefix(rootLogger, 'GetPublicKeys'); + +export const getUserVerifiedPublicKey = async ( + sdk: ISDK, + params: IGetUserVerifiedPublicKeyParams, +): Promise => { + assert(params, 'Params should be defined'); + assert(params.walletId, 'walletId should be defined'); + assert(params.derivationPath, 'derivationPath should be defined'); + assert( + params.derivationPath.length === 5, + 'derivationPath should be equal to 5', + ); + + await sdk.checkAppCompatibility(APP_VERSION); + + const { onStatus, forceStatusUpdate } = createStatusListener({ + enums: GetPublicKeysEvent, + operationEnums: GetPublicKeysStatus, + seedGenerationEnums: SeedGenerationStatus, + onEvent: params.onEvent, + logger, + }); + + const helper = new OperationHelper({ + sdk, + queryKey: 'getUserVerifiedPublicKey', + resultKey: 'getUserVerifiedPublicKey', + onStatus, + }); + + const result = await runGetPublicKeysOnDevice( + helper, + { + walletId: params.walletId, + derivationPaths: [{ path: params.derivationPath }], + onEvent: params.onEvent, + }, + forceStatusUpdate, + ); + + return { + publicKey: { + stake: result.publicKeys.stake[0], + payment: result.publicKeys.payment[0], + }, + addresse: { + stake: result.addresses.stake[0], + payment: result.addresses.payment[0], + }, + }; +}; diff --git a/packages/app-cardano/src/operations/getUserVerifiedPublicKey/types.ts b/packages/app-cardano/src/operations/getUserVerifiedPublicKey/types.ts new file mode 100644 index 00000000..4c359aa1 --- /dev/null +++ b/packages/app-cardano/src/operations/getUserVerifiedPublicKey/types.ts @@ -0,0 +1,19 @@ +import { GetPublicKeysEventHandler } from '../runGetPublicKeys/types'; + +export interface IGetUserVerifiedPublicKeyParams { + onEvent?: GetPublicKeysEventHandler; + + walletId: Uint8Array; + derivationPath: number[]; +} + +export interface IGetUserVerifiedPublicKeyResult { + publicKey: { + stake: string; + payment: string; + }; + addresse: { + stake: string; + payment: string; + }; +} From a4c9c7000c0ebf8c3d3fda0bd269a200bf58b59c Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Tue, 20 Jan 2026 18:56:03 +0530 Subject: [PATCH 07/15] refactor(cardano): export app, types from cardano src Signed-off-by: Prashant Rahul --- packages/app-cardano/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/app-cardano/src/index.ts b/packages/app-cardano/src/index.ts index e1c20d43..3c4fac02 100644 --- a/packages/app-cardano/src/index.ts +++ b/packages/app-cardano/src/index.ts @@ -1,3 +1,5 @@ +export * from './app'; +export * from './types'; export { updateLogger, derivePaymentAddress, From 5f23fad691b94090a8b707e47278be88c02cd579 Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Tue, 20 Jan 2026 19:36:10 +0530 Subject: [PATCH 08/15] chore: update root pnpm lock file Signed-off-by: Prashant Rahul --- pnpm-lock.yaml | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f6401d9..da7fe108 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: '@cypherock/sdk-app-canton': specifier: workspace:^ version: link:../../packages/app-canton + '@cypherock/sdk-app-cardano': + specifier: workspace:^ + version: link:../../packages/app-cardano '@cypherock/sdk-app-constellation': specifier: workspace:^ version: link:../../packages/app-constellation @@ -363,6 +366,76 @@ importers: specifier: ^4.5.2 version: 4.9.5 + packages/app-cardano: + dependencies: + '@cypherock/sdk-core': + specifier: workspace:^0.1.2 + version: link:../core + '@cypherock/sdk-interfaces': + specifier: workspace:^0.0.16 + version: link:../interfaces + '@cypherock/sdk-utils': + specifier: workspace:^0.0.21 + version: link:../util + long: + specifier: ^5.2.1 + version: 5.2.1 + protobufjs: + specifier: ^7.2.2 + version: 7.2.2 + devDependencies: + '@cypherock/eslint-config': + specifier: workspace:* + version: link:../util-eslint-config + '@cypherock/prettier-config': + specifier: workspace:^0.0.8 + version: link:../util-prettier-config + '@cypherock/tsconfig': + specifier: workspace:* + version: link:../util-tsconfig + '@jest/globals': + specifier: ^29.4.1 + version: 29.4.1 + '@stryker-mutator/core': + specifier: ^6.4.1 + version: 6.4.1 + '@stryker-mutator/jest-runner': + specifier: ^6.4.1 + version: 6.4.1(@stryker-mutator/core@6.4.1) + '@stryker-mutator/typescript-checker': + specifier: ^6.4.1 + version: 6.4.1(@stryker-mutator/core@6.4.1)(typescript@4.9.5) + '@types/jest': + specifier: ^29.4.0 + version: 29.4.0 + '@types/node': + specifier: 18.11.18 + version: 18.11.18 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + eslint: + specifier: ^7.32.0 + version: 7.32.0 + jest: + specifier: ^29.4.1 + version: 29.4.1(@types/node@18.11.18)(ts-node@10.9.2) + lint-staged: + specifier: ^13.2.0 + version: 13.2.0 + rimraf: + specifier: ^4.1.2 + version: 4.1.2 + ts-jest: + specifier: ^29.0.5 + version: 29.0.5(@babel/core@7.26.0)(jest@29.4.1)(typescript@4.9.5) + ts-proto: + specifier: ^1.139.0 + version: 1.139.0 + typescript: + specifier: ^4.5.2 + version: 4.9.5 + packages/app-constellation: dependencies: '@cypherock/sdk-core': @@ -1556,6 +1629,12 @@ importers: '@cypherock/sdk-interfaces': specifier: workspace:^0.0.16 version: link:../interfaces + bech32: + specifier: ^2.0.0 + version: 2.0.0 + blakejs: + specifier: ^1.2.1 + version: 1.2.1 compare-versions: specifier: 6.0.0-rc.1 version: 6.0.0-rc.1 From 60d1c18f4634237f44a4d3ff0f4ebe14bec1987f Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Fri, 23 Jan 2026 15:23:11 +0530 Subject: [PATCH 09/15] chore: update common submodule Signed-off-by: Prashant Rahul --- submodules/common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/common b/submodules/common index 294390c4..31b29f50 160000 --- a/submodules/common +++ b/submodules/common @@ -1 +1 @@ -Subproject commit 294390c4adc9389a1d8b453fea6a2065fedafe27 +Subproject commit 31b29f505907202d6d653ba73472fd856b62076a From cfbf62db1753435d6e0f24e78597238bd534cb44 Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Fri, 23 Jan 2026 15:25:36 +0530 Subject: [PATCH 10/15] fix: add sdk-app-cardano to node app Signed-off-by: Prashant Rahul --- apps/node/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/node/package.json b/apps/node/package.json index ac4d4e10..4c316429 100644 --- a/apps/node/package.json +++ b/apps/node/package.json @@ -24,6 +24,7 @@ "@canton-network/core-ledger-proto": "^0.6.0", "@cypherock/sdk-app-btc": "workspace:^", "@cypherock/sdk-app-canton": "workspace:^", + "@cypherock/sdk-app-cardano": "workspace:^", "@cypherock/sdk-app-constellation": "workspace:^", "@cypherock/sdk-app-evm": "workspace:^", "@cypherock/sdk-app-icp": "workspace:^", From 77414220c95dc1077cef214b649539d0a80407c5 Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Fri, 23 Jan 2026 15:34:15 +0530 Subject: [PATCH 11/15] test: add cardano sdk create test case Signed-off-by: Prashant Rahul --- packages/app-cardano/tests/01.create/index.ts | 25 ++++++ .../app-cardano/tests/__helpers__/index.ts | 86 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 packages/app-cardano/tests/01.create/index.ts create mode 100644 packages/app-cardano/tests/__helpers__/index.ts diff --git a/packages/app-cardano/tests/01.create/index.ts b/packages/app-cardano/tests/01.create/index.ts new file mode 100644 index 00000000..7dc3d0b7 --- /dev/null +++ b/packages/app-cardano/tests/01.create/index.ts @@ -0,0 +1,25 @@ +import { MockDeviceConnection } from '@cypherock/sdk-interfaces'; +import { afterEach, beforeEach, describe, expect, test } from '@jest/globals'; + +import * as sdkMocks from '../../src/__mocks__/sdk'; +import { CardanoApp } from '../../src'; + +describe('CardanoApp.create', () => { + let connection: MockDeviceConnection; + + beforeEach(async () => { + connection = await MockDeviceConnection.create(); + sdkMocks.create.mockClear(); + }); + + afterEach(async () => { + await connection.destroy(); + }); + + test('should be able to create cardano app instance', async () => { + await CardanoApp.create(connection); + + expect(sdkMocks.create).toHaveBeenCalledTimes(1); + expect(sdkMocks.create.mock.lastCall).toContainEqual(connection); + }); +}); diff --git a/packages/app-cardano/tests/__helpers__/index.ts b/packages/app-cardano/tests/__helpers__/index.ts new file mode 100644 index 00000000..17ff3283 --- /dev/null +++ b/packages/app-cardano/tests/__helpers__/index.ts @@ -0,0 +1,86 @@ +import { expect, jest } from '@jest/globals'; + +import * as sdkMocks from '../../src/__mocks__/sdk'; + +const onEvent = jest.fn(); + +export interface ITestCase { + queries: { + name: string; + data: Uint8Array; + }[]; + results: { + name: string; + data: Uint8Array; + statuses?: { flowStatus: number; expectEventCalls?: number[] }[]; + }[]; + mocks?: { + eventCalls?: number[][]; + }; +} + +export function setupMocks(testCase: ITestCase) { + testCase.queries.forEach(() => { + sdkMocks.sendQuery.mockReturnValueOnce(Promise.resolve(undefined)); + }); + + testCase.results.forEach(result => { + /* + * 1. Simulate calling onStatus event for waitForResult + * 2. Expect the onEvent to be called as described in the testCase + */ + sdkMocks.waitForResult.mockImplementationOnce(async params => { + if (params?.onStatus && result.statuses) { + let onEventCalls = 0; + + for (const status of result.statuses) { + params.onStatus({ flowStatus: status.flowStatus } as any); + + if (status.expectEventCalls !== undefined) { + for (let i = 0; i < status.expectEventCalls.length; i += 1) { + const mockIndex = + onEvent.mock.calls.length - status.expectEventCalls.length + i; + + expect(onEvent.mock.calls[mockIndex]).toEqual([ + status.expectEventCalls[i], + ]); + } + + onEventCalls += status.expectEventCalls.length; + expect(onEvent).toHaveBeenCalledTimes(onEventCalls); + } else { + expect(onEvent).toHaveBeenCalledTimes(onEventCalls); + } + } + } + + return result.data; + }); + }); + + return onEvent; +} + +export function clearMocks() { + onEvent.mockClear(); + + sdkMocks.create.mockClear(); + + sdkMocks.sendQuery.mockReset(); + sdkMocks.waitForResult.mockReset(); + + sdkMocks.runOperation.mockClear(); +} + +export function expectMockCalls(testCase: ITestCase) { + expect(sdkMocks.runOperation).toHaveBeenCalledTimes(1); + expect(sdkMocks.sendQuery.mock.calls.map(elem => elem[0])).toEqual( + testCase.queries.map(elem => elem.data), + ); + + if (!testCase.mocks) return; + + if (testCase.mocks.eventCalls) { + expect(onEvent.mock.calls).toEqual(testCase.mocks.eventCalls); + } +} From 227207204e6c68f5c0cb34dbd8ad533fee686bd9 Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Tue, 27 Jan 2026 16:32:01 +0530 Subject: [PATCH 12/15] fix: address typo in verified public key result Signed-off-by: Prashant Rahul --- .../src/operations/getUserVerifiedPublicKey/index.ts | 2 +- .../src/operations/getUserVerifiedPublicKey/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-cardano/src/operations/getUserVerifiedPublicKey/index.ts b/packages/app-cardano/src/operations/getUserVerifiedPublicKey/index.ts index 457c27c9..67c21884 100644 --- a/packages/app-cardano/src/operations/getUserVerifiedPublicKey/index.ts +++ b/packages/app-cardano/src/operations/getUserVerifiedPublicKey/index.ts @@ -65,7 +65,7 @@ export const getUserVerifiedPublicKey = async ( stake: result.publicKeys.stake[0], payment: result.publicKeys.payment[0], }, - addresse: { + address: { stake: result.addresses.stake[0], payment: result.addresses.payment[0], }, diff --git a/packages/app-cardano/src/operations/getUserVerifiedPublicKey/types.ts b/packages/app-cardano/src/operations/getUserVerifiedPublicKey/types.ts index 4c359aa1..77e18e68 100644 --- a/packages/app-cardano/src/operations/getUserVerifiedPublicKey/types.ts +++ b/packages/app-cardano/src/operations/getUserVerifiedPublicKey/types.ts @@ -12,7 +12,7 @@ export interface IGetUserVerifiedPublicKeyResult { stake: string; payment: string; }; - addresse: { + address: { stake: string; payment: string; }; From d13fb7aaed8394715a5f4eb12c781d5adfabc01a Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Wed, 28 Jan 2026 21:00:00 +0530 Subject: [PATCH 13/15] feat: stake verification status update Signed-off-by: Prashant Rahul --- .../src/operations/runGetPublicKeys/index.ts | 12 +++++++++++- .../src/operations/runGetPublicKeys/types.ts | 3 ++- submodules/common | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/app-cardano/src/operations/runGetPublicKeys/index.ts b/packages/app-cardano/src/operations/runGetPublicKeys/index.ts index 008dc9e3..50e899dd 100644 --- a/packages/app-cardano/src/operations/runGetPublicKeys/index.ts +++ b/packages/app-cardano/src/operations/runGetPublicKeys/index.ts @@ -27,6 +27,16 @@ export const runGetPublicKeysOnDevice = async ( paymentKeys: [], stakeKeys: [], }; + + /* wait for stake verification */ + const stakeResult = await helper.waitForResult(); + assertOrThrowInvalidResult(stakeResult.stakeVerify); + forceStatusUpdate(GetPublicKeysEvent.VERIFY_STAKE); + + await helper.sendQuery({ + result: {}, + }); + const hasMore = () => publicKeys.paymentKeys.length !== params.derivationPaths.length; do { @@ -48,7 +58,7 @@ export const runGetPublicKeysOnDevice = async ( } } while (hasMore()); - forceStatusUpdate(GetPublicKeysEvent.VERIFY); + forceStatusUpdate(GetPublicKeysEvent.VERIFY_PAYMENT); const addresses: { stake: string[]; payment: string[] } = { stake: [], diff --git a/packages/app-cardano/src/operations/runGetPublicKeys/types.ts b/packages/app-cardano/src/operations/runGetPublicKeys/types.ts index 43818cd0..e206a1a6 100644 --- a/packages/app-cardano/src/operations/runGetPublicKeys/types.ts +++ b/packages/app-cardano/src/operations/runGetPublicKeys/types.ts @@ -5,7 +5,8 @@ export enum GetPublicKeysEvent { CONFIRM = 1, PASSPHRASE = 2, PIN_CARD = 3, - VERIFY = 4, + VERIFY_STAKE = 4, + VERIFY_PAYMENT = 5, } export type GetPublicKeysEventHandler = (event: GetPublicKeysEvent) => void; diff --git a/submodules/common b/submodules/common index 31b29f50..da0d6e9f 160000 --- a/submodules/common +++ b/submodules/common @@ -1 +1 @@ -Subproject commit 31b29f505907202d6d653ba73472fd856b62076a +Subproject commit da0d6e9f716291bcc71d0e8fe6a5845ed677e1d3 From 455a3f32d99166a34cc79956abe9a76ae6d4b85c Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Thu, 29 Jan 2026 21:37:20 +0530 Subject: [PATCH 14/15] chore: update proto for txn signing Signed-off-by: Prashant Rahul --- submodules/common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/common b/submodules/common index da0d6e9f..249f45b9 160000 --- a/submodules/common +++ b/submodules/common @@ -1 +1 @@ -Subproject commit da0d6e9f716291bcc71d0e8fe6a5845ed677e1d3 +Subproject commit 249f45b9feccbe8f552dffdd813dba6284719fa1 From 6f403f62d1837fcc0169d7eeeed497cfcb9abbd2 Mon Sep 17 00:00:00 2001 From: Prashant Rahul Date: Fri, 30 Jan 2026 21:44:17 +0530 Subject: [PATCH 15/15] feat(cardano): add txn signing flow Signed-off-by: Prashant Rahul --- packages/app-cardano/src/app.ts | 4 + packages/app-cardano/src/operations/index.ts | 1 + .../src/operations/signTxn/index.ts | 89 +++++++++++++++++++ .../src/operations/signTxn/types.ts | 25 ++++++ packages/app-cardano/src/operations/types.ts | 1 + 5 files changed, 120 insertions(+) create mode 100644 packages/app-cardano/src/operations/signTxn/index.ts create mode 100644 packages/app-cardano/src/operations/signTxn/types.ts diff --git a/packages/app-cardano/src/app.ts b/packages/app-cardano/src/app.ts index a85e1184..3b1e3f86 100644 --- a/packages/app-cardano/src/app.ts +++ b/packages/app-cardano/src/app.ts @@ -31,6 +31,10 @@ export class CardanoApp { ); } + public async signTxn(params: operations.ISignTxnParams) { + return this.sdk.runOperation(() => operations.signTxn(this.sdk, params)); + } + public async destroy() { return this.sdk.destroy(); } diff --git a/packages/app-cardano/src/operations/index.ts b/packages/app-cardano/src/operations/index.ts index ac301862..f6018ac5 100644 --- a/packages/app-cardano/src/operations/index.ts +++ b/packages/app-cardano/src/operations/index.ts @@ -1,3 +1,4 @@ export * from './getPublicKeys'; export * from './getUserVerifiedPublicKey'; export * from './runGetPublicKeys'; +export * from './signTxn'; diff --git a/packages/app-cardano/src/operations/signTxn/index.ts b/packages/app-cardano/src/operations/signTxn/index.ts new file mode 100644 index 00000000..169773cc --- /dev/null +++ b/packages/app-cardano/src/operations/signTxn/index.ts @@ -0,0 +1,89 @@ +import { ISDK } from '@cypherock/sdk-core'; +import { + createStatusListener, + assert, + hexToUint8Array, + uint8ArrayToHex, + createLoggerWithPrefix, +} from '@cypherock/sdk-utils'; +import { APP_VERSION } from '../../constants/appId'; +import { + SeedGenerationStatus, + SignTxnStatus, +} from '../../proto/generated/types'; +import { + assertOrThrowInvalidResult, + OperationHelper, + logger as rootLogger, +} from '../../utils'; +import { ISignTxnParams, ISignTxnResult, SignTxnEvent } from './types'; + +export * from './types'; + +const logger = createLoggerWithPrefix(rootLogger, 'SignTxn'); + +export const signTxn = async ( + sdk: ISDK, + params: ISignTxnParams, +): Promise => { + assert(params, 'Params should be defined'); + assert(params.walletId, 'walletId should be defined'); + assert(params.txn, 'txn should be defined'); + assert(typeof params.txn === 'object', 'txn should be an object'); + assert( + typeof params.txn.txnDataHex === 'string', + 'txn.raw_data_hex should be a string', + ); + assert(params.derivationPath, 'derivationPath should be defined'); + assert( + params.derivationPath.length > 2, + 'derivationPath should be greater than 2', + ); + + await sdk.checkAppCompatibility(APP_VERSION); + + const { onStatus, forceStatusUpdate } = createStatusListener({ + enums: SignTxnEvent, + operationEnums: SignTxnStatus, + seedGenerationEnums: SeedGenerationStatus, + onEvent: params.onEvent, + logger, + }); + + const helper = new OperationHelper({ + sdk, + queryKey: 'signTxn', + resultKey: 'signTxn', + onStatus, + }); + + const txnBytes = hexToUint8Array(params.txn.txnDataHex); + + await helper.sendQuery({ + initiate: { + walletId: params.walletId, + derivationPath: params.derivationPath, + transactionSize: txnBytes.length, + }, + }); + + const { confirmation } = await helper.waitForResult(); + assertOrThrowInvalidResult(confirmation); + forceStatusUpdate(SignTxnEvent.CONFIRM); + + await helper.sendInChunks(txnBytes, 'txnData', 'dataAccepted'); + + await helper.sendQuery({ + signature: {}, + }); + const result = await helper.waitForResult(); + assertOrThrowInvalidResult(result.signature); + + forceStatusUpdate(SignTxnEvent.PIN_CARD); + + const signature = uint8ArrayToHex(result.signature.signature); + + return { + signature, + }; +}; diff --git a/packages/app-cardano/src/operations/signTxn/types.ts b/packages/app-cardano/src/operations/signTxn/types.ts new file mode 100644 index 00000000..e5262b14 --- /dev/null +++ b/packages/app-cardano/src/operations/signTxn/types.ts @@ -0,0 +1,25 @@ +export enum SignTxnEvent { + INIT = 0, + CONFIRM = 1, + VERIFY = 2, + PASSPHRASE = 3, + PIN_CARD = 4, +} + +export type SignTxnEventHandler = (event: SignTxnEvent) => void; + +export interface IUnsignedTransaction { + txnDataHex: string; +} + +export interface ISignTxnParams { + onEvent?: SignTxnEventHandler; + + walletId: Uint8Array; + derivationPath: number[]; + txn: IUnsignedTransaction; +} + +export interface ISignTxnResult { + signature: string; +} diff --git a/packages/app-cardano/src/operations/types.ts b/packages/app-cardano/src/operations/types.ts index 170894c2..b7adcbc3 100644 --- a/packages/app-cardano/src/operations/types.ts +++ b/packages/app-cardano/src/operations/types.ts @@ -1,2 +1,3 @@ export * from './runGetPublicKeys/types'; export * from './getUserVerifiedPublicKey/types'; +export * from './signTxn/types';