@0xpolygonid/js-sdk
Version:
SDK to work with Polygon ID
328 lines (299 loc) • 11.4 kB
text/typescript
import { Claim, DID, Id } from '@iden3/js-iden3-core';
import { NonMerklizedIssuerBase__factory } from '@iden3/onchain-non-merklized-issuer-base-abi';
import { NonMerklizedIssuerBase } from '@iden3/onchain-non-merklized-issuer-base-abi';
import { INonMerklizedIssuer } from '@iden3/onchain-non-merklized-issuer-base-abi';
import {
CredentialStatusType,
DisplayMethod,
DisplayMethodType,
Iden3SparseMerkleTreeProof,
W3CCredential
} from '../../../../../../verifiable';
import { Merklizer, Path } from '@iden3/js-jsonld-merklization';
import { XSDNS } from '../../../../../../circuits';
import { Hash, Proof } from '@iden3/js-merkletree';
import { JsonDocumentObject } from '../../../../../../iden3comm';
import { ethers } from 'ethers';
import { getDateFromUnixTimestamp } from '@iden3/js-iden3-core';
import { Options } from '@iden3/js-jsonld-merklization';
import { EthConnectionConfig } from '../../../../state';
enum NonMerklizedIssuerInterfaces {
InterfaceDetection = '0x01ffc9a7',
InterfaceNonMerklizedIssuer = '0x58874949',
InterfaceGetCredential = '0x5d1ca631'
}
enum ValueHashes {
BooleanTrue = '18586133768512220936620570745912940619677854269274689475585506675881198879027',
BooleanFalse = '19014214495641488759237505126948346942972912379615652741039992445865937985820'
}
/**
* `OnchainNonMerklizedIssuerAdapter` provides functionality to interact with a non-merklized on-chain credential issuer.
* This adapter enables interface detection, credential retrieval, and conversion to the W3C Verifiable Credential format.
*
* @public
* @beta
* @class OnchainNonMerklizedIssuerAdapter
*/
export class OnchainNonMerklizedIssuerAdapter {
private readonly _contract: NonMerklizedIssuerBase;
private readonly _contractAddress: string;
private readonly _chainId: number;
private readonly _issuerDid: DID;
private readonly _merklizationOptions?: Options;
/**
* Initializes an instance of `OnchainNonMerklizedIssuerAdapter`.
*
* @param ethConnectionConfig The configuration for the Ethereum connection.
* @param issuerDid The decentralized identifier (DID) of the issuer.
* @param merklizationOptions Optional settings for merklization.
*/
constructor(
ethConnectionConfig: EthConnectionConfig,
issuerDid: DID,
options?: {
merklizationOptions?: Options;
}
) {
if (!ethConnectionConfig.chainId) {
throw new Error('Chain ID is required');
}
this._chainId = ethConnectionConfig.chainId;
this._contractAddress = ethers.getAddress(
ethers.hexlify(Id.ethAddressFromId(DID.idFromDID(issuerDid)))
);
this._contract = NonMerklizedIssuerBase__factory.connect(
this._contractAddress,
new ethers.JsonRpcProvider(ethConnectionConfig.url)
);
this._issuerDid = issuerDid;
this._merklizationOptions = options?.merklizationOptions;
}
/**
* Checks if the contract supports required interfaces.
* Throws an error if any required interface is unsupported.
*
* @throws Error - If required interfaces are not supported.
*/
public async isInterfaceSupported() {
const supportedInterfaces = [
{
name: 'Interface detection ERC-165',
value: NonMerklizedIssuerInterfaces.InterfaceDetection
},
{
name: 'Interface non-merklized issuer',
value: NonMerklizedIssuerInterfaces.InterfaceNonMerklizedIssuer
},
{
name: 'Interface get credential',
value: NonMerklizedIssuerInterfaces.InterfaceGetCredential
}
];
const unsupportedInterfaces = await Promise.all(
supportedInterfaces.map(async (interfaceObj) => {
const isSupported = await this._contract.supportsInterface(interfaceObj.value);
return isSupported ? null : interfaceObj.name;
})
);
const unsupportedInterfacesFiltered = unsupportedInterfaces.filter(
(interfaceName) => interfaceName !== null
);
if (unsupportedInterfacesFiltered.length > 0) {
throw new Error(`Unsupported interfaces: ${unsupportedInterfacesFiltered.join(', ')}`);
}
}
/**
* Retrieves a credential from the on-chain non-merklized contract.
* @param userId The user's core.Id.
* @param credentialId The unique identifier of the credential.
*/
public async getCredential(
userId: Id,
credentialId: bigint
): Promise<{
credentialData: INonMerklizedIssuer.CredentialDataStructOutput;
coreClaimBigInts: bigint[];
credentialSubjectFields: INonMerklizedIssuer.SubjectFieldStructOutput[];
}> {
const [credentialData, coreClaimBigInts, credentialSubjectFields] =
await this._contract.getCredential(userId.bigInt(), credentialId);
return { credentialData, coreClaimBigInts, credentialSubjectFields };
}
/**
* Retrieves the credential IDs of a user.
* @param userId The user's core.Id.
* @returns An array of credential IDs.
*/
public async getUserCredentialsIds(userId: Id): Promise<bigint[]> {
return this._contract.getUserCredentialIds(userId.bigInt());
}
/**
* Converts on-chain credential to a verifiable credential.
*
* @param credentialData Data structure of the credential from the contract.
* @param coreClaimBigInts Claim data in bigint format.
* @param credentialSubjectFields Subject fields of the credential.
*/
public async convertOnChainInfoToW3CCredential(
credentialData: INonMerklizedIssuer.CredentialDataStructOutput,
coreClaimBigInts: bigint[],
credentialSubjectFields: INonMerklizedIssuer.SubjectFieldStructOutput[]
): Promise<W3CCredential> {
const c = new Claim().unMarshalJson(JSON.stringify(coreClaimBigInts.map((b) => b.toString())));
const credentialSubject = await this.convertCredentialSubject(
c,
credentialData.context,
credentialData._type,
credentialSubjectFields
);
const credentialRequest = {
id: this.credentialId(credentialData.id),
credentialSchema: credentialData.credentialSchema.id,
type: credentialData._type,
credentialSubject: credentialSubject,
expiration: c.getExpirationDate()?.getTime(),
displayMethod: this.convertDisplayMethod(credentialData.displayMethod),
context: credentialData.context,
revocationOpts: {
id: this._contractAddress,
nonce: Number(c.getRevocationNonce()),
type: CredentialStatusType.Iden3OnchainSparseMerkleTreeProof2023
},
issuanceDate: getDateFromUnixTimestamp(Number(credentialData.issuanceDate)).getTime()
};
const existenceProof = await this.existenceProof(c);
const w3c = W3CCredential.fromCredentialRequest(this._issuerDid, credentialRequest);
w3c.proof = [existenceProof];
return w3c;
}
private credentialId(id: bigint): string {
return `urn:iden3:onchain:${this._chainId}:${this._contractAddress}:${id}`;
}
private async convertCredentialSubject(
coreClaim: Claim,
contractContexts: string[],
credentialType: string,
credentialSubjectFields: INonMerklizedIssuer.SubjectFieldStructOutput[]
): Promise<JsonDocumentObject> {
const contractContextsStr = JSON.stringify({
'@context': contractContexts
});
const credentialSubject = {} as JsonDocumentObject;
for (const f of credentialSubjectFields) {
const dataType = await Path.newTypeFromContext(
contractContextsStr,
`${credentialType}.${f.key}`,
this._merklizationOptions
);
switch (dataType) {
case XSDNS.Boolean: {
switch (f.rawValue.toString()) {
case ValueHashes.BooleanTrue:
credentialSubject[f.key] = true;
break;
case ValueHashes.BooleanFalse:
credentialSubject[f.key] = false;
break;
}
break;
}
case (XSDNS.NonNegativeInteger,
XSDNS.NonPositiveInteger,
XSDNS.NegativeInteger,
XSDNS.PositiveInteger): {
credentialSubject[f.key] = f.value.toString();
break;
}
case XSDNS.Integer: {
credentialSubject[f.key] = Number(f.value);
break;
}
case XSDNS.String: {
this.validateSourceValue(dataType, f.value, f.rawValue);
credentialSubject[f.key] = f.rawValue;
break;
}
case XSDNS.DateTime: {
const timestamp = BigInt(f.rawValue);
const sourceTimestamp = getDateFromUnixTimestamp(Number(timestamp)).toISOString();
this.validateSourceValue(dataType, f.value, sourceTimestamp);
credentialSubject[f.key] = sourceTimestamp;
break;
}
case XSDNS.Double: {
const rawFloat = Number(f.rawValue);
this.validateSourceValue(dataType, f.value, rawFloat);
credentialSubject[f.key] = rawFloat;
break;
}
default: {
throw new Error(`Unsupported data type ${dataType}`);
}
}
}
credentialSubject['type'] = credentialType;
const subjectId = coreClaim.getId();
const subjectDid = DID.parseFromId(subjectId);
credentialSubject['id'] = subjectDid.string();
return credentialSubject;
}
private async existenceProof(coreClaim: Claim): Promise<Iden3SparseMerkleTreeProof> {
const [mtpProof, stateInfo] = await this._contract.getClaimProofWithStateInfo(
coreClaim.hIndex()
);
if (!mtpProof.existence) {
throw new Error('Claim does not exist');
}
const latestStateHash = Hash.fromBigInt(stateInfo.state);
const latestClaimsOfRootHash = Hash.fromBigInt(stateInfo.claimsRoot);
const latestRevocationOfRootHash = Hash.fromBigInt(stateInfo.revocationsRoot);
const latestRootsOfRootHash = Hash.fromBigInt(stateInfo.rootsRoot);
const p = new Proof({
siblings: mtpProof.siblings.map((s) => Hash.fromBigInt(s)),
existence: mtpProof.existence,
nodeAux: mtpProof.auxExistence
? {
key: Hash.fromBigInt(mtpProof.auxIndex),
value: Hash.fromBigInt(mtpProof.auxValue)
}
: undefined
});
return new Iden3SparseMerkleTreeProof({
issuerData: {
id: this._issuerDid,
state: {
value: latestStateHash,
claimsTreeRoot: latestClaimsOfRootHash,
revocationTreeRoot: latestRevocationOfRootHash,
rootOfRoots: latestRootsOfRootHash
}
},
mtp: p,
coreClaim: coreClaim
});
}
private async validateSourceValue(dataType: string, originHash: bigint, source: unknown) {
const sourceHash = await Merklizer.hashValue(dataType, source);
if (sourceHash !== originHash) {
throw new Error(`Invalid source value for ${dataType} type`);
}
}
private convertDisplayMethod(
onchainDisplayMethod: INonMerklizedIssuer.DisplayMethodStructOutput
): DisplayMethod | undefined {
if (!onchainDisplayMethod.id || !onchainDisplayMethod._type) {
return undefined;
}
switch (onchainDisplayMethod._type) {
case DisplayMethodType.Iden3BasicDisplayMethodV1: {
return {
id: onchainDisplayMethod.id,
type: DisplayMethodType.Iden3BasicDisplayMethodV1
};
}
default: {
throw new Error(`Unsupported display method type ${onchainDisplayMethod._type}`);
}
}
}
}