This commit is contained in:
Win 2025-04-21 11:15:30 +07:00
parent 45d524369b
commit 9955743801
11 changed files with 1196 additions and 0 deletions

87
demo/README.md Normal file
View File

@ -0,0 +1,87 @@
<h1 align="center"><b>DEMO</b></h1>
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

36
demo/package.json Normal file
View File

@ -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"
}
}

151
demo/src/BaseAgent.ts Normal file
View File

@ -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<ReturnType<typeof getAskarAnonCredsIndyModules>>
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
}

55
demo/src/BaseInquirer.ts Normal file
View File

@ -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
}
}

83
demo/src/Holder.ts Normal file
View File

@ -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<Holder> {
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()
}
}

128
demo/src/HolderInquirer.ts Normal file
View File

@ -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<HolderInquirer> {
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()

76
demo/src/Index.ts Normal file
View File

@ -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<IndexThing>
{
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()

297
demo/src/Issuer.ts Normal file
View File

@ -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<Issuer> {
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<ConnectionRecord>((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<ConnectionStateChangedEvent>(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<IndyVdrRegisterSchemaOptions>({
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<IndyVdrRegisterCredentialDefinitionOptions>({
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()
}
}

133
demo/src/IssuerInquirer.ts Normal file
View File

@ -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<IssuerInquirer> {
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()

110
demo/src/Listener.ts Normal file
View File

@ -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()
}
}

40
demo/src/OutputClass.ts Normal file
View File

@ -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
}