UNPKG

@ew-did-registry/did-ethr-resolver

Version:

The package resolve CRUD operations on DID Documents

465 lines (434 loc) 12.6 kB
import { Contract, ethers, providers, utils, BigNumber } from 'ethers'; import { IDIDDocument, IDIDLogData, IPublicKey, IAttributePayload, IServiceEndpoint, RegistrySettings, IAuthentication, DocumentSelector, AttributeChangedEvent, DelegateChangedEvent, DidEventNames, } from '@ew-did-registry/did-resolver-interface'; import { attributeNamePattern } from '../constants'; import { addressOf, matchDIDPattern } from '../utils'; /** * This function updates the document if the event type is 'DelegateChange' * * @param event * @param did * @param document * @param validTo * @param block */ const handleDelegateChange = ( event: DelegateChangedEvent, did: string, document: IDIDLogData, validTo: BigNumber, block: number ): IDIDLogData => { const stringDelegateType = ethers.utils.parseBytes32String( event.values.delegateType ); const publicKeyID = `${did}#delegate-${stringDelegateType}-${event.values.delegate}`; const publicKeyBlock = document.publicKey[publicKeyID]?.block; if ( document.publicKey[publicKeyID] === undefined || (publicKeyBlock !== undefined && publicKeyBlock < block) ) { switch (stringDelegateType) { case 'sigAuth': document.authentication[publicKeyID] = { type: 'sigAuth', publicKey: publicKeyID, validity: validTo, block, }; // eslint-disable-next-line no-fallthrough case 'veriKey': document.publicKey[publicKeyID] = { id: publicKeyID, type: 'Secp256k1VerificationKey2018', controller: did, ethereumAddress: event.values.delegate, validity: validTo, block, }; break; default: break; } } return document; }; /** * This function updates the document on Attribute change event * * @param event * @param did * @param document * @param validTo * @param block */ const handleAttributeChange = ( event: AttributeChangedEvent, did: string, document: IDIDLogData, validTo: BigNumber, block: number ): IDIDLogData => { const matchDID = matchDIDPattern(did); const identity = matchDID[1]; const attributeType = event.values.name; const stringAttributeType = ethers.utils.parseBytes32String(attributeType); const match = stringAttributeType.match(attributeNamePattern); if (match) { const section = match[1]; const algo = match[2]; const type = match[4]; const encoding = match[6]; if (section === 'pub') { let publicKeysPayload: IAttributePayload; try { const parsed = JSON.parse( Buffer.from(event.values.value.slice(2), 'hex').toString() ); if (typeof parsed === 'object') { publicKeysPayload = parsed; } else { return document; } } catch (e) { return document; } const pk: IPublicKey = { id: `${did}#${publicKeysPayload.tag}`, type: `${algo}${type}`, controller: identity, validity: validTo, block, }; const publicKeyBlock = document.publicKey[pk.id]?.block; if ( document.publicKey[pk.id] === undefined || (publicKeyBlock !== undefined && publicKeyBlock < block) ) { switch (encoding) { case null: case undefined: case 'hex': pk.publicKeyHex = publicKeysPayload.publicKey; break; case 'base64': pk.publicKeyBase64 = Buffer.from( event.values.value.slice(2), 'hex' ).toString('base64'); break; case 'pem': pk.publicKeyPem = Buffer.from( event.values.value.slice(2), 'hex' ).toString(); break; default: break; } document.publicKey[pk.id] = pk; } return document; } if (section === 'svc') { const servicePoint: IServiceEndpoint = JSON.parse( Buffer.from(event.values.value.slice(2), 'hex').toString() ); servicePoint.validity = validTo; servicePoint.block = block; const serviceEndpointBlock = document.service[servicePoint.id]?.block; if ( document.service[servicePoint.id] === undefined || (serviceEndpointBlock !== undefined && serviceEndpointBlock < block) ) { document.service[servicePoint.id] = servicePoint; } return document; } return document; } const attrBlock = document.attributes.get(stringAttributeType) ?.block as number; if (!attrBlock || attrBlock < block) { const attributeData = { attribute: Buffer.from(event.values.value.slice(2), 'hex').toString(), validity: validTo, block, }; document.attributes.set(stringAttributeType, attributeData); } return document; }; /** * Update document checks the event validity, and, if valid, * passes the event parsing to the handler * * @param event * @param eventName * @param did * @param document * @param block */ const updateDocument = ( event: AttributeChangedEvent | DelegateChangedEvent, did: string, document: IDIDLogData, block: number ): IDIDLogData => { const { validTo } = event.values; if (validTo) { switch (event.name) { case DidEventNames.AttributeChanged: return handleAttributeChange(event, did, document, validTo, block); case DidEventNames.DelegateChanged: return handleDelegateChange(event, did, document, validTo, block); default: return document; } } return document; }; /** * Given a certain block from the chain, this function returns the events * associated with the did within the block * * @param block * @param did * @param document * @param provider * @param contractInterface * @param address */ const getEventsFromBlock = ( block: ethers.BigNumber, did: string, document: IDIDLogData, provider: ethers.providers.Provider, contractInterface: utils.Interface, address: string ): Promise<unknown> => new Promise((resolve, reject) => { const identity = addressOf(did); provider .getLogs({ address, fromBlock: block.toNumber(), toBlock: block.toNumber(), topics: [ null, `0x000000000000000000000000${identity.slice(2).toLowerCase()}`, ] as string[], }) .then((log) => { const { name, args, signature, topic } = contractInterface.parseLog( log[0] ); const event = { name, values: args, signature, topic, } as unknown as AttributeChangedEvent | DelegateChangedEvent; updateDocument(event, did, document, block.toNumber()); resolve(event.values.previousChange); }) .catch((error) => { reject(error); }); }); export const query = ( document: IDIDDocument, selector: DocumentSelector ): IPublicKey | IServiceEndpoint | IAuthentication | undefined => { const attrName = Object.keys(selector)[0] as keyof DocumentSelector; const attributes = Object.values(document[attrName]); if (attributes.length === 0) { return undefined; } const filter = Object.entries( selector[attrName] as | Partial<IPublicKey> | Partial<IAuthentication> | Partial<IServiceEndpoint> ); return attributes.find((a) => filter.every(([prop, val]) => a[prop] && a[prop] === val) ); }; /** * A high level function that manages the flow to read data from the blockchain * * @param did * @param document * @param registrySettings * @param contract * @param provider */ export const fetchDataFromEvents = async ( did: string, document: IDIDLogData, registrySettings: Required<RegistrySettings>, contract: Contract, provider: providers.Provider, selector?: DocumentSelector ): Promise<void> => { const identity = addressOf(did); let nextBlock; let topBlock; try { nextBlock = await contract.changed(identity); topBlock = nextBlock; } catch (error) { throw new Error('Blockchain address did not interact with smart contract'); } if (nextBlock) { document.owner = await contract.owners(identity); } else if (identity) { document.owner = identity; } const contractInterface = new ethers.utils.Interface( JSON.stringify(registrySettings.abi) ); const { address } = registrySettings; while ( nextBlock.toNumber() !== 0 && nextBlock.toNumber() >= document.topBlock.toNumber() ) { // eslint-disable-next-line no-await-in-loop nextBlock = await getEventsFromBlock( nextBlock, did, document, provider, contractInterface, address ); if (selector) { const attribute = query(document as unknown as IDIDDocument, selector); if (attribute) { return; } } } document.topBlock = topBlock; }; /** * The logs from ERC1056 have a validity and a block number */ interface ILogWithValidityAndBlock { validity?: BigNumber; block?: number; } /** * Makes a copy of a log event and remove the validity and block * The log is used to construct DID Document, * but we don't want to include validity and block in DID Document. * It ,akes a copy of the logs so as to not remove from the original log * (as that log maybe need those properties elsewhere) * @param log log event from ERC1056 * @returns copy of log without validity and block */ const copyAndRemoveValidityAndBlock = <T extends ILogWithValidityAndBlock>( log: T ): Omit<T, 'validity' | 'block'> => { const copy = { ...log }; if (log.block) { delete copy.block; } if (log.validity) { delete copy.validity; } return copy; }; /** * Provided with the fetched data, the function parses it and returns the * DID Document associated with the relevant user * * @param did * @param document * @param context */ export const wrapDidDocument = ( did: string, document: IDIDLogData, context = 'https://www.w3.org/ns/did/v1' ): IDIDDocument => { const now = BigNumber.from(Math.floor(new Date().getTime() / 1000)); const publicKey: IPublicKey[] = []; const authentication = [ { type: 'owner', publicKey: `${did}#owner`, // -1 is preventing BigNumber.from overflow error https://github.com/ethers-io/ethers.js/discussions/1582 validity: BigNumber.from(Number.MAX_SAFE_INTEGER - 1), }, ]; const didDocument: IDIDDocument = { '@context': context, id: did, publicKey, authentication, service: [], }; // eslint-disable-next-line guard-for-in,no-restricted-syntax for (const key in document.publicKey) { const pubKey = document.publicKey[key]; const pubKeyValidity = pubKey.validity?.gt(now); if (pubKeyValidity) { const pubKeyCopy = copyAndRemoveValidityAndBlock(pubKey); didDocument.publicKey.push(pubKeyCopy as IPublicKey); } } // eslint-disable-next-line guard-for-in,no-restricted-syntax for (const key in document.authentication) { const authenticator = document.authentication[key]; const authenticatorValidity = authenticator.validity?.gt(now); if (authenticatorValidity) { const authenticatorCopy = copyAndRemoveValidityAndBlock(authenticator); didDocument.authentication.push(authenticatorCopy as IAuthentication); } } // eslint-disable-next-line guard-for-in,no-restricted-syntax for (const key in document.service) { const serviceEndpoint = document.service[key]; const serviceEndpointValidity = serviceEndpoint.validity?.gt(now); if (serviceEndpointValidity) { const serviceEndpointCopy = copyAndRemoveValidityAndBlock(serviceEndpoint); didDocument.service.push(serviceEndpointCopy as IServiceEndpoint); } } return didDocument; }; /** * Restore document from partially read logs * * @param logs {IDIDLogData[]} */ export const mergeLogs = (logs: IDIDLogData[]): IDIDLogData => { logs = logs.sort((a, b) => a.topBlock.sub(b.topBlock).toNumber()); return logs.reduce((doc, log) => { doc.service = { ...doc.service, ...log.service }; doc.publicKey = { ...doc.publicKey, ...log.publicKey }; doc.authentication = { ...doc.authentication, ...log.authentication }; return doc; }, logs[0]); }; export const documentFromLogs = ( did: string, logs: IDIDLogData[] ): IDIDDocument => { const mergedLogs: IDIDLogData = mergeLogs(logs); return wrapDidDocument(did, mergedLogs); };