javascriptnode.jstypescripthyperledgerhyperledger-aries

Am i issuing the credential correctly with hyperledger aries?


i'm trying to issue the credential from my cloud agent to the mobile agent running Aries-Bifold.

inside my cloud agent i'm using:

const anonCredsCredentialExchangeRecord =
      await cloudAgent?.agent?.credentials.offerCredential({
        protocolVersion: "v2",
        connectionId: connectionId,
        credentialFormats: {
          anoncreds: {
            credentialDefinitionId: cloudAgent?.credentialDefinitionId,
            attributes: [
              { name: "full_name", value: fullName },
              { name: "date_of_birth", value: dateOfBirth },
              { name: "address", value: address },
              { name: "government_id", value: governmentID },
              { name: "contact_info", value: contactInfo },
            ],
          },
        },
      });

but i'm getting this error:

Error issuing credential: AriesFrameworkError: Unable to create offer. No supported formats
at V2CredentialProtocol.createOffer (C:\\Users\\Tosat\\Desktop\\Ladon\\LadonCloudAgent\\node_modules@aries-framework\\core\\src\\modules\\credentials\\protocol\\v2\\V2CredentialProtocol.ts:334:13)
at CredentialsApi.offerCredential (C:\\Users\\Tosat\\Desktop\\Ladon\\LadonCloudAgent\\node_modules@aries-framework\\core\\src\\modules\\credentials\\CredentialsApi.ts:266:58)
at processTicksAndRejections (node:internal/process/task_queues:96:5) {
\[cause\]: undefined
}

also, i'm getting the error from visual studio code saying

Type 'string' is not assignable to type 'never'.ts(2322)
CredentialsApiOptions.d.ts(46, 5): The expected type comes from property 'protocolVersion' which is declared here on type 'OfferCredentialOptions<[]>'

on line

 protocolVersion: "v2",

this is my schema:

import * as initConfigurationData from "./configurationData.json";

export const governmentDigitalCredentialSchema = {
    attrNames: ['full_name', 'date_of_birth', 'address', 'government_id', 'contact_info'],
    issuerId: initConfigurationData.BCOVRIN_TEST_STARTOFTHECOMPLETEID + initConfigurationData.DID, // it means->did:indy:bcovrin:test:BYisj4xqWijKxoMrvR1KZW
    name: 'Digital Identity Schema for Government ServicesXYZ',
    version: '1.0.0',
};

This is my complete class code snippet for the cloud agent:

import {
  Agent,
  ConsoleLogger,
  KeyDerivationMethod,
  LogLevel,
  HttpOutboundTransport,
  WsOutboundTransport,
  ConnectionsModule,
  DidsModule,
  OutOfBandRecord,
  ConnectionEventTypes,
  DidExchangeState,
  ConnectionStateChangedEvent,
  KeyDidResolver,
  TypedArrayEncoder,
  KeyType,
  DidRecord,
} from "@aries-framework/core";
import { agentDependencies, HttpInboundTransport } from "@aries-framework/node";
import { AskarModule } from "@aries-framework/askar";
import { ariesAskar } from "@hyperledger/aries-askar-nodejs";
import { anoncreds } from "@hyperledger/anoncreds-nodejs";
import { AnonCredsModule } from "@aries-framework/anoncreds";
import { AnonCredsRsModule } from "@aries-framework/anoncreds-rs";
import { indyVdr } from "@hyperledger/indy-vdr-nodejs";
import {
  IndyVdrAnonCredsRegistry,
  IndyVdrIndyDidRegistrar,
  IndyVdrIndyDidResolver,
  IndyVdrModule,
} from "@aries-framework/indy-vdr";
import {
  CheqdAnonCredsRegistry,
  CheqdDidRegistrar,
  CheqdDidResolver,
  CheqdModule,
  CheqdModuleConfig,
} from "@aries-framework/cheqd";
import { connect } from "ngrok";
import { BCOVRIN_TEST_GENESIS } from "./bc_ovrin";
import * as initConfigurationData from "./configurationData.json";
import { governmentDigitalCredentialSchema } from "./GovernmentDigitalCredentialSchema";

class LadonCloudAgent {
  agent: Agent | undefined;
  outOfBandRecord: OutOfBandRecord | undefined;
  invitationUrl: string | undefined;
  // Add this property to store the credential definition id
  credentialDefinitionId: any;
  connectionIds: any;
  schemaId: any;
  schema: any;

  constructor() {
    this.connectionIds = []; // Initialize an empty array to store connection IDs

    // Call the initialize method in the constructor
    this.initializeAgent().catch((error) => {
      console.error(
        "Error initializing LadonCloudAgent Inside CONSTRUCTOR:",
        error
      );
    });
  }

