From 213187fb29ea6c6585c20937af625f6a251f3cdd Mon Sep 17 00:00:00 2001 From: vickysomtee Date: Thu, 24 Nov 2022 15:24:43 +0100 Subject: [PATCH 01/41] Created the indy-vdr package Signed-off-by: vickysomtee --- packages/indy-vdr/README.md | 78 ++++ packages/indy-vdr/jest.config.ts | 14 + packages/indy-vdr/package.json | 47 +++ packages/indy-vdr/src/index.ts | 5 + .../indy-vdr/tests/action-menu.e2e.test.ts | 354 ++++++++++++++++++ packages/indy-vdr/tests/helpers.ts | 60 +++ packages/indy-vdr/tests/setup.ts | 3 + packages/indy-vdr/tsconfig.build.json | 7 + packages/indy-vdr/tsconfig.json | 6 + 9 files changed, 574 insertions(+) create mode 100644 packages/indy-vdr/README.md create mode 100644 packages/indy-vdr/jest.config.ts create mode 100644 packages/indy-vdr/package.json create mode 100644 packages/indy-vdr/src/index.ts create mode 100644 packages/indy-vdr/tests/action-menu.e2e.test.ts create mode 100644 packages/indy-vdr/tests/helpers.ts create mode 100644 packages/indy-vdr/tests/setup.ts create mode 100644 packages/indy-vdr/tsconfig.build.json create mode 100644 packages/indy-vdr/tsconfig.json diff --git a/packages/indy-vdr/README.md b/packages/indy-vdr/README.md new file mode 100644 index 0000000000..ffd98caf55 --- /dev/null +++ b/packages/indy-vdr/README.md @@ -0,0 +1,78 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript Action Menu Plugin

+

+ License + typescript + @aries-framework/action-menu version + +

+
+ +Action Menu plugin for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript.git). Implements [Aries RFC 0509](https://github.com/hyperledger/aries-rfcs/blob/1795d5c2d36f664f88f5e8045042ace8e573808c/features/0509-action-menu/README.md). + +### Installation + +Make sure you have set up the correct version of Aries Framework JavaScript according to the AFJ repository. To find out which version of AFJ you need to have installed you can run the following command. This will list the required peer dependency for `@aries-framework/core`. + +```sh +npm info "@aries-framework/action-menu" peerDependencies +``` + +Then add the action-menu plugin to your project. + +```sh +yarn add @aries-framework/action-menu +``` + +### Quick start + +In order for this plugin to work, we have to inject it into the agent to access agent functionality. See the example for more information. + +### Example of usage + +```ts +import { ActionMenuModule } from '@aries-framework/action-menu' + +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + actionMenu: new ActionMenuModule(), + /* other custom modules */ + }, +}) + +await agent.initialize() + +// To request root menu to a given connection (menu will be received +// asynchronously in a ActionMenuStateChangedEvent) +await agent.modules.actionMenu.requestMenu({ connectionId }) + +// To select an option from the action menu +await agent.modules.actionMenu.performAction({ + connectionId, + performedAction: { name: 'option-1' }, +}) +``` diff --git a/packages/indy-vdr/jest.config.ts b/packages/indy-vdr/jest.config.ts new file mode 100644 index 0000000000..55c67d70a6 --- /dev/null +++ b/packages/indy-vdr/jest.config.ts @@ -0,0 +1,14 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + name: packageJson.name, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json new file mode 100644 index 0000000000..e5673abf75 --- /dev/null +++ b/packages/indy-vdr/package.json @@ -0,0 +1,47 @@ +{ + "name": "@aries-framework/indy-vdr", + "main": "build/index", + "types": "build/index", + "version": "0.2.5", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/indy-vdr", + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "packages/indy-vdr" + }, + "scripts": { + "build": "yarn run clean && yarn run compile", + "clean": "rimraf -rf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "yarn run build", + "test": "jest" + }, + "dependencies": { + "class-transformer": "0.5.1", + "class-validator": "0.13.1", + "rxjs": "^7.2.0" + }, + "peerDependencies": { + "@aries-framework/core": "0.2.5", + "indy-vdr-test-shared": "^0.1.2" + }, + "devDependencies": { + "@aries-framework/node": "0.2.5", + "indy-vdr-test-shared": "^0.1.2", + "reflect-metadata": "^0.1.13", + "rimraf": "~3.0.2", + "typescript": "~4.3.0" + }, + "peerDependenciesMeta": { + "indy-vdr-test-shared": { + "optional": true + } + } +} diff --git a/packages/indy-vdr/src/index.ts b/packages/indy-vdr/src/index.ts new file mode 100644 index 0000000000..79d8efe525 --- /dev/null +++ b/packages/indy-vdr/src/index.ts @@ -0,0 +1,5 @@ +try { + require("indy-vdr-test-nodejs"); + } catch (error) { + throw new Error("Error registering nodejs bindings for Indy VDR"); + } \ No newline at end of file diff --git a/packages/indy-vdr/tests/action-menu.e2e.test.ts b/packages/indy-vdr/tests/action-menu.e2e.test.ts new file mode 100644 index 0000000000..b15524fd93 --- /dev/null +++ b/packages/indy-vdr/tests/action-menu.e2e.test.ts @@ -0,0 +1,354 @@ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { ConnectionRecord } from '@aries-framework/core' + +import { Agent } from '@aries-framework/core' +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { getAgentOptions, makeConnection } from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' + +import { waitForActionMenuRecord } from './helpers' + +import { + ActionMenu, + ActionMenuModule, + ActionMenuRecord, + ActionMenuRole, + ActionMenuState, +} from '@aries-framework/action-menu' + +const faberAgentOptions = getAgentOptions( + 'Faber Action Menu', + { + endpoints: ['rxjs:faber'], + }, + { + actionMenu: new ActionMenuModule(), + } +) + +const aliceAgentOptions = getAgentOptions( + 'Alice Action Menu', + { + endpoints: ['rxjs:alice'], + }, + { + actionMenu: new ActionMenuModule(), + } +) + +describe('Action Menu', () => { + let faberAgent: Agent<{ + actionMenu: ActionMenuModule + }> + let aliceAgent: Agent<{ + actionMenu: ActionMenuModule + }> + let faberConnection: ConnectionRecord + let aliceConnection: ConnectionRecord + + const rootMenu = new ActionMenu({ + title: 'Welcome', + description: 'This is the root menu', + options: [ + { + name: 'option-1', + description: 'Option 1 description', + title: 'Option 1', + }, + { + name: 'option-2', + description: 'Option 2 description', + title: 'Option 2', + }, + ], + }) + + const submenu1 = new ActionMenu({ + title: 'Menu 1', + description: 'This is first submenu', + options: [ + { + name: 'option-1-1', + description: '1-1 desc', + title: '1-1 title', + }, + { + name: 'option-1-2', + description: '1-1 desc', + title: '1-1 title', + }, + ], + }) + + beforeEach(async () => { + const faberMessages = new Subject() + const aliceMessages = new Subject() + const subjectMap = { + 'rxjs:faber': faberMessages, + 'rxjs:alice': aliceMessages, + } + + faberAgent = new Agent(faberAgentOptions) + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + aliceAgent = new Agent(aliceAgentOptions) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + ;[aliceConnection, faberConnection] = await makeConnection(aliceAgent, faberAgent) + }) + + afterEach(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice requests menu to Faber and selects an option once received', async () => { + testLogger.test('Alice sends menu request to Faber') + let aliceActionMenuRecord = await aliceAgent.modules.actionMenu.requestMenu({ connectionId: aliceConnection.id }) + + testLogger.test('Faber waits for menu request from Alice') + await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.PreparingRootMenu, + }) + + testLogger.test('Faber sends root menu to Alice') + await faberAgent.modules.actionMenu.sendMenu({ connectionId: faberConnection.id, menu: rootMenu }) + + testLogger.test('Alice waits until she receives menu') + aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.PreparingSelection, + }) + + expect(aliceActionMenuRecord.menu).toEqual(rootMenu) + const faberActiveMenu = await faberAgent.modules.actionMenu.findActiveMenu({ + connectionId: faberConnection.id, + role: ActionMenuRole.Responder, + }) + expect(faberActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(faberActiveMenu?.state).toBe(ActionMenuState.AwaitingSelection) + + testLogger.test('Alice selects menu item') + await aliceAgent.modules.actionMenu.performAction({ + connectionId: aliceConnection.id, + performedAction: { name: 'option-1' }, + }) + + testLogger.test('Faber waits for menu selection from Alice') + await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.Done, + }) + + // As Alice has responded, menu should be closed (done state) + const aliceActiveMenu = await aliceAgent.modules.actionMenu.findActiveMenu({ + connectionId: aliceConnection.id, + role: ActionMenuRole.Requester, + }) + expect(aliceActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(aliceActiveMenu?.state).toBe(ActionMenuState.Done) + }) + + test('Faber sends root menu and Alice selects an option', async () => { + testLogger.test('Faber sends root menu to Alice') + await faberAgent.modules.actionMenu.sendMenu({ connectionId: faberConnection.id, menu: rootMenu }) + + testLogger.test('Alice waits until she receives menu') + const aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.PreparingSelection, + }) + + expect(aliceActionMenuRecord.menu).toEqual(rootMenu) + const faberActiveMenu = await faberAgent.modules.actionMenu.findActiveMenu({ + connectionId: faberConnection.id, + role: ActionMenuRole.Responder, + }) + expect(faberActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(faberActiveMenu?.state).toBe(ActionMenuState.AwaitingSelection) + + testLogger.test('Alice selects menu item') + await aliceAgent.modules.actionMenu.performAction({ + connectionId: aliceConnection.id, + performedAction: { name: 'option-1' }, + }) + + testLogger.test('Faber waits for menu selection from Alice') + await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.Done, + }) + + // As Alice has responded, menu should be closed (done state) + const aliceActiveMenu = await aliceAgent.modules.actionMenu.findActiveMenu({ + connectionId: aliceConnection.id, + role: ActionMenuRole.Requester, + }) + expect(aliceActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(aliceActiveMenu?.state).toBe(ActionMenuState.Done) + }) + + test('Menu navigation', async () => { + testLogger.test('Faber sends root menu ') + let faberActionMenuRecord = await faberAgent.modules.actionMenu.sendMenu({ + connectionId: faberConnection.id, + menu: rootMenu, + }) + + const rootThreadId = faberActionMenuRecord.threadId + + testLogger.test('Alice waits until she receives menu') + let aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.PreparingSelection, + }) + + expect(aliceActionMenuRecord.menu).toEqual(rootMenu) + expect(aliceActionMenuRecord.threadId).toEqual(rootThreadId) + + testLogger.test('Alice selects menu item 1') + await aliceAgent.modules.actionMenu.performAction({ + connectionId: aliceConnection.id, + performedAction: { name: 'option-1' }, + }) + + testLogger.test('Faber waits for menu selection from Alice') + faberActionMenuRecord = await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.Done, + }) + + // As Alice has responded, menu should be closed (done state) + let aliceActiveMenu = await aliceAgent.modules.actionMenu.findActiveMenu({ + connectionId: aliceConnection.id, + role: ActionMenuRole.Requester, + }) + expect(aliceActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(aliceActiveMenu?.state).toBe(ActionMenuState.Done) + expect(aliceActiveMenu?.threadId).toEqual(rootThreadId) + + testLogger.test('Faber sends submenu to Alice') + faberActionMenuRecord = await faberAgent.modules.actionMenu.sendMenu({ + connectionId: faberConnection.id, + menu: submenu1, + }) + + testLogger.test('Alice waits until she receives submenu') + aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.PreparingSelection, + }) + + expect(aliceActionMenuRecord.menu).toEqual(submenu1) + expect(aliceActionMenuRecord.threadId).toEqual(rootThreadId) + + testLogger.test('Alice selects menu item 1-1') + await aliceAgent.modules.actionMenu.performAction({ + connectionId: aliceConnection.id, + performedAction: { name: 'option-1-1' }, + }) + + testLogger.test('Faber waits for menu selection from Alice') + faberActionMenuRecord = await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.Done, + }) + + // As Alice has responded, menu should be closed (done state) + aliceActiveMenu = await aliceAgent.modules.actionMenu.findActiveMenu({ + connectionId: aliceConnection.id, + role: ActionMenuRole.Requester, + }) + expect(aliceActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(aliceActiveMenu?.state).toBe(ActionMenuState.Done) + expect(aliceActiveMenu?.threadId).toEqual(rootThreadId) + + testLogger.test('Alice sends menu request to Faber') + aliceActionMenuRecord = await aliceAgent.modules.actionMenu.requestMenu({ connectionId: aliceConnection.id }) + + testLogger.test('Faber waits for menu request from Alice') + faberActionMenuRecord = await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.PreparingRootMenu, + }) + + testLogger.test('This new menu request must have a different thread Id') + expect(faberActionMenuRecord.menu).toBeUndefined() + expect(aliceActionMenuRecord.threadId).not.toEqual(rootThreadId) + expect(faberActionMenuRecord.threadId).toEqual(aliceActionMenuRecord.threadId) + }) + + test('Menu clearing', async () => { + testLogger.test('Faber sends root menu to Alice') + await faberAgent.modules.actionMenu.sendMenu({ connectionId: faberConnection.id, menu: rootMenu }) + + testLogger.test('Alice waits until she receives menu') + let aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.PreparingSelection, + }) + + expect(aliceActionMenuRecord.menu).toEqual(rootMenu) + let faberActiveMenu = await faberAgent.modules.actionMenu.findActiveMenu({ + connectionId: faberConnection.id, + role: ActionMenuRole.Responder, + }) + expect(faberActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(faberActiveMenu?.state).toBe(ActionMenuState.AwaitingSelection) + + await faberAgent.modules.actionMenu.clearActiveMenu({ + connectionId: faberConnection.id, + role: ActionMenuRole.Responder, + }) + + testLogger.test('Alice selects menu item') + await aliceAgent.modules.actionMenu.performAction({ + connectionId: aliceConnection.id, + performedAction: { name: 'option-1' }, + }) + + // Exception + + testLogger.test('Faber rejects selection, as menu has been cleared') + // Faber sends error report to Alice, meaning that her Menu flow will be cleared + aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.Null, + role: ActionMenuRole.Requester, + }) + + testLogger.test('Alice request a new menu') + await aliceAgent.modules.actionMenu.requestMenu({ + connectionId: aliceConnection.id, + }) + + testLogger.test('Faber waits for menu request from Alice') + await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.PreparingRootMenu, + }) + + testLogger.test('Faber sends root menu to Alice') + await faberAgent.modules.actionMenu.sendMenu({ connectionId: faberConnection.id, menu: rootMenu }) + + testLogger.test('Alice waits until she receives menu') + aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { + state: ActionMenuState.PreparingSelection, + }) + + expect(aliceActionMenuRecord.menu).toEqual(rootMenu) + faberActiveMenu = await faberAgent.modules.actionMenu.findActiveMenu({ + connectionId: faberConnection.id, + role: ActionMenuRole.Responder, + }) + expect(faberActiveMenu).toBeInstanceOf(ActionMenuRecord) + expect(faberActiveMenu?.state).toBe(ActionMenuState.AwaitingSelection) + + testLogger.test('Alice selects menu item') + await aliceAgent.modules.actionMenu.performAction({ + connectionId: aliceConnection.id, + performedAction: { name: 'option-1' }, + }) + + testLogger.test('Faber waits for menu selection from Alice') + await waitForActionMenuRecord(faberAgent, { + state: ActionMenuState.Done, + }) + }) +}) diff --git a/packages/indy-vdr/tests/helpers.ts b/packages/indy-vdr/tests/helpers.ts new file mode 100644 index 0000000000..c4044b448b --- /dev/null +++ b/packages/indy-vdr/tests/helpers.ts @@ -0,0 +1,60 @@ +import type { ActionMenuStateChangedEvent, ActionMenuRole, ActionMenuState } from '@aries-framework/action-menu' +import type { Agent } from '@aries-framework/core' +import type { Observable } from 'rxjs' + +import { catchError, filter, firstValueFrom, map, ReplaySubject, timeout } from 'rxjs' + +import { ActionMenuEventTypes } from '@aries-framework/action-menu' + +export async function waitForActionMenuRecord( + agent: Agent, + options: { + threadId?: string + role?: ActionMenuRole + state?: ActionMenuState + previousState?: ActionMenuState | null + timeoutMs?: number + } +) { + const observable = agent.events.observable(ActionMenuEventTypes.ActionMenuStateChanged) + + return waitForActionMenuRecordSubject(observable, options) +} + +export function waitForActionMenuRecordSubject( + subject: ReplaySubject | Observable, + { + threadId, + role, + state, + previousState, + timeoutMs = 10000, + }: { + threadId?: string + role?: ActionMenuRole + state?: ActionMenuState + previousState?: ActionMenuState | null + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter((e) => previousState === undefined || e.payload.previousState === previousState), + filter((e) => threadId === undefined || e.payload.actionMenuRecord.threadId === threadId), + filter((e) => role === undefined || e.payload.actionMenuRecord.role === role), + filter((e) => state === undefined || e.payload.actionMenuRecord.state === state), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `ActionMenuStateChangedEvent event not emitted within specified timeout: { + previousState: ${previousState}, + threadId: ${threadId}, + state: ${state} + }` + ) + }), + map((e) => e.payload.actionMenuRecord) + ) + ) +} diff --git a/packages/indy-vdr/tests/setup.ts b/packages/indy-vdr/tests/setup.ts new file mode 100644 index 0000000000..4955aeb601 --- /dev/null +++ b/packages/indy-vdr/tests/setup.ts @@ -0,0 +1,3 @@ +import 'reflect-metadata' + +jest.setTimeout(20000) diff --git a/packages/indy-vdr/tsconfig.build.json b/packages/indy-vdr/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/indy-vdr/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/indy-vdr/tsconfig.json b/packages/indy-vdr/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/indy-vdr/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} From 6998abea7f298c6a002403320f97ed266ada5251 Mon Sep 17 00:00:00 2001 From: vickysomtee Date: Mon, 28 Nov 2022 10:52:45 +0100 Subject: [PATCH 02/41] feat: create IndyVdrPool Signed-off-by: vickysomtee --- packages/indy-vdr/src/pool/IndyVdrPool.ts | 42 +++++++++++++++++++++++ packages/indy-vdr/src/pool/index.ts | 2 ++ 2 files changed, 44 insertions(+) create mode 100644 packages/indy-vdr/src/pool/IndyVdrPool.ts create mode 100644 packages/indy-vdr/src/pool/index.ts diff --git a/packages/indy-vdr/src/pool/IndyVdrPool.ts b/packages/indy-vdr/src/pool/IndyVdrPool.ts new file mode 100644 index 0000000000..8050049d6e --- /dev/null +++ b/packages/indy-vdr/src/pool/IndyVdrPool.ts @@ -0,0 +1,42 @@ +import type { Logger } from '@aries-framework/core' +import {IndyPool} from '@aries-framework/core' +import { PoolCreate } from "indy-vdr-test-shared" + +interface TransactionAuthorAgreement { + version: `${number}.${number}` | `${number}` + acceptanceMechanism: string +} + +interface IndyVdrPoolConfig { + genesisTransactions: string + isProduction: boolean + indyNamespace: string + transactionAuthorAgreement?: TransactionAuthorAgreement +} + +export class IndyVdrPool { + private pool: IndyPool // Not sure this is the correct type for the pool + private logger: Logger + private poolConfig: IndyVdrPoolConfig + private poolConnected?: Promise + + constructor(poolConfig: IndyVdrPoolConfig, logger: Logger) { + this.logger = logger + this.poolConfig = poolConfig + } + + public get IndyNamespace(): string { + return this.poolConfig.indyNamespace + } + + public get config() { + return this.poolConfig + } + + public async connect() { + this.pool = new PoolCreate({parameters: {}}) + + + + } +} diff --git a/packages/indy-vdr/src/pool/index.ts b/packages/indy-vdr/src/pool/index.ts new file mode 100644 index 0000000000..a10a471490 --- /dev/null +++ b/packages/indy-vdr/src/pool/index.ts @@ -0,0 +1,2 @@ +import * as IndyVdr from 'indy-vdr-test-shared'; + From a7f05cae935e9ef73a22bab4a0bf8ae7adbe24a5 Mon Sep 17 00:00:00 2001 From: vickysomtee Date: Mon, 28 Nov 2022 10:57:42 +0100 Subject: [PATCH 03/41] feat: create IndyVdrPool Signed-off-by: vickysomtee --- packages/indy-vdr/src/pool/IndyVdrPool.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/indy-vdr/src/pool/IndyVdrPool.ts b/packages/indy-vdr/src/pool/IndyVdrPool.ts index 8050049d6e..932439735a 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPool.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPool.ts @@ -36,7 +36,6 @@ export class IndyVdrPool { public async connect() { this.pool = new PoolCreate({parameters: {}}) - - + } } From f2e24550141cbd0f41def0f45c3dbf2eb26d6ab7 Mon Sep 17 00:00:00 2001 From: vickysomtee Date: Mon, 28 Nov 2022 11:06:10 +0100 Subject: [PATCH 04/41] removed e-2-e test Signed-off-by: vickysomtee --- packages/indy-vdr/src/pool/IndyVdrPool.ts | 8 +- .../indy-vdr/tests/action-menu.e2e.test.ts | 354 ------------------ packages/indy-vdr/tests/helpers.ts | 60 --- packages/indy-vdr/tests/setup.ts | 3 - 4 files changed, 3 insertions(+), 422 deletions(-) delete mode 100644 packages/indy-vdr/tests/action-menu.e2e.test.ts delete mode 100644 packages/indy-vdr/tests/helpers.ts delete mode 100644 packages/indy-vdr/tests/setup.ts diff --git a/packages/indy-vdr/src/pool/IndyVdrPool.ts b/packages/indy-vdr/src/pool/IndyVdrPool.ts index 932439735a..916e9dfdb0 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPool.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPool.ts @@ -1,6 +1,6 @@ import type { Logger } from '@aries-framework/core' import {IndyPool} from '@aries-framework/core' -import { PoolCreate } from "indy-vdr-test-shared" +import { PoolCreate } from 'indy-vdr-test-shared' interface TransactionAuthorAgreement { version: `${number}.${number}` | `${number}` @@ -15,7 +15,7 @@ interface IndyVdrPoolConfig { } export class IndyVdrPool { - private pool: IndyPool // Not sure this is the correct type for the pool + // private pool: IndyPool // Not sure this is the correct type for the pool private logger: Logger private poolConfig: IndyVdrPoolConfig private poolConnected?: Promise @@ -34,8 +34,6 @@ export class IndyVdrPool { } public async connect() { - this.pool = new PoolCreate({parameters: {}}) - - + // this.pool = new PoolCreate({parameters: {}}) } } diff --git a/packages/indy-vdr/tests/action-menu.e2e.test.ts b/packages/indy-vdr/tests/action-menu.e2e.test.ts deleted file mode 100644 index b15524fd93..0000000000 --- a/packages/indy-vdr/tests/action-menu.e2e.test.ts +++ /dev/null @@ -1,354 +0,0 @@ -import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' -import type { ConnectionRecord } from '@aries-framework/core' - -import { Agent } from '@aries-framework/core' -import { Subject } from 'rxjs' - -import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' -import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { getAgentOptions, makeConnection } from '../../core/tests/helpers' -import testLogger from '../../core/tests/logger' - -import { waitForActionMenuRecord } from './helpers' - -import { - ActionMenu, - ActionMenuModule, - ActionMenuRecord, - ActionMenuRole, - ActionMenuState, -} from '@aries-framework/action-menu' - -const faberAgentOptions = getAgentOptions( - 'Faber Action Menu', - { - endpoints: ['rxjs:faber'], - }, - { - actionMenu: new ActionMenuModule(), - } -) - -const aliceAgentOptions = getAgentOptions( - 'Alice Action Menu', - { - endpoints: ['rxjs:alice'], - }, - { - actionMenu: new ActionMenuModule(), - } -) - -describe('Action Menu', () => { - let faberAgent: Agent<{ - actionMenu: ActionMenuModule - }> - let aliceAgent: Agent<{ - actionMenu: ActionMenuModule - }> - let faberConnection: ConnectionRecord - let aliceConnection: ConnectionRecord - - const rootMenu = new ActionMenu({ - title: 'Welcome', - description: 'This is the root menu', - options: [ - { - name: 'option-1', - description: 'Option 1 description', - title: 'Option 1', - }, - { - name: 'option-2', - description: 'Option 2 description', - title: 'Option 2', - }, - ], - }) - - const submenu1 = new ActionMenu({ - title: 'Menu 1', - description: 'This is first submenu', - options: [ - { - name: 'option-1-1', - description: '1-1 desc', - title: '1-1 title', - }, - { - name: 'option-1-2', - description: '1-1 desc', - title: '1-1 title', - }, - ], - }) - - beforeEach(async () => { - const faberMessages = new Subject() - const aliceMessages = new Subject() - const subjectMap = { - 'rxjs:faber': faberMessages, - 'rxjs:alice': aliceMessages, - } - - faberAgent = new Agent(faberAgentOptions) - faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) - faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) - await faberAgent.initialize() - - aliceAgent = new Agent(aliceAgentOptions) - aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) - aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) - await aliceAgent.initialize() - ;[aliceConnection, faberConnection] = await makeConnection(aliceAgent, faberAgent) - }) - - afterEach(async () => { - await faberAgent.shutdown() - await faberAgent.wallet.delete() - await aliceAgent.shutdown() - await aliceAgent.wallet.delete() - }) - - test('Alice requests menu to Faber and selects an option once received', async () => { - testLogger.test('Alice sends menu request to Faber') - let aliceActionMenuRecord = await aliceAgent.modules.actionMenu.requestMenu({ connectionId: aliceConnection.id }) - - testLogger.test('Faber waits for menu request from Alice') - await waitForActionMenuRecord(faberAgent, { - state: ActionMenuState.PreparingRootMenu, - }) - - testLogger.test('Faber sends root menu to Alice') - await faberAgent.modules.actionMenu.sendMenu({ connectionId: faberConnection.id, menu: rootMenu }) - - testLogger.test('Alice waits until she receives menu') - aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { - state: ActionMenuState.PreparingSelection, - }) - - expect(aliceActionMenuRecord.menu).toEqual(rootMenu) - const faberActiveMenu = await faberAgent.modules.actionMenu.findActiveMenu({ - connectionId: faberConnection.id, - role: ActionMenuRole.Responder, - }) - expect(faberActiveMenu).toBeInstanceOf(ActionMenuRecord) - expect(faberActiveMenu?.state).toBe(ActionMenuState.AwaitingSelection) - - testLogger.test('Alice selects menu item') - await aliceAgent.modules.actionMenu.performAction({ - connectionId: aliceConnection.id, - performedAction: { name: 'option-1' }, - }) - - testLogger.test('Faber waits for menu selection from Alice') - await waitForActionMenuRecord(faberAgent, { - state: ActionMenuState.Done, - }) - - // As Alice has responded, menu should be closed (done state) - const aliceActiveMenu = await aliceAgent.modules.actionMenu.findActiveMenu({ - connectionId: aliceConnection.id, - role: ActionMenuRole.Requester, - }) - expect(aliceActiveMenu).toBeInstanceOf(ActionMenuRecord) - expect(aliceActiveMenu?.state).toBe(ActionMenuState.Done) - }) - - test('Faber sends root menu and Alice selects an option', async () => { - testLogger.test('Faber sends root menu to Alice') - await faberAgent.modules.actionMenu.sendMenu({ connectionId: faberConnection.id, menu: rootMenu }) - - testLogger.test('Alice waits until she receives menu') - const aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { - state: ActionMenuState.PreparingSelection, - }) - - expect(aliceActionMenuRecord.menu).toEqual(rootMenu) - const faberActiveMenu = await faberAgent.modules.actionMenu.findActiveMenu({ - connectionId: faberConnection.id, - role: ActionMenuRole.Responder, - }) - expect(faberActiveMenu).toBeInstanceOf(ActionMenuRecord) - expect(faberActiveMenu?.state).toBe(ActionMenuState.AwaitingSelection) - - testLogger.test('Alice selects menu item') - await aliceAgent.modules.actionMenu.performAction({ - connectionId: aliceConnection.id, - performedAction: { name: 'option-1' }, - }) - - testLogger.test('Faber waits for menu selection from Alice') - await waitForActionMenuRecord(faberAgent, { - state: ActionMenuState.Done, - }) - - // As Alice has responded, menu should be closed (done state) - const aliceActiveMenu = await aliceAgent.modules.actionMenu.findActiveMenu({ - connectionId: aliceConnection.id, - role: ActionMenuRole.Requester, - }) - expect(aliceActiveMenu).toBeInstanceOf(ActionMenuRecord) - expect(aliceActiveMenu?.state).toBe(ActionMenuState.Done) - }) - - test('Menu navigation', async () => { - testLogger.test('Faber sends root menu ') - let faberActionMenuRecord = await faberAgent.modules.actionMenu.sendMenu({ - connectionId: faberConnection.id, - menu: rootMenu, - }) - - const rootThreadId = faberActionMenuRecord.threadId - - testLogger.test('Alice waits until she receives menu') - let aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { - state: ActionMenuState.PreparingSelection, - }) - - expect(aliceActionMenuRecord.menu).toEqual(rootMenu) - expect(aliceActionMenuRecord.threadId).toEqual(rootThreadId) - - testLogger.test('Alice selects menu item 1') - await aliceAgent.modules.actionMenu.performAction({ - connectionId: aliceConnection.id, - performedAction: { name: 'option-1' }, - }) - - testLogger.test('Faber waits for menu selection from Alice') - faberActionMenuRecord = await waitForActionMenuRecord(faberAgent, { - state: ActionMenuState.Done, - }) - - // As Alice has responded, menu should be closed (done state) - let aliceActiveMenu = await aliceAgent.modules.actionMenu.findActiveMenu({ - connectionId: aliceConnection.id, - role: ActionMenuRole.Requester, - }) - expect(aliceActiveMenu).toBeInstanceOf(ActionMenuRecord) - expect(aliceActiveMenu?.state).toBe(ActionMenuState.Done) - expect(aliceActiveMenu?.threadId).toEqual(rootThreadId) - - testLogger.test('Faber sends submenu to Alice') - faberActionMenuRecord = await faberAgent.modules.actionMenu.sendMenu({ - connectionId: faberConnection.id, - menu: submenu1, - }) - - testLogger.test('Alice waits until she receives submenu') - aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { - state: ActionMenuState.PreparingSelection, - }) - - expect(aliceActionMenuRecord.menu).toEqual(submenu1) - expect(aliceActionMenuRecord.threadId).toEqual(rootThreadId) - - testLogger.test('Alice selects menu item 1-1') - await aliceAgent.modules.actionMenu.performAction({ - connectionId: aliceConnection.id, - performedAction: { name: 'option-1-1' }, - }) - - testLogger.test('Faber waits for menu selection from Alice') - faberActionMenuRecord = await waitForActionMenuRecord(faberAgent, { - state: ActionMenuState.Done, - }) - - // As Alice has responded, menu should be closed (done state) - aliceActiveMenu = await aliceAgent.modules.actionMenu.findActiveMenu({ - connectionId: aliceConnection.id, - role: ActionMenuRole.Requester, - }) - expect(aliceActiveMenu).toBeInstanceOf(ActionMenuRecord) - expect(aliceActiveMenu?.state).toBe(ActionMenuState.Done) - expect(aliceActiveMenu?.threadId).toEqual(rootThreadId) - - testLogger.test('Alice sends menu request to Faber') - aliceActionMenuRecord = await aliceAgent.modules.actionMenu.requestMenu({ connectionId: aliceConnection.id }) - - testLogger.test('Faber waits for menu request from Alice') - faberActionMenuRecord = await waitForActionMenuRecord(faberAgent, { - state: ActionMenuState.PreparingRootMenu, - }) - - testLogger.test('This new menu request must have a different thread Id') - expect(faberActionMenuRecord.menu).toBeUndefined() - expect(aliceActionMenuRecord.threadId).not.toEqual(rootThreadId) - expect(faberActionMenuRecord.threadId).toEqual(aliceActionMenuRecord.threadId) - }) - - test('Menu clearing', async () => { - testLogger.test('Faber sends root menu to Alice') - await faberAgent.modules.actionMenu.sendMenu({ connectionId: faberConnection.id, menu: rootMenu }) - - testLogger.test('Alice waits until she receives menu') - let aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { - state: ActionMenuState.PreparingSelection, - }) - - expect(aliceActionMenuRecord.menu).toEqual(rootMenu) - let faberActiveMenu = await faberAgent.modules.actionMenu.findActiveMenu({ - connectionId: faberConnection.id, - role: ActionMenuRole.Responder, - }) - expect(faberActiveMenu).toBeInstanceOf(ActionMenuRecord) - expect(faberActiveMenu?.state).toBe(ActionMenuState.AwaitingSelection) - - await faberAgent.modules.actionMenu.clearActiveMenu({ - connectionId: faberConnection.id, - role: ActionMenuRole.Responder, - }) - - testLogger.test('Alice selects menu item') - await aliceAgent.modules.actionMenu.performAction({ - connectionId: aliceConnection.id, - performedAction: { name: 'option-1' }, - }) - - // Exception - - testLogger.test('Faber rejects selection, as menu has been cleared') - // Faber sends error report to Alice, meaning that her Menu flow will be cleared - aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { - state: ActionMenuState.Null, - role: ActionMenuRole.Requester, - }) - - testLogger.test('Alice request a new menu') - await aliceAgent.modules.actionMenu.requestMenu({ - connectionId: aliceConnection.id, - }) - - testLogger.test('Faber waits for menu request from Alice') - await waitForActionMenuRecord(faberAgent, { - state: ActionMenuState.PreparingRootMenu, - }) - - testLogger.test('Faber sends root menu to Alice') - await faberAgent.modules.actionMenu.sendMenu({ connectionId: faberConnection.id, menu: rootMenu }) - - testLogger.test('Alice waits until she receives menu') - aliceActionMenuRecord = await waitForActionMenuRecord(aliceAgent, { - state: ActionMenuState.PreparingSelection, - }) - - expect(aliceActionMenuRecord.menu).toEqual(rootMenu) - faberActiveMenu = await faberAgent.modules.actionMenu.findActiveMenu({ - connectionId: faberConnection.id, - role: ActionMenuRole.Responder, - }) - expect(faberActiveMenu).toBeInstanceOf(ActionMenuRecord) - expect(faberActiveMenu?.state).toBe(ActionMenuState.AwaitingSelection) - - testLogger.test('Alice selects menu item') - await aliceAgent.modules.actionMenu.performAction({ - connectionId: aliceConnection.id, - performedAction: { name: 'option-1' }, - }) - - testLogger.test('Faber waits for menu selection from Alice') - await waitForActionMenuRecord(faberAgent, { - state: ActionMenuState.Done, - }) - }) -}) diff --git a/packages/indy-vdr/tests/helpers.ts b/packages/indy-vdr/tests/helpers.ts deleted file mode 100644 index c4044b448b..0000000000 --- a/packages/indy-vdr/tests/helpers.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { ActionMenuStateChangedEvent, ActionMenuRole, ActionMenuState } from '@aries-framework/action-menu' -import type { Agent } from '@aries-framework/core' -import type { Observable } from 'rxjs' - -import { catchError, filter, firstValueFrom, map, ReplaySubject, timeout } from 'rxjs' - -import { ActionMenuEventTypes } from '@aries-framework/action-menu' - -export async function waitForActionMenuRecord( - agent: Agent, - options: { - threadId?: string - role?: ActionMenuRole - state?: ActionMenuState - previousState?: ActionMenuState | null - timeoutMs?: number - } -) { - const observable = agent.events.observable(ActionMenuEventTypes.ActionMenuStateChanged) - - return waitForActionMenuRecordSubject(observable, options) -} - -export function waitForActionMenuRecordSubject( - subject: ReplaySubject | Observable, - { - threadId, - role, - state, - previousState, - timeoutMs = 10000, - }: { - threadId?: string - role?: ActionMenuRole - state?: ActionMenuState - previousState?: ActionMenuState | null - timeoutMs?: number - } -) { - const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject - return firstValueFrom( - observable.pipe( - filter((e) => previousState === undefined || e.payload.previousState === previousState), - filter((e) => threadId === undefined || e.payload.actionMenuRecord.threadId === threadId), - filter((e) => role === undefined || e.payload.actionMenuRecord.role === role), - filter((e) => state === undefined || e.payload.actionMenuRecord.state === state), - timeout(timeoutMs), - catchError(() => { - throw new Error( - `ActionMenuStateChangedEvent event not emitted within specified timeout: { - previousState: ${previousState}, - threadId: ${threadId}, - state: ${state} - }` - ) - }), - map((e) => e.payload.actionMenuRecord) - ) - ) -} diff --git a/packages/indy-vdr/tests/setup.ts b/packages/indy-vdr/tests/setup.ts deleted file mode 100644 index 4955aeb601..0000000000 --- a/packages/indy-vdr/tests/setup.ts +++ /dev/null @@ -1,3 +0,0 @@ -import 'reflect-metadata' - -jest.setTimeout(20000) From 552d3a3fbaa4f1cdc9d721f90009acda90a66d1a Mon Sep 17 00:00:00 2001 From: vickysomtee Date: Wed, 14 Dec 2022 14:04:04 +0100 Subject: [PATCH 05/41] feat: create IndyVdrPool Service Signed-off-by: vickysomtee --- packages/indy-vdr/package.json | 2 +- .../indy-vdr/src/error/IndyVdrPoolError.ts | 7 + packages/indy-vdr/src/index.ts | 8 +- packages/indy-vdr/src/pool/IndyVdrPool.ts | 153 +++++++++++- .../indy-vdr/src/pool/IndyVdrPoolService.ts | 230 ++++++++++++++++++ packages/indy-vdr/src/pool/index.ts | 2 +- 6 files changed, 387 insertions(+), 15 deletions(-) create mode 100644 packages/indy-vdr/src/error/IndyVdrPoolError.ts create mode 100644 packages/indy-vdr/src/pool/IndyVdrPoolService.ts diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json index e5673abf75..4e73444465 100644 --- a/packages/indy-vdr/package.json +++ b/packages/indy-vdr/package.json @@ -34,7 +34,7 @@ }, "devDependencies": { "@aries-framework/node": "0.2.5", - "indy-vdr-test-shared": "^0.1.2", + "indy-vdr-test-shared": "^0.1.13", "reflect-metadata": "^0.1.13", "rimraf": "~3.0.2", "typescript": "~4.3.0" diff --git a/packages/indy-vdr/src/error/IndyVdrPoolError.ts b/packages/indy-vdr/src/error/IndyVdrPoolError.ts new file mode 100644 index 0000000000..a7d26ec8e4 --- /dev/null +++ b/packages/indy-vdr/src/error/IndyVdrPoolError.ts @@ -0,0 +1,7 @@ +import { AriesFrameworkError } from '@aries-framework/core' + +export class IndyVdrPoolError extends AriesFrameworkError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/indy-vdr/src/index.ts b/packages/indy-vdr/src/index.ts index 79d8efe525..6e9d822ced 100644 --- a/packages/indy-vdr/src/index.ts +++ b/packages/indy-vdr/src/index.ts @@ -1,5 +1,5 @@ try { - require("indy-vdr-test-nodejs"); - } catch (error) { - throw new Error("Error registering nodejs bindings for Indy VDR"); - } \ No newline at end of file + require('indy-vdr-test-nodejs') +} catch (error) { + throw new Error('Error registering nodejs bindings for Indy VDR') +} diff --git a/packages/indy-vdr/src/pool/IndyVdrPool.ts b/packages/indy-vdr/src/pool/IndyVdrPool.ts index 916e9dfdb0..e17c3dd960 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPool.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPool.ts @@ -1,13 +1,28 @@ -import type { Logger } from '@aries-framework/core' -import {IndyPool} from '@aries-framework/core' -import { PoolCreate } from 'indy-vdr-test-shared' +import { Logger, TypedArrayEncoder } from '@aries-framework/core' -interface TransactionAuthorAgreement { - version: `${number}.${number}` | `${number}` +import * as IndyVdr from 'indy-vdr-test-shared' +import { AgentDependencies, AgentContext, LedgerError, AriesFrameworkError, Key } from '@aries-framework/core' + +export interface TransactionAuthorAgreement { + version?: `${number}.${number}` | `${number}` acceptanceMechanism: string } -interface IndyVdrPoolConfig { +export interface AuthorAgreement { + digest: string + version: string + text: string + ratification_ts: number + acceptanceMechanisms: AcceptanceMechanisms +} + +export interface AcceptanceMechanisms { + aml: string[] + amlContext: string + version: string +} + +export interface IndyVdrPoolConfig { genesisTransactions: string isProduction: boolean indyNamespace: string @@ -15,10 +30,11 @@ interface IndyVdrPoolConfig { } export class IndyVdrPool { - // private pool: IndyPool // Not sure this is the correct type for the pool + // indyVdr?: typeof IndyVdr + private _pool?: IndyVdr.IndyVdrPool private logger: Logger private poolConfig: IndyVdrPoolConfig - private poolConnected?: Promise + public authorAgreement?: AuthorAgreement | null constructor(poolConfig: IndyVdrPoolConfig, logger: Logger) { this.logger = logger @@ -33,7 +49,126 @@ export class IndyVdrPool { return this.poolConfig } + public get id() { + return this.pool.handle + } + public async connect() { - // this.pool = new PoolCreate({parameters: {}}) + this._pool = new IndyVdr.PoolCreate({ + parameters: { + transactions: this.config.genesisTransactions, + }, + }) + + return this.pool.handle + } + + private get pool(): IndyVdr.IndyVdrPool { + if (!this._pool) { + // TODO: create custom IndyVdrError + throw new AriesFrameworkError('Pool is not connected. Make sure to call .connect() first') + } + + return this._pool + } + + public async close() { + this.pool?.close() + } + + public async submitWriteRequest(agentContext: AgentContext, request: IndyVdr.IndyVdrRequest, signingKey: Key) { + await this.appendTaa(request) + + const signature = await agentContext.wallet.sign({ + data: TypedArrayEncoder.fromString(request.signatureInput), + key: signingKey + }) + + request.setSignature({ + signature + }) + + await this.pool.submitRequest(request) + } + + public async submitReadRequest(request: Request) { + return await this.pool.submitRequest(request) + } + + private async appendTaa(request: IndyVdr.IndyVdrRequest) { + const authorAgreement = await this.getTransactionAuthorAgreement() + const poolTaa = this.config.transactionAuthorAgreement + + // If ledger does not have TAA, we can just send request + if (authorAgreement == null) { + return request + } + + // Ledger has taa but user has not specified which one to use + if (!poolTaa) { + throw new LedgerError( + `Please, specify a transaction author agreement with version and acceptance mechanism. ${JSON.stringify( + authorAgreement + )}` + ) + } + + // Throw an error if the pool doesn't have the specified version and acceptance mechanism + if ( + authorAgreement.version !== poolTaa.version || + !authorAgreement.acceptanceMechanisms.aml.includes(poolTaa.acceptanceMechanism) + ) { + // Throw an error with a helpful message + const errMessage = `Unable to satisfy matching TAA with mechanism ${JSON.stringify( + poolTaa.acceptanceMechanism + )} and version ${poolTaa.version} in pool.\n Found ${JSON.stringify( + authorAgreement.acceptanceMechanisms.aml + )} and version ${authorAgreement.version} in pool.` + throw new LedgerError(errMessage) + } + + const acceptance = IndyVdr.indyVdr.prepareTxnAuthorAgreementAcceptance({ + text: authorAgreement.text, + version: authorAgreement.version, + taaDigest: authorAgreement.digest, + time: Math.floor(new Date().getTime() / 1000), + acceptanceMechanismType: poolTaa.acceptanceMechanism, + }) + + request.setTransactionAuthorAgreementAcceptance({ acceptance }) + } + + private async getTransactionAuthorAgreement( + ): Promise { + // TODO Replace this condition with memoization + if (this.authorAgreement !== undefined) { + return this.authorAgreement + } + + const taaRequest =new IndyVdr.GetTransactionAuthorAgreementRequest({}) + const taaResponse = await this.submitReadRequest(taaRequest) + + const acceptanceMechanismRequest = new IndyVdr.GetAcceptanceMechanismsRequest({}) + const acceptanceMechanismResponse = await this.submitReadRequest( + acceptanceMechanismRequest + ) + + const taaData = taaResponse.result.data + + // TAA can be null + if (taaData == null) { + this.authorAgreement = null + return null + } + + // If TAA is not null, we can be sure AcceptanceMechanisms is also not null + const authorAgreement = taaData as Omit + const acceptanceMechanisms = acceptanceMechanismResponse.result.data as AcceptanceMechanisms + this.authorAgreement = { + ...authorAgreement, + acceptanceMechanisms, + } + + return this.authorAgreement } } diff --git a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts new file mode 100644 index 0000000000..9e1460102e --- /dev/null +++ b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts @@ -0,0 +1,230 @@ +import type { AgentContext } from '../../../core/src/agent' +import { GetNymRequest, indyVdr,} from 'indy-vdr-test-shared' + +import { + AgentDependencies, + Logger, + InjectionSymbols, + injectable, + inject, + LedgerError, + LedgerNotConfiguredError, + LedgerNotFoundError, +} from '@aries-framework/core' + +import { IndyVdrError, GetNymResponse } from 'indy-vdr-test-shared' +import { CacheRepository, PersistedLruCache } from '../../../core/src/cache' +import { IndySdkError } from '../../../core/src/error' +import { isSelfCertifiedDid } from '../../../core/src/utils/did' +import { isIndyError } from '../../../core/src/utils/indyError' +import { allSettled, onlyFulfilled, onlyRejected } from '../../../core/src/utils/promises' +import { assertIndyWallet } from '../../../core/src/wallet/util/assertIndyWallet' +import { IndyVdrPool } from './IndyVdrPool' + +export const DID_POOL_CACHE_ID = 'DID_POOL_CACHE' +export const DID_POOL_CACHE_LIMIT = 500 +export interface CachedDidResponse { + nymResponse: { + did: string + verkey: string + } + poolId?: string +} +@injectable() +export class IndyVdrPoolService { + public pools: IndyVdrPool[] = [] + private logger: Logger + private indyVdr!: typeof indyVdr + private agentDependencies: AgentDependencies + private didCache: PersistedLruCache + + public constructor( + cacheRepository: CacheRepository, + @inject(InjectionSymbols.AgentDependencies) agentDependencies: AgentDependencies, + @inject(InjectionSymbols.Logger) logger: Logger + ) { + this.logger = logger + this.agentDependencies = agentDependencies + + this.didCache = new PersistedLruCache(DID_POOL_CACHE_ID, DID_POOL_CACHE_LIMIT, cacheRepository) + } + + /** + * Create connections to all ledger pools + */ + public async connectToPools() { + const handleArray: number[] = [] + // Sequentially connect to pools so we don't use up too many resources connecting in parallel + for (const pool of this.pools) { + this.logger.debug(`Connecting to pool: ${pool.id}`) + const poolHandle = await pool.connect() + this.logger.debug(`Finished connection to pool: ${pool.id}`) + handleArray.push(poolHandle) + } + return handleArray + } + + /** + * Get the most appropriate pool for the given did. The algorithm is based on the approach as described in this document: + * https://docs.google.com/document/d/109C_eMsuZnTnYe2OAd02jAts1vC4axwEKIq7_4dnNVA/edit + */ + public async getPoolForDid(agentContext: AgentContext, did: string): Promise<{ pool: IndyVdrPool }> { + // Check if the did starts with did:indy + if (did.startsWith('did:indy')) { + const nameSpace = did.split(':')[2] + + const pool = this.pools.find((pool) => pool.IndyNamespace === nameSpace) + + if (pool) return { pool } + + throw new LedgerNotFoundError('Pool not found') + } else { + return await this.getPoolForLegacyDid(agentContext, did) + } + } + + private async getPoolForLegacyDid( + agentContext: AgentContext, + did: string + ): Promise<{ pool: IndyVdrPool; did: string }> { + const pools = this.pools + + if (pools.length === 0) { + throw new LedgerNotConfiguredError( + "No indy ledgers configured. Provide at least one pool configuration in the 'indyLedgers' agent configuration" + ) + } + + const cachedNymResponse = await this.didCache.get(agentContext, did) + const pool = this.pools.find((pool) => pool.config.indyNamespace === cachedNymResponse?.poolId) + + // If we have the nym response with associated pool in the cache, we'll use that + if (cachedNymResponse && pool) { + this.logger.trace(`Found ledger id '${pool.id}' for did '${did}' in cache`) + return { did: cachedNymResponse.nymResponse.did, pool } + } + + const { successful, rejected } = await this.getSettledDidResponsesFromPools(did, pools) + + if (successful.length === 0) { + const allNotFound = rejected.every((e) => e.reason instanceof LedgerNotFoundError) + const rejectedOtherThanNotFound = rejected.filter((e) => !(e.reason instanceof LedgerNotFoundError)) + + // All ledgers returned response that the did was not found + if (allNotFound) { + throw new LedgerNotFoundError(`Did '${did}' not found on any of the ledgers (total ${this.pools.length}).`) + } + + // one or more of the ledgers returned an unknown error + throw new LedgerError( + `Unknown error retrieving did '${did}' from '${rejectedOtherThanNotFound.length}' of '${pools.length}' ledgers`, + { cause: rejectedOtherThanNotFound[0].reason } + ) + } + + // If there are self certified DIDs we always prefer it over non self certified DIDs + // We take the first self certifying DID as we take the order in the + // indyLedgers config as the order of preference of ledgers + let value = successful.find((response) => isSelfCertifiedDid(response.value.did.nymResponse.did, response.value.did.nymResponse.verkey))?.value + + if (!value) { + // Split between production and nonProduction ledgers. If there is at least one + // successful response from a production ledger, only keep production ledgers + // otherwise we only keep the non production ledgers. + const production = successful.filter((s) => s.value.pool.config.isProduction) + const nonProduction = successful.filter((s) => !s.value.pool.config.isProduction) + const productionOrNonProduction = production.length >= 1 ? production : nonProduction + + // We take the first value as we take the order in the indyLedgers config as + // the order of preference of ledgers + value = productionOrNonProduction[0].value + } + + await this.didCache.set(agentContext, did, { + nymResponse: { + did: value.did.nymResponse.did, + verkey: value.did.nymResponse.verkey, + }, + }) + return { pool: value.pool, did: value.did.nymResponse.did } + } + + private async getSettledDidResponsesFromPools(did: string, pools: IndyVdrPool[]) { + this.logger.trace(`Retrieving did '${did}' from ${pools.length} ledgers`) + const didResponses = await allSettled(pools.map((pool) => this.getDidFromPool(did, pool))) + + const successful = onlyFulfilled(didResponses) + this.logger.trace(`Retrieved ${successful.length} responses from ledgers for did '${did}'`) + + const rejected = onlyRejected(didResponses) + + return { + rejected, + successful, + } + } + + /** + * Get the most appropriate pool for the given indyNamespace + */ + public getPoolForNamespace(indyNamespace: string) { + if (this.pools.length === 0) { + throw new LedgerNotConfiguredError( + "No indy ledgers configured. Provide at least one pool configuration in the 'indyLedgers' agent configuration" + ) + } + + if (!indyNamespace) { + this.logger.warn('Not passing the indyNamespace is deprecated and will be removed in the future version.') + return this.pools[0] + } + + const pool = this.pools.find((pool) => pool.config.indyNamespace === indyNamespace) // TODO check if this is corect + + if (!pool) { + throw new LedgerNotFoundError(`No ledgers found for IndyNamespace '${indyNamespace}'.`) + } + + return pool + } + + private async getDidFromPool(did: string, pool: IndyVdrPool): Promise { + try { + this.logger.trace(`Get public did '${did}' from ledger '${pool.id}'`) + const request = await new GetNymRequest({ dest: did }) + + this.logger.trace(`Submitting get did request for did '${did}' to ledger '${pool.id}'`) + const response = await pool.submitReadRequest(request) + + if (!response.result.data) { + throw new LedgerError('Not Found') + } + + const result = JSON.parse(response.result.data) + + this.logger.trace(`Retrieved did '${did}' from ledger '${pool.id}'`, result) + + return { + did: result, + pool, + response, + } + } catch (error) { + this.logger.trace(`Error retrieving did '${did}' from ledger '${pool.id}'`, { + error, + did, + }) + if (isIndyError(error, 'LedgerNotFound')) { + throw new LedgerNotFoundError(`Did '${did}' not found on ledger ${pool.id}`) + } else { + throw isIndyError(error) ? new IndySdkError(error) : error + } + } + } +} + +export interface PublicDidRequest { + did: CachedDidResponse + pool: IndyVdrPool + response: GetNymResponse +} diff --git a/packages/indy-vdr/src/pool/index.ts b/packages/indy-vdr/src/pool/index.ts index a10a471490..8c09951384 100644 --- a/packages/indy-vdr/src/pool/index.ts +++ b/packages/indy-vdr/src/pool/index.ts @@ -1,2 +1,2 @@ -import * as IndyVdr from 'indy-vdr-test-shared'; +export * from './IndyVdrPool' From 3cb42597e2041f6e618aecd96ad77ec49b5f422a Mon Sep 17 00:00:00 2001 From: vickysomtee Date: Thu, 15 Dec 2022 21:23:18 +0100 Subject: [PATCH 06/41] feat: created the IndyVdrNotFoundError Signed-off-by: vickysomtee --- .../src/error/{IndyVdrPoolError.ts => IndyVdrError.ts} | 2 +- packages/indy-vdr/src/error/IndyVdrNotFound.ts | 7 +++++++ packages/indy-vdr/src/error/index.ts | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) rename packages/indy-vdr/src/error/{IndyVdrPoolError.ts => IndyVdrError.ts} (74%) create mode 100644 packages/indy-vdr/src/error/IndyVdrNotFound.ts create mode 100644 packages/indy-vdr/src/error/index.ts diff --git a/packages/indy-vdr/src/error/IndyVdrPoolError.ts b/packages/indy-vdr/src/error/IndyVdrError.ts similarity index 74% rename from packages/indy-vdr/src/error/IndyVdrPoolError.ts rename to packages/indy-vdr/src/error/IndyVdrError.ts index a7d26ec8e4..501f428640 100644 --- a/packages/indy-vdr/src/error/IndyVdrPoolError.ts +++ b/packages/indy-vdr/src/error/IndyVdrError.ts @@ -1,6 +1,6 @@ import { AriesFrameworkError } from '@aries-framework/core' -export class IndyVdrPoolError extends AriesFrameworkError { +export class IndyVdrError extends AriesFrameworkError { public constructor(message: string, { cause }: { cause?: Error } = {}) { super(message, { cause }) } diff --git a/packages/indy-vdr/src/error/IndyVdrNotFound.ts b/packages/indy-vdr/src/error/IndyVdrNotFound.ts new file mode 100644 index 0000000000..09bcc2277c --- /dev/null +++ b/packages/indy-vdr/src/error/IndyVdrNotFound.ts @@ -0,0 +1,7 @@ +import { IndyVdrError } from "./IndyVdrError" + +export class IndyVdrNotFoundError extends IndyVdrError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } + } \ No newline at end of file diff --git a/packages/indy-vdr/src/error/index.ts b/packages/indy-vdr/src/error/index.ts new file mode 100644 index 0000000000..627a3517b1 --- /dev/null +++ b/packages/indy-vdr/src/error/index.ts @@ -0,0 +1,2 @@ +export * from './IndyVdrError' +export * from './IndyVdrNotFound' \ No newline at end of file From 8794d1e882c9b946fc07dce0771572091c45bb13 Mon Sep 17 00:00:00 2001 From: vickysomtee Date: Tue, 20 Dec 2022 10:49:36 +0100 Subject: [PATCH 07/41] refactor: IndyVdr package Work Funded by the Government of Ontario Signed-off-by: vickysomtee --- packages/indy-vdr/src/pool/DidIdentifier.ts | 1 + packages/indy-vdr/src/pool/IndyVdrPool.ts | 38 ++++---- .../indy-vdr/src/pool/IndyVdrPoolService.ts | 91 ++++++++++--------- packages/indy-vdr/src/utils/did.ts | 65 +++++++++++++ packages/indy-vdr/src/utils/promises.ts | 44 +++++++++ 5 files changed, 176 insertions(+), 63 deletions(-) create mode 100644 packages/indy-vdr/src/pool/DidIdentifier.ts create mode 100644 packages/indy-vdr/src/utils/did.ts create mode 100644 packages/indy-vdr/src/utils/promises.ts diff --git a/packages/indy-vdr/src/pool/DidIdentifier.ts b/packages/indy-vdr/src/pool/DidIdentifier.ts new file mode 100644 index 0000000000..39384a2c8d --- /dev/null +++ b/packages/indy-vdr/src/pool/DidIdentifier.ts @@ -0,0 +1 @@ +export const DID_INDY_REGEX = /^did:indy:((?:[a-z][_a-z0-9-]*)(?::[a-z][_a-z0-9-]*)):([1-9A-HJ-NP-Za-km-z]{21,22})$/ \ No newline at end of file diff --git a/packages/indy-vdr/src/pool/IndyVdrPool.ts b/packages/indy-vdr/src/pool/IndyVdrPool.ts index e17c3dd960..445fc30d87 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPool.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPool.ts @@ -1,7 +1,9 @@ -import { Logger, TypedArrayEncoder } from '@aries-framework/core' - import * as IndyVdr from 'indy-vdr-test-shared' -import { AgentDependencies, AgentContext, LedgerError, AriesFrameworkError, Key } from '@aries-framework/core' +import { GetTransactionAuthorAgreementRequest, GetAcceptanceMechanismsRequest, PoolCreate, IndyVdrRequest, IndyVdrPool as indyVdrPool, indyVdr } from 'indy-vdr-test-shared' +import { Logger, TypedArrayEncoder, AgentContext, AriesFrameworkError, Key } from '@aries-framework/core' + +import { IndyVdrError, IndyVdrNotFoundError } from '../error' + export interface TransactionAuthorAgreement { version?: `${number}.${number}` | `${number}` @@ -31,7 +33,7 @@ export interface IndyVdrPoolConfig { export class IndyVdrPool { // indyVdr?: typeof IndyVdr - private _pool?: IndyVdr.IndyVdrPool + private _pool?: indyVdrPool private logger: Logger private poolConfig: IndyVdrPoolConfig public authorAgreement?: AuthorAgreement | null @@ -41,7 +43,7 @@ export class IndyVdrPool { this.poolConfig = poolConfig } - public get IndyNamespace(): string { + public get indyNamespace(): string { return this.poolConfig.indyNamespace } @@ -49,12 +51,8 @@ export class IndyVdrPool { return this.poolConfig } - public get id() { - return this.pool.handle - } - public async connect() { - this._pool = new IndyVdr.PoolCreate({ + this._pool = new PoolCreate({ parameters: { transactions: this.config.genesisTransactions, }, @@ -63,7 +61,7 @@ export class IndyVdrPool { return this.pool.handle } - private get pool(): IndyVdr.IndyVdrPool { + private get pool(): indyVdrPool { if (!this._pool) { // TODO: create custom IndyVdrError throw new AriesFrameworkError('Pool is not connected. Make sure to call .connect() first') @@ -76,7 +74,7 @@ export class IndyVdrPool { this.pool?.close() } - public async submitWriteRequest(agentContext: AgentContext, request: IndyVdr.IndyVdrRequest, signingKey: Key) { + public async submitWriteRequest(agentContext: AgentContext, request: Request, signingKey: Key) { await this.appendTaa(request) const signature = await agentContext.wallet.sign({ @@ -88,14 +86,14 @@ export class IndyVdrPool { signature }) - await this.pool.submitRequest(request) + return await this.pool.submitRequest(request) } - public async submitReadRequest(request: Request) { + public async submitReadRequest(request: Request) { return await this.pool.submitRequest(request) } - private async appendTaa(request: IndyVdr.IndyVdrRequest) { + private async appendTaa(request: IndyVdrRequest) { const authorAgreement = await this.getTransactionAuthorAgreement() const poolTaa = this.config.transactionAuthorAgreement @@ -106,7 +104,7 @@ export class IndyVdrPool { // Ledger has taa but user has not specified which one to use if (!poolTaa) { - throw new LedgerError( + throw new IndyVdrError( `Please, specify a transaction author agreement with version and acceptance mechanism. ${JSON.stringify( authorAgreement )}` @@ -124,10 +122,10 @@ export class IndyVdrPool { )} and version ${poolTaa.version} in pool.\n Found ${JSON.stringify( authorAgreement.acceptanceMechanisms.aml )} and version ${authorAgreement.version} in pool.` - throw new LedgerError(errMessage) + throw new IndyVdrError(errMessage) } - const acceptance = IndyVdr.indyVdr.prepareTxnAuthorAgreementAcceptance({ + const acceptance = indyVdr.prepareTxnAuthorAgreementAcceptance({ text: authorAgreement.text, version: authorAgreement.version, taaDigest: authorAgreement.digest, @@ -145,10 +143,10 @@ export class IndyVdrPool { return this.authorAgreement } - const taaRequest =new IndyVdr.GetTransactionAuthorAgreementRequest({}) + const taaRequest = new GetTransactionAuthorAgreementRequest({}) const taaResponse = await this.submitReadRequest(taaRequest) - const acceptanceMechanismRequest = new IndyVdr.GetAcceptanceMechanismsRequest({}) + const acceptanceMechanismRequest = new GetAcceptanceMechanismsRequest({}) const acceptanceMechanismResponse = await this.submitReadRequest( acceptanceMechanismRequest ) diff --git a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts index 9e1460102e..d06c196d78 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts @@ -1,40 +1,35 @@ -import type { AgentContext } from '../../../core/src/agent' -import { GetNymRequest, indyVdr,} from 'indy-vdr-test-shared' +import { GetNymRequest, GetNymResponse } from 'indy-vdr-test-shared' import { AgentDependencies, Logger, InjectionSymbols, injectable, + AgentContext, inject, - LedgerError, LedgerNotConfiguredError, - LedgerNotFoundError, } from '@aries-framework/core' -import { IndyVdrError, GetNymResponse } from 'indy-vdr-test-shared' +import { DID_INDY_REGEX } from './DidIdentifier' import { CacheRepository, PersistedLruCache } from '../../../core/src/cache' -import { IndySdkError } from '../../../core/src/error' -import { isSelfCertifiedDid } from '../../../core/src/utils/did' -import { isIndyError } from '../../../core/src/utils/indyError' -import { allSettled, onlyFulfilled, onlyRejected } from '../../../core/src/utils/promises' -import { assertIndyWallet } from '../../../core/src/wallet/util/assertIndyWallet' +import { isSelfCertifiedDid } from '../utils/did' +import { allSettled, onlyFulfilled, onlyRejected } from '../utils/promises' import { IndyVdrPool } from './IndyVdrPool' +import { IndyVdrError, IndyVdrNotFoundError } from '../error' -export const DID_POOL_CACHE_ID = 'DID_POOL_CACHE' +export const INDY_VDR_LEGACY_DID_POOL_CACHE_ID = 'INDY_VDR_LEGACY_DID_POOL_CACHE' export const DID_POOL_CACHE_LIMIT = 500 export interface CachedDidResponse { nymResponse: { did: string verkey: string } - poolId?: string + indyNamespace: string } @injectable() export class IndyVdrPoolService { public pools: IndyVdrPool[] = [] private logger: Logger - private indyVdr!: typeof indyVdr private agentDependencies: AgentDependencies private didCache: PersistedLruCache @@ -46,7 +41,7 @@ export class IndyVdrPoolService { this.logger = logger this.agentDependencies = agentDependencies - this.didCache = new PersistedLruCache(DID_POOL_CACHE_ID, DID_POOL_CACHE_LIMIT, cacheRepository) + this.didCache = new PersistedLruCache(INDY_VDR_LEGACY_DID_POOL_CACHE_ID, DID_POOL_CACHE_LIMIT, cacheRepository) } /** @@ -56,28 +51,38 @@ export class IndyVdrPoolService { const handleArray: number[] = [] // Sequentially connect to pools so we don't use up too many resources connecting in parallel for (const pool of this.pools) { - this.logger.debug(`Connecting to pool: ${pool.id}`) + this.logger.debug(`Connecting to pool: ${pool.indyNamespace}`) const poolHandle = await pool.connect() - this.logger.debug(`Finished connection to pool: ${pool.id}`) + this.logger.debug(`Finished connection to pool: ${pool.indyNamespace}`) handleArray.push(poolHandle) } return handleArray } /** - * Get the most appropriate pool for the given did. The algorithm is based on the approach as described in this document: + * Get the most appropriate pool for the given did. + * If the did is a qualified indy did, the pool will be determined based on the namespace. + * If it is a legacy unqualified indy did, the pool will be determined based on the algorithm as described in this document: * https://docs.google.com/document/d/109C_eMsuZnTnYe2OAd02jAts1vC4axwEKIq7_4dnNVA/edit */ - public async getPoolForDid(agentContext: AgentContext, did: string): Promise<{ pool: IndyVdrPool }> { + public async getPoolForDid(agentContext: AgentContext, did: string): Promise< IndyVdrPool > { // Check if the did starts with did:indy + const match = did.match(DID_INDY_REGEX) + + // if (match) { + // const [, namespace] = match + // } else { + // // legacy way + // } + if (did.startsWith('did:indy')) { const nameSpace = did.split(':')[2] - const pool = this.pools.find((pool) => pool.IndyNamespace === nameSpace) + const pool = this.pools.find((pool) => pool.indyNamespace === nameSpace) - if (pool) return { pool } + if (pool) return pool - throw new LedgerNotFoundError('Pool not found') + throw new IndyVdrNotFoundError('Pool not found') } else { return await this.getPoolForLegacyDid(agentContext, did) } @@ -86,7 +91,7 @@ export class IndyVdrPoolService { private async getPoolForLegacyDid( agentContext: AgentContext, did: string - ): Promise<{ pool: IndyVdrPool; did: string }> { + ): Promise { const pools = this.pools if (pools.length === 0) { @@ -96,27 +101,27 @@ export class IndyVdrPoolService { } const cachedNymResponse = await this.didCache.get(agentContext, did) - const pool = this.pools.find((pool) => pool.config.indyNamespace === cachedNymResponse?.poolId) + const pool = this.pools.find((pool) => pool.indyNamespace === cachedNymResponse?.indyNamespace) // If we have the nym response with associated pool in the cache, we'll use that if (cachedNymResponse && pool) { - this.logger.trace(`Found ledger id '${pool.id}' for did '${did}' in cache`) - return { did: cachedNymResponse.nymResponse.did, pool } + this.logger.trace(`Found ledger id '${pool.indyNamespace}' for did '${did}' in cache`) + return pool } const { successful, rejected } = await this.getSettledDidResponsesFromPools(did, pools) if (successful.length === 0) { - const allNotFound = rejected.every((e) => e.reason instanceof LedgerNotFoundError) - const rejectedOtherThanNotFound = rejected.filter((e) => !(e.reason instanceof LedgerNotFoundError)) + const allNotFound = rejected.every((e) => e.reason instanceof IndyVdrNotFoundError) + const rejectedOtherThanNotFound = rejected.filter((e) => !(e.reason instanceof IndyVdrNotFoundError)) // All ledgers returned response that the did was not found if (allNotFound) { - throw new LedgerNotFoundError(`Did '${did}' not found on any of the ledgers (total ${this.pools.length}).`) + throw new IndyVdrNotFoundError(`Did '${did}' not found on any of the ledgers (total ${this.pools.length}).`) } // one or more of the ledgers returned an unknown error - throw new LedgerError( + throw new IndyVdrError( `Unknown error retrieving did '${did}' from '${rejectedOtherThanNotFound.length}' of '${pools.length}' ledgers`, { cause: rejectedOtherThanNotFound[0].reason } ) @@ -125,7 +130,9 @@ export class IndyVdrPoolService { // If there are self certified DIDs we always prefer it over non self certified DIDs // We take the first self certifying DID as we take the order in the // indyLedgers config as the order of preference of ledgers - let value = successful.find((response) => isSelfCertifiedDid(response.value.did.nymResponse.did, response.value.did.nymResponse.verkey))?.value + let value = successful.find((response) => + isSelfCertifiedDid(response.value.did.nymResponse.did, response.value.did.nymResponse.verkey) + )?.value if (!value) { // Split between production and nonProduction ledgers. If there is at least one @@ -145,8 +152,9 @@ export class IndyVdrPoolService { did: value.did.nymResponse.did, verkey: value.did.nymResponse.verkey, }, + indyNamespace: value.did.indyNamespace, }) - return { pool: value.pool, did: value.did.nymResponse.did } + return value.pool } private async getSettledDidResponsesFromPools(did: string, pools: IndyVdrPool[]) { @@ -179,10 +187,10 @@ export class IndyVdrPoolService { return this.pools[0] } - const pool = this.pools.find((pool) => pool.config.indyNamespace === indyNamespace) // TODO check if this is corect + const pool = this.pools.find((pool) => pool.indyNamespace === indyNamespace) if (!pool) { - throw new LedgerNotFoundError(`No ledgers found for IndyNamespace '${indyNamespace}'.`) + throw new IndyVdrNotFoundError(`No ledgers found for IndyNamespace '${indyNamespace}'.`) } return pool @@ -190,19 +198,20 @@ export class IndyVdrPoolService { private async getDidFromPool(did: string, pool: IndyVdrPool): Promise { try { - this.logger.trace(`Get public did '${did}' from ledger '${pool.id}'`) + this.logger.trace(`Get public did '${did}' from ledger '${pool.indyNamespace}'`) const request = await new GetNymRequest({ dest: did }) - this.logger.trace(`Submitting get did request for did '${did}' to ledger '${pool.id}'`) + this.logger.trace(`Submitting get did request for did '${did}' to ledger '${pool.indyNamespace}'`) const response = await pool.submitReadRequest(request) if (!response.result.data) { - throw new LedgerError('Not Found') + // TODO: Set a descriptive message + throw new IndyVdrError('Not Found') } const result = JSON.parse(response.result.data) - this.logger.trace(`Retrieved did '${did}' from ledger '${pool.id}'`, result) + this.logger.trace(`Retrieved did '${did}' from ledger '${pool.indyNamespace}'`, result) return { did: result, @@ -210,15 +219,11 @@ export class IndyVdrPoolService { response, } } catch (error) { - this.logger.trace(`Error retrieving did '${did}' from ledger '${pool.id}'`, { + this.logger.trace(`Error retrieving did '${did}' from ledger '${pool.indyNamespace}'`, { error, did, }) - if (isIndyError(error, 'LedgerNotFound')) { - throw new LedgerNotFoundError(`Did '${did}' not found on ledger ${pool.id}`) - } else { - throw isIndyError(error) ? new IndySdkError(error) : error - } + throw error } } } diff --git a/packages/indy-vdr/src/utils/did.ts b/packages/indy-vdr/src/utils/did.ts new file mode 100644 index 0000000000..a9f2c95d02 --- /dev/null +++ b/packages/indy-vdr/src/utils/did.ts @@ -0,0 +1,65 @@ +/** + * Based on DidUtils implementation in Aries Framework .NET + * @see: https://github.com/hyperledger/aries-framework-dotnet/blob/f90eaf9db8548f6fc831abea917e906201755763/src/Hyperledger.Aries/Utils/DidUtils.cs + * + * Some context about full verkeys versus abbreviated verkeys: + * A standard verkey is 32 bytes, and by default in Indy the DID is chosen as the first 16 bytes of that key, before base58 encoding. + * An abbreviated verkey replaces the first 16 bytes of the verkey with ~ when it matches the DID. + * + * When a full verkey is used to register on the ledger, this is stored as a full verkey on the ledger and also returned from the ledger as a full verkey. + * The same applies to an abbreviated verkey. If an abbreviated verkey is used to register on the ledger, this is stored as an abbreviated verkey on the ledger and also returned from the ledger as an abbreviated verkey. + * + * For this reason we need some methods to check whether verkeys are full or abbreviated, so we can align this with `indy.abbreviateVerkey` + * + * Aries Framework .NET also abbreviates verkey before sending to ledger: + * https://github.com/hyperledger/aries-framework-dotnet/blob/f90eaf9db8548f6fc831abea917e906201755763/src/Hyperledger.Aries/Ledger/DefaultLedgerService.cs#L139-L147 + */ + + +import { TypedArrayEncoder } from '@aries-framework/core' + +export const FULL_VERKEY_REGEX = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/ +export const ABBREVIATED_VERKEY_REGEX = /^~[1-9A-HJ-NP-Za-km-z]{21,22}$/ +export const VERKEY_REGEX = new RegExp(`${FULL_VERKEY_REGEX.source}|${ABBREVIATED_VERKEY_REGEX.source}`) +export const DID_REGEX = /^did:([a-z]+):([a-zA-z\d]+)/ +export const DID_IDENTIFIER_REGEX = /^[a-zA-z\d-]+$/ + +/** + * Check whether the did is a self certifying did. If the verkey is abbreviated this method + * will always return true. Make sure that the verkey you pass in this method belongs to the + * did passed in + * + * @return Boolean indicating whether the did is self certifying + */ +export function isSelfCertifiedDid(did: string, verkey: string): boolean { + // If the verkey is Abbreviated, it means the full verkey + // is the did + the verkey + if (isAbbreviatedVerkey(verkey)) { + return true + } + + const didFromVerkey = indyDidFromPublicKeyBase58(verkey) + + if (didFromVerkey === did) { + return true + } + + return false +} + +export function indyDidFromPublicKeyBase58(publicKeyBase58: string): string { + const buffer = TypedArrayEncoder.fromBase58(publicKeyBase58) + + const did = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + + return did +} + +/** + * Check a base58 encoded string against a regex expression to determine if it is a valid abbreviated verkey + * @param verkey Base58 encoded string representation of an abbreviated verkey + * @returns Boolean indicating if the string is a valid abbreviated verkey + */ +export function isAbbreviatedVerkey(verkey: string): boolean { + return ABBREVIATED_VERKEY_REGEX.test(verkey) +} diff --git a/packages/indy-vdr/src/utils/promises.ts b/packages/indy-vdr/src/utils/promises.ts new file mode 100644 index 0000000000..0e843d73b5 --- /dev/null +++ b/packages/indy-vdr/src/utils/promises.ts @@ -0,0 +1,44 @@ +// This file polyfills the allSettled method introduced in ESNext + +export type AllSettledFulfilled = { + status: 'fulfilled' + value: T +} + +export type AllSettledRejected = { + status: 'rejected' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reason: any +} + +export function allSettled(promises: Promise[]) { + return Promise.all( + promises.map((p) => + p + .then( + (value) => + ({ + status: 'fulfilled', + value, + } as AllSettledFulfilled) + ) + .catch( + (reason) => + ({ + status: 'rejected', + reason, + } as AllSettledRejected) + ) + ) + ) +} + +export function onlyFulfilled(entries: Array | AllSettledRejected>) { + // We filter for only the rejected values, so we can safely cast the type + return entries.filter((e) => e.status === 'fulfilled') as AllSettledFulfilled[] +} + +export function onlyRejected(entries: Array | AllSettledRejected>) { + // We filter for only the rejected values, so we can safely cast the type + return entries.filter((e) => e.status === 'rejected') as AllSettledRejected[] +} From c0f4764119757c8787569564a508212e4688f2df Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 20 Dec 2022 21:55:10 +0800 Subject: [PATCH 08/41] test: add indy-vdr tests Signed-off-by: Timo Glastra --- packages/core/tests/helpers.ts | 3 + packages/indy-vdr/package.json | 7 +- packages/indy-vdr/src/pool/IndyVdrPool.ts | 54 +++++---- .../indy-vdr/src/pool/IndyVdrPoolService.ts | 51 ++++----- .../indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 103 +++++++++++++++++ packages/indy-vdr/tests/setup.ts | 5 + yarn.lock | 106 +++++++++++++++++- 7 files changed, 274 insertions(+), 55 deletions(-) create mode 100644 packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts create mode 100644 packages/indy-vdr/tests/setup.ts diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index a8b89bfaec..8b1a8fa70a 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -19,6 +19,7 @@ import type { AutoAcceptProof } from '../src/modules/proofs/models/ProofAutoAcce import type { CredDef, Schema } from 'indy-sdk' import type { Observable } from 'rxjs' +import { readFileSync } from 'fs' import path from 'path' import { firstValueFrom, ReplaySubject, Subject } from 'rxjs' import { catchError, filter, map, timeout } from 'rxjs/operators' @@ -70,6 +71,8 @@ export const genesisPath = process.env.GENESIS_TXN_PATH ? path.resolve(process.env.GENESIS_TXN_PATH) : path.join(__dirname, '../../../network/genesis/local-genesis.txn') +export const genesisTransactions = readFileSync(genesisPath).toString('utf-8') + export const publicDidSeed = process.env.TEST_AGENT_PUBLIC_DID_SEED ?? '000000000000000000000000Trustee9' export { agentDependencies } diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json index 4e73444465..56d34b01b3 100644 --- a/packages/indy-vdr/package.json +++ b/packages/indy-vdr/package.json @@ -26,14 +26,15 @@ "dependencies": { "class-transformer": "0.5.1", "class-validator": "0.13.1", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "indy-vdr-test-shared": "^0.1.3" }, "peerDependencies": { - "@aries-framework/core": "0.2.5", - "indy-vdr-test-shared": "^0.1.2" + "@aries-framework/core": "0.2.5" }, "devDependencies": { "@aries-framework/node": "0.2.5", + "indy-vdr-test-nodejs": "^0.1.3", "indy-vdr-test-shared": "^0.1.13", "reflect-metadata": "^0.1.13", "rimraf": "~3.0.2", diff --git a/packages/indy-vdr/src/pool/IndyVdrPool.ts b/packages/indy-vdr/src/pool/IndyVdrPool.ts index 445fc30d87..75f1bed05a 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPool.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPool.ts @@ -1,9 +1,15 @@ -import * as IndyVdr from 'indy-vdr-test-shared' -import { GetTransactionAuthorAgreementRequest, GetAcceptanceMechanismsRequest, PoolCreate, IndyVdrRequest, IndyVdrPool as indyVdrPool, indyVdr } from 'indy-vdr-test-shared' -import { Logger, TypedArrayEncoder, AgentContext, AriesFrameworkError, Key } from '@aries-framework/core' +import type { Logger, AgentContext, Key } from '@aries-framework/core' +import type { IndyVdrRequest, IndyVdrPool as indyVdrPool } from 'indy-vdr-test-shared' -import { IndyVdrError, IndyVdrNotFoundError } from '../error' +import { TypedArrayEncoder, AriesFrameworkError } from '@aries-framework/core' +import { + GetTransactionAuthorAgreementRequest, + GetAcceptanceMechanismsRequest, + PoolCreate, + indyVdr, +} from 'indy-vdr-test-shared' +import { IndyVdrError } from '../error' export interface TransactionAuthorAgreement { version?: `${number}.${number}` | `${number}` @@ -19,7 +25,7 @@ export interface AuthorAgreement { } export interface AcceptanceMechanisms { - aml: string[] + aml: Record amlContext: string version: string } @@ -38,7 +44,7 @@ export class IndyVdrPool { private poolConfig: IndyVdrPoolConfig public authorAgreement?: AuthorAgreement | null - constructor(poolConfig: IndyVdrPoolConfig, logger: Logger) { + public constructor(poolConfig: IndyVdrPoolConfig, logger: Logger) { this.logger = logger this.poolConfig = poolConfig } @@ -70,22 +76,31 @@ export class IndyVdrPool { return this._pool } - public async close() { - this.pool?.close() + public close() { + if (!this.pool) { + throw new IndyVdrError("Can't close pool. Pool is not connected") + } + + // FIXME: this method doesn't work?? + // this.pool.close() } - public async submitWriteRequest(agentContext: AgentContext, request: Request, signingKey: Key) { + public async submitWriteRequest( + agentContext: AgentContext, + request: Request, + signingKey: Key + ) { await this.appendTaa(request) - + const signature = await agentContext.wallet.sign({ data: TypedArrayEncoder.fromString(request.signatureInput), - key: signingKey + key: signingKey, }) - + request.setSignature({ - signature + signature, }) - + return await this.pool.submitRequest(request) } @@ -114,7 +129,7 @@ export class IndyVdrPool { // Throw an error if the pool doesn't have the specified version and acceptance mechanism if ( authorAgreement.version !== poolTaa.version || - !authorAgreement.acceptanceMechanisms.aml.includes(poolTaa.acceptanceMechanism) + !authorAgreement.acceptanceMechanisms.aml[poolTaa.acceptanceMechanism] ) { // Throw an error with a helpful message const errMessage = `Unable to satisfy matching TAA with mechanism ${JSON.stringify( @@ -136,20 +151,17 @@ export class IndyVdrPool { request.setTransactionAuthorAgreementAcceptance({ acceptance }) } - private async getTransactionAuthorAgreement( - ): Promise { + private async getTransactionAuthorAgreement(): Promise { // TODO Replace this condition with memoization if (this.authorAgreement !== undefined) { return this.authorAgreement } - const taaRequest = new GetTransactionAuthorAgreementRequest({}) + const taaRequest = new GetTransactionAuthorAgreementRequest({}) const taaResponse = await this.submitReadRequest(taaRequest) const acceptanceMechanismRequest = new GetAcceptanceMechanismsRequest({}) - const acceptanceMechanismResponse = await this.submitReadRequest( - acceptanceMechanismRequest - ) + const acceptanceMechanismResponse = await this.submitReadRequest(acceptanceMechanismRequest) const taaData = taaResponse.result.data diff --git a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts index d06c196d78..5084c0801e 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts @@ -1,21 +1,17 @@ -import { GetNymRequest, GetNymResponse } from 'indy-vdr-test-shared' - -import { - AgentDependencies, - Logger, - InjectionSymbols, - injectable, - AgentContext, - inject, - LedgerNotConfiguredError, -} from '@aries-framework/core' +import type { IndyVdrPoolConfig } from './IndyVdrPool' +import type { AgentContext } from '@aries-framework/core' +import type { GetNymResponse } from 'indy-vdr-test-shared' + +import { Logger, InjectionSymbols, injectable, inject, LedgerNotConfiguredError } from '@aries-framework/core' +import { GetNymRequest } from 'indy-vdr-test-shared' -import { DID_INDY_REGEX } from './DidIdentifier' import { CacheRepository, PersistedLruCache } from '../../../core/src/cache' +import { IndyVdrError, IndyVdrNotFoundError } from '../error' import { isSelfCertifiedDid } from '../utils/did' import { allSettled, onlyFulfilled, onlyRejected } from '../utils/promises' + +import { DID_INDY_REGEX } from './DidIdentifier' import { IndyVdrPool } from './IndyVdrPool' -import { IndyVdrError, IndyVdrNotFoundError } from '../error' export const INDY_VDR_LEGACY_DID_POOL_CACHE_ID = 'INDY_VDR_LEGACY_DID_POOL_CACHE' export const DID_POOL_CACHE_LIMIT = 500 @@ -30,20 +26,18 @@ export interface CachedDidResponse { export class IndyVdrPoolService { public pools: IndyVdrPool[] = [] private logger: Logger - private agentDependencies: AgentDependencies private didCache: PersistedLruCache - public constructor( - cacheRepository: CacheRepository, - @inject(InjectionSymbols.AgentDependencies) agentDependencies: AgentDependencies, - @inject(InjectionSymbols.Logger) logger: Logger - ) { + public constructor(cacheRepository: CacheRepository, @inject(InjectionSymbols.Logger) logger: Logger) { this.logger = logger - this.agentDependencies = agentDependencies this.didCache = new PersistedLruCache(INDY_VDR_LEGACY_DID_POOL_CACHE_ID, DID_POOL_CACHE_LIMIT, cacheRepository) } + public setPools(poolConfigs: IndyVdrPoolConfig[]) { + this.pools = poolConfigs.map((poolConfig) => new IndyVdrPool(poolConfig, this.logger)) + } + /** * Create connections to all ledger pools */ @@ -60,12 +54,12 @@ export class IndyVdrPoolService { } /** - * Get the most appropriate pool for the given did. - * If the did is a qualified indy did, the pool will be determined based on the namespace. + * Get the most appropriate pool for the given did. + * If the did is a qualified indy did, the pool will be determined based on the namespace. * If it is a legacy unqualified indy did, the pool will be determined based on the algorithm as described in this document: * https://docs.google.com/document/d/109C_eMsuZnTnYe2OAd02jAts1vC4axwEKIq7_4dnNVA/edit */ - public async getPoolForDid(agentContext: AgentContext, did: string): Promise< IndyVdrPool > { + public async getPoolForDid(agentContext: AgentContext, did: string): Promise { // Check if the did starts with did:indy const match = did.match(DID_INDY_REGEX) @@ -80,7 +74,7 @@ export class IndyVdrPoolService { const pool = this.pools.find((pool) => pool.indyNamespace === nameSpace) - if (pool) return pool + if (pool) return pool throw new IndyVdrNotFoundError('Pool not found') } else { @@ -88,10 +82,7 @@ export class IndyVdrPoolService { } } - private async getPoolForLegacyDid( - agentContext: AgentContext, - did: string - ): Promise { + private async getPoolForLegacyDid(agentContext: AgentContext, did: string): Promise { const pools = this.pools if (pools.length === 0) { @@ -106,7 +97,7 @@ export class IndyVdrPoolService { // If we have the nym response with associated pool in the cache, we'll use that if (cachedNymResponse && pool) { this.logger.trace(`Found ledger id '${pool.indyNamespace}' for did '${did}' in cache`) - return pool + return pool } const { successful, rejected } = await this.getSettledDidResponsesFromPools(did, pools) @@ -154,7 +145,7 @@ export class IndyVdrPoolService { }, indyNamespace: value.did.indyNamespace, }) - return value.pool + return value.pool } private async getSettledDidResponsesFromPools(did: string, pools: IndyVdrPool[]) { diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts new file mode 100644 index 0000000000..59e6dc89b1 --- /dev/null +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -0,0 +1,103 @@ +import type { CacheRecord } from '../../core/src/cache' + +import { + EventEmitter, + IndyWallet, + Key, + KeyType, + KeyType, + SigningProviderRegistry, + TypedArrayEncoder, +} from '@aries-framework/core' +import { GetNymRequest, NymRequest } from 'indy-vdr-test-shared' +import { Subject } from 'rxjs' + +import { CacheRepository } from '../../core/src/cache' +import { IndyStorageService } from '../../core/src/storage/IndyStorageService' +import { agentDependencies, genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' +import { IndyVdrPool } from '../src/pool' +import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' + +const storageService = new IndyStorageService(agentDependencies) +const eventEmitter = new EventEmitter(agentDependencies, new Subject()) +const cacheRepository = new CacheRepository(storageService, eventEmitter) +const indyVdrPoolService = new IndyVdrPoolService(cacheRepository, testLogger) +const wallet = new IndyWallet(agentDependencies, testLogger, new SigningProviderRegistry([])) +const agentConfig = getAgentConfig('IndyVdrPoolService') +const agentContext = getAgentContext({ wallet, agentConfig }) + +const config = { + isProduction: false, + genesisTransactions, + indyNamespace: `pool:localtest`, + transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, +} as const + +indyVdrPoolService.setPools([config]) + +describe('IndyVdrPoolService', () => { + beforeAll(async () => { + await indyVdrPoolService.connectToPools() + + await wallet.createAndOpen(agentConfig.walletConfig!) + }) + + afterAll(async () => { + for (const pool of indyVdrPoolService.pools) { + pool.close() + } + + await wallet.delete() + }) + + test('can get a pool based on the namespace', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + expect(pool).toBeInstanceOf(IndyVdrPool) + expect(pool.config).toEqual(config) + }) + + test('can resolve a did using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + const request = new GetNymRequest({ + dest: 'TL1EaPFCZ8Si5aUrqScBDt', + }) + + const response = await pool.submitReadRequest(request) + + expect(response).toMatchObject({ + op: 'REPLY', + result: { + dest: 'TL1EaPFCZ8Si5aUrqScBDt', + type: '105', + data: '{"dest":"TL1EaPFCZ8Si5aUrqScBDt","identifier":"V4SGRU86Z58d6TV7PBUe6f","role":"0","seqNo":11,"txnTime":1671530269,"verkey":"~43X4NhAFqREffK7eWdKgFH"}', + identifier: 'LibindyDid111111111111', + reqId: expect.any(Number), + seqNo: expect.any(Number), + txnTime: expect.any(Number), + state_proof: expect.any(Object), + }, + }) + }) + + test('can write a did using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) + + const buffer = TypedArrayEncoder.fromBase58(key.publicKeyBase58) + const did = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + const signerKey = Key.fromPublicKeyBase58('FMGcFuU3QwAQLywxvmEnSorQT3NwU9wgDMMTaDFtvswm', KeyType.Ed25519) + + const request = new NymRequest({ + dest: did, + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + verkey: key.publicKeyBase58, + }) + + const response = await pool.submitWriteRequest(agentContext, request, signerKey) + + console.log(response) + }) +}) diff --git a/packages/indy-vdr/tests/setup.ts b/packages/indy-vdr/tests/setup.ts new file mode 100644 index 0000000000..574e646994 --- /dev/null +++ b/packages/indy-vdr/tests/setup.ts @@ -0,0 +1,5 @@ +import 'reflect-metadata' +// Needed to register indy-vdr node bindings +import '../src/index' + +jest.setTimeout(20000) diff --git a/yarn.lock b/yarn.lock index 030c9671e1..5d951674d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1792,6 +1792,21 @@ semver "^7.3.5" tar "^6.1.11" +"@mapbox/node-pre-gyp@^1.0.10": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c" + integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + "@mattrglobal/bbs-signatures@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@mattrglobal/bbs-signatures/-/bbs-signatures-1.0.0.tgz#8ff272c6d201aadab7e08bd84dbfd6e0d48ba12d" @@ -3047,6 +3062,14 @@ array-includes@^3.1.4: get-intrinsic "^1.1.1" is-string "^1.0.7" +array-index@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-index/-/array-index-1.0.0.tgz#ec56a749ee103e4e08c790b9c353df16055b97f9" + integrity sha512-jesyNbBkLQgGZMSwA1FanaFjalb1mZUGxGeUEkSDidzgrbjBGhvizJkaItdhkt8eIHFOJC7nDsrXk+BaehTdRw== + dependencies: + debug "^2.2.0" + es6-symbol "^3.0.2" + array-map@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" @@ -4272,6 +4295,14 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + dargs@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" @@ -4706,6 +4737,32 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.50: + version "0.10.62" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" + integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.0.2, es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -5057,6 +5114,13 @@ express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +ext@^1.1.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -5968,6 +6032,23 @@ indy-sdk@^1.16.0-dev-1636: nan "^2.11.1" node-gyp "^8.0.0" +indy-vdr-test-nodejs@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/indy-vdr-test-nodejs/-/indy-vdr-test-nodejs-0.1.3.tgz#97eaf38b1035bfabcd772a8399f23d766dfd493e" + integrity sha512-E6r86QGbswa+hBgMJKVWJycqvvmOgepFMDaAvuZQtxQK1Z2gghco6m/9EOAPYaJRs0MMEEhzUGhvtSpCzeZ6sg== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.10" + ffi-napi "^4.0.3" + indy-vdr-test-shared "0.1.3" + ref-array-di "^1.2.2" + ref-napi "^3.0.3" + ref-struct-di "^1.1.1" + +indy-vdr-test-shared@0.1.3, indy-vdr-test-shared@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/indy-vdr-test-shared/-/indy-vdr-test-shared-0.1.3.tgz#3b5ee9492ebc3367a027670aa9686c493de5929c" + integrity sha512-fdgV388zi3dglu49kqrV+i40w+18uJkv96Tk4nziLdP280SLnZKKnIRAiq11Hj8aHpnZmwMloyQCsIyQZDZk2g== + infer-owner@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" @@ -8125,6 +8206,11 @@ neon-cli@0.8.2: validate-npm-package-license "^3.0.4" validate-npm-package-name "^3.0.0" +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -9434,6 +9520,14 @@ reduce-flatten@^2.0.0: resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== +ref-array-di@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ref-array-di/-/ref-array-di-1.2.2.tgz#ceee9d667d9c424b5a91bb813457cc916fb1f64d" + integrity sha512-jhCmhqWa7kvCVrWhR/d7RemkppqPUdxEil1CtTtm7FkZV8LcHHCK3Or9GinUiFP5WY3k0djUkMvhBhx49Jb2iA== + dependencies: + array-index "^1.0.0" + debug "^3.1.0" + "ref-napi@^2.0.1 || ^3.0.2", ref-napi@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/ref-napi/-/ref-napi-3.0.3.tgz#e259bfc2bbafb3e169e8cd9ba49037dd00396b22" @@ -9444,7 +9538,7 @@ reduce-flatten@^2.0.0: node-addon-api "^3.0.0" node-gyp-build "^4.2.1" -ref-struct-di@^1.1.0: +ref-struct-di@^1.1.0, ref-struct-di@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ref-struct-di/-/ref-struct-di-1.1.1.tgz#5827b1d3b32372058f177547093db1fe1602dc10" integrity sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g== @@ -10771,6 +10865,16 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" From d11da2aa3a10e24838363e9f115bb972e8270e3f Mon Sep 17 00:00:00 2001 From: vickysomtee Date: Tue, 20 Dec 2022 16:08:34 +0100 Subject: [PATCH 09/41] refactor: IndyVdr package Work Funded by the Government of Ontario Signed-off-by: vickysomtee --- packages/core/src/index.ts | 2 ++ packages/indy-vdr/package.json | 7 ++++--- packages/indy-vdr/src/pool/IndyVdrPoolService.ts | 15 +++++---------- yarn.lock | 5 +++++ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 355383f062..0a847c5f8b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,6 +10,7 @@ export { EventEmitter } from './agent/EventEmitter' export { FeatureRegistry } from './agent/FeatureRegistry' export { Handler, HandlerInboundMessage } from './agent/Handler' export * from './agent/models' +export * from './modules/ledger/error' export * from './agent/helpers' export { AgentConfig } from './agent/AgentConfig' export { AgentMessage } from './agent/AgentMessage' @@ -25,6 +26,7 @@ export type { JsonObject, JsonValue, } from './types' +export * from './cache' export { DidCommMimeType, KeyDerivationMethod } from './types' export type { FileSystem } from './storage/FileSystem' export * from './storage/BaseRecord' diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json index 4e73444465..8af4a8b651 100644 --- a/packages/indy-vdr/package.json +++ b/packages/indy-vdr/package.json @@ -25,22 +25,23 @@ }, "dependencies": { "class-transformer": "0.5.1", + "indy-vdr-test-shared": "^0.1.3", "class-validator": "0.13.1", "rxjs": "^7.2.0" }, "peerDependencies": { "@aries-framework/core": "0.2.5", - "indy-vdr-test-shared": "^0.1.2" + "indy-vdr-test-nodejs": "^0.1.3" }, "devDependencies": { "@aries-framework/node": "0.2.5", - "indy-vdr-test-shared": "^0.1.13", + "indy-vdr-test-nodejs": "^0.1.3", "reflect-metadata": "^0.1.13", "rimraf": "~3.0.2", "typescript": "~4.3.0" }, "peerDependenciesMeta": { - "indy-vdr-test-shared": { + "indy-vdr-test-nodejs": { "optional": true } } diff --git a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts index d06c196d78..134ec2716e 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts @@ -8,10 +8,11 @@ import { AgentContext, inject, LedgerNotConfiguredError, + PersistedLruCache, + CacheRepository } from '@aries-framework/core' import { DID_INDY_REGEX } from './DidIdentifier' -import { CacheRepository, PersistedLruCache } from '../../../core/src/cache' import { isSelfCertifiedDid } from '../utils/did' import { allSettled, onlyFulfilled, onlyRejected } from '../utils/promises' import { IndyVdrPool } from './IndyVdrPool' @@ -69,16 +70,10 @@ export class IndyVdrPoolService { // Check if the did starts with did:indy const match = did.match(DID_INDY_REGEX) - // if (match) { - // const [, namespace] = match - // } else { - // // legacy way - // } + if (match) { + const [, namespace] = match - if (did.startsWith('did:indy')) { - const nameSpace = did.split(':')[2] - - const pool = this.pools.find((pool) => pool.indyNamespace === nameSpace) + const pool = this.getPoolForNamespace(namespace); if (pool) return pool diff --git a/yarn.lock b/yarn.lock index 030c9671e1..23979b09ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5968,6 +5968,11 @@ indy-sdk@^1.16.0-dev-1636: nan "^2.11.1" node-gyp "^8.0.0" +indy-vdr-test-shared@^0.1.13: + version "0.1.3" + resolved "https://registry.yarnpkg.com/indy-vdr-test-shared/-/indy-vdr-test-shared-0.1.3.tgz#3b5ee9492ebc3367a027670aa9686c493de5929c" + integrity sha512-fdgV388zi3dglu49kqrV+i40w+18uJkv96Tk4nziLdP280SLnZKKnIRAiq11Hj8aHpnZmwMloyQCsIyQZDZk2g== + infer-owner@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" From 6ec514ee65c4103219cb0e8f6521190d31eb71ce Mon Sep 17 00:00:00 2001 From: Karim Stekelenburg Date: Tue, 17 Jan 2023 16:52:37 +0100 Subject: [PATCH 10/41] test(indy-vdr): add basic tests Signed-off-by: Karim Stekelenburg --- .../indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 205 +++++++++++++++--- 1 file changed, 169 insertions(+), 36 deletions(-) diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts index 59e6dc89b1..12cf952814 100644 --- a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -5,11 +5,10 @@ import { IndyWallet, Key, KeyType, - KeyType, SigningProviderRegistry, TypedArrayEncoder, } from '@aries-framework/core' -import { GetNymRequest, NymRequest } from 'indy-vdr-test-shared' +import { GetNymRequest, NymRequest, SchemaRequest } from 'indy-vdr-test-shared' import { Subject } from 'rxjs' import { CacheRepository } from '../../core/src/cache' @@ -34,6 +33,8 @@ const config = { transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, } as const +let signerKey: Key + indyVdrPoolService.setPools([config]) describe('IndyVdrPoolService', () => { @@ -41,6 +42,8 @@ describe('IndyVdrPoolService', () => { await indyVdrPoolService.connectToPools() await wallet.createAndOpen(agentConfig.walletConfig!) + signerKey = await wallet.createKey({ seed: '000000000000000000000000Trustee9', keyType: KeyType.Ed25519 }) + }) afterAll(async () => { @@ -51,53 +54,183 @@ describe('IndyVdrPoolService', () => { await wallet.delete() }) - test('can get a pool based on the namespace', async () => { - const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') - expect(pool).toBeInstanceOf(IndyVdrPool) - expect(pool.config).toEqual(config) - }) - - test('can resolve a did using the pool', async () => { - const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + describe('DIDs', () => { - const request = new GetNymRequest({ - dest: 'TL1EaPFCZ8Si5aUrqScBDt', + test('can get a pool based on the namespace', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + expect(pool).toBeInstanceOf(IndyVdrPool) + expect(pool.config).toEqual(config) }) - const response = await pool.submitReadRequest(request) + test('can resolve a did using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') - expect(response).toMatchObject({ - op: 'REPLY', - result: { + const request = new GetNymRequest({ dest: 'TL1EaPFCZ8Si5aUrqScBDt', - type: '105', - data: '{"dest":"TL1EaPFCZ8Si5aUrqScBDt","identifier":"V4SGRU86Z58d6TV7PBUe6f","role":"0","seqNo":11,"txnTime":1671530269,"verkey":"~43X4NhAFqREffK7eWdKgFH"}', - identifier: 'LibindyDid111111111111', - reqId: expect.any(Number), - seqNo: expect.any(Number), - txnTime: expect.any(Number), - state_proof: expect.any(Object), - }, + }) + + const response = await pool.submitReadRequest(request) + + expect(response).toMatchObject({ + op: 'REPLY', + result: { + dest: 'TL1EaPFCZ8Si5aUrqScBDt', + type: '105', + data: '{"dest":"TL1EaPFCZ8Si5aUrqScBDt","identifier":"V4SGRU86Z58d6TV7PBUe6f","role":"0","seqNo":11,"txnTime":1671530269,"verkey":"~43X4NhAFqREffK7eWdKgFH"}', + identifier: 'LibindyDid111111111111', + reqId: expect.any(Number), + seqNo: expect.any(Number), + txnTime: expect.any(Number), + state_proof: expect.any(Object), + }, + }) }) + + test('can write a did using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + // prepare the DID we are going to write to the ledger + const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const buffer = TypedArrayEncoder.fromBase58(key.publicKeyBase58) + const did = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + + + const request = new NymRequest({ + dest: did, + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + verkey: key.publicKeyBase58, + }) + + const response = await pool.submitWriteRequest(agentContext, request, signerKey) + + console.log(response) + }) + }) - test('can write a did using the pool', async () => { - const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + describe('Schemas', () => { + + test('can write a schema using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + const dynamicVersion = `1.${Math.random() * 100}` // TODO Remove this before pushing + + const schemaRequest = new SchemaRequest({ + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + schema: { + id: 'test-schema-id', + name: 'test-schema', + ver: '1.0', + version: dynamicVersion, // TODO remove this before pushing + attrNames: [ + 'first_name', + 'last_name', + 'age' + ] + } + }) + - const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const response = await pool.submitWriteRequest(agentContext, schemaRequest, signerKey) + expect(response.op).toEqual("REPLY") - const buffer = TypedArrayEncoder.fromBase58(key.publicKeyBase58) - const did = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) - const signerKey = Key.fromPublicKeyBase58('FMGcFuU3QwAQLywxvmEnSorQT3NwU9wgDMMTaDFtvswm', KeyType.Ed25519) + // FIXME ts-ignore is required. Check that the response type is typed correctly. - const request = new NymRequest({ - dest: did, - submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', - verkey: key.publicKeyBase58, + // @ts-ignore + expect(response.result.txn.data.data.name).toEqual('test-schema') + + // @ts-ignore + expect(response.result.txn.data.data.version).toEqual(dynamicVersion) // TODO remove this before pushing + + // @ts-ignore + expect(response.result.txn.data.data.attr_names.sort()).toEqual(expect.arrayContaining( + [ + 'first_name', + 'last_name', + 'age' + ].sort() + )) + + expect(response.result.txn.protocolVersion).toEqual(2) + + expect(response.result.txn.metadata.from).toEqual('TL1EaPFCZ8Si5aUrqScBDt') + + expect(response.result.txn.metadata.taaAcceptance).toBeDefined() + + expect(response.result.txn.metadata.taaAcceptance!.mechanism).toEqual('accept') + + // testing response.result.tnxMetadata.txnId + const txnIdArray = response.result.txnMetadata.txnId.split(':') + + const receivedSubmitterDid = txnIdArray[0] + const receivedProtocolVersion = txnIdArray[1] + const receivedSchemaName = txnIdArray[2] + const receivedSchemaVersionVersion = txnIdArray[3] + + expect(receivedSubmitterDid).toEqual('TL1EaPFCZ8Si5aUrqScBDt') + + expect(receivedProtocolVersion).toEqual("2") + + expect(receivedSchemaName).toEqual('test-schema') + + expect(receivedSchemaVersionVersion).toEqual(dynamicVersion) // TODO change this before pushing + + // testing reqSignature + expect(response.result.reqSignature.type).toEqual('ED25519') + + expect(response.result.reqSignature.values[0].from).toEqual('TL1EaPFCZ8Si5aUrqScBDt') + + // testing ver + expect(response.result.ver).toEqual("1") }) - const response = await pool.submitWriteRequest(agentContext, request, signerKey) + test('fails writing a schema with existing verson number using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + const dynamicVersion = `1.${Math.random() * 100}` // TODO Remove this before pushing + + const schemaRequest = new SchemaRequest({ + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + schema: { + id: 'test-schema-id', + name: 'test-schema', + ver: '1.0', + version: dynamicVersion, + attrNames: [ + 'first_name', + 'last_name', + 'age' + ] + } + }) + + const schemaRequest2 = new SchemaRequest({ + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + schema: { + id: 'test-schema-id', + name: 'test-schema', + ver: '1.0', + version: dynamicVersion, + attrNames: [ + 'first_name', + 'last_name', + 'age' + ] + } + }) + + + const response = await pool.submitWriteRequest(agentContext, schemaRequest, signerKey) + expect(response).toBeDefined() + + const response2 = await pool.submitWriteRequest(agentContext, schemaRequest2, signerKey) + + expect(response2.op).toEqual('REJECT') + + // @ts-ignore + expect(response2.identifier).toEqual('TL1EaPFCZ8Si5aUrqScBDt') + + }) - console.log(response) }) }) From 8ff1dc9dafc69fd5f4e907107c0221f36fafaa3b Mon Sep 17 00:00:00 2001 From: Karim Stekelenburg Date: Tue, 17 Jan 2023 17:43:42 +0100 Subject: [PATCH 11/41] fix(indy-vdr): fix import Signed-off-by: Karim Stekelenburg --- packages/core/src/index.ts | 1 - packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0a847c5f8b..1ca60ad263 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,7 +10,6 @@ export { EventEmitter } from './agent/EventEmitter' export { FeatureRegistry } from './agent/FeatureRegistry' export { Handler, HandlerInboundMessage } from './agent/Handler' export * from './agent/models' -export * from './modules/ledger/error' export * from './agent/helpers' export { AgentConfig } from './agent/AgentConfig' export { AgentMessage } from './agent/AgentMessage' diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts index 12cf952814..f75b73c97a 100644 --- a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -132,6 +132,9 @@ describe('IndyVdrPoolService', () => { const response = await pool.submitWriteRequest(agentContext, schemaRequest, signerKey) + + console.log(JSON.stringify(response, null, 2)) + expect(response.op).toEqual("REPLY") // FIXME ts-ignore is required. Check that the response type is typed correctly. @@ -223,12 +226,12 @@ describe('IndyVdrPoolService', () => { const response = await pool.submitWriteRequest(agentContext, schemaRequest, signerKey) expect(response).toBeDefined() - const response2 = await pool.submitWriteRequest(agentContext, schemaRequest2, signerKey) + // const response2 = await pool.submitWriteRequest(agentContext, schemaRequest2, signerKey) - expect(response2.op).toEqual('REJECT') + // expect(response2.op).toEqual('REJECT') // @ts-ignore - expect(response2.identifier).toEqual('TL1EaPFCZ8Si5aUrqScBDt') + // expect(response2.identifier).toEqual('TL1EaPFCZ8Si5aUrqScBDt') }) From 798406c1aba69babfb305ab8c68089c7584c811c Mon Sep 17 00:00:00 2001 From: vickysomtee Date: Fri, 20 Jan 2023 16:28:12 +0100 Subject: [PATCH 12/41] test(indy-vdr): start credential definition Signed-off-by: vickysomtee --- .../indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts index f75b73c97a..6c80d3ee55 100644 --- a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -8,7 +8,7 @@ import { SigningProviderRegistry, TypedArrayEncoder, } from '@aries-framework/core' -import { GetNymRequest, NymRequest, SchemaRequest } from 'indy-vdr-test-shared' +import { GetNymRequest, NymRequest, SchemaRequest, CredentialDefinitionRequest } from 'indy-vdr-test-shared' import { Subject } from 'rxjs' import { CacheRepository } from '../../core/src/cache' @@ -43,7 +43,6 @@ describe('IndyVdrPoolService', () => { await wallet.createAndOpen(agentConfig.walletConfig!) signerKey = await wallet.createKey({ seed: '000000000000000000000000Trustee9', keyType: KeyType.Ed25519 }) - }) afterAll(async () => { @@ -55,7 +54,6 @@ describe('IndyVdrPoolService', () => { }) describe('DIDs', () => { - test('can get a pool based on the namespace', async () => { const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') expect(pool).toBeInstanceOf(IndyVdrPool) @@ -94,7 +92,6 @@ describe('IndyVdrPoolService', () => { const buffer = TypedArrayEncoder.fromBase58(key.publicKeyBase58) const did = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) - const request = new NymRequest({ dest: did, submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', @@ -105,11 +102,33 @@ describe('IndyVdrPoolService', () => { console.log(response) }) + }) + + describe('CredentialDefinition', () => { + test('can write a credential definition using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + const credentialDefinitionRequest = new CredentialDefinitionRequest({ + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + credentialDefinition: { + ver: '1.0', + id: 'test-credential-id', + schemaId: 'test-schema-id', + type: 'CL', + tag: 'TAG', + value: { + primary: {}, + }, + }, + }) + const response = await pool.submitWriteRequest(agentContext, credentialDefinitionRequest, signerKey) + + console.log(response) + }) }) describe('Schemas', () => { - test('can write a schema using the pool', async () => { const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') @@ -122,20 +141,15 @@ describe('IndyVdrPoolService', () => { name: 'test-schema', ver: '1.0', version: dynamicVersion, // TODO remove this before pushing - attrNames: [ - 'first_name', - 'last_name', - 'age' - ] - } + attrNames: ['first_name', 'last_name', 'age'], + }, }) - const response = await pool.submitWriteRequest(agentContext, schemaRequest, signerKey) console.log(JSON.stringify(response, null, 2)) - expect(response.op).toEqual("REPLY") + expect(response.op).toEqual('REPLY') // FIXME ts-ignore is required. Check that the response type is typed correctly. @@ -146,13 +160,9 @@ describe('IndyVdrPoolService', () => { expect(response.result.txn.data.data.version).toEqual(dynamicVersion) // TODO remove this before pushing // @ts-ignore - expect(response.result.txn.data.data.attr_names.sort()).toEqual(expect.arrayContaining( - [ - 'first_name', - 'last_name', - 'age' - ].sort() - )) + expect(response.result.txn.data.data.attr_names.sort()).toEqual( + expect.arrayContaining(['first_name', 'last_name', 'age'].sort()) + ) expect(response.result.txn.protocolVersion).toEqual(2) @@ -172,7 +182,7 @@ describe('IndyVdrPoolService', () => { expect(receivedSubmitterDid).toEqual('TL1EaPFCZ8Si5aUrqScBDt') - expect(receivedProtocolVersion).toEqual("2") + expect(receivedProtocolVersion).toEqual('2') expect(receivedSchemaName).toEqual('test-schema') @@ -184,7 +194,7 @@ describe('IndyVdrPoolService', () => { expect(response.result.reqSignature.values[0].from).toEqual('TL1EaPFCZ8Si5aUrqScBDt') // testing ver - expect(response.result.ver).toEqual("1") + expect(response.result.ver).toEqual('1') }) test('fails writing a schema with existing verson number using the pool', async () => { @@ -199,12 +209,8 @@ describe('IndyVdrPoolService', () => { name: 'test-schema', ver: '1.0', version: dynamicVersion, - attrNames: [ - 'first_name', - 'last_name', - 'age' - ] - } + attrNames: ['first_name', 'last_name', 'age'], + }, }) const schemaRequest2 = new SchemaRequest({ @@ -214,15 +220,10 @@ describe('IndyVdrPoolService', () => { name: 'test-schema', ver: '1.0', version: dynamicVersion, - attrNames: [ - 'first_name', - 'last_name', - 'age' - ] - } + attrNames: ['first_name', 'last_name', 'age'], + }, }) - const response = await pool.submitWriteRequest(agentContext, schemaRequest, signerKey) expect(response).toBeDefined() @@ -232,8 +233,6 @@ describe('IndyVdrPoolService', () => { // @ts-ignore // expect(response2.identifier).toEqual('TL1EaPFCZ8Si5aUrqScBDt') - }) - }) }) From b51d60a0abb49a8b53cb199b4e9c5508943f49ba Mon Sep 17 00:00:00 2001 From: vickysomtee Date: Tue, 24 Jan 2023 07:40:06 +0100 Subject: [PATCH 13/41] feat: created IndyNotCOnfiguredError Work funded by the Ontario Government Signed-off-by: vickysomtee --- packages/indy-vdr/src/error/IndyVdrNotConfiguredError.ts | 7 +++++++ packages/indy-vdr/src/error/index.ts | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 packages/indy-vdr/src/error/IndyVdrNotConfiguredError.ts diff --git a/packages/indy-vdr/src/error/IndyVdrNotConfiguredError.ts b/packages/indy-vdr/src/error/IndyVdrNotConfiguredError.ts new file mode 100644 index 0000000000..a93198e7f5 --- /dev/null +++ b/packages/indy-vdr/src/error/IndyVdrNotConfiguredError.ts @@ -0,0 +1,7 @@ +import {IndyVdrError} from './IndyVdrError' + +export class IndyVdrConfiguredError extends IndyVdrError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } + } \ No newline at end of file diff --git a/packages/indy-vdr/src/error/index.ts b/packages/indy-vdr/src/error/index.ts index 627a3517b1..d6d15db9bb 100644 --- a/packages/indy-vdr/src/error/index.ts +++ b/packages/indy-vdr/src/error/index.ts @@ -1,2 +1,3 @@ export * from './IndyVdrError' -export * from './IndyVdrNotFound' \ No newline at end of file +export * from './IndyVdrNotFound' +export * from './IndyVdrNotConfiguredError' \ No newline at end of file From 139eacfd2c9d9ac072fad2fad386bbaa5641d6c6 Mon Sep 17 00:00:00 2001 From: vickysomtee Date: Tue, 24 Jan 2023 07:42:18 +0100 Subject: [PATCH 14/41] test(IndyVdr): schema && credential definition Work funded by the Ontario Government Signed-off-by: vickysomtee --- .../indy-vdr/src/pool/IndyVdrPoolService.ts | 8 +- .../indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 173 ++++++++++-------- 2 files changed, 97 insertions(+), 84 deletions(-) diff --git a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts index b03de790fe..05e6e57f4a 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts @@ -2,10 +2,10 @@ import type { IndyVdrPoolConfig } from './IndyVdrPool' import type { AgentContext } from '@aries-framework/core' import type { GetNymResponse } from 'indy-vdr-test-shared' -import { Logger, InjectionSymbols, injectable, inject, LedgerNotConfiguredError, PersistedLruCache, CacheRepository } from '@aries-framework/core' +import { Logger, InjectionSymbols, injectable, inject, PersistedLruCache, CacheRepository } from '@aries-framework/core' import { GetNymRequest } from 'indy-vdr-test-shared' -import { IndyVdrError, IndyVdrNotFoundError } from '../error' +import { IndyVdrError, IndyVdrNotFoundError, IndyVdrConfiguredError } from '../error' import { isSelfCertifiedDid } from '../utils/did' import { allSettled, onlyFulfilled, onlyRejected } from '../utils/promises' @@ -84,7 +84,7 @@ export class IndyVdrPoolService { const pools = this.pools if (pools.length === 0) { - throw new LedgerNotConfiguredError( + throw new IndyVdrConfiguredError( "No indy ledgers configured. Provide at least one pool configuration in the 'indyLedgers' agent configuration" ) } @@ -166,7 +166,7 @@ export class IndyVdrPoolService { */ public getPoolForNamespace(indyNamespace: string) { if (this.pools.length === 0) { - throw new LedgerNotConfiguredError( + throw new IndyVdrConfiguredError( "No indy ledgers configured. Provide at least one pool configuration in the 'indyLedgers' agent configuration" ) } diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts index 6c80d3ee55..6e9e9b1f44 100644 --- a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -74,7 +74,7 @@ describe('IndyVdrPoolService', () => { result: { dest: 'TL1EaPFCZ8Si5aUrqScBDt', type: '105', - data: '{"dest":"TL1EaPFCZ8Si5aUrqScBDt","identifier":"V4SGRU86Z58d6TV7PBUe6f","role":"0","seqNo":11,"txnTime":1671530269,"verkey":"~43X4NhAFqREffK7eWdKgFH"}', + data: expect.any(String), identifier: 'LibindyDid111111111111', reqId: expect.any(Number), seqNo: expect.any(Number), @@ -82,6 +82,15 @@ describe('IndyVdrPoolService', () => { state_proof: expect.any(Object), }, }) + + expect(JSON.parse(response.result.data as string)).toMatchObject({ + dest: 'TL1EaPFCZ8Si5aUrqScBDt', + identifier: 'V4SGRU86Z58d6TV7PBUe6f', + role: '0', + seqNo: expect.any(Number), + txnTime: expect.any(Number), + verkey: '~43X4NhAFqREffK7eWdKgFH', + }) }) test('can write a did using the pool', async () => { @@ -99,36 +108,12 @@ describe('IndyVdrPoolService', () => { }) const response = await pool.submitWriteRequest(agentContext, request, signerKey) - - console.log(response) }) }) - describe('CredentialDefinition', () => { - test('can write a credential definition using the pool', async () => { - const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') - - const credentialDefinitionRequest = new CredentialDefinitionRequest({ - submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', - credentialDefinition: { - ver: '1.0', - id: 'test-credential-id', - schemaId: 'test-schema-id', - type: 'CL', - tag: 'TAG', - value: { - primary: {}, - }, - }, - }) + xdescribe('CredentialDefinition', () => {}) - const response = await pool.submitWriteRequest(agentContext, credentialDefinitionRequest, signerKey) - - console.log(response) - }) - }) - - describe('Schemas', () => { + describe('Schemas & credential Definition', () => { test('can write a schema using the pool', async () => { const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') @@ -145,56 +130,91 @@ describe('IndyVdrPoolService', () => { }, }) - const response = await pool.submitWriteRequest(agentContext, schemaRequest, signerKey) - - console.log(JSON.stringify(response, null, 2)) - - expect(response.op).toEqual('REPLY') - - // FIXME ts-ignore is required. Check that the response type is typed correctly. - - // @ts-ignore - expect(response.result.txn.data.data.name).toEqual('test-schema') - - // @ts-ignore - expect(response.result.txn.data.data.version).toEqual(dynamicVersion) // TODO remove this before pushing - - // @ts-ignore - expect(response.result.txn.data.data.attr_names.sort()).toEqual( - expect.arrayContaining(['first_name', 'last_name', 'age'].sort()) - ) - - expect(response.result.txn.protocolVersion).toEqual(2) - - expect(response.result.txn.metadata.from).toEqual('TL1EaPFCZ8Si5aUrqScBDt') - - expect(response.result.txn.metadata.taaAcceptance).toBeDefined() + const schemaResponse = await pool.submitWriteRequest(agentContext, schemaRequest, signerKey) - expect(response.result.txn.metadata.taaAcceptance!.mechanism).toEqual('accept') - - // testing response.result.tnxMetadata.txnId - const txnIdArray = response.result.txnMetadata.txnId.split(':') - - const receivedSubmitterDid = txnIdArray[0] - const receivedProtocolVersion = txnIdArray[1] - const receivedSchemaName = txnIdArray[2] - const receivedSchemaVersionVersion = txnIdArray[3] - - expect(receivedSubmitterDid).toEqual('TL1EaPFCZ8Si5aUrqScBDt') - - expect(receivedProtocolVersion).toEqual('2') - - expect(receivedSchemaName).toEqual('test-schema') - - expect(receivedSchemaVersionVersion).toEqual(dynamicVersion) // TODO change this before pushing + expect(schemaResponse).toMatchObject({ + op: 'REPLY', + result: { + ver: '1', + txn: { + metadata: expect.any(Object), + type: '101', + data: { + data: { + attr_names: expect.arrayContaining(['age', 'last_name', 'first_name']), + name: 'test-schema', + version: dynamicVersion, + }, + }, + }, + }, + }) - // testing reqSignature - expect(response.result.reqSignature.type).toEqual('ED25519') + const credentialDefinitionRequest = new CredentialDefinitionRequest({ + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + credentialDefinition: { + ver: '1.0', + id: `TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResponse.result.txnMetadata.seqNo}:TAG`, + // must be string version of the schema seqNo + schemaId: `${schemaResponse.result.txnMetadata.seqNo}`, + type: 'CL', + tag: 'TAG', + value: { + primary: { + n: '95671911213029889766246243339609567053285242961853979532076192834533577534909796042025401129640348836502648821408485216223269830089771714177855160978214805993386076928594836829216646288195127289421136294309746871614765411402917891972999085287429566166932354413679994469616357622976775651506242447852304853465380257226445481515631782793575184420720296120464167257703633829902427169144462981949944348928086406211627174233811365419264314148304536534528344413738913277713548403058098093453580992173145127632199215550027527631259565822872315784889212327945030315062879193999012349220118290071491899498795367403447663354833', + s: '1573939820553851804028472930351082111827449763317396231059458630252708273163050576299697385049087601314071156646675105028237105229428440185022593174121924731226634356276616495327358864865629675802738680754755949997611920669823449540027707876555408118172529688443208301403297680159171306000341239398135896274940688268460793682007115152428685521865921925309154307574955324973580144009271977076586453011938089159885164705002797196738438392179082905738155386545935208094240038135576042886730802817809757582039362798495805441520744154270346780731494125065136433163757697326955962282840631850597919384092584727207908978907', + r: { + master_secret: + '51468326064458249697956272807708948542001661888325200180968238787091473418947480867518174106588127385097619219536294589148765074804124925845579871788369264160902401097166484002617399484700234182426993061977152961670486891123188739266793651668791365808983166555735631354925174224786218771453042042304773095663181121735652667614424198057134974727791329623974680096491276337756445057223988781749506082654194307092164895251308088903000573135447235553684949564809677864522417041639512933806794232354223826262154508950271949764583849083972967642587778197779127063591201123312548182885603427440981731822883101260509710567731', + last_name: + '35864556460959997092903171610228165251001245539613587319116151716453114432309327039517115215674024166920383445379522674504803469517283236033110568676156285676664363558333716898161685255450536856645604857714925836474250821415182026707218622134953915013803750771185050002646661004119778318524426368842019753903741998256374803456282688037624993010626333853831264356355867746685055670790915539230702546586615988121383960277550317876816983602795121749533628953449405383896799464872758725899520173321672584180060465965090049734285011738428381648013150818429882628144544132356242262467090140003979917439514443707537952643217', + first_name: + '26405366527417391838431479783966663952336302347775179063968690502492620867161212873635806190080000833725932174641667734138216137047349915190546601368424742647800764149890590518336588437317392528514313749533980651547425554257026971104775208127915118918084350210726664749850578299247705298976657301433446491575776774836993110356033664644761593799921221474617858131678955318702706530853801195330271860527250931569815553226145458665481867408279941785848264018364216087471931232367137301987457054918438087686484522112532447779498424748261678616461026788516567300969886029412198319909977473167405879110243445062391837349387', + age: '19865805272519696320755573045337531955436490760876870776207490804137339344112305203631892390827288264857621916650098902064979838987400911652887344763586495880167030031364467726355103327059673023946234460960685398768709062405377107912774045508870580108596597470880834205563197111550140867466625683117333370595295321833757429488192170551320637065066368716366317421169802474954914904380304190861641082310805418122837214965865969459724848071006870574514215255412289237027267424055400593307112849859757094597401668252862525566316402695830217450073667487951799749275437192883439584518905943435472478496028380016245355151988', + }, + rctxt: + '17146114573198643698878017247599007910707723139165264508694101989891626297408755744139587708989465136799243292477223763665064840330721616213638280284119891715514951989022398510785960099708705561761504012512387129498731093386014964896897751536856287377064154297370092339714578039195258061017640952790913108285519632654466006255438773382930416822756630391947263044087385305540191237328903426888518439803354213792647775798033294505898635058814132665832000734168261793545453678083703704122695006541391598116359796491845268631009298069826949515604008666680160398698425061157356267086946953480945396595351944425658076127674', + z: '57056568014385132434061065334124327103768023932445648883765905576432733866307137325457775876741578717650388638737098805750938053855430851133826479968450532729423746605371536096355616166421996729493639634413002114547787617999178137950004782677177313856876420539744625174205603354705595789330008560775613287118432593300023801651460885523314713996258581986238928077688246511704050386525431448517516821261983193275502089060128363906909778842476516981025598807378338053788433033754999771876361716562378445777250912525673660842724168260417083076824975992327559199634032439358787956784395443246565622469187082767614421691234', + }, + }, + }, + }) - expect(response.result.reqSignature.values[0].from).toEqual('TL1EaPFCZ8Si5aUrqScBDt') + const response = await pool.submitWriteRequest(agentContext, credentialDefinitionRequest, signerKey) - // testing ver - expect(response.result.ver).toEqual('1') + expect(response).toMatchObject({ + op: 'REPLY', + result: { + ver: '1', + txn: { + metadata: expect.any(Object), + type: '102', + data: { + data: { + primary: { + r: { + last_name: + '35864556460959997092903171610228165251001245539613587319116151716453114432309327039517115215674024166920383445379522674504803469517283236033110568676156285676664363558333716898161685255450536856645604857714925836474250821415182026707218622134953915013803750771185050002646661004119778318524426368842019753903741998256374803456282688037624993010626333853831264356355867746685055670790915539230702546586615988121383960277550317876816983602795121749533628953449405383896799464872758725899520173321672584180060465965090049734285011738428381648013150818429882628144544132356242262467090140003979917439514443707537952643217', + first_name: + '26405366527417391838431479783966663952336302347775179063968690502492620867161212873635806190080000833725932174641667734138216137047349915190546601368424742647800764149890590518336588437317392528514313749533980651547425554257026971104775208127915118918084350210726664749850578299247705298976657301433446491575776774836993110356033664644761593799921221474617858131678955318702706530853801195330271860527250931569815553226145458665481867408279941785848264018364216087471931232367137301987457054918438087686484522112532447779498424748261678616461026788516567300969886029412198319909977473167405879110243445062391837349387', + age: '19865805272519696320755573045337531955436490760876870776207490804137339344112305203631892390827288264857621916650098902064979838987400911652887344763586495880167030031364467726355103327059673023946234460960685398768709062405377107912774045508870580108596597470880834205563197111550140867466625683117333370595295321833757429488192170551320637065066368716366317421169802474954914904380304190861641082310805418122837214965865969459724848071006870574514215255412289237027267424055400593307112849859757094597401668252862525566316402695830217450073667487951799749275437192883439584518905943435472478496028380016245355151988', + master_secret: + '51468326064458249697956272807708948542001661888325200180968238787091473418947480867518174106588127385097619219536294589148765074804124925845579871788369264160902401097166484002617399484700234182426993061977152961670486891123188739266793651668791365808983166555735631354925174224786218771453042042304773095663181121735652667614424198057134974727791329623974680096491276337756445057223988781749506082654194307092164895251308088903000573135447235553684949564809677864522417041639512933806794232354223826262154508950271949764583849083972967642587778197779127063591201123312548182885603427440981731822883101260509710567731', + }, + z: '57056568014385132434061065334124327103768023932445648883765905576432733866307137325457775876741578717650388638737098805750938053855430851133826479968450532729423746605371536096355616166421996729493639634413002114547787617999178137950004782677177313856876420539744625174205603354705595789330008560775613287118432593300023801651460885523314713996258581986238928077688246511704050386525431448517516821261983193275502089060128363906909778842476516981025598807378338053788433033754999771876361716562378445777250912525673660842724168260417083076824975992327559199634032439358787956784395443246565622469187082767614421691234', + rctxt: + '17146114573198643698878017247599007910707723139165264508694101989891626297408755744139587708989465136799243292477223763665064840330721616213638280284119891715514951989022398510785960099708705561761504012512387129498731093386014964896897751536856287377064154297370092339714578039195258061017640952790913108285519632654466006255438773382930416822756630391947263044087385305540191237328903426888518439803354213792647775798033294505898635058814132665832000734168261793545453678083703704122695006541391598116359796491845268631009298069826949515604008666680160398698425061157356267086946953480945396595351944425658076127674', + n: '95671911213029889766246243339609567053285242961853979532076192834533577534909796042025401129640348836502648821408485216223269830089771714177855160978214805993386076928594836829216646288195127289421136294309746871614765411402917891972999085287429566166932354413679994469616357622976775651506242447852304853465380257226445481515631782793575184420720296120464167257703633829902427169144462981949944348928086406211627174233811365419264314148304536534528344413738913277713548403058098093453580992173145127632199215550027527631259565822872315784889212327945030315062879193999012349220118290071491899498795367403447663354833', + s: '1573939820553851804028472930351082111827449763317396231059458630252708273163050576299697385049087601314071156646675105028237105229428440185022593174121924731226634356276616495327358864865629675802738680754755949997611920669823449540027707876555408118172529688443208301403297680159171306000341239398135896274940688268460793682007115152428685521865921925309154307574955324973580144009271977076586453011938089159885164705002797196738438392179082905738155386545935208094240038135576042886730802817809757582039362798495805441520744154270346780731494125065136433163757697326955962282840631850597919384092584727207908978907', + }, + }, + signature_type: 'CL', + ref: schemaResponse.result.txnMetadata.seqNo, + tag: 'TAG', + }, + }, + }, + }) }) test('fails writing a schema with existing verson number using the pool', async () => { @@ -226,13 +246,6 @@ describe('IndyVdrPoolService', () => { const response = await pool.submitWriteRequest(agentContext, schemaRequest, signerKey) expect(response).toBeDefined() - - // const response2 = await pool.submitWriteRequest(agentContext, schemaRequest2, signerKey) - - // expect(response2.op).toEqual('REJECT') - - // @ts-ignore - // expect(response2.identifier).toEqual('TL1EaPFCZ8Si5aUrqScBDt') }) }) }) From 62b7ae325ee5eaeb938d80f31f814895624baac6 Mon Sep 17 00:00:00 2001 From: vickysomtee Date: Tue, 24 Jan 2023 14:10:46 +0100 Subject: [PATCH 15/41] feat: IndyVdr Package Signed-off-by: vickysomtee --- packages/indy-vdr/src/pool/IndyVdrPoolService.ts | 12 ++++++------ packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 10 +--------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts index 05e6e57f4a..2fa5f001ca 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts @@ -2,7 +2,7 @@ import type { IndyVdrPoolConfig } from './IndyVdrPool' import type { AgentContext } from '@aries-framework/core' import type { GetNymResponse } from 'indy-vdr-test-shared' -import { Logger, InjectionSymbols, injectable, inject, PersistedLruCache, CacheRepository } from '@aries-framework/core' +import { Logger, InjectionSymbols, injectable, inject, CacheModuleConfig} from '@aries-framework/core' import { GetNymRequest } from 'indy-vdr-test-shared' import { IndyVdrError, IndyVdrNotFoundError, IndyVdrConfiguredError } from '../error' @@ -25,12 +25,10 @@ export interface CachedDidResponse { export class IndyVdrPoolService { public pools: IndyVdrPool[] = [] private logger: Logger - private didCache: PersistedLruCache - public constructor(cacheRepository: CacheRepository, @inject(InjectionSymbols.Logger) logger: Logger) { + public constructor( @inject(InjectionSymbols.Logger) logger: Logger) { this.logger = logger - this.didCache = new PersistedLruCache(INDY_VDR_LEGACY_DID_POOL_CACHE_ID, DID_POOL_CACHE_LIMIT, cacheRepository) } public setPools(poolConfigs: IndyVdrPoolConfig[]) { @@ -89,7 +87,9 @@ export class IndyVdrPoolService { ) } - const cachedNymResponse = await this.didCache.get(agentContext, did) + const didCache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache + + const cachedNymResponse = await didCache.get(agentContext, `IndyVdrPoolService:${did}`) const pool = this.pools.find((pool) => pool.indyNamespace === cachedNymResponse?.indyNamespace) // If we have the nym response with associated pool in the cache, we'll use that @@ -136,7 +136,7 @@ export class IndyVdrPoolService { value = productionOrNonProduction[0].value } - await this.didCache.set(agentContext, did, { + await didCache.set(agentContext, did, { nymResponse: { did: value.did.nymResponse.did, verkey: value.did.nymResponse.verkey, diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts index 6e9e9b1f44..7b6c0b0522 100644 --- a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -1,7 +1,5 @@ -import type { CacheRecord } from '../../core/src/cache' import { - EventEmitter, IndyWallet, Key, KeyType, @@ -9,19 +7,13 @@ import { TypedArrayEncoder, } from '@aries-framework/core' import { GetNymRequest, NymRequest, SchemaRequest, CredentialDefinitionRequest } from 'indy-vdr-test-shared' -import { Subject } from 'rxjs' -import { CacheRepository } from '../../core/src/cache' -import { IndyStorageService } from '../../core/src/storage/IndyStorageService' import { agentDependencies, genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' import { IndyVdrPool } from '../src/pool' import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' -const storageService = new IndyStorageService(agentDependencies) -const eventEmitter = new EventEmitter(agentDependencies, new Subject()) -const cacheRepository = new CacheRepository(storageService, eventEmitter) -const indyVdrPoolService = new IndyVdrPoolService(cacheRepository, testLogger) +const indyVdrPoolService = new IndyVdrPoolService(testLogger) const wallet = new IndyWallet(agentDependencies, testLogger, new SigningProviderRegistry([])) const agentConfig = getAgentConfig('IndyVdrPoolService') const agentContext = getAgentContext({ wallet, agentConfig }) From e113e1e11226c306939de13a9288fed7d7fad344 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 25 Jan 2023 07:32:34 -0300 Subject: [PATCH 16/41] import initial sov resolver Signed-off-by: Ariel Gentile --- .../src/dids/IndyVdrSovDidRegistrar.ts | 259 ++++++++++++++++++ .../src/dids/IndyVdrSovDidResolver.ts | 99 +++++++ packages/indy-vdr/src/dids/didSovUtil.ts | 166 +++++++++++ packages/indy-vdr/src/dids/index.ts | 7 + 4 files changed, 531 insertions(+) create mode 100644 packages/indy-vdr/src/dids/IndyVdrSovDidRegistrar.ts create mode 100644 packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts create mode 100644 packages/indy-vdr/src/dids/didSovUtil.ts create mode 100644 packages/indy-vdr/src/dids/index.ts diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidRegistrar.ts new file mode 100644 index 0000000000..b0743843b1 --- /dev/null +++ b/packages/indy-vdr/src/dids/IndyVdrSovDidRegistrar.ts @@ -0,0 +1,259 @@ +import type { IndyVdrPool } from '../pool' +import type { IndyEndpointAttrib } from './didSovUtil' +import type { + AgentContext, + DidRegistrar, + DidCreateOptions, + DidCreateResult, + DidDeactivateResult, + DidUpdateResult, +} from '@aries-framework/core' + +import { Key, KeyType, injectable, DidDocumentRole, DidRecord, DidRepository } from '@aries-framework/core' +import { AttribRequest, NymRequest } from 'indy-vdr-test-shared' + +import { IndyVdrError } from '../error' +import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' + +import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovUtil' + +@injectable() +export class IndyVdrSovDidRegistrar implements DidRegistrar { + public readonly supportedMethods = ['sov'] + private didRepository: DidRepository + private indyVdrPoolService: IndyVdrPoolService + + public constructor(didRepository: DidRepository, indyVdrPoolService: IndyVdrPoolService) { + this.didRepository = didRepository + this.indyVdrPoolService = indyVdrPoolService + } + + public async create(agentContext: AgentContext, options: IndyVdrSovDidCreateOptions): Promise { + const { alias, role, submitterDid, indyNamespace } = options.options + const seed = options.secret?.seed + + if (seed && (typeof seed !== 'string' || seed.length !== 32)) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Invalid seed provided', + }, + } + } + + if (!submitterDid.startsWith('did:sov:')) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Submitter did must be a valid did:sov did', + }, + } + } + + try { + const key = await agentContext.wallet.createKey({ seed, keyType: KeyType.Ed25519 }) + + const unqualifiedIndyDid = key.publicKeyBase58 + const verkey = key.fingerprint + + const qualifiedSovDid = `did:sov:${unqualifiedIndyDid}` + const unqualifiedSubmitterDid = submitterDid.replace('did:sov:', '') + + // TODO: it should be possible to pass the pool used for writing to the indy ledger service. + // The easiest way to do this would be to make the submitterDid a fully qualified did, including the indy namespace. + const pool = indyNamespace + ? this.indyVdrPoolService.getPoolForNamespace(indyNamespace) + : this.indyVdrPoolService.pools[0] + await this.registerPublicDid(agentContext, unqualifiedSubmitterDid, unqualifiedIndyDid, verkey, alias, pool, role) + + // Create did document + const didDocumentBuilder = sovDidDocumentFromDid(qualifiedSovDid, verkey) + + // Add services if endpoints object was passed. + if (options.options.endpoints) { + await this.setEndpointsForDid(agentContext, unqualifiedIndyDid, options.options.endpoints, pool) + addServicesFromEndpointsAttrib( + didDocumentBuilder, + qualifiedSovDid, + options.options.endpoints, + `${qualifiedSovDid}#key-agreement-1` + ) + } + + // Build did document. + const didDocument = didDocumentBuilder.build() + + const didIndyNamespace = pool.config.indyNamespace + const qualifiedIndyDid = `did:indy:${didIndyNamespace}:${unqualifiedIndyDid}` + + // Save the did so we know we created it and can issue with it + const didRecord = new DidRecord({ + id: qualifiedSovDid, + did: qualifiedSovDid, + role: DidDocumentRole.Created, + tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key: Key) => key.fingerprint), + qualifiedIndyDid, + }, + }) + await this.didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: { + qualifiedIndyDid, + }, + didRegistrationMetadata: { + didIndyNamespace, + }, + didState: { + state: 'finished', + did: qualifiedSovDid, + didDocument, + secret: { + // FIXME: the uni-registrar creates the seed in the registrar method + // if it doesn't exist so the seed can always be returned. Currently + // we can only return it if the seed was passed in by the user. Once + // we have a secure method for generating seeds we should use the same + // approach + seed: options.secret?.seed, + }, + }, + } + } catch (error) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:sov not implemented yet`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:sov not implemented yet`, + }, + } + } + + public async registerPublicDid( + agentContext: AgentContext, + submitterDid: string, + targetDid: string, + verkey: string, + alias: string, + pool: IndyVdrPool, + role?: string + ) { + try { + agentContext.config.logger.debug(`Register public did '${targetDid}' on ledger '${pool}'`) + + const request = new NymRequest({ submitterDid, dest: targetDid, verkey, alias }) + + const key = await agentContext.wallet.createKey({ keyType: KeyType.Ed25519, seed: 'bla' }) + + const signingKey = Key.fromPublicKeyBase58(submitterDid, KeyType.Ed25519) + + const response = await pool.submitWriteRequest(agentContext, request, signingKey) + + agentContext.config.logger.debug(`Registered public did '${targetDid}' on ledger '${pool.indyNamespace}'`, { + response, + }) + + return targetDid + } catch (error) { + agentContext.config.logger.error( + `Error registering public did '${targetDid}' on ledger '${pool.indyNamespace}'`, + { + error, + submitterDid, + targetDid, + verkey, + alias, + role, + pool: pool.indyNamespace, + } + ) + + throw error + } + } + + public async setEndpointsForDid( + agentContext: AgentContext, + did: string, + endpoints: IndyEndpointAttrib, + pool: IndyVdrPool + ): Promise { + try { + agentContext.config.logger.debug(`Set endpoints for did '${did}' on ledger '${pool.indyNamespace}'`, endpoints) + + const request = new AttribRequest({ + submitterDid: did, + targetDid: did, + raw: JSON.stringify({ endpoint: endpoints }), + }) + + const signingKey = Key.fromPublicKeyBase58(did, KeyType.Ed25519) + const response = await pool.submitWriteRequest(agentContext, request, signingKey) + agentContext.config.logger.debug( + `Successfully set endpoints for did '${did}' on ledger '${pool.indyNamespace}'`, + { + response, + endpoints, + } + ) + } catch (error) { + agentContext.config.logger.error(`Error setting endpoints for did '${did}' on ledger '${pool.indyNamespace}'`, { + error, + did, + endpoints, + }) + + throw new IndyVdrError(error) + } + } +} + +export interface IndyVdrSovDidCreateOptions extends DidCreateOptions { + method: 'sov' + did?: undefined + // As did:sov is so limited, we require everything needed to construct the did document to be passed + // through the options object. Once we support did:indy we can allow the didDocument property. + didDocument?: never + options: { + alias: string + role?: string + endpoints?: IndyEndpointAttrib + indyNamespace?: string + submitterDid: string + } + secret?: { + seed?: string + } +} + +// Update and Deactivate not supported for did:sov +export type IndyVdrSovDidUpdateOptions = never +export type IndyVdrSovDidDeactivateOptions = never diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts new file mode 100644 index 0000000000..4cad4a1d20 --- /dev/null +++ b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts @@ -0,0 +1,99 @@ +import type { GetNymResponseData, IndyEndpointAttrib } from './didSovUtil' +import type { DidResolutionResult, ParsedDid, DidResolver, AgentContext } from '@aries-framework/core' + +import { injectable } from '@aries-framework/core' +import { GetAttribRequest, GetNymRequest } from 'indy-vdr-test-shared' + +import { IndyVdrError, IndyVdrNotFoundError } from '../error' +import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' + +import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovUtil' + +@injectable() +export class IndyVdrSovDidResolver implements DidResolver { + private indyVdrPoolService: IndyVdrPoolService + + public constructor(indyVdrPoolService: IndyVdrPoolService) { + this.indyVdrPoolService = indyVdrPoolService + } + + public readonly supportedMethods = ['sov'] + + public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { + const didDocumentMetadata = {} + + try { + const nym = await this.getPublicDid(agentContext, parsed.id) + const endpoints = await this.getEndpointsForDid(agentContext, parsed.id) + + const keyAgreementId = `${parsed.did}#key-agreement-1` + const builder = sovDidDocumentFromDid(parsed.did, nym.verkey) + addServicesFromEndpointsAttrib(builder, parsed.did, endpoints, keyAgreementId) + + return { + didDocument: builder.build(), + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } + + private async getPublicDid(agentContext: AgentContext, did: string) { + const pool = await this.indyVdrPoolService.getPoolForDid(agentContext, did) + + const request = new GetNymRequest({ dest: did }) + + const didResponse = await pool.submitReadRequest(request) + + if (!didResponse.result.data) { + throw new IndyVdrNotFoundError(`DID ${did} not found`) + } + return JSON.parse(didResponse.result.data) as GetNymResponseData + } + + private async getEndpointsForDid(agentContext: AgentContext, did: string) { + const pool = await this.indyVdrPoolService.getPoolForDid(agentContext, did) + + try { + agentContext.config.logger.debug(`Get endpoints for did '${did}' from ledger '${pool.indyNamespace}'`) + + const request = new GetAttribRequest({ targetDid: did }) + + agentContext.config.logger.debug( + `Submitting get endpoint ATTRIB request for did '${did}' to ledger '${pool.indyNamespace}'` + ) + const response = await pool.submitReadRequest(request) + + if (!response.result.data) return {} + + const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib + agentContext.config.logger.debug( + `Got endpoints '${JSON.stringify(endpoints)}' for did '${did}' from ledger '${pool.indyNamespace}'`, + { + response, + endpoints, + } + ) + + return endpoints ?? {} + } catch (error) { + agentContext.config.logger.error( + `Error retrieving endpoints for did '${did}' from ledger '${pool.indyNamespace}'`, + { + error, + } + ) + + throw new IndyVdrError(error) + } + } +} diff --git a/packages/indy-vdr/src/dids/didSovUtil.ts b/packages/indy-vdr/src/dids/didSovUtil.ts new file mode 100644 index 0000000000..9fbe414b78 --- /dev/null +++ b/packages/indy-vdr/src/dids/didSovUtil.ts @@ -0,0 +1,166 @@ +import { + TypedArrayEncoder, + DidDocumentService, + DidDocumentBuilder, + DidCommV1Service, + DidCommV2Service, + convertPublicKeyToX25519, +} from '@aries-framework/core' + +export interface IndyEndpointAttrib { + endpoint?: string + types?: Array<'endpoint' | 'did-communication' | 'DIDComm'> + routingKeys?: string[] + [key: string]: unknown +} + +export interface GetNymResponseData { + did: string + verkey: string + role: string +} + +export const FULL_VERKEY_REGEX = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/ + +/** + * Check a base58 encoded string against a regex expression to determine if it is a full valid verkey + * @param verkey Base58 encoded string representation of a verkey + * @return Boolean indicating if the string is a valid verkey + */ +export function isFullVerkey(verkey: string): boolean { + return FULL_VERKEY_REGEX.test(verkey) +} + +export function getFullVerkey(did: string, verkey: string) { + if (isFullVerkey(verkey)) return verkey + + // Did could have did:xxx prefix, only take the last item after : + const id = did.split(':').pop() ?? did + // Verkey is prefixed with ~ if abbreviated + const verkeyWithoutTilde = verkey.slice(1) + + // Create base58 encoded public key (32 bytes) + return TypedArrayEncoder.toBase58( + Buffer.concat([ + // Take did identifier (16 bytes) + TypedArrayEncoder.fromBase58(id), + // Concat the abbreviated verkey (16 bytes) + TypedArrayEncoder.fromBase58(verkeyWithoutTilde), + ]) + ) +} + +export function sovDidDocumentFromDid(fullDid: string, verkey: string) { + const verificationMethodId = `${fullDid}#key-1` + const keyAgreementId = `${fullDid}#key-agreement-1` + + const publicKeyBase58 = getFullVerkey(fullDid, verkey) + const publicKeyX25519 = TypedArrayEncoder.toBase58( + convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(publicKeyBase58)) + ) + + const builder = new DidDocumentBuilder(fullDid) + .addContext('https://w3id.org/security/suites/ed25519-2018/v1') + .addContext('https://w3id.org/security/suites/x25519-2019/v1') + .addVerificationMethod({ + controller: fullDid, + id: verificationMethodId, + publicKeyBase58: publicKeyBase58, + type: 'Ed25519VerificationKey2018', + }) + .addVerificationMethod({ + controller: fullDid, + id: keyAgreementId, + publicKeyBase58: publicKeyX25519, + type: 'X25519KeyAgreementKey2019', + }) + .addAuthentication(verificationMethodId) + .addAssertionMethod(verificationMethodId) + .addKeyAgreement(keyAgreementId) + + return builder +} + +// Process Indy Attrib Endpoint Types according to: https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html > Read (Resolve) > DID Service Endpoint +function processEndpointTypes(types?: string[]) { + const expectedTypes = ['endpoint', 'did-communication', 'DIDComm'] + const defaultTypes = ['endpoint', 'did-communication'] + + // Return default types if types "is NOT present [or] empty" + if (!types || types.length <= 0) { + return defaultTypes + } + + // Return default types if types "contain any other values" + for (const type of types) { + if (!expectedTypes.includes(type)) { + return defaultTypes + } + } + + // Return provided types + return types +} + +export function addServicesFromEndpointsAttrib( + builder: DidDocumentBuilder, + did: string, + endpoints: IndyEndpointAttrib, + keyAgreementId: string +) { + const { endpoint, routingKeys, types, ...otherEndpoints } = endpoints + + if (endpoint) { + const processedTypes = processEndpointTypes(types) + + // If 'endpoint' included in types, add id to the services array + if (processedTypes.includes('endpoint')) { + builder.addService( + new DidDocumentService({ + id: `${did}#endpoint`, + serviceEndpoint: endpoint, + type: 'endpoint', + }) + ) + } + + // If 'did-communication' included in types, add DIDComm v1 entry + if (processedTypes.includes('did-communication')) { + builder.addService( + new DidCommV1Service({ + id: `${did}#did-communication`, + serviceEndpoint: endpoint, + priority: 0, + routingKeys: routingKeys ?? [], + recipientKeys: [keyAgreementId], + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + + // If 'DIDComm' included in types, add DIDComm v2 entry + if (processedTypes.includes('DIDComm')) { + builder + .addService( + new DidCommV2Service({ + id: `${did}#didcomm-1`, + serviceEndpoint: endpoint, + routingKeys: routingKeys ?? [], + accept: ['didcomm/v2'], + }) + ) + .addContext('https://didcomm.org/messaging/contexts/v2') + } + } + } + + // Add other endpoint types + for (const [type, endpoint] of Object.entries(otherEndpoints)) { + builder.addService( + new DidDocumentService({ + id: `${did}#${type}`, + serviceEndpoint: endpoint as string, + type, + }) + ) + } +} diff --git a/packages/indy-vdr/src/dids/index.ts b/packages/indy-vdr/src/dids/index.ts new file mode 100644 index 0000000000..62d9017675 --- /dev/null +++ b/packages/indy-vdr/src/dids/index.ts @@ -0,0 +1,7 @@ +export { + IndyVdrSovDidRegistrar, + IndyVdrSovDidCreateOptions, + IndyVdrSovDidDeactivateOptions, + IndyVdrSovDidUpdateOptions, +} from './IndyVdrSovDidRegistrar' +export { IndySdkSovDidResolver } from './IndyVdrSovDidResolver' From 06c58c5e11c50cabfe6a3c9fee85c3bd3944160f Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 26 Jan 2023 20:47:31 -0300 Subject: [PATCH 17/41] test: add tests for SOV resolver Signed-off-by: Ariel Gentile --- .../src/dids/IndyVdrSovDidRegistrar.ts | 259 ------------------ .../src/dids/IndyVdrSovDidResolver.ts | 2 +- .../__tests__/IndyVdrSovDidResolver.test.ts | 127 +++++++++ packages/indy-vdr/src/dids/index.ts | 8 +- packages/indy-vdr/tests/helpers.ts | 43 +++ .../tests/indy-vdr-did-resolver.e2e.test.ts | 184 +++++++++++++ 6 files changed, 356 insertions(+), 267 deletions(-) delete mode 100644 packages/indy-vdr/src/dids/IndyVdrSovDidRegistrar.ts create mode 100644 packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts create mode 100644 packages/indy-vdr/tests/helpers.ts create mode 100644 packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidRegistrar.ts deleted file mode 100644 index b0743843b1..0000000000 --- a/packages/indy-vdr/src/dids/IndyVdrSovDidRegistrar.ts +++ /dev/null @@ -1,259 +0,0 @@ -import type { IndyVdrPool } from '../pool' -import type { IndyEndpointAttrib } from './didSovUtil' -import type { - AgentContext, - DidRegistrar, - DidCreateOptions, - DidCreateResult, - DidDeactivateResult, - DidUpdateResult, -} from '@aries-framework/core' - -import { Key, KeyType, injectable, DidDocumentRole, DidRecord, DidRepository } from '@aries-framework/core' -import { AttribRequest, NymRequest } from 'indy-vdr-test-shared' - -import { IndyVdrError } from '../error' -import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' - -import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovUtil' - -@injectable() -export class IndyVdrSovDidRegistrar implements DidRegistrar { - public readonly supportedMethods = ['sov'] - private didRepository: DidRepository - private indyVdrPoolService: IndyVdrPoolService - - public constructor(didRepository: DidRepository, indyVdrPoolService: IndyVdrPoolService) { - this.didRepository = didRepository - this.indyVdrPoolService = indyVdrPoolService - } - - public async create(agentContext: AgentContext, options: IndyVdrSovDidCreateOptions): Promise { - const { alias, role, submitterDid, indyNamespace } = options.options - const seed = options.secret?.seed - - if (seed && (typeof seed !== 'string' || seed.length !== 32)) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Invalid seed provided', - }, - } - } - - if (!submitterDid.startsWith('did:sov:')) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Submitter did must be a valid did:sov did', - }, - } - } - - try { - const key = await agentContext.wallet.createKey({ seed, keyType: KeyType.Ed25519 }) - - const unqualifiedIndyDid = key.publicKeyBase58 - const verkey = key.fingerprint - - const qualifiedSovDid = `did:sov:${unqualifiedIndyDid}` - const unqualifiedSubmitterDid = submitterDid.replace('did:sov:', '') - - // TODO: it should be possible to pass the pool used for writing to the indy ledger service. - // The easiest way to do this would be to make the submitterDid a fully qualified did, including the indy namespace. - const pool = indyNamespace - ? this.indyVdrPoolService.getPoolForNamespace(indyNamespace) - : this.indyVdrPoolService.pools[0] - await this.registerPublicDid(agentContext, unqualifiedSubmitterDid, unqualifiedIndyDid, verkey, alias, pool, role) - - // Create did document - const didDocumentBuilder = sovDidDocumentFromDid(qualifiedSovDid, verkey) - - // Add services if endpoints object was passed. - if (options.options.endpoints) { - await this.setEndpointsForDid(agentContext, unqualifiedIndyDid, options.options.endpoints, pool) - addServicesFromEndpointsAttrib( - didDocumentBuilder, - qualifiedSovDid, - options.options.endpoints, - `${qualifiedSovDid}#key-agreement-1` - ) - } - - // Build did document. - const didDocument = didDocumentBuilder.build() - - const didIndyNamespace = pool.config.indyNamespace - const qualifiedIndyDid = `did:indy:${didIndyNamespace}:${unqualifiedIndyDid}` - - // Save the did so we know we created it and can issue with it - const didRecord = new DidRecord({ - id: qualifiedSovDid, - did: qualifiedSovDid, - role: DidDocumentRole.Created, - tags: { - recipientKeyFingerprints: didDocument.recipientKeys.map((key: Key) => key.fingerprint), - qualifiedIndyDid, - }, - }) - await this.didRepository.save(agentContext, didRecord) - - return { - didDocumentMetadata: { - qualifiedIndyDid, - }, - didRegistrationMetadata: { - didIndyNamespace, - }, - didState: { - state: 'finished', - did: qualifiedSovDid, - didDocument, - secret: { - // FIXME: the uni-registrar creates the seed in the registrar method - // if it doesn't exist so the seed can always be returned. Currently - // we can only return it if the seed was passed in by the user. Once - // we have a secure method for generating seeds we should use the same - // approach - seed: options.secret?.seed, - }, - }, - } - } catch (error) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `unknownError: ${error.message}`, - }, - } - } - } - - public async update(): Promise { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `notImplemented: updating did:sov not implemented yet`, - }, - } - } - - public async deactivate(): Promise { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `notImplemented: deactivating did:sov not implemented yet`, - }, - } - } - - public async registerPublicDid( - agentContext: AgentContext, - submitterDid: string, - targetDid: string, - verkey: string, - alias: string, - pool: IndyVdrPool, - role?: string - ) { - try { - agentContext.config.logger.debug(`Register public did '${targetDid}' on ledger '${pool}'`) - - const request = new NymRequest({ submitterDid, dest: targetDid, verkey, alias }) - - const key = await agentContext.wallet.createKey({ keyType: KeyType.Ed25519, seed: 'bla' }) - - const signingKey = Key.fromPublicKeyBase58(submitterDid, KeyType.Ed25519) - - const response = await pool.submitWriteRequest(agentContext, request, signingKey) - - agentContext.config.logger.debug(`Registered public did '${targetDid}' on ledger '${pool.indyNamespace}'`, { - response, - }) - - return targetDid - } catch (error) { - agentContext.config.logger.error( - `Error registering public did '${targetDid}' on ledger '${pool.indyNamespace}'`, - { - error, - submitterDid, - targetDid, - verkey, - alias, - role, - pool: pool.indyNamespace, - } - ) - - throw error - } - } - - public async setEndpointsForDid( - agentContext: AgentContext, - did: string, - endpoints: IndyEndpointAttrib, - pool: IndyVdrPool - ): Promise { - try { - agentContext.config.logger.debug(`Set endpoints for did '${did}' on ledger '${pool.indyNamespace}'`, endpoints) - - const request = new AttribRequest({ - submitterDid: did, - targetDid: did, - raw: JSON.stringify({ endpoint: endpoints }), - }) - - const signingKey = Key.fromPublicKeyBase58(did, KeyType.Ed25519) - const response = await pool.submitWriteRequest(agentContext, request, signingKey) - agentContext.config.logger.debug( - `Successfully set endpoints for did '${did}' on ledger '${pool.indyNamespace}'`, - { - response, - endpoints, - } - ) - } catch (error) { - agentContext.config.logger.error(`Error setting endpoints for did '${did}' on ledger '${pool.indyNamespace}'`, { - error, - did, - endpoints, - }) - - throw new IndyVdrError(error) - } - } -} - -export interface IndyVdrSovDidCreateOptions extends DidCreateOptions { - method: 'sov' - did?: undefined - // As did:sov is so limited, we require everything needed to construct the did document to be passed - // through the options object. Once we support did:indy we can allow the didDocument property. - didDocument?: never - options: { - alias: string - role?: string - endpoints?: IndyEndpointAttrib - indyNamespace?: string - submitterDid: string - } - secret?: { - seed?: string - } -} - -// Update and Deactivate not supported for did:sov -export type IndyVdrSovDidUpdateOptions = never -export type IndyVdrSovDidDeactivateOptions = never diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts index 4cad4a1d20..f9f424a1e9 100644 --- a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts @@ -66,7 +66,7 @@ export class IndyVdrSovDidResolver implements DidResolver { try { agentContext.config.logger.debug(`Get endpoints for did '${did}' from ledger '${pool.indyNamespace}'`) - const request = new GetAttribRequest({ targetDid: did }) + const request = new GetAttribRequest({ targetDid: did, raw: 'endpoint' }) agentContext.config.logger.debug( `Submitting get endpoint ATTRIB request for did '${did}' to ledger '${pool.indyNamespace}'` diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts new file mode 100644 index 0000000000..dc811ea2ba --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts @@ -0,0 +1,127 @@ +import { JsonTransformer } from '@aries-framework/core' + +import didSovR1xKJw17sUoXhejEpugMYJFixture from '../../../../core/src/modules/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json' +import didSovWJz9mHyW9BZksioQnRsrAoFixture from '../../../../core/src/modules/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json' +import { parseDid } from '../../../../core/src/modules/dids/domain/parse' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { IndyVdrPool } from '../../pool' +import { IndyVdrPoolService } from '../../pool/IndyVdrPoolService' +import { IndyVdrSovDidResolver } from '../IndyVdrSovDidResolver' + +jest.mock('../../pool/IndyVdrPoolService') +const IndyVdrPoolServiceMock = IndyVdrPoolService as jest.Mock +const poolServiceMock = new IndyVdrPoolServiceMock() + +jest.mock('../../pool/IndyVdrPool') +const IndyVdrPoolMock = IndyVdrPool as jest.Mock +const poolMock = new IndyVdrPoolMock() + +mockFunction(poolServiceMock.getPoolForDid).mockResolvedValue(poolMock) + +const agentConfig = getAgentConfig('IndyVdrSovDidResolver') + +const agentContext = getAgentContext({ + agentConfig, + registerInstances: [[IndyVdrPoolService, poolServiceMock]], +}) + +const resolver = new IndyVdrSovDidResolver(poolServiceMock) + +describe('DidResolver', () => { + describe('IndyVdrSovDidResolver', () => { + it('should correctly resolve a did:sov document', async () => { + const did = 'did:sov:R1xKJw17sUoXhejEpugMYJ' + + const nymResponse = { + result: { + data: JSON.stringify({ + did: 'R1xKJw17sUoXhejEpugMYJ', + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + role: 'ENDORSER', + }), + }, + } + + const attribResponse = { + result: { + data: JSON.stringify({ + endpoint: { + endpoint: 'https://ssi.com', + profile: 'https://profile.com', + hub: 'https://hub.com', + }, + }), + }, + } + + jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(nymResponse) + jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(attribResponse) + + const result = await resolver.resolve(agentContext, did, parseDid(did)) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didSovR1xKJw17sUoXhejEpugMYJFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should resolve a did:sov document with routingKeys and types entries in the attrib', async () => { + const did = 'did:sov:WJz9mHyW9BZksioQnRsrAo' + + const nymResponse = { + result: { + data: JSON.stringify({ + did: 'WJz9mHyW9BZksioQnRsrAo', + verkey: 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8', + role: 'ENDORSER', + }), + }, + } + + const attribResponse = { + result: { + data: JSON.stringify({ + endpoint: { + endpoint: 'https://agent.com', + types: ['endpoint', 'did-communication', 'DIDComm'], + routingKeys: ['routingKey1', 'routingKey2'], + }, + }), + }, + } + + jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(nymResponse) + jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(attribResponse) + + const result = await resolver.resolve(agentContext, did, parseDid(did)) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didSovWJz9mHyW9BZksioQnRsrAoFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should return did resolution metadata with error if the indy ledger service throws an error', async () => { + const did = 'did:sov:R1xKJw17sUoXhejEpugMYJ' + + jest.spyOn(poolMock, 'submitReadRequest').mockRejectedValue(new Error('Error submitting read request')) + + const result = await resolver.resolve(agentContext, did, parseDid(did)) + + expect(result).toMatchObject({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did 'did:sov:R1xKJw17sUoXhejEpugMYJ': Error: Error submitting read request`, + }, + }) + }) + }) +}) diff --git a/packages/indy-vdr/src/dids/index.ts b/packages/indy-vdr/src/dids/index.ts index 62d9017675..7f9973684d 100644 --- a/packages/indy-vdr/src/dids/index.ts +++ b/packages/indy-vdr/src/dids/index.ts @@ -1,7 +1 @@ -export { - IndyVdrSovDidRegistrar, - IndyVdrSovDidCreateOptions, - IndyVdrSovDidDeactivateOptions, - IndyVdrSovDidUpdateOptions, -} from './IndyVdrSovDidRegistrar' -export { IndySdkSovDidResolver } from './IndyVdrSovDidResolver' +export { IndyVdrSovDidResolver } from './IndyVdrSovDidResolver' diff --git a/packages/indy-vdr/tests/helpers.ts b/packages/indy-vdr/tests/helpers.ts new file mode 100644 index 0000000000..7ea99d8263 --- /dev/null +++ b/packages/indy-vdr/tests/helpers.ts @@ -0,0 +1,43 @@ +import type { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' +import type { AgentContext, Key } from '@aries-framework/core' + +import { KeyType } from '@aries-framework/core' +import { AttribRequest, NymRequest } from 'indy-vdr-test-shared' + +import { indyDidFromPublicKeyBase58 } from '../src/utils/did' + +export async function createDidOnLedger( + indyVdrPoolService: IndyVdrPoolService, + agentContext: AgentContext, + submitterDid: string, + signerKey: Key +) { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + const key = await agentContext.wallet.createKey({ keyType: KeyType.Ed25519 }) + const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) + + const nymRequest = new NymRequest({ + dest: did, + submitterDid, + verkey: key.publicKeyBase58, + }) + + await pool.submitWriteRequest(agentContext, nymRequest, signerKey) + + const attribRequest = new AttribRequest({ + submitterDid: did, + targetDid: did, + raw: JSON.stringify({ + endpoint: { + endpoint: 'https://agent.com', + types: ['endpoint', 'did-communication', 'DIDComm'], + routingKeys: ['routingKey1', 'routingKey2'], + }, + }), + }) + + await pool.submitWriteRequest(agentContext, attribRequest, key) + + return { did, key } +} diff --git a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts new file mode 100644 index 0000000000..6292bd2cd9 --- /dev/null +++ b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts @@ -0,0 +1,184 @@ +import type { Key } from '@aries-framework/core' + +import { + CacheModuleConfig, + ConsoleLogger, + InMemoryLruCache, + LogLevel, + JsonTransformer, + IndyWallet, + KeyType, + SigningProviderRegistry, + TypedArrayEncoder, +} from '@aries-framework/core' +import { AttribRequest, NymRequest } from 'indy-vdr-test-shared' + +import { parseDid } from '../../core/src/modules/dids/domain/parse' +import { agentDependencies, genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import { IndyVdrSovDidResolver } from '../src/dids' +import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' +import { indyDidFromPublicKeyBase58 } from '../src/utils/did' +import { createDidOnLedger } from './helpers' + +const logger = new ConsoleLogger(LogLevel.trace) +const indyVdrPoolService = new IndyVdrPoolService(logger) +const wallet = new IndyWallet(agentDependencies, logger, new SigningProviderRegistry([])) +const agentConfig = getAgentConfig('IndyVdrPoolService', { logger }) + +const cache = new InMemoryLruCache({ limit: 200 }) +const agentContext = getAgentContext({ + wallet, + agentConfig, + registerInstances: [[CacheModuleConfig, new CacheModuleConfig({ cache })]], +}) + +const indyVdrSovDidResolver = new IndyVdrSovDidResolver(indyVdrPoolService) + +const config = { + isProduction: false, + genesisTransactions, + indyNamespace: `pool:localtest`, + transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, +} as const + +let signerKey: Key + +indyVdrPoolService.setPools([config]) + +describe('IndyVdrSov', () => { + beforeAll(async () => { + await indyVdrPoolService.connectToPools() + + await wallet.createAndOpen(agentConfig.walletConfig!) + signerKey = await wallet.createKey({ seed: '000000000000000000000000Trustee9', keyType: KeyType.Ed25519 }) + }) + + afterAll(async () => { + for (const pool of indyVdrPoolService.pools) { + pool.close() + } + + await wallet.delete() + }) + + describe('did:sov resolver', () => { + test('can resolve a did sov using the pool', async () => { + const did = 'did:sov:TL1EaPFCZ8Si5aUrqScBDt' + const didResult = await indyVdrSovDidResolver.resolve(agentContext, did, parseDid(did)) + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#key-1`, + publicKeyBase58: expect.any(String), + }, + { + controller: did, + type: 'X25519KeyAgreementKey2019', + id: `${did}#key-agreement-1`, + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#key-1`], + assertionMethod: [`${did}#key-1`], + keyAgreement: [`${did}#key-agreement-1`], + service: undefined, + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + test('resolve a did with endpoints', async () => { + // First we need to create a new DID and add ATTRIB endpoint to it + const { did } = await createDidOnLedger( + indyVdrPoolService, + agentContext, + indyDidFromPublicKeyBase58(signerKey.publicKeyBase58), + signerKey + ) + + // DID created. Now resolve it + + const fullyQualifiedDid = `did:sov:${did}` + const didResult = await indyVdrSovDidResolver.resolve( + agentContext, + fullyQualifiedDid, + parseDid(fullyQualifiedDid) + ) + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + 'https://didcomm.org/messaging/contexts/v2', + ], + id: fullyQualifiedDid, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: fullyQualifiedDid, + id: `${fullyQualifiedDid}#key-1`, + publicKeyBase58: expect.any(String), + }, + { + controller: fullyQualifiedDid, + type: 'X25519KeyAgreementKey2019', + id: `${fullyQualifiedDid}#key-agreement-1`, + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${fullyQualifiedDid}#key-1`], + assertionMethod: [`${fullyQualifiedDid}#key-1`], + keyAgreement: [`${fullyQualifiedDid}#key-agreement-1`], + service: [ + { + id: `${fullyQualifiedDid}#endpoint`, + type: 'endpoint', + serviceEndpoint: 'https://agent.com', + }, + { + id: `${fullyQualifiedDid}#did-communication`, + type: 'did-communication', + priority: 0, + recipientKeys: [`${fullyQualifiedDid}#key-agreement-1`], + routingKeys: ['routingKey1', 'routingKey2'], + accept: ['didcomm/aip2;env=rfc19'], + serviceEndpoint: 'https://agent.com', + }, + { + id: `${fullyQualifiedDid}#didcomm-1`, + type: 'DIDComm', + serviceEndpoint: 'https://agent.com', + accept: ['didcomm/v2'], + routingKeys: ['routingKey1', 'routingKey2'], + }, + ], + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + }) +}) From 1958381ee564377b826c3bafc80953385893c57d Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 26 Jan 2023 21:03:37 -0300 Subject: [PATCH 18/41] fix: did resolution result Signed-off-by: Ariel Gentile --- packages/core/src/index.ts | 1 - packages/indy-vdr/src/pool/IndyVdrPoolService.ts | 4 ++-- .../indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts | 10 ++++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 18187846a3..e2b1665a2a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -27,7 +27,6 @@ export type { WalletConfigRekey, WalletExportImportConfig, } from './types' -export * from './cache' export { DidCommMimeType, KeyDerivationMethod } from './types' export type { FileSystem } from './storage/FileSystem' export * from './storage/BaseRecord' diff --git a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts index 7e3e0981fe..0ac5d9a1aa 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts @@ -47,7 +47,7 @@ export class IndyVdrPoolService { } /** - * Get the most appropriatez pool for the given did. + * Get the most appropriate pool for the given did. * If the did is a qualified indy did, the pool will be determined based on the namespace. * If it is a legacy unqualified indy did, the pool will be determined based on the algorithm as described in this document: * https://docs.google.com/document/d/109C_eMsuZnTnYe2OAd02jAts1vC4axwEKIq7_4dnNVA/edit @@ -188,7 +188,7 @@ export class IndyVdrPoolService { this.logger.trace(`Retrieved did '${did}' from ledger '${pool.indyNamespace}'`, result) return { - did: result, + did: { nymResponse: { did: result.dest, verkey: result.verkey }, indyNamespace: pool.indyNamespace }, pool, response, } diff --git a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts index 6292bd2cd9..bbaf7db286 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts @@ -9,21 +9,20 @@ import { IndyWallet, KeyType, SigningProviderRegistry, - TypedArrayEncoder, } from '@aries-framework/core' -import { AttribRequest, NymRequest } from 'indy-vdr-test-shared' import { parseDid } from '../../core/src/modules/dids/domain/parse' import { agentDependencies, genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' import { IndyVdrSovDidResolver } from '../src/dids' import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' import { indyDidFromPublicKeyBase58 } from '../src/utils/did' + import { createDidOnLedger } from './helpers' const logger = new ConsoleLogger(LogLevel.trace) const indyVdrPoolService = new IndyVdrPoolService(logger) const wallet = new IndyWallet(agentDependencies, logger, new SigningProviderRegistry([])) -const agentConfig = getAgentConfig('IndyVdrPoolService', { logger }) +const agentConfig = getAgentConfig('IndyVdrResolver E2E', { logger }) const cache = new InMemoryLruCache({ limit: 200 }) const agentContext = getAgentContext({ @@ -49,7 +48,10 @@ describe('IndyVdrSov', () => { beforeAll(async () => { await indyVdrPoolService.connectToPools() - await wallet.createAndOpen(agentConfig.walletConfig!) + if (agentConfig.walletConfig) { + await wallet.createAndOpen(agentConfig.walletConfig) + } + signerKey = await wallet.createKey({ seed: '000000000000000000000000Trustee9', keyType: KeyType.Ed25519 }) }) From 7265465cc627cb00bc8944d3697f3675750b826a Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 26 Jan 2023 21:17:33 -0300 Subject: [PATCH 19/41] feat: initial changes for did:indy Signed-off-by: Ariel Gentile --- packages/core/package.json | 1 + .../__fixtures__/didExample123base.json | 11 + .../didExample123extracontent.json | 93 +++++++ .../src/modules/dids/domain/DidDocument.ts | 18 ++ .../dids/domain/__tests__/DidDocument.test.ts | 12 + .../indy-vdr/src/dids/IndyVdrDidRegistrar.ts | 256 ++++++++++++++++++ .../src/dids/IndyVdrIndyDidResolver.ts | 115 ++++++++ packages/indy-vdr/src/dids/didSovUtil.ts | 20 ++ yarn.lock | 14 +- 9 files changed, 538 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/modules/dids/__tests__/__fixtures__/didExample123base.json create mode 100644 packages/core/src/modules/dids/__tests__/__fixtures__/didExample123extracontent.json create mode 100644 packages/indy-vdr/src/dids/IndyVdrDidRegistrar.ts create mode 100644 packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts diff --git a/packages/core/package.json b/packages/core/package.json index 8a54af7192..659427c6be 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -40,6 +40,7 @@ "class-transformer": "0.5.1", "class-validator": "0.13.1", "did-resolver": "^3.1.3", + "lodash": "^4.17.21", "lru_map": "^0.4.1", "luxon": "^1.27.0", "make-error": "^1.3.6", diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123base.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123base.json new file mode 100644 index 0000000000..97126de120 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123base.json @@ -0,0 +1,11 @@ +{ + "id": "did:example:123", + "verificationMethod": [ + { + "id": "did:example:123#key-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC X..." + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123extracontent.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123extracontent.json new file mode 100644 index 0000000000..64b650cb0d --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123extracontent.json @@ -0,0 +1,93 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "alsoKnownAs": ["did:example:456"], + "controller": ["did:example:456"], + "verificationMethod": [ + { + "id": "did:example:123#key-2", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "-----BEGIN PUBLIC 9..." + }, + { + "id": "did:example:123#key-3", + "type": "Secp256k1VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyHex": "-----BEGIN PUBLIC A..." + } + ], + "service": [ + { + "id": "did:example:123#service-1", + "type": "Mediator", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" + }, + { + "id": "did:example:123#service-2", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "priority": 5 + }, + { + "id": "did:example:123#service-3", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 + } + ], + "authentication": [ + "did:example:123#key-1", + { + "id": "did:example:123#authentication-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "assertionMethod": [ + "did:example:123#key-1", + { + "id": "did:example:123#assertionMethod-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityDelegation": [ + "did:example:123#key-1", + { + "id": "did:example:123#capabilityDelegation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityInvocation": [ + "did:example:123#key-1", + { + "id": "did:example:123#capabilityInvocation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "keyAgreement": [ + "did:example:123#key-1", + { + "id": "did:example:123#keyAgreement-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + }, + { + "id": "did:example:123#keyAgreement-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ] +} diff --git a/packages/core/src/modules/dids/domain/DidDocument.ts b/packages/core/src/modules/dids/domain/DidDocument.ts index 5316933952..6d77454aa0 100644 --- a/packages/core/src/modules/dids/domain/DidDocument.ts +++ b/packages/core/src/modules/dids/domain/DidDocument.ts @@ -2,6 +2,7 @@ import type { DidDocumentService } from './service' import { Expose, Type } from 'class-transformer' import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator' +import { mergeWith } from 'lodash' import { KeyType, Key } from '../../../crypto' import { JsonTransformer } from '../../../utils/JsonTransformer' @@ -200,6 +201,23 @@ export class DidDocument { return recipientKeys } + /** + * Combine a JSON content with the contents of this one + * @param json object containing extra DIDDoc contents + * + * @returns a DidDocument object resulting from the combination of both + */ + public combine(json: Record) { + const didDocJson = this.toJSON() + const combinedJson = mergeWith(didDocJson, json, (objValue: unknown, srcValue: unknown) => { + if (Array.isArray(objValue) && Array.isArray(srcValue)) { + return [...new Set([...objValue, ...srcValue])] + } + }) + + return JsonTransformer.fromJSON(combinedJson, DidDocument) + } + public toJSON() { return JsonTransformer.toJSON(this) } diff --git a/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts b/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts index 09837a0d2e..a5137dd70e 100644 --- a/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts +++ b/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts @@ -1,6 +1,8 @@ import { ClassValidationError } from '../../../../error/ClassValidationError' import { JsonTransformer } from '../../../../utils/JsonTransformer' import didExample123Fixture from '../../__tests__/__fixtures__/didExample123.json' +import didExample123Base from '../../__tests__/__fixtures__/didExample123base.json' +import didExample123Extra from '../../__tests__/__fixtures__/didExample123extracontent.json' import didExample456Invalid from '../../__tests__/__fixtures__/didExample456Invalid.json' import { DidDocument, findVerificationMethodByKeyType } from '../DidDocument' import { DidDocumentService, IndyAgentService, DidCommV1Service } from '../service' @@ -182,6 +184,16 @@ describe('Did | DidDocument', () => { expect(didDocumentJson).toMatchObject(didExample123Fixture) }) + describe('combine', () => { + it('should correctly combine a base DIDDoc with extra contents from a JSON object', async () => { + const didDocument = JsonTransformer.fromJSON(didExample123Base, DidDocument) + + expect(didDocument.combine(didExample123Extra).toJSON()).toEqual(didExample123Fixture) + }) + }) + + + describe('getServicesByType', () => { it('returns all services with specified type', async () => { expect(didDocumentInstance.getServicesByType('IndyAgent')).toEqual( diff --git a/packages/indy-vdr/src/dids/IndyVdrDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrDidRegistrar.ts new file mode 100644 index 0000000000..140f731e05 --- /dev/null +++ b/packages/indy-vdr/src/dids/IndyVdrDidRegistrar.ts @@ -0,0 +1,256 @@ +import type { IndyEndpointAttrib } from './didSovUtil' +import type { IndyVdrPool } from '../pool' +import type { + AgentContext, + DidRegistrar, + DidCreateOptions, + DidCreateResult, + DidDeactivateResult, + DidUpdateResult, +} from '@aries-framework/core' + +import { Key, KeyType, injectable, DidDocumentRole, DidRecord, DidRepository } from '@aries-framework/core' +import { AttribRequest, NymRequest } from 'indy-vdr-test-shared' + +import { parseDid } from 'packages/core/src/modules/dids/domain/parse' +import { IndyVdrError } from '../error' +import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' + +import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovUtil' + +@injectable() +export class IndyVdrSovDidRegistrar implements DidRegistrar { + public readonly supportedMethods = ['indy'] + private didRepository: DidRepository + private indyVdrPoolService: IndyVdrPoolService + + public constructor(didRepository: DidRepository, indyVdrPoolService: IndyVdrPoolService) { + this.didRepository = didRepository + this.indyVdrPoolService = indyVdrPoolService + } + + public async create(agentContext: AgentContext, options: IndyVdrDidCreateOptions): Promise { + const { alias, role, submitterDid } = options.options + const seed = options.secret?.seed + + if (seed && (typeof seed !== 'string' || seed.length !== 32)) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Invalid seed provided', + }, + } + } + + const DID_REGEX = 'did:([a-z0-9]+):([a-z0-9]+):([a-z0-9]+)' + const [, method, namespace, unqualifiedSubmitterDid] = DID_REGEX.match(submitterDid) + if (!(method === 'indy')) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Submitter did must be a valid did:indy did', + }, + } + } + + try { + const key = await agentContext.wallet.createKey({ seed, keyType: KeyType.Ed25519 }) + + const unqualifiedIndyDid = key.publicKeyBase58 + const verkey = key.fingerprint + + // TODO: it should be possible to pass the pool used for writing to the indy ledger service. + // The easiest way to do this would be to make the submitterDid a fully qualified did, including the indy namespace. + await this.registerPublicDid(agentContext, unqualifiedSubmitterDid, unqualifiedIndyDid, verkey, alias, pool, role) + + // Create did document + const didDocumentBuilder = sovDidDocumentFromDid(qualifiedSovDid, verkey) + + // Add services if endpoints object was passed. + if (options.options.endpoints) { + await this.setEndpointsForDid(agentContext, unqualifiedIndyDid, options.options.endpoints, pool) + addServicesFromEndpointsAttrib( + didDocumentBuilder, + qualifiedSovDid, + options.options.endpoints, + `${qualifiedSovDid}#key-agreement-1` + ) + } + + // Build did document. + const didDocument = didDocumentBuilder.build() + + const didIndyNamespace = pool.config.indyNamespace + const qualifiedIndyDid = `did:indy:${didIndyNamespace}:${unqualifiedIndyDid}` + + // Save the did so we know we created it and can issue with it + const didRecord = new DidRecord({ + id: qualifiedSovDid, + did: qualifiedSovDid, + role: DidDocumentRole.Created, + tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key: Key) => key.fingerprint), + qualifiedIndyDid, + }, + }) + await this.didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: { + qualifiedIndyDid, + }, + didRegistrationMetadata: { + didIndyNamespace, + }, + didState: { + state: 'finished', + did: qualifiedSovDid, + didDocument, + secret: { + // FIXME: the uni-registrar creates the seed in the registrar method + // if it doesn't exist so the seed can always be returned. Currently + // we can only return it if the seed was passed in by the user. Once + // we have a secure method for generating seeds we should use the same + // approach + seed: options.secret?.seed, + }, + }, + } + } catch (error) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:sov not implemented yet`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:sov not implemented yet`, + }, + } + } + + public async registerPublicDid( + agentContext: AgentContext, + submitterDid: string, + targetDid: string, + verkey: string, + alias: string, + pool: IndyVdrPool, + role?: string + ) { + try { + agentContext.config.logger.debug(`Register public did '${targetDid}' on ledger '${pool}'`) + + await agentContext.wallet.createKey({ keyType: KeyType.Ed25519, seed: targetDid }) + + const request = new NymRequest({ submitterDid, dest: targetDid, verkey, alias }) + + const signingKey = Key.fromPublicKeyBase58(submitterDid, KeyType.Ed25519) + + const response = await pool.submitWriteRequest(agentContext, request, signingKey) + + agentContext.config.logger.debug(`Registered public did '${targetDid}' on ledger '${pool.indyNamespace}'`, { + response, + }) + + return targetDid + } catch (error) { + agentContext.config.logger.error( + `Error registering public did '${targetDid}' on ledger '${pool.indyNamespace}'`, + { + error, + submitterDid, + targetDid, + verkey, + alias, + role, + pool: pool.indyNamespace, + } + ) + + throw error + } + } + + public async setEndpointsForDid( + agentContext: AgentContext, + did: string, + endpoints: IndyEndpointAttrib, + pool: IndyVdrPool + ): Promise { + try { + agentContext.config.logger.debug(`Set endpoints for did '${did}' on ledger '${pool.indyNamespace}'`, endpoints) + + const request = new AttribRequest({ + submitterDid: did, + targetDid: did, + raw: JSON.stringify({ endpoint: endpoints }), + }) + + const signingKey = Key.fromPublicKeyBase58(did, KeyType.Ed25519) + const response = await pool.submitWriteRequest(agentContext, request, signingKey) + agentContext.config.logger.debug( + `Successfully set endpoints for did '${did}' on ledger '${pool.indyNamespace}'`, + { + response, + endpoints, + } + ) + } catch (error) { + agentContext.config.logger.error(`Error setting endpoints for did '${did}' on ledger '${pool.indyNamespace}'`, { + error, + did, + endpoints, + }) + + throw new IndyVdrError(error) + } + } +} + +export interface IndyVdrDidCreateOptions extends DidCreateOptions { + method: 'sov' + did?: undefined + // As did:sov is so limited, we require everything needed to construct the did document to be passed + // through the options object. Once we support did:indy we can allow the didDocument property. + didDocument?: never + options: { + alias: string + role?: string + endpoints?: IndyEndpointAttrib + indyNamespace?: string + submitterDid: string + } + secret?: { + seed?: string + } +} + +// Update and Deactivate not supported for did:sov +export type IndyVdrSovDidUpdateOptions = never +export type IndyVdrSovDidDeactivateOptions = never diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts new file mode 100644 index 0000000000..3c43ca64c2 --- /dev/null +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts @@ -0,0 +1,115 @@ +import type { GetNymResponseData, IndyEndpointAttrib } from './didSovUtil' +import type { DidResolutionResult, ParsedDid, DidResolver, AgentContext } from '@aries-framework/core' + +import { injectable } from '@aries-framework/core' +import { GetAttribRequest, GetNymRequest } from 'indy-vdr-test-shared' + +import { IndyVdrError, IndyVdrNotFoundError } from '../error' +import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' + +import { indyDidDocumentFromDid, addServicesFromEndpointsAttrib } from './didSovUtil' + +@injectable() +export class IndyVdrIndyDidResolver implements DidResolver { + private indyVdrPoolService: IndyVdrPoolService + + public constructor(indyVdrPoolService: IndyVdrPoolService) { + this.indyVdrPoolService = indyVdrPoolService + } + + public readonly supportedMethods = ['indy'] + + public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { + const didDocumentMetadata = {} + + try { + const nym = await this.getPublicDid(agentContext, did) + + // Get DID Document from Get NYM response or fallback re-build it if not present + const didDocument = await this.buildDidDocument(agentContext, nym, parsed) + + return { + didDocument, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } + + // did:indy uses indyDidDocumentFromDid while did:sov uses sovDidDocumentFromDid + private async buildDidDocument(agentContext: AgentContext, nym: GetNymResponseData, parsed: ParsedDid) { + if (!nym.diddocContent) { + const endpoints = await this.getEndpointsForDid(agentContext, parsed.id) + + const keyAgreementId = `${parsed.did}#key-agreement-1` + const builder = indyDidDocumentFromDid(parsed.did, nym.verkey) + addServicesFromEndpointsAttrib(builder, parsed.did, endpoints, keyAgreementId) + return builder.build() + } else { + // Create base Did Document + const builder = indyDidDocumentFromDid(parsed.did, nym.verkey) + + // Combine it with didDoc + return builder.build().combine(JSON.parse(nym.diddocContent)) + } + } + + private async getPublicDid(agentContext: AgentContext, did: string) { + const pool = await this.indyVdrPoolService.getPoolForDid(agentContext, did) + + const request = new GetNymRequest({ dest: did }) + + const didResponse = await pool.submitReadRequest(request) + + if (!didResponse.result.data) { + throw new IndyVdrNotFoundError(`DID ${did} not found`) + } + return JSON.parse(didResponse.result.data) as GetNymResponseData + } + + private async getEndpointsForDid(agentContext: AgentContext, did: string) { + const pool = await this.indyVdrPoolService.getPoolForDid(agentContext, did) + + try { + agentContext.config.logger.debug(`Get endpoints for did '${did}' from ledger '${pool.indyNamespace}'`) + + const request = new GetAttribRequest({ targetDid: did, raw: 'endpoint' }) + + agentContext.config.logger.debug( + `Submitting get endpoint ATTRIB request for did '${did}' to ledger '${pool.indyNamespace}'` + ) + const response = await pool.submitReadRequest(request) + + if (!response.result.data) return {} + + const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib + agentContext.config.logger.debug( + `Got endpoints '${JSON.stringify(endpoints)}' for did '${did}' from ledger '${pool.indyNamespace}'`, + { + response, + endpoints, + } + ) + + return endpoints ?? {} + } catch (error) { + agentContext.config.logger.error( + `Error retrieving endpoints for did '${did}' from ledger '${pool.indyNamespace}'`, + { + error, + } + ) + + throw new IndyVdrError(error) + } + } +} diff --git a/packages/indy-vdr/src/dids/didSovUtil.ts b/packages/indy-vdr/src/dids/didSovUtil.ts index 9fbe414b78..6b98f7f55d 100644 --- a/packages/indy-vdr/src/dids/didSovUtil.ts +++ b/packages/indy-vdr/src/dids/didSovUtil.ts @@ -18,6 +18,8 @@ export interface GetNymResponseData { did: string verkey: string role: string + alias?: string + diddocContent?: string } export const FULL_VERKEY_REGEX = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/ @@ -81,6 +83,24 @@ export function sovDidDocumentFromDid(fullDid: string, verkey: string) { return builder } +// Create a base DIDDoc template according to https://hyperledger.github.io/indy-did-method/#base-diddoc-template +export function indyDidDocumentFromDid(fullDid: string, verkey: string) { + const verificationMethodId = `${fullDid}#key-1` + + const publicKeyBase58 = getFullVerkey(fullDid, verkey) + + const builder = new DidDocumentBuilder(fullDid) + .addVerificationMethod({ + controller: fullDid, + id: verificationMethodId, + publicKeyBase58: publicKeyBase58, + type: 'Ed25519VerificationKey2018', + }) + .addAuthentication(verificationMethodId) + + return builder +} + // Process Indy Attrib Endpoint Types according to: https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html > Read (Resolve) > DID Service Endpoint function processEndpointTypes(types?: string[]) { const expectedTypes = ['endpoint', 'did-communication', 'DIDComm'] diff --git a/yarn.lock b/yarn.lock index 30954778a4..8f282380e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7574,7 +7574,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.7.0: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -9921,7 +9921,7 @@ rimraf@^2.5.4, rimraf@^2.6.3, rimraf@^2.7.1: dependencies: glob "^7.1.3" -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.0, rimraf@^3.0.2, rimraf@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -11035,6 +11035,16 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" From d415b4063577b94e4edf590706c227288c4a45a8 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 26 Jan 2023 22:19:53 -0300 Subject: [PATCH 20/41] remove unused file Signed-off-by: Ariel Gentile --- packages/indy-vdr/src/pool/DidIdentifier.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/indy-vdr/src/pool/DidIdentifier.ts diff --git a/packages/indy-vdr/src/pool/DidIdentifier.ts b/packages/indy-vdr/src/pool/DidIdentifier.ts deleted file mode 100644 index 39384a2c8d..0000000000 --- a/packages/indy-vdr/src/pool/DidIdentifier.ts +++ /dev/null @@ -1 +0,0 @@ -export const DID_INDY_REGEX = /^did:indy:((?:[a-z][_a-z0-9-]*)(?::[a-z][_a-z0-9-]*)):([1-9A-HJ-NP-Za-km-z]{21,22})$/ \ No newline at end of file From ec6230ab2969f2ede27610d49925702a1bd00102 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 26 Jan 2023 22:23:15 -0300 Subject: [PATCH 21/41] export types Signed-off-by: Ariel Gentile --- packages/indy-vdr/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/indy-vdr/src/index.ts b/packages/indy-vdr/src/index.ts index 8a5ca6c21a..d25da592c9 100644 --- a/packages/indy-vdr/src/index.ts +++ b/packages/indy-vdr/src/index.ts @@ -1,3 +1,7 @@ +export * from './dids' +export * from './error' +export * from './pool' + try { // eslint-disable-next-line import/no-extraneous-dependencies require('indy-vdr-test-nodejs') From db0963ea4748a4808e26f19e0be2a622ebcf824c Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 26 Jan 2023 23:29:39 -0300 Subject: [PATCH 22/41] test: fix pool mock and increase timeout Signed-off-by: Ariel Gentile --- .../src/dids/__tests__/IndyVdrSovDidResolver.test.ts | 4 ++-- packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts | 5 ++--- packages/indy-vdr/tests/setup.ts | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts index dc811ea2ba..d7deeb2c23 100644 --- a/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts @@ -3,7 +3,7 @@ import { JsonTransformer } from '@aries-framework/core' import didSovR1xKJw17sUoXhejEpugMYJFixture from '../../../../core/src/modules/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json' import didSovWJz9mHyW9BZksioQnRsrAoFixture from '../../../../core/src/modules/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json' import { parseDid } from '../../../../core/src/modules/dids/domain/parse' -import { getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { getAgentConfig, getAgentContext, mockFunction, mockProperty } from '../../../../core/tests/helpers' import { IndyVdrPool } from '../../pool' import { IndyVdrPoolService } from '../../pool/IndyVdrPoolService' import { IndyVdrSovDidResolver } from '../IndyVdrSovDidResolver' @@ -15,7 +15,7 @@ const poolServiceMock = new IndyVdrPoolServiceMock() jest.mock('../../pool/IndyVdrPool') const IndyVdrPoolMock = IndyVdrPool as jest.Mock const poolMock = new IndyVdrPoolMock() - +mockProperty(poolMock, 'indyNamespace', 'local') mockFunction(poolServiceMock.getPoolForDid).mockResolvedValue(poolMock) const agentConfig = getAgentConfig('IndyVdrSovDidResolver') diff --git a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts index bbaf7db286..e80867cdd2 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts @@ -2,9 +2,7 @@ import type { Key } from '@aries-framework/core' import { CacheModuleConfig, - ConsoleLogger, InMemoryLruCache, - LogLevel, JsonTransformer, IndyWallet, KeyType, @@ -13,13 +11,14 @@ import { import { parseDid } from '../../core/src/modules/dids/domain/parse' import { agentDependencies, genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' import { IndyVdrSovDidResolver } from '../src/dids' import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' import { indyDidFromPublicKeyBase58 } from '../src/utils/did' import { createDidOnLedger } from './helpers' -const logger = new ConsoleLogger(LogLevel.trace) +const logger = testLogger const indyVdrPoolService = new IndyVdrPoolService(logger) const wallet = new IndyWallet(agentDependencies, logger, new SigningProviderRegistry([])) const agentConfig = getAgentConfig('IndyVdrResolver E2E', { logger }) diff --git a/packages/indy-vdr/tests/setup.ts b/packages/indy-vdr/tests/setup.ts index ce7749d25e..2c9ea26d5b 100644 --- a/packages/indy-vdr/tests/setup.ts +++ b/packages/indy-vdr/tests/setup.ts @@ -1,4 +1,4 @@ // Needed to register indy-vdr node bindings import '../src/index' -jest.setTimeout(20000) +jest.setTimeout(30000) From f41e01de51281286f67d09fde8b22dc7beebc6ea Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Fri, 27 Jan 2023 08:13:33 -0300 Subject: [PATCH 23/41] test: increase timeout Signed-off-by: Ariel Gentile --- packages/indy-vdr/tests/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/indy-vdr/tests/setup.ts b/packages/indy-vdr/tests/setup.ts index 2c9ea26d5b..d69181fd10 100644 --- a/packages/indy-vdr/tests/setup.ts +++ b/packages/indy-vdr/tests/setup.ts @@ -1,4 +1,4 @@ // Needed to register indy-vdr node bindings import '../src/index' -jest.setTimeout(30000) +jest.setTimeout(60000) From 474b7050b7edde1eda749f9f235adbebb1d754db Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Fri, 27 Jan 2023 14:55:05 -0300 Subject: [PATCH 24/41] fix: resolve PR feedback Signed-off-by: Ariel Gentile --- .../src/dids/IndyVdrSovDidResolver.ts | 16 +++--- .../__tests__/IndyVdrSovDidResolver.test.ts | 14 ++--- .../didSovR1xKJw17sUoXhejEpugMYJ.json | 51 +++++++++++++++++++ .../didSovWJz9mHyW9BZksioQnRsrAo.json | 49 ++++++++++++++++++ packages/indy-vdr/src/index.ts | 4 +- packages/indy-vdr/src/pool/index.ts | 1 + .../tests/indy-vdr-did-resolver.e2e.test.ts | 19 ++++--- 7 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json create mode 100644 packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts index f9f424a1e9..69ef868982 100644 --- a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts @@ -5,18 +5,12 @@ import { injectable } from '@aries-framework/core' import { GetAttribRequest, GetNymRequest } from 'indy-vdr-test-shared' import { IndyVdrError, IndyVdrNotFoundError } from '../error' -import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' +import { IndyVdrPoolService } from '../pool' import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovUtil' @injectable() export class IndyVdrSovDidResolver implements DidResolver { - private indyVdrPoolService: IndyVdrPoolService - - public constructor(indyVdrPoolService: IndyVdrPoolService) { - this.indyVdrPoolService = indyVdrPoolService - } - public readonly supportedMethods = ['sov'] public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { @@ -48,7 +42,9 @@ export class IndyVdrSovDidResolver implements DidResolver { } private async getPublicDid(agentContext: AgentContext, did: string) { - const pool = await this.indyVdrPoolService.getPoolForDid(agentContext, did) + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const pool = await indyVdrPoolService.getPoolForDid(agentContext, did) const request = new GetNymRequest({ dest: did }) @@ -61,7 +57,9 @@ export class IndyVdrSovDidResolver implements DidResolver { } private async getEndpointsForDid(agentContext: AgentContext, did: string) { - const pool = await this.indyVdrPoolService.getPoolForDid(agentContext, did) + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const pool = await indyVdrPoolService.getPoolForDid(agentContext, did) try { agentContext.config.logger.debug(`Get endpoints for did '${did}' from ledger '${pool.indyNamespace}'`) diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts index d7deeb2c23..269aaa1a46 100644 --- a/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts @@ -1,13 +1,13 @@ import { JsonTransformer } from '@aries-framework/core' -import didSovR1xKJw17sUoXhejEpugMYJFixture from '../../../../core/src/modules/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json' -import didSovWJz9mHyW9BZksioQnRsrAoFixture from '../../../../core/src/modules/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json' import { parseDid } from '../../../../core/src/modules/dids/domain/parse' -import { getAgentConfig, getAgentContext, mockFunction, mockProperty } from '../../../../core/tests/helpers' -import { IndyVdrPool } from '../../pool' -import { IndyVdrPoolService } from '../../pool/IndyVdrPoolService' +import { getAgentConfig, getAgentContext, mockProperty } from '../../../../core/tests/helpers' +import { IndyVdrPool, IndyVdrPoolService } from '../../pool' import { IndyVdrSovDidResolver } from '../IndyVdrSovDidResolver' +import didSovR1xKJw17sUoXhejEpugMYJFixture from './__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json' +import didSovWJz9mHyW9BZksioQnRsrAoFixture from './__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json' + jest.mock('../../pool/IndyVdrPoolService') const IndyVdrPoolServiceMock = IndyVdrPoolService as jest.Mock const poolServiceMock = new IndyVdrPoolServiceMock() @@ -16,7 +16,7 @@ jest.mock('../../pool/IndyVdrPool') const IndyVdrPoolMock = IndyVdrPool as jest.Mock const poolMock = new IndyVdrPoolMock() mockProperty(poolMock, 'indyNamespace', 'local') -mockFunction(poolServiceMock.getPoolForDid).mockResolvedValue(poolMock) +jest.spyOn(poolServiceMock, 'getPoolForDid').mockResolvedValue(poolMock) const agentConfig = getAgentConfig('IndyVdrSovDidResolver') @@ -25,7 +25,7 @@ const agentContext = getAgentContext({ registerInstances: [[IndyVdrPoolService, poolServiceMock]], }) -const resolver = new IndyVdrSovDidResolver(poolServiceMock) +const resolver = new IndyVdrSovDidResolver() describe('DidResolver', () => { describe('IndyVdrSovDidResolver', () => { diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json new file mode 100644 index 0000000000..6a6e4ed706 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json @@ -0,0 +1,51 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:R1xKJw17sUoXhejEpugMYJ", + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#key-1", + "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" + }, + { + "type": "X25519KeyAgreementKey2019", + "controller": "did:sov:R1xKJw17sUoXhejEpugMYJ", + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1", + "publicKeyBase58": "Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt" + } + ], + "authentication": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-1"], + "assertionMethod": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-1"], + "keyAgreement": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], + "service": [ + { + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://ssi.com" + }, + { + "accept": ["didcomm/aip2;env=rfc19"], + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#did-communication", + "priority": 0, + "recipientKeys": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], + "routingKeys": [], + "serviceEndpoint": "https://ssi.com", + "type": "did-communication" + }, + { + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#profile", + "serviceEndpoint": "https://profile.com", + "type": "profile" + }, + { + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#hub", + "serviceEndpoint": "https://hub.com", + "type": "hub" + } + ] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json new file mode 100644 index 0000000000..7b74e0587f --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json @@ -0,0 +1,49 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1", + "https://didcomm.org/messaging/contexts/v2" + ], + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:WJz9mHyW9BZksioQnRsrAo", + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#key-1", + "publicKeyBase58": "GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8" + }, + { + "type": "X25519KeyAgreementKey2019", + "controller": "did:sov:WJz9mHyW9BZksioQnRsrAo", + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1", + "publicKeyBase58": "S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud" + } + ], + "authentication": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-1"], + "assertionMethod": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-1"], + "keyAgreement": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "service": [ + { + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#did-communication", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "routingKeys": ["routingKey1", "routingKey2"], + "accept": ["didcomm/aip2;env=rfc19"], + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#didcomm-1", + "type": "DIDComm", + "serviceEndpoint": "https://agent.com", + "accept": ["didcomm/v2"], + "routingKeys": ["routingKey1", "routingKey2"] + } + ] +} diff --git a/packages/indy-vdr/src/index.ts b/packages/indy-vdr/src/index.ts index d25da592c9..ca0fe42285 100644 --- a/packages/indy-vdr/src/index.ts +++ b/packages/indy-vdr/src/index.ts @@ -1,6 +1,4 @@ -export * from './dids' -export * from './error' -export * from './pool' +export { IndyVdrSovDidResolver } from './dids' try { // eslint-disable-next-line import/no-extraneous-dependencies diff --git a/packages/indy-vdr/src/pool/index.ts b/packages/indy-vdr/src/pool/index.ts index 1e1f1b52f8..ec4bc06677 100644 --- a/packages/indy-vdr/src/pool/index.ts +++ b/packages/indy-vdr/src/pool/index.ts @@ -1 +1,2 @@ export * from './IndyVdrPool' +export * from './IndyVdrPoolService' diff --git a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts index e80867cdd2..72a09afc83 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts @@ -19,18 +19,11 @@ import { indyDidFromPublicKeyBase58 } from '../src/utils/did' import { createDidOnLedger } from './helpers' const logger = testLogger -const indyVdrPoolService = new IndyVdrPoolService(logger) const wallet = new IndyWallet(agentDependencies, logger, new SigningProviderRegistry([])) const agentConfig = getAgentConfig('IndyVdrResolver E2E', { logger }) const cache = new InMemoryLruCache({ limit: 200 }) -const agentContext = getAgentContext({ - wallet, - agentConfig, - registerInstances: [[CacheModuleConfig, new CacheModuleConfig({ cache })]], -}) - -const indyVdrSovDidResolver = new IndyVdrSovDidResolver(indyVdrPoolService) +const indyVdrSovDidResolver = new IndyVdrSovDidResolver() const config = { isProduction: false, @@ -41,6 +34,16 @@ const config = { let signerKey: Key +const agentContext = getAgentContext({ + wallet, + agentConfig, + registerInstances: [ + [IndyVdrPoolService, new IndyVdrPoolService(logger)], + [CacheModuleConfig, new CacheModuleConfig({ cache })], + ], +}) + +const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) indyVdrPoolService.setPools([config]) describe('IndyVdrSov', () => { From 9d10bd4b700108788a547abed956e55ec6046dd5 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Fri, 27 Jan 2023 18:43:25 -0300 Subject: [PATCH 25/41] remove injectable decorator Signed-off-by: Ariel Gentile --- packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts index 69ef868982..bb51d4aeaa 100644 --- a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts @@ -1,7 +1,6 @@ import type { GetNymResponseData, IndyEndpointAttrib } from './didSovUtil' import type { DidResolutionResult, ParsedDid, DidResolver, AgentContext } from '@aries-framework/core' -import { injectable } from '@aries-framework/core' import { GetAttribRequest, GetNymRequest } from 'indy-vdr-test-shared' import { IndyVdrError, IndyVdrNotFoundError } from '../error' @@ -9,7 +8,6 @@ import { IndyVdrPoolService } from '../pool' import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovUtil' -@injectable() export class IndyVdrSovDidResolver implements DidResolver { public readonly supportedMethods = ['sov'] From 5ed0d92e6093491858a37201b8a1b5a039892c39 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sat, 28 Jan 2023 15:10:27 -0300 Subject: [PATCH 26/41] feat: add initial classes for did:indy resolver and registrar Signed-off-by: Ariel Gentile --- ...egistrar.ts => IndyVdrIndyDidRegistrar.ts} | 116 +++++++++------- .../src/dids/IndyVdrIndyDidResolver.ts | 92 ++++++++----- .../__tests__/IndyVdrIndyDidResolver.test.ts | 127 ++++++++++++++++++ packages/indy-vdr/src/dids/didIndyUtil.ts | 30 +++++ packages/indy-vdr/src/dids/didSovUtil.ts | 22 +-- 5 files changed, 282 insertions(+), 105 deletions(-) rename packages/indy-vdr/src/dids/{IndyVdrDidRegistrar.ts => IndyVdrIndyDidRegistrar.ts} (67%) create mode 100644 packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts create mode 100644 packages/indy-vdr/src/dids/didIndyUtil.ts diff --git a/packages/indy-vdr/src/dids/IndyVdrDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts similarity index 67% rename from packages/indy-vdr/src/dids/IndyVdrDidRegistrar.ts rename to packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts index 140f731e05..a134ac20aa 100644 --- a/packages/indy-vdr/src/dids/IndyVdrDidRegistrar.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -1,4 +1,4 @@ -import type { IndyEndpointAttrib } from './didSovUtil' +import type { CommEndpointType, IndyEndpointAttrib } from './didSovUtil' import type { IndyVdrPool } from '../pool' import type { AgentContext, @@ -7,27 +7,29 @@ import type { DidCreateResult, DidDeactivateResult, DidUpdateResult, + DidDocument, } from '@aries-framework/core' -import { Key, KeyType, injectable, DidDocumentRole, DidRecord, DidRepository } from '@aries-framework/core' +import { + TypedArrayEncoder, + Key, + KeyType, + injectable, + DidDocumentRole, + DidRecord, + DidRepository, +} from '@aries-framework/core' import { AttribRequest, NymRequest } from 'indy-vdr-test-shared' -import { parseDid } from 'packages/core/src/modules/dids/domain/parse' import { IndyVdrError } from '../error' import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' +import { DID_INDY_REGEX } from '../utils/did' -import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovUtil' +import { createKeyAgreementKey, indyDidDocumentFromDid } from './didIndyUtil' +import { addServicesFromEndpointsAttrib } from './didSovUtil' -@injectable() export class IndyVdrSovDidRegistrar implements DidRegistrar { public readonly supportedMethods = ['indy'] - private didRepository: DidRepository - private indyVdrPoolService: IndyVdrPoolService - - public constructor(didRepository: DidRepository, indyVdrPoolService: IndyVdrPoolService) { - this.didRepository = didRepository - this.indyVdrPoolService = indyVdrPoolService - } public async create(agentContext: AgentContext, options: IndyVdrDidCreateOptions): Promise { const { alias, role, submitterDid } = options.options @@ -44,9 +46,8 @@ export class IndyVdrSovDidRegistrar implements DidRegistrar { } } - const DID_REGEX = 'did:([a-z0-9]+):([a-z0-9]+):([a-z0-9]+)' - const [, method, namespace, unqualifiedSubmitterDid] = DID_REGEX.match(submitterDid) - if (!(method === 'indy')) { + const match = submitterDid.match(DID_INDY_REGEX) + if (!match) { return { didDocumentMetadata: {}, didRegistrationMetadata: {}, @@ -57,58 +58,76 @@ export class IndyVdrSovDidRegistrar implements DidRegistrar { } } + const [, , namespace, submitterId] = match + try { const key = await agentContext.wallet.createKey({ seed, keyType: KeyType.Ed25519 }) - const unqualifiedIndyDid = key.publicKeyBase58 - const verkey = key.fingerprint + const buffer = key.publicKey + const id = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + + const did = `did:indy:${namespace}:${id}` + const verkey = key.publicKeyBase58 - // TODO: it should be possible to pass the pool used for writing to the indy ledger service. - // The easiest way to do this would be to make the submitterDid a fully qualified did, including the indy namespace. - await this.registerPublicDid(agentContext, unqualifiedSubmitterDid, unqualifiedIndyDid, verkey, alias, pool, role) + const pool = agentContext.dependencyManager.resolve(IndyVdrPoolService).getPoolForNamespace(namespace) + await this.registerPublicDid(agentContext, submitterId, id, verkey, pool, alias, role) // Create did document - const didDocumentBuilder = sovDidDocumentFromDid(qualifiedSovDid, verkey) + const didDocumentBuilder = indyDidDocumentFromDid(did, verkey) // Add services if endpoints object was passed. if (options.options.endpoints) { - await this.setEndpointsForDid(agentContext, unqualifiedIndyDid, options.options.endpoints, pool) - addServicesFromEndpointsAttrib( - didDocumentBuilder, - qualifiedSovDid, - options.options.endpoints, - `${qualifiedSovDid}#key-agreement-1` - ) + // For legacy indy-nodes not supporting diddocContent + if (options.options.useEndpointAttrib) { + await this.setEndpointsForDid(agentContext, id, options.options.endpoints, pool) + + // TODO: Move this logic to didIndyUtil, as it is common to Resolver and Registrar for legacy networks + // If there is at least a didcomm endpoint, generate and a key agreement key + const keyAgreementId = `${did}#key-agreement-1` + const commTypes: CommEndpointType[] = ['endpoint', 'did-communication', 'DIDComm'] + if (commTypes.some((type) => options.options.endpoints?.types?.includes(type))) { + didDocumentBuilder + .addVerificationMethod({ + controller: did, + id: keyAgreementId, + publicKeyBase58: createKeyAgreementKey(did, verkey), + type: 'X25519KeyAgreementKey2019', + }) + .addKeyAgreement(keyAgreementId) + } + addServicesFromEndpointsAttrib(didDocumentBuilder, did, options.options.endpoints, keyAgreementId) + } else { + // TODO: create diddocContent parameter + } } - // Build did document. + // Build did document const didDocument = didDocumentBuilder.build() - const didIndyNamespace = pool.config.indyNamespace - const qualifiedIndyDid = `did:indy:${didIndyNamespace}:${unqualifiedIndyDid}` - // Save the did so we know we created it and can issue with it const didRecord = new DidRecord({ - id: qualifiedSovDid, - did: qualifiedSovDid, + id: did, + did, role: DidDocumentRole.Created, tags: { recipientKeyFingerprints: didDocument.recipientKeys.map((key: Key) => key.fingerprint), - qualifiedIndyDid, + did, }, }) - await this.didRepository.save(agentContext, didRecord) + + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + await didRepository.save(agentContext, didRecord) return { didDocumentMetadata: { - qualifiedIndyDid, + did, }, didRegistrationMetadata: { - didIndyNamespace, + namespace, }, didState: { state: 'finished', - did: qualifiedSovDid, + did, didDocument, secret: { // FIXME: the uni-registrar creates the seed in the registrar method @@ -159,15 +178,13 @@ export class IndyVdrSovDidRegistrar implements DidRegistrar { submitterDid: string, targetDid: string, verkey: string, - alias: string, pool: IndyVdrPool, + alias?: string, role?: string ) { try { agentContext.config.logger.debug(`Register public did '${targetDid}' on ledger '${pool}'`) - await agentContext.wallet.createKey({ keyType: KeyType.Ed25519, seed: targetDid }) - const request = new NymRequest({ submitterDid, dest: targetDid, verkey, alias }) const signingKey = Key.fromPublicKeyBase58(submitterDid, KeyType.Ed25519) @@ -234,15 +251,14 @@ export class IndyVdrSovDidRegistrar implements DidRegistrar { } export interface IndyVdrDidCreateOptions extends DidCreateOptions { - method: 'sov' + method: 'indy' did?: undefined - // As did:sov is so limited, we require everything needed to construct the did document to be passed - // through the options object. Once we support did:indy we can allow the didDocument property. - didDocument?: never + didDocument?: DidDocument options: { - alias: string + alias?: string role?: string endpoints?: IndyEndpointAttrib + useEndpointAttrib?: boolean indyNamespace?: string submitterDid: string } @@ -251,6 +267,6 @@ export interface IndyVdrDidCreateOptions extends DidCreateOptions { } } -// Update and Deactivate not supported for did:sov -export type IndyVdrSovDidUpdateOptions = never -export type IndyVdrSovDidDeactivateOptions = never +// TODO: Add Update and Deactivate +export type IndyVdrIndyDidUpdateOptions = never +export type IndyVdrIndyDidDeactivateOptions = never diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts index 3c43ca64c2..5e481ef0e1 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts @@ -1,37 +1,38 @@ -import type { GetNymResponseData, IndyEndpointAttrib } from './didSovUtil' -import type { DidResolutionResult, ParsedDid, DidResolver, AgentContext } from '@aries-framework/core' +import type { CommEndpointType, GetNymResponseData, IndyEndpointAttrib } from './didSovUtil' +import type { DidResolutionResult, DidResolver, AgentContext } from '@aries-framework/core' -import { injectable } from '@aries-framework/core' import { GetAttribRequest, GetNymRequest } from 'indy-vdr-test-shared' import { IndyVdrError, IndyVdrNotFoundError } from '../error' import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' +import { DID_INDY_REGEX } from '../utils/did' -import { indyDidDocumentFromDid, addServicesFromEndpointsAttrib } from './didSovUtil' +import { createKeyAgreementKey, indyDidDocumentFromDid } from './didIndyUtil' +import { addServicesFromEndpointsAttrib } from './didSovUtil' -@injectable() export class IndyVdrIndyDidResolver implements DidResolver { - private indyVdrPoolService: IndyVdrPoolService - - public constructor(indyVdrPoolService: IndyVdrPoolService) { - this.indyVdrPoolService = indyVdrPoolService - } - public readonly supportedMethods = ['indy'] - public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { + public async resolve(agentContext: AgentContext, did: string): Promise { const didDocumentMetadata = {} - try { - const nym = await this.getPublicDid(agentContext, did) + const match = did.match(DID_INDY_REGEX) - // Get DID Document from Get NYM response or fallback re-build it if not present - const didDocument = await this.buildDidDocument(agentContext, nym, parsed) + if (match) { + const [, namespace, id] = match - return { - didDocument, - didDocumentMetadata, - didResolutionMetadata: { contentType: 'application/did+ld+json' }, + const nym = await this.getPublicDid(agentContext, namespace, id) + + // Get DID Document from Get NYM response + const didDocument = await this.buildDidDocument(agentContext, nym, did) + + return { + didDocument, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } else { + throw new IndyVdrError(`${did} is not a did:indy DID`) } } catch (error) { return { @@ -45,39 +46,58 @@ export class IndyVdrIndyDidResolver implements DidResolver { } } - // did:indy uses indyDidDocumentFromDid while did:sov uses sovDidDocumentFromDid - private async buildDidDocument(agentContext: AgentContext, nym: GetNymResponseData, parsed: ParsedDid) { - if (!nym.diddocContent) { - const endpoints = await this.getEndpointsForDid(agentContext, parsed.id) + private async buildDidDocument(agentContext: AgentContext, getNymResponseData: GetNymResponseData, did: string) { + // Create base Did Document + // We assume that verkey from GET_NYM is always a full verkey in base58 + const builder = indyDidDocumentFromDid(did, getNymResponseData.verkey) + + // If GET_NYM does not return any diddocContent, fallback to legacy GET_ATTRIB endpoint + if (!getNymResponseData.diddocContent) { + const keyAgreementId = `${did}#key-agreement-1` + + const endpoints = await this.getEndpointsForDid(agentContext, did) + + // If there is at least a didcomm endpoint, generate and a key agreement key + const commTypes: CommEndpointType[] = ['endpoint', 'did-communication', 'DIDComm'] + if (commTypes.some((type) => endpoints.types?.includes(type))) { + builder + .addVerificationMethod({ + controller: did, + id: keyAgreementId, + publicKeyBase58: createKeyAgreementKey(did, getNymResponseData.verkey), + type: 'X25519KeyAgreementKey2019', + }) + .addKeyAgreement(keyAgreementId) + } - const keyAgreementId = `${parsed.did}#key-agreement-1` - const builder = indyDidDocumentFromDid(parsed.did, nym.verkey) - addServicesFromEndpointsAttrib(builder, parsed.did, endpoints, keyAgreementId) + // Process endpoint attrib following the same rules as for did:sov + addServicesFromEndpointsAttrib(builder, did, endpoints, keyAgreementId) return builder.build() } else { - // Create base Did Document - const builder = indyDidDocumentFromDid(parsed.did, nym.verkey) - // Combine it with didDoc - return builder.build().combine(JSON.parse(nym.diddocContent)) + return builder.build().combine(JSON.parse(getNymResponseData.diddocContent)) } } - private async getPublicDid(agentContext: AgentContext, did: string) { - const pool = await this.indyVdrPoolService.getPoolForDid(agentContext, did) + private async getPublicDid(agentContext: AgentContext, namespace: string, id: string) { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const pool = indyVdrPoolService.getPoolForNamespace(namespace) - const request = new GetNymRequest({ dest: did }) + const request = new GetNymRequest({ dest: id }) const didResponse = await pool.submitReadRequest(request) if (!didResponse.result.data) { - throw new IndyVdrNotFoundError(`DID ${did} not found`) + throw new IndyVdrNotFoundError(`DID ${id} not found in indy namespace ${namespace}`) } return JSON.parse(didResponse.result.data) as GetNymResponseData } private async getEndpointsForDid(agentContext: AgentContext, did: string) { - const pool = await this.indyVdrPoolService.getPoolForDid(agentContext, did) + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const pool = await indyVdrPoolService.getPoolForDid(agentContext, did) try { agentContext.config.logger.debug(`Get endpoints for did '${did}' from ledger '${pool.indyNamespace}'`) diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts new file mode 100644 index 0000000000..8723a2ad61 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts @@ -0,0 +1,127 @@ +import { JsonTransformer } from '@aries-framework/core' +import { parseDid } from '@aries-framework/core/src/modules/dids/domain/parse' +import { getAgentConfig, getAgentContext, mockProperty } from '@aries-framework/core/tests/helpers' + +import { IndyVdrPool, IndyVdrPoolService } from '../../pool' +import { IndyVdrSovDidResolver } from '../IndyVdrSovDidResolver' + +import didSovR1xKJw17sUoXhejEpugMYJFixture from './__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json' +import didSovWJz9mHyW9BZksioQnRsrAoFixture from './__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json' + +jest.mock('../../pool/IndyVdrPoolService') +const IndyVdrPoolServiceMock = IndyVdrPoolService as jest.Mock +const poolServiceMock = new IndyVdrPoolServiceMock() + +jest.mock('../../pool/IndyVdrPool') +const IndyVdrPoolMock = IndyVdrPool as jest.Mock +const poolMock = new IndyVdrPoolMock() +mockProperty(poolMock, 'indyNamespace', 'local') +jest.spyOn(poolServiceMock, 'getPoolForDid').mockResolvedValue(poolMock) + +const agentConfig = getAgentConfig('IndyVdrIndyDidResolver') + +const agentContext = getAgentContext({ + agentConfig, + registerInstances: [[IndyVdrPoolService, poolServiceMock]], +}) + +const resolver = new IndyVdrSovDidResolver() + +describe('IndyVdrIndyDidResolver', () => { + describe('NYMs without diddocContent', () => { + it('should correctly resolve a did:indy document', async () => { + const did = 'did:indy:indyNamespace:R1xKJw17sUoXhejEpugMYJ' + + const nymResponse = { + result: { + data: JSON.stringify({ + did: 'R1xKJw17sUoXhejEpugMYJ', + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + role: 'ENDORSER', + }), + }, + } + + const attribResponse = { + result: { + data: JSON.stringify({ + endpoint: { + endpoint: 'https://ssi.com', + profile: 'https://profile.com', + hub: 'https://hub.com', + }, + }), + }, + } + + jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(nymResponse) + jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(attribResponse) + + const result = await resolver.resolve(agentContext, did, parseDid(did)) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didSovR1xKJw17sUoXhejEpugMYJFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should resolve a did:indy document with routingKeys and types entries in the attrib', async () => { + const did = 'did:indy:indyNamespace:WJz9mHyW9BZksioQnRsrAo' + + const nymResponse = { + result: { + data: JSON.stringify({ + did: 'WJz9mHyW9BZksioQnRsrAo', + verkey: 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8', + role: 'ENDORSER', + }), + }, + } + + const attribResponse = { + result: { + data: JSON.stringify({ + endpoint: { + endpoint: 'https://agent.com', + types: ['endpoint', 'did-communication', 'DIDComm'], + routingKeys: ['routingKey1', 'routingKey2'], + }, + }), + }, + } + + jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(nymResponse) + jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(attribResponse) + + const result = await resolver.resolve(agentContext, did, parseDid(did)) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didSovWJz9mHyW9BZksioQnRsrAoFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should return did resolution metadata with error if the indy ledger service throws an error', async () => { + const did = 'did:indy:indyNamespace:R1xKJw17sUoXhejEpugMYJ' + + jest.spyOn(poolMock, 'submitReadRequest').mockRejectedValue(new Error('Error submitting read request')) + + const result = await resolver.resolve(agentContext, did, parseDid(did)) + + expect(result).toMatchObject({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did 'did:indy:R1xKJw17sUoXhejEpugMYJ': Error: Error submitting read request`, + }, + }) + }) + }) +}) diff --git a/packages/indy-vdr/src/dids/didIndyUtil.ts b/packages/indy-vdr/src/dids/didIndyUtil.ts new file mode 100644 index 0000000000..df0eb6ef2b --- /dev/null +++ b/packages/indy-vdr/src/dids/didIndyUtil.ts @@ -0,0 +1,30 @@ +import { convertPublicKeyToX25519, DidDocumentBuilder, TypedArrayEncoder } from '@aries-framework/core' + +import { getFullVerkey } from './didSovUtil' + +// Create a base DIDDoc template according to https://hyperledger.github.io/indy-did-method/#base-diddoc-template +export function indyDidDocumentFromDid(did: string, verKeyBase58: string) { + const verificationMethodId = `${did}#key-1` + + const publicKeyBase58 = verKeyBase58 + + const builder = new DidDocumentBuilder(did) + .addVerificationMethod({ + controller: did, + id: verificationMethodId, + publicKeyBase58, + type: 'Ed25519VerificationKey2018', + }) + .addAuthentication(verificationMethodId) + + return builder +} + +export function createKeyAgreementKey(fullDid: string, verkey: string) { + const publicKeyBase58 = getFullVerkey(fullDid, verkey) + const publicKeyX25519 = TypedArrayEncoder.toBase58( + convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(publicKeyBase58)) + ) + + return publicKeyX25519 +} diff --git a/packages/indy-vdr/src/dids/didSovUtil.ts b/packages/indy-vdr/src/dids/didSovUtil.ts index 6b98f7f55d..9d53f8b89d 100644 --- a/packages/indy-vdr/src/dids/didSovUtil.ts +++ b/packages/indy-vdr/src/dids/didSovUtil.ts @@ -7,9 +7,11 @@ import { convertPublicKeyToX25519, } from '@aries-framework/core' +export type CommEndpointType = 'endpoint' | 'did-communication' | 'DIDComm' + export interface IndyEndpointAttrib { endpoint?: string - types?: Array<'endpoint' | 'did-communication' | 'DIDComm'> + types?: Array routingKeys?: string[] [key: string]: unknown } @@ -83,24 +85,6 @@ export function sovDidDocumentFromDid(fullDid: string, verkey: string) { return builder } -// Create a base DIDDoc template according to https://hyperledger.github.io/indy-did-method/#base-diddoc-template -export function indyDidDocumentFromDid(fullDid: string, verkey: string) { - const verificationMethodId = `${fullDid}#key-1` - - const publicKeyBase58 = getFullVerkey(fullDid, verkey) - - const builder = new DidDocumentBuilder(fullDid) - .addVerificationMethod({ - controller: fullDid, - id: verificationMethodId, - publicKeyBase58: publicKeyBase58, - type: 'Ed25519VerificationKey2018', - }) - .addAuthentication(verificationMethodId) - - return builder -} - // Process Indy Attrib Endpoint Types according to: https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html > Read (Resolve) > DID Service Endpoint function processEndpointTypes(types?: string[]) { const expectedTypes = ['endpoint', 'did-communication', 'DIDComm'] From 3551c0fd831530187c3f7c2cd4226779108740be Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 1 Feb 2023 17:58:35 -0300 Subject: [PATCH 27/41] add did:indy resolver tests and fixes Signed-off-by: Ariel Gentile --- .../src/dids/IndyVdrIndyDidRegistrar.ts | 10 +- .../src/dids/IndyVdrIndyDidResolver.ts | 81 +++++++------ .../src/dids/IndyVdrSovDidResolver.ts | 14 ++- .../__tests__/IndyVdrIndyDidResolver.test.ts | 81 ++++++++----- .../didIndyLjgpST2rjsoxYegQDRm7EL.json | 109 ++++++++++++++++++ ...dyLjgpST2rjsoxYegQDRm7ELdiddocContent.json | 101 ++++++++++++++++ .../didIndyR1xKJw17sUoXhejEpugMYJ.json | 13 +++ .../didIndyWJz9mHyW9BZksioQnRsrAo.json | 46 ++++++++ packages/indy-vdr/src/dids/didIndyUtil.ts | 19 ++- packages/indy-vdr/src/dids/didSovUtil.ts | 3 +- packages/indy-vdr/src/utils/did.ts | 3 +- 11 files changed, 397 insertions(+), 83 deletions(-) create mode 100644 packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json create mode 100644 packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json create mode 100644 packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json create mode 100644 packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts index a134ac20aa..bcd5714d8b 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -10,15 +10,7 @@ import type { DidDocument, } from '@aries-framework/core' -import { - TypedArrayEncoder, - Key, - KeyType, - injectable, - DidDocumentRole, - DidRecord, - DidRepository, -} from '@aries-framework/core' +import { TypedArrayEncoder, Key, KeyType, DidDocumentRole, DidRecord, DidRepository } from '@aries-framework/core' import { AttribRequest, NymRequest } from 'indy-vdr-test-shared' import { IndyVdrError } from '../error' diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts index 5e481ef0e1..e36b8d2ecb 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts @@ -4,10 +4,9 @@ import type { DidResolutionResult, DidResolver, AgentContext } from '@aries-fram import { GetAttribRequest, GetNymRequest } from 'indy-vdr-test-shared' import { IndyVdrError, IndyVdrNotFoundError } from '../error' -import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' -import { DID_INDY_REGEX } from '../utils/did' +import { IndyVdrPoolService } from '../pool' -import { createKeyAgreementKey, indyDidDocumentFromDid } from './didIndyUtil' +import { createKeyAgreementKey, indyDidDocumentFromDid, parseIndyDid } from './didIndyUtil' import { addServicesFromEndpointsAttrib } from './didSovUtil' export class IndyVdrIndyDidResolver implements DidResolver { @@ -16,23 +15,15 @@ export class IndyVdrIndyDidResolver implements DidResolver { public async resolve(agentContext: AgentContext, did: string): Promise { const didDocumentMetadata = {} try { - const match = did.match(DID_INDY_REGEX) + const nym = await this.getPublicDid(agentContext, did) - if (match) { - const [, namespace, id] = match + // Get DID Document from Get NYM response + const didDocument = await this.buildDidDocument(agentContext, nym, did) - const nym = await this.getPublicDid(agentContext, namespace, id) - - // Get DID Document from Get NYM response - const didDocument = await this.buildDidDocument(agentContext, nym, did) - - return { - didDocument, - didDocumentMetadata, - didResolutionMetadata: { contentType: 'application/did+ld+json' }, - } - } else { - throw new IndyVdrError(`${did} is not a did:indy DID`) + return { + didDocument, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, } } catch (error) { return { @@ -57,31 +48,35 @@ export class IndyVdrIndyDidResolver implements DidResolver { const endpoints = await this.getEndpointsForDid(agentContext, did) - // If there is at least a didcomm endpoint, generate and a key agreement key - const commTypes: CommEndpointType[] = ['endpoint', 'did-communication', 'DIDComm'] - if (commTypes.some((type) => endpoints.types?.includes(type))) { - builder - .addVerificationMethod({ - controller: did, - id: keyAgreementId, - publicKeyBase58: createKeyAgreementKey(did, getNymResponseData.verkey), - type: 'X25519KeyAgreementKey2019', - }) - .addKeyAgreement(keyAgreementId) - } + if (endpoints) { + // If there is at least a didcomm endpoint, generate and a key agreement key + const commTypes: CommEndpointType[] = ['endpoint', 'did-communication', 'DIDComm'] + if (commTypes.some((type) => endpoints.types?.includes(type))) { + builder + .addVerificationMethod({ + controller: did, + id: keyAgreementId, + publicKeyBase58: createKeyAgreementKey(did, getNymResponseData.verkey), + type: 'X25519KeyAgreementKey2019', + }) + .addKeyAgreement(keyAgreementId) + } - // Process endpoint attrib following the same rules as for did:sov - addServicesFromEndpointsAttrib(builder, did, endpoints, keyAgreementId) + // Process endpoint attrib following the same rules as for did:sov + addServicesFromEndpointsAttrib(builder, did, endpoints, keyAgreementId) + } return builder.build() } else { - // Combine it with didDoc - return builder.build().combine(JSON.parse(getNymResponseData.diddocContent)) + // Combine it with didDoc (TODO: Check if diddocContent is returned as a JSON object or a string) + return builder.build().combine(getNymResponseData.diddocContent) } } - private async getPublicDid(agentContext: AgentContext, namespace: string, id: string) { + private async getPublicDid(agentContext: AgentContext, did: string) { const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + const { namespace, id } = parseIndyDid(did) + const pool = indyVdrPoolService.getPoolForNamespace(namespace) const request = new GetNymRequest({ dest: id }) @@ -97,19 +92,23 @@ export class IndyVdrIndyDidResolver implements DidResolver { private async getEndpointsForDid(agentContext: AgentContext, did: string) { const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) - const pool = await indyVdrPoolService.getPoolForDid(agentContext, did) + const { namespace, id } = parseIndyDid(did) + + const pool = indyVdrPoolService.getPoolForNamespace(namespace) try { - agentContext.config.logger.debug(`Get endpoints for did '${did}' from ledger '${pool.indyNamespace}'`) + agentContext.config.logger.debug(`Get endpoints for did '${id}' from ledger '${pool.indyNamespace}'`) - const request = new GetAttribRequest({ targetDid: did, raw: 'endpoint' }) + const request = new GetAttribRequest({ targetDid: id, raw: 'endpoint' }) agentContext.config.logger.debug( - `Submitting get endpoint ATTRIB request for did '${did}' to ledger '${pool.indyNamespace}'` + `Submitting get endpoint ATTRIB request for did '${id}' to ledger '${pool.indyNamespace}'` ) const response = await pool.submitReadRequest(request) - if (!response.result.data) return {} + if (!response.result.data) { + return + } const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib agentContext.config.logger.debug( @@ -120,7 +119,7 @@ export class IndyVdrIndyDidResolver implements DidResolver { } ) - return endpoints ?? {} + return endpoints } catch (error) { agentContext.config.logger.error( `Error retrieving endpoints for did '${did}' from ledger '${pool.indyNamespace}'`, diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts index bb51d4aeaa..8ba092201d 100644 --- a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts @@ -16,11 +16,15 @@ export class IndyVdrSovDidResolver implements DidResolver { try { const nym = await this.getPublicDid(agentContext, parsed.id) + const builder = sovDidDocumentFromDid(parsed.did, nym.verkey) + const endpoints = await this.getEndpointsForDid(agentContext, parsed.id) - const keyAgreementId = `${parsed.did}#key-agreement-1` - const builder = sovDidDocumentFromDid(parsed.did, nym.verkey) - addServicesFromEndpointsAttrib(builder, parsed.did, endpoints, keyAgreementId) + if (endpoints) { + const keyAgreementId = `${parsed.did}#key-agreement-1` + + addServicesFromEndpointsAttrib(builder, parsed.did, endpoints, keyAgreementId) + } return { didDocument: builder.build(), @@ -69,7 +73,9 @@ export class IndyVdrSovDidResolver implements DidResolver { ) const response = await pool.submitReadRequest(request) - if (!response.result.data) return {} + if (!response.result.data) { + return + } const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib agentContext.config.logger.debug( diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts index 8723a2ad61..f3cbb7608a 100644 --- a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts @@ -1,12 +1,13 @@ import { JsonTransformer } from '@aries-framework/core' -import { parseDid } from '@aries-framework/core/src/modules/dids/domain/parse' -import { getAgentConfig, getAgentContext, mockProperty } from '@aries-framework/core/tests/helpers' +import { getAgentConfig, getAgentContext, mockProperty } from '../../../../core/tests/helpers' import { IndyVdrPool, IndyVdrPoolService } from '../../pool' -import { IndyVdrSovDidResolver } from '../IndyVdrSovDidResolver' +import { IndyVdrIndyDidResolver } from '../IndyVdrIndyDidResolver' -import didSovR1xKJw17sUoXhejEpugMYJFixture from './__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json' -import didSovWJz9mHyW9BZksioQnRsrAoFixture from './__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json' +import didIndyLjgpST2rjsoxYegQDRm7EL from './__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json' +import didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent from './__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json' +import didIndyR1xKJw17sUoXhejEpugMYJFixture from './__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json' +import didIndyWJz9mHyW9BZksioQnRsrAoFixture from './__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json' jest.mock('../../pool/IndyVdrPoolService') const IndyVdrPoolServiceMock = IndyVdrPoolService as jest.Mock @@ -15,8 +16,8 @@ const poolServiceMock = new IndyVdrPoolServiceMock() jest.mock('../../pool/IndyVdrPool') const IndyVdrPoolMock = IndyVdrPool as jest.Mock const poolMock = new IndyVdrPoolMock() -mockProperty(poolMock, 'indyNamespace', 'local') -jest.spyOn(poolServiceMock, 'getPoolForDid').mockResolvedValue(poolMock) +mockProperty(poolMock, 'indyNamespace', 'ns1') +jest.spyOn(poolServiceMock, 'getPoolForNamespace').mockReturnValue(poolMock) const agentConfig = getAgentConfig('IndyVdrIndyDidResolver') @@ -25,12 +26,43 @@ const agentContext = getAgentContext({ registerInstances: [[IndyVdrPoolService, poolServiceMock]], }) -const resolver = new IndyVdrSovDidResolver() +const resolver = new IndyVdrIndyDidResolver() describe('IndyVdrIndyDidResolver', () => { + describe('NYMs with diddocContent', () => { + it('should correctly resolve a did:indy document with arbitrary diddocContent', async () => { + const did = 'did:indy:ns2:LjgpST2rjsoxYegQDRm7EL' + + const nymResponse = { + result: { + data: JSON.stringify({ + did: 'LjgpST2rjsoxYegQDRm7EL', + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + role: 'ENDORSER', + diddocContent: didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent, + }), + }, + } + + const poolMockSubmitReadRequest = jest.spyOn(poolMock, 'submitReadRequest') + poolMockSubmitReadRequest.mockResolvedValueOnce(nymResponse) + + const result = await resolver.resolve(agentContext, did) + + expect(poolMockSubmitReadRequest).toHaveBeenCalledTimes(1) + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didIndyLjgpST2rjsoxYegQDRm7EL, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + }) + describe('NYMs without diddocContent', () => { - it('should correctly resolve a did:indy document', async () => { - const did = 'did:indy:indyNamespace:R1xKJw17sUoXhejEpugMYJ' + it('should correctly resolve a did:indy document without endpoint attrib', async () => { + const did = 'did:indy:ns1:R1xKJw17sUoXhejEpugMYJ' const nymResponse = { result: { @@ -44,23 +76,17 @@ describe('IndyVdrIndyDidResolver', () => { const attribResponse = { result: { - data: JSON.stringify({ - endpoint: { - endpoint: 'https://ssi.com', - profile: 'https://profile.com', - hub: 'https://hub.com', - }, - }), + data: null, }, } jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(nymResponse) jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(attribResponse) - const result = await resolver.resolve(agentContext, did, parseDid(did)) + const result = await resolver.resolve(agentContext, did) expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocument: didSovR1xKJw17sUoXhejEpugMYJFixture, + didDocument: didIndyR1xKJw17sUoXhejEpugMYJFixture, didDocumentMetadata: {}, didResolutionMetadata: { contentType: 'application/did+ld+json', @@ -68,8 +94,8 @@ describe('IndyVdrIndyDidResolver', () => { }) }) - it('should resolve a did:indy document with routingKeys and types entries in the attrib', async () => { - const did = 'did:indy:indyNamespace:WJz9mHyW9BZksioQnRsrAo' + it('should correctly resolve a did:indy document with endpoint attrib', async () => { + const did = 'did:indy:ns1:WJz9mHyW9BZksioQnRsrAo' const nymResponse = { result: { @@ -96,10 +122,13 @@ describe('IndyVdrIndyDidResolver', () => { jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(nymResponse) jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(attribResponse) - const result = await resolver.resolve(agentContext, did, parseDid(did)) + const result = await resolver.resolve(agentContext, did) + + // TODO: Check DIDDocument context, as currently we are forcing it to have at least https://w3id.org/did/v1, + // while did:indy method spec does not enforce any context expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocument: didSovWJz9mHyW9BZksioQnRsrAoFixture, + didDocument: didIndyWJz9mHyW9BZksioQnRsrAoFixture, didDocumentMetadata: {}, didResolutionMetadata: { contentType: 'application/did+ld+json', @@ -108,18 +137,18 @@ describe('IndyVdrIndyDidResolver', () => { }) it('should return did resolution metadata with error if the indy ledger service throws an error', async () => { - const did = 'did:indy:indyNamespace:R1xKJw17sUoXhejEpugMYJ' + const did = 'did:indy:ns1:R1xKJw17sUoXhejEpugMYJ' jest.spyOn(poolMock, 'submitReadRequest').mockRejectedValue(new Error('Error submitting read request')) - const result = await resolver.resolve(agentContext, did, parseDid(did)) + const result = await resolver.resolve(agentContext, did) expect(result).toMatchObject({ didDocument: null, didDocumentMetadata: {}, didResolutionMetadata: { error: 'notFound', - message: `resolver_error: Unable to resolve did 'did:indy:R1xKJw17sUoXhejEpugMYJ': Error: Error submitting read request`, + message: `resolver_error: Unable to resolve did 'did:indy:ns1:R1xKJw17sUoXhejEpugMYJ': Error: Error submitting read request`, }, }) }) diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json new file mode 100644 index 0000000000..1218fe2ee0 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json @@ -0,0 +1,109 @@ +{ + "@context": ["https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" +], + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL", + "alsoKnownAs": ["did:indy:ns2:R1xKJw17sUoXhejEpugMYJ"], + "controller": ["did:indy:ns2:R1xKJw17sUoXhejEpugMYJ"], + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL", + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-2", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC X..." + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-3", + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyBase58": "-----BEGIN PUBLIC 9..." + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-4", + "type": "Secp256k1VerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyHex": "-----BEGIN PUBLIC A..." + } + ], + "service": [ + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#service-1", + "type": "Mediator", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#service-2", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "priority": 5 + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#service-3", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 + } + ], + "authentication": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#authentication-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "assertionMethod": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#assertionMethod-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityDelegation": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#capabilityDelegation-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityInvocation": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#capabilityInvocation-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "keyAgreement": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#keyAgreement-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#keyAgreement-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json new file mode 100644 index 0000000000..857ffe1c84 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json @@ -0,0 +1,101 @@ +{ + "@context": [ + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" +], + "alsoKnownAs": ["did:indy:ns2:R1xKJw17sUoXhejEpugMYJ"], + "controller": ["did:indy:ns2:R1xKJw17sUoXhejEpugMYJ"], + "verificationMethod": [ + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-2", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC X..." + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-3", + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyBase58": "-----BEGIN PUBLIC 9..." + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-4", + "type": "Secp256k1VerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyHex": "-----BEGIN PUBLIC A..." + } + ], + "service": [ + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#service-1", + "type": "Mediator", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#service-2", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "priority": 5 + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#service-3", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 + } + ], + "authentication": [ + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#authentication-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "assertionMethod": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#assertionMethod-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityDelegation": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#capabilityDelegation-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityInvocation": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#capabilityInvocation-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "keyAgreement": [ + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#keyAgreement-1", + "type": "RsaVerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + }, + { + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#keyAgreement-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json new file mode 100644 index 0000000000..815bc02a94 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json @@ -0,0 +1,13 @@ +{ + "@context": [ "https://w3id.org/did/v1" ], + "id": "did:indy:ns1:R1xKJw17sUoXhejEpugMYJ", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns1:R1xKJw17sUoXhejEpugMYJ", + "id": "did:indy:ns1:R1xKJw17sUoXhejEpugMYJ#key-1", + "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" + } + ], + "authentication": ["did:indy:ns1:R1xKJw17sUoXhejEpugMYJ#key-1"] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json new file mode 100644 index 0000000000..829e40b26b --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json @@ -0,0 +1,46 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://didcomm.org/messaging/contexts/v2" + ], + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo", + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#key-1", + "publicKeyBase58": "GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8" + }, + { + "type": "X25519KeyAgreementKey2019", + "controller": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo", + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1", + "publicKeyBase58": "S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud" + } + ], + "authentication": ["did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#key-1"], + "keyAgreement": ["did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "service": [ + { + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#did-communication", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "routingKeys": ["routingKey1", "routingKey2"], + "accept": ["didcomm/aip2;env=rfc19"], + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#didcomm-1", + "type": "DIDComm", + "serviceEndpoint": "https://agent.com", + "accept": ["didcomm/v2"], + "routingKeys": ["routingKey1", "routingKey2"] + } + ] +} diff --git a/packages/indy-vdr/src/dids/didIndyUtil.ts b/packages/indy-vdr/src/dids/didIndyUtil.ts index df0eb6ef2b..55f50c783c 100644 --- a/packages/indy-vdr/src/dids/didIndyUtil.ts +++ b/packages/indy-vdr/src/dids/didIndyUtil.ts @@ -1,4 +1,11 @@ -import { convertPublicKeyToX25519, DidDocumentBuilder, TypedArrayEncoder } from '@aries-framework/core' +import { + AriesFrameworkError, + convertPublicKeyToX25519, + DidDocumentBuilder, + TypedArrayEncoder, +} from '@aries-framework/core' + +import { DID_INDY_REGEX } from '../utils/did' import { getFullVerkey } from './didSovUtil' @@ -28,3 +35,13 @@ export function createKeyAgreementKey(fullDid: string, verkey: string) { return publicKeyX25519 } + +export function parseIndyDid(did: string) { + const match = did.match(DID_INDY_REGEX) + if (match) { + const [, namespace, id] = match + return { namespace, id } + } else { + throw new AriesFrameworkError(`${did} is not a valid did:indy did`) + } +} diff --git a/packages/indy-vdr/src/dids/didSovUtil.ts b/packages/indy-vdr/src/dids/didSovUtil.ts index 9d53f8b89d..37aa5201e1 100644 --- a/packages/indy-vdr/src/dids/didSovUtil.ts +++ b/packages/indy-vdr/src/dids/didSovUtil.ts @@ -21,7 +21,7 @@ export interface GetNymResponseData { verkey: string role: string alias?: string - diddocContent?: string + diddocContent?: Record } export const FULL_VERKEY_REGEX = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/ @@ -142,6 +142,7 @@ export function addServicesFromEndpointsAttrib( ) // If 'DIDComm' included in types, add DIDComm v2 entry + // TODO: should it be DIDComm or DIDCommMessaging? (see https://github.com/sovrin-foundation/sovrin/issues/343) if (processedTypes.includes('DIDComm')) { builder .addService( diff --git a/packages/indy-vdr/src/utils/did.ts b/packages/indy-vdr/src/utils/did.ts index 9cda8ee95d..2ef5c155e9 100644 --- a/packages/indy-vdr/src/utils/did.ts +++ b/packages/indy-vdr/src/utils/did.ts @@ -17,7 +17,8 @@ import { TypedArrayEncoder } from '@aries-framework/core' -export const DID_INDY_REGEX = /^did:indy:((?:[a-z][_a-z0-9-]*)(?::[a-z][_a-z0-9-]*)):([1-9A-HJ-NP-Za-km-z]{21,22})$/ + +export const DID_INDY_REGEX = /^did:indy:((?:[a-z][_a-z0-9-]*)(?::[a-z][_a-z0-9-]*)?):([1-9A-HJ-NP-Za-km-z]{21,22})$/ export const ABBREVIATED_VERKEY_REGEX = /^~[1-9A-HJ-NP-Za-km-z]{21,22}$/ /** From b22b2b52de6e2b330c1a5ed55b4a697eb5cac7d0 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 1 Feb 2023 23:34:50 -0300 Subject: [PATCH 28/41] add/move tests for utility methods and initial registrar for did:indy Signed-off-by: Ariel Gentile --- packages/core/package.json | 1 - .../src/modules/dids/domain/DidDocument.ts | 18 -- .../dids/domain/__tests__/DidDocument.test.ts | 12 -- packages/indy-vdr/package.json | 3 +- .../src/dids/IndyVdrIndyDidRegistrar.ts | 157 +++++++++++------- .../src/dids/IndyVdrIndyDidResolver.ts | 6 +- .../__tests__/__fixtures__/didExample123.json | 100 +++++++++++ .../__fixtures__/didExample123base.json | 5 +- .../didExample123extracontent.json | 9 +- .../didIndyLjgpST2rjsoxYegQDRm7EL.json | 15 +- ...dyLjgpST2rjsoxYegQDRm7ELdiddocContent.json | 5 +- .../didIndyR1xKJw17sUoXhejEpugMYJ.json | 6 +- .../didIndyWJz9mHyW9BZksioQnRsrAo.json | 9 +- .../src/dids/__tests__/didIndyUtil.test.ts | 23 +++ .../src/dids/__tests__/didSovUtil.test.ts | 5 + packages/indy-vdr/src/dids/didIndyUtil.ts | 49 ++++-- packages/indy-vdr/src/dids/didSovUtil.ts | 30 ++++ packages/indy-vdr/src/utils/did.ts | 1 - yarn.lock | 4 +- 19 files changed, 327 insertions(+), 131 deletions(-) create mode 100644 packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123.json rename packages/{core/src/modules => indy-vdr/src}/dids/__tests__/__fixtures__/didExample123base.json (70%) rename packages/{core/src/modules => indy-vdr/src}/dids/__tests__/__fixtures__/didExample123extracontent.json (94%) create mode 100644 packages/indy-vdr/src/dids/__tests__/didIndyUtil.test.ts create mode 100644 packages/indy-vdr/src/dids/__tests__/didSovUtil.test.ts diff --git a/packages/core/package.json b/packages/core/package.json index 659427c6be..8a54af7192 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -40,7 +40,6 @@ "class-transformer": "0.5.1", "class-validator": "0.13.1", "did-resolver": "^3.1.3", - "lodash": "^4.17.21", "lru_map": "^0.4.1", "luxon": "^1.27.0", "make-error": "^1.3.6", diff --git a/packages/core/src/modules/dids/domain/DidDocument.ts b/packages/core/src/modules/dids/domain/DidDocument.ts index 6d77454aa0..5316933952 100644 --- a/packages/core/src/modules/dids/domain/DidDocument.ts +++ b/packages/core/src/modules/dids/domain/DidDocument.ts @@ -2,7 +2,6 @@ import type { DidDocumentService } from './service' import { Expose, Type } from 'class-transformer' import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator' -import { mergeWith } from 'lodash' import { KeyType, Key } from '../../../crypto' import { JsonTransformer } from '../../../utils/JsonTransformer' @@ -201,23 +200,6 @@ export class DidDocument { return recipientKeys } - /** - * Combine a JSON content with the contents of this one - * @param json object containing extra DIDDoc contents - * - * @returns a DidDocument object resulting from the combination of both - */ - public combine(json: Record) { - const didDocJson = this.toJSON() - const combinedJson = mergeWith(didDocJson, json, (objValue: unknown, srcValue: unknown) => { - if (Array.isArray(objValue) && Array.isArray(srcValue)) { - return [...new Set([...objValue, ...srcValue])] - } - }) - - return JsonTransformer.fromJSON(combinedJson, DidDocument) - } - public toJSON() { return JsonTransformer.toJSON(this) } diff --git a/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts b/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts index a5137dd70e..09837a0d2e 100644 --- a/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts +++ b/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts @@ -1,8 +1,6 @@ import { ClassValidationError } from '../../../../error/ClassValidationError' import { JsonTransformer } from '../../../../utils/JsonTransformer' import didExample123Fixture from '../../__tests__/__fixtures__/didExample123.json' -import didExample123Base from '../../__tests__/__fixtures__/didExample123base.json' -import didExample123Extra from '../../__tests__/__fixtures__/didExample123extracontent.json' import didExample456Invalid from '../../__tests__/__fixtures__/didExample456Invalid.json' import { DidDocument, findVerificationMethodByKeyType } from '../DidDocument' import { DidDocumentService, IndyAgentService, DidCommV1Service } from '../service' @@ -184,16 +182,6 @@ describe('Did | DidDocument', () => { expect(didDocumentJson).toMatchObject(didExample123Fixture) }) - describe('combine', () => { - it('should correctly combine a base DIDDoc with extra contents from a JSON object', async () => { - const didDocument = JsonTransformer.fromJSON(didExample123Base, DidDocument) - - expect(didDocument.combine(didExample123Extra).toJSON()).toEqual(didExample123Fixture) - }) - }) - - - describe('getServicesByType', () => { it('returns all services with specified type', async () => { expect(didDocumentInstance.getServicesByType('IndyAgent')).toEqual( diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json index 32c8689d5d..6f9031d809 100644 --- a/packages/indy-vdr/package.json +++ b/packages/indy-vdr/package.json @@ -26,7 +26,8 @@ }, "dependencies": { "@aries-framework/core": "0.3.3", - "indy-vdr-test-shared": "^0.1.3" + "indy-vdr-test-shared": "^0.1.3", + "lodash": "^4.17.21" }, "devDependencies": { "indy-vdr-test-nodejs": "^0.1.3", diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts index bcd5714d8b..cef1c907a7 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -8,25 +8,31 @@ import type { DidDeactivateResult, DidUpdateResult, DidDocument, + DidDocumentService, } from '@aries-framework/core' -import { TypedArrayEncoder, Key, KeyType, DidDocumentRole, DidRecord, DidRepository } from '@aries-framework/core' +import { + Hasher, + TypedArrayEncoder, + Key, + KeyType, + DidDocumentRole, + DidRecord, + DidRepository, +} from '@aries-framework/core' import { AttribRequest, NymRequest } from 'indy-vdr-test-shared' import { IndyVdrError } from '../error' import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' -import { DID_INDY_REGEX } from '../utils/did' -import { createKeyAgreementKey, indyDidDocumentFromDid } from './didIndyUtil' -import { addServicesFromEndpointsAttrib } from './didSovUtil' +import { createKeyAgreementKey, deepObjectDiff, indyDidDocumentFromDid, parseIndyDid } from './didIndyUtil' +import { endpointsAttribFromServices } from './didSovUtil' -export class IndyVdrSovDidRegistrar implements DidRegistrar { +export class IndyVdrIndyDidRegistrar implements DidRegistrar { public readonly supportedMethods = ['indy'] public async create(agentContext: AgentContext, options: IndyVdrDidCreateOptions): Promise { - const { alias, role, submitterDid } = options.options const seed = options.secret?.seed - if (seed && (typeof seed !== 'string' || seed.length !== 32)) { return { didDocumentMetadata: {}, @@ -38,64 +44,97 @@ export class IndyVdrSovDidRegistrar implements DidRegistrar { } } - const match = submitterDid.match(DID_INDY_REGEX) - if (!match) { + const { alias, role, submitterDid, services, useEndpointAttrib } = options.options + let verkey = options.options.verkey + let did = options.did + let id + + if (seed && did) { return { didDocumentMetadata: {}, didRegistrationMetadata: {}, didState: { state: 'failed', - reason: 'Submitter did must be a valid did:indy did', + reason: `Only one of 'seed' and 'did' must be provided`, }, } } - const [, , namespace, submitterId] = match - try { - const key = await agentContext.wallet.createKey({ seed, keyType: KeyType.Ed25519 }) - - const buffer = key.publicKey - const id = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) - - const did = `did:indy:${namespace}:${id}` - const verkey = key.publicKeyBase58 - - const pool = agentContext.dependencyManager.resolve(IndyVdrPoolService).getPoolForNamespace(namespace) - await this.registerPublicDid(agentContext, submitterId, id, verkey, pool, alias, role) + const { namespace, id: submitterId } = parseIndyDid(submitterDid) + + if (did) { + id = parseIndyDid(did).id + if (!verkey) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'If a did is defined, a matching verkey must be provided', + }, + } + } + // TODO: Validate that specified did matches to verkey + } else { + // Create a new key and calculate did according to the rules for indy did method + const key = await agentContext.wallet.createKey({ seed, keyType: KeyType.Ed25519 }) + const buffer = Hasher.hash(key.publicKey, 'sha2-256') + + id = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + verkey = key.publicKeyBase58 + did = `did:indy:${namespace}:${id}` + } - // Create did document + // Create base did document const didDocumentBuilder = indyDidDocumentFromDid(did, verkey) + let diddocContent = {} + + // Add services if object was passed + if (services) { + services.forEach(didDocumentBuilder.addService) + + const commTypes: CommEndpointType[] = ['endpoint', 'did-communication', 'DIDComm'] + const serviceTypes = new Set(services.map((item) => item.type)) + + const keyAgreementId = `${did}#key-agreement-1` + + // If there is at least a communication service, add the key agreement key + if (commTypes.some((type) => serviceTypes.has(type))) { + didDocumentBuilder + .addVerificationMethod({ + controller: did, + id: keyAgreementId, + publicKeyBase58: createKeyAgreementKey(verkey), + type: 'X25519KeyAgreementKey2019', + }) + .addKeyAgreement(keyAgreementId) + } - // Add services if endpoints object was passed. - if (options.options.endpoints) { - // For legacy indy-nodes not supporting diddocContent - if (options.options.useEndpointAttrib) { - await this.setEndpointsForDid(agentContext, id, options.options.endpoints, pool) - - // TODO: Move this logic to didIndyUtil, as it is common to Resolver and Registrar for legacy networks - // If there is at least a didcomm endpoint, generate and a key agreement key - const keyAgreementId = `${did}#key-agreement-1` - const commTypes: CommEndpointType[] = ['endpoint', 'did-communication', 'DIDComm'] - if (commTypes.some((type) => options.options.endpoints?.types?.includes(type))) { - didDocumentBuilder - .addVerificationMethod({ - controller: did, - id: keyAgreementId, - publicKeyBase58: createKeyAgreementKey(did, verkey), - type: 'X25519KeyAgreementKey2019', - }) - .addKeyAgreement(keyAgreementId) - } - addServicesFromEndpointsAttrib(didDocumentBuilder, did, options.options.endpoints, keyAgreementId) - } else { - // TODO: create diddocContent parameter + if (!useEndpointAttrib) { + // create diddocContent parameter based on the diff between the base and the resulting DID Document + diddocContent = deepObjectDiff( + didDocumentBuilder.build().toJSON(), + indyDidDocumentFromDid(did, verkey).build().toJSON() + ) } } // Build did document const didDocument = didDocumentBuilder.build() + // If there are services and we are using legacy indy endpoint attrib, make sure they are suitable before registering the DID + if (services && useEndpointAttrib) { + endpointsAttribFromServices(services) + } + + const pool = agentContext.dependencyManager.resolve(IndyVdrPoolService).getPoolForNamespace(namespace) + await this.registerPublicDid(agentContext, pool, submitterId, id, verkey, alias, role, diddocContent) + + if (services && useEndpointAttrib) { + await this.setEndpointsForDid(agentContext, pool, id, endpointsAttribFromServices(services)) + } + // Save the did so we know we created it and can issue with it const didRecord = new DidRecord({ id: did, @@ -149,7 +188,7 @@ export class IndyVdrSovDidRegistrar implements DidRegistrar { didRegistrationMetadata: {}, didState: { state: 'failed', - reason: `notImplemented: updating did:sov not implemented yet`, + reason: `notImplemented: updating did:indy not implemented yet`, }, } } @@ -160,23 +199,29 @@ export class IndyVdrSovDidRegistrar implements DidRegistrar { didRegistrationMetadata: {}, didState: { state: 'failed', - reason: `notImplemented: deactivating did:sov not implemented yet`, + reason: `notImplemented: deactivating did:indy not implemented yet`, }, } } public async registerPublicDid( agentContext: AgentContext, + pool: IndyVdrPool, submitterDid: string, targetDid: string, verkey: string, - pool: IndyVdrPool, alias?: string, - role?: string + role?: string, + diddocContent?: Record ) { try { agentContext.config.logger.debug(`Register public did '${targetDid}' on ledger '${pool}'`) + // FIXME: Add diddocContent when supported by indy-vdr + if (diddocContent) { + throw new IndyVdrError('diddocContent is not yet supported') + } + const request = new NymRequest({ submitterDid, dest: targetDid, verkey, alias }) const signingKey = Key.fromPublicKeyBase58(submitterDid, KeyType.Ed25519) @@ -208,9 +253,9 @@ export class IndyVdrSovDidRegistrar implements DidRegistrar { public async setEndpointsForDid( agentContext: AgentContext, + pool: IndyVdrPool, did: string, - endpoints: IndyEndpointAttrib, - pool: IndyVdrPool + endpoints: IndyEndpointAttrib ): Promise { try { agentContext.config.logger.debug(`Set endpoints for did '${did}' on ledger '${pool.indyNamespace}'`, endpoints) @@ -244,15 +289,15 @@ export class IndyVdrSovDidRegistrar implements DidRegistrar { export interface IndyVdrDidCreateOptions extends DidCreateOptions { method: 'indy' - did?: undefined - didDocument?: DidDocument + did?: string + didDocument?: never // Not yet supported options: { alias?: string role?: string - endpoints?: IndyEndpointAttrib + services?: DidDocumentService[] useEndpointAttrib?: boolean - indyNamespace?: string submitterDid: string + verkey?: string } secret?: { seed?: string diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts index e36b8d2ecb..62d9a989dc 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts @@ -6,7 +6,7 @@ import { GetAttribRequest, GetNymRequest } from 'indy-vdr-test-shared' import { IndyVdrError, IndyVdrNotFoundError } from '../error' import { IndyVdrPoolService } from '../pool' -import { createKeyAgreementKey, indyDidDocumentFromDid, parseIndyDid } from './didIndyUtil' +import { combineDidDocumentWithJson, createKeyAgreementKey, indyDidDocumentFromDid, parseIndyDid } from './didIndyUtil' import { addServicesFromEndpointsAttrib } from './didSovUtil' export class IndyVdrIndyDidResolver implements DidResolver { @@ -56,7 +56,7 @@ export class IndyVdrIndyDidResolver implements DidResolver { .addVerificationMethod({ controller: did, id: keyAgreementId, - publicKeyBase58: createKeyAgreementKey(did, getNymResponseData.verkey), + publicKeyBase58: createKeyAgreementKey(getNymResponseData.verkey), type: 'X25519KeyAgreementKey2019', }) .addKeyAgreement(keyAgreementId) @@ -68,7 +68,7 @@ export class IndyVdrIndyDidResolver implements DidResolver { return builder.build() } else { // Combine it with didDoc (TODO: Check if diddocContent is returned as a JSON object or a string) - return builder.build().combine(getNymResponseData.diddocContent) + return combineDidDocumentWithJson(builder.build(), getNymResponseData.diddocContent) } } diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123.json new file mode 100644 index 0000000000..c26715ddc1 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123.json @@ -0,0 +1,100 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:example:123", + "alsoKnownAs": ["did:example:456"], + "controller": ["did:example:456"], + "verificationMethod": [ + { + "id": "did:example:123#verkey", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC X..." + }, + { + "id": "did:example:123#key-2", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "-----BEGIN PUBLIC 9..." + }, + { + "id": "did:example:123#key-3", + "type": "Secp256k1VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyHex": "-----BEGIN PUBLIC A..." + } + ], + "service": [ + { + "id": "did:example:123#service-1", + "type": "Mediator", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" + }, + { + "id": "did:example:123#service-2", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "priority": 5 + }, + { + "id": "did:example:123#service-3", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 + } + ], + "authentication": [ + "did:example:123#verkey", + { + "id": "did:example:123#authentication-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "assertionMethod": [ + "did:example:123#verkey", + { + "id": "did:example:123#assertionMethod-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityDelegation": [ + "did:example:123#verkey", + { + "id": "did:example:123#capabilityDelegation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityInvocation": [ + "did:example:123#verkey", + { + "id": "did:example:123#capabilityInvocation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "keyAgreement": [ + "did:example:123#verkey", + { + "id": "did:example:123#keyAgreement-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + }, + { + "id": "did:example:123#keyAgreement-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123base.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123base.json similarity index 70% rename from packages/core/src/modules/dids/__tests__/__fixtures__/didExample123base.json rename to packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123base.json index 97126de120..ce8b62392f 100644 --- a/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123base.json +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123base.json @@ -2,10 +2,11 @@ "id": "did:example:123", "verificationMethod": [ { - "id": "did:example:123#key-1", + "id": "did:example:123#verkey", "type": "RsaVerificationKey2018", "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", "publicKeyPem": "-----BEGIN PUBLIC X..." } - ] + ], + "authentication": ["did:example:123#verkey"] } diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123extracontent.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123extracontent.json similarity index 94% rename from packages/core/src/modules/dids/__tests__/__fixtures__/didExample123extracontent.json rename to packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123extracontent.json index 64b650cb0d..81397021cd 100644 --- a/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123extracontent.json +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didExample123extracontent.json @@ -40,7 +40,6 @@ } ], "authentication": [ - "did:example:123#key-1", { "id": "did:example:123#authentication-1", "type": "RsaVerificationKey2018", @@ -49,7 +48,7 @@ } ], "assertionMethod": [ - "did:example:123#key-1", + "did:example:123#verkey", { "id": "did:example:123#assertionMethod-1", "type": "RsaVerificationKey2018", @@ -58,7 +57,7 @@ } ], "capabilityDelegation": [ - "did:example:123#key-1", + "did:example:123#verkey", { "id": "did:example:123#capabilityDelegation-1", "type": "RsaVerificationKey2018", @@ -67,7 +66,7 @@ } ], "capabilityInvocation": [ - "did:example:123#key-1", + "did:example:123#verkey", { "id": "did:example:123#capabilityInvocation-1", "type": "RsaVerificationKey2018", @@ -76,7 +75,7 @@ } ], "keyAgreement": [ - "did:example:123#key-1", + "did:example:123#verkey", { "id": "did:example:123#keyAgreement-1", "type": "RsaVerificationKey2018", diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json index 1218fe2ee0..49e43fa742 100644 --- a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7EL.json @@ -1,8 +1,9 @@ { - "@context": ["https://w3id.org/did/v1", - "https://w3id.org/security/suites/ed25519-2018/v1", - "https://w3id.org/security/suites/x25519-2019/v1" -], + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL", "alsoKnownAs": ["did:indy:ns2:R1xKJw17sUoXhejEpugMYJ"], "controller": ["did:indy:ns2:R1xKJw17sUoXhejEpugMYJ"], @@ -10,7 +11,7 @@ { "type": "Ed25519VerificationKey2018", "controller": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL", - "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#verkey", "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" }, { @@ -18,7 +19,7 @@ "type": "RsaVerificationKey2018", "controller": "did:indy:ns2:WJz9mHyW9BZksioQnRsrAo", "publicKeyPem": "-----BEGIN PUBLIC X..." - }, + }, { "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-3", "type": "Ed25519VerificationKey2018", @@ -56,7 +57,7 @@ } ], "authentication": [ - "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#key-1", + "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#verkey", { "id": "did:indy:ns2:LjgpST2rjsoxYegQDRm7EL#authentication-1", "type": "RsaVerificationKey2018", diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json index 857ffe1c84..94bfaed219 100644 --- a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent.json @@ -1,8 +1,5 @@ { - "@context": [ - "https://w3id.org/security/suites/ed25519-2018/v1", - "https://w3id.org/security/suites/x25519-2019/v1" -], + "@context": ["https://w3id.org/security/suites/ed25519-2018/v1", "https://w3id.org/security/suites/x25519-2019/v1"], "alsoKnownAs": ["did:indy:ns2:R1xKJw17sUoXhejEpugMYJ"], "controller": ["did:indy:ns2:R1xKJw17sUoXhejEpugMYJ"], "verificationMethod": [ diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json index 815bc02a94..56014e70be 100644 --- a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json @@ -1,13 +1,13 @@ { - "@context": [ "https://w3id.org/did/v1" ], + "@context": ["https://w3id.org/did/v1"], "id": "did:indy:ns1:R1xKJw17sUoXhejEpugMYJ", "verificationMethod": [ { "type": "Ed25519VerificationKey2018", "controller": "did:indy:ns1:R1xKJw17sUoXhejEpugMYJ", - "id": "did:indy:ns1:R1xKJw17sUoXhejEpugMYJ#key-1", + "id": "did:indy:ns1:R1xKJw17sUoXhejEpugMYJ#verkey", "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" } ], - "authentication": ["did:indy:ns1:R1xKJw17sUoXhejEpugMYJ#key-1"] + "authentication": ["did:indy:ns1:R1xKJw17sUoXhejEpugMYJ#verkey"] } diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json index 829e40b26b..c131549e18 100644 --- a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json @@ -1,14 +1,11 @@ { - "@context": [ - "https://w3id.org/did/v1", - "https://didcomm.org/messaging/contexts/v2" - ], + "@context": ["https://w3id.org/did/v1", "https://didcomm.org/messaging/contexts/v2"], "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo", "verificationMethod": [ { "type": "Ed25519VerificationKey2018", "controller": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo", - "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#key-1", + "id": "did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#verkey", "publicKeyBase58": "GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8" }, { @@ -18,7 +15,7 @@ "publicKeyBase58": "S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud" } ], - "authentication": ["did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#key-1"], + "authentication": ["did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#verkey"], "keyAgreement": ["did:indy:ns1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], "service": [ { diff --git a/packages/indy-vdr/src/dids/__tests__/didIndyUtil.test.ts b/packages/indy-vdr/src/dids/__tests__/didIndyUtil.test.ts new file mode 100644 index 0000000000..c3b13e21b5 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/didIndyUtil.test.ts @@ -0,0 +1,23 @@ +import { DidDocument, JsonTransformer } from '@aries-framework/core' + +import { combineDidDocumentWithJson, deepObjectDiff } from '../didIndyUtil' + +import didExample123Fixture from './__fixtures__/didExample123.json' +import didExample123Base from './__fixtures__/didExample123base.json' +import didExample123Extra from './__fixtures__/didExample123extracontent.json' + +describe('didIndyUtil', () => { + describe('combineDidDocumentWithJson', () => { + it('should correctly combine a base DIDDoc with extra contents from a JSON object', async () => { + const didDocument = JsonTransformer.fromJSON(didExample123Base, DidDocument) + + expect(combineDidDocumentWithJson(didDocument, didExample123Extra).toJSON()).toEqual(didExample123Fixture) + }) + }) + + describe('deepObjectDiff', () => { + it('should correctly show the diff between a base DidDocument and a full DidDocument', async () => { + expect(deepObjectDiff(didExample123Fixture, didExample123Base)).toMatchObject(didExample123Extra) + }) + }) +}) diff --git a/packages/indy-vdr/src/dids/__tests__/didSovUtil.test.ts b/packages/indy-vdr/src/dids/__tests__/didSovUtil.test.ts new file mode 100644 index 0000000000..f09f8060bc --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/didSovUtil.test.ts @@ -0,0 +1,5 @@ +describe('didSovUtil', () => { + describe('endpointsAttribFromServices', () => { + it.todo('should correctly transform DidDocumentService instances to endpoint Attrib') + }) +}) diff --git a/packages/indy-vdr/src/dids/didIndyUtil.ts b/packages/indy-vdr/src/dids/didIndyUtil.ts index 55f50c783c..0708af85a8 100644 --- a/packages/indy-vdr/src/dids/didIndyUtil.ts +++ b/packages/indy-vdr/src/dids/didIndyUtil.ts @@ -1,17 +1,18 @@ import { AriesFrameworkError, convertPublicKeyToX25519, + DidDocument, DidDocumentBuilder, + JsonTransformer, TypedArrayEncoder, } from '@aries-framework/core' +import { mergeWith, compact, transform, isUndefined, isEqual, isObject, isArray } from 'lodash' import { DID_INDY_REGEX } from '../utils/did' -import { getFullVerkey } from './didSovUtil' - // Create a base DIDDoc template according to https://hyperledger.github.io/indy-did-method/#base-diddoc-template export function indyDidDocumentFromDid(did: string, verKeyBase58: string) { - const verificationMethodId = `${did}#key-1` + const verificationMethodId = `${did}#verkey` const publicKeyBase58 = verKeyBase58 @@ -27,13 +28,8 @@ export function indyDidDocumentFromDid(did: string, verKeyBase58: string) { return builder } -export function createKeyAgreementKey(fullDid: string, verkey: string) { - const publicKeyBase58 = getFullVerkey(fullDid, verkey) - const publicKeyX25519 = TypedArrayEncoder.toBase58( - convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(publicKeyBase58)) - ) - - return publicKeyX25519 +export function createKeyAgreementKey(verkey: string) { + return TypedArrayEncoder.toBase58(convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(verkey))) } export function parseIndyDid(did: string) { @@ -45,3 +41,36 @@ export function parseIndyDid(did: string) { throw new AriesFrameworkError(`${did} is not a valid did:indy did`) } } + +/** + * Combine a JSON content with the contents of a DidDocument + * @param didDoc object containing original DIDDocument + * @param json object containing extra DIDDoc contents + * + * @returns a DidDocument object resulting from the combination of both + */ +export function combineDidDocumentWithJson(didDoc: DidDocument, json: Record) { + const didDocJson = didDoc.toJSON() + const combinedJson = mergeWith(didDocJson, json, (objValue: unknown, srcValue: unknown) => { + if (Array.isArray(objValue) && Array.isArray(srcValue)) { + return [...new Set([...objValue, ...srcValue])] + } + }) + + return JsonTransformer.fromJSON(combinedJson, DidDocument) +} + +export function deepObjectDiff(object: any, base: any) { + function changes(object: any, base: any) { + return transform(object, function (result: any, value: any, key: any) { + if (isUndefined(base[key])) { + result[key] = value + } else { + if (!isEqual(value, base[key])) { + result[key] = isObject(value) || isArray(value) ? compact(changes(value, base[key])) : value + } + } + }) + } + return changes(object, base) +} diff --git a/packages/indy-vdr/src/dids/didSovUtil.ts b/packages/indy-vdr/src/dids/didSovUtil.ts index 37aa5201e1..33cfd1c1b8 100644 --- a/packages/indy-vdr/src/dids/didSovUtil.ts +++ b/packages/indy-vdr/src/dids/didSovUtil.ts @@ -5,6 +5,7 @@ import { DidCommV1Service, DidCommV2Service, convertPublicKeyToX25519, + AriesFrameworkError, } from '@aries-framework/core' export type CommEndpointType = 'endpoint' | 'did-communication' | 'DIDComm' @@ -106,6 +107,35 @@ function processEndpointTypes(types?: string[]) { return types } +export function endpointsAttribFromServices(services: DidDocumentService[]) { + const commTypes: CommEndpointType[] = ['endpoint', 'did-communication', 'DIDComm'] + const commServices = services.filter((item) => commTypes.includes(item.type as CommEndpointType)) + + const attrib: IndyEndpointAttrib = { endpoint: services[0].serviceEndpoint, types: [], routingKeys: [] } + + // Check that all services use the same endpoint, as only one is accepted + if (!commServices.every((item) => item.serviceEndpoint === services[0].serviceEndpoint)) { + throw new AriesFrameworkError('serviceEndpoint for all services must match') + } + + for (const commService of commServices) { + const commServiceType = commService.type as CommEndpointType + if (attrib.types?.includes(commServiceType)) { + throw new AriesFrameworkError('Only a single communication service per type is supported') + } else { + attrib.types?.push(commServiceType) + } + + if (commService instanceof DidCommV1Service || commService instanceof DidCommV2Service) { + if (commService.routingKeys) { + attrib.routingKeys?.push() + } + } + } + + return attrib +} + export function addServicesFromEndpointsAttrib( builder: DidDocumentBuilder, did: string, diff --git a/packages/indy-vdr/src/utils/did.ts b/packages/indy-vdr/src/utils/did.ts index 2ef5c155e9..44632246bb 100644 --- a/packages/indy-vdr/src/utils/did.ts +++ b/packages/indy-vdr/src/utils/did.ts @@ -17,7 +17,6 @@ import { TypedArrayEncoder } from '@aries-framework/core' - export const DID_INDY_REGEX = /^did:indy:((?:[a-z][_a-z0-9-]*)(?::[a-z][_a-z0-9-]*)?):([1-9A-HJ-NP-Za-km-z]{21,22})$/ export const ABBREVIATED_VERKEY_REGEX = /^~[1-9A-HJ-NP-Za-km-z]{21,22}$/ diff --git a/yarn.lock b/yarn.lock index cf61d3984e..55e4922311 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9921,14 +9921,14 @@ rimraf@^2.5.4, rimraf@^2.6.3, rimraf@^2.7.1: dependencies: glob "^7.1.3" -rimraf@^3.0.0, rimraf@^3.0.2, rimraf@~3.0.2: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" -rimraf@^4.0.7: +rimraf@^4.0.7, rimraf@~4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.0.7.tgz#f438c7d6a2d5e5cca1d81e3904a48ac7b053a542" integrity sha512-CUEDDrZvc0swDgVdXGiv3FcYYQMpJxjvSGt85Amj6yU+MCVWurrLCeLiJDdJPHCzNJnwuebBEdcO//eP11Xa7w== From 81d3b5de03d516f8edd4243f591b530d352e8fc5 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 6 Feb 2023 08:50:11 -0300 Subject: [PATCH 29/41] fix: return null Signed-off-by: Ariel Gentile --- packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts | 2 +- packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts index 62d9a989dc..b64f4662b8 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts @@ -107,7 +107,7 @@ export class IndyVdrIndyDidResolver implements DidResolver { const response = await pool.submitReadRequest(request) if (!response.result.data) { - return + return null } const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts index 8ba092201d..465a10a58a 100644 --- a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts @@ -74,7 +74,7 @@ export class IndyVdrSovDidResolver implements DidResolver { const response = await pool.submitReadRequest(request) if (!response.result.data) { - return + return null } const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib @@ -86,7 +86,7 @@ export class IndyVdrSovDidResolver implements DidResolver { } ) - return endpoints ?? {} + return endpoints ?? null } catch (error) { agentContext.config.logger.error( `Error retrieving endpoints for did '${did}' from ledger '${pool.indyNamespace}'`, From 9d19ab5ca2dba598cf849dd25902068c87bfd87f Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Fri, 10 Feb 2023 17:39:40 -0300 Subject: [PATCH 30/41] fix imports and remove did from tags Signed-off-by: Ariel Gentile --- packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts | 4 +--- packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts | 2 +- yarn.lock | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts index cef1c907a7..200e29974d 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -7,7 +7,6 @@ import type { DidCreateResult, DidDeactivateResult, DidUpdateResult, - DidDocument, DidDocumentService, } from '@aries-framework/core' @@ -20,7 +19,7 @@ import { DidRecord, DidRepository, } from '@aries-framework/core' -import { AttribRequest, NymRequest } from 'indy-vdr-test-shared' +import { AttribRequest, NymRequest } from '@hyperledger/indy-vdr-shared' import { IndyVdrError } from '../error' import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' @@ -142,7 +141,6 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { role: DidDocumentRole.Created, tags: { recipientKeyFingerprints: didDocument.recipientKeys.map((key: Key) => key.fingerprint), - did, }, }) diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts index b64f4662b8..05ede7c069 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts @@ -1,7 +1,7 @@ import type { CommEndpointType, GetNymResponseData, IndyEndpointAttrib } from './didSovUtil' import type { DidResolutionResult, DidResolver, AgentContext } from '@aries-framework/core' -import { GetAttribRequest, GetNymRequest } from 'indy-vdr-test-shared' +import { GetAttribRequest, GetNymRequest } from '@hyperledger/indy-vdr-shared' import { IndyVdrError, IndyVdrNotFoundError } from '../error' import { IndyVdrPoolService } from '../pool' diff --git a/yarn.lock b/yarn.lock index bcc836c2a4..4a8447c5fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9975,7 +9975,7 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rimraf@^4.0.7, rimraf@~4.0.7: +rimraf@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.0.7.tgz#f438c7d6a2d5e5cca1d81e3904a48ac7b053a542" integrity sha512-CUEDDrZvc0swDgVdXGiv3FcYYQMpJxjvSGt85Amj6yU+MCVWurrLCeLiJDdJPHCzNJnwuebBEdcO//eP11Xa7w== From ab4bd2002b346d84f59aec1f38d4c831fcf3636c Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 13 Feb 2023 22:30:45 -0300 Subject: [PATCH 31/41] feat: add tests using askar as backend Signed-off-by: Ariel Gentile --- packages/askar/src/wallet/AskarWallet.ts | 13 +- .../src/dids/IndyVdrIndyDidRegistrar.ts | 24 +++- .../tests/indy-vdr-did-registrar.e2e.test.ts | 112 ++++++++++++++++++ .../tests/indy-vdr-did-resolver.e2e.test.ts | 2 +- .../indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 12 +- 5 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index 432e50cdda..07f92a32a0 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -354,13 +354,17 @@ export class AskarWallet implements Wallet { * @throws {WalletError} When an unsupported keytype is requested * @throws {WalletError} When the key could not be created */ - public async createKey({ seed, keyType }: WalletCreateKeyOptions): Promise { + public async createKey({ seed, secretKey, keyType }: AskarWalletCreateKeyOptions): Promise { try { if (keyTypeSupportedByAskar(keyType)) { const algorithm = keyAlgFromString(keyType) // Create key from seed - const key = seed ? AskarKey.fromSeed({ seed: Buffer.from(seed), algorithm }) : AskarKey.generate(algorithm) + const key = secretKey + ? AskarKey.fromSecretBytes({ secretKey, algorithm }) + : seed + ? AskarKey.fromSeed({ seed: Buffer.from(seed), algorithm }) + : AskarKey.generate(algorithm) // Store key await this.session.insertKey({ key, name: encodeToBase58(key.publicBytes) }) @@ -398,7 +402,6 @@ export class AskarWallet implements Wallet { if (!TypedArrayEncoder.isTypedArray(data)) { throw new WalletError(`Currently not supporting signing of multiple messages`) } - const keyEntry = await this.session.fetchKey({ name: key.publicKeyBase58 }) if (!keyEntry) { @@ -746,3 +749,7 @@ export class AskarWallet implements Wallet { } } } + +export interface AskarWalletCreateKeyOptions extends WalletCreateKeyOptions { + secretKey?: Uint8Array +} diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts index 200e29974d..c6e711a6ac 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -43,7 +43,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { } } - const { alias, role, submitterDid, services, useEndpointAttrib } = options.options + const { alias, role, submitterDid, submitterVerkey, services, useEndpointAttrib } = options.options let verkey = options.options.verkey let did = options.did let id @@ -87,7 +87,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { // Create base did document const didDocumentBuilder = indyDidDocumentFromDid(did, verkey) - let diddocContent = {} + let diddocContent // Add services if object was passed if (services) { @@ -128,7 +128,17 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { } const pool = agentContext.dependencyManager.resolve(IndyVdrPoolService).getPoolForNamespace(namespace) - await this.registerPublicDid(agentContext, pool, submitterId, id, verkey, alias, role, diddocContent) + await this.registerPublicDid( + agentContext, + pool, + submitterId, + submitterVerkey, + id, + verkey, + alias, + role, + diddocContent + ) if (services && useEndpointAttrib) { await this.setEndpointsForDid(agentContext, pool, id, endpointsAttribFromServices(services)) @@ -202,10 +212,11 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { } } - public async registerPublicDid( + private async registerPublicDid( agentContext: AgentContext, pool: IndyVdrPool, submitterDid: string, + submitterVerkey: string, targetDid: string, verkey: string, alias?: string, @@ -222,7 +233,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { const request = new NymRequest({ submitterDid, dest: targetDid, verkey, alias }) - const signingKey = Key.fromPublicKeyBase58(submitterDid, KeyType.Ed25519) + const signingKey = Key.fromPublicKeyBase58(submitterVerkey, KeyType.Ed25519) const response = await pool.submitWriteRequest(agentContext, request, signingKey) @@ -249,7 +260,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { } } - public async setEndpointsForDid( + private async setEndpointsForDid( agentContext: AgentContext, pool: IndyVdrPool, did: string, @@ -295,6 +306,7 @@ export interface IndyVdrDidCreateOptions extends DidCreateOptions { services?: DidDocumentService[] useEndpointAttrib?: boolean submitterDid: string + submitterVerkey: string verkey?: string } secret?: { diff --git a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts new file mode 100644 index 0000000000..13003b2a09 --- /dev/null +++ b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts @@ -0,0 +1,112 @@ +import type { Key } from '@aries-framework/core' + +import { AskarStorageService, AskarWallet } from '@aries-framework/askar' +import { + InjectionSymbols, + CacheModuleConfig, + InMemoryLruCache, + JsonTransformer, + KeyType, + SigningProviderRegistry, + TypedArrayEncoder, +} from '@aries-framework/core' +import { Subject } from 'rxjs' + +import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' +import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' +import { IndyVdrIndyDidResolver } from '../src/dids/IndyVdrIndyDidResolver' +import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' + +import '@hyperledger/aries-askar-nodejs' +import { indyVdrModuleConfig } from './helpers' + +const logger = testLogger +const wallet = new AskarWallet(logger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + +const agentConfig = getAgentConfig('IndyVdrIndyDidRegistrar E2E', { logger }) + +const cache = new InMemoryLruCache({ limit: 200 }) +const indyVdrIndyDidResolver = new IndyVdrIndyDidResolver() +const indyVdrIndyDidRegistrar = new IndyVdrIndyDidRegistrar() + +let signerKey: Key + +const agentContext = getAgentContext({ + wallet, + agentConfig, + registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.StorageService, new AskarStorageService()], + [IndyVdrPoolService, new IndyVdrPoolService(logger, indyVdrModuleConfig)], + [CacheModuleConfig, new CacheModuleConfig({ cache })], + ], +}) + +const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + +describe('Indy VDR registrar E2E', () => { + beforeAll(async () => { + await indyVdrPoolService.connectToPools() + + if (agentConfig.walletConfig) { + await wallet.createAndOpen(agentConfig.walletConfig) + } + + signerKey = await wallet.createKey({ + secretKey: TypedArrayEncoder.fromString('000000000000000000000000Trustee9'), + keyType: KeyType.Ed25519, + }) + }) + + afterAll(async () => { + for (const pool of indyVdrPoolService.pools) { + pool.close() + } + + await wallet.delete() + }) + + test('can register a did:indy without endpoints', async () => { + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + method: 'indy', + options: { + submitterDid: 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt', + submitterVerkey: signerKey.publicKeyBase58, + }, + }) + + const did = result.didState.did + + if (!did) { + throw Error('did not defined') + } + + const didResult = await indyVdrIndyDidResolver.resolve(agentContext, did) + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + didDocument: { + '@context': ['https://w3id.org/did/v1'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: undefined, + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) +}) diff --git a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts index e9f3aa4eab..b1e61b8149 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts @@ -38,7 +38,7 @@ const agentContext = getAgentContext({ const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) -describe('IndyVdrSov', () => { +describe('indy-vdr DID Resolver E2E', () => { beforeAll(async () => { await indyVdrPoolService.connectToPools() diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts index fea5b8fe0f..c096a0cf69 100644 --- a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -1,6 +1,7 @@ import type { Key } from '@aries-framework/core' -import { IndyWallet, KeyType, SigningProviderRegistry } from '@aries-framework/core' +import { AskarWallet } from '@aries-framework/askar' +import { TypedArrayEncoder, KeyType, SigningProviderRegistry } from '@aries-framework/core' import { GetNymRequest, NymRequest, SchemaRequest, CredentialDefinitionRequest } from '@hyperledger/indy-vdr-shared' import { agentDependencies, genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' @@ -10,9 +11,11 @@ import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' import { indyDidFromPublicKeyBase58 } from '../src/utils/did' import { indyVdrModuleConfig } from './helpers' +import '@hyperledger/aries-askar-nodejs' const indyVdrPoolService = new IndyVdrPoolService(testLogger, indyVdrModuleConfig) -const wallet = new IndyWallet(agentDependencies, testLogger, new SigningProviderRegistry([])) +const wallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + const agentConfig = getAgentConfig('IndyVdrPoolService') const agentContext = getAgentContext({ wallet, agentConfig }) @@ -33,7 +36,10 @@ describe('IndyVdrPoolService', () => { await wallet.createAndOpen(agentConfig.walletConfig) } - signerKey = await wallet.createKey({ seed: '000000000000000000000000Trustee9', keyType: KeyType.Ed25519 }) + signerKey = await wallet.createKey({ + secretKey: TypedArrayEncoder.fromString('000000000000000000000000Trustee9'), + keyType: KeyType.Ed25519, + }) }) afterAll(async () => { From 86b96dbfbc5bc565ec86a504e45abc806b19bf83 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 14 Feb 2023 01:48:04 -0300 Subject: [PATCH 32/41] did registrar and resolver tests using askar Signed-off-by: Ariel Gentile --- packages/indy-vdr/package.json | 5 + .../src/dids/IndyVdrIndyDidRegistrar.ts | 13 +- .../__tests__/IndyVdrIndyDidResolver.test.ts | 7 +- packages/indy-vdr/src/dids/didIndyUtil.ts | 34 ++++ packages/indy-vdr/src/dids/didSovUtil.ts | 15 +- .../tests/indy-vdr-did-registrar.e2e.test.ts | 178 +++++++++++++++++- .../tests/indy-vdr-did-resolver.e2e.test.ts | 12 +- yarn.lock | 5 + 8 files changed, 246 insertions(+), 23 deletions(-) diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json index 0cdeac34d3..956103c1b8 100644 --- a/packages/indy-vdr/package.json +++ b/packages/indy-vdr/package.json @@ -30,8 +30,13 @@ "lodash": "^4.17.21" }, "devDependencies": { + "@aries-framework/askar": "0.3.3", + "@hyperledger/aries-askar-nodejs": "^0.1.0-dev.1", "@hyperledger/indy-vdr-nodejs": "^0.1.0-dev.4", + "@stablelib/ed25519": "^1.0.2", + "@types/lodash": "^4.14.191", "rimraf": "^4.0.7", + "rxjs": "^7.2.0", "typescript": "~4.9.4" } } diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts index c6e711a6ac..8d013ad2ba 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -23,6 +23,7 @@ import { AttribRequest, NymRequest } from '@hyperledger/indy-vdr-shared' import { IndyVdrError } from '../error' import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' +import { isSelfCertifiedDid } from '../utils/did' import { createKeyAgreementKey, deepObjectDiff, indyDidDocumentFromDid, parseIndyDid } from './didIndyUtil' import { endpointsAttribFromServices } from './didSovUtil' @@ -74,7 +75,9 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { }, } } - // TODO: Validate that specified did matches to verkey + if (!isSelfCertifiedDid) { + throw new Error(`Initial verkey ${verkey} does not match did ˇ${did}`) + } } else { // Create a new key and calculate did according to the rules for indy did method const key = await agentContext.wallet.createKey({ seed, keyType: KeyType.Ed25519 }) @@ -91,7 +94,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { // Add services if object was passed if (services) { - services.forEach(didDocumentBuilder.addService) + services.forEach((item) => didDocumentBuilder.addService(item)) const commTypes: CommEndpointType[] = ['endpoint', 'did-communication', 'DIDComm'] const serviceTypes = new Set(services.map((item) => item.type)) @@ -141,7 +144,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { ) if (services && useEndpointAttrib) { - await this.setEndpointsForDid(agentContext, pool, id, endpointsAttribFromServices(services)) + await this.setEndpointsForDid(agentContext, pool, verkey, id, endpointsAttribFromServices(services)) } // Save the did so we know we created it and can issue with it @@ -263,6 +266,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { private async setEndpointsForDid( agentContext: AgentContext, pool: IndyVdrPool, + submitterVerkey: string, did: string, endpoints: IndyEndpointAttrib ): Promise { @@ -275,7 +279,8 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { raw: JSON.stringify({ endpoint: endpoints }), }) - const signingKey = Key.fromPublicKeyBase58(did, KeyType.Ed25519) + const signingKey = Key.fromPublicKeyBase58(submitterVerkey, KeyType.Ed25519) + const response = await pool.submitWriteRequest(agentContext, request, signingKey) agentContext.config.logger.debug( `Successfully set endpoints for did '${did}' on ledger '${pool.indyNamespace}'`, diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts index f3cbb7608a..1b31e390e3 100644 --- a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts @@ -9,21 +9,16 @@ import didIndyLjgpST2rjsoxYegQDRm7ELdiddocContent from './__fixtures__/didIndyLj import didIndyR1xKJw17sUoXhejEpugMYJFixture from './__fixtures__/didIndyR1xKJw17sUoXhejEpugMYJ.json' import didIndyWJz9mHyW9BZksioQnRsrAoFixture from './__fixtures__/didIndyWJz9mHyW9BZksioQnRsrAo.json' -jest.mock('../../pool/IndyVdrPoolService') -const IndyVdrPoolServiceMock = IndyVdrPoolService as jest.Mock -const poolServiceMock = new IndyVdrPoolServiceMock() - jest.mock('../../pool/IndyVdrPool') const IndyVdrPoolMock = IndyVdrPool as jest.Mock const poolMock = new IndyVdrPoolMock() mockProperty(poolMock, 'indyNamespace', 'ns1') -jest.spyOn(poolServiceMock, 'getPoolForNamespace').mockReturnValue(poolMock) const agentConfig = getAgentConfig('IndyVdrIndyDidResolver') const agentContext = getAgentContext({ agentConfig, - registerInstances: [[IndyVdrPoolService, poolServiceMock]], + registerInstances: [[IndyVdrPoolService, { getPoolForNamespace: jest.fn().mockReturnValue(poolMock) }]], }) const resolver = new IndyVdrIndyDidResolver() diff --git a/packages/indy-vdr/src/dids/didIndyUtil.ts b/packages/indy-vdr/src/dids/didIndyUtil.ts index 0708af85a8..c37135071b 100644 --- a/packages/indy-vdr/src/dids/didIndyUtil.ts +++ b/packages/indy-vdr/src/dids/didIndyUtil.ts @@ -3,7 +3,10 @@ import { convertPublicKeyToX25519, DidDocument, DidDocumentBuilder, + Hasher, JsonTransformer, + Key, + KeyType, TypedArrayEncoder, } from '@aries-framework/core' import { mergeWith, compact, transform, isUndefined, isEqual, isObject, isArray } from 'lodash' @@ -74,3 +77,34 @@ export function deepObjectDiff(object: any, base: any) { } return changes(object, base) } + +/** + * Check whether the did is a self certifying did. If the verkey is abbreviated this method + * will always return true. Make sure that the verkey you pass in this method belongs to the + * did passed in + * + * @return Boolean indicating whether the did is self certifying + */ +export function isSelfCertifiedIndyDid(did: string, verkey: string): boolean { + const { namespace } = parseIndyDid(did) + const { did: didFromVerkey } = indyDidFromNamespaceAndInitialKey( + namespace, + Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) + ) + + if (didFromVerkey === did) { + return true + } + + return false +} + +export function indyDidFromNamespaceAndInitialKey(namespace: string, initialKey: Key) { + const buffer = Hasher.hash(initialKey.publicKey, 'sha2-256') + + const id = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + const verkey = initialKey.publicKeyBase58 + const did = `did:indy:${namespace}:${id}` + + return { did, id, verkey } +} diff --git a/packages/indy-vdr/src/dids/didSovUtil.ts b/packages/indy-vdr/src/dids/didSovUtil.ts index 33cfd1c1b8..b836eb2eae 100644 --- a/packages/indy-vdr/src/dids/didSovUtil.ts +++ b/packages/indy-vdr/src/dids/didSovUtil.ts @@ -107,33 +107,34 @@ function processEndpointTypes(types?: string[]) { return types } -export function endpointsAttribFromServices(services: DidDocumentService[]) { +export function endpointsAttribFromServices(services: DidDocumentService[]): IndyEndpointAttrib { const commTypes: CommEndpointType[] = ['endpoint', 'did-communication', 'DIDComm'] const commServices = services.filter((item) => commTypes.includes(item.type as CommEndpointType)) - const attrib: IndyEndpointAttrib = { endpoint: services[0].serviceEndpoint, types: [], routingKeys: [] } - // Check that all services use the same endpoint, as only one is accepted if (!commServices.every((item) => item.serviceEndpoint === services[0].serviceEndpoint)) { throw new AriesFrameworkError('serviceEndpoint for all services must match') } + const types: CommEndpointType[] = [] + const routingKeys = new Set() + for (const commService of commServices) { const commServiceType = commService.type as CommEndpointType - if (attrib.types?.includes(commServiceType)) { + if (types.includes(commServiceType)) { throw new AriesFrameworkError('Only a single communication service per type is supported') } else { - attrib.types?.push(commServiceType) + types.push(commServiceType) } if (commService instanceof DidCommV1Service || commService instanceof DidCommV2Service) { if (commService.routingKeys) { - attrib.routingKeys?.push() + commService.routingKeys.forEach((item) => routingKeys.add(item)) } } } - return attrib + return { endpoint: services[0].serviceEndpoint, types, routingKeys: Array.from(routingKeys) } } export function addServicesFromEndpointsAttrib( diff --git a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts index 13003b2a09..c06869c913 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts @@ -1,7 +1,6 @@ -import type { Key } from '@aries-framework/core' - import { AskarStorageService, AskarWallet } from '@aries-framework/askar' import { + Key, InjectionSymbols, CacheModuleConfig, InMemoryLruCache, @@ -9,16 +8,22 @@ import { KeyType, SigningProviderRegistry, TypedArrayEncoder, + DidCommV1Service, + DidCommV2Service, + DidDocumentService, } from '@aries-framework/core' +import { convertPublicKeyToX25519, generateKeyPairFromSeed } from '@stablelib/ed25519' import { Subject } from 'rxjs' import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' import { IndyVdrIndyDidResolver } from '../src/dids/IndyVdrIndyDidResolver' +import { indyDidFromNamespaceAndInitialKey } from '../src/dids/didIndyUtil' import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' import '@hyperledger/aries-askar-nodejs' + import { indyVdrModuleConfig } from './helpers' const logger = testLogger @@ -68,7 +73,7 @@ describe('Indy VDR registrar E2E', () => { await wallet.delete() }) - test('can register a did:indy without endpoints', async () => { + test('can register a did:indy without services', async () => { const result = await indyVdrIndyDidRegistrar.create(agentContext, { method: 'indy', options: { @@ -109,4 +114,171 @@ describe('Indy VDR registrar E2E', () => { }, }) }) + + test('can register a did:indy without services - did and verkey specified', async () => { + // Generate a seed and the indy did. This allows us to create a new did every time + // but still check if the created output document is as expected. + const seed = Array(32 + 1) + .join((Math.random().toString(36) + '00000000000000000').slice(2, 18)) + .slice(0, 32) + + const keyPair = generateKeyPairFromSeed(TypedArrayEncoder.fromString(seed)) + const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(keyPair.publicKey) + + const { did, verkey } = indyDidFromNamespaceAndInitialKey( + 'pool:localtest', + Key.fromPublicKey(keyPair.publicKey, KeyType.Ed25519) + ) + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + method: 'indy', + did, + options: { + submitterDid: 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt', + submitterVerkey: signerKey.publicKeyBase58, + verkey, + }, + }) + + const receivedDid = result.didState.did + + if (!receivedDid) { + throw Error('did not defined') + } + + const didResult = await indyVdrIndyDidResolver.resolve(agentContext, did) + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + didDocument: { + '@context': ['https://w3id.org/did/v1'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: undefined, + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + test('can register a did:indy with services - did and verkey specified - using attrib endpoint', async () => { + // Generate a seed and the indy did. This allows us to create a new did every time + // but still check if the created output document is as expected. + const seed = Array(32 + 1) + .join((Math.random().toString(36) + '00000000000000000').slice(2, 18)) + .slice(0, 32) + + const key = await wallet.createKey({ seed: seed, keyType: KeyType.Ed25519 }) + const x25519PublicKeyBase58 = TypedArrayEncoder.toBase58(convertPublicKeyToX25519(key.publicKey)) + const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(key.publicKey) + + const { did, verkey } = indyDidFromNamespaceAndInitialKey( + 'pool:localtest', + Key.fromPublicKey(key.publicKey, KeyType.Ed25519) + ) + + const result = await indyVdrIndyDidRegistrar.create(agentContext, { + method: 'indy', + did, + options: { + submitterDid: 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt', + submitterVerkey: signerKey.publicKeyBase58, + useEndpointAttrib: true, + verkey, + services: [ + new DidDocumentService({ + id: `${did}#endpoint`, + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }), + new DidCommV1Service({ + id: `${did}#did-communication`, + priority: 0, + recipientKeys: [`${did}#key-agreement-1`], + routingKeys: ['a-routing-key'], + serviceEndpoint: 'https://example.com/endpoint', + }), + new DidCommV2Service({ + accept: ['didcomm/v2'], + id: `${did}#didcomm-1`, + routingKeys: ['a-routing-key'], + serviceEndpoint: 'https://example.com/endpoint', + }), + ], + }, + }) + + const services = [ + { + id: `${did}#endpoint`, + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }, + { + accept: ['didcomm/aip2;env=rfc19'], + id: `${did}#did-communication`, + priority: 0, + recipientKeys: [`${did}#key-agreement-1`], + routingKeys: ['a-routing-key'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + { + accept: ['didcomm/v2'], + id: `${did}#didcomm-1`, + routingKeys: ['a-routing-key'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'DIDComm', + }, + ] + + const receivedDid = result.didState.did + + if (!receivedDid) { + throw Error('did not defined') + } + + const didResult = await indyVdrIndyDidResolver.resolve(agentContext, did) + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + didDocument: { + '@context': ['https://w3id.org/did/v1', 'https://didcomm.org/messaging/contexts/v2'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + { + type: 'X25519KeyAgreementKey2019', + controller: did, + id: `${did}#key-agreement-1`, + publicKeyBase58: x25519PublicKeyBase58, + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: services, + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) }) diff --git a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts index b1e61b8149..32999a22fa 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts @@ -1,10 +1,11 @@ import type { Key } from '@aries-framework/core' +import { AskarWallet } from '@aries-framework/askar' import { + TypedArrayEncoder, CacheModuleConfig, InMemoryLruCache, JsonTransformer, - IndyWallet, KeyType, SigningProviderRegistry, } from '@aries-framework/core' @@ -18,8 +19,10 @@ import { indyDidFromPublicKeyBase58 } from '../src/utils/did' import { createDidOnLedger, indyVdrModuleConfig } from './helpers' +import '@hyperledger/aries-askar-nodejs' + const logger = testLogger -const wallet = new IndyWallet(agentDependencies, logger, new SigningProviderRegistry([])) +const wallet = new AskarWallet(logger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) const agentConfig = getAgentConfig('IndyVdrResolver E2E', { logger }) const cache = new InMemoryLruCache({ limit: 200 }) @@ -46,7 +49,10 @@ describe('indy-vdr DID Resolver E2E', () => { await wallet.createAndOpen(agentConfig.walletConfig) } - signerKey = await wallet.createKey({ seed: '000000000000000000000000Trustee9', keyType: KeyType.Ed25519 }) + signerKey = await wallet.createKey({ + secretKey: TypedArrayEncoder.fromString('000000000000000000000000Trustee9'), + keyType: KeyType.Ed25519, + }) }) afterAll(async () => { diff --git a/yarn.lock b/yarn.lock index 0a86c85b57..3e1fab901f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2585,6 +2585,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash@^4.14.191": + version "4.14.191" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" + integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== + "@types/luxon@^1.27.0": version "1.27.1" resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.27.1.tgz#aceeb2d5be8fccf541237e184e37ecff5faa9096" From fa7da5c061d0c326c411d6e53bc83d97a4c8f50b Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 14 Feb 2023 02:11:12 -0300 Subject: [PATCH 33/41] fix: check did and initial verkey Signed-off-by: Ariel Gentile --- .../indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts index 8d013ad2ba..8165afd608 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -23,9 +23,15 @@ import { AttribRequest, NymRequest } from '@hyperledger/indy-vdr-shared' import { IndyVdrError } from '../error' import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' -import { isSelfCertifiedDid } from '../utils/did' +import {} from '../utils/did' -import { createKeyAgreementKey, deepObjectDiff, indyDidDocumentFromDid, parseIndyDid } from './didIndyUtil' +import { + createKeyAgreementKey, + deepObjectDiff, + indyDidDocumentFromDid, + parseIndyDid, + isSelfCertifiedIndyDid, +} from './didIndyUtil' import { endpointsAttribFromServices } from './didSovUtil' export class IndyVdrIndyDidRegistrar implements DidRegistrar { @@ -75,7 +81,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { }, } } - if (!isSelfCertifiedDid) { + if (!isSelfCertifiedIndyDid(did, verkey)) { throw new Error(`Initial verkey ${verkey} does not match did ˇ${did}`) } } else { From 22adc13b9a9583b709ad246adf4869a4eca71d43 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 14 Feb 2023 02:12:06 -0300 Subject: [PATCH 34/41] remove unused import Signed-off-by: Ariel Gentile --- packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts index 8165afd608..8274e79ff3 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -23,7 +23,6 @@ import { AttribRequest, NymRequest } from '@hyperledger/indy-vdr-shared' import { IndyVdrError } from '../error' import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' -import {} from '../utils/did' import { createKeyAgreementKey, From ab6ea44a81ad4338515efb17817cb3f412605606 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 14 Feb 2023 12:03:17 -0300 Subject: [PATCH 35/41] remove lodash dependency, fix registrar return interface, use askar only for registrar e2e tests Signed-off-by: Ariel Gentile --- packages/indy-vdr/package.json | 4 +- .../src/dids/IndyVdrIndyDidRegistrar.ts | 55 ++--- .../src/dids/__tests__/didIndyUtil.test.ts | 4 +- packages/indy-vdr/src/dids/didIndyUtil.ts | 89 +++++++-- .../tests/indy-vdr-did-registrar.e2e.test.ts | 189 ++++++++++++------ .../tests/indy-vdr-did-resolver.e2e.test.ts | 9 +- .../indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 7 +- yarn.lock | 5 - 8 files changed, 240 insertions(+), 122 deletions(-) diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json index 956103c1b8..15cd719ca1 100644 --- a/packages/indy-vdr/package.json +++ b/packages/indy-vdr/package.json @@ -26,15 +26,13 @@ "dependencies": { "@aries-framework/anoncreds": "0.3.3", "@aries-framework/core": "0.3.3", - "@hyperledger/indy-vdr-shared": "^0.1.0-dev.4", - "lodash": "^4.17.21" + "@hyperledger/indy-vdr-shared": "^0.1.0-dev.4" }, "devDependencies": { "@aries-framework/askar": "0.3.3", "@hyperledger/aries-askar-nodejs": "^0.1.0-dev.1", "@hyperledger/indy-vdr-nodejs": "^0.1.0-dev.4", "@stablelib/ed25519": "^1.0.2", - "@types/lodash": "^4.14.191", "rimraf": "^4.0.7", "rxjs": "^7.2.0", "typescript": "~4.9.4" diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts index 8274e79ff3..257acf4459 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -1,4 +1,4 @@ -import type { CommEndpointType, IndyEndpointAttrib } from './didSovUtil' +import type { IndyEndpointAttrib } from './didSovUtil' import type { IndyVdrPool } from '../pool' import type { AgentContext, @@ -11,6 +11,9 @@ import type { } from '@aries-framework/core' import { + IndyAgentService, + DidCommV1Service, + DidCommV2Service, Hasher, TypedArrayEncoder, Key, @@ -26,7 +29,7 @@ import { IndyVdrPoolService } from '../pool/IndyVdrPoolService' import { createKeyAgreementKey, - deepObjectDiff, + didDocDiff, indyDidDocumentFromDid, parseIndyDid, isSelfCertifiedIndyDid, @@ -101,7 +104,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { if (services) { services.forEach((item) => didDocumentBuilder.addService(item)) - const commTypes: CommEndpointType[] = ['endpoint', 'did-communication', 'DIDComm'] + const commTypes = [IndyAgentService.type, DidCommV1Service.type, DidCommV2Service.type] const serviceTypes = new Set(services.map((item) => item.type)) const keyAgreementId = `${did}#key-agreement-1` @@ -118,9 +121,14 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { .addKeyAgreement(keyAgreementId) } + // If there is a DIDComm V2 service, add context + if (serviceTypes.has(DidCommV2Service.type)) { + didDocumentBuilder.addContext('https://didcomm.org/messaging/contexts/v2') + } + if (!useEndpointAttrib) { // create diddocContent parameter based on the diff between the base and the resulting DID Document - diddocContent = deepObjectDiff( + diddocContent = didDocDiff( didDocumentBuilder.build().toJSON(), indyDidDocumentFromDid(did, verkey).build().toJSON() ) @@ -130,35 +138,34 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { // Build did document const didDocument = didDocumentBuilder.build() - // If there are services and we are using legacy indy endpoint attrib, make sure they are suitable before registering the DID - if (services && useEndpointAttrib) { - endpointsAttribFromServices(services) - } - const pool = agentContext.dependencyManager.resolve(IndyVdrPoolService).getPoolForNamespace(namespace) - await this.registerPublicDid( - agentContext, - pool, - submitterId, - submitterVerkey, - id, - verkey, - alias, - role, - diddocContent - ) + // If there are services and we are using legacy indy endpoint attrib, make sure they are suitable before registering the DID if (services && useEndpointAttrib) { - await this.setEndpointsForDid(agentContext, pool, verkey, id, endpointsAttribFromServices(services)) + const endpoints = endpointsAttribFromServices(services) + await this.registerPublicDid(agentContext, pool, submitterId, submitterVerkey, id, verkey, alias, role) + await this.setEndpointsForDid(agentContext, pool, verkey, id, endpoints) + } else { + await this.registerPublicDid( + agentContext, + pool, + submitterId, + submitterVerkey, + id, + verkey, + alias, + role, + diddocContent + ) } // Save the did so we know we created it and can issue with it const didRecord = new DidRecord({ - id: did, did, role: DidDocumentRole.Created, tags: { recipientKeyFingerprints: didDocument.recipientKeys.map((key: Key) => key.fingerprint), + qualifiedIndyDid: did, }, }) @@ -167,10 +174,10 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { return { didDocumentMetadata: { - did, + qualifiedIndyDid: did, }, didRegistrationMetadata: { - namespace, + didIndyNamespace: namespace, }, didState: { state: 'finished', diff --git a/packages/indy-vdr/src/dids/__tests__/didIndyUtil.test.ts b/packages/indy-vdr/src/dids/__tests__/didIndyUtil.test.ts index c3b13e21b5..81c8218274 100644 --- a/packages/indy-vdr/src/dids/__tests__/didIndyUtil.test.ts +++ b/packages/indy-vdr/src/dids/__tests__/didIndyUtil.test.ts @@ -1,6 +1,6 @@ import { DidDocument, JsonTransformer } from '@aries-framework/core' -import { combineDidDocumentWithJson, deepObjectDiff } from '../didIndyUtil' +import { combineDidDocumentWithJson, didDocDiff } from '../didIndyUtil' import didExample123Fixture from './__fixtures__/didExample123.json' import didExample123Base from './__fixtures__/didExample123base.json' @@ -17,7 +17,7 @@ describe('didIndyUtil', () => { describe('deepObjectDiff', () => { it('should correctly show the diff between a base DidDocument and a full DidDocument', async () => { - expect(deepObjectDiff(didExample123Fixture, didExample123Base)).toMatchObject(didExample123Extra) + expect(didDocDiff(didExample123Fixture, didExample123Base)).toMatchObject(didExample123Extra) }) }) }) diff --git a/packages/indy-vdr/src/dids/didIndyUtil.ts b/packages/indy-vdr/src/dids/didIndyUtil.ts index c37135071b..6644703169 100644 --- a/packages/indy-vdr/src/dids/didIndyUtil.ts +++ b/packages/indy-vdr/src/dids/didIndyUtil.ts @@ -9,7 +9,6 @@ import { KeyType, TypedArrayEncoder, } from '@aries-framework/core' -import { mergeWith, compact, transform, isUndefined, isEqual, isObject, isArray } from 'lodash' import { DID_INDY_REGEX } from '../utils/did' @@ -45,6 +44,42 @@ export function parseIndyDid(did: string) { } } +const deepMerge = (a: Record, b: Record) => { + const output: Record = {} + + ;[...new Set([...Object.keys(a), ...Object.keys(b)])].forEach((key) => { + // Only an object includes a given key: just output it + if (a[key] && !b[key]) { + output[key] = a[key] + } else if (!a[key] && b[key]) { + output[key] = b[key] + } else { + // Both objects do include the key + // Some or both are arrays + if (Array.isArray(a[key])) { + if (Array.isArray(b[key])) { + const element = new Set() + ;(a[key] as Array).forEach((item: unknown) => element.add(item)) + ;(b[key] as Array).forEach((item: unknown) => element.add(item)) + output[key] = Array.from(element) + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const arr = a[key] as Array + output[key] = Array.from(new Set(...arr, b[key])) + } + } else if (Array.isArray(b[key])) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const arr = b[key] as Array + output[key] = Array.from(new Set(...arr, a[key])) + // Both elements are objects: recursive merge + } else if (typeof a[key] == 'object' && typeof b[key] == 'object') { + output[key] = deepMerge(a, b) + } + } + }) + return output +} + /** * Combine a JSON content with the contents of a DidDocument * @param didDoc object containing original DIDDocument @@ -54,28 +89,48 @@ export function parseIndyDid(did: string) { */ export function combineDidDocumentWithJson(didDoc: DidDocument, json: Record) { const didDocJson = didDoc.toJSON() - const combinedJson = mergeWith(didDocJson, json, (objValue: unknown, srcValue: unknown) => { - if (Array.isArray(objValue) && Array.isArray(srcValue)) { - return [...new Set([...objValue, ...srcValue])] - } - }) - + const combinedJson = deepMerge(didDocJson, json) return JsonTransformer.fromJSON(combinedJson, DidDocument) } -export function deepObjectDiff(object: any, base: any) { - function changes(object: any, base: any) { - return transform(object, function (result: any, value: any, key: any) { - if (isUndefined(base[key])) { - result[key] = value - } else { - if (!isEqual(value, base[key])) { - result[key] = isObject(value) || isArray(value) ? compact(changes(value, base[key])) : value +/** + * Processes the difference between a base DidDocument and a complete DidDocument + * + * Note: it does deep comparison based only on "id" field to determine whether is + * the same object or is a different one + * + * @param extra complete DidDocument + * @param base base DidDocument + * @returns diff object + */ +export function didDocDiff(extra: Record, base: Record) { + const output: Record = {} + for (const key in extra) { + if (!(key in base)) { + output[key] = extra[key] + } else { + // They are arrays: compare elements + if (Array.isArray(extra[key]) && Array.isArray(base[key])) { + // Different types: return the extra + output[key] = [] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const baseAsArray = base[key] as Array + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const extraAsArray = extra[key] as Array + for (const element of extraAsArray) { + if (!baseAsArray.find((item) => item.id === element.id)) { + ;(output[key] as Array).push(element) + } } + } // They are both objects: do recursive diff + else if (typeof extra[key] == 'object' && typeof base[key] == 'object') { + output[key] = didDocDiff(extra[key] as Record, base[key] as Record) + } else { + output[key] = extra[key] } - }) + } } - return changes(object, base) + return output } /** diff --git a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts index c06869c913..5b21638448 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts @@ -20,10 +20,13 @@ import testLogger from '../../core/tests/logger' import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' import { IndyVdrIndyDidResolver } from '../src/dids/IndyVdrIndyDidResolver' import { indyDidFromNamespaceAndInitialKey } from '../src/dids/didIndyUtil' +// eslint-disable-next-line import/order import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' import '@hyperledger/aries-askar-nodejs' +import { DID_INDY_REGEX } from '../src/utils/did' + import { indyVdrModuleConfig } from './helpers' const logger = testLogger @@ -74,7 +77,7 @@ describe('Indy VDR registrar E2E', () => { }) test('can register a did:indy without services', async () => { - const result = await indyVdrIndyDidRegistrar.create(agentContext, { + const didRegistrationResult = await indyVdrIndyDidRegistrar.create(agentContext, { method: 'indy', options: { submitterDid: 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt', @@ -82,14 +85,44 @@ describe('Indy VDR registrar E2E', () => { }, }) - const did = result.didState.did + expect(JsonTransformer.toJSON(didRegistrationResult)).toMatchObject({ + didDocumentMetadata: { + qualifiedIndyDid: expect.stringMatching(DID_INDY_REGEX), + }, + didRegistrationMetadata: { + didIndyNamespace: 'pool:localtest', + }, + didState: { + state: 'finished', + did: expect.stringMatching(DID_INDY_REGEX), + didDocument: { + '@context': ['https://w3id.org/did/v1'], + id: expect.stringMatching(DID_INDY_REGEX), + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: expect.stringMatching(DID_INDY_REGEX), + id: expect.stringContaining('#verkey'), + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [expect.stringContaining('#verkey')], + service: undefined, + }, + }, + }) + const did = didRegistrationResult.didState.did if (!did) { throw Error('did not defined') } - const didResult = await indyVdrIndyDidResolver.resolve(agentContext, did) - expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + const didResolutionResult = await indyVdrIndyDidResolver.resolve(agentContext, did) + expect(JsonTransformer.toJSON(didResolutionResult)).toMatchObject({ didDocument: { '@context': ['https://w3id.org/did/v1'], id: did, @@ -129,7 +162,7 @@ describe('Indy VDR registrar E2E', () => { 'pool:localtest', Key.fromPublicKey(keyPair.publicKey, KeyType.Ed25519) ) - const result = await indyVdrIndyDidRegistrar.create(agentContext, { + const didRegistrationResult = await indyVdrIndyDidRegistrar.create(agentContext, { method: 'indy', did, options: { @@ -139,11 +172,36 @@ describe('Indy VDR registrar E2E', () => { }, }) - const receivedDid = result.didState.did - - if (!receivedDid) { - throw Error('did not defined') - } + expect(JsonTransformer.toJSON(didRegistrationResult)).toMatchObject({ + didDocumentMetadata: { + qualifiedIndyDid: did, + }, + didRegistrationMetadata: { + didIndyNamespace: 'pool:localtest', + }, + didState: { + state: 'finished', + did, + didDocument: { + '@context': ['https://w3id.org/did/v1'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: undefined, + }, + }, + }) const didResult = await indyVdrIndyDidResolver.resolve(agentContext, did) expect(JsonTransformer.toJSON(didResult)).toMatchObject({ @@ -188,7 +246,7 @@ describe('Indy VDR registrar E2E', () => { Key.fromPublicKey(key.publicKey, KeyType.Ed25519) ) - const result = await indyVdrIndyDidRegistrar.create(agentContext, { + const didRegistrationResult = await indyVdrIndyDidRegistrar.create(agentContext, { method: 'indy', did, options: { @@ -208,6 +266,7 @@ describe('Indy VDR registrar E2E', () => { recipientKeys: [`${did}#key-agreement-1`], routingKeys: ['a-routing-key'], serviceEndpoint: 'https://example.com/endpoint', + accept: ['didcomm/aip2;env=rfc19'], }), new DidCommV2Service({ accept: ['didcomm/v2'], @@ -219,62 +278,70 @@ describe('Indy VDR registrar E2E', () => { }, }) - const services = [ - { - id: `${did}#endpoint`, - serviceEndpoint: 'https://example.com/endpoint', - type: 'endpoint', + const expectedDidDocument = { + '@context': ['https://w3id.org/did/v1', 'https://didcomm.org/messaging/contexts/v2'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + { + type: 'X25519KeyAgreementKey2019', + controller: did, + id: `${did}#key-agreement-1`, + publicKeyBase58: x25519PublicKeyBase58, + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: [ + { + id: `${did}#endpoint`, + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }, + { + accept: ['didcomm/aip2;env=rfc19'], + id: `${did}#did-communication`, + priority: 0, + recipientKeys: [`${did}#key-agreement-1`], + routingKeys: ['a-routing-key'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + { + accept: ['didcomm/v2'], + id: `${did}#didcomm-1`, + routingKeys: ['a-routing-key'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'DIDComm', + }, + ], + } + + expect(JsonTransformer.toJSON(didRegistrationResult)).toMatchObject({ + didDocumentMetadata: { + qualifiedIndyDid: did, }, - { - accept: ['didcomm/aip2;env=rfc19'], - id: `${did}#did-communication`, - priority: 0, - recipientKeys: [`${did}#key-agreement-1`], - routingKeys: ['a-routing-key'], - serviceEndpoint: 'https://example.com/endpoint', - type: 'did-communication', + didRegistrationMetadata: { + didIndyNamespace: 'pool:localtest', }, - { - accept: ['didcomm/v2'], - id: `${did}#didcomm-1`, - routingKeys: ['a-routing-key'], - serviceEndpoint: 'https://example.com/endpoint', - type: 'DIDComm', + didState: { + state: 'finished', + did, + didDocument: expectedDidDocument, }, - ] - - const receivedDid = result.didState.did - - if (!receivedDid) { - throw Error('did not defined') - } + }) const didResult = await indyVdrIndyDidResolver.resolve(agentContext, did) expect(JsonTransformer.toJSON(didResult)).toMatchObject({ - didDocument: { - '@context': ['https://w3id.org/did/v1', 'https://didcomm.org/messaging/contexts/v2'], - id: did, - alsoKnownAs: undefined, - controller: undefined, - verificationMethod: [ - { - type: 'Ed25519VerificationKey2018', - controller: did, - id: `${did}#verkey`, - publicKeyBase58: ed25519PublicKeyBase58, - }, - { - type: 'X25519KeyAgreementKey2019', - controller: did, - id: `${did}#key-agreement-1`, - publicKeyBase58: x25519PublicKeyBase58, - }, - ], - capabilityDelegation: undefined, - capabilityInvocation: undefined, - authentication: [`${did}#verkey`], - service: services, - }, + didDocument: expectedDidDocument, didDocumentMetadata: {}, didResolutionMetadata: { contentType: 'application/did+ld+json', diff --git a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts index 32999a22fa..c3b0eaacbe 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts @@ -1,8 +1,7 @@ import type { Key } from '@aries-framework/core' -import { AskarWallet } from '@aries-framework/askar' import { - TypedArrayEncoder, + IndyWallet, CacheModuleConfig, InMemoryLruCache, JsonTransformer, @@ -19,10 +18,8 @@ import { indyDidFromPublicKeyBase58 } from '../src/utils/did' import { createDidOnLedger, indyVdrModuleConfig } from './helpers' -import '@hyperledger/aries-askar-nodejs' - const logger = testLogger -const wallet = new AskarWallet(logger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) +const wallet = new IndyWallet(agentDependencies, logger, new SigningProviderRegistry([])) const agentConfig = getAgentConfig('IndyVdrResolver E2E', { logger }) const cache = new InMemoryLruCache({ limit: 200 }) @@ -50,7 +47,7 @@ describe('indy-vdr DID Resolver E2E', () => { } signerKey = await wallet.createKey({ - secretKey: TypedArrayEncoder.fromString('000000000000000000000000Trustee9'), + seed: '000000000000000000000000Trustee9', keyType: KeyType.Ed25519, }) }) diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts index c096a0cf69..de7517cc01 100644 --- a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -1,7 +1,6 @@ import type { Key } from '@aries-framework/core' -import { AskarWallet } from '@aries-framework/askar' -import { TypedArrayEncoder, KeyType, SigningProviderRegistry } from '@aries-framework/core' +import { IndyWallet, KeyType, SigningProviderRegistry } from '@aries-framework/core' import { GetNymRequest, NymRequest, SchemaRequest, CredentialDefinitionRequest } from '@hyperledger/indy-vdr-shared' import { agentDependencies, genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' @@ -14,7 +13,7 @@ import { indyVdrModuleConfig } from './helpers' import '@hyperledger/aries-askar-nodejs' const indyVdrPoolService = new IndyVdrPoolService(testLogger, indyVdrModuleConfig) -const wallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) +const wallet = new IndyWallet(agentDependencies, testLogger, new SigningProviderRegistry([])) const agentConfig = getAgentConfig('IndyVdrPoolService') const agentContext = getAgentContext({ wallet, agentConfig }) @@ -37,7 +36,7 @@ describe('IndyVdrPoolService', () => { } signerKey = await wallet.createKey({ - secretKey: TypedArrayEncoder.fromString('000000000000000000000000Trustee9'), + seed: '000000000000000000000000Trustee9', keyType: KeyType.Ed25519, }) }) diff --git a/yarn.lock b/yarn.lock index 3e1fab901f..0a86c85b57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2585,11 +2585,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@^4.14.191": - version "4.14.191" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" - integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== - "@types/luxon@^1.27.0": version "1.27.1" resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.27.1.tgz#aceeb2d5be8fccf541237e184e37ecff5faa9096" From 8c302883aff49888ba33f04c1fec998e79ed92a5 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 14 Feb 2023 12:27:14 -0300 Subject: [PATCH 36/41] fix: remove usage of askar in indy-vdr. Revert changes in AskarWallet Signed-off-by: Ariel Gentile --- packages/askar/src/wallet/AskarWallet.ts | 12 ++---------- packages/indy-vdr/package.json | 2 -- .../tests/indy-vdr-did-registrar.e2e.test.ts | 14 ++++++-------- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index ef9d41fbe9..b0113f585d 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -355,17 +355,13 @@ export class AskarWallet implements Wallet { * @throws {WalletError} When an unsupported keytype is requested * @throws {WalletError} When the key could not be created */ - public async createKey({ seed, secretKey, keyType }: AskarWalletCreateKeyOptions): Promise { + public async createKey({ seed, keyType }: WalletCreateKeyOptions): Promise { try { if (keyTypeSupportedByAskar(keyType)) { const algorithm = keyAlgFromString(keyType) // Create key from seed - const key = secretKey - ? AskarKey.fromSecretBytes({ secretKey, algorithm }) - : seed - ? AskarKey.fromSeed({ seed: Buffer.from(seed), algorithm }) - : AskarKey.generate(algorithm) + const key = seed ? AskarKey.fromSeed({ seed: Buffer.from(seed), algorithm }) : AskarKey.generate(algorithm) // Store key await this.session.insertKey({ key, name: encodeToBase58(key.publicBytes) }) @@ -752,7 +748,3 @@ export class AskarWallet implements Wallet { } } } - -export interface AskarWalletCreateKeyOptions extends WalletCreateKeyOptions { - secretKey?: Uint8Array -} diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json index 15cd719ca1..fa1c9d3e39 100644 --- a/packages/indy-vdr/package.json +++ b/packages/indy-vdr/package.json @@ -29,8 +29,6 @@ "@hyperledger/indy-vdr-shared": "^0.1.0-dev.4" }, "devDependencies": { - "@aries-framework/askar": "0.3.3", - "@hyperledger/aries-askar-nodejs": "^0.1.0-dev.1", "@hyperledger/indy-vdr-nodejs": "^0.1.0-dev.4", "@stablelib/ed25519": "^1.0.2", "rimraf": "^4.0.7", diff --git a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts index 5b21638448..11970321ec 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts @@ -1,4 +1,3 @@ -import { AskarStorageService, AskarWallet } from '@aries-framework/askar' import { Key, InjectionSymbols, @@ -11,26 +10,24 @@ import { DidCommV1Service, DidCommV2Service, DidDocumentService, + IndyWallet, } from '@aries-framework/core' import { convertPublicKeyToX25519, generateKeyPairFromSeed } from '@stablelib/ed25519' import { Subject } from 'rxjs' +import { IndyStorageService } from '../../core/src/storage/IndyStorageService' import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' import { IndyVdrIndyDidResolver } from '../src/dids/IndyVdrIndyDidResolver' import { indyDidFromNamespaceAndInitialKey } from '../src/dids/didIndyUtil' -// eslint-disable-next-line import/order import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' - -import '@hyperledger/aries-askar-nodejs' - import { DID_INDY_REGEX } from '../src/utils/did' import { indyVdrModuleConfig } from './helpers' const logger = testLogger -const wallet = new AskarWallet(logger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) +const wallet = new IndyWallet(agentDependencies, logger, new SigningProviderRegistry([])) const agentConfig = getAgentConfig('IndyVdrIndyDidRegistrar E2E', { logger }) @@ -46,7 +43,7 @@ const agentContext = getAgentContext({ registerInstances: [ [InjectionSymbols.Stop$, new Subject()], [InjectionSymbols.AgentDependencies, agentDependencies], - [InjectionSymbols.StorageService, new AskarStorageService()], + [InjectionSymbols.StorageService, new IndyStorageService(agentDependencies)], [IndyVdrPoolService, new IndyVdrPoolService(logger, indyVdrModuleConfig)], [CacheModuleConfig, new CacheModuleConfig({ cache })], ], @@ -63,7 +60,7 @@ describe('Indy VDR registrar E2E', () => { } signerKey = await wallet.createKey({ - secretKey: TypedArrayEncoder.fromString('000000000000000000000000Trustee9'), + seed: '000000000000000000000000Trustee9', keyType: KeyType.Ed25519, }) }) @@ -172,6 +169,7 @@ describe('Indy VDR registrar E2E', () => { }, }) + console.log(`${JSON.stringify(didRegistrationResult)}`) expect(JsonTransformer.toJSON(didRegistrationResult)).toMatchObject({ didDocumentMetadata: { qualifiedIndyDid: did, From 693ddd306160c6ab78f06300f6f7b5585ab69dea Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 14 Feb 2023 12:43:36 -0300 Subject: [PATCH 37/41] accept abbreviated verkeys in did:indy resolver Signed-off-by: Ariel Gentile --- packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts | 10 +++++++--- .../src/dids/__tests__/IndyVdrIndyDidResolver.test.ts | 3 --- .../indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts | 1 - 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts index 05ede7c069..6f6d40cbcf 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts @@ -7,7 +7,7 @@ import { IndyVdrError, IndyVdrNotFoundError } from '../error' import { IndyVdrPoolService } from '../pool' import { combineDidDocumentWithJson, createKeyAgreementKey, indyDidDocumentFromDid, parseIndyDid } from './didIndyUtil' -import { addServicesFromEndpointsAttrib } from './didSovUtil' +import { getFullVerkey, addServicesFromEndpointsAttrib } from './didSovUtil' export class IndyVdrIndyDidResolver implements DidResolver { public readonly supportedMethods = ['indy'] @@ -39,8 +39,12 @@ export class IndyVdrIndyDidResolver implements DidResolver { private async buildDidDocument(agentContext: AgentContext, getNymResponseData: GetNymResponseData, did: string) { // Create base Did Document - // We assume that verkey from GET_NYM is always a full verkey in base58 - const builder = indyDidDocumentFromDid(did, getNymResponseData.verkey) + + // For modern did:indy DIDs, we assume that GET_NYM is always a full verkey in base58. + // For backwards compatibility, we accept a shortened verkey and convert it using previous convention + const verkey = getFullVerkey(did, getNymResponseData.verkey) + + const builder = indyDidDocumentFromDid(did, verkey) // If GET_NYM does not return any diddocContent, fallback to legacy GET_ATTRIB endpoint if (!getNymResponseData.diddocContent) { diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts index 1b31e390e3..e37266f5c7 100644 --- a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidResolver.test.ts @@ -119,9 +119,6 @@ describe('IndyVdrIndyDidResolver', () => { const result = await resolver.resolve(agentContext, did) - // TODO: Check DIDDocument context, as currently we are forcing it to have at least https://w3id.org/did/v1, - // while did:indy method spec does not enforce any context - expect(JsonTransformer.toJSON(result)).toMatchObject({ didDocument: didIndyWJz9mHyW9BZksioQnRsrAoFixture, didDocumentMetadata: {}, diff --git a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts index 11970321ec..1e02e73242 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts @@ -169,7 +169,6 @@ describe('Indy VDR registrar E2E', () => { }, }) - console.log(`${JSON.stringify(didRegistrationResult)}`) expect(JsonTransformer.toJSON(didRegistrationResult)).toMatchObject({ didDocumentMetadata: { qualifiedIndyDid: did, From 6262190076e830811db110431465f3c5da4903ae Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 14 Feb 2023 12:51:55 -0300 Subject: [PATCH 38/41] revert file Signed-off-by: Ariel Gentile --- packages/askar/src/wallet/AskarWallet.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index b0113f585d..c84370cadf 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -399,6 +399,7 @@ export class AskarWallet implements Wallet { if (!TypedArrayEncoder.isTypedArray(data)) { throw new WalletError(`Currently not supporting signing of multiple messages`) } + const keyEntry = await this.session.fetchKey({ name: key.publicKeyBase58 }) if (!keyEntry) { From 86c576d06d149eb95f6c81b70aaec40d2a523571 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 14 Feb 2023 13:21:25 -0300 Subject: [PATCH 39/41] fix: remove unused import Signed-off-by: Ariel Gentile --- packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts index de7517cc01..2ea85b5e04 100644 --- a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -10,7 +10,6 @@ import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' import { indyDidFromPublicKeyBase58 } from '../src/utils/did' import { indyVdrModuleConfig } from './helpers' -import '@hyperledger/aries-askar-nodejs' const indyVdrPoolService = new IndyVdrPoolService(testLogger, indyVdrModuleConfig) const wallet = new IndyWallet(agentDependencies, testLogger, new SigningProviderRegistry([])) From 2cfba555e72041ef538955912d54d09529a0a015 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 14 Feb 2023 17:35:02 -0300 Subject: [PATCH 40/41] fix: remove useless didDocumentMetadata Signed-off-by: Ariel Gentile --- .../indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts | 4 +--- .../tests/indy-vdr-did-registrar.e2e.test.ts | 12 +++--------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts index 257acf4459..7505c09280 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -173,9 +173,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { await didRepository.save(agentContext, didRecord) return { - didDocumentMetadata: { - qualifiedIndyDid: did, - }, + didDocumentMetadata: {}, didRegistrationMetadata: { didIndyNamespace: namespace, }, diff --git a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts index 1e02e73242..6b24cc5c82 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts @@ -83,9 +83,7 @@ describe('Indy VDR registrar E2E', () => { }) expect(JsonTransformer.toJSON(didRegistrationResult)).toMatchObject({ - didDocumentMetadata: { - qualifiedIndyDid: expect.stringMatching(DID_INDY_REGEX), - }, + didDocumentMetadata: {}, didRegistrationMetadata: { didIndyNamespace: 'pool:localtest', }, @@ -170,9 +168,7 @@ describe('Indy VDR registrar E2E', () => { }) expect(JsonTransformer.toJSON(didRegistrationResult)).toMatchObject({ - didDocumentMetadata: { - qualifiedIndyDid: did, - }, + didDocumentMetadata: {}, didRegistrationMetadata: { didIndyNamespace: 'pool:localtest', }, @@ -323,9 +319,7 @@ describe('Indy VDR registrar E2E', () => { } expect(JsonTransformer.toJSON(didRegistrationResult)).toMatchObject({ - didDocumentMetadata: { - qualifiedIndyDid: did, - }, + didDocumentMetadata: {}, didRegistrationMetadata: { didIndyNamespace: 'pool:localtest', }, From 22abbff2a82dcdd135ef3901b02247e2a16c4072 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 14 Feb 2023 17:53:26 -0300 Subject: [PATCH 41/41] fix: expose did:indy registrar and resolver Signed-off-by: Ariel Gentile --- packages/indy-vdr/src/dids/index.ts | 2 ++ packages/indy-vdr/src/index.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/indy-vdr/src/dids/index.ts b/packages/indy-vdr/src/dids/index.ts index 7f9973684d..224dcb9d13 100644 --- a/packages/indy-vdr/src/dids/index.ts +++ b/packages/indy-vdr/src/dids/index.ts @@ -1 +1,3 @@ +export { IndyVdrIndyDidRegistrar } from './IndyVdrIndyDidRegistrar' +export { IndyVdrIndyDidResolver } from './IndyVdrIndyDidResolver' export { IndyVdrSovDidResolver } from './IndyVdrSovDidResolver' diff --git a/packages/indy-vdr/src/index.ts b/packages/indy-vdr/src/index.ts index be45d47b96..fb687ae77f 100644 --- a/packages/indy-vdr/src/index.ts +++ b/packages/indy-vdr/src/index.ts @@ -1,4 +1,4 @@ -export { IndyVdrSovDidResolver } from './dids' +export { IndyVdrIndyDidRegistrar, IndyVdrIndyDidResolver, IndyVdrSovDidResolver } from './dids' export * from './IndyVdrModule' export * from './IndyVdrModuleConfig' export * from './anoncreds'