Issue a Verifiable Credential

Issue a JSON-LD Verifiable Credential, signed by a did:cheqd Decentralized Identifier (DID), using Credo.

Using the Issue Credential v2 Protocol, you can issue JSON-LD Verifiable Credentials signed by a did:cheqd identifier with just a few lines of code. This guide walks through the full flow using the Credo Agent.

Prerequisites

Before you begin, ensure you have:

Step 1: Install dependencies

npm install @credo-ts/core @credo-ts/node @credo-ts/askar @credo-ts/cheqd
npm install @hyperledger/aries-askar-nodejs

Step 2: Set up the Issuer Agent

The issuer agent requires the cheqd module for DID operations and additional modules for W3C JSON-LD credential processing.

Issuer
import type { InitConfig } from '@credo-ts/core'
import { AskarModule } from '@credo-ts/askar'
import {
  Agent,
  CredentialsModule,
  V2CredentialProtocol,
  JsonLdCredentialFormatService,
  DidsModule,
  HttpOutboundTransport,
  WsOutboundTransport,
  ProofsModule,
  V2ProofProtocol,
  DifPresentationExchangeProofFormatService,
  CacheModule,
  InMemoryLruCache,
  W3cCredentialsModule,
  KeyType,
  DidDocumentBuilder,
  utils,
  getEd25519VerificationKey2018,
} from '@credo-ts/core'
import { agentDependencies, HttpInboundTransport } from '@credo-ts/node'
import { ariesAskar } from '@hyperledger/aries-askar-nodejs'
import { CheqdModule, CheqdModuleConfig, CheqdDidRegistrar, CheqdDidResolver } from '@credo-ts/cheqd'

let issuerDid: string

const issuerConfig: InitConfig = {
  label: 'cheqd-jsonld-issuer',
  walletConfig: {
    id: 'cheqd-issuer-wallet',
    key: 'testkey0000000000000000000000000',
  },
}

const initializeIssuerAgent = async () => {
  const issuer = new Agent({
    config: issuerConfig,
    dependencies: agentDependencies,
    modules: {
      askar: new AskarModule({ ariesAskar }),
      dids: new DidsModule({
        registrars: [new CheqdDidRegistrar()],
        resolvers: [new CheqdDidResolver()],
      }),
      cheqd: new CheqdModule(
        new CheqdModuleConfig({
          networks: [
            {
              network: 'testnet', // or 'mainnet'
              cosmosPayerSeed: 'your-cosmos-payer-seed-here',
            },
          ],
        })
      ),
      credentials: new CredentialsModule({
        credentialProtocols: [
          new V2CredentialProtocol({
            credentialFormats: [new JsonLdCredentialFormatService()],
          }),
        ],
      }),
      proofs: new ProofsModule({
        proofProtocols: [
          new V2ProofProtocol({
            proofFormats: [new DifPresentationExchangeProofFormatService()],
          }),
        ],
      }),
      cache: new CacheModule({
        cache: new InMemoryLruCache({ limit: 100 }),
      }),
      w3cCredentials: new W3cCredentialsModule({}),
    },
  })

  // Register transports
  issuer.registerOutboundTransport(new WsOutboundTransport())
  issuer.registerOutboundTransport(new HttpOutboundTransport())
  issuer.registerInboundTransport(new HttpInboundTransport({ port: 3001 }))

  await issuer.initialize()
  return issuer
}

Step 3: Set up the Holder Agent

The holder agent needs to resolve cheqd DIDs and handle JSON-LD credentials.

Holder
const holderConfig: InitConfig = {
  label: 'cheqd-jsonld-holder',
  walletConfig: {
    id: 'cheqd-holder-wallet',
    key: 'testkey0000000000000000000000000',
  },
}

const initializeHolderAgent = async () => {
  const holder = new Agent({
    config: holderConfig,
    dependencies: agentDependencies,
    modules: {
      askar: new AskarModule({ ariesAskar }),
      dids: new DidsModule({
        resolvers: [new CheqdDidResolver()],
      }),
      credentials: new CredentialsModule({
        credentialProtocols: [
          new V2CredentialProtocol({
            credentialFormats: [new JsonLdCredentialFormatService()],
          }),
        ],
      }),
      proofs: new ProofsModule({
        proofProtocols: [
          new V2ProofProtocol({
            proofFormats: [new DifPresentationExchangeProofFormatService()],
          }),
        ],
      }),
      cache: new CacheModule({
        cache: new InMemoryLruCache({ limit: 100 }),
      }),
      w3cCredentials: new W3cCredentialsModule({}),
    },
  })

  // Register transports
  holder.registerOutboundTransport(new WsOutboundTransport())
  holder.registerOutboundTransport(new HttpOutboundTransport())
  holder.registerInboundTransport(new HttpInboundTransport({ port: 3002 }))

  await holder.initialize()
  return holder
}

Step 4: Create Issuer DID

Issuer
// Create a cheqd DID with Ed25519 verification method
const did = `did:cheqd:testnet:${utils.uuid()}`
const ed25519Key = await issuer.wallet.createKey({ keyType: KeyType.Ed25519 })