  async initializeAgent() {
    const config = await this.setupAgentConfiguration();

    this.agent = this.createAgentInstance(config);

    this.registerTransports();

    await this.initializeAgentInstance();

    //await this.createAndImportDID();

    this.printAllDIDs();

    await this.registerSchema();

    await this.registerCredentialDefinition();

    return this.agent;
  }

  async setupAgentConfiguration() {
    const endpoint =
      initConfigurationData.endpointURL ||
      (await connect(initConfigurationData.agentPort));

    return {
      label: initConfigurationData.label,
      logger: new ConsoleLogger(LogLevel.info),
      connectionImageUrl: initConfigurationData.connectionImageUrl,
      walletConfig: {
        id: initConfigurationData.id,
        key: initConfigurationData.walletPassword,
        keyDerivationMethod: KeyDerivationMethod.Argon2IMod,
      },
      endpoints: [endpoint],
    };
  }

  private createAgentInstance(config: any) {
    return new Agent({
      config,
      modules: {
        dids: new DidsModule({
          registrars: [new CheqdDidRegistrar(), new IndyVdrIndyDidRegistrar()],
          resolvers: [
            new CheqdDidResolver(),
            new KeyDidResolver(),
            new IndyVdrIndyDidResolver(),
          ],
        }),
        anoncredsRs: new AnonCredsRsModule({
          anoncreds,
        }),
        anoncreds: new AnonCredsModule({
          // Here we add an Indy VDR registry as an example, any AnonCreds registry can be used
          registries: [
            new CheqdAnonCredsRegistry(),
            new IndyVdrAnonCredsRegistry(),
          ],
        }),
        indyVdr: new IndyVdrModule({
          indyVdr,
          networks: [
            {
              isProduction: false,
              indyNamespace: "bcovrin:test",
              genesisTransactions: BCOVRIN_TEST_GENESIS,
              connectOnStartup: true,
            },
          ],
        }),
        // Add cheqd module
        cheqd: new CheqdModule(
          new CheqdModuleConfig({
            networks: [
              {
                network: initConfigurationData.cheqdNetwork,
                cosmosPayerSeed: initConfigurationData.seedPhrase24WordsKeplr,
              },
            ],
          })
        ),
        connections: new ConnectionsModule({ autoAcceptConnections: true }),
        // Register the Askar module on the agent
        askar: new AskarModule({ ariesAskar }),
      },
      dependencies: agentDependencies,
    });
  }

  private registerTransports() {
    this.agent?.registerOutboundTransport(new WsOutboundTransport());
    this.agent?.registerOutboundTransport(new HttpOutboundTransport());
    this.agent?.registerInboundTransport(
      new HttpInboundTransport({ port: initConfigurationData.agentPort })
    );
  }

  private async initializeAgentInstance() {
    try {
      await this.agent?.initialize();
      console.log("Agent initialized!");
    } catch (error) {
      console.error("Error initializing agent:", error);
      throw error;
    }
  }

  async createInvitation() {
    if (!this.agent) {
      throw new Error("Agent is not initialized.");
    }

    this.outOfBandRecord = await this.agent.oob.createInvitation();

    return {
      invitationUrl: this.outOfBandRecord.outOfBandInvitation.toUrl({
        domain: "",
      }),
      outOfBandRecord: this.outOfBandRecord,
    };
  }

  setupConnectionListener(
    outOfBandRecord: OutOfBandRecord, // Add this parameter
    cb: () => void
  ) {
    if (!this.agent) {
      throw new Error("Agent is not initialized.");
    }
    if (!outOfBandRecord) {
      // Use the parameter here
      throw new Error("There is no outOfBandRecord initialized.");
    }
    if (!outOfBandRecord.id) {
      // Use the parameter here
      throw new Error("There is no outOfBandRecord id initialized.");
    }

    this.agent.events.on<ConnectionStateChangedEvent>(
      ConnectionEventTypes.ConnectionStateChanged,
      ({ payload }) => {
        if (!outOfBandRecord) {
          // Use the parameter here
          throw new Error("There is no outOfBandRecord initialized.");
        }
        if (!outOfBandRecord.id) {
          // Use the parameter here
          throw new Error("There is no outOfBandRecord id initialized.");
        }
        if (payload.connectionRecord.outOfBandId !== outOfBandRecord.id) return;
        if (payload.connectionRecord.state === DidExchangeState.Completed) {
          console.log(
            `Connection for out-of-band id ${outOfBandRecord.id} completed`
          );

          // Store the connection ID in the array
          this.connectionIds.push(payload.connectionRecord.id);

          console.log("connections array content:");
          console.log(this.connectionIds);

          // Custom business logic can be included here
          // In this example we can send a basic message to the connection, but
          // anything is possible
          cb();
        }
      }
    );
  }

