@ew-did-registry/did-ethr-resolver
Version:
The package resolve CRUD operations on DID Documents
452 lines (430 loc) • 12.8 kB
text/typescript
/* eslint-disable no-restricted-syntax */
import { Contract, ethers, Event, utils, BigNumber } from 'ethers';
import {
DIDAttribute,
Encoding,
IAuthentication,
IOperator,
IPublicKey,
IServiceEndpoint,
IUpdateData,
IAttributePayload,
PubKeyType,
KeyTags,
RegistrySettings,
IUpdateAttributeData,
} from '@ew-did-registry/did-resolver-interface';
import { Methods } from '@ew-did-registry/did';
import { KeyType } from '@ew-did-registry/keys';
import Resolver from './resolver';
import { delegatePubKeyIdPattern, pubKeyIdPattern } from '../constants';
import { encodedPubKeyName, hexify, addressOf } from '../utils';
import { EwSigner } from './ewSigner';
const { PublicKey, ServicePoint } = DIDAttribute;
const { formatBytes32String } = utils;
/**
* To support/extend this Class, one just has to work with this file.
* All the supporting functions are stored as private methods (i.e. with the '_' symbol)
* One can easily extend the methods available by researching the smart contract functionality,
* as well as by understanding how the read is performed.
*/
export class Operator extends Resolver implements IOperator {
/**
* ERC-1056 compliant ethereum smart-contract
*/
private _didRegistry: Contract;
private _owner: EwSigner;
private readonly _keys = {
privateKey: '',
publicKey: '',
};
private address?: string;
/**
* @param owner - Entity which controls document
* @param settings - Settings to connect to Ethr registry
*/
constructor(owner: EwSigner, settings: RegistrySettings) {
super(owner.provider, settings);
const { address, abi } = this.settings;
this._owner = owner;
this._keys.publicKey = owner.publicKey;
this._didRegistry = new ethers.Contract(address, abi, owner);
}
protected async getAddress(): Promise<string> {
if (!this.address) {
this.address = await this._owner.getAddress();
}
return this.address as string;
}
private async did(): Promise<string> {
return `did:${this.settings.method}:${await this.getAddress()}`;
}
public getPublicKey(): string {
return this._keys.publicKey;
}
/**
* Relevant did should have positive cryptocurrency balance to perform
* the transaction. Create method saves the public key in smart contract's
* event, which can be qualified as document creation
*
* @param did
* @param context
* @returns Promise<boolean>
*/
async create(): Promise<boolean> {
const did = await this.did();
const readPubKey = await this.readOwnerPubKey(did);
if (readPubKey) {
return true;
}
const attribute = DIDAttribute.PublicKey;
const updateData: IUpdateData = {
algo: KeyType.Secp256k1,
type: PubKeyType.VerificationKey2018,
encoding: Encoding.HEX,
value: { publicKey: `0x${this.getPublicKey()}`, tag: KeyTags.OWNER },
};
await this.update(did, attribute, updateData);
return true;
}
/**
* Sets attribute value in DID document identified by the did
*
* @example
*```typescript
* import {
* Operator, DIDAttribute, Algorithms, PubKeyType, Encoding
* } from '@ew-did-registry/did-resolver';
* import { Keys } from '@ew-did-registry/keys';
* const providerSettings = {
* type: ProviderTypes.HTTP,
* uriOrInfo: 'https://volta-rpc.energyweb.org',
* }
* const ownerKeys = new Keys();
* const owner = EwSigner.fromPrivateKey(ownerKeys.privateKey, providerSettings);
* const operator = new Operator(
* owner,
* resolverSettings,
* );
* const pKey = DIDAttribute.PublicKey;
* const updateData = {
* algo: Algorithms.ED25519,
* type: PubKeyType.VerificationKey2018,
* encoding: Encoding.HEX,
* value: new Keys().publicKey,
* };
* const validity = 10 * 60 * 1000;
* const updated = await operator.update(did, pKey, updateData, validity);
* ```
*
* @param { string } did - did associated with DID document
* @param { DIDAttribute } didAttribute - specifies updated section in DID document. Must be 31
* bytes or shorter
* @param { IUpdateData } updateData
* @param { number } validity - time in milliseconds during which
* attribute will be valid
* @returns Promise<number>
*/
async update(
did: string,
didAttribute: DIDAttribute,
updateData: IUpdateData,
validity: number = Number.MAX_SAFE_INTEGER - 1 // preventing BigNumber.from overflow error
): Promise<BigNumber> {
const method =
didAttribute === PublicKey || didAttribute === ServicePoint
? 'setAttribute'
: 'addDelegate';
if (validity < 0) {
throw new Error('Validity must be non negative value');
}
if (didAttribute === ServicePoint) {
if (!updateData.value?.serviceEndpoint) {
throw new Error('Service Endpoint is required');
}
const userDIDDoc = await this.read(did);
for (const svc of userDIDDoc.service) {
if (svc.serviceEndpoint === updateData.value?.serviceEndpoint) {
throw new Error('Service Endpoint already exist');
}
}
}
return this._sendTransaction(
method,
did,
didAttribute,
updateData,
validity
);
}
/**
* Revokes the delegate from DID Document
* Returns true on success
*
* @param { string } did - did of identity of interest
* @param { PubKeyType } delegateType - type of delegate of interest
* @param { string } delegate - did of delegate of interest
* @returns Promise<boolean>
*/
async revokeDelegate(
did: string,
delegateType: PubKeyType,
delegateDID: string
): Promise<boolean> {
await this._sendTransaction(
'revokeDelegate',
did,
DIDAttribute.Authenticate,
{
type: delegateType,
delegate: addressOf(delegateDID),
}
);
return true;
}
/**
* Revokes attribute from DID Document
* Returns true on success
*
* @param { string } did - did of identity of interest
* @param { DIDAttribute } attributeType - type of attribute to revoke
* @param { IUpdateData } updateData - data required to identify the correct attribute to revoke
* @returns Promise<boolean>
*/
async revokeAttribute(
did: string,
attributeType: DIDAttribute,
updateData: IUpdateAttributeData
): Promise<boolean> {
await this._sendTransaction(
'revokeAttribute',
did,
attributeType,
updateData
);
return true;
}
/**
* Changes the owner of particular decentralised identity
* Returns true on success
*
* @param { string } did - did of current identity owner
* @param { string } newOwner - did of new owner that will be set on success
* @returns Promise<boolean>
*/
async changeOwner(did: string, newOwner: string): Promise<boolean> {
try {
const tx = await this._didRegistry.changeOwner(
addressOf(did),
addressOf(newOwner)
);
const receipt = await tx.wait();
const event = receipt.events.find(
(e: Event) => e.event === 'DIDOwnerChanged'
);
if (!event) return false;
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message);
}
}
return true;
}
/**
* Revokes authentication methods, public keys and delegates from DID document
*
* @example
* ```typescript
*import { Operator } from '@ew-did-registry/did-resolver';
*import { Keys } from '@ew-did-registry/keys';
*
* const providerSettings = {
* type: ProviderTypes.HTTP,
* uriOrInfo: 'https://volta-rpc.energyweb.org',
* }
* const ownerKeys = new Keys();
* const owner = EwSigner.fromPrivateKey(ownerKeys.privateKey, providerSettings);
* const operator = new Operator(
* owner,
* resolverSettings,
* );
* const updated = await operator.deactivate(did);
* ```
*
* @param did
* @returns Promise<boolean>
*/
async deactivate(did: string): Promise<void> {
const document = await this.read(did);
await this._revokeAuthentications(
did,
document.authentication as IAuthentication[],
document.publicKey
);
await this._revokePublicKeys(did, document.publicKey);
await this._revokeServices(did, document.service);
}
/**
* Revokes authentication attributes
*
* @param did
* @param auths
* @param publicKeys
* @private
*/
protected async _revokeAuthentications(
did: string,
auths: IAuthentication[],
publicKeys: IPublicKey[]
): Promise<void> {
for await (const pk of publicKeys) {
const match = pk.id.match(delegatePubKeyIdPattern);
if (match) {
const type = auths.find((auth) => auth.publicKey === match[0])
? PubKeyType.SignatureAuthentication2018
: PubKeyType.VerificationKey2018;
await this.revokeDelegate(
did,
type,
`did:${Methods.Erc1056}:${pk.ethereumAddress}`
);
}
}
}
/**
* Revokes Public key attribute
*
* @param did
* @param publicKeys
* @private
*/
protected async _revokePublicKeys(
did: string,
publicKeys: IPublicKey[]
): Promise<void> {
for await (const pk of publicKeys) {
const match = pk.id.match(pubKeyIdPattern);
if (match) {
const encoding = Object.values(Encoding).find(
(enc) => pk[encodedPubKeyName(enc)]
) as Encoding;
await this.revokeAttribute(did, DIDAttribute.PublicKey, {
type: DIDAttribute.PublicKey,
value: {
id: pk.id,
publicKey: pk[encodedPubKeyName(encoding)] as string,
tag: pk.id.split('#')[1],
},
});
}
}
}
/**
* Revokes service attributes
*
* @param did
* @param services
* @private
*/
protected async _revokeServices(
did: string,
services: IServiceEndpoint[]
): Promise<void> {
for await (const service of services) {
await this.revokeAttribute(did, DIDAttribute.ServicePoint, {
type: DIDAttribute.ServicePoint,
value: {
id: service.id,
type: service.type,
serviceEndpoint: service.serviceEndpoint,
},
});
}
}
/**
* Private function to send transactions
*
* @param method
* @param did
* @param didAttribute
* @param updateData
* @param validity
* @param overrides
* @private
*/
protected async _sendTransaction(
method: string,
did: string,
didAttribute: DIDAttribute,
updateData: IUpdateData,
validity?: number,
overrides?: {
nonce?: number;
}
): Promise<BigNumber> {
const identity = addressOf(did);
const name = formatBytes32String(
this._composeAttributeName(didAttribute, updateData)
);
const value = hexify(
didAttribute === PublicKey || didAttribute === ServicePoint
? (updateData.value as IAttributePayload)
: (updateData.delegate as string)
);
const params: (string | number | Record<string, unknown>)[] = [
identity,
name,
value,
];
if (validity !== undefined) {
params.push(validity);
}
if (overrides) {
params.push(overrides);
}
try {
const tx = await this._didRegistry[method](...params);
const receipt = await tx.wait();
const event: Event = receipt.events.find(
(e: Event) =>
(didAttribute === DIDAttribute.PublicKey &&
e.event === 'DIDAttributeChanged') ||
(didAttribute === DIDAttribute.ServicePoint &&
e.event === 'DIDAttributeChanged') ||
(didAttribute === DIDAttribute.Authenticate &&
e.event === 'DIDDelegateChanged')
);
return BigNumber.from(event.blockNumber as number);
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message);
}
}
return BigNumber.from(0);
}
/**
* Util functions to create attribute name, supported by read method
*
* @param attribute
* @param updateData
* @private
*/
protected _composeAttributeName(
attribute: DIDAttribute,
updateData: IUpdateData
): string {
const { algo, type, encoding } = updateData;
switch (attribute) {
case DIDAttribute.PublicKey:
return `did/${DIDAttribute.PublicKey}/${algo}/${type}/${encoding}`;
case DIDAttribute.Authenticate:
return updateData.type;
case DIDAttribute.ServicePoint:
return `did/${DIDAttribute.ServicePoint}/${
(updateData.value as IAttributePayload).type
}`;
default:
throw new Error('Unknown attribute name');
}
}
}