import type { RegisterCredentialDefinitionReturnStateFinished } from '@aries-framework/anoncreds' import type { ConnectionRecord, ConnectionStateChangedEvent } from '@aries-framework/core' import type { IndyVdrRegisterSchemaOptions, IndyVdrRegisterCredentialDefinitionOptions, } from '@aries-framework/indy-vdr' import type BottomBar from 'inquirer/lib/ui/bottom-bar' import { ConnectionEventTypes, KeyType, TypedArrayEncoder, utils } from '@aries-framework/core' import { ui } from 'inquirer' import { BaseAgent, indyNetworkConfig } from './BaseAgent' import { Color, Output, greenText, purpleText, redText } from './OutputClass' export enum RegistryOptions { indy = 'did:indy', cheqd = 'did:cheqd', } export class Faber extends BaseAgent { public outOfBandId?: string public credentialDefinition?: RegisterCredentialDefinitionReturnStateFinished public anonCredsIssuerId?: string public ui: BottomBar public constructor(port: number, name: string) { super({ port, name, useLegacyIndySdk: true }) this.ui = new ui.BottomBar() } public static async build(): Promise { const faber = new Faber(9001, 'faber') await faber.initializeAgent() return faber } public async importDid(registry: string) { // NOTE: we assume the did is already registered on the ledger, we just store the private key in the wallet // and store the existing did in the wallet // indy did is based on private key (seed) const unqualifiedIndyDid = '2jEvRuKmfBJTRa7QowDpNN' const cheqdDid = 'did:cheqd:testnet:d37eba59-513d-42d3-8f9f-d1df0548b675' const indyDid = `did:indy:${indyNetworkConfig.indyNamespace}:${unqualifiedIndyDid}` const did = registry === RegistryOptions.indy ? indyDid : cheqdDid await this.agent.dids.import({ did, overwrite: true, privateKeys: [ { keyType: KeyType.Ed25519, privateKey: TypedArrayEncoder.fromString('afjdemoverysercure00000000000000'), }, ], }) this.anonCredsIssuerId = did } private async getConnectionRecord() { if (!this.outOfBandId) { throw Error(redText(Output.MissingConnectionRecord)) } const [connection] = await this.agent.connections.findAllByOutOfBandId(this.outOfBandId) if (!connection) { throw Error(redText(Output.MissingConnectionRecord)) } return connection } private async printConnectionInvite() { const outOfBand = await this.agent.oob.createInvitation() this.outOfBandId = outOfBand.id console.log( Output.ConnectionLink, outOfBand.outOfBandInvitation.toUrl({ domain: `http://localhost:${this.port}` }), '\n' ) } private async waitForConnection() { if (!this.outOfBandId) { throw new Error(redText(Output.MissingConnectionRecord)) } console.log('Waiting for Alice to finish connection...') const getConnectionRecord = (outOfBandId: string) => new Promise((resolve, reject) => { // Timeout of 20 seconds const timeoutId = setTimeout(() => reject(new Error(redText(Output.MissingConnectionRecord))), 20000) // Start listener this.agent.events.on(ConnectionEventTypes.ConnectionStateChanged, (e) => { if (e.payload.connectionRecord.outOfBandId !== outOfBandId) return clearTimeout(timeoutId) resolve(e.payload.connectionRecord) }) // Also retrieve the connection record by invitation if the event has already fired void this.agent.connections.findAllByOutOfBandId(outOfBandId).then(([connectionRecord]) => { if (connectionRecord) { clearTimeout(timeoutId) resolve(connectionRecord) } }) }) const connectionRecord = await getConnectionRecord(this.outOfBandId) try { await this.agent.connections.returnWhenIsConnected(connectionRecord.id) } catch (e) { console.log(redText(`\nTimeout of 20 seconds reached.. Returning to home screen.\n`)) return } console.log(greenText(Output.ConnectionEstablished)) } public async setupConnection() { await this.printConnectionInvite() await this.waitForConnection() } private printSchema(name: string, version: string, attributes: string[]) { console.log(`\n\nThe credential definition will look like this:\n`) console.log(purpleText(`Name: ${Color.Reset}${name}`)) console.log(purpleText(`Version: ${Color.Reset}${version}`)) console.log(purpleText(`Attributes: ${Color.Reset}${attributes[0]}, ${attributes[1]}, ${attributes[2]}\n`)) } private async registerSchema() { if (!this.anonCredsIssuerId) { throw new Error(redText('Missing anoncreds issuerId')) } const schemaTemplate = { name: 'Faber College' + utils.uuid(), version: '1.0.0', attrNames: ['name', 'degree', 'date'], issuerId: this.anonCredsIssuerId, } this.printSchema(schemaTemplate.name, schemaTemplate.version, schemaTemplate.attrNames) this.ui.updateBottomBar(greenText('\nRegistering schema...\n', false)) const { schemaState } = await this.agent.modules.anoncreds.registerSchema({ schema: schemaTemplate, options: { endorserMode: 'internal', endorserDid: this.anonCredsIssuerId, }, }) if (schemaState.state !== 'finished') { throw new Error( `Error registering schema: ${schemaState.state === 'failed' ? schemaState.reason : 'Not Finished'}` ) } this.ui.updateBottomBar('\nSchema registered!\n') return schemaState } private async registerCredentialDefinition(schemaId: string) { if (!this.anonCredsIssuerId) { throw new Error(redText('Missing anoncreds issuerId')) } this.ui.updateBottomBar('\nRegistering credential definition...\n') const { credentialDefinitionState } = await this.agent.modules.anoncreds.registerCredentialDefinition({ credentialDefinition: { schemaId, issuerId: this.anonCredsIssuerId, tag: 'latest', }, options: { supportRevocation: false, endorserMode: 'internal', endorserDid: this.anonCredsIssuerId, }, }) if (credentialDefinitionState.state !== 'finished') { throw new Error( `Error registering credential definition: ${ credentialDefinitionState.state === 'failed' ? credentialDefinitionState.reason : 'Not Finished' }}` ) } this.credentialDefinition = credentialDefinitionState this.ui.updateBottomBar('\nCredential definition registered!!\n') return this.credentialDefinition } public async issueCredential() { const schema = await this.registerSchema() const credentialDefinition = await this.registerCredentialDefinition(schema.schemaId) const connectionRecord = await this.getConnectionRecord() this.ui.updateBottomBar('\nSending credential offer...\n') await this.agent.credentials.offerCredential({ connectionId: connectionRecord.id, protocolVersion: 'v2', credentialFormats: { anoncreds: { attributes: [ { name: 'name', value: 'Alice Smith', }, { name: 'degree', value: 'Computer Science', }, { name: 'date', value: '01/01/2022', }, ], credentialDefinitionId: credentialDefinition.credentialDefinitionId, }, }, }) this.ui.updateBottomBar( `\nCredential offer sent!\n\nGo to the Alice agent to accept the credential offer\n\n${Color.Reset}` ) } private async printProofFlow(print: string) { this.ui.updateBottomBar(print) await new Promise((f) => setTimeout(f, 2000)) } private async newProofAttribute() { await this.printProofFlow(greenText(`Creating new proof attribute for 'name' ...\n`)) const proofAttribute = { name: { name: 'name', restrictions: [ { cred_def_id: this.credentialDefinition?.credentialDefinitionId, }, ], }, } return proofAttribute } public async sendProofRequest() { const connectionRecord = await this.getConnectionRecord() const proofAttribute = await this.newProofAttribute() await this.printProofFlow(greenText('\nRequesting proof...\n', false)) await this.agent.proofs.requestProof({ protocolVersion: 'v2', connectionId: connectionRecord.id, proofFormats: { anoncreds: { name: 'proof-request', version: '1.0', requested_attributes: proofAttribute, }, }, }) this.ui.updateBottomBar( `\nProof request sent!\n\nGo to the Alice agent to accept the proof request\n\n${Color.Reset}` ) } public async sendMessage(message: string) { const connectionRecord = await this.getConnectionRecord() await this.agent.basicMessages.sendMessage(connectionRecord.id, message) } public async exit() { console.log(Output.Exit) await this.agent.shutdown() process.exit(0) } public async restart() { await this.agent.shutdown() } }