  async registerSchema() {
    try {
      if (!this.agent) {
        console.error("Agent is not initialized.");
        return;
      }

      // Check if the schema already exists
      this.schemaId =
        governmentDigitalCredentialSchema.issuerId +
        "/anoncreds/v0/SCHEMA/" +
        governmentDigitalCredentialSchema.name +
        "/" +
        governmentDigitalCredentialSchema.version;
      const existingSchema = await this.agent.modules.anoncreds.getSchema(
        this.schemaId
      );

      console.log(existingSchema);

      if (existingSchema) {
        console.log(`Schema with ID ${this.schemaId} already exists.`);
        this.schema = existingSchema;
        return;
      }

      // Register the schema
      const schemaResult = await this.agent.modules.anoncreds.registerSchema({
        schema: governmentDigitalCredentialSchema,
        options: {},
      });

      if (schemaResult.schemaState.state === "failed") {
        throw new Error(schemaResult.schemaState.reason);
      }

      this.schema = schemaResult;
      this.schemaId = schemaResult.schemaState.schemaId;
    } catch (error) {
      console.error("Error registering schema", error);
      throw error;
    }
  }

  async registerCredentialDefinition() {
    if (!this.agent) {
      console.error("Agent is not initialized.");
      return;
    }

    // Register the credential definition if not already registered
    const credentialDefinitionResult =
      await this.agent.modules.anoncreds.registerCredentialDefinition({
        credentialDefinition: {
          tag: initConfigurationData.CredentialDefinitionTag,
          issuerId:
            initConfigurationData.BCOVRIN_TEST_STARTOFTHECOMPLETEID +
            initConfigurationData.DID,
          schemaId: this.schemaId,
        },
        options: {},
      });

    if (
      credentialDefinitionResult.credentialDefinitionState.state === "failed"
    ) {
      throw new Error(
        `Error creating credential definition: ${credentialDefinitionResult.credentialDefinitionState.reason}`
      );
    }

    // Store the credential definition id in the class property
    //this.credentialDefinitionId =
    //credentialDefinitionResult.credentialDefinitionState.credentialDefinitionId;

    this.credentialDefinitionId =
    credentialDefinitionResult.credentialDefinitionState.credentialDefinitionId

    console.log(
      "Credential definition registered (simone):",
      this.credentialDefinitionId
    );

    /*
    Here's an example of what a credentialDefinitionId might look like after registering a credential definition using the AnonCreds module in Hyperledger Aries:

    NcYxiDXkpYi6ov5FcYDi1e:3:CL:18:TAG1
    In this example:

    NcYxiDXkpYi6ov5FcYDi1e is the issuer DID (Decentralized Identifier) that represents the entity issuing the credential.
    3 is the credential definition version.
    CL indicates the method used to create the credential definition. In this case, "CL" refers to Camenisch-Lysyanskaya.
    18 is the schema sequence number, which helps identify the specific schema associated with the credential definition.
    TAG1 is a tag or alias that helps identify the credential definition more easily.
    */
  }

  /*async createAndImportDID() {
    const didExists = await this.checkIfDIDExists();

    if (!didExists) {
      const unqualifiedIndyDid = initConfigurationData.DID;
      const indyDid = `did:indy:bcovrin:test:${unqualifiedIndyDid}`;

      let did;
      let privateKey;
      try {
        const data = fs.readFileSync("did_private_key.json", "utf-8");
        const { savedDid, savedPrivateKey } = JSON.parse(data);
        did = savedDid;
        privateKey = savedPrivateKey;
      } catch (error) {
        const seed = TypedArrayEncoder.fromString(initConfigurationData.DIDSeed);
        privateKey = seed;

        const data = JSON.stringify({ savedDid: indyDid, savedPrivateKey: seed });
        fs.writeFileSync("did_private_key.json", data, "utf-8");
      }

      if (!this.agent) {
        throw new Error("Agent is not initialized.");
      }

      await this.agent.dids.import({
        did: did || indyDid,
        overwrite: true,
        privateKeys: [
          {
            privateKey: privateKey,
            keyType: KeyType.Ed25519,
          },
        ],
      });
    }
  }

  async checkIfDIDExists() {
    if (!this.agent) return false;

    const didRecords = await this.agent.dids.getCreatedDids();
    const unqualifiedIndyDid = initConfigurationData.DID;
    const indyDid = `did:indy:bcovrin:test:${unqualifiedIndyDid}`;

    return didRecords.some((record) => record.did === indyDid);
  }
  */
  async printAllDIDs() {
    if (!this.agent) {
      throw new Error("Agent is not initialized.");
    }

    const didRecords = await this.agent.dids.getCreatedDids();

    console.log("All DIDs:");
    for (const record of didRecords) {
      console.log(`DID: ${record.did}`);
      console.log(`Verkey: ${record.id}`);
      console.log(`Role: ${record.role}`);
      console.log(`Metadata: ${JSON.stringify(record.metadata)}`);
      console.log("------------");
    }
  }
}

