diff --git a/packages/app-inheritance/src/app.ts b/packages/app-inheritance/src/app.ts index 19e7ff751..5232d16c8 100644 --- a/packages/app-inheritance/src/app.ts +++ b/packages/app-inheritance/src/app.ts @@ -1,5 +1,6 @@ import { IDeviceConnection } from '@cypherock/sdk-interfaces'; import { SDK } from '@cypherock/sdk-core'; +import * as operations from './operations'; export class InheritanceApp { private readonly sdk: SDK; @@ -15,6 +16,10 @@ export class InheritanceApp { return new InheritanceApp(sdk); } + public async authWallet(params: operations.IAuthWalletParams) { + return this.sdk.runOperation(() => operations.authWallet(this.sdk, params)); + } + public async destroy() { return this.sdk.destroy(); } diff --git a/packages/app-inheritance/src/operations/authWallet/index.ts b/packages/app-inheritance/src/operations/authWallet/index.ts new file mode 100644 index 000000000..e818e9fe2 --- /dev/null +++ b/packages/app-inheritance/src/operations/authWallet/index.ts @@ -0,0 +1,71 @@ +import { ISDK } from '@cypherock/sdk-core'; +import { + assert, + createLoggerWithPrefix, + createStatusListener, +} from '@cypherock/sdk-utils'; +import { APP_VERSION } from '../../constants/appId'; +import { IWalletAuthResultResponse } from '../../proto/generated/types'; +import { + assertOrThrowInvalidResult, + OperationHelper, + logger as rootLogger, +} from '../../utils'; +import { IAuthWalletParams, WALLET_ID_LENGTH, WalletAuthEvent } from './types'; +import { WalletAuthStatus } from '../../proto/generated/inheritance/wallet_auth'; + +export * from './types'; + +const logger = createLoggerWithPrefix(rootLogger, 'authWallet'); + +export const authWallet = async ( + sdk: ISDK, + params: IAuthWalletParams, +): Promise => { + assert(params, 'Params should be defined'); + assert(params.walletId, 'walletId should be defined'); + assert(params.challenge, 'challenge should be defined'); + assert( + typeof params.isPublicKey === 'boolean', + 'isPublicKey should be defined', + ); + assert( + params.walletId.length === WALLET_ID_LENGTH, + `Wallet Id should be exactly ${WALLET_ID_LENGTH} bytes`, + ); + + await sdk.checkAppCompatibility(APP_VERSION); + + logger.info('Started', { ...params, onEvent: undefined }); + const { forceStatusUpdate, onStatus } = createStatusListener({ + enums: WalletAuthEvent, + operationEnums: WalletAuthStatus, + onEvent: params.onEvent, + logger, + }); + + const helper = new OperationHelper({ + sdk, + queryKey: 'walletAuth', + resultKey: 'walletAuth', + onStatus, + }); + + await helper.sendQuery({ + initiate: { + challenge: params.challenge, + walletId: params.walletId, + isPublickey: params.isPublicKey, + }, + }); + + const result = await helper.waitForResult(); + logger.verbose('WalletAuthResponse', result); + + assertOrThrowInvalidResult(result.result); + + forceStatusUpdate(WalletAuthEvent.CARD_TAP); + + logger.info('Completed'); + return result.result; +}; diff --git a/packages/app-inheritance/src/operations/authWallet/types.ts b/packages/app-inheritance/src/operations/authWallet/types.ts new file mode 100644 index 000000000..b6a7f3a4f --- /dev/null +++ b/packages/app-inheritance/src/operations/authWallet/types.ts @@ -0,0 +1,14 @@ +export enum WalletAuthEvent { + INIT = 0, + CARD_TAP = 1, +} + +export type AuthWalletEventHandler = (event: WalletAuthEvent) => void; +export const WALLET_ID_LENGTH = 32; + +export interface IAuthWalletParams { + challenge: Uint8Array; + walletId: Uint8Array; + isPublicKey: boolean; + onEvent?: AuthWalletEventHandler; +} diff --git a/packages/app-inheritance/src/operations/index.ts b/packages/app-inheritance/src/operations/index.ts new file mode 100644 index 000000000..2716dae43 --- /dev/null +++ b/packages/app-inheritance/src/operations/index.ts @@ -0,0 +1 @@ +export * from './authWallet'; diff --git a/packages/app-inheritance/src/operations/types.ts b/packages/app-inheritance/src/operations/types.ts new file mode 100644 index 000000000..492996f4c --- /dev/null +++ b/packages/app-inheritance/src/operations/types.ts @@ -0,0 +1 @@ +export * from './authWallet/types'; diff --git a/packages/app-inheritance/src/types.ts b/packages/app-inheritance/src/types.ts index deae53a38..e92392fb0 100644 --- a/packages/app-inheritance/src/types.ts +++ b/packages/app-inheritance/src/types.ts @@ -1 +1,2 @@ export * from './proto/generated/types'; +export * from './operations/types'; diff --git a/packages/app-inheritance/tests/02.authWallet/__fixtures__/error.ts b/packages/app-inheritance/tests/02.authWallet/__fixtures__/error.ts new file mode 100644 index 000000000..2fd14cdc4 --- /dev/null +++ b/packages/app-inheritance/tests/02.authWallet/__fixtures__/error.ts @@ -0,0 +1,80 @@ +import { + DeviceAppError, + DeviceAppErrorType, + deviceAppErrorTypeDetails, +} from '@cypherock/sdk-interfaces'; +import { Query } from '../../../src/proto/generated/inheritance/core'; +import { IAuthWalletTestCase } from './types'; + +const commonParams = { + params: { + walletId: new Uint8Array([ + 199, 89, 252, 26, 32, 135, 183, 211, 90, 220, 38, 17, 160, 103, 233, 62, + 110, 172, 92, 20, 35, 250, 190, 146, 62, 8, 53, 86, 128, 26, 3, 187, + ]), + challenge: new Uint8Array([ + 199, 89, 252, 26, 32, 135, 183, 211, 90, 220, 38, 17, 160, 103, 233, 62, + 110, 172, 92, 20, 35, 250, 190, 146, 62, 8, 53, 86, 128, 26, 3, 187, 121, + 64, + ]), + isPublicKey: true, + }, + queries: [ + { + name: 'Initate query', + data: Uint8Array.from( + Query.encode( + Query.create({ + walletAuth: { + initiate: { + walletId: new Uint8Array([ + 199, 89, 252, 26, 32, 135, 183, 211, 90, 220, 38, 17, 160, + 103, 233, 62, 110, 172, 92, 20, 35, 250, 190, 146, 62, 8, 53, + 86, 128, 26, 3, 187, + ]), + challenge: new Uint8Array([ + 199, 89, 252, 26, 32, 135, 183, 211, 90, 220, 38, 17, 160, + 103, 233, 62, 110, 172, 92, 20, 35, 250, 190, 146, 62, 8, 53, + 86, 128, 26, 3, 187, 121, 64, + ]), + isPublickey: true, + }, + }, + }), + ).finish(), + ), + }, + ], +}; + +const withUnknownError: IAuthWalletTestCase = { + name: 'With unknown error', + ...commonParams, + results: [ + { + name: 'error', + data: new Uint8Array([10, 4, 18, 2, 8, 1]), + }, + ], + errorInstance: DeviceAppError, + errorMessage: + deviceAppErrorTypeDetails[DeviceAppErrorType.UNKNOWN_ERROR].message, +}; + +const withInvalidAppId: IAuthWalletTestCase = { + name: 'With invalid msg from device', + ...commonParams, + results: [ + { + name: 'error', + data: new Uint8Array([10, 4, 18, 2, 16, 1]), + }, + ], + errorInstance: DeviceAppError, + errorMessage: + deviceAppErrorTypeDetails[DeviceAppErrorType.CORRUPT_DATA].message, +}; + +const error: IAuthWalletTestCase[] = [withUnknownError, withInvalidAppId]; + +export default error; diff --git a/packages/app-inheritance/tests/02.authWallet/__fixtures__/index.ts b/packages/app-inheritance/tests/02.authWallet/__fixtures__/index.ts new file mode 100644 index 000000000..6d0428fb1 --- /dev/null +++ b/packages/app-inheritance/tests/02.authWallet/__fixtures__/index.ts @@ -0,0 +1,12 @@ +import { IFixtures } from './types'; +import error from './error'; +import valid from './valid'; +import invalidArgs from './invalidArgs'; + +const fixtures: IFixtures = { + valid, + error, + invalidArgs, +}; + +export default fixtures; diff --git a/packages/app-inheritance/tests/02.authWallet/__fixtures__/invalidArgs.ts b/packages/app-inheritance/tests/02.authWallet/__fixtures__/invalidArgs.ts new file mode 100644 index 000000000..4d1ad4f43 --- /dev/null +++ b/packages/app-inheritance/tests/02.authWallet/__fixtures__/invalidArgs.ts @@ -0,0 +1,64 @@ +import { IAuthWalletTestCase } from './types'; + +const commonParams = { + queries: [{ name: 'empty', data: new Uint8Array([]) }], + results: [{ name: 'empty', data: new Uint8Array([]) }], + errorInstance: Error, + errorMessage: /AssertionError/, +}; + +const validParams = { + walletId: new Uint8Array([ + 199, 89, 252, 26, 32, 135, 183, 211, 90, 220, 38, 17, 160, 103, 233, 62, + 110, 172, 92, 20, 35, 250, 190, 146, 62, 8, 53, 86, 128, 26, 3, 187, + ]), + challenge: new Uint8Array([ + 199, 89, 252, 26, 32, 135, 183, 211, 90, 220, 38, 17, 160, 103, 233, 62, + 110, 172, 92, 20, 35, 250, 190, 146, 62, 8, 53, 86, 128, 26, 3, 187, 121, + 64, + ]), + isPublicKey: true, +}; + +const invalidArgs: IAuthWalletTestCase[] = [ + { + name: 'Null', + ...commonParams, + params: null as any, + }, + { + name: 'Undefined', + ...commonParams, + params: null as any, + }, + { + name: 'Empty Object', + ...commonParams, + params: {} as any, + }, + { + name: 'No challenge', + ...commonParams, + params: { ...validParams, challenge: undefined } as any, + }, + { + name: 'No wallet id', + ...commonParams, + params: { ...validParams, walletId: undefined } as any, + }, + { + name: 'No isPublicKey', + ...commonParams, + params: { ...validParams, isPublicKey: undefined } as any, + }, + { + name: 'Incorrect length walletId', + ...commonParams, + params: { + ...validParams, + walletId: [0], + } as any, + }, +]; + +export default invalidArgs; diff --git a/packages/app-inheritance/tests/02.authWallet/__fixtures__/types.ts b/packages/app-inheritance/tests/02.authWallet/__fixtures__/types.ts new file mode 100644 index 000000000..26a35690a --- /dev/null +++ b/packages/app-inheritance/tests/02.authWallet/__fixtures__/types.ts @@ -0,0 +1,27 @@ +import { IAuthWalletParams, IWalletAuthResultResponse } from '../../../src'; + +export interface IAuthWalletTestCase { + name: string; + params: IAuthWalletParams; + queries: { + name: string; + data: Uint8Array; + }[]; + results: { + name: string; + data: Uint8Array; + statuses?: { flowStatus: number; expectEventCalls?: number[] }[]; + }[]; + mocks?: { + eventCalls?: number[][]; + }; + output?: Partial; + errorInstance?: any; + [key: string]: any; +} + +export interface IFixtures { + valid: IAuthWalletTestCase[]; + error: IAuthWalletTestCase[]; + invalidArgs: IAuthWalletTestCase[]; +} diff --git a/packages/app-inheritance/tests/02.authWallet/__fixtures__/valid.ts b/packages/app-inheritance/tests/02.authWallet/__fixtures__/valid.ts new file mode 100644 index 000000000..44075d046 --- /dev/null +++ b/packages/app-inheritance/tests/02.authWallet/__fixtures__/valid.ts @@ -0,0 +1,89 @@ +import { hexToUint8Array, createFlowStatus } from '@cypherock/sdk-utils'; +import { Query, Result } from '../../../src/proto/generated/inheritance/core'; +import { IAuthWalletTestCase } from './types'; + +const authenticateWalletWithPublicKey: IAuthWalletTestCase = { + name: 'Authenticate wallet with publickey', + params: { + walletId: new Uint8Array([ + 199, 89, 252, 26, 32, 135, 183, 211, 90, 220, 38, 17, 160, 103, 233, 62, + 110, 172, 92, 20, 35, 250, 190, 146, 62, 8, 53, 86, 128, 26, 3, 187, + ]), + challenge: new Uint8Array([ + 199, 89, 252, 26, 32, 135, 183, 211, 90, 220, 38, 17, 160, 103, 233, 62, + 110, 172, 92, 20, 35, 250, 190, 146, 62, 8, 53, 86, 128, 26, 3, 187, 121, + 64, + ]), + isPublicKey: true, + }, + queries: [ + { + name: 'Initate query', + data: Uint8Array.from( + Query.encode( + Query.create({ + walletAuth: { + initiate: { + walletId: new Uint8Array([ + 199, 89, 252, 26, 32, 135, 183, 211, 90, 220, 38, 17, 160, + 103, 233, 62, 110, 172, 92, 20, 35, 250, 190, 146, 62, 8, 53, + 86, 128, 26, 3, 187, + ]), + challenge: new Uint8Array([ + 199, 89, 252, 26, 32, 135, 183, 211, 90, 220, 38, 17, 160, + 103, 233, 62, 110, 172, 92, 20, 35, 250, 190, 146, 62, 8, 53, + 86, 128, 26, 3, 187, 121, 64, + ]), + isPublickey: true, + }, + }, + }), + ).finish(), + ), + }, + ], + results: [ + { + name: 'result', + data: Uint8Array.from( + Result.encode( + Result.create({ + walletAuth: { + result: { + signature: hexToUint8Array( + '0x032891c403786eed3405bf29304abbcbb5282bc2b30eb3c45759f42bc9bb1b62c6', + ), + publicKey: hexToUint8Array( + '0x032891c403786eed3405bf29304abbcbb5282bc2b30eb3c45759f42bc9bb1b62c6', + ), + }, + }, + }), + ).finish(), + ), + statuses: [ + { + flowStatus: createFlowStatus(0, 0), + expectEventCalls: [0], + }, + { + flowStatus: createFlowStatus(1, 0), + expectEventCalls: [1], + }, + ], + }, + ], + mocks: { eventCalls: [[0], [1]] }, + output: { + signature: hexToUint8Array( + '0x032891c403786eed3405bf29304abbcbb5282bc2b30eb3c45759f42bc9bb1b62c6', + ), + publicKey: hexToUint8Array( + '0x032891c403786eed3405bf29304abbcbb5282bc2b30eb3c45759f42bc9bb1b62c6', + ), + }, +}; + +const valid: IAuthWalletTestCase[] = [authenticateWalletWithPublicKey]; + +export default valid; diff --git a/packages/app-inheritance/tests/02.authWallet/__helpers__/index.ts b/packages/app-inheritance/tests/02.authWallet/__helpers__/index.ts new file mode 100644 index 000000000..bc0172863 --- /dev/null +++ b/packages/app-inheritance/tests/02.authWallet/__helpers__/index.ts @@ -0,0 +1,14 @@ +import * as superMocks from '../../__helpers__'; +import { IAuthWalletTestCase } from '../__fixtures__/types'; + +export function setupMocks(testCase: IAuthWalletTestCase) { + return superMocks.setupMocks(testCase); +} + +export function clearMocks() { + superMocks.clearMocks(); +} + +export function expectMockCalls(testCase: IAuthWalletTestCase) { + superMocks.expectMockCalls(testCase); +} diff --git a/packages/app-inheritance/tests/02.authWallet/index.ts b/packages/app-inheritance/tests/02.authWallet/index.ts new file mode 100644 index 000000000..c1f88259d --- /dev/null +++ b/packages/app-inheritance/tests/02.authWallet/index.ts @@ -0,0 +1,79 @@ +import { MockDeviceConnection } from '@cypherock/sdk-interfaces'; +import { afterEach, beforeEach, describe, expect, test } from '@jest/globals'; + +import { clearMocks, expectMockCalls, setupMocks } from './__helpers__'; +import fixtures from './__fixtures__'; + +import { InheritanceApp } from '../../src/index'; + +describe('InheritanceApp.authWallet', () => { + let connection: MockDeviceConnection; + let inheritanceApp: InheritanceApp; + + beforeEach(async () => { + clearMocks(); + + connection = await MockDeviceConnection.create(); + inheritanceApp = await InheritanceApp.create(connection); + }); + + afterEach(async () => { + await inheritanceApp.destroy(); + }); + + describe('should be able to authenticate wallet', () => { + fixtures.valid.forEach(testCase => { + test(testCase.name, async () => { + const onEvent = setupMocks(testCase); + + const output = await inheritanceApp.authWallet({ + ...testCase.params, + onEvent, + }); + expect(output).toEqual(testCase.output); + + expectMockCalls(testCase); + }); + }); + }); + + describe('should throw error with invalid arguments', () => { + fixtures.invalidArgs.forEach(testCase => { + test(testCase.name, async () => { + setupMocks(testCase); + + const rejectedPromise = inheritanceApp.authWallet(testCase.params); + + await expect(rejectedPromise).rejects.toThrow(testCase.errorInstance); + if (testCase.errorMessage) { + try { + await rejectedPromise; + } catch (error: any) { + expect(error.message).toMatch(testCase.errorMessage); + } + } + }); + }); + }); + + describe('should throw error when device returns error', () => { + fixtures.error.forEach(testCase => { + test(testCase.name, async () => { + setupMocks(testCase); + + const rejectedPromise = inheritanceApp.authWallet(testCase.params); + + await expect(rejectedPromise).rejects.toThrow(testCase.errorInstance); + if (testCase.errorMessage) { + try { + await rejectedPromise; + } catch (error: any) { + expect(error.message).toMatch(testCase.errorMessage); + } + } + + expectMockCalls(testCase); + }); + }); + }); +}); diff --git a/packages/util-prettier-config/index.js b/packages/util-prettier-config/index.js index 9e553b03a..8c65dbb1f 100644 --- a/packages/util-prettier-config/index.js +++ b/packages/util-prettier-config/index.js @@ -1,9 +1,9 @@ module.exports = { - printWidth: 80, - tabWidth: 2, - semi: true, - singleQuote: true, - trailingComma: "all", - bracketSpacing: true, - arrowParens: "avoid", + printWidth: 80, + tabWidth: 2, + semi: true, + singleQuote: true, + trailingComma: 'all', + bracketSpacing: true, + arrowParens: 'avoid', };