diff --git a/packages/credentials/src/V1/KiltRevocationStatusV1.ts b/packages/credentials/src/V1/KiltRevocationStatusV1.ts index 16130a9d7..237781418 100644 --- a/packages/credentials/src/V1/KiltRevocationStatusV1.ts +++ b/packages/credentials/src/V1/KiltRevocationStatusV1.ts @@ -9,23 +9,133 @@ import { u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util' import { base58Decode, base58Encode } from '@polkadot/util-crypto' import type { ApiPromise } from '@polkadot/api' import type { U8aLike } from '@polkadot/util/types' - +import { authorizeTx } from '@kiltprotocol/did' import { ConfigService } from '@kiltprotocol/config' -import type { Caip2ChainId } from '@kiltprotocol/types' -import { Caip2, SDKErrors } from '@kiltprotocol/utils' - +import type { + Caip2ChainId, + KiltAddress, + SignerInterface, + MultibaseKeyPair, +} from '@kiltprotocol/types' +import { Caip2, SDKErrors, Signers } from '@kiltprotocol/utils' +import { Blockchain } from '@kiltprotocol/chain-helpers' import * as CType from '../ctype/index.js' import * as Attestation from '../attestation/index.js' import { assertMatchingConnection, getDelegationNodeIdForCredential, } from './common.js' -import type { KiltCredentialV1, KiltRevocationStatusV1 } from './types.js' +import type { IssuerOptions } from '../interfaces.js' +import type { + KiltCredentialV1, + KiltRevocationStatusV1, + VerifiableCredential, +} from './types.js' export type Interface = KiltRevocationStatusV1 export const STATUS_TYPE = 'KiltRevocationStatusV1' +interface RevokeResult { + success: boolean + error?: string[] + info: { + blockNumber?: string + blockHash?: string + transactionHash?: string + } +} + +interface BlockchainResponse { + blockNumber: string + status: { + finalized: string + } + txHash: string +} + +/** + * Revokes a Kilt credential on the blockchain, making it invalid. + * + * @param params Named parameters for the revocation process. + * @param params.issuer Interfaces for interacting with the issuer identity. + * @param params.issuer.didDocument The DID Document of the issuer revoking the credential. + * @param params.issuer.signers Array of signer interfaces for credential authorization. + * @param params.issuer.submitter The submitter can be one of: + * - A MultibaseKeyPair for signing transactions + * - A Ed25519 type keypair for blockchain interactions + * The submitter will be used to cover transaction fees and blockchain operations. + * @param params.credential The Verifiable Credential to be revoked. Must contain a valid credential ID. + * @param issuer + * @param credential + * @returns An object containing: + * - success: Boolean indicating if revocation was successful + * - error?: Array of error messages if revocation failed + * - info: Object containing blockchain transaction details: + * - blockNumber?: The block number where revocation was included + * - blockHash?: The hash of the finalized block + * - transactionHash?: The hash of the revocation transaction. + * @throws Will return error response if: + * - Credential ID is invalid or cannot be decoded + * - DID authorization fails + * - Transaction signing or submission fails. + */ +export async function revoke( + issuer: IssuerOptions, + credential: VerifiableCredential +): Promise { + try { + if (!credential.id) { + throw new Error('Credential ID is required for revocation') + } + + const rootHash = credential.id.split(':').pop() + if (!rootHash) { + throw new Error('Invalid credential ID format') + } + + const decodedroothash = base58Decode(rootHash) + const { didDocument, signers, submitter } = issuer + const api = ConfigService.get('api') + + const revokeTx = api.tx.attestation.revoke(decodedroothash, null) as any + const [Txsubmitter] = (await Signers.getSignersForKeypair({ + keypair: submitter as MultibaseKeyPair, + type: 'Ed25519', + })) as Array> + const authorizedTx = await authorizeTx( + didDocument, + revokeTx, + signers as SignerInterface[], + Txsubmitter.id + ) + + const response = (await Blockchain.signAndSubmitTx( + authorizedTx, + Txsubmitter + )) as unknown as BlockchainResponse + + const responseObj = JSON.parse(JSON.stringify(response)) + + return { + success: true, + info: { + blockNumber: responseObj.blockNumber, + blockHash: responseObj.status.finalized, + transactionHash: responseObj.txHash, + }, + } + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : 'Unknown error occurred' + return { + success: false, + error: [errorMessage], + info: {}, + } + } +} + /** * Check attestation and revocation status of a credential at the latest block available. * diff --git a/packages/credentials/src/issuer.ts b/packages/credentials/src/issuer.ts index 712b40442..c18a13999 100644 --- a/packages/credentials/src/issuer.ts +++ b/packages/credentials/src/issuer.ts @@ -5,17 +5,30 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { Did, ICType, IClaimContents } from '@kiltprotocol/types' - import { SDKErrors } from '@kiltprotocol/utils' -import { KiltAttestationProofV1, KiltCredentialV1 } from './V1/index.js' -import type { UnsignedVc, VerifiableCredential } from './V1/types.js' -import type { CTypeLoader } from './ctype/index.js' // eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { IssuerOptions, SubmitOverride } from './interfaces.js' +import type { Did, ICType, IClaimContents } from '@kiltprotocol/types' +import type { IssuerOptions } from './interfaces.js' +import type { CTypeLoader } from './ctype/index.js' +import type { UnsignedVc, VerifiableCredential } from './V1/types.js' +import { + KiltAttestationProofV1, + KiltCredentialV1, + KiltRevocationStatusV1, +} from './V1/index.js' export type { IssuerOptions } +interface RevokeResult { + success: boolean + error?: string[] + info: { + blockNumber?: string + blockHash?: string + transactionHash?: string + } +} + /** * Creates a new credential document as a basis for issuing a credential. * This document can be shown to users as a preview or be extended with additional properties before moving on to the second step of credential issuance: @@ -134,3 +147,41 @@ export async function issue({ ) } } +/** + * Revokes a Kilt credential on the blockchain, making it invalid. + * + * @param params Named parameters for the revocation process. + * @param params.credential The Verifiable Credential to be revoked. + * @param params.issuer Options for the issuer performing the revocation. + * @param params.proofOptions Optional parameters for proof configuration. + * @param params.proofOptions.proofType The type of proof to use for revocation. Currently only supports KiltAttestationProofV1. + * @returns Promise containing the revocation result. + * @throws {SDKError} When an unsupported proof type is provided. + */ +export async function revoke({ + credential, + issuer, + proofOptions = {}, +}: { + credential: VerifiableCredential + issuer: IssuerOptions + proofOptions?: { + proofType?: string + } +}): Promise { + const { proofType } = proofOptions + switch (proofType) { + case undefined: + case KiltAttestationProofV1.PROOF_TYPE: { + const result = await KiltRevocationStatusV1.revoke( + issuer as IssuerOptions, + credential as VerifiableCredential + ) + return result + } + default: + throw new SDKErrors.SDKError( + `Only proof type ${KiltAttestationProofV1.PROOF_TYPE} is currently supported.` + ) + } +} diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 43ca10776..302a45344 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -370,6 +370,24 @@ async function runAll() { console.log('Did deactivated') + // ┏━━━━━━━━━━━━━━━━━━┓ + // ┃ Revoke credential┃ + // ┗━━━━━━━━━━━━━━━━━━┛ + // + // Revoke a previously issued credential on chain + const revokeCredentialResult = await Kilt.Issuer.revoke( + didDocument.id, + credential + ) + + if (!revokeCredentialResult.success) { + throw new Error( + `revoke credential failed: ${revokeCredentialResult.error?.join(', ')}` + ) + } + + console.log('credential revoked') + // Release the connection to the blockchain. await api.disconnect()