export default LadonCloudAgent;

and this is my route file:

import { Router, Request, Response } from "express";
import qrcode from "qrcode";

const routes = Router();

routes.get("/", async (req: Request, res: Response) => {
  try {
    const cloudAgent = req.agentInstance;

    // Data to be passed to the rendered template
    const renderedData = {
      agentIsInitialized: cloudAgent?.agent?.isInitialized ?? false,
    };
    console.log("cloudAgent?.credentialDefinitionId: -------------")
    console.log(cloudAgent?.credentialDefinitionId)
    // Use the 'res.render()' method to send the 'index.ejs' file as the response
    // Provide the correct path to the 'views' folder relative to the project's root
    // Pass the data object as the second argument to the 'res.render()' method
    res.render("index.ejs", renderedData);
  } catch (error) {
    console.error("Error initializing agent:", error);
    res.status(500).json({ error: "Something went wrong" });
  }
});

routes.get("/credential-issue-endpoint", (req, res) => {
  try {
    // Retrieve the list of connections
    const connections = req.agentInstance?.connectionIds;
    // Render the credential-issue.ejs template and pass the connections data
    res.render("credential-issue.ejs", { connections });
  } catch (error) {
    console.error("Error rendering credential-issue:", error);
    res.status(500).json({ error: "Something went wrong" });
  }
});

routes.get("/generateDynamicQRCode", async (req, res) => {
  try {
    const cloudAgent = req.agentInstance;

    // Create a new invitation using the agent
    const invitationData = await cloudAgent?.createInvitation();

    if (!invitationData) {
      return res.status(500).json({ error: "Failed to create invitation" });
    }

    const { invitationUrl, outOfBandRecord } = invitationData;

    // Generate a QR code from the invitation URL
    const qrCodeDataURL = await qrcode.toDataURL(invitationUrl);

    // Set up the connection listener for the new out-of-band record
    cloudAgent?.setupConnectionListener(outOfBandRecord, () => {
      console.log(
        "We now have an active connection to use in the following tutorials"
      );
    });

    // Send the QR code data URL as the response
    res.json({ qrCodeDataURL });
  } catch (error) {
    console.error("Error creating dynamic invitation:", error);
    res.status(500).json({ error: "Something went wrong" });
  }
});

routes.post("/issue-credential-from-ejs-form", async (req, res) => {
  try {
    const cloudAgent = req.agentInstance;
    const connectionId = req.body.connectionId;
    const fullName = req.body.fullName;
    const address = req.body.address;
    const dateOfBirth = req.body.dateOfBirth;
    const governmentID = req.body.governmentID;
    const contactInfo = req.body.contactInfo;


    //console.log(cloudAgent?.credentialDefinitionId?.schemaId)

    // Use the connectionId and attribute values to issue the credential
    const anonCredsCredentialExchangeRecord =
      await cloudAgent?.agent?.credentials.offerCredential({
        connectionId: connectionId,
        protocolVersion: "v2",
        credentialFormats: {
          anoncreds: {
            attributes: [
              { name: "full_name", value: fullName },
              { name: "date_of_birth", value: dateOfBirth },
              { name: "address", value: address },
              { name: "government_id", value: governmentID },
              { name: "contact_info", value: contactInfo },
            ],
            credentialDefinitionId: cloudAgent?.credentialDefinitionId,
          },
        },
      });

    if (!anonCredsCredentialExchangeRecord) {
      return res.status(500).json({ error: "Failed to offer credential" });
    }

    // check the state of the credential exchange
    // and log appropriate messages or take further actions.

    console.log("Credential offer sent successfully");
    console.log(
      "Credential exchange ID:",
      anonCredsCredentialExchangeRecord.id
    );
    console.log(
      "Credential exchange state:",
      anonCredsCredentialExchangeRecord.state
    );

    res.redirect("/"); // Redirect back to the main page
  } catch (error) {
    console.error("Error issuing credential:", error);
    res.status(500).json({ error: "Something went wrong" });
  }
});

export default routes;

Thanks in advance for the help!

I tried checking the values that i put inside "await cloudAgent?.agent?.credentials.offerCredential()" but they seem to be right so i have no clue about where is the error.


Solution

  • I fixed it by adding this to my modules:

    credentials: new CredentialsModule({
          credentialProtocols: [
            new V2CredentialProtocol({
              credentialFormats: [new LegacyIndyCredentialFormatService(), new AnonCredsCredentialFormatService()],
            }),
          ],
        }),