const createResult = await issuer.dids.create({
  method: 'cheqd',
  didDocument: new DidDocumentBuilder(did)
    .addController(did)
    .addVerificationMethod(
      getEd25519VerificationKey2018({
        key: ed25519Key,
        controller: did,
        id: `${did}#${ed25519Key.fingerprint}`,
      })
    )
    .addAssertionMethod(`${did}#${ed25519Key.fingerprint}`)
    .addAuthentication(`${did}#${ed25519Key.fingerprint}`)
    .build(),
})

if (!createResult.didState.did) {
  throw new Error('cheqd DID creation failed')
}

issuerDid = createResult.didState.did
console.log('Issuer DID created:', issuerDid)

Step 5: Create connection between Issuer and Holder

Use any supported method to create a connection with the Holder of the credential. Automated out-of-band protocol is recommended.

5a: Issuer creates Connection Invite

The Issuer agent will create a new connection invite for the Holder. This is needed to securely communicate between the Issuer and the Holder agents.

Issuer
const createNewInvitation = async (agent: Agent) => {
  const outOfBandRecord = await agent.modules.oob.createInvitation()

  return {
    invitationUrl: outOfBandRecord.outOfBandInvitation.toUrl({ domain: 'http://localhost:3001' }),
    outOfBandRecord,
  }
}

5b: Holder receives invitation

The above request will have an invitation in the response. Holder will have to copy that invitation and pass URL as invitationUrl in the following code:

Holder
const receiveInvitation = async (agent: Agent, invitationUrl: string) => {
  const { outOfBandRecord } = await agent.modules.oob.receiveInvitationFromUrl(invitationUrl)
  if (!outOfBandRecord) {
      throw new Error(redText(Output.NoConnectionRecordFromOutOfBand))
  }
  return outOfBandRecord
}

Step 6: Set up Credential Event Listeners

Both agents need event listeners to handle the credential exchange protocol automatically.

Issuer and Holder
import { CredentialEventTypes, CredentialState } from '@credo-ts/core'

const setupCredentialListener = (agent: Agent) => {
  agent.events.on(CredentialEventTypes.CredentialStateChanged, async ({ payload }) => {
    const { credentialRecord } = payload
    
    switch (credentialRecord.state) {
      case CredentialState.ProposalReceived:
        console.log('Issuer: Credential proposal received')
        await agent.credentials.acceptProposal({
          credentialRecordId: credentialRecord.id,
          comment: 'JSON-LD Credential Offer',
        })
        break
        
      case CredentialState.OfferReceived:
        console.log('Holder: Credential offer received, accepting...')
        await agent.credentials.acceptOffer({
          credentialRecordId: credentialRecord.id,
          credentialFormats: { jsonld: {} },
        })
        break
        
      case CredentialState.RequestReceived:
        console.log('Issuer: Credential request received, issuing credential...')
        await agent.credentials.acceptRequest({
          credentialRecordId: credentialRecord.id,
          comment: 'JSON-LD Credential',
        })
        break
        
      case CredentialState.CredentialReceived:
        console.log('Holder: Credential received, accepting...')
        await agent.credentials.acceptCredential({
          credentialRecordId: credentialRecord.id,
        })
        break
        
      case CredentialState.Done:
        console.log('Done: Credential exchange completed!')
        break
    }
  })
}

Step 7: Holder proposes the Credential

In this example, we will initiate the credential issuance process by having the holder propose a credential.

Holder
// Define the credential to be proposed
const credentialOptions = {
  credential: {
    '@context': [
      'https://www.w3.org/2018/credentials/v1',
      'https://w3id.org/citizenship/v1',
      'https://w3id.org/security/bbs/v1',
    ],
    id: 'https://cheqd.io/credentials/permanent-resident-card',
    type: ['VerifiableCredential', 'PermanentResidentCard'],
    issuer: issuerDid,
    issuanceDate: new Date().toISOString(),
    expirationDate: new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000).toISOString(), // 10 years
    credentialSubject: {
      id: 'did:example:holder123',
      type: ['PermanentResident', 'Person'],
      givenName: 'John',
      familyName: 'Doe',
      gender: 'Male',
      birthDate: '1990-01-01',
      birthCountry: 'United States',
      residentSince: '2020-01-01',
      lprNumber: '123-456-789',
      lprCategory: 'IR1',
    },
  },
  options: {
    proofType: 'Ed25519Signature2018',
    proofPurpose: 'assertionMethod',
  },
}
// Get Connection
const holderConnection = await receiveInvitation(holderAgent, invitationUrl)
// Holder proposes credential
console.log('Starting credential proposal...')
const credentialExchange = await holderAgent.credentials.proposeCredential({
  connectionId: holderConnection.id,
  protocolVersion: 'v2',
  credentialFormats: { jsonld: credentialOptions },
  comment: 'Requesting Permanent Resident Card',
})

console.log('Credential exchange initiated:', credentialExchange.id)

// The rest of the flow is handled automatically by event listeners
// Wait for completion...

The Credential Acceptance and storage is handled automatically by the event listeners registered for both Issuer and Holder.

Last updated

Was this helpful?