diff --git a/demo/README.md b/demo/README.md
new file mode 100644
index 0000000..5bd3fd7
--- /dev/null
+++ b/demo/README.md
@@ -0,0 +1,87 @@
+
DEMO
+
+This is the Credo demo. Walk through the Credo flow yourself together with agents Alice and Faber.
+
+Alice, a former student of Faber College, connects with the College, is issued a credential about her degree and then is asked by the College for a proof.
+
+## Features
+
+- ✅ Creating a connection
+- ✅ Offering a credential
+- ✅ Requesting a proof
+- ✅ Sending basic messages
+
+## Getting Started
+
+### Platform Specific Setup
+
+In order to run the Credo demo, you need to make sure you have Node.JS and PNPM installed. See the [Credo Prerequisites](https://credo.js.org/guides/getting-started/prerequisites) for more information.
+
+### Run the demo
+
+These are the steps for running the Credo demo:
+
+Clone the Credo git repository:
+
+```sh
+git clone https://github.com/openwallet-foundation/credo-ts.git
+```
+
+Open two different terminals next to each other and in both, go to the demo folder:
+
+```sh
+cd credo-ts/demo
+```
+
+Install the project in one of the terminals:
+
+```sh
+pnpm install
+```
+
+In the left terminal run Alice:
+
+```sh
+pnpm alice
+```
+
+In the right terminal run Faber:
+
+```sh
+pnpm faber
+```
+
+### Usage
+
+To set up a connection:
+
+- Select 'receive connection invitation' in Alice and 'create connection invitation' in Faber
+- Faber will print a invitation link which you then copy and paste to Alice
+- You have now set up a connection!
+
+To offer a credential:
+
+- Select 'offer credential' in Faber
+- Faber will start with registering a schema and the credential definition accordingly
+- You have now send a credential offer to Alice!
+- Go to Alice to accept the incoming credential offer by selecting 'yes'.
+
+To request a proof:
+
+- Select 'request proof' in Faber
+- Faber will create a new proof attribute and will then send a proof request to Alice!
+- Go to Alice to accept the incoming proof request
+
+To send a basic message:
+
+- Select 'send message' in either one of the Agents
+- Type your message and press enter
+- Message sent!
+
+Exit:
+
+- Select 'exit' to shutdown the agent.
+
+Restart:
+
+- Select 'restart', to shutdown the current agent and start a new one
diff --git a/demo/package.json b/demo/package.json
new file mode 100644
index 0000000..e9a36cd
--- /dev/null
+++ b/demo/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "credo-demo",
+ "version": "1.0.0",
+ "private": true,
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/openwallet-foundation/credo-ts",
+ "directory": "demo/"
+ },
+ "license": "Apache-2.0",
+ "scripts": {
+ "holder": "ts-node src/HolderInquirer.ts",
+ "issuer": "ts-node src/IssuerInquirer.ts",
+ "index": "ts-node src/Index.ts"
+ },
+ "dependencies": {
+ "@hyperledger/indy-vdr-nodejs": "^0.2.2",
+ "@hyperledger/anoncreds-nodejs": "^0.2.2",
+ "@openwallet-foundation/askar-nodejs": "^0.3.1",
+ "inquirer": "^8.2.5"
+ },
+ "devDependencies": {
+ "@credo-ts/anoncreds": "workspace:*",
+ "@credo-ts/askar": "workspace:*",
+ "@credo-ts/core": "workspace:*",
+ "@credo-ts/didcomm": "workspace:*",
+ "@credo-ts/indy-vdr": "workspace:*",
+ "@credo-ts/cheqd": "workspace:*",
+ "@credo-ts/node": "workspace:*",
+ "@types/figlet": "^1.5.4",
+ "@types/inquirer": "^8.2.6",
+ "clear": "^0.1.0",
+ "figlet": "^1.5.2",
+ "ts-node": "^10.9.2"
+ }
+}
diff --git a/demo/src/BaseAgent.ts b/demo/src/BaseAgent.ts
new file mode 100644
index 0000000..297b77b
--- /dev/null
+++ b/demo/src/BaseAgent.ts
@@ -0,0 +1,151 @@
+import type { InitConfig } from '@credo-ts/core'
+import type { DidCommModuleConfigOptions } from '@credo-ts/didcomm'
+import type { IndyVdrPoolConfig } from '@credo-ts/indy-vdr'
+
+import {
+ AnonCredsCredentialFormatService,
+ AnonCredsModule,
+ AnonCredsProofFormatService,
+ LegacyIndyCredentialFormatService,
+ LegacyIndyProofFormatService,
+ V1CredentialProtocol,
+ V1ProofProtocol,
+} from '@credo-ts/anoncreds'
+import { AskarModule } from '@credo-ts/askar'
+import {
+ CheqdAnonCredsRegistry,
+ CheqdDidRegistrar,
+ CheqdDidResolver,
+ CheqdModule,
+ CheqdModuleConfig,
+} from '@credo-ts/cheqd'
+import { DidsModule, Agent, ConsoleLogger, LogLevel } from '@credo-ts/core'
+import {
+ ConnectionsModule,
+ V2ProofProtocol,
+ V2CredentialProtocol,
+ ProofsModule,
+ AutoAcceptProof,
+ AutoAcceptCredential,
+ CredentialsModule,
+ HttpOutboundTransport,
+ getDefaultDidcommModules,
+} from '@credo-ts/didcomm'
+import { IndyVdrIndyDidResolver, IndyVdrAnonCredsRegistry, IndyVdrModule } from '@credo-ts/indy-vdr'
+import { agentDependencies, HttpInboundTransport } from '@credo-ts/node'
+import { anoncreds } from '@hyperledger/anoncreds-nodejs'
+import { indyVdr } from '@hyperledger/indy-vdr-nodejs'
+import { askar } from '@openwallet-foundation/askar-nodejs'
+
+import { greenText } from './OutputClass'
+
+const bcovrin = `{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blskey":"4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba","blskey_pop":"RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1","client_ip":"10.133.133.1","client_port":9702,"node_ip":"10.133.133.1","node_port":9701,"services":["VALIDATOR"]},"dest":"Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv"},"metadata":{"from":"Th7MpTaRZVRYnPiabds81Y"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62"},"ver":"1"}
+{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node2","blskey":"37rAPpXVoxzKhz7d9gkUe52XuXryuLXoM6P6LbWDB7LSbG62Lsb33sfG7zqS8TK1MXwuCHj1FKNzVpsnafmqLG1vXN88rt38mNFs9TENzm4QHdBzsvCuoBnPH7rpYYDo9DZNJePaDvRvqJKByCabubJz3XXKbEeshzpz4Ma5QYpJqjk","blskey_pop":"Qr658mWZ2YC8JXGXwMDQTzuZCWF7NK9EwxphGmcBvCh6ybUuLxbG65nsX4JvD4SPNtkJ2w9ug1yLTj6fgmuDg41TgECXjLCij3RMsV8CwewBVgVN67wsA45DFWvqvLtu4rjNnE9JbdFTc1Z4WCPA3Xan44K1HoHAq9EVeaRYs8zoF5","client_ip":"10.133.133.2","client_port":9704,"node_ip":"10.133.133.2","node_port":9703,"services":["VALIDATOR"]},"dest":"8ECVSk179mjsjKRLWiQtssMLgp6EPhWXtaYyStWPSGAb"},"metadata":{"from":"EbP4aYNeTHL6q385GuVpRV"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"1ac8aece2a18ced660fef8694b61aac3af08ba875ce3026a160acbc3a3af35fc"},"ver":"1"}
+{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node3","blskey":"3WFpdbg7C5cnLYZwFZevJqhubkFALBfCBBok15GdrKMUhUjGsk3jV6QKj6MZgEubF7oqCafxNdkm7eswgA4sdKTRc82tLGzZBd6vNqU8dupzup6uYUf32KTHTPQbuUM8Yk4QFXjEf2Usu2TJcNkdgpyeUSX42u5LqdDDpNSWUK5deC5","blskey_pop":"QwDeb2CkNSx6r8QC8vGQK3GRv7Yndn84TGNijX8YXHPiagXajyfTjoR87rXUu4G4QLk2cF8NNyqWiYMus1623dELWwx57rLCFqGh7N4ZRbGDRP4fnVcaKg1BcUxQ866Ven4gw8y4N56S5HzxXNBZtLYmhGHvDtk6PFkFwCvxYrNYjh","client_ip":"10.133.133.3","client_port":9706,"node_ip":"10.133.133.3","node_port":9705,"services":["VALIDATOR"]},"dest":"DKVxG2fXXTU8yT5N7hGEbXB3dfdAnYv1JczDUHpmDxya"},"metadata":{"from":"4cU41vWW82ArfxJxHkzXPG"},"type":"0"},"txnMetadata":{"seqNo":3,"txnId":"7e9f355dffa78ed24668f0e0e369fd8c224076571c51e2ea8be5f26479edebe4"},"ver":"1"}
+{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node4","blskey":"2zN3bHM1m4rLz54MJHYSwvqzPchYp8jkHswveCLAEJVcX6Mm1wHQD1SkPYMzUDTZvWvhuE6VNAkK3KxVeEmsanSmvjVkReDeBEMxeDaayjcZjFGPydyey1qxBHmTvAnBKoPydvuTAqx5f7YNNRAdeLmUi99gERUU7TD8KfAa6MpQ9bw","blskey_pop":"RPLagxaR5xdimFzwmzYnz4ZhWtYQEj8iR5ZU53T2gitPCyCHQneUn2Huc4oeLd2B2HzkGnjAff4hWTJT6C7qHYB1Mv2wU5iHHGFWkhnTX9WsEAbunJCV2qcaXScKj4tTfvdDKfLiVuU2av6hbsMztirRze7LvYBkRHV3tGwyCptsrP","client_ip":"10.133.133.4","client_port":9708,"node_ip":"10.133.133.4","node_port":9707,"services":["VALIDATOR"]},"dest":"4PS3EDQ3dW1tci1Bp6543CfuuebjFrg36kLAUcskGfaA"},"metadata":{"from":"TWwCRQRZ2ZHMJFn9TzLp7W"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"aa5e817d7cc626170eca175822029339a444eb0ee8f0bd20d3b0b76e566fb008"},"ver":"1"}`
+
+export const indyNetworkConfig = {
+ genesisTransactions: bcovrin,
+ indyNamespace: 'bcovrin:test',
+ isProduction: false,
+ connectOnStartup: true,
+} satisfies IndyVdrPoolConfig
+
+type DemoAgent = Agent>
+
+export class BaseAgent {
+ public port: number
+ public name: string
+ public config: InitConfig
+ public agent: DemoAgent
+
+ public constructor({ port, name }: { port: number; name: string }) {
+ this.name = name
+ this.port = port
+
+ const config = {
+ label: name,
+ walletConfig: {
+ id: name,
+ key: name,
+ },
+ //logger: new ConsoleLogger(LogLevel.debug),
+ } satisfies InitConfig
+
+ this.config = config
+
+ this.agent = new Agent({
+ config,
+ dependencies: agentDependencies,
+ modules: getAskarAnonCredsIndyModules({ endpoints: [`http://localhost:${this.port}`] }),
+ })
+ this.agent.modules.didcomm.registerInboundTransport(new HttpInboundTransport({ port }))
+ this.agent.modules.didcomm.registerOutboundTransport(new HttpOutboundTransport())
+ }
+
+ public async initializeAgent() {
+ await this.agent.initialize()
+
+ console.log(greenText(`\nAgent ${this.name} created!\n`))
+ }
+}
+
+function getAskarAnonCredsIndyModules(didcommConfig: DidCommModuleConfigOptions) {
+ const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService()
+ const legacyIndyProofFormatService = new LegacyIndyProofFormatService()
+
+ return {
+ ...getDefaultDidcommModules(didcommConfig),
+ connections: new ConnectionsModule({
+ autoAcceptConnections: true,
+ }),
+ credentials: new CredentialsModule({
+ autoAcceptCredentials: AutoAcceptCredential.ContentApproved,
+ credentialProtocols: [
+ new V1CredentialProtocol({
+ indyCredentialFormat: legacyIndyCredentialFormatService,
+ }),
+ new V2CredentialProtocol({
+ credentialFormats: [legacyIndyCredentialFormatService, new AnonCredsCredentialFormatService()],
+ }),
+ ],
+ }),
+ proofs: new ProofsModule({
+ autoAcceptProofs: AutoAcceptProof.ContentApproved,
+ proofProtocols: [
+ new V1ProofProtocol({
+ indyProofFormat: legacyIndyProofFormatService,
+ }),
+ new V2ProofProtocol({
+ proofFormats: [legacyIndyProofFormatService, new AnonCredsProofFormatService()],
+ }),
+ ],
+ }),
+ anoncreds: new AnonCredsModule({
+ registries: [new IndyVdrAnonCredsRegistry(), new CheqdAnonCredsRegistry()],
+ anoncreds,
+ }),
+ indyVdr: new IndyVdrModule({
+ indyVdr,
+ networks: [indyNetworkConfig],
+ }),
+ cheqd: new CheqdModule(
+ new CheqdModuleConfig({
+ networks: [
+ {
+ network: 'testnet',
+ cosmosPayerSeed:
+ 'robust across amount corn curve panther opera wish toe ring bleak empower wreck party abstract glad average muffin picnic jar squeeze annual long aunt',
+ },
+ ],
+ })
+ ),
+ dids: new DidsModule({
+ resolvers: [new IndyVdrIndyDidResolver(), new CheqdDidResolver()],
+ registrars: [new CheqdDidRegistrar()],
+ }),
+ askar: new AskarModule({
+ askar,
+ }),
+ } as const
+}
diff --git a/demo/src/BaseInquirer.ts b/demo/src/BaseInquirer.ts
new file mode 100644
index 0000000..358d72b
--- /dev/null
+++ b/demo/src/BaseInquirer.ts
@@ -0,0 +1,55 @@
+import { prompt } from 'inquirer'
+
+import { Title } from './OutputClass'
+
+export enum ConfirmOptions {
+ Yes = 'yes',
+ No = 'no',
+}
+
+export class BaseInquirer {
+ public optionsInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] }
+ public inputInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] }
+
+ public constructor() {
+ this.optionsInquirer = {
+ type: 'list',
+ prefix: '',
+ name: 'options',
+ message: '',
+ choices: [],
+ }
+
+ this.inputInquirer = {
+ type: 'input',
+ prefix: '',
+ name: 'input',
+ message: '',
+ choices: [],
+ }
+ }
+
+ public inquireOptions(promptOptions: string[]) {
+ this.optionsInquirer.message = Title.OptionsTitle
+ this.optionsInquirer.choices = promptOptions
+ return this.optionsInquirer
+ }
+
+ public inquireInput(title: string) {
+ this.inputInquirer.message = title
+ return this.inputInquirer
+ }
+
+ public inquireConfirmation(title: string) {
+ this.optionsInquirer.message = title
+ this.optionsInquirer.choices = [ConfirmOptions.Yes, ConfirmOptions.No]
+ return this.optionsInquirer
+ }
+
+ public async inquireMessage() {
+ this.inputInquirer.message = Title.MessageTitle
+ const message = await prompt([this.inputInquirer])
+
+ return message.input[0] === 'q' ? null : message.input
+ }
+}
diff --git a/demo/src/Holder.ts b/demo/src/Holder.ts
new file mode 100644
index 0000000..a84d7a1
--- /dev/null
+++ b/demo/src/Holder.ts
@@ -0,0 +1,83 @@
+import type { ConnectionRecord, CredentialExchangeRecord, ProofExchangeRecord } from '@credo-ts/didcomm'
+
+import { BaseAgent } from './BaseAgent'
+import { greenText, Output, redText } from './OutputClass'
+
+export class Holder extends BaseAgent {
+ public connected: boolean
+ public connectionRecordFaberId?: string
+
+ public constructor(port: number, name: string) {
+ super({ port, name })
+ this.connected = false
+ }
+
+ public static async build(): Promise {
+ const holder = new Holder(9004, 'holder')
+ await holder.initializeAgent()
+ return holder
+ }
+
+ private async getConnectionRecord() {
+ if (!this.connectionRecordFaberId) {
+ throw Error(redText(Output.MissingConnectionRecord))
+ }
+ return await this.agent.modules.connections.getById(this.connectionRecordFaberId)
+ }
+
+ private async receiveConnectionRequest(invitationUrl: string) {
+ const { connectionRecord } = await this.agent.modules.oob.receiveInvitationFromUrl(invitationUrl)
+ if (!connectionRecord) {
+ throw new Error(redText(Output.NoConnectionRecordFromOutOfBand))
+ }
+ return connectionRecord
+ }
+
+ private async waitForConnection(connectionRecord: ConnectionRecord) {
+ connectionRecord = await this.agent.modules.connections.returnWhenIsConnected(connectionRecord.id)
+ this.connected = true
+ console.log("[HOLDER:]" + greenText(Output.ConnectionEstablished))
+ return connectionRecord.id
+ }
+
+ public async acceptConnection(invitation_url: string) {
+ const connectionRecord = await this.receiveConnectionRequest(invitation_url)
+ this.connectionRecordFaberId = await this.waitForConnection(connectionRecord)
+ }
+
+ public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord) {
+ await this.agent.modules.credentials.acceptOffer({
+ credentialRecordId: credentialRecord.id,
+ })
+
+ let [ credentials ] = await this.agent.modules.credentials.getAll();
+ console.log(credentials);
+ }
+
+ public async acceptProofRequest(proofRecord: ProofExchangeRecord) {
+ const requestedCredentials = await this.agent.modules.proofs.selectCredentialsForRequest({
+ proofRecordId: proofRecord.id,
+ })
+
+ await this.agent.modules.proofs.acceptRequest({
+ proofRecordId: proofRecord.id,
+ proofFormats: requestedCredentials.proofFormats,
+ })
+ console.log(greenText('\nProof request accepted!\n'))
+ }
+
+ public async sendMessage(message: string) {
+ const connectionRecord = await this.getConnectionRecord()
+ await this.agent.modules.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()
+ }
+}
diff --git a/demo/src/HolderInquirer.ts b/demo/src/HolderInquirer.ts
new file mode 100644
index 0000000..47add61
--- /dev/null
+++ b/demo/src/HolderInquirer.ts
@@ -0,0 +1,128 @@
+import type { CredentialExchangeRecord, ProofExchangeRecord } from '@credo-ts/didcomm'
+
+import { clear } from 'console'
+import { textSync } from 'figlet'
+import { prompt } from 'inquirer'
+
+import { Holder } from './Holder'
+import { BaseInquirer, ConfirmOptions } from './BaseInquirer'
+import { Listener } from './Listener'
+import { Title } from './OutputClass'
+
+export const runHolder = async () => {
+ clear()
+ console.log(textSync('Holder', { horizontalLayout: 'full' }))
+ const holder = await HolderInquirer.build()
+ await holder.processAnswer()
+}
+
+enum PromptOptions {
+ ReceiveConnectionUrl = 'Receive connection invitation',
+ SendMessage = 'Send message',
+ Exit = 'Exit',
+ Restart = 'Restart',
+}
+
+export class HolderInquirer extends BaseInquirer {
+ public holder: Holder
+ public promptOptionsString: string[]
+ public listener: Listener
+
+ public constructor(holder: Holder) {
+ super()
+ this.holder = holder
+ this.listener = new Listener()
+ this.promptOptionsString = Object.values(PromptOptions)
+ this.listener.messageListener(this.holder.agent, this.holder.name)
+ }
+
+ public static async build(): Promise {
+ const holder = await Holder.build()
+ return new HolderInquirer(holder)
+ }
+
+ private async getPromptChoice() {
+ if (this.holder.connectionRecordFaberId) return prompt([this.inquireOptions(this.promptOptionsString)])
+
+ const reducedOption = [PromptOptions.ReceiveConnectionUrl, PromptOptions.Exit, PromptOptions.Restart]
+ return prompt([this.inquireOptions(reducedOption)])
+ }
+
+ public async processAnswer() {
+ const choice = await this.getPromptChoice()
+ if (this.listener.on) return
+
+ switch (choice.options) {
+ case PromptOptions.ReceiveConnectionUrl:
+ await this.connection()
+ break
+ case PromptOptions.SendMessage:
+ await this.message()
+ break
+ case PromptOptions.Exit:
+ await this.exit()
+ break
+ case PromptOptions.Restart:
+ await this.restart()
+ return
+ }
+ await this.processAnswer()
+ }
+
+ public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord) {
+ const confirm = await prompt([this.inquireConfirmation(Title.CredentialOfferTitle)])
+ if (confirm.options === ConfirmOptions.No) {
+ await this.holder.agent.modules.credentials.declineOffer(credentialRecord.id)
+ } else if (confirm.options === ConfirmOptions.Yes) {
+ await this.holder.acceptCredentialOffer(credentialRecord)
+ }
+ }
+
+ public async acceptProofRequest(proofRecord: ProofExchangeRecord) {
+ const confirm = await prompt([this.inquireConfirmation(Title.ProofRequestTitle)])
+ if (confirm.options === ConfirmOptions.No) {
+ await this.holder.agent.modules.proofs.declineRequest({ proofRecordId: proofRecord.id })
+ } else if (confirm.options === ConfirmOptions.Yes) {
+ await this.holder.acceptProofRequest(proofRecord)
+ }
+ }
+
+ public async connection() {
+ const title = Title.InvitationTitle
+ const getUrl = await prompt([this.inquireInput(title)])
+ await this.holder.acceptConnection(getUrl.input)
+ if (!this.holder.connected) return
+
+ this.listener.credentialOfferListener(this.holder, this)
+ this.listener.proofRequestListener(this.holder, this)
+ }
+
+ public async message() {
+ const message = await this.inquireMessage()
+ if (!message) return
+
+ await this.holder.sendMessage(message)
+ }
+
+ public async exit() {
+ const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)])
+ if (confirm.options === ConfirmOptions.No) {
+ return
+ } else if (confirm.options === ConfirmOptions.Yes) {
+ await this.holder.exit()
+ }
+ }
+
+ public async restart() {
+ const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)])
+ if (confirm.options === ConfirmOptions.No) {
+ await this.processAnswer()
+ return
+ } else if (confirm.options === ConfirmOptions.Yes) {
+ await this.holder.restart()
+ await runHolder()
+ }
+ }
+}
+
+void runHolder()
diff --git a/demo/src/Index.ts b/demo/src/Index.ts
new file mode 100644
index 0000000..079a7b0
--- /dev/null
+++ b/demo/src/Index.ts
@@ -0,0 +1,76 @@
+import { Issuer, RegistryOptions } from './Issuer'
+import { Holder } from './Holder'
+import { Listener } from './Listener'
+import { CredentialExchangeRecord } from '@credo-ts/didcomm'
+
+function delay(ms: number) {
+ return new Promise( resolve => setTimeout(resolve, ms) );
+}
+
+export const runIndex = async () => {
+ const index = await IndexThing.build()
+ index.process()
+}
+
+export class IndexThing {
+ public issuer: Issuer;
+ public holder: Holder;
+ public listener: Listener;
+
+ public constructor(issuer: Issuer, holder: Holder)
+ {
+ this.issuer = issuer;
+ this.holder = holder;
+ this.listener = new Listener()
+ }
+
+ public static async build(): Promise
+ {
+ const issuer = await Issuer.build()
+ const holder = await Holder.build()
+ return new IndexThing(issuer, holder)
+ }
+
+
+ public async process()
+ {
+ let url = await this.issuer.createConnectionInvite()
+ console.log(url);
+
+ [await this.holderConnection(url), await this.issuer.waitForConnection()];
+ //this.issuer.waitForConnection()
+ //this.holderConnection(url)
+
+ await this.issuer.importDid(RegistryOptions.indy)
+ await this.credential()
+
+ }
+
+ public async issuerWaitConnection()
+ {
+ await this.issuer.waitForConnection()
+ }
+
+ public async credential()
+ {
+ await this.issuer.importDid(RegistryOptions.indy)
+ await this.issuer.issueCredential()
+ }
+
+ public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord)
+ {
+ await delay(10000);
+ await this.holder.acceptCredentialOffer(credentialRecord)
+ }
+
+ public async holderConnection(url: string)
+ {
+ await delay(10000);
+ await this.holder.acceptConnection(url);
+ if (!this.holder.connected) return
+
+ this.listener.credentialOfferListener(this.holder, this)
+ }
+}
+
+runIndex()
\ No newline at end of file
diff --git a/demo/src/Issuer.ts b/demo/src/Issuer.ts
new file mode 100644
index 0000000..a63f7ad
--- /dev/null
+++ b/demo/src/Issuer.ts
@@ -0,0 +1,297 @@
+import type { RegisterCredentialDefinitionReturnStateFinished } from '@credo-ts/anoncreds'
+import type { ConnectionRecord, ConnectionStateChangedEvent } from '@credo-ts/didcomm'
+import type { IndyVdrRegisterSchemaOptions, IndyVdrRegisterCredentialDefinitionOptions } from '@credo-ts/indy-vdr'
+import type BottomBar from 'inquirer/lib/ui/bottom-bar'
+
+import { KeyType, TypedArrayEncoder, utils } from '@credo-ts/core'
+import { ConnectionEventTypes } from '@credo-ts/didcomm'
+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 Issuer extends BaseAgent {
+ public outOfBandId?: string
+ public credentialDefinition?: RegisterCredentialDefinitionReturnStateFinished
+ public anonCredsIssuerId?: string
+ public ui: BottomBar
+
+ public constructor(port: number, name: string) {
+ super({ port, name })
+ this.ui = new ui.BottomBar()
+ }
+
+ public static async build(): Promise {
+ const issuer = new Issuer(9005, 'issuer')
+ await issuer.initializeAgent()
+ return issuer
+ }
+
+ 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.modules.connections.findAllByOutOfBandId(this.outOfBandId)
+
+ if (!connection) {
+ throw Error(redText(Output.MissingConnectionRecord))
+ }
+
+ return connection
+ }
+
+ private async printConnectionInvite() {
+ const outOfBand = await this.agent.modules.oob.createInvitation()
+ this.outOfBandId = outOfBand.id
+
+ console.log(
+ Output.ConnectionLink,
+ outOfBand.outOfBandInvitation.toUrl({ domain: `http://localhost:${this.port}` }),
+ '\n'
+ )
+ }
+
+ public async createConnectionInvite() {
+ const outOfBand = await this.agent.modules.oob.createInvitation()
+ this.outOfBandId = outOfBand.id
+
+ return outOfBand.outOfBandInvitation.toUrl({ domain: `http://localhost:${this.port}` })
+ }
+
+ public 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)
+ console.log("son ofa bitch")
+
+ // 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.modules.connections.findAllByOutOfBandId(outOfBandId).then(([connectionRecord]) => {
+ if (connectionRecord) {
+ clearTimeout(timeoutId)
+ resolve(connectionRecord)
+ }
+ })
+ })
+
+ const connectionRecord = await getConnectionRecord(this.outOfBandId)
+
+ try {
+ await this.agent.modules.connections.returnWhenIsConnected(connectionRecord.id)
+ } catch (e) {
+ console.log(redText(`\nTimeout of 20 seconds reached.. Returning to home screen.\n`))
+ return
+ }
+ console.log("deez")
+ 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\ndidcommThe 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: 'Issuer 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.modules.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.modules.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.modules.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()
+ }
+}
diff --git a/demo/src/IssuerInquirer.ts b/demo/src/IssuerInquirer.ts
new file mode 100644
index 0000000..324cd9b
--- /dev/null
+++ b/demo/src/IssuerInquirer.ts
@@ -0,0 +1,133 @@
+import { clear } from 'console'
+import { textSync } from 'figlet'
+import { prompt } from 'inquirer'
+
+import { BaseInquirer, ConfirmOptions } from './BaseInquirer'
+import { Issuer, RegistryOptions } from './Issuer'
+import { Listener } from './Listener'
+import { Title } from './OutputClass'
+
+export const runIssuer = async () => {
+ clear()
+ console.log(textSync('Issuer', { horizontalLayout: 'full' }))
+ const issuer = await IssuerInquirer.build()
+ await issuer.processAnswer()
+}
+
+enum PromptOptions {
+ CreateConnection = 'Create connection invitation',
+ OfferCredential = 'Offer credential',
+ RequestProof = 'Request proof',
+ SendMessage = 'Send message',
+ Exit = 'Exit',
+ Restart = 'Restart',
+}
+
+export class IssuerInquirer extends BaseInquirer {
+ public issuer: Issuer
+ public promptOptionsString: string[]
+ public listener: Listener
+
+ public constructor(issuer: Issuer) {
+ super()
+ this.issuer = issuer
+ this.listener = new Listener()
+ this.promptOptionsString = Object.values(PromptOptions)
+ this.listener.messageListener(this.issuer.agent, this.issuer.name)
+ }
+
+ public static async build(): Promise {
+ const issuer = await Issuer.build()
+ return new IssuerInquirer(issuer)
+ }
+
+ private async getPromptChoice() {
+ if (this.issuer.outOfBandId) return prompt([this.inquireOptions(this.promptOptionsString)])
+
+ const reducedOption = [PromptOptions.CreateConnection, PromptOptions.Exit, PromptOptions.Restart]
+ return prompt([this.inquireOptions(reducedOption)])
+ }
+
+ public async processAnswer() {
+ const choice = await this.getPromptChoice()
+ if (this.listener.on) return
+
+ switch (choice.options) {
+ case PromptOptions.CreateConnection:
+ await this.connection()
+ break
+ case PromptOptions.OfferCredential:
+ await this.credential()
+ return
+ case PromptOptions.RequestProof:
+ await this.proof()
+ return
+ case PromptOptions.SendMessage:
+ await this.message()
+ break
+ case PromptOptions.Exit:
+ await this.exit()
+ break
+ case PromptOptions.Restart:
+ await this.restart()
+ return
+ }
+ await this.processAnswer()
+ }
+
+ public async connection() {
+ await this.issuer.setupConnection()
+ }
+
+ public async exitUseCase(title: string) {
+ const confirm = await prompt([this.inquireConfirmation(title)])
+ if (confirm.options === ConfirmOptions.No) {
+ return false
+ } else if (confirm.options === ConfirmOptions.Yes) {
+ return true
+ }
+ }
+
+ public async credential() {
+ const registry = await prompt([this.inquireOptions([RegistryOptions.indy, RegistryOptions.cheqd])])
+ await this.issuer.importDid(registry.options)
+ await this.issuer.issueCredential()
+ const title = 'Is the credential offer accepted?'
+ await this.listener.newAcceptedPrompt(title, this)
+ }
+
+ public async proof() {
+ await this.issuer.sendProofRequest()
+ const title = 'Is the proof request accepted?'
+ await this.listener.newAcceptedPrompt(title, this)
+ }
+
+ public async message() {
+ const message = await this.inquireMessage()
+ if (!message) return
+
+ await this.issuer.sendMessage(message)
+ }
+
+ public async exit() {
+ const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)])
+ if (confirm.options === ConfirmOptions.No) {
+ return
+ } else if (confirm.options === ConfirmOptions.Yes) {
+ await this.issuer.exit()
+ }
+ }
+
+ public async restart() {
+ const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)])
+ if (confirm.options === ConfirmOptions.No) {
+ await this.processAnswer()
+ return
+ } else if (confirm.options === ConfirmOptions.Yes) {
+ await this.issuer.restart()
+ await runIssuer()
+ }
+ }
+}
+
+void runIssuer()
diff --git a/demo/src/Listener.ts b/demo/src/Listener.ts
new file mode 100644
index 0000000..a62a918
--- /dev/null
+++ b/demo/src/Listener.ts
@@ -0,0 +1,110 @@
+import type { Holder } from './Holder'
+import type { HolderInquirer } from './HolderInquirer'
+import type { Issuer } from './Issuer'
+import type { IssuerInquirer } from './IssuerInquirer'
+import type { Agent } from '@credo-ts/core'
+import type {
+ BasicMessageStateChangedEvent,
+ CredentialExchangeRecord,
+ CredentialStateChangedEvent,
+ ProofExchangeRecord,
+ ProofStateChangedEvent,
+} from '@credo-ts/didcomm'
+import type BottomBar from 'inquirer/lib/ui/bottom-bar'
+
+import {
+ BasicMessageEventTypes,
+ BasicMessageRole,
+ CredentialEventTypes,
+ CredentialState,
+ ProofEventTypes,
+ ProofState,
+} from '@credo-ts/didcomm'
+import { ui } from 'inquirer'
+
+import { Color, purpleText } from './OutputClass'
+
+export class Listener {
+ public on: boolean
+ private ui: BottomBar
+
+ public constructor() {
+ this.on = false
+ this.ui = new ui.BottomBar()
+ }
+
+ private turnListenerOn() {
+ this.on = true
+ }
+
+ private turnListenerOff() {
+ this.on = false
+ }
+
+ private printCredentialAttributes(credentialRecord: CredentialExchangeRecord) {
+ if (credentialRecord.credentialAttributes) {
+ const attribute = credentialRecord.credentialAttributes
+ console.log('\n\nCredential preview:')
+ attribute.forEach((element) => {
+ console.log(purpleText(`${element.name} ${Color.Reset}${element.value}`))
+ })
+ }
+ }
+
+ private async newCredentialPrompt(credentialRecord: CredentialExchangeRecord, aliceInquirer: HolderInquirer) {
+ this.printCredentialAttributes(credentialRecord)
+ this.turnListenerOn()
+ await aliceInquirer.acceptCredentialOffer(credentialRecord)
+ this.turnListenerOff()
+ await aliceInquirer.processAnswer()
+ }
+
+ public credentialOfferListener(alice: Holder, aliceInquirer: any) {
+ alice.agent.events.on(
+ CredentialEventTypes.CredentialStateChanged,
+ async ({ payload }: CredentialStateChangedEvent) => {
+ if (payload.credentialRecord.state === CredentialState.OfferReceived) {
+ await this.newCredentialPrompt(payload.credentialRecord, aliceInquirer)
+ }
+ }
+ )
+ }
+
+ public messageListener(agent: Agent, name: string) {
+ agent.events.on(BasicMessageEventTypes.BasicMessageStateChanged, async (event: BasicMessageStateChangedEvent) => {
+ if (event.payload.basicMessageRecord.role === BasicMessageRole.Receiver) {
+ this.ui.updateBottomBar(purpleText(`\n${name} received a message: ${event.payload.message.content}\n`))
+ }
+ })
+ }
+
+ private async newProofRequestPrompt(proofRecord: ProofExchangeRecord, aliceInquirer: HolderInquirer) {
+ this.turnListenerOn()
+ await aliceInquirer.acceptProofRequest(proofRecord)
+ this.turnListenerOff()
+ await aliceInquirer.processAnswer()
+ }
+
+ public proofRequestListener(alice: Holder, aliceInquirer: HolderInquirer) {
+ alice.agent.events.on(ProofEventTypes.ProofStateChanged, async ({ payload }: ProofStateChangedEvent) => {
+ if (payload.proofRecord.state === ProofState.RequestReceived) {
+ await this.newProofRequestPrompt(payload.proofRecord, aliceInquirer)
+ }
+ })
+ }
+
+ public proofAcceptedListener(faber: Issuer, faberInquirer: IssuerInquirer) {
+ faber.agent.events.on(ProofEventTypes.ProofStateChanged, async ({ payload }: ProofStateChangedEvent) => {
+ if (payload.proofRecord.state === ProofState.Done) {
+ await faberInquirer.processAnswer()
+ }
+ })
+ }
+
+ public async newAcceptedPrompt(title: string, faberInquirer: IssuerInquirer) {
+ this.turnListenerOn()
+ await faberInquirer.exitUseCase(title)
+ this.turnListenerOff()
+ await faberInquirer.processAnswer()
+ }
+}
diff --git a/demo/src/OutputClass.ts b/demo/src/OutputClass.ts
new file mode 100644
index 0000000..b9e69c7
--- /dev/null
+++ b/demo/src/OutputClass.ts
@@ -0,0 +1,40 @@
+export enum Color {
+ Green = `\x1b[32m`,
+ Red = `\x1b[31m`,
+ Purple = `\x1b[35m`,
+ Reset = `\x1b[0m`,
+}
+
+export enum Output {
+ NoConnectionRecordFromOutOfBand = `\nNo connectionRecord has been created from invitation\n`,
+ ConnectionEstablished = `\nConnection established!`,
+ MissingConnectionRecord = `\nNo connectionRecord ID has been set yet\n`,
+ ConnectionLink = `\nRun 'Receive connection invitation' in Alice and paste this invitation link:\n\n`,
+ Exit = 'Shutting down agent...\nExiting...',
+}
+
+export enum Title {
+ OptionsTitle = '\nOptions:',
+ InvitationTitle = '\n\nPaste the invitation url here:',
+ MessageTitle = '\n\nWrite your message here:\n(Press enter to send or press q to exit)\n',
+ ConfirmTitle = '\n\nAre you sure?',
+ CredentialOfferTitle = '\n\nCredential offer received, do you want to accept it?',
+ ProofRequestTitle = '\n\nProof request received, do you want to accept it?',
+}
+
+export const greenText = (text: string, reset?: boolean) => {
+ if (reset) return Color.Green + text + Color.Reset
+
+ return Color.Green + text
+}
+
+export const purpleText = (text: string, reset?: boolean) => {
+ if (reset) return Color.Purple + text + Color.Reset
+ return Color.Purple + text
+}
+
+export const redText = (text: string, reset?: boolean) => {
+ if (reset) return Color.Red + text + Color.Reset
+
+ return Color.Red + text
+}