UNPKG

@0xpolygonid/js-sdk

Version:
175 lines (160 loc) 6.37 kB
import { RevocationStatus, CredentialStatus } from '../../verifiable'; import { EthConnectionConfig } from '../../storage/blockchain'; import { CredentialStatusResolver, CredentialStatusResolveOptions } from './resolver'; import { OnChainRevocationStorage } from '../../storage/blockchain/onchain-revocation'; import { DID } from '@iden3/js-iden3-core'; import { isGenesisState } from '../../utils'; import { EthStateStorage, EthStateStorageOptions } from '../../storage/blockchain/state'; import { IStateStorage, IOnchainRevocationStore } from '../../storage'; import { Hash } from '@iden3/js-merkletree'; import { isIdentityDoesNotExistError } from '../../storage/blockchain/errors'; /* * Options for OnChainResolver * * @public * @typedef {Object} OnChainResolverOptions * @property {EthStateStorageOptions} [stateStorageOptions] - options for state storage */ export type OnChainResolverOptions = { stateStorageOptions?: EthStateStorageOptions; }; /** * OnChainIssuer is a class that allows to interact with the onchain contract * and build the revocation status. * * @public * @class OnChainIssuer */ export class OnChainResolver implements CredentialStatusResolver { private readonly _stateStorage: IStateStorage; /** * * Creates an instance of OnChainIssuer. * @public * @param {Array<EthConnectionConfig>} _configs - list of ethereum network connections */ constructor(private readonly _configs: EthConnectionConfig[], _opts?: OnChainResolverOptions) { this._stateStorage = new EthStateStorage(_configs, _opts?.stateStorageOptions); } /** * resolve is a method to resolve a credential status from the blockchain. * * @public * @param {CredentialStatus} credentialStatus - credential status to resolve * @param {CredentialStatusResolveOptions} credentialStatusResolveOptions - options for resolver * @returns `{Promise<RevocationStatus>}` */ async resolve( credentialStatus: CredentialStatus, credentialStatusResolveOptions?: CredentialStatusResolveOptions ): Promise<RevocationStatus> { if (!credentialStatusResolveOptions?.issuerDID) { throw new Error('IssuerDID is not set in options'); } return this.getRevocationOnChain(credentialStatus, credentialStatusResolveOptions.issuerDID); } /** * Gets partial revocation status info from onchain issuer contract. * * @param {CredentialStatus} credentialStatus - credential status section of credential * @param {DID} issuerDid - issuer did * @returns `{Promise<RevocationStatus>}` */ async getRevocationOnChain( credentialStatus: CredentialStatus, issuer: DID ): Promise<RevocationStatus> { const { contractAddress, chainId, revocationNonce, stateHex } = this.extractCredentialStatusInfo(credentialStatus); if (revocationNonce !== credentialStatus.revocationNonce) { throw new Error('revocationNonce does not match'); } const issuerId = DID.idFromDID(issuer); let latestIssuerState: bigint; try { const latestStateInfo = await this._stateStorage.getLatestStateById(issuerId.bigInt()); if (!latestStateInfo.state) { throw new Error('state contract returned empty state'); } latestIssuerState = latestStateInfo.state; } catch (e) { if (!isIdentityDoesNotExistError(e)) { throw e; } if (!stateHex) { throw new Error( 'latest state not found and state parameter is not present in credentialStatus.id' ); } const stateBigInt = Hash.fromHex(stateHex).bigInt(); if (!isGenesisState(issuer, stateBigInt)) { throw new Error( `latest state not found and state parameter ${stateHex} is not genesis state` ); } latestIssuerState = stateBigInt; } const id = DID.idFromDID(issuer); const onChainCaller = this._getOnChainRevocationStorageForIssuer(chainId, contractAddress); const revocationStatus = await onChainCaller.getRevocationStatusByIdAndState( id.bigInt(), latestIssuerState, revocationNonce ); return revocationStatus; } /** * Extract information about credential status * * @param {credentialStatus} CredentialStatus - credential status * @returns {{contractAddress: string, chainId: number, revocationNonce: number, issuer: string;}} */ extractCredentialStatusInfo(credentialStatus: CredentialStatus): { contractAddress: string; chainId: number; revocationNonce: number; stateHex: string; } { if (!credentialStatus.id) { throw new Error('credentialStatus id is empty'); } const idParts = credentialStatus.id.split('/'); if (idParts.length !== 2) { throw new Error('invalid credentialStatus id'); } const idURL = new URL(credentialStatus.id); const stateHex = idURL.searchParams.get('state') || ''; const contractIdentifier = idURL.searchParams.get('contractAddress'); if (!contractIdentifier) { throw new Error('contractAddress not found in credentialStatus.id field'); } const parts = contractIdentifier.split(':'); if (parts.length != 2) { throw new Error('invalid contract address encoding. should be chainId:contractAddress'); } const chainId = parseInt(parts[0], 10); const contractAddress = parts[1]; // if revocationNonce is not present in id as param, then it should be extract from credentialStatus const rv = idURL.searchParams.get('revocationNonce') || credentialStatus.revocationNonce; if (rv === undefined || rv === null) { throw new Error('revocationNonce not found in credentialStatus id field'); } const revocationNonce = typeof rv === 'number' ? rv : parseInt(rv, 10); return { contractAddress, chainId, revocationNonce, stateHex }; } networkByChainId(chainId: number): EthConnectionConfig { const network = this._configs.find((c) => c.chainId === chainId); if (!network) { throw new Error(`chainId "${chainId}" not supported`); } return network; } private _getOnChainRevocationStorageForIssuer( chainId: number, contractAddress: string ): IOnchainRevocationStore { const networkConfig = this.networkByChainId(chainId); const onChainCaller = new OnChainRevocationStorage(networkConfig, contractAddress); return onChainCaller; } }