Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cheqd): do not crash agent if cheqd down #1808

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/cheqd/src/dids/didCheqdUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,13 @@ export interface IDidDocOptions {
}

export function getClosestResourceVersion(resources: Metadata[], date: Date) {
const result = resources.sort(function (a, b) {
const result = [...resources].sort(function (a, b) {
if (!a.created || !b.created) throw new CredoError("Missing required property 'created' on resource")
const distancea = Math.abs(date.getTime() - a.created.getTime())
const distanceb = Math.abs(date.getTime() - b.created.getTime())
return distancea - distanceb
})

return result[0]
}

Expand Down
84 changes: 61 additions & 23 deletions packages/cheqd/src/ledger/CheqdLedgerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { MsgCreateResourcePayload } from '@cheqd/ts-proto/cheqd/resource/v2
import type { DirectSecp256k1HdWallet, DirectSecp256k1Wallet } from '@cosmjs/proto-signing'

import { createCheqdSDK, DIDModule, ResourceModule, CheqdNetwork } from '@cheqd/sdk'
import { CredoError, injectable } from '@credo-ts/core'
import { CredoError, inject, injectable, InjectionSymbols, Logger } from '@credo-ts/core'

import { CheqdModuleConfig } from '../CheqdModuleConfig'
import { parseCheqdDid } from '../anoncreds/utils/identifiers'
Expand All @@ -14,7 +14,7 @@ export interface ICheqdLedgerConfig {
network: string
rpcUrl: string
readonly cosmosPayerWallet: Promise<DirectSecp256k1HdWallet | DirectSecp256k1Wallet>
sdk?: CheqdSDK
sdk?: Promise<CheqdSDK>
}

export enum DefaultRPCUrl {
Expand All @@ -25,8 +25,10 @@ export enum DefaultRPCUrl {
@injectable()
export class CheqdLedgerService {
private networks: ICheqdLedgerConfig[]
private logger: Logger

public constructor(cheqdSdkModuleConfig: CheqdModuleConfig) {
public constructor(cheqdSdkModuleConfig: CheqdModuleConfig, @inject(InjectionSymbols.Logger) logger: Logger) {
this.logger = logger
this.networks = cheqdSdkModuleConfig.networks.map((config) => {
const { network, rpcUrl, cosmosPayerSeed } = config
return {
Expand All @@ -39,17 +41,11 @@ export class CheqdLedgerService {

public async connect() {
for (const network of this.networks) {
network.sdk = await createCheqdSDK({
modules: [DIDModule as unknown as AbstractCheqdSDKModule, ResourceModule as unknown as AbstractCheqdSDKModule],
rpcUrl: network.rpcUrl,
wallet: await network.cosmosPayerWallet.catch((error) => {
throw new CredoError(`Error initializing cosmos payer wallet: ${error.message}`, { cause: error })
}),
})
await this.initializeSdkForNetwork(network)
}
}

private getSdk(did: string) {
private async getSdk(did: string) {
const parsedDid = parseCheqdDid(did)
if (!parsedDid) {
throw new Error('Invalid DID')
Expand All @@ -59,10 +55,43 @@ export class CheqdLedgerService {
}

const network = this.networks.find((network) => network.network === parsedDid.network)
if (!network || !network.sdk) {
throw new Error('Network not configured')
if (!network) {
throw new Error(`Network ${network} not found in cheqd networks configuration`)
}

if (!network.sdk) {
const sdk = await this.initializeSdkForNetwork(network)
if (!sdk) throw new Error(`Cheqd SDK not initialized for network ${parsedDid.network}`)
return sdk
}

try {
const sdk = await network.sdk
return sdk
} catch (error) {
throw new Error(`Error initializing cheqd sdk for network ${parsedDid.network}: ${error.message}`)
}
}

private async initializeSdkForNetwork(network: ICheqdLedgerConfig) {
try {
// Initialize cheqd sdk with promise
network.sdk = createCheqdSDK({
modules: [DIDModule as unknown as AbstractCheqdSDKModule, ResourceModule as unknown as AbstractCheqdSDKModule],
rpcUrl: network.rpcUrl,
wallet: await network.cosmosPayerWallet.catch((error) => {
throw new CredoError(`Error initializing cosmos payer wallet: ${error.message}`, { cause: error })
}),
})

return await network.sdk
} catch (error) {
this.logger.error(
`Skipping connection for network ${network.network} in cheqd sdk due to error in initialization: ${error.message}`
)
network.sdk = undefined
return undefined
}
return network.sdk
}

public async create(
Expand All @@ -71,7 +100,8 @@ export class CheqdLedgerService {
versionId?: string | undefined,
fee?: DidStdFee
) {
return await this.getSdk(didPayload.id).createDidDocTx(signInputs, didPayload, '', fee, undefined, versionId)
const sdk = await this.getSdk(didPayload.id)
return sdk.createDidDocTx(signInputs, didPayload, '', fee, undefined, versionId)
}

public async update(
Expand All @@ -80,7 +110,8 @@ export class CheqdLedgerService {
versionId?: string | undefined,
fee?: DidStdFee
) {
return await this.getSdk(didPayload.id).updateDidDocTx(signInputs, didPayload, '', fee, undefined, versionId)
const sdk = await this.getSdk(didPayload.id)
return sdk.updateDidDocTx(signInputs, didPayload, '', fee, undefined, versionId)
}

public async deactivate(
Expand All @@ -89,15 +120,18 @@ export class CheqdLedgerService {
versionId?: string | undefined,
fee?: DidStdFee
) {
return await this.getSdk(didPayload.id).deactivateDidDocTx(signInputs, didPayload, '', fee, undefined, versionId)
const sdk = await this.getSdk(didPayload.id)
return sdk.deactivateDidDocTx(signInputs, didPayload, '', fee, undefined, versionId)
}

public async resolve(did: string, version?: string) {
return version ? await this.getSdk(did).queryDidDocVersion(did, version) : await this.getSdk(did).queryDidDoc(did)
const sdk = await this.getSdk(did)
return version ? sdk.queryDidDocVersion(did, version) : sdk.queryDidDoc(did)
}

public async resolveMetadata(did: string) {
return await this.getSdk(did).queryAllDidDocVersionsMetadata(did)
const sdk = await this.getSdk(did)
return sdk.queryAllDidDocVersionsMetadata(did)
}

public async createResource(
Expand All @@ -106,18 +140,22 @@ export class CheqdLedgerService {
signInputs: SignInfo[],
fee?: DidStdFee
) {
return await this.getSdk(did).createLinkedResourceTx(signInputs, resourcePayload, '', fee, undefined)
const sdk = await this.getSdk(did)
return sdk.createLinkedResourceTx(signInputs, resourcePayload, '', fee, undefined)
}

public async resolveResource(did: string, collectionId: string, resourceId: string) {
return await this.getSdk(did).queryLinkedResource(collectionId, resourceId)
const sdk = await this.getSdk(did)
return sdk.queryLinkedResource(collectionId, resourceId)
}

public async resolveCollectionResources(did: string, collectionId: string) {
return await this.getSdk(did).queryLinkedResources(collectionId)
const sdk = await this.getSdk(did)
return sdk.queryLinkedResources(collectionId)
}

public async resolveResourceMetadata(did: string, collectionId: string, resourceId: string) {
return await this.getSdk(did).queryLinkedResourceMetadata(collectionId, resourceId)
const sdk = await this.getSdk(did)
return sdk.queryLinkedResourceMetadata(collectionId, resourceId)
}
}
111 changes: 82 additions & 29 deletions packages/cheqd/tests/cheqd-did-resolver.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,125 @@
import { Agent, JsonTransformer } from '@credo-ts/core'
import type { CheqdDidCreateOptions } from '../src'

import { Agent, JsonTransformer, utils } from '@credo-ts/core'

import { getInMemoryAgentOptions } from '../../core/tests/helpers'
import { CheqdDidRegistrar } from '../src'
import { getClosestResourceVersion } from '../src/dids/didCheqdUtil'
import { DefaultRPCUrl } from '../src/ledger/CheqdLedgerService'

import { getCheqdModules } from './setupCheqdModule'

export const resolverAgent = new Agent(
getInMemoryAgentOptions('Cheqd resolver', {}, getCheqdModules(undefined, DefaultRPCUrl.Testnet))
)
export const resolverAgent = new Agent(getInMemoryAgentOptions('Cheqd resolver', {}, getCheqdModules(undefined)))

describe('Cheqd DID resolver', () => {
let did: string
let resourceResult1: Awaited<ReturnType<CheqdDidRegistrar['createResource']>>
let resourceResult2: Awaited<ReturnType<CheqdDidRegistrar['createResource']>>
let resourceResult3: Awaited<ReturnType<CheqdDidRegistrar['createResource']>>

beforeAll(async () => {
await resolverAgent.initialize()
const cheqdDidRegistrar = resolverAgent.dependencyManager.resolve(CheqdDidRegistrar)

const didResult = await resolverAgent.dids.create<CheqdDidCreateOptions>({
method: 'cheqd',
secret: {
verificationMethod: {
id: 'key-1',
type: 'Ed25519VerificationKey2020',
},
},
options: {
network: 'testnet',
methodSpecificIdAlgo: 'uuid',
},
})

if (!didResult.didState.did) {
throw new Error('No DID created')
}
did = didResult.didState.did

resourceResult1 = await cheqdDidRegistrar.createResource(resolverAgent.context, did, {
id: utils.uuid(),
name: 'LocalResource',
resourceType: 'test',
data: { hello: 'world' },
version: '1',
})
resourceResult2 = await cheqdDidRegistrar.createResource(resolverAgent.context, did, {
id: utils.uuid(),
name: 'LocalResource1',
resourceType: 'test',
data: { hello: 'world' },
version: '1',
})

resourceResult3 = await cheqdDidRegistrar.createResource(resolverAgent.context, did, {
id: utils.uuid(),
name: 'LocalResource2',
resourceType: 'test',
data: { hello: 'world' },
version: '1',
})

for (const resource of [resourceResult1, resourceResult2, resourceResult3]) {
if (resource.resourceState.state !== 'finished') {
throw new Error(`Resource creation failed: ${resource.resourceState.reason}`)
}
}
})

afterAll(async () => {
await resolverAgent.shutdown()
await resolverAgent.wallet.delete()
})

it('should resolve a did:cheqd:testnet did', async () => {
const did = await resolverAgent.dids.resolve('did:cheqd:testnet:3053e034-8faa-458d-9f01-2e3e1e8b2ab8')
expect(JsonTransformer.toJSON(did)).toMatchObject({
it('should resolve a did:cheqd did from local testnet', async () => {
const resolveResult = await resolverAgent.dids.resolve(did)
expect(JsonTransformer.toJSON(resolveResult)).toMatchObject({
didDocument: {
'@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/ed25519-2020/v1'],
id: 'did:cheqd:testnet:3053e034-8faa-458d-9f01-2e3e1e8b2ab8',
controller: ['did:cheqd:testnet:3053e034-8faa-458d-9f01-2e3e1e8b2ab8'],
id: did,
controller: [did],
verificationMethod: [
{
controller: 'did:cheqd:testnet:3053e034-8faa-458d-9f01-2e3e1e8b2ab8',
id: 'did:cheqd:testnet:3053e034-8faa-458d-9f01-2e3e1e8b2ab8#key-1',
publicKeyMultibase: 'z6MksPpyxgw5aFymMboa81CQ7h1kJJ9yehNzPgo714y1HrAA',
controller: did,
id: `${did}#key-1`,
publicKeyMultibase: expect.any(String),
type: 'Ed25519VerificationKey2020',
},
],
authentication: ['did:cheqd:testnet:3053e034-8faa-458d-9f01-2e3e1e8b2ab8#key-1'],
authentication: [`${did}#key-1`],
},
didDocumentMetadata: {
created: '2022-10-17T13:42:37.000Z',
updated: '0001-01-01T00:00:00.000Z',
created: expect.any(String),
updated: undefined,
deactivated: false,
versionId: '7314e3e5-f9cc-50e9-b249-348963937c96',
versionId: expect.any(String),
nextVersionId: '',
},
didResolutionMetadata: {},
})
})

it('should getClosestResourceVersion', async () => {
const did = await resolverAgent.dids.resolve('did:cheqd:testnet:SiVQgrFZ7jFZFrTGstT4ZD')
let resource = getClosestResourceVersion(did.didDocumentMetadata.linkedResourceMetadata, new Date())
const didResult = await resolverAgent.dids.resolve(did)

const inFuture = new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 10) // 10 years in future

// should get the latest resource
let resource = getClosestResourceVersion(didResult.didDocumentMetadata.linkedResourceMetadata, inFuture)
expect(resource).toMatchObject({
id: '0b02ebf4-07c4-4df7-9015-e93c21108240',
id: resourceResult3.resourceState.resourceId,
})

// Date in past should match first created resource
resource = getClosestResourceVersion(
did.didDocumentMetadata.linkedResourceMetadata,
didResult.didDocumentMetadata.linkedResourceMetadata,
new Date('2022-11-16T10:56:34Z')
)
expect(resource).toMatchObject({
id: '8140ec3a-d8bb-4f59-9784-a1cbf91a4a35',
})
resource = getClosestResourceVersion(
did.didDocumentMetadata.linkedResourceMetadata,
new Date('2022-11-16T11:41:48Z')
)
expect(resource).toMatchObject({
id: 'a20aa56a-a76f-4828-8a98-4c85d9494545',
id: resourceResult1.resourceState.resourceId,
})
})
})
10 changes: 5 additions & 5 deletions packages/cheqd/tests/cheqd-sdk-anoncreds-registry.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,20 +190,20 @@ describe('cheqdAnonCredsRegistry', () => {

// Should resolve query based url
test('resolve query based url', async () => {
const schemaResourceId =
'did:cheqd:testnet:d8ac0372-0d4b-413e-8ef5-8e8f07822b2c?resourceName=test - 11&resourceType=anonCredsSchema'
const schemaResponse = await cheqdAnonCredsRegistry.getSchema(resolverAgent.context, schemaResourceId)
const schemaResourceId = `${issuerId}?resourceName=test11-Schema&resourceType=anonCredsSchema`

const schemaResponse = await cheqdAnonCredsRegistry.getSchema(resolverAgent.context, schemaResourceId)
expect(schemaResponse).toMatchObject({
schema: {
attrNames: ['name'],
name: 'test - 11',
name: 'test11',
},
})
})

// TODO: re-add once we support registering revocation registries and revocation status lists
// Should resolve revocationRegistryDefinition and statusList
test('resolve revocation registry definition and statusList', async () => {
xtest('resolve revocation registry definition and statusList', async () => {
const revocationRegistryId = 'did:cheqd:testnet:e42ccb8b-78e8-4e54-9d11-f375153d63f8?resourceName=universityDegree'
const revocationDefinitionResponse = await cheqdAnonCredsRegistry.getRevocationRegistryDefinition(
resolverAgent.context,
Expand Down
Loading