Indy VDR
Hyperledger Indy networks, such as BCovrin Test and Sovrin, store DIDs and AnonCreds objects. @credo-ts/indy-vdr connects to one or more Indy networks through Indy VDR. It resolves did:indy and did:sov, registers did:indy DIDs, and provides an AnonCreds registry.
Use it when your ecosystem uses an Indy network, usually to issue or verify AnonCreds credentials.
Install
- Node.js
- React Native
- npm
- pnpm
- yarn
- bun
npm install @credo-ts/indy-vdr@^0.7.2 @hyperledger/indy-vdr-nodejs@^0.3.0
pnpm add @credo-ts/indy-vdr@^0.7.2 @hyperledger/indy-vdr-nodejs@^0.3.0
yarn add @credo-ts/indy-vdr@^0.7.2 @hyperledger/indy-vdr-nodejs@^0.3.0
bun add @credo-ts/indy-vdr@^0.7.2 @hyperledger/indy-vdr-nodejs@^0.3.0
- npm
- pnpm
- yarn
- bun
npm install @credo-ts/indy-vdr@^0.7.2 @hyperledger/indy-vdr-react-native@^0.3.0
pnpm add @credo-ts/indy-vdr@^0.7.2 @hyperledger/indy-vdr-react-native@^0.3.0
yarn add @credo-ts/indy-vdr@^0.7.2 @hyperledger/indy-vdr-react-native@^0.3.0
bun add @credo-ts/indy-vdr@^0.7.2 @hyperledger/indy-vdr-react-native@^0.3.0
This version of Credo needs version 0.3 of the native Indy VDR packages: @credo-ts/indy-vdr has a peer dependency on @hyperledger/indy-vdr-shared ^0.3.0.
Configure
Register IndyVdrModule with the networks you use, and add the Indy registrar and resolvers to DidsModule:
import { readFile } from 'node:fs/promises'
import {
DidsModule,
JwkDidRegistrar,
JwkDidResolver,
KeyDidRegistrar,
KeyDidResolver,
PeerDidRegistrar,
PeerDidResolver,
WebDidResolver,
} from '@credo-ts/core'
import {
IndyVdrIndyDidRegistrar,
IndyVdrIndyDidResolver,
IndyVdrModule,
IndyVdrSovDidResolver,
} from '@credo-ts/indy-vdr'
import { indyVdr } from '@hyperledger/indy-vdr-nodejs'
const indyVdrModules = {
indyVdr: new IndyVdrModule({
indyVdr,
networks: [
{
indyNamespace: 'bcovrin:test',
isProduction: false,
genesisTransactions: await readFile('genesis/bcovrin-test.txn', 'utf8'),
connectOnStartup: true,
},
],
}),
dids: new DidsModule({
registrars: [new KeyDidRegistrar(), new PeerDidRegistrar(), new JwkDidRegistrar(), new IndyVdrIndyDidRegistrar()],
resolvers: [
new KeyDidResolver(),
new PeerDidResolver(),
new JwkDidResolver(),
new WebDidResolver(),
new IndyVdrIndyDidResolver(),
new IndyVdrSovDidResolver(),
],
}),
}
On React Native, import indyVdr from the React Native package instead, and bundle the genesis transactions with your app:
import { indyVdr } from '@hyperledger/indy-vdr-react-native'
Pass the indyVdr object itself: unlike Askar and AnonCreds, Indy VDR has no Native* class. Each network takes:
| Option | Description |
|---|---|
indyNamespace | Namespace of the network in did:indy identifiers, such as bcovrin:test in did:indy:bcovrin:test:.... |
genesisTransactions | Contents of the network's genesis file, as a string. |
isProduction | Whether this is a production network. |
connectOnStartup | Connect when the agent starts instead of on first use. Defaults to false. |
transactionAuthorAgreement | { version, acceptanceMechanism }, required to write to networks with a transaction author agreement. acceptanceMechanism is a label from the network's acceptance mechanism list. |
IndyVdrModule doesn't register the DID registrar and resolvers itself, so list them in DidsModule. Your lists replace the default registrars and resolvers, see Configure DID methods.
Resolve did:indy and did:sov
With the resolvers registered, agent.dids.resolve handles both methods. For did:indy, Credo uses the network whose indyNamespace matches the DID. A did:sov or unqualified Indy identifier doesn't say which network it's on, so Credo queries all networks and prefers a production network when the DID exists on more than one.
Import an existing DID
Writing to an Indy network needs a DID that the network allows to write, usually with the ENDORSER role. To use a DID that was registered from a seed, import its key and the DID:
// The seed the DID was registered with on the ledger. Keep it secret.
const endorserSeed = process.env.ENDORSER_SEED
if (!endorserSeed) throw new Error('ENDORSER_SEED is not set')
const { privateJwk } = transformPrivateKeyToPrivateJwk({
type: { kty: 'OKP', crv: 'Ed25519' },
privateKey: TypedArrayEncoder.fromUtf8String(endorserSeed),
})
const { keyId, publicJwk } = await endorser.kms.importKey({ privateJwk })
// A DID registered from a seed is the base58 encoding of the first 16 bytes of the public key
const publicKey = PublicJwk.fromPublicJwk(publicJwk).publicKey.publicKey
const endorserDid = `did:indy:bcovrin:test:${TypedArrayEncoder.toBase58(publicKey.slice(0, 16))}`
await endorser.dids.import({
did: endorserDid,
// did:indy documents reference the main key as #verkey
keys: [{ didDocumentRelativeKeyId: '#verkey', kmsKeyId: keyId }],
})
transformPrivateKeyToPrivateJwk comes from @credo-ts/askar. Without a didDocument, import resolves the DID from the network.
Register a did:indy
With an endorser DID in the agent's wallet, register a new DID and let the same agent endorse it:
const { didState } = await endorser.dids.create<IndyVdrDidCreateOptions>({
method: 'indy',
options: {
endorserDid,
endorserMode: 'internal',
},
})
if (didState.state !== 'finished') throw new Error('Registering the did failed')
The new DID gets an Ed25519 key, or the key you pass as keyId. Other options are alias, role (STEWARD, TRUSTEE, ENDORSER or NETWORK_MONITOR) and services. Services are written to the DID document on the ledger, or as a legacy endpoint attribute with useEndpointAttrib: true. Updating and deactivating did:indy DIDs isn't supported.
External endorser
When the endorser is a different agent, the author prepares the transaction, the endorser signs it, and the author submits it:
// Author: prepare the transaction. The returned state asks for an endorsement.
const authorResult = (await author.dids.create<IndyVdrDidCreateOptions>({
method: 'indy',
options: { endorserDid, endorserMode: 'external' },
})) as IndyVdrDidCreateResult
const authorState = authorResult.didState
if (authorState.state !== 'action' || authorState.action !== 'endorseIndyTransaction') {
throw new Error('Expected an endorsement request')
}
// Endorser: sign the transaction it received from the author
const endorsedNymRequest = await endorser.modules.indyVdr.endorseTransaction(
authorState.nymRequest,
authorState.endorserDid
)
// Author: submit the endorsed transaction
const submitResult = await author.dids.create<IndyVdrDidCreateOptions>({
did: authorState.did,
options: { endorserMode: 'external', endorsedTransaction: { nymRequest: endorsedNymRequest } },
})
With endorserMode: 'external', the registrar returns the state action with action: 'endorseIndyTransaction'. Cast the result to IndyVdrDidCreateResult to read nymRequest and, when you pass services with useEndpointAttrib, attribRequest. Send these to the endorser, who signs each one with agent.modules.indyVdr.endorseTransaction. The author then calls create again with the DID and the endorsed transactions.
AnonCreds registry
IndyVdrAnonCredsRegistry registers and resolves AnonCreds schemas, credential definitions and revocation data on Indy. It's exported from the @credo-ts/indy-vdr root. Set up AnonCreds as described in AnonCreds, with the Indy registry:
import { AnonCredsModule } from '@credo-ts/anoncreds'
import { IndyVdrAnonCredsRegistry } from '@credo-ts/indy-vdr'
import { NativeAnoncreds } from '@hyperledger/anoncreds-nodejs'
const anoncreds = new AnonCredsModule({
anoncreds: NativeAnoncreds,
registries: [new IndyVdrAnonCredsRegistry()],
})
The registry needs IndyVdrModule. To register objects, the issuer needs a did:indy with write permission, and passes endorserMode and endorserDid in the registration options.