From 79b03d27b4ec698cc979f67781d0bff46802889d Mon Sep 17 00:00:00 2001 From: olegnn Date: Sat, 3 Feb 2024 11:05:57 +0400 Subject: [PATCH 01/12] Script tweaks --- scripts/helpers.js | 22 ++++++++++++++-------- scripts/set-storage.js | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 scripts/set-storage.js diff --git a/scripts/helpers.js b/scripts/helpers.js index 343dbb43d..b860d7cc4 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -306,13 +306,18 @@ export const formatDock = (value, config = {}) => * @param {function(dock: DockAPI, ...args: any[]): Promise} * @returns {function(...args: any[]): Promise} */ -export const withDockAPI = curry((params, fn) => async (...args) => { +export const withDockAPI = curry(({ senderAccountURI, ...params }, fn) => async (...args) => { console.log("Connecting..."); let err, res; const dockAPI = new DockAPI(); try { await dockAPI.init(params); + if (senderAccountURI) { + const account = dockAPI.keyring.addFromUri(senderAccountURI); + dockAPI.setAccount(account); + } + res = await fn(dockAPI, ...args); } catch (e) { err = e; @@ -380,14 +385,14 @@ export const addLoggerPrefix = curry((buildPrefix, logger) => ); /** - * Returns hash and number of the first block satisfying provided predicate. - * Throws an error in case such block can't be found. + * Returns hash and number of the first block satisfying the provided predicate. + * Throws an error in case such a block can't be found. * * @template T * @param {*} api - * @param {{ start: number, end: number, targetValue: T, fetchValue: (number) => Promise, checkBlock: (block: any) => boolean }} - * @param {{ maxBlocksPerUnit: number = null, debug = false }} targetEra - * @returns {{ number: number, hash: * }} + * @param {{ start: number, end: number, targetValue: T, fetchValue: (number) => Promise, checkBlock: (block: any) => Promise }} + * @param {{ maxBlocksPerUnit: number = null, debug = false }} + * @returns {{ number: number, hash: any }} */ export const binarySearchFirstSatisfyingBlock = curry( async ( @@ -405,7 +410,7 @@ export const binarySearchFirstSatisfyingBlock = curry( const midBlockHash = await api.rpc.chain.getBlockHash(midBlockNumber); const midValue = await fetchValue(midBlockHash); - if (debug) + if (debug) { timestampLogger.log( "target value:", targetValue, @@ -418,6 +423,7 @@ export const binarySearchFirstSatisfyingBlock = curry( "jumps:", jumps ); + } if (midValue < targetValue) { startBlockNumber = midBlockNumber + 1; @@ -445,7 +451,7 @@ export const binarySearchFirstSatisfyingBlock = curry( } const midBlock = await api.derive.chain.getBlock(midBlockHash); - const found = checkBlock(midBlock); + const found = await checkBlock(midBlock); if (found) { if (debug) diff --git a/scripts/set-storage.js b/scripts/set-storage.js new file mode 100644 index 000000000..c9a5ed3f0 --- /dev/null +++ b/scripts/set-storage.js @@ -0,0 +1,16 @@ +import { toPairs } from "ramda"; +import { withDockAPI } from "./helpers"; +import fs from 'fs'; + +const { FullNodeEndpoint, SudoSecretURI } = process.env; +const [_, __, filePath] = process.argv; + +async function main(dock, filePath) { + const keyValuePairs = JSON.parse(fs.readFileSync(filePath)); + console.log('Setting', JSON.stringify(keyValuePairs, null, 2)); + const setStorageTx = dock.api.tx.system.setStorage(toPairs(keyValuePairs)); + return dock.signAndSend(dock.api.tx.sudo.sudo(setStorageTx)) +} + +withDockAPI({ senderAccountURI: SudoSecretURI, address: FullNodeEndpoint })(main)(filePath).catch(console.error); + From 656582400ed94741401dc18327e38ae3871c44b4 Mon Sep 17 00:00:00 2001 From: olegnn Date: Mon, 5 Feb 2024 11:10:31 +0400 Subject: [PATCH 02/12] Tweaks for `did:key:*` and `did:dock:*` impls --- src/modules/did/did.js | 4 +- src/modules/schema.js | 4 +- src/resolver/generic/const.js | 2 +- src/resolver/generic/multi-resolver.js | 6 +- src/resolver/generic/resolver.js | 2 +- src/utils/codec.js | 1 + src/utils/did.js | 473 ------------------ src/utils/did/constants.js | 15 + src/utils/did/did-keypair.js | 56 +++ src/utils/did/index.js | 4 + src/utils/did/typed-did/did-method-key.js | 163 ++++++ .../typed-did/dock-did-or-did-method-key.js | 136 +++++ src/utils/did/typed-did/dock-did.js | 76 +++ src/utils/did/typed-did/helpers.js | 82 +++ src/utils/did/typed-did/index.js | 4 + src/utils/did/utils.js | 127 +++++ src/utils/inheritance.js | 4 +- src/utils/misc.js | 75 ++- src/utils/vc/schema.js | 4 +- tests/integration/did/did-key.test.js | 11 +- tests/integration/schema.test.js | 5 +- .../integration/trust-registry-module.test.js | 4 +- tests/unit/did.test.js | 4 +- 23 files changed, 744 insertions(+), 518 deletions(-) delete mode 100644 src/utils/did.js create mode 100644 src/utils/did/constants.js create mode 100644 src/utils/did/did-keypair.js create mode 100644 src/utils/did/index.js create mode 100644 src/utils/did/typed-did/did-method-key.js create mode 100644 src/utils/did/typed-did/dock-did-or-did-method-key.js create mode 100644 src/utils/did/typed-did/dock-did.js create mode 100644 src/utils/did/typed-did/helpers.js create mode 100644 src/utils/did/typed-did/index.js create mode 100644 src/utils/did/utils.js diff --git a/src/modules/did/did.js b/src/modules/did/did.js index 8b5dc969e..eed80dbaf 100644 --- a/src/modules/did/did.js +++ b/src/modules/did/did.js @@ -10,7 +10,7 @@ import { NoOnchainDIDError, NoOffchainDIDError, createDidSig, - DockDIDMethodKeyQualifier, + DidMethodKeyQualifier, } from '../../utils/did'; import { getStateChange } from '../../utils/misc'; @@ -681,7 +681,7 @@ class DIDModule { * @return {string} The DID identifier. */ getFullyQualifiedDIDMethodKey(didMethodKey) { - return `${DockDIDMethodKeyQualifier}${didMethodKey}`; + return `${DidMethodKeyQualifier}${didMethodKey}`; } /** diff --git a/src/modules/schema.js b/src/modules/schema.js index 84a624fa3..0d76db191 100644 --- a/src/modules/schema.js +++ b/src/modules/schema.js @@ -1,7 +1,7 @@ import { canonicalize } from 'json-canonicalize'; import { validate } from 'jsonschema'; -import { hexDIDToQualified } from '../utils/did'; +import { typedHexDID, typedHexDIDFromSubstrate } from '../utils/did'; import { createNewDockBlobId, @@ -140,7 +140,7 @@ export default class Schema { return { ...chainValue, id, - author: hexDIDToQualified(chainBlob[0]), + author: typedHexDIDFromSubstrate(dockApi, chainBlob[0]).toQualifiedString(), }; } throw new Error('Incorrect schema format'); diff --git a/src/resolver/generic/const.js b/src/resolver/generic/const.js index 8322336a1..90cdd1e11 100644 --- a/src/resolver/generic/const.js +++ b/src/resolver/generic/const.js @@ -9,6 +9,6 @@ export const METHOD_REG_EXP_PATTERN = '([a-zA-Z0-9_]+)'; export const HEX_ID_REG_EXP_PATTERN = '(0x[0-9a-fA-F]+)'; /** - * Use to specify `PREFIX`/`METHOD` that will be dispatched in case no direct matches found. + * Use to specify `PREFIX`/`METHOD` that will be dispatched in case no direct matches are found. */ export const WILDCARD = Symbol.for('@docknetwork/sdk/wildcard-resolver'); diff --git a/src/resolver/generic/multi-resolver.js b/src/resolver/generic/multi-resolver.js index 83bc6dfa7..fa107ec28 100644 --- a/src/resolver/generic/multi-resolver.js +++ b/src/resolver/generic/multi-resolver.js @@ -23,14 +23,14 @@ import { ensureString } from '../../utils/type-helpers'; */ export default class MultiResolver extends Resolver { /** - * Matching string prefix, array of string prefixes, or wildcard pattern. + * Matching string prefix, an array of string prefixes, or wildcard pattern. * @type {Array | string | symbol} * @abstract * @static */ static PREFIX; /** - * Matching string method, array of string methods, or wildcard pattern. + * Matching string method, an array of string methods, or wildcard pattern. * @type {Array | string | symbol} * @abstract * @static @@ -58,7 +58,7 @@ export default class MultiResolver extends Resolver { if (!resolverList.length) { throw new Error( - 'No resolvers were provided. You need to either implement `resolve` or provide a list of resolvers.', + 'No resolvers were provided. You need to either implement `resolve` or provide a list of resolvers', ); } } diff --git a/src/resolver/generic/resolver.js b/src/resolver/generic/resolver.js index 69400492c..a471a0337 100644 --- a/src/resolver/generic/resolver.js +++ b/src/resolver/generic/resolver.js @@ -80,7 +80,7 @@ class Resolver { } /** - * Resolves an entity if it's identifier is supported. + * Resolves an entity if its identifier is supported. * Each resolver must have static `string` or `WILDCARD` symbol properties `PREFIX` and `METHOD`, * then the `supports` method will return `true` if they match the identifier. * In case the resolver must be used for any `PREFIX`/`METHOD` as default, use the `WILDCARD` symbol. diff --git a/src/utils/codec.js b/src/utils/codec.js index 0539b0058..0b9601dec 100644 --- a/src/utils/codec.js +++ b/src/utils/codec.js @@ -30,6 +30,7 @@ export function isHexWithGivenByteSize(value, byteSize = undefined) { */ export function getHexIdentifier(id, qualifiers, byteSize) { const strId = String(id); + for (const qualifier of [].concat(qualifiers)) { if (strId.startsWith(qualifier)) { // Fully qualified ID. Remove the qualifier diff --git a/src/utils/did.js b/src/utils/did.js deleted file mode 100644 index 087c1d56f..000000000 --- a/src/utils/did.js +++ /dev/null @@ -1,473 +0,0 @@ -// This file will be turned to a folder and will have files like `did/dock.js` and `did/ethr.js` - -// Import some utils from Polkadot JS -// eslint-disable-next-line max-classes-per-file -import { randomAsHex, encodeAddress } from '@polkadot/util-crypto'; -import { u8aToHex, hexToU8a } from '@polkadot/util'; -import { base58btc } from 'multiformats/bases/base58'; -import bs58 from 'bs58'; -import varint from 'varint'; - -import { isHexWithGivenByteSize, getHexIdentifier } from './codec'; -import { - PublicKeyEd25519, - PublicKeySecp256k1, - PublicKey, // eslint-disable-line - VerificationRelationship, // eslint-disable-line -} from '../public-keys'; - -import { Signature } from "../signatures"; // eslint-disable-line -import { - getPublicKeyFromKeyringPair, - getSignatureFromKeyringPair, - getStateChange, -} from './misc'; -import { parseDIDUrl } from '../resolver/did/did-resolver'; - -export const DockDIDMethod = 'dock'; -export const Secp256k1PublicKeyPrefix = 'zQ3s'; -export const Ed25519PublicKeyPrefix = 'z6Mk'; - -export const DockDIDQualifier = `did:${DockDIDMethod}:`; -export const DockDIDMethodKeyQualifier = 'did:key:'; -export const DockDIDByteSize = 32; -export const DockDIDMethodKeySecp256k1ByteSize = 33; -export const DockDIDMethodKeyEd25519ByteSize = 32; - -export const DockDidMethodKeySecp256k1Prefix = `${DockDIDMethodKeyQualifier}${Secp256k1PublicKeyPrefix}`; -export const DockDidMethodKeyEd25519Prefix = `${DockDIDMethodKeyQualifier}${Ed25519PublicKeyPrefix}`; - -const DidKeyBytePrefixED25519 = new Uint8Array([0xed, 0x01]); -const DidKeyBytePrefixSecp256k1 = new Uint8Array([0xe7, 0x01]); - -export class DidKeypair { - constructor(keyPair, keyId) { - this.keyPair = keyPair; - this.keyId = keyId; - } - - publicKey() { - return getPublicKeyFromKeyringPair(this.keyPair); - } - - sign(message) { - return getSignatureFromKeyringPair(this.keyPair, message); - } - - /** - * Create a new keypair from a DockAPI object. - * @param {DockAPI} dockApi - * @param seed - Generates 32 byte random seed if not provided - * @param keypairType - Defaults to ed25519. - * @param meta - * @param keyId - Defaults to 1 - * @returns {DidKeypair} - */ - static fromApi( - dockApi, - { - seed = randomAsHex(32), - keypairType = 'ed25519', - meta = null, - keyId = 1, - } = {}, - ) { - return new DidKeypair( - dockApi.keyring.addFromUri(seed, meta, keypairType), - keyId, - ); - } -} - -/** - * Either `did:*` or `did:key:*`. - */ -export class DockDidOrDidMethodKey { - get asDid() { - throw new Error('Not a `Did`'); - } - - get asDidMethodKey() { - throw new Error('Not a `DidMethodKey`'); - } - - get isDid() { - return false; - } - - get isDidMethodKey() { - return false; - } - - /** - * Creates signature for the state change with supplied arguments. - * - * @param api - * @param name - * @param payload - * @param keyRef - * @returns {object} - */ - signStateChange(api, name, payload, keyRef) { - const stateChange = getStateChange(api, name, payload); - const keySignature = keyRef.sign(stateChange); - - return createDidSig(this, keyRef, keySignature); - } - - /** - * Creates a transaction that will modify the chain state. - * - * @param api - * @param method - * @param name - * @param payload - * @param keyRef - */ - changeState(api, method, name, payload, keyRef) { - const signature = this.signStateChange(api, name, payload, keyRef); - - return method(payload, signature); - } -} - -/** - * `did:*` - */ -export class DockDid extends DockDidOrDidMethodKey { - constructor(did) { - super(); - this.did = did; - } - - get isDid() { - return true; - } - - get asDid() { - return this.did; - } - - toJSON() { - return { Did: this.did }; - } - - toString() { - return `${DockDIDQualifier}${this.asDid}`; - } - - toStringSS58() { - return `${DockDIDQualifier}${encodeAddress(this.asDid)}`; - } -} - -/** - * `did:key:*` - */ -export class DockDidMethodKey extends DockDidOrDidMethodKey { - constructor(didMethodKey) { - super(); - - if (didMethodKey instanceof PublicKeyEd25519) { - this.didMethodKey = { ed25519: didMethodKey.value }; - } else if (didMethodKey instanceof PublicKeySecp256k1) { - this.didMethodKey = { secp256k1: didMethodKey.value }; - } else { - throw new Error('Unsupported public key type'); - } - } - - get isDidMethodKey() { - return true; - } - - get asDidMethodKey() { - return this.didMethodKey; - } - - toJSON() { - return { - DidMethodKey: this.didMethodKey.ed25519 - ? { Ed25519: this.didMethodKey.ed25519 } - : { Secp256k1: this.didMethodKey.secp256k1 }, - }; - } - - toString() { - return this.toStringBS58(); - } - - toStringBS58() { - return `${DockDIDMethodKeyQualifier}${this.fingerprint()}`; - } - - get publicKey() { - return this.didMethodKey.ed25519 || this.didMethodKey.secp256k1; - } - - fingerprint() { - // Convert the hex public key to bytes - if (!this.publicKey) { - throw new Error('Unsupported public key type'); - } - - // Define the prefix for ed25519 DID key - const publicKeyBytes = hexToU8a(this.publicKey); - const prefix = this.didMethodKey.ed25519 ? DidKeyBytePrefixED25519 : DidKeyBytePrefixSecp256k1; - - // Concatenate the prefix and the public key bytes - const didKeyBytes = new Uint8Array(prefix.length + publicKeyBytes.length); - didKeyBytes.set(prefix); - didKeyBytes.set(publicKeyBytes, prefix.length); - - // Encode the concatenated bytes to Base58 with z prefix - return `z${bs58.encode(didKeyBytes)}`; - } -} - -/** - * Error thrown when a DID document lookup was successful, but the DID in question does not exist. - * This is different from a network error. - */ -export class NoDIDError extends Error { - constructor(did) { - super(`DID (${did}) does not exist`); - this.name = 'NoDIDError'; - this.did = did; - this.message = `A DID document lookup was successful, but the DID in question does not exist (${did}). This is different from a network error.`; - } -} - -/** - * Error thrown when a DID exists on chain but is an off-chain DID, meaning the DID document exists off-chain. - */ -export class NoOnchainDIDError extends Error { - constructor(did) { - super(`DID (${did}) is an off-chain DID`); - this.name = 'NoOnchainDIDError'; - this.did = did; - this.message = 'The DID exists on chain but is an off-chain DID, meaning the DID document exists off-chain.'; - } -} - -/** - * Error thrown when a DID exists on chain and is an on-chain DID but the lookup was performed for an off-chain DID. - */ -export class NoOffchainDIDError extends Error { - constructor(did) { - super(`DID (${did}) is an on-chain DID`); - this.name = 'NoOffchainDIDError'; - this.did = did; - this.message = 'The DID exists on chain and is an on-chain DID but the lookup was performed for an off-chain DID.'; - } -} - -/** - * Check if the given identifier is the hex representation of a Dock DID. - * @param {string} identifier - The identifier to check. - * @return {void} Throws exception if invalid identifier - */ -export function validateDockDIDHexIdentifier(identifier) { - // Byte size of the Dock DID identifier, i.e. the `DockDIDQualifier` is not counted. - if (!isHexWithGivenByteSize(identifier, DockDIDByteSize)) { - throw new Error(`DID identifier must be ${DockDIDByteSize} bytes`); - } -} - -/** - * Check if the given identifier is 32 byte valid SS58 string - * @param {string} identifier - The identifier to check. - * @return {void} Throws exception if invalid identifier - */ -export function validateDockDIDSS58Identifier(identifier) { - // base58-check regex - const regex = new RegExp(/^[5KL][1-9A-HJ-NP-Za-km-z]{47}$/); - const matches = regex.exec(identifier); - if (!matches) { - throw new Error('The identifier must be 32 bytes and valid SS58 string'); - } -} - -/** - * Temporary solution for the DID's backward compatibility. - * - * -------------------------------------------------------- - */ -// eslint-disable-next-line no-extend-native -Object.defineProperty(String.prototype, 'asDid', { - get() { - if (this.isDid) { - return String(this); - } else { - throw new Error('Not a `Did`'); - } - }, -}); -// eslint-disable-next-line no-extend-native -Object.defineProperty(String.prototype, 'isDid', { - get() { - return isHexWithGivenByteSize(String(this), DockDIDByteSize); - }, -}); -// eslint-disable-next-line no-extend-native -Object.defineProperty(String.prototype, 'toStringSS58', { - get() { - return new DockDid(this.asDid).toStringSS58(); - }, -}); - -/** - * -------------------------------------------------------- - */ - -export function getDidKeyPublicKeyHex(did) { - const parsed = parseDIDUrl(did); - const multicodecPubKey = base58btc.decode(parsed.id); - varint.decode(multicodecPubKey); // NOTE: called to get byte length below - const pubKeyBytes = multicodecPubKey.slice(varint.decode.bytes); - return u8aToHex(pubKeyBytes); -} - -/** - * Takes a DID string, gets the hexadecimal value of that and returns a `DockDidMethodKey` or `DockDid` object. - * @param api - * @param {string} did - The DID can be passed as fully qualified DID like `did:dock:` or - * `did:key:` or a 32 byte hex string - * @return {string|DockDidOrDidMethodKey} Returns a `string` or `DockDidMethodKey` or `DockDid` object. - */ -export function typedHexDID(api, did) { - const strDid = did.toString(); - if (api.specVersion < 50) { - return getHexIdentifier(strDid, DockDIDQualifier, DockDIDByteSize); - } - - if (strDid.startsWith(DockDidMethodKeySecp256k1Prefix)) { - const hex = getDidKeyPublicKeyHex(strDid); - return new DockDidMethodKey(new PublicKeySecp256k1(hex)); - } else if (strDid.startsWith(DockDidMethodKeyEd25519Prefix)) { - const hex = getDidKeyPublicKeyHex(strDid); - return new DockDidMethodKey(new PublicKeyEd25519(hex)); - } else { - const hex = getHexIdentifier(strDid, DockDIDQualifier, DockDIDByteSize); - - return new DockDid(hex); - } -} - -/** - * Gets the hexadecimal value of the given DID received from the substrate side. - * @param api - * @param {string} did - The DID can be passed as fully qualified DID like `did:dock:` or - * a 32 byte hex string - * @return {string|DockDidOrDidMethodKey} Returns an object wrapping the DID. - */ -export function typedHexDIDFromSubstrate(api, did) { - if (api.specVersion < 50) { - return getHexIdentifier(did, DockDIDQualifier, DockDIDByteSize); - } else if (did.isDid) { - const hex = getHexIdentifier(u8aToHex(did.asDid), [], DockDIDByteSize); - - return new DockDid(hex); - } else if (did.isDidMethodKey) { - const key = did.asDidMethodKey; - - if (key.isSecp256k1) { - const hex = getHexIdentifier( - u8aToHex(key.asSecp256k1), - [], - DockDIDMethodKeySecp256k1ByteSize, - ); - - return new DockDidMethodKey(new PublicKeySecp256k1(hex)); - } else if (key.isEd25519) { - const hex = getHexIdentifier( - u8aToHex(key.asEd25519), - [], - DockDIDMethodKeyEd25519ByteSize, - ); - - return new DockDidMethodKey(new PublicKeyEd25519(hex)); - } else { - throw new Error(`Invalid did key: provided: \`${key}\``); - } - } else { - throw new Error(`Invalid did provided: \`${did}\``); - } -} - -/** - * Return a fully qualified Dock DID id, i.e. "did:dock:" - * @param {string|object} hexDid - The hex DID (without the qualifier) or wrapper on DID - * @returns {string} - The fully qualified DID - */ -export function hexDIDToQualified(hexDid) { - if (typeof hexDid?.toStringSS58 === 'function') { - return hexDid.toStringSS58(); - } else { - const ss58Address = encodeAddress(hexDid); - - return `${DockDIDQualifier}${ss58Address}`; - } -} - -/** - * Create and return a fully qualified Dock DID, i.e. "did:dock:" - * @returns {string} - The DID - */ -export function createNewDockDID() { - const hexId = randomAsHex(DockDIDByteSize); - return hexDIDToQualified(hexId); -} - -/** - * Returns a `DidKey` as expected by the Substrate node - * @param {PublicKey} publicKey - The public key for the DID. The Keyring is intentionally avoided here as it may not be - * accessible always, like in case of hardware wallet - * @param {VerificationRelationship} verRel - * @returns {object} - The object has structure and keys with same names as expected by the Substrate node - */ -export function createDidKey(publicKey, verRel) { - return { - publicKey: publicKey.toJSON(), - verRels: verRel.value, - }; -} - -/** - * - * @param {string|DockDidOrDidMethodKey} did - DID as string or an object - * @param {number} keyId - - * @param rawSig - * @param {Signature} sig - * @returns {object} - */ -export function createDidSig(did, { keyId }, rawSig) { - const sig = rawSig.toJSON(); - - if (typeof did === 'string') { - return { - did, - keyId, - sig, - }; - } else if (did.isDid) { - return { - DidSignature: { - did: did.asDid, - keyId, - sig, - }, - }; - } else if (did.isDidMethodKey) { - return { - DidMethodKeySignature: { - didMethodKey: did.asDidMethodKey, - sig, - }, - }; - } else { - throw new Error( - `Incorrect DID passed: \`${did}\`, expected instance of either \`DockDid\` or \`DockDidMethodKey\``, - ); - } -} diff --git a/src/utils/did/constants.js b/src/utils/did/constants.js new file mode 100644 index 000000000..588cd9e5e --- /dev/null +++ b/src/utils/did/constants.js @@ -0,0 +1,15 @@ +export const DockDIDMethod = "dock"; +export const Secp256k1PublicKeyPrefix = "zQ3s"; +export const Ed25519PublicKeyPrefix = "z6Mk"; + +export const DockDIDQualifier = `did:${DockDIDMethod}:`; +export const DidMethodKeyQualifier = "did:key:"; +export const DockDIDByteSize = 32; +export const DidMethodKeySecp256k1ByteSize = 33; +export const DidMethodKeyEd25519ByteSize = 32; + +export const DidMethodKeySecp256k1Prefix = `${DidMethodKeyQualifier}${Secp256k1PublicKeyPrefix}`; +export const DidMethodKeyEd25519Prefix = `${DidMethodKeyQualifier}${Ed25519PublicKeyPrefix}`; + +export const DidMethodKeyBytePrefixEd25519 = new Uint8Array([0xed, 0x01]); +export const DidMethodKeyBytePrefixSecp256k1 = new Uint8Array([0xe7, 0x01]); diff --git a/src/utils/did/did-keypair.js b/src/utils/did/did-keypair.js new file mode 100644 index 000000000..e3582ae1c --- /dev/null +++ b/src/utils/did/did-keypair.js @@ -0,0 +1,56 @@ +import { randomAsHex } from "@polkadot/util-crypto"; +import { DockKeyPair } from "../misc"; + +/** + * Signing keypair along with the optional key identifier. + */ +export class DidKeypair extends DockKeyPair { + /** + * Wraps supplied keypair into a `DidKeypair`. + * + * @param {*} keyPair + * @param {?number} keyId + */ + constructor(keyPair, keyId = void 0) { + super(keyPair); + this.keyId = keyId; + } + + /** + * Create a new keypair from a DockAPI object. + * @param {DockAPI} dockApi + * @param seed - Generates 32-byte random seed if not provided + * @param keypairType - Defaults to ed25519. + * @param meta + * @param keyId - Defaults to 1 + * @returns {DidKeypair} + */ + static fromApi( + dockApi, + { + seed = randomAsHex(32), + keypairType = "ed25519", + meta = null, + keyId = 1, + } = {} + ) { + return new DidKeypair( + dockApi.keyring.addFromUri(seed, meta, keypairType), + keyId + ); + } + + /** + * Generates random `Secp256k1` keypair. + * + * @param {?Object} params + * @property {?number} keyId - optional keypair identifier + * @returns {this} + */ + static randomSecp256k1({ keyId = void 0 } = {}) { + const keypair = super.randomSecp256k1.call(this); + keypair.keyId = keyId; + + return keypair; + } +} diff --git a/src/utils/did/index.js b/src/utils/did/index.js new file mode 100644 index 000000000..beb115591 --- /dev/null +++ b/src/utils/did/index.js @@ -0,0 +1,4 @@ +export * from './utils'; +export * from './did-keypair'; +export * from './constants'; +export * from './typed-did'; diff --git a/src/utils/did/typed-did/did-method-key.js b/src/utils/did/typed-did/did-method-key.js new file mode 100644 index 000000000..194020ce2 --- /dev/null +++ b/src/utils/did/typed-did/did-method-key.js @@ -0,0 +1,163 @@ +import { u8aToHex, hexToU8a } from "@polkadot/util"; +import { base58btc } from "multiformats/bases/base58"; +import bs58 from "bs58"; +import varint from "varint"; + +import { getHexIdentifier } from "../../codec"; +import { PublicKeyEd25519, PublicKeySecp256k1 } from "../../../public-keys"; + +import { parseDIDUrl } from "../../../resolver/did/did-resolver"; + +import { + DidMethodKeyQualifier, + DidMethodKeyBytePrefixEd25519, + DidMethodKeyBytePrefixSecp256k1, + DidMethodKeyEd25519ByteSize, + DidMethodKeySecp256k1ByteSize, +} from "../constants"; +import { DockKeyPair } from "../../misc"; +import DockDidOrDidMethodKey from './dock-did-or-did-method-key'; + +/** + * `did:key:*` + * + * As of now, the public key can be either `PublicKeyEd25519` or `PublicKeySecp256k1`. + */ +export default class DidMethodKey extends DockDidOrDidMethodKey { + /** + * Instantiates `did:key:*` using supplied public key. + * As of now, the public key can be either `PublicKeyEd25519` or `PublicKeySecp256k1`. + * + * @param {PublicKeyEd25519|PublicKeySecp256k1} didMethodKey + */ + constructor(didMethodKey) { + super(); + + if (didMethodKey instanceof PublicKeyEd25519) { + this.didMethodKey = { ed25519: didMethodKey.value }; + } else if (didMethodKey instanceof PublicKeySecp256k1) { + this.didMethodKey = { secp256k1: didMethodKey.value }; + } else { + throw new Error("Unsupported public key type"); + } + } + + /** + * Creates a new `DidMethodKey` from the supplied keypair. + * + * @param {DockKeyPair} keypair + * @returns {this} + */ + static fromKeypair(keypair) { + return new this(keypair.publicKey()); + } + + /** + * Instantiates `DidMethodKey` from a fully qualified did string. + * @param {string} did - fully qualified `did:key:*` string + * @returns {this} + */ + static fromQualifiedString(did) { + const { id } = parseDIDUrl(did); + + const multicodecPubKey = base58btc.decode(id); + varint.decode(multicodecPubKey); // NOTE: called to get byte length below + const pubKeyBytes = multicodecPubKey.slice(varint.decode.bytes); + const pubkeyHex = u8aToHex(pubKeyBytes); + + if (id.startsWith(DidMethodKeySecp256k1Prefix)) { + return new this(new PublicKeySecp256k1(pubkeyHex)); + } else if (id.startsWith(DidMethodKeyEd25519Prefix)) { + return new this(new PublicKeyEd25519(pubkeyHex)); + } else { + throw new Error("Unsupported `did:key:*`"); + } + } + + /** + * Instantiates `DockDid` from a did method key object received from the substrate side. + * @param {object} key - substrate did method key + * @returns {this} + */ + static fromSubstrate(did) { + const key = did.asDidMethodKey; + + if (key.isSecp256k1) { + const hex = getHexIdentifier( + u8aToHex(key.asSecp256k1), + [], + DidMethodKeySecp256k1ByteSize + ); + + return new this(new PublicKeySecp256k1(hex)); + } else if (key.isEd25519) { + const hex = getHexIdentifier( + u8aToHex(key.asEd25519), + [], + DidMethodKeyEd25519ByteSize + ); + + return new this(new PublicKeyEd25519(hex)); + } else { + throw new Error(`Invalid \`did:key:*\`: provided: \`${key}\``); + } + } + + get isDidMethodKey() { + return true; + } + + get asDidMethodKey() { + return this.didMethodKey; + } + + toJSON() { + return { + DidMethodKey: this.didMethodKey.ed25519 + ? { Ed25519: this.didMethodKey.ed25519 } + : { Secp256k1: this.didMethodKey.secp256k1 }, + }; + } + + toString() { + return this.toQualifiedString(); + } + + /** + * Returns fully qualified public key encoded in `BS58` according to the `did:key:*` spec. + */ + toQualifiedString() { + // Convert the hex public key to bytes + if (!this.publicKey) { + throw new Error("Unsupported public key type"); + } + + // Define the prefix for ed25519 DID key + const publicKeyBytes = hexToU8a(this.publicKey.value); + const prefix = this.didMethodKey.ed25519 + ? DidMethodKeyBytePrefixEd25519 + : DidMethodKeyBytePrefixSecp256k1; + + // Concatenate the prefix and the public key bytes + const didKeyBytes = new Uint8Array(prefix.length + publicKeyBytes.length); + didKeyBytes.set(prefix); + didKeyBytes.set(publicKeyBytes, prefix.length); + + // Encode the concatenated bytes to Base58 with z prefix + const encodedDidKey = `z${bs58.encode(didKeyBytes)}`; + + return `${DidMethodKeyQualifier}${encodedDidKey}`; + } + + /** + * Returns underlying public key. + * @returns {PublicKeyEd25519|PublicKeySecp256k1} + */ + get publicKey() { + return this.didMethodKey.ed25519 + ? new PublicKeyEd25519(this.didMethodKey.ed25519) + : new PublicKeySecp256k1(this.didMethodKey.secp256k1); + } +} + +DockDidOrDidMethodKey.DidMethodKey = DidMethodKey; diff --git a/src/utils/did/typed-did/dock-did-or-did-method-key.js b/src/utils/did/typed-did/dock-did-or-did-method-key.js new file mode 100644 index 000000000..58e953e29 --- /dev/null +++ b/src/utils/did/typed-did/dock-did-or-did-method-key.js @@ -0,0 +1,136 @@ +import { getStateChange } from "../../misc"; +import { createDidSig } from "../utils"; + +import { DockDIDQualifier, DidMethodKeyQualifier } from "../constants"; + +/** + * Either `did:dock:*` or `did:key:*`. + */ +export default class DockDidOrDidMethodKey { + /** + * @type {typeof this} + */ + static DockDid; + /** + * @type {typeof this} + */ + static DidMethodKey; + + /** + * Instantiates `DockDid` or `DidMethodKey` from a fully qualified did string or a raw hex did. + * @param {string} did - fully qualified `dock:did:` or `dock:key:` or a raw hex DID + * @returns {this} + */ + static fromString(did) { + if (did.startsWith("0x")) { + return new this.DockDid(did); + } else { + return DockDidOrDidMethodKey.fromQualifiedString(did); + } + } + + /** + * Instantiates `DockDid` or `DidMethodKey` from a fully qualified did string. + * @param {string} did - fully qualified `dock:did:` or `dock:key:` string + * @returns {this} + */ + static fromQualifiedString(did) { + if (did.startsWith(DockDIDQualifier)) { + return this.DockDid.fromQualifiedString(did); + } else if (did.startsWith(DidMethodKeyQualifier)) { + return this.DidMethodKey.fromQualifiedString(did); + } else { + throw new Error( + `Unsupported did string: ${did}, expected either \`dock:did:\` or \`dock:key:\`` + ); + } + } + + /** + * Instantiates `DockDid` or `DockDidMethodKey` from a did or did method key object received from the substrate side. + * @param {object} did - substrate did or did method key + * @returns {this} + */ + static fromSubstrate(did) { + if (did.isDid) { + return this.DockDid.fromSubstrate(did); + } else if (did.isDidMethodKey) { + return this.DidMethodKey.fromSubstrate(did); + } else { + throw new Error(`Invalid \`did:*\` provided: \`${did}\``); + } + } + + /** + * Extracts raw underlying value if it's `did:dock:*`, throws an error otherwise. + */ + get asDid() { + throw new Error("Not a `Did`"); + } + + /** + * Extracts raw underlying value if it's `did:key:*`, throws an error otherwise. + */ + get asDidMethodKey() { + throw new Error("Not a `DidMethodKey`"); + } + + /** + * Returns `true` if the underlying value is a `did:dock:*`. + */ + get isDid() { + return false; + } + + /** + * Returns `true` if the underlying value is a `did:key:*`. + */ + get isDidMethodKey() { + return false; + } + + /** + * Creates signature for the state change with supplied arguments. + * + * @param api + * @param name + * @param payload + * @param keyRef + * @returns {object} + */ + signStateChange(api, name, payload, keyRef) { + const stateChange = getStateChange(api, name, payload); + const keySignature = keyRef.sign(stateChange); + + return createDidSig(this, keyRef, keySignature); + } + + /** + * Creates a transaction that will modify the chain state. + * + * @param api + * @param method + * @param name + * @param payload + * @param keyRef + */ + changeState(api, method, name, payload, keyRef) { + const signature = this.signStateChange(api, name, payload, keyRef); + + return method(payload, signature); + } + + /** + * Converts underlying object to the `JSON` representation suitable for substrate JSON-RPC. + */ + toJSON() { + throw new Error("Unimplemented"); + } + + /** + * Returns fully qualified `did:dock:*` encoded in SS58 or `did:key:* encoded in BS58. + */ + toQualifiedString() { + throw new Error("Unimplemented"); + } +} diff --git a/src/utils/did/typed-did/dock-did.js b/src/utils/did/typed-did/dock-did.js new file mode 100644 index 000000000..6a893bba9 --- /dev/null +++ b/src/utils/did/typed-did/dock-did.js @@ -0,0 +1,76 @@ +import { randomAsHex, encodeAddress } from "@polkadot/util-crypto"; +import { getHexIdentifier } from "../../codec"; + +import { validateDockDIDHexIdentifier } from "../utils"; + +import { DockDIDByteSize, DockDIDQualifier } from "../constants"; +import DockDidOrDidMethodKey from "./dock-did-or-did-method-key"; + +/** + * `did:dock:*` + */ +export default class DockDid extends DockDidOrDidMethodKey { + /** + * Instantiates `DockDid` using supplied 32-byte hex sequence. + * @param {*} did + */ + constructor(did) { + super(); + validateDockDIDHexIdentifier(did); + + this.did = did; + } + + /** + * Generates a random DID. + * + * @returns {this} + */ + static random() { + return new this(randomAsHex(DockDIDByteSize)); + } + + /** + * Instantiates `DockDid` from a fully qualified did string. + * @param {string} did - fully qualified `did:dock:*` string + * @returns {this} + */ + static fromQualifiedString(did) { + return new this(getHexIdentifier(did, DockDIDQualifier, DockDIDByteSize)); + } + + /** + * Instantiates `DockDid` from a did object received from the substrate side. + * @param {object} did - substrate did + * @returns {this} + */ + static fromSubstrate(did) { + return new this(getHexIdentifier(did.asDid, [], DockDIDByteSize)); + } + + get isDid() { + return true; + } + + get asDid() { + return this.did; + } + + toJSON() { + return { Did: this.did }; + } + + toString() { + return `${DockDIDQualifier}${this.asDid}`; + } + + /** + * Returns fully qualified DID encoded as a `SS58` address. + */ + toQualifiedString() { + return `${DockDIDQualifier}${encodeAddress(this.asDid)}`; + } +} + + +DockDidOrDidMethodKey.DockDid = DockDid; diff --git a/src/utils/did/typed-did/helpers.js b/src/utils/did/typed-did/helpers.js new file mode 100644 index 000000000..99c229fa9 --- /dev/null +++ b/src/utils/did/typed-did/helpers.js @@ -0,0 +1,82 @@ + +import { getHexIdentifier } from "../../codec"; + +import { ApiPromise } from "@polkadot/api"; +import DockDidOrDidMethodKey from './dock-did-or-did-method-key'; +import DockDid from './dock-did'; +import DidMethodKey from './did-method-key'; // eslint-disable-line + +import { DockDIDQualifier, DockDIDByteSize } from '../constants'; + +/** + * Takes a DID string, gets the hexadecimal value of that and returns either the `DockDid` or `DidMethodKey` object. + * @param {ApiPromise} api + * @param {string|DockDid|DidMethodKey} did - The DID can be passed as fully qualified DID like `did:dock:` or + * `did:key:` or a 32 byte hex string + * @return {string|DockDid|DidMethodKey} Returns a `string` or `DockDid` or `DidMethodKey` object. + */ +export function typedHexDID(api, did) { + if (api.specVersion < 50) { + return getHexIdentifier(did, DockDIDQualifier, DockDIDByteSize); + } + + if (did instanceof DockDidOrDidMethodKey) { + return did; + } else { + return DockDidOrDidMethodKey.fromString(String(did)); + } +} + +/** + * Gets the hexadecimal value of the given DID received from the substrate side. + * @param {ApiPromise} api + * @param {object} did - The DID can be passed as fully qualified DID like `did:dock:` or + * a 32 byte hex string + * @return {string|DockDid|DidMethodKey} Returns a `string` or `DockDid` or `DidMethodKey` object. + */ +export function typedHexDIDFromSubstrate(api, did) { + if (api.specVersion < 50) { + return getHexIdentifier(did, DockDIDQualifier, DockDIDByteSize); + } else { + return DockDidOrDidMethodKey.fromSubstrate(did); + } +} + +/** + * Create and return a fully qualified Dock DID, i.e. "did:dock:" + * @returns {string} - The DID + */ +export const createNewDockDID = () => DockDid.random().toQualifiedString(); + + +/** + * Temporary solution for the DID's backward compatibility. + * + * -------------------------------------------------------- + */ +// eslint-disable-next-line no-extend-native +Object.defineProperty(String.prototype, "asDid", { + get() { + if (this.isDid) { + return String(this); + } else { + throw new Error("Not a `Did`"); + } + }, +}); +// eslint-disable-next-line no-extend-native +Object.defineProperty(String.prototype, "isDid", { + get() { + return isHexWithGivenByteSize(String(this), DockDIDByteSize); + }, +}); +// eslint-disable-next-line no-extend-native +Object.defineProperty(String.prototype, "toQualifiedString", { + get() { + return new DockDid(this.asDid).toQualifiedString(); + }, +}); + +/** + * -------------------------------------------------------- + */ diff --git a/src/utils/did/typed-did/index.js b/src/utils/did/typed-did/index.js new file mode 100644 index 000000000..40ed46d96 --- /dev/null +++ b/src/utils/did/typed-did/index.js @@ -0,0 +1,4 @@ +export { default as DidMethodKey } from "./did-method-key"; +export { default as DockDid } from "./dock-did"; +export { default as DockDidOrDidMethodKey } from "./dock-did-or-did-method-key"; +export * from './helpers'; diff --git a/src/utils/did/utils.js b/src/utils/did/utils.js new file mode 100644 index 000000000..18fa40120 --- /dev/null +++ b/src/utils/did/utils.js @@ -0,0 +1,127 @@ +import { isHexWithGivenByteSize } from "../codec"; +import { + PublicKey, // eslint-disable-line + VerificationRelationship, // eslint-disable-line +} from "../../public-keys"; + +import { Signature } from "../../signatures"; // eslint-disable-line + +import { DockDIDByteSize } from './constants'; + +/** + * Error thrown when a DID document lookup was successful, but the DID in question does not exist. + * This is different from a network error. + */ +export class NoDIDError extends Error { + constructor(did) { + super(`DID (${did}) does not exist`); + this.name = "NoDIDError"; + this.did = did; + this.message = `A DID document lookup was successful, but the DID in question does not exist (${did}). This is different from a network error.`; + } +} + +/** + * Error thrown when a DID exists on chain but is an off-chain DID, meaning the DID document exists off-chain. + */ +export class NoOnchainDIDError extends Error { + constructor(did) { + super(`DID (${did}) is an off-chain DID`); + this.name = "NoOnchainDIDError"; + this.did = did; + this.message = + "The DID exists on chain but is an off-chain DID, meaning the DID document exists off-chain."; + } +} + +/** + * Error thrown when a DID exists on chain and is an on-chain DID but the lookup was performed for an off-chain DID. + */ +export class NoOffchainDIDError extends Error { + constructor(did) { + super(`DID (${did}) is an on-chain DID`); + this.name = "NoOffchainDIDError"; + this.did = did; + this.message = + "The DID exists on chain and is an on-chain DID but the lookup was performed for an off-chain DID."; + } +} + +/** + * Check if the given identifier is the hex representation of a Dock DID. + * @param {string} identifier - The identifier to check. + * @return {void} Throws exception if invalid identifier + */ +export function validateDockDIDHexIdentifier(identifier) { + // Byte size of the Dock DID identifier, i.e. the `DockDIDQualifier` is not counted. + if (!isHexWithGivenByteSize(identifier, DockDIDByteSize)) { + throw new Error(`DID identifier must be ${DockDIDByteSize} bytes`); + } +} + +/** + * Check if the given identifier is 32 byte valid SS58 string + * @param {string} identifier - The identifier to check. + * @return {void} Throws exception if invalid identifier + */ +export function validateDockDIDSS58Identifier(identifier) { + // base58-check regex + const regex = new RegExp(/^[5KL][1-9A-HJ-NP-Za-km-z]{47}$/); + const matches = regex.exec(identifier); + if (!matches) { + throw new Error("The identifier must be 32 bytes and valid SS58 string"); + } +} + +/** + * Returns a `DidKey` as expected by the Substrate node + * @param {PublicKey} publicKey - The public key for the DID. The Keyring is intentionally avoided here as it may not be + * accessible always, like in case of hardware wallet + * @param {VerificationRelationship} verRel + * @returns {object} - The object has structure and keys with same names as expected by the Substrate node + */ +export function createDidKey(publicKey, verRel) { + return { + publicKey: publicKey.toJSON(), + verRels: verRel.value, + }; +} + +/** + * + * @param {string|DockDidOrDidMethodKey} did - DID as string or an object + * @param {number} keyId - + * @param rawSig + * @param {Signature} sig + * @returns {object} + */ +export function createDidSig(did, { keyId }, rawSig) { + const sig = rawSig.toJSON(); + + if (typeof did === "string") { + return { + did, + keyId, + sig, + }; + } else if (did.isDid) { + return { + DidSignature: { + did: did.asDid, + keyId, + sig, + }, + }; + } else if (did.isDidMethodKey) { + return { + DidMethodKeySignature: { + didMethodKey: did.asDidMethodKey, + sig, + }, + }; + } else { + throw new Error( + `Incorrect DID passed: \`${did}\`, expected instance of either \`DockDid\` or \`DidMethodKey\`` + ); + } +} diff --git a/src/utils/inheritance.js b/src/utils/inheritance.js index d4e486a98..18cbf8b05 100644 --- a/src/utils/inheritance.js +++ b/src/utils/inheritance.js @@ -2,7 +2,7 @@ * Enhances the provided class with the given list of static properties to require * in the inherited class. * All properties will be checked for presence during the object constructor call. - * Each property on its own will be checked every time it will be accessed. + * Each property on its own will be checked every time it is accessed. * In case some property is missing, an error will be thrown. * * @template T @@ -16,7 +16,7 @@ export function withExtendedStaticProperties(properties, parentClass) { super(...args); /* - * Ensures that properties were extended properly. + * Ensures that properties are extended properly. */ for (const property of properties) { if (this.constructor[property] == null) { diff --git a/src/utils/misc.js b/src/utils/misc.js index 2be84e5ad..1d3bb2cf2 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -1,20 +1,20 @@ -import elliptic from 'elliptic'; -import { blake2AsHex } from '@polkadot/util-crypto'; +import elliptic from "elliptic"; +import { blake2AsHex, randomAsHex } from "@polkadot/util-crypto"; -import { sha256 } from 'js-sha256'; +import { sha256 } from "js-sha256"; import { PublicKeyEd25519, PublicKeySecp256k1, PublicKeySr25519, // eslint-disable-line -} from '../public-keys'; +} from "../public-keys"; import { SignatureEd25519, SignatureSecp256k1, SignatureSr25519, // eslint-disable-line -} from '../signatures'; +} from "../signatures"; const EC = elliptic.ec; -const secp256k1Curve = new EC('secp256k1'); +const secp256k1Curve = new EC("secp256k1"); /** // TODO: Error handling when `stateChange` is not registered * Helper function to return bytes of a `StateChange` enum. Updates like key change, DID removal, revocation, etc @@ -24,7 +24,7 @@ const secp256k1Curve = new EC('secp256k1'); * @return {array} An array of Uint8 */ export function getBytesForStateChange(api, stateChange) { - return api.createType('StateChange', stateChange).toU8a(); + return api.createType("StateChange", stateChange).toU8a(); } export function getStateChange(api, name, value) { @@ -67,7 +67,7 @@ export function verifyEcdsaSecp256k1Sig(message, signature, publicKey) { export function verifyEcdsaSecp256k1SigPrehashed( messageHash, signature, - publicKey, + publicKey ) { // Remove the leading `0x` const sigHex = signature.value.slice(2); @@ -77,7 +77,7 @@ export function verifyEcdsaSecp256k1SigPrehashed( const pkHex = publicKey.value.slice(2); // Generate public key object. Not extracting the public key for signature as the verifier // should always know what public key is being used. - const pk = secp256k1Curve.keyFromPublic(pkHex, 'hex'); + const pk = secp256k1Curve.keyFromPublic(pkHex, "hex"); return secp256k1Curve.verify(messageHash, sig, pk); } @@ -93,9 +93,9 @@ export function getKeyPairType(pair) { if (pair.ec && pair.priv) { // elliptic library's pair has `ec`, `priv` and `pub`. There is not a cleaner way to detect that - return 'secp256k1'; + return "secp256k1"; } - throw new Error('Cannot detect key pair type'); + throw new Error("Cannot detect key pair type"); } /** @@ -106,9 +106,9 @@ export function getKeyPairType(pair) { export function getPublicKeyFromKeyringPair(pair) { const type = getKeyPairType(pair); let Cls; - if (type === 'ed25519') { + if (type === "ed25519") { Cls = PublicKeyEd25519; - } else if (type === 'sr25519') { + } else if (type === "sr25519") { Cls = PublicKeySr25519; } else { Cls = PublicKeySecp256k1; @@ -125,9 +125,9 @@ export function getPublicKeyFromKeyringPair(pair) { export function getSignatureFromKeyringPair(pair, message) { const type = getKeyPairType(pair); let Cls; - if (type === 'ed25519') { + if (type === "ed25519") { Cls = SignatureEd25519; - } else if (type === 'sr25519') { + } else if (type === "sr25519") { Cls = SignatureSr25519; } else { Cls = SignatureSecp256k1; @@ -135,6 +135,45 @@ export function getSignatureFromKeyringPair(pair, message) { return new Cls(message, pair); } +/** + * Wrapped keypair used to interact with the dock blockchain. + */ +export class DockKeyPair { + /** + * Wraps supplied keypair into a `DockKeyPair`. + * + * @param {*} keyPair + */ + constructor(keyPair) { + this.keyPair = keyPair; + } + + /** + * Returns underlying public key. + */ + publicKey() { + return getPublicKeyFromKeyringPair(this.keyPair); + } + + /** + * Signs supplied message using underlying keypair. + * @param {*} message + */ + sign(message) { + return getSignatureFromKeyringPair(this.keyPair, message); + } + + /** + * Generates random `Secp256k1` keypair. + * + * @param params + * @returns {this} + */ + static randomSecp256k1() { + return new this(generateEcdsaSecp256k1Keypair(randomAsHex(32))); + } +} + /** * Get unique elements from an array as seen by the filterCallback function. * @param {array} a - Array to check for duplicates. @@ -156,7 +195,7 @@ export function getUniqueElementsFromArray(a, filterCallback) { * @returns {string} */ export function encodeExtrinsicAsHash(api, tx) { - return blake2AsHex(api.createType('Call', tx).toU8a()); + return blake2AsHex(api.createType("Call", tx).toU8a()); } /** @@ -170,11 +209,11 @@ export function encodeExtrinsicAsHash(api, tx) { export async function getDidNonce( didOrDidMethodKey, nonce = undefined, - didModule = undefined, + didModule = undefined ) { if (nonce === undefined && didModule === undefined) { throw new Error( - 'Provide either nonce or didModule to fetch nonce but none provided', + "Provide either nonce or didModule to fetch nonce but none provided" ); } if (nonce === undefined) { diff --git a/src/utils/vc/schema.js b/src/utils/vc/schema.js index f8fc66236..ca126b8f2 100644 --- a/src/utils/vc/schema.js +++ b/src/utils/vc/schema.js @@ -1,7 +1,6 @@ import jsonld from 'jsonld'; import { validate } from 'jsonschema'; import defaultDocumentLoader from './document-loader'; -import { hexDIDToQualified } from '../did'; import { expandedSubjectProperty, @@ -9,6 +8,7 @@ import { credentialIDField, credentialContextField, } from './constants'; +import { DockDid } from '../did'; /** * The function uses `jsonschema` package to verify that the expanded `credential`'s subject `credentialSubject` has the JSON @@ -89,7 +89,7 @@ export async function getAndValidateSchemaIfPresent( schemaObj = { ...data, id: schemaUri, - author: hexDIDToQualified(author), + author: author.toQualifiedString(), }; } else { schemaObj = document; diff --git a/tests/integration/did/did-key.test.js b/tests/integration/did/did-key.test.js index 61661f50b..52c7c54e7 100644 --- a/tests/integration/did/did-key.test.js +++ b/tests/integration/did/did-key.test.js @@ -7,7 +7,7 @@ import { typedHexDIDFromSubstrate, NoDIDError, DidKeypair, - DockDidMethodKey, + DidMethodKey, } from '../../../src/utils/did'; import { FullNodeEndpoint, @@ -26,7 +26,6 @@ buildTest('Basic DID tests', () => { let testDidMethodKeyPair1; let testDidMethodKey1; - const testDidMethodKeySeed2 = randomAsHex(32); let testDidMethodKeyPair2; let testDidMethodKey2; @@ -41,12 +40,10 @@ buildTest('Basic DID tests', () => { testDidMethodKeyPair1 = new DidKeypair( dock.keyring.addFromUri(testDidMethodKeySeed1, null, 'ed25519'), ); - testDidMethodKey1 = new DockDidMethodKey(testDidMethodKeyPair1.publicKey()); + testDidMethodKey1 = new DidMethodKey(testDidMethodKeyPair1.publicKey()); - testDidMethodKeyPair2 = new DidKeypair( - generateEcdsaSecp256k1Keypair(testDidMethodKeySeed2), - ); - testDidMethodKey2 = new DockDidMethodKey(testDidMethodKeyPair2.publicKey()); + testDidMethodKeyPair2 = DidKeypair.randomSecp256k1(); + testDidMethodKey2 = DidMethodKey.fromKeypair(testDidMethodKeyPair2); await dock.did.newDidMethodKey(testDidMethodKey1.asDidMethodKey, false); }); diff --git a/tests/integration/schema.test.js b/tests/integration/schema.test.js index b01fda246..1ddf2e0b0 100644 --- a/tests/integration/schema.test.js +++ b/tests/integration/schema.test.js @@ -4,8 +4,7 @@ import { randomAsHex } from '@polkadot/util-crypto'; import { DockAPI } from '../../src/index'; import { - createNewDockDID, typedHexDID, DidKeypair, hexDIDToQualified, -} from '../../src/utils/did'; + createNewDockDID, typedHexDID, DidKeypair } from '../../src/utils/did'; import { FullNodeEndpoint, TestKeyringOpts, TestAccountURI } from '../test-constants'; import { verifyCredential, verifyPresentation } from '../../src/utils/vc/index'; import { blobHexIdToQualified, createNewDockBlobId, DockBlobIdByteSize } from '../../src/modules/blob'; @@ -127,7 +126,7 @@ describe('Schema Blob Module Integration', () => { expect(schemaObj).toMatchObject({ ...exampleSchema, id: blobId, - author: hexDIDToQualified(hexDid), + author: hexDid.toQualifiedString(), }); }, 20000); diff --git a/tests/integration/trust-registry-module.test.js b/tests/integration/trust-registry-module.test.js index c318d4545..e76382916 100644 --- a/tests/integration/trust-registry-module.test.js +++ b/tests/integration/trust-registry-module.test.js @@ -14,7 +14,7 @@ import { import { createNewDockDID, DidKeypair, - DockDidMethodKey, + DidMethodKey, typedHexDID, } from '../../src/utils/did'; import { registerNewDIDUsingPair } from './helpers'; @@ -86,7 +86,7 @@ buildTest('Trust Registry', () => { verifierDIDMethodKeyPair = new DidKeypair( dock.keyring.addFromUri(verifierDIDMethodKeySeed, null, 'ed25519'), ); - verifierDIDMethodKey = new DockDidMethodKey(verifierDIDMethodKeyPair.publicKey()); + verifierDIDMethodKey = new DidMethodKey(verifierDIDMethodKeyPair.publicKey()); // The keyring should be initialized before any test begins as this suite is testing trust registry const account = dock.keyring.addFromUri(TestAccountURI); diff --git a/tests/unit/did.test.js b/tests/unit/did.test.js index 646084d55..497ab4ff2 100644 --- a/tests/unit/did.test.js +++ b/tests/unit/did.test.js @@ -3,14 +3,14 @@ import { randomAsHex, encodeAddress } from '@polkadot/util-crypto'; import { validateDockDIDHexIdentifier, validateDockDIDSS58Identifier, - DockDIDQualifier, DockDIDByteSize, DockDIDMethodKeyQualifier, + DockDIDQualifier, DockDIDByteSize, DidMethodKeyQualifier, typedHexDID, } from '../../src/utils/did'; import { getHexIdentifier } from '../../src/utils/codec'; const hexDid = (did) => getHexIdentifier( did, - [DockDIDQualifier, DockDIDMethodKeyQualifier], + [DockDIDQualifier, DidMethodKeyQualifier], DockDIDByteSize, ); From 4e48124cbdf3580ba3bfb1de1960a99f59d4377d Mon Sep 17 00:00:00 2001 From: olegnn Date: Mon, 5 Feb 2024 11:15:15 +0400 Subject: [PATCH 03/12] Lints --- .jsdoc | 1 + package.json | 3 +- src/modules/schema.js | 2 +- src/utils/did/constants.js | 8 ++--- src/utils/did/did-keypair.js | 10 +++--- src/utils/did/typed-did/did-method-key.js | 29 +++++++-------- .../typed-did/dock-did-or-did-method-key.js | 24 ++++++------- src/utils/did/typed-did/dock-did.js | 11 +++--- src/utils/did/typed-did/helpers.js | 13 +++---- src/utils/did/typed-did/index.js | 6 ++-- src/utils/did/utils.js | 24 ++++++------- src/utils/misc.js | 36 +++++++++---------- src/utils/vc/schema.js | 1 - yarn.lock | 5 +++ 14 files changed, 88 insertions(+), 85 deletions(-) diff --git a/.jsdoc b/.jsdoc index da0866a4e..2e8aed529 100644 --- a/.jsdoc +++ b/.jsdoc @@ -9,6 +9,7 @@ "allowUnknownTags": true, "dictionaries": ["jsdoc","closure"] }, + "plugins": ["node_modules/jsdoc-typeof-plugin"], "templates": { "cleverLinks": false, "monospaceLinks": false diff --git a/package.json b/package.json index 373bdbdcf..7d3d7e68c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/sdk", - "version": "7.2.1", + "version": "7.2.2", "main": "index.js", "license": "MIT", "repository": { @@ -45,6 +45,7 @@ "jest": "^24.5.0", "jest-environment-node": "^24.5.0", "jsdoc": "^3.6.3", + "jsdoc-typeof-plugin": "^1.0.0", "parity-scale-codec": "^0.5.3", "r1csfile": "0.0.41", "ramda": "^0.27.2", diff --git a/src/modules/schema.js b/src/modules/schema.js index 0d76db191..00002d56b 100644 --- a/src/modules/schema.js +++ b/src/modules/schema.js @@ -1,7 +1,7 @@ import { canonicalize } from 'json-canonicalize'; import { validate } from 'jsonschema'; -import { typedHexDID, typedHexDIDFromSubstrate } from '../utils/did'; +import { typedHexDIDFromSubstrate } from '../utils/did'; import { createNewDockBlobId, diff --git a/src/utils/did/constants.js b/src/utils/did/constants.js index 588cd9e5e..b88774c7b 100644 --- a/src/utils/did/constants.js +++ b/src/utils/did/constants.js @@ -1,9 +1,9 @@ -export const DockDIDMethod = "dock"; -export const Secp256k1PublicKeyPrefix = "zQ3s"; -export const Ed25519PublicKeyPrefix = "z6Mk"; +export const DockDIDMethod = 'dock'; +export const Secp256k1PublicKeyPrefix = 'zQ3s'; +export const Ed25519PublicKeyPrefix = 'z6Mk'; export const DockDIDQualifier = `did:${DockDIDMethod}:`; -export const DidMethodKeyQualifier = "did:key:"; +export const DidMethodKeyQualifier = 'did:key:'; export const DockDIDByteSize = 32; export const DidMethodKeySecp256k1ByteSize = 33; export const DidMethodKeyEd25519ByteSize = 32; diff --git a/src/utils/did/did-keypair.js b/src/utils/did/did-keypair.js index e3582ae1c..72985758f 100644 --- a/src/utils/did/did-keypair.js +++ b/src/utils/did/did-keypair.js @@ -1,5 +1,5 @@ -import { randomAsHex } from "@polkadot/util-crypto"; -import { DockKeyPair } from "../misc"; +import { randomAsHex } from '@polkadot/util-crypto'; +import { DockKeyPair } from '../misc'; /** * Signing keypair along with the optional key identifier. @@ -29,14 +29,14 @@ export class DidKeypair extends DockKeyPair { dockApi, { seed = randomAsHex(32), - keypairType = "ed25519", + keypairType = 'ed25519', meta = null, keyId = 1, - } = {} + } = {}, ) { return new DidKeypair( dockApi.keyring.addFromUri(seed, meta, keypairType), - keyId + keyId, ); } diff --git a/src/utils/did/typed-did/did-method-key.js b/src/utils/did/typed-did/did-method-key.js index 194020ce2..e1d49ceb7 100644 --- a/src/utils/did/typed-did/did-method-key.js +++ b/src/utils/did/typed-did/did-method-key.js @@ -1,12 +1,12 @@ -import { u8aToHex, hexToU8a } from "@polkadot/util"; -import { base58btc } from "multiformats/bases/base58"; -import bs58 from "bs58"; -import varint from "varint"; +import { u8aToHex, hexToU8a } from '@polkadot/util'; +import { base58btc } from 'multiformats/bases/base58'; +import bs58 from 'bs58'; +import varint from 'varint'; -import { getHexIdentifier } from "../../codec"; -import { PublicKeyEd25519, PublicKeySecp256k1 } from "../../../public-keys"; +import { getHexIdentifier } from '../../codec'; +import { PublicKeyEd25519, PublicKeySecp256k1 } from '../../../public-keys'; -import { parseDIDUrl } from "../../../resolver/did/did-resolver"; +import { parseDIDUrl } from '../../../resolver/did/did-resolver'; import { DidMethodKeyQualifier, @@ -14,8 +14,9 @@ import { DidMethodKeyBytePrefixSecp256k1, DidMethodKeyEd25519ByteSize, DidMethodKeySecp256k1ByteSize, -} from "../constants"; -import { DockKeyPair } from "../../misc"; + DidMethodKeyEd25519Prefix, + DidMethodKeySecp256k1Prefix, +} from '../constants'; import DockDidOrDidMethodKey from './dock-did-or-did-method-key'; /** @@ -38,7 +39,7 @@ export default class DidMethodKey extends DockDidOrDidMethodKey { } else if (didMethodKey instanceof PublicKeySecp256k1) { this.didMethodKey = { secp256k1: didMethodKey.value }; } else { - throw new Error("Unsupported public key type"); + throw new Error('Unsupported public key type'); } } @@ -70,7 +71,7 @@ export default class DidMethodKey extends DockDidOrDidMethodKey { } else if (id.startsWith(DidMethodKeyEd25519Prefix)) { return new this(new PublicKeyEd25519(pubkeyHex)); } else { - throw new Error("Unsupported `did:key:*`"); + throw new Error('Unsupported `did:key:*`'); } } @@ -86,7 +87,7 @@ export default class DidMethodKey extends DockDidOrDidMethodKey { const hex = getHexIdentifier( u8aToHex(key.asSecp256k1), [], - DidMethodKeySecp256k1ByteSize + DidMethodKeySecp256k1ByteSize, ); return new this(new PublicKeySecp256k1(hex)); @@ -94,7 +95,7 @@ export default class DidMethodKey extends DockDidOrDidMethodKey { const hex = getHexIdentifier( u8aToHex(key.asEd25519), [], - DidMethodKeyEd25519ByteSize + DidMethodKeyEd25519ByteSize, ); return new this(new PublicKeyEd25519(hex)); @@ -129,7 +130,7 @@ export default class DidMethodKey extends DockDidOrDidMethodKey { toQualifiedString() { // Convert the hex public key to bytes if (!this.publicKey) { - throw new Error("Unsupported public key type"); + throw new Error('Unsupported public key type'); } // Define the prefix for ed25519 DID key diff --git a/src/utils/did/typed-did/dock-did-or-did-method-key.js b/src/utils/did/typed-did/dock-did-or-did-method-key.js index 58e953e29..62940429b 100644 --- a/src/utils/did/typed-did/dock-did-or-did-method-key.js +++ b/src/utils/did/typed-did/dock-did-or-did-method-key.js @@ -1,7 +1,7 @@ -import { getStateChange } from "../../misc"; -import { createDidSig } from "../utils"; +import { getStateChange } from '../../misc'; +import { createDidSig } from '../utils'; -import { DockDIDQualifier, DidMethodKeyQualifier } from "../constants"; +import { DockDIDQualifier, DidMethodKeyQualifier } from '../constants'; /** * Either `did:dock:*` or `did:key:*`. @@ -22,7 +22,7 @@ export default class DockDidOrDidMethodKey { * @returns {this} */ static fromString(did) { - if (did.startsWith("0x")) { + if (did.startsWith('0x')) { return new this.DockDid(did); } else { return DockDidOrDidMethodKey.fromQualifiedString(did); @@ -41,7 +41,7 @@ export default class DockDidOrDidMethodKey { return this.DidMethodKey.fromQualifiedString(did); } else { throw new Error( - `Unsupported did string: ${did}, expected either \`dock:did:\` or \`dock:key:\`` + `Unsupported did string: ${did}, expected either \`dock:did:\` or \`dock:key:\``, ); } } @@ -65,14 +65,14 @@ export default class DockDidOrDidMethodKey { * Extracts raw underlying value if it's `did:dock:*`, throws an error otherwise. */ get asDid() { - throw new Error("Not a `Did`"); + throw new Error('Not a `Did`'); } /** * Extracts raw underlying value if it's `did:key:*`, throws an error otherwise. */ get asDidMethodKey() { - throw new Error("Not a `DidMethodKey`"); + throw new Error('Not a `DidMethodKey`'); } /** @@ -124,13 +124,13 @@ export default class DockDidOrDidMethodKey { * Converts underlying object to the `JSON` representation suitable for substrate JSON-RPC. */ toJSON() { - throw new Error("Unimplemented"); + throw new Error('Unimplemented'); } - /** + /** * Returns fully qualified `did:dock:*` encoded in SS58 or `did:key:* encoded in BS58. */ - toQualifiedString() { - throw new Error("Unimplemented"); - } + toQualifiedString() { + throw new Error('Unimplemented'); + } } diff --git a/src/utils/did/typed-did/dock-did.js b/src/utils/did/typed-did/dock-did.js index 6a893bba9..54d4ed3c9 100644 --- a/src/utils/did/typed-did/dock-did.js +++ b/src/utils/did/typed-did/dock-did.js @@ -1,10 +1,10 @@ -import { randomAsHex, encodeAddress } from "@polkadot/util-crypto"; -import { getHexIdentifier } from "../../codec"; +import { randomAsHex, encodeAddress } from '@polkadot/util-crypto'; +import { getHexIdentifier } from '../../codec'; -import { validateDockDIDHexIdentifier } from "../utils"; +import { validateDockDIDHexIdentifier } from '../utils'; -import { DockDIDByteSize, DockDIDQualifier } from "../constants"; -import DockDidOrDidMethodKey from "./dock-did-or-did-method-key"; +import { DockDIDByteSize, DockDIDQualifier } from '../constants'; +import DockDidOrDidMethodKey from './dock-did-or-did-method-key'; /** * `did:dock:*` @@ -72,5 +72,4 @@ export default class DockDid extends DockDidOrDidMethodKey { } } - DockDidOrDidMethodKey.DockDid = DockDid; diff --git a/src/utils/did/typed-did/helpers.js b/src/utils/did/typed-did/helpers.js index 99c229fa9..a0a685631 100644 --- a/src/utils/did/typed-did/helpers.js +++ b/src/utils/did/typed-did/helpers.js @@ -1,7 +1,5 @@ +import { getHexIdentifier, isHexWithGivenByteSize } from '../../codec'; -import { getHexIdentifier } from "../../codec"; - -import { ApiPromise } from "@polkadot/api"; import DockDidOrDidMethodKey from './dock-did-or-did-method-key'; import DockDid from './dock-did'; import DidMethodKey from './did-method-key'; // eslint-disable-line @@ -48,30 +46,29 @@ export function typedHexDIDFromSubstrate(api, did) { */ export const createNewDockDID = () => DockDid.random().toQualifiedString(); - /** * Temporary solution for the DID's backward compatibility. * * -------------------------------------------------------- */ // eslint-disable-next-line no-extend-native -Object.defineProperty(String.prototype, "asDid", { +Object.defineProperty(String.prototype, 'asDid', { get() { if (this.isDid) { return String(this); } else { - throw new Error("Not a `Did`"); + throw new Error('Not a `Did`'); } }, }); // eslint-disable-next-line no-extend-native -Object.defineProperty(String.prototype, "isDid", { +Object.defineProperty(String.prototype, 'isDid', { get() { return isHexWithGivenByteSize(String(this), DockDIDByteSize); }, }); // eslint-disable-next-line no-extend-native -Object.defineProperty(String.prototype, "toQualifiedString", { +Object.defineProperty(String.prototype, 'toQualifiedString', { get() { return new DockDid(this.asDid).toQualifiedString(); }, diff --git a/src/utils/did/typed-did/index.js b/src/utils/did/typed-did/index.js index 40ed46d96..ed02f6134 100644 --- a/src/utils/did/typed-did/index.js +++ b/src/utils/did/typed-did/index.js @@ -1,4 +1,4 @@ -export { default as DidMethodKey } from "./did-method-key"; -export { default as DockDid } from "./dock-did"; -export { default as DockDidOrDidMethodKey } from "./dock-did-or-did-method-key"; +export { default as DidMethodKey } from './did-method-key'; +export { default as DockDid } from './dock-did'; +export { default as DockDidOrDidMethodKey } from './dock-did-or-did-method-key'; export * from './helpers'; diff --git a/src/utils/did/utils.js b/src/utils/did/utils.js index 18fa40120..ff41b9347 100644 --- a/src/utils/did/utils.js +++ b/src/utils/did/utils.js @@ -1,8 +1,10 @@ -import { isHexWithGivenByteSize } from "../codec"; +/* eslint-disable max-classes-per-file */ + +import { isHexWithGivenByteSize } from '../codec'; import { PublicKey, // eslint-disable-line VerificationRelationship, // eslint-disable-line -} from "../../public-keys"; +} from '../../public-keys'; import { Signature } from "../../signatures"; // eslint-disable-line @@ -15,7 +17,7 @@ import { DockDIDByteSize } from './constants'; export class NoDIDError extends Error { constructor(did) { super(`DID (${did}) does not exist`); - this.name = "NoDIDError"; + this.name = 'NoDIDError'; this.did = did; this.message = `A DID document lookup was successful, but the DID in question does not exist (${did}). This is different from a network error.`; } @@ -27,10 +29,9 @@ export class NoDIDError extends Error { export class NoOnchainDIDError extends Error { constructor(did) { super(`DID (${did}) is an off-chain DID`); - this.name = "NoOnchainDIDError"; + this.name = 'NoOnchainDIDError'; this.did = did; - this.message = - "The DID exists on chain but is an off-chain DID, meaning the DID document exists off-chain."; + this.message = 'The DID exists on chain but is an off-chain DID, meaning the DID document exists off-chain.'; } } @@ -40,10 +41,9 @@ export class NoOnchainDIDError extends Error { export class NoOffchainDIDError extends Error { constructor(did) { super(`DID (${did}) is an on-chain DID`); - this.name = "NoOffchainDIDError"; + this.name = 'NoOffchainDIDError'; this.did = did; - this.message = - "The DID exists on chain and is an on-chain DID but the lookup was performed for an off-chain DID."; + this.message = 'The DID exists on chain and is an on-chain DID but the lookup was performed for an off-chain DID.'; } } @@ -69,7 +69,7 @@ export function validateDockDIDSS58Identifier(identifier) { const regex = new RegExp(/^[5KL][1-9A-HJ-NP-Za-km-z]{47}$/); const matches = regex.exec(identifier); if (!matches) { - throw new Error("The identifier must be 32 bytes and valid SS58 string"); + throw new Error('The identifier must be 32 bytes and valid SS58 string'); } } @@ -98,7 +98,7 @@ export function createDidKey(publicKey, verRel) { export function createDidSig(did, { keyId }, rawSig) { const sig = rawSig.toJSON(); - if (typeof did === "string") { + if (typeof did === 'string') { return { did, keyId, @@ -121,7 +121,7 @@ export function createDidSig(did, { keyId }, rawSig) { }; } else { throw new Error( - `Incorrect DID passed: \`${did}\`, expected instance of either \`DockDid\` or \`DidMethodKey\`` + `Incorrect DID passed: \`${did}\`, expected instance of either \`DockDid\` or \`DidMethodKey\``, ); } } diff --git a/src/utils/misc.js b/src/utils/misc.js index 1d3bb2cf2..55d511e20 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -1,20 +1,20 @@ -import elliptic from "elliptic"; -import { blake2AsHex, randomAsHex } from "@polkadot/util-crypto"; +import elliptic from 'elliptic'; +import { blake2AsHex, randomAsHex } from '@polkadot/util-crypto'; -import { sha256 } from "js-sha256"; +import { sha256 } from 'js-sha256'; import { PublicKeyEd25519, PublicKeySecp256k1, PublicKeySr25519, // eslint-disable-line -} from "../public-keys"; +} from '../public-keys'; import { SignatureEd25519, SignatureSecp256k1, SignatureSr25519, // eslint-disable-line -} from "../signatures"; +} from '../signatures'; const EC = elliptic.ec; -const secp256k1Curve = new EC("secp256k1"); +const secp256k1Curve = new EC('secp256k1'); /** // TODO: Error handling when `stateChange` is not registered * Helper function to return bytes of a `StateChange` enum. Updates like key change, DID removal, revocation, etc @@ -24,7 +24,7 @@ const secp256k1Curve = new EC("secp256k1"); * @return {array} An array of Uint8 */ export function getBytesForStateChange(api, stateChange) { - return api.createType("StateChange", stateChange).toU8a(); + return api.createType('StateChange', stateChange).toU8a(); } export function getStateChange(api, name, value) { @@ -67,7 +67,7 @@ export function verifyEcdsaSecp256k1Sig(message, signature, publicKey) { export function verifyEcdsaSecp256k1SigPrehashed( messageHash, signature, - publicKey + publicKey, ) { // Remove the leading `0x` const sigHex = signature.value.slice(2); @@ -77,7 +77,7 @@ export function verifyEcdsaSecp256k1SigPrehashed( const pkHex = publicKey.value.slice(2); // Generate public key object. Not extracting the public key for signature as the verifier // should always know what public key is being used. - const pk = secp256k1Curve.keyFromPublic(pkHex, "hex"); + const pk = secp256k1Curve.keyFromPublic(pkHex, 'hex'); return secp256k1Curve.verify(messageHash, sig, pk); } @@ -93,9 +93,9 @@ export function getKeyPairType(pair) { if (pair.ec && pair.priv) { // elliptic library's pair has `ec`, `priv` and `pub`. There is not a cleaner way to detect that - return "secp256k1"; + return 'secp256k1'; } - throw new Error("Cannot detect key pair type"); + throw new Error('Cannot detect key pair type'); } /** @@ -106,9 +106,9 @@ export function getKeyPairType(pair) { export function getPublicKeyFromKeyringPair(pair) { const type = getKeyPairType(pair); let Cls; - if (type === "ed25519") { + if (type === 'ed25519') { Cls = PublicKeyEd25519; - } else if (type === "sr25519") { + } else if (type === 'sr25519') { Cls = PublicKeySr25519; } else { Cls = PublicKeySecp256k1; @@ -125,9 +125,9 @@ export function getPublicKeyFromKeyringPair(pair) { export function getSignatureFromKeyringPair(pair, message) { const type = getKeyPairType(pair); let Cls; - if (type === "ed25519") { + if (type === 'ed25519') { Cls = SignatureEd25519; - } else if (type === "sr25519") { + } else if (type === 'sr25519') { Cls = SignatureSr25519; } else { Cls = SignatureSecp256k1; @@ -195,7 +195,7 @@ export function getUniqueElementsFromArray(a, filterCallback) { * @returns {string} */ export function encodeExtrinsicAsHash(api, tx) { - return blake2AsHex(api.createType("Call", tx).toU8a()); + return blake2AsHex(api.createType('Call', tx).toU8a()); } /** @@ -209,11 +209,11 @@ export function encodeExtrinsicAsHash(api, tx) { export async function getDidNonce( didOrDidMethodKey, nonce = undefined, - didModule = undefined + didModule = undefined, ) { if (nonce === undefined && didModule === undefined) { throw new Error( - "Provide either nonce or didModule to fetch nonce but none provided" + 'Provide either nonce or didModule to fetch nonce but none provided', ); } if (nonce === undefined) { diff --git a/src/utils/vc/schema.js b/src/utils/vc/schema.js index ca126b8f2..adf453dcf 100644 --- a/src/utils/vc/schema.js +++ b/src/utils/vc/schema.js @@ -8,7 +8,6 @@ import { credentialIDField, credentialContextField, } from './constants'; -import { DockDid } from '../did'; /** * The function uses `jsonschema` package to verify that the expanded `credential`'s subject `credentialSubject` has the JSON diff --git a/yarn.lock b/yarn.lock index 045aa453b..3655fb541 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8930,6 +8930,11 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== +jsdoc-typeof-plugin@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/jsdoc-typeof-plugin/-/jsdoc-typeof-plugin-1.0.0.tgz#d9b621d1c63b607116d53b20d73f87a2f917a51f" + integrity sha512-iG/LKaVnwRgi+EET6oi6uWNi+hVSUNM1t1NAywfCPIayDgsbQ4LWynnqO4IDTOPS7qObEWLJOjoqNby8+oiqXg== + jsdoc@^3.6.3: version "3.6.11" resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.6.11.tgz#8bbb5747e6f579f141a5238cbad4e95e004458ce" From 276921f09f6f64362e46319ccf2a3b601071a5ed Mon Sep 17 00:00:00 2001 From: olegnn Date: Mon, 5 Feb 2024 19:33:40 +0400 Subject: [PATCH 04/12] Fixes --- src/modules/schema.js | 2 +- src/utils/did/typed-did/did-method-key.js | 40 ++++++++----------- .../typed-did/dock-did-or-did-method-key.js | 33 +++++++++++---- src/utils/did/typed-did/dock-did.js | 10 +++-- src/utils/did/typed-did/helpers.js | 10 ++--- src/utils/vc/schema.js | 2 +- tests/integration/schema.test.js | 2 +- tests/unit/resolvers.test.js | 2 +- 8 files changed, 58 insertions(+), 43 deletions(-) diff --git a/src/modules/schema.js b/src/modules/schema.js index 00002d56b..5a3505b2f 100644 --- a/src/modules/schema.js +++ b/src/modules/schema.js @@ -140,7 +140,7 @@ export default class Schema { return { ...chainValue, id, - author: typedHexDIDFromSubstrate(dockApi, chainBlob[0]).toQualifiedString(), + author: typedHexDIDFromSubstrate(dockApi, chainBlob[0]).toQualifiedEncodedString(), }; } throw new Error('Incorrect schema format'); diff --git a/src/utils/did/typed-did/did-method-key.js b/src/utils/did/typed-did/did-method-key.js index e1d49ceb7..975db7370 100644 --- a/src/utils/did/typed-did/did-method-key.js +++ b/src/utils/did/typed-did/did-method-key.js @@ -25,6 +25,7 @@ import DockDidOrDidMethodKey from './dock-did-or-did-method-key'; * As of now, the public key can be either `PublicKeyEd25519` or `PublicKeySecp256k1`. */ export default class DidMethodKey extends DockDidOrDidMethodKey { + static Qualifier = DidMethodKeyQualifier; /** * Instantiates `did:key:*` using supplied public key. * As of now, the public key can be either `PublicKeyEd25519` or `PublicKeySecp256k1`. @@ -71,7 +72,7 @@ export default class DidMethodKey extends DockDidOrDidMethodKey { } else if (id.startsWith(DidMethodKeyEd25519Prefix)) { return new this(new PublicKeyEd25519(pubkeyHex)); } else { - throw new Error('Unsupported `did:key:*`'); + throw new Error(`Unsupported \`did:key:*\`: \`${did}\``); } } @@ -80,7 +81,7 @@ export default class DidMethodKey extends DockDidOrDidMethodKey { * @param {object} key - substrate did method key * @returns {this} */ - static fromSubstrate(did) { + static fromSubstrateValue(did) { const key = did.asDidMethodKey; if (key.isSecp256k1) { @@ -112,6 +113,16 @@ export default class DidMethodKey extends DockDidOrDidMethodKey { return this.didMethodKey; } + /** + * Returns underlying public key. + * @returns {PublicKeyEd25519|PublicKeySecp256k1} + */ + get publicKey() { + return this.didMethodKey.ed25519 + ? new PublicKeyEd25519(this.didMethodKey.ed25519) + : new PublicKeySecp256k1(this.didMethodKey.secp256k1); + } + toJSON() { return { DidMethodKey: this.didMethodKey.ed25519 @@ -121,18 +132,13 @@ export default class DidMethodKey extends DockDidOrDidMethodKey { } toString() { - return this.toQualifiedString(); + return this.toQualifiedEncodedString(); } /** - * Returns fully qualified public key encoded in `BS58` according to the `did:key:*` spec. + * Returns unqualified public key encoded in `BS58`. */ - toQualifiedString() { - // Convert the hex public key to bytes - if (!this.publicKey) { - throw new Error('Unsupported public key type'); - } - + toEncodedString() { // Define the prefix for ed25519 DID key const publicKeyBytes = hexToU8a(this.publicKey.value); const prefix = this.didMethodKey.ed25519 @@ -145,19 +151,7 @@ export default class DidMethodKey extends DockDidOrDidMethodKey { didKeyBytes.set(publicKeyBytes, prefix.length); // Encode the concatenated bytes to Base58 with z prefix - const encodedDidKey = `z${bs58.encode(didKeyBytes)}`; - - return `${DidMethodKeyQualifier}${encodedDidKey}`; - } - - /** - * Returns underlying public key. - * @returns {PublicKeyEd25519|PublicKeySecp256k1} - */ - get publicKey() { - return this.didMethodKey.ed25519 - ? new PublicKeyEd25519(this.didMethodKey.ed25519) - : new PublicKeySecp256k1(this.didMethodKey.secp256k1); + return `z${bs58.encode(didKeyBytes)}`; } } diff --git a/src/utils/did/typed-did/dock-did-or-did-method-key.js b/src/utils/did/typed-did/dock-did-or-did-method-key.js index 62940429b..50dc8dbaf 100644 --- a/src/utils/did/typed-did/dock-did-or-did-method-key.js +++ b/src/utils/did/typed-did/dock-did-or-did-method-key.js @@ -2,11 +2,18 @@ import { getStateChange } from '../../misc'; import { createDidSig } from '../utils'; import { DockDIDQualifier, DidMethodKeyQualifier } from '../constants'; +import { withExtendedStaticProperties } from '../../inheritance'; /** * Either `did:dock:*` or `did:key:*`. */ -export default class DockDidOrDidMethodKey { +class DockDidOrDidMethodKey { + /** + * Prefix to form the fully qualified string. + * + * @type {string} + */ + static Qualifier; /** * @type {typeof this} */ @@ -25,7 +32,7 @@ export default class DockDidOrDidMethodKey { if (did.startsWith('0x')) { return new this.DockDid(did); } else { - return DockDidOrDidMethodKey.fromQualifiedString(did); + return this.fromQualifiedString(did); } } @@ -51,11 +58,11 @@ export default class DockDidOrDidMethodKey { * @param {object} did - substrate did or did method key * @returns {this} */ - static fromSubstrate(did) { + static fromSubstrateValue(did) { if (did.isDid) { - return this.DockDid.fromSubstrate(did); + return this.DockDid.fromSubstrateValue(did); } else if (did.isDidMethodKey) { - return this.DidMethodKey.fromSubstrate(did); + return this.DidMethodKey.fromSubstrateValue(did); } else { throw new Error(`Invalid \`did:*\` provided: \`${did}\``); } @@ -128,9 +135,21 @@ export default class DockDidOrDidMethodKey { } /** - * Returns fully qualified `did:dock:*` encoded in SS58 or `did:key:* encoded in BS58. + * Returns underlying value encoded according to the specification. */ - toQualifiedString() { + toEncodedString() { throw new Error('Unimplemented'); } + + /** + * Returns fully qualified `did:dock:*` encoded in SS58 or `did:key:*` encoded in BS58. + */ + toQualifiedEncodedString() { + return `${this.constructor.Qualifier}${this.toEncodedString()}`; + } } + +export default withExtendedStaticProperties( + ['Qualifier'], + DockDidOrDidMethodKey, +); diff --git a/src/utils/did/typed-did/dock-did.js b/src/utils/did/typed-did/dock-did.js index 54d4ed3c9..2ebfb1c8f 100644 --- a/src/utils/did/typed-did/dock-did.js +++ b/src/utils/did/typed-did/dock-did.js @@ -10,6 +10,8 @@ import DockDidOrDidMethodKey from './dock-did-or-did-method-key'; * `did:dock:*` */ export default class DockDid extends DockDidOrDidMethodKey { + static Qualifier = DockDIDQualifier; + /** * Instantiates `DockDid` using supplied 32-byte hex sequence. * @param {*} did @@ -44,7 +46,7 @@ export default class DockDid extends DockDidOrDidMethodKey { * @param {object} did - substrate did * @returns {this} */ - static fromSubstrate(did) { + static fromSubstrateValue(did) { return new this(getHexIdentifier(did.asDid, [], DockDIDByteSize)); } @@ -65,10 +67,10 @@ export default class DockDid extends DockDidOrDidMethodKey { } /** - * Returns fully qualified DID encoded as a `SS58` address. + * Returns unqualified DID encoded as a `SS58` address. */ - toQualifiedString() { - return `${DockDIDQualifier}${encodeAddress(this.asDid)}`; + toEncodedString() { + return encodeAddress(this.asDid); } } diff --git a/src/utils/did/typed-did/helpers.js b/src/utils/did/typed-did/helpers.js index a0a685631..a1fe286ba 100644 --- a/src/utils/did/typed-did/helpers.js +++ b/src/utils/did/typed-did/helpers.js @@ -36,7 +36,7 @@ export function typedHexDIDFromSubstrate(api, did) { if (api.specVersion < 50) { return getHexIdentifier(did, DockDIDQualifier, DockDIDByteSize); } else { - return DockDidOrDidMethodKey.fromSubstrate(did); + return DockDidOrDidMethodKey.fromSubstrateValue(did); } } @@ -44,7 +44,7 @@ export function typedHexDIDFromSubstrate(api, did) { * Create and return a fully qualified Dock DID, i.e. "did:dock:" * @returns {string} - The DID */ -export const createNewDockDID = () => DockDid.random().toQualifiedString(); +export const createNewDockDID = () => DockDid.random().toQualifiedEncodedString(); /** * Temporary solution for the DID's backward compatibility. @@ -68,9 +68,9 @@ Object.defineProperty(String.prototype, 'isDid', { }, }); // eslint-disable-next-line no-extend-native -Object.defineProperty(String.prototype, 'toQualifiedString', { - get() { - return new DockDid(this.asDid).toQualifiedString(); +Object.defineProperty(String.prototype, 'toQualifiedEncodedString', { + value: function toQualifiedEncodedString() { + return new DockDid(this.asDid).toQualifiedEncodedString(); }, }); diff --git a/src/utils/vc/schema.js b/src/utils/vc/schema.js index adf453dcf..66eabe680 100644 --- a/src/utils/vc/schema.js +++ b/src/utils/vc/schema.js @@ -88,7 +88,7 @@ export async function getAndValidateSchemaIfPresent( schemaObj = { ...data, id: schemaUri, - author: author.toQualifiedString(), + author: author.toQualifiedEncodedString(), }; } else { schemaObj = document; diff --git a/tests/integration/schema.test.js b/tests/integration/schema.test.js index 1ddf2e0b0..68779934c 100644 --- a/tests/integration/schema.test.js +++ b/tests/integration/schema.test.js @@ -126,7 +126,7 @@ describe('Schema Blob Module Integration', () => { expect(schemaObj).toMatchObject({ ...exampleSchema, id: blobId, - author: hexDid.toQualifiedString(), + author: hexDid.toQualifiedEncodedString(), }); }, 20000); diff --git a/tests/unit/resolvers.test.js b/tests/unit/resolvers.test.js index afe258ce9..eda982dd7 100644 --- a/tests/unit/resolvers.test.js +++ b/tests/unit/resolvers.test.js @@ -83,7 +83,7 @@ describe('Resolvers', () => { "Static property `PREFIX` of `BMethod` isn't extended properly", ); expect(() => new APrefixBMethod()).toThrowError( - 'No resolvers were provided. You need to either implement `resolve` or provide a list of resolvers.', + 'No resolvers were provided. You need to either implement `resolve` or provide a list of resolvers', ); expect(() => new APrefix()).toThrowError( "Static property `METHOD` of `APrefix` isn't extended properly", From 45a53552dbb1082344e2a192265f4c25136b04f1 Mon Sep 17 00:00:00 2001 From: olegnn Date: Mon, 5 Feb 2024 19:34:43 +0400 Subject: [PATCH 05/12] Bump up version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d3d7e68c..382f39edd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/sdk", - "version": "7.2.2", + "version": "7.3.0", "main": "index.js", "license": "MIT", "repository": { From 1377fc1836d2da25c87608ff7496c479340e7cec Mon Sep 17 00:00:00 2001 From: olegnn Date: Mon, 5 Feb 2024 19:37:59 +0400 Subject: [PATCH 06/12] Use correct prefix --- src/utils/did/typed-did/did-method-key.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/did/typed-did/did-method-key.js b/src/utils/did/typed-did/did-method-key.js index 975db7370..a11da470c 100644 --- a/src/utils/did/typed-did/did-method-key.js +++ b/src/utils/did/typed-did/did-method-key.js @@ -14,8 +14,8 @@ import { DidMethodKeyBytePrefixSecp256k1, DidMethodKeyEd25519ByteSize, DidMethodKeySecp256k1ByteSize, - DidMethodKeyEd25519Prefix, - DidMethodKeySecp256k1Prefix, + Ed25519PublicKeyPrefix, + Secp256k1PublicKeyPrefix, } from '../constants'; import DockDidOrDidMethodKey from './dock-did-or-did-method-key'; @@ -67,9 +67,9 @@ export default class DidMethodKey extends DockDidOrDidMethodKey { const pubKeyBytes = multicodecPubKey.slice(varint.decode.bytes); const pubkeyHex = u8aToHex(pubKeyBytes); - if (id.startsWith(DidMethodKeySecp256k1Prefix)) { + if (id.startsWith(Secp256k1PublicKeyPrefix)) { return new this(new PublicKeySecp256k1(pubkeyHex)); - } else if (id.startsWith(DidMethodKeyEd25519Prefix)) { + } else if (id.startsWith(Ed25519PublicKeyPrefix)) { return new this(new PublicKeyEd25519(pubkeyHex)); } else { throw new Error(`Unsupported \`did:key:*\`: \`${did}\``); From 33a1f2fa94be25f451fe3435e77ba814a90ab305 Mon Sep 17 00:00:00 2001 From: olegnn Date: Wed, 7 Feb 2024 11:03:22 +0400 Subject: [PATCH 07/12] Implement new update mechanism for the `Trust Registry` --- .github/workflows/integrations-tests.yml | 1 - src/modules/trust-registry.js | 195 ++++++--- src/resolver/utils.js | 11 +- src/utils/misc.js | 205 +++++++++ .../integration/trust-registry-module.test.js | 389 +++++++++++++----- tests/unit/__snapshots__/pattern.test.js.snap | 45 ++ tests/unit/pattern.test.js | 140 +++++++ 7 files changed, 825 insertions(+), 161 deletions(-) create mode 100644 tests/unit/__snapshots__/pattern.test.js.snap create mode 100644 tests/unit/pattern.test.js diff --git a/.github/workflows/integrations-tests.yml b/.github/workflows/integrations-tests.yml index b2f737262..f68952ca1 100644 --- a/.github/workflows/integrations-tests.yml +++ b/.github/workflows/integrations-tests.yml @@ -40,7 +40,6 @@ jobs: testnet: runs-on: ubuntu-latest env: - DisableDidKeyAndTrustRegistryTests: true CONFIG_DOCK_NODE_IMAGE_TAG: 'testnet' steps: - uses: actions/checkout@v2 diff --git a/src/modules/trust-registry.js b/src/modules/trust-registry.js index 841a33160..673ad4918 100644 --- a/src/modules/trust-registry.js +++ b/src/modules/trust-registry.js @@ -1,6 +1,6 @@ -import { BTreeSet } from '@polkadot/types'; -import { typedHexDID } from '../utils/did'; -import { getDidNonce } from '../utils/misc'; +import { BTreeSet, BTreeMap } from '@polkadot/types'; +import { DidMethodKey, DockDid, typedHexDID } from '../utils/did'; +import { getDidNonce, ensureMatchesStructurePattern } from '../utils/misc'; /** * `Trust Registry` module. @@ -42,7 +42,10 @@ export default class TrustRegistryModule { waitForFinalization = true, params = {}, ) { - const [convenerHexDid, lastNonce] = await this.getActorDidAndNonce(convenerDid, { nonce, didModule }); + const [convenerHexDid, lastNonce] = await this.getActorDidAndNonce( + convenerDid, + { nonce, didModule }, + ); return this.signAndSend( convenerHexDid.changeState( @@ -62,43 +65,6 @@ export default class TrustRegistryModule { ); } - /** - * Appends new schemas to the registry. - * @param convenerDid - * @param registryId - * @param schemas - * @param signingKeyRef - * @param nonce - * @param didModule - * @param params - * @param waitForFinalization - * @param params - * @returns {Promise} - */ - async addSchemaMetadata( - convenerDid, - registryId, - schemas, - signingKeyRef, - { nonce = undefined, didModule = undefined } = {}, - waitForFinalization = true, - params = {}, - ) { - const [convenerHexDid, lastNonce] = await this.getActorDidAndNonce(convenerDid, { nonce, didModule }); - - return this.signAndSend( - convenerHexDid.changeState( - this.api, - this.module.addSchemaMetadata, - 'AddSchemaMetadata', - { registryId, schemas, nonce: lastNonce }, - signingKeyRef, - ), - waitForFinalization, - params, - ); - } - /** * Updates schemas metadatas in the registry. * @param convenerOrIssuerOrVerifierDid @@ -112,7 +78,7 @@ export default class TrustRegistryModule { * @param params * @returns {Promise} */ - async updateSchemaMetadata( + async setSchemasMetadata( convenerOrIssuerOrVerifierDid, registryId, schemas, @@ -121,13 +87,20 @@ export default class TrustRegistryModule { waitForFinalization = true, params = {}, ) { - const [convenerOrIssuerOrVerifierHexDid, lastNonce] = await this.getActorDidAndNonce(convenerOrIssuerOrVerifierDid, { nonce, didModule }); + const [convenerOrIssuerOrVerifierHexDid, lastNonce] = await this.getActorDidAndNonce(convenerOrIssuerOrVerifierDid, { + nonce, + didModule, + }); + ensureMatchesStructurePattern( + this.constructor.SchemasUpdatePattern, + schemas, + ); return this.signAndSend( convenerOrIssuerOrVerifierHexDid.changeState( this.api, - this.module.updateSchemaMetadata, - 'UpdateSchemaMetadata', + this.module.setSchemasMetadata, + 'SetSchemasMetadata', { registryId, schemas, nonce: lastNonce }, signingKeyRef, ), @@ -158,7 +131,10 @@ export default class TrustRegistryModule { waitForFinalization = true, params = {}, ) { - const [convenerHexDid, lastNonce] = await this.getActorDidAndNonce(convenerDid, { nonce, didModule }); + const [convenerHexDid, lastNonce] = await this.getActorDidAndNonce( + convenerDid, + { nonce, didModule }, + ); const hexIssuers = new BTreeSet(); for (const issuer of issuers) { @@ -200,7 +176,10 @@ export default class TrustRegistryModule { waitForFinalization = true, params = {}, ) { - const [convenerHexDid, lastNonce] = await this.getActorDidAndNonce(convenerDid, { nonce, didModule }); + const [convenerHexDid, lastNonce] = await this.getActorDidAndNonce( + convenerDid, + { nonce, didModule }, + ); const hexIssuers = new BTreeSet(); for (const issuer of issuers) { @@ -241,7 +220,10 @@ export default class TrustRegistryModule { waitForFinalization = true, params = {}, ) { - const [issuerHexDid, lastNonce] = await this.getActorDidAndNonce(issuerDid, { nonce, didModule }); + const [issuerHexDid, lastNonce] = await this.getActorDidAndNonce( + issuerDid, + { nonce, didModule }, + ); return this.signAndSend( issuerHexDid.changeState( @@ -263,9 +245,124 @@ export default class TrustRegistryModule { * @param didModule * @returns {Promise} */ - async getActorDidAndNonce(actorDid, { nonce = undefined, didModule = undefined } = {}) { + async getActorDidAndNonce( + actorDid, + { nonce = undefined, didModule = undefined } = {}, + ) { const hexDID = typedHexDID(this.api, actorDid); const lastNonce = nonce ?? (await getDidNonce(hexDID, nonce, didModule)); return [hexDID, lastNonce]; } } + +const DockDidOrDidMethodKeyPattern = { + $anyOf: [{ $instanceOf: DockDid }, { $instanceOf: DidMethodKey }], +}; + +const VerificationPricePattern = { + $anyOf: [{ $matchType: 'number' }, { $matchType: 'object' }], +}; + +const VerifiersPattern = { + $instanceOf: BTreeSet, + $iterableOf: DockDidOrDidMethodKeyPattern, +}; + +const VerifiersUpdatePattern = { + $instanceOf: BTreeMap, + $mapOf: [ + DockDidOrDidMethodKeyPattern, + { + $anyOf: [ + { $matchValue: 'Remove' }, + { + $matchValue: 'Add', + }, + ], + }, + ], +}; + +const IssuerPricesPattern = { + $instanceOf: BTreeMap, + $mapOf: [{ $matchType: 'string' }, VerificationPricePattern], +}; + +const IssuerPricesUpdatePattern = { + $instanceOf: BTreeMap, + $mapOf: [ + { $matchType: 'string' }, + { + $anyOf: [ + { $matchValue: 'Remove' }, + { + $objOf: { + Add: VerificationPricePattern, + Set: VerificationPricePattern, + }, + }, + ], + }, + ], +}; + +const IssuersPattern = { + $instanceOf: BTreeMap, + $mapOf: [DockDidOrDidMethodKeyPattern, IssuerPricesPattern], +}; + +const IssuersUpdatePattern = { + $instanceOf: BTreeMap, + $mapOf: [ + DockDidOrDidMethodKeyPattern, + { + $objOf: { + Modify: IssuerPricesUpdatePattern, + Set: IssuerPricesPattern, + }, + }, + ], +}; + +TrustRegistryModule.SchemasUpdatePattern = { + $instanceOf: BTreeMap, + $mapOf: [ + { $matchType: 'string' }, + { + $anyOf: [ + { + $objOf: { + Add: { + $matchObject: { + issuers: IssuersPattern, + verifiers: VerifiersPattern, + }, + }, + Set: { + $matchObject: { + issuers: IssuersPattern, + verifiers: VerifiersPattern, + }, + }, + Modify: { + $matchObject: { + issuers: { + $objOf: { Modify: IssuersUpdatePattern, Set: IssuersPattern }, + }, + verifiers: { + $objOf: { + Modify: VerifiersUpdatePattern, + Set: VerifiersPattern, + }, + }, + }, + }, + }, + }, + { + $matchValue: 'Remove', + }, + ], + }, + ], +}; diff --git a/src/resolver/utils.js b/src/resolver/utils.js index 6a63d58a3..03ea98dce 100644 --- a/src/resolver/utils.js +++ b/src/resolver/utils.js @@ -1,3 +1,5 @@ +import { fmtIter } from '../utils/misc'; + /** * Before resolving an entity, ensures that `DockAPI` is initialized, throws an error otherwise. * @template T @@ -18,15 +20,6 @@ export const withInitializedDockAPI = ( } }; -/** - * Returns string containing comma separated items of the provided iterable. - * - * @template V - * @param {Iterable} iter - * @returns {string} - */ -export const fmtIter = (iter) => `\`[${[...iter].map((item) => item.toString()).join(', ')}]\``; - /** * Caches last function result and returns it if function called with the same args again. * @param {Function} diff --git a/src/utils/misc.js b/src/utils/misc.js index 55d511e20..aedf9eaae 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -221,3 +221,208 @@ export async function getDidNonce( } return nonce; } + +/** + * Returns string containing comma separated items of the provided iterable. + * + * @template V + * @param {Iterable} iter + * @returns {string} + */ +export const fmtIter = (iter) => `\`[${[...iter].map((item) => item.toString()).join(', ')}]\``; + +const PatternAttrs = new Set([ + '$matchType', + '$matchValue', + '$matchObject', + '$matchIterable', + '$instanceOf', + '$iterableOf', + '$mapOf', + '$anyOf', + '$objOf', +]); + +/** + * Ensures that provided value matches supplied pattern, throws an error otherwise. + * + * @param pattern + * @param value + * @param path + */ +// eslint-disable-next-line sonarjs/cognitive-complexity +export const ensureMatchesStructurePattern = (pattern, value, path = []) => { + const patternAttrs = new Set(Object.keys(pattern)); + + for (const key of patternAttrs) { + if (!PatternAttrs.has(key)) { + throw new Error( + `Invalid pattern modifier: ${key}, path: \`${path.join( + '.', + )}\`.`, + ); + } + } + + // eslint-disable-next-line valid-typeof + if (patternAttrs.has('$matchType') && typeof value !== pattern.$matchType) { + throw new Error( + `Invalid value provided, expected value with type \`${ + pattern.$matchType + }\`, received value with type \`${typeof value}\`, path: \`${path.join( + '.', + )}\`.`, + ); + } + + if (patternAttrs.has('$matchValue') && value !== pattern.$matchValue) { + throw new Error( + `Unknown value \`${value}\`, expected ${ + pattern.$matchValue + }, path: \`${path.join('.')}\`.`, + ); + } + + if (patternAttrs.has('$matchObject')) { + for (const key of Object.keys(value)) { + if (!Object.hasOwnProperty.call(pattern.$matchObject, key)) { + throw new Error( + `Invalid property \`${key}\`, expected keys: \`${fmtIter( + Object.keys(pattern.$matchObject), + )}\`, path: \`${path.join('.')}\`, pattern: \`${JSON.stringify( + pattern, + )}\``, + ); + } + + ensureMatchesStructurePattern( + pattern.$matchObject[key], + value[key], + path.concat(key), + ); + } + } + + if (patternAttrs.has('$matchIterable')) { + if (typeof value[Symbol.iterator] !== 'function') { + throw new Error( + `Iterable expected, received: ${value}, path: \`${path.join( + '.', + )}\`.`, + ); + } + const objectIter = value[Symbol.iterator](); + + let idx = 0; + for (const pat of pattern.$matchIterable) { + const { value: item, done } = objectIter.next(); + if (done) { + throw new Error( + `Value iterable is shorter than expected, received: ${value}, path: \`${path.join( + '.', + )}\`.`, + ); + } + + ensureMatchesStructurePattern(pat, item, path.concat(`@item#${idx++}`)); + } + } + + if (patternAttrs.has('$instanceOf') && !(value instanceof pattern.$instanceOf)) { + throw new Error( + `Invalid value provided, expected instance of \`${ + pattern.$instanceOf.name + }\`, received instance of \`${ + value?.constructor?.name + }\`, path: \`${path.join('.')}\`.`, + ); + } + + if (patternAttrs.has('$iterableOf')) { + if (typeof value?.[Symbol.iterator] !== 'function') { + throw new Error( + `Iterable expected, received \`${value}\`, path: \`${path.join( + '.', + )}\`.`, + ); + } + + let idx = 0; + for (const entry of value) { + ensureMatchesStructurePattern( + pattern.$iterableOf, + entry, + path.concat(`@item#${idx++}`), + ); + } + } + + if (patternAttrs.has('$mapOf')) { + if (typeof value?.entries !== 'function') { + throw new Error( + `Map expected, received \`${value}\`, path: \`${path.join( + '.', + )}\`.`, + ); + } + + if (!Array.isArray(pattern.$mapOf) || pattern.$mapOf.length !== 2) { + throw new Error( + `\`$mapOf\` pattern should be an array with two items, received \`${JSON.stringify( + pattern, + )}\`, path: \`${path.join('.')}\``, + ); + } + + for (const [key, item] of value.entries()) { + ensureMatchesStructurePattern( + pattern.$mapOf[0], + key, + path.concat(`${key}#key`), + ); + ensureMatchesStructurePattern(pattern.$mapOf[1], item, path.concat(key)); + } + } + + if (patternAttrs.has('$anyOf')) { + let anySucceeded = false; + const errors = []; + for (const patEntry of pattern.$anyOf) { + try { + ensureMatchesStructurePattern(patEntry, value, path); + anySucceeded = true; + } catch (err) { + errors.push(err); + } + } + + if (!anySucceeded) { + throw new Error( + `Neither of pattern succeeded for \`${value}\`: ${JSON.stringify( + errors.map((err) => err.message), + )}, path: \`${path.join('.')}\`.`, + ); + } + } + + if (patternAttrs.has('$objOf')) { + const keys = Object.keys(value); + if (keys.length !== 1) { + throw new Error('Expected a single key'); + } + if (!Object.hasOwnProperty.call(pattern.$objOf, keys[0])) { + throw new Error( + `Invalid value key provided, expected one of \`${fmtIter( + Object.keys(pattern.$objOf), + )}\`, received \`${keys[0]}\`, path: \`${path.join( + '.', + )}\`.`, + ); + } + ensureMatchesStructurePattern( + pattern.$objOf[keys[0]], + value[keys[0]], + path.concat(keys[0]), + ); + } +}; diff --git a/tests/integration/trust-registry-module.test.js b/tests/integration/trust-registry-module.test.js index e76382916..5137766bb 100644 --- a/tests/integration/trust-registry-module.test.js +++ b/tests/integration/trust-registry-module.test.js @@ -1,27 +1,27 @@ -import { randomAsHex } from '@polkadot/util-crypto'; -import { BTreeSet, BTreeMap } from '@polkadot/types'; -import { u8aToHex, stringToU8a } from '@polkadot/util'; +import { randomAsHex } from "@polkadot/util-crypto"; +import { BTreeSet, BTreeMap } from "@polkadot/types"; +import { u8aToHex, stringToU8a } from "@polkadot/util"; -import { DockAPI } from '../../src/index'; +import { DockAPI } from "../../src/index"; import { FullNodeEndpoint, TestKeyringOpts, TestAccountURI, DisableDidKeyAndTrustRegistryTests, -} from '../test-constants'; +} from "../test-constants"; import { createNewDockDID, DidKeypair, DidMethodKey, typedHexDID, -} from '../../src/utils/did'; -import { registerNewDIDUsingPair } from './helpers'; +} from "../../src/utils/did"; +import { registerNewDIDUsingPair } from "./helpers"; const buildTest = DisableDidKeyAndTrustRegistryTests ? describe.skip : describe; -buildTest('Trust Registry', () => { +buildTest("Trust Registry", () => { const dock = new DockAPI(); // Create a random trust registry id @@ -59,34 +59,36 @@ buildTest('Trust Registry', () => { }); convenerPair = new DidKeypair( - dock.keyring.addFromUri(ownerSeed, null, 'ed25519'), - 1, + dock.keyring.addFromUri(ownerSeed, null, "ed25519"), + 1 ); issuerPair = new DidKeypair( - dock.keyring.addFromUri(issuerSeed, null, 'ed25519'), - 1, + dock.keyring.addFromUri(issuerSeed, null, "ed25519"), + 1 ); issuerPair2 = new DidKeypair( - dock.keyring.addFromUri(issuerSeed2, null, 'ed25519'), - 1, + dock.keyring.addFromUri(issuerSeed2, null, "ed25519"), + 1 ); verifierPair = new DidKeypair( - dock.keyring.addFromUri(verifierSeed, null, 'ed25519'), - 1, + dock.keyring.addFromUri(verifierSeed, null, "ed25519"), + 1 ); verifierPair2 = new DidKeypair( - dock.keyring.addFromUri(verifierSeed2, null, 'ed25519'), - 1, + dock.keyring.addFromUri(verifierSeed2, null, "ed25519"), + 1 ); verifierDIDMethodKeyPair = new DidKeypair( - dock.keyring.addFromUri(verifierDIDMethodKeySeed, null, 'ed25519'), + dock.keyring.addFromUri(verifierDIDMethodKeySeed, null, "ed25519") + ); + verifierDIDMethodKey = new DidMethodKey( + verifierDIDMethodKeyPair.publicKey() ); - verifierDIDMethodKey = new DidMethodKey(verifierDIDMethodKeyPair.publicKey()); // The keyring should be initialized before any test begins as this suite is testing trust registry const account = dock.keyring.addFromUri(TestAccountURI); @@ -106,19 +108,19 @@ buildTest('Trust Registry', () => { await dock.disconnect(); }, 10000); - it('Initializes Trust Registry', async () => { + it("Initializes Trust Registry", async () => { expect( (await dock.api.query.trustRegistry.trustRegistriesInfo(trustRegistryId)) - .isNone, + .isNone ).toEqual(true); await dock.trustRegistry.initOrUpdate( convenerDID, trustRegistryId, - 'Test Registry', - 'Gov framework', + "Test Registry", + "Gov framework", convenerPair, - dock, + dock ); const registryInfo = ( @@ -126,22 +128,23 @@ buildTest('Trust Registry', () => { ).toJSON(); expect(registryInfo).toEqual({ convener: typedHexDID(dock.api, convenerDID), - name: 'Test Registry', - govFramework: u8aToHex(stringToU8a('Gov framework')), + name: "Test Registry", + govFramework: u8aToHex(stringToU8a("Gov framework")), }); }); - it('Adds schemas to the existing Trust Registry', async () => { + it("Adds schemas metadata to the existing Trust Registry", async () => { // Create a random trust registry id const schemaId = randomAsHex(32); + const otherSchemaId = randomAsHex(32); await dock.trustRegistry.initOrUpdate( convenerDID, trustRegistryId, - 'Test Registry', - 'Gov framework', + "Test Registry", + "Gov framework", convenerPair, - dock, + dock ); const verifiers = new BTreeSet(); @@ -151,51 +154,171 @@ buildTest('Trust Registry', () => { const issuers = new BTreeMap(); const issuerPrices = new BTreeMap(); - issuerPrices.set('A', 20); + issuerPrices.set("A", 20); const issuer2Prices = new BTreeMap(); - issuer2Prices.set('A', 20); + issuer2Prices.set("A", 20); issuers.set(typedHexDID(dock.api, issuerDID), issuerPrices); issuers.set(typedHexDID(dock.api, issuerDID2), issuer2Prices); const schemas = new BTreeMap(); schemas.set(schemaId, { - issuers, - verifiers, + Set: { + issuers, + verifiers, + }, + }); + schemas.set(otherSchemaId, { + Add: { + issuers, + }, }); - await dock.trustRegistry.addSchemaMetadata( + await dock.trustRegistry.setSchemasMetadata( convenerDID, trustRegistryId, schemas, convenerPair, - dock, + dock ); expect( ( await dock.api.query.trustRegistry.trustRegistrySchemasMetadata( schemaId, - trustRegistryId, + trustRegistryId + ) + ).toJSON() + ).toEqual({ + issuers: expectedFormattedIssuers(issuers), + verifiers: expectedFormattedVerifiers(verifiers), + }); + expect( + ( + await dock.api.query.trustRegistry.trustRegistrySchemasMetadata( + otherSchemaId, + trustRegistryId + ) + ).toJSON() + ).toEqual({ + issuers: expectedFormattedIssuers(issuers), + verifiers: expectedFormattedVerifiers(new BTreeSet()), + }); + }); + + it("Removes schemas metadata from the Trust Registry", async () => { + // Create a random trust registry id + const schemaId = randomAsHex(32); + const otherSchemaId = randomAsHex(32); + + await dock.trustRegistry.initOrUpdate( + convenerDID, + trustRegistryId, + "Test Registry", + "Gov framework", + convenerPair, + dock + ); + + const verifiers = new BTreeSet(); + verifiers.add(typedHexDID(dock.api, verifierDID)); + verifiers.add(typedHexDID(dock.api, verifierDID2)); + verifiers.add(verifierDIDMethodKey); + + const issuers = new BTreeMap(); + const issuerPrices = new BTreeMap(); + issuerPrices.set("A", 20); + const issuer2Prices = new BTreeMap(); + issuer2Prices.set("A", 20); + + issuers.set(typedHexDID(dock.api, issuerDID), issuerPrices); + issuers.set(typedHexDID(dock.api, issuerDID2), issuer2Prices); + + let schemas = new BTreeMap(); + schemas.set(schemaId, { + Add: { + issuers, + verifiers, + }, + }); + schemas.set(otherSchemaId, { + Add: { + issuers, + }, + }); + + await dock.trustRegistry.setSchemasMetadata( + convenerDID, + trustRegistryId, + schemas, + convenerPair, + dock + ); + + expect( + ( + await dock.api.query.trustRegistry.trustRegistrySchemasMetadata( + schemaId, + trustRegistryId ) - ).toJSON(), + ).toJSON() ).toEqual({ issuers: expectedFormattedIssuers(issuers), verifiers: expectedFormattedVerifiers(verifiers), }); + expect( + ( + await dock.api.query.trustRegistry.trustRegistrySchemasMetadata( + otherSchemaId, + trustRegistryId + ) + ).toJSON() + ).toEqual({ + issuers: expectedFormattedIssuers(issuers), + verifiers: expectedFormattedVerifiers(new BTreeSet()), + }); + + schemas = new BTreeMap(); + schemas.set(schemaId, "Remove"); + schemas.set(otherSchemaId, "Remove"); + + await dock.trustRegistry.setSchemasMetadata( + convenerDID, + trustRegistryId, + schemas, + convenerPair, + dock + ); + + expect( + ( + await dock.api.query.trustRegistry.trustRegistrySchemasMetadata( + schemaId, + trustRegistryId + ) + ).toJSON() + ).toEqual(null); + expect( + ( + await dock.api.query.trustRegistry.trustRegistrySchemasMetadata( + otherSchemaId, + trustRegistryId + ) + ).toJSON() + ).toEqual(null); }); - it('Suspends issuers in the existing Trust Registry', async () => { + it("Suspends issuers in the existing Trust Registry", async () => { // Create a random trust registry id const schemaId = randomAsHex(32); await dock.trustRegistry.initOrUpdate( convenerDID, trustRegistryId, - 'Test Registry', - 'Gov framework', + "Test Registry", + "Gov framework", convenerPair, - dock, + dock ); const verifiers = new BTreeSet(); @@ -205,25 +328,27 @@ buildTest('Trust Registry', () => { const issuers = new BTreeMap(); const issuerPrices = new BTreeMap(); - issuerPrices.set('A', 20); + issuerPrices.set("A", 20); const issuer2Prices = new BTreeMap(); - issuer2Prices.set('A', 20); + issuer2Prices.set("A", 20); issuers.set(typedHexDID(dock.api, issuerDID), issuerPrices); issuers.set(typedHexDID(dock.api, issuerDID2), issuer2Prices); const schemas = new BTreeMap(); schemas.set(schemaId, { - issuers, - verifiers, + Set: { + issuers, + verifiers, + }, }); - await dock.trustRegistry.addSchemaMetadata( + await dock.trustRegistry.setSchemasMetadata( convenerDID, trustRegistryId, schemas, convenerPair, - dock, + dock ); await dock.trustRegistry.suspendIssuers( @@ -231,11 +356,18 @@ buildTest('Trust Registry', () => { trustRegistryId, [issuerDID, issuerDID2], convenerPair, - dock, + dock ); for (const issuer of [issuerDID, issuerDID2]) { - expect((await dock.api.query.trustRegistry.trustRegistryIssuerConfigurations(trustRegistryId, typedHexDID(dock.api, issuer))).toJSON()).toEqual({ + expect( + ( + await dock.api.query.trustRegistry.trustRegistryIssuerConfigurations( + trustRegistryId, + typedHexDID(dock.api, issuer) + ) + ).toJSON() + ).toEqual({ suspended: true, delegated: [], }); @@ -246,36 +378,57 @@ buildTest('Trust Registry', () => { trustRegistryId, [issuerDID], convenerPair, - dock, + dock ); for (const issuer of [issuerDID]) { - expect((await dock.api.query.trustRegistry.trustRegistryIssuerConfigurations(trustRegistryId, typedHexDID(dock.api, issuer))).toJSON()).toEqual({ + expect( + ( + await dock.api.query.trustRegistry.trustRegistryIssuerConfigurations( + trustRegistryId, + typedHexDID(dock.api, issuer) + ) + ).toJSON() + ).toEqual({ suspended: false, delegated: [], }); } for (const issuer of [issuerDID2]) { - expect((await dock.api.query.trustRegistry.trustRegistryIssuerConfigurations(trustRegistryId, typedHexDID(dock.api, issuer))).toJSON()).toEqual({ + expect( + ( + await dock.api.query.trustRegistry.trustRegistryIssuerConfigurations( + trustRegistryId, + typedHexDID(dock.api, issuer) + ) + ).toJSON() + ).toEqual({ suspended: true, delegated: [], }); } }); - it('Updates delegated issuers in the existing Trust Registry', async () => { + it("Updates delegated issuers in the existing Trust Registry", async () => { // Create a random trust registry id await dock.trustRegistry.initOrUpdate( convenerDID, trustRegistryId, - 'Test Registry', - 'Gov framework', + "Test Registry", + "Gov framework", convenerPair, - dock, + dock ); - expect((await dock.api.query.trustRegistry.trustRegistryIssuerConfigurations(trustRegistryId, typedHexDID(dock.api, issuerDID))).toJSON()).toEqual({ + expect( + ( + await dock.api.query.trustRegistry.trustRegistryIssuerConfigurations( + trustRegistryId, + typedHexDID(dock.api, issuerDID) + ) + ).toJSON() + ).toEqual({ suspended: false, delegated: [], }); @@ -288,26 +441,33 @@ buildTest('Trust Registry', () => { trustRegistryId, { Set: issuers }, issuerPair, - dock, + dock ); - expect((await dock.api.query.trustRegistry.trustRegistryIssuerConfigurations(trustRegistryId, typedHexDID(dock.api, issuerDID))).toJSON()).toEqual({ + expect( + ( + await dock.api.query.trustRegistry.trustRegistryIssuerConfigurations( + trustRegistryId, + typedHexDID(dock.api, issuerDID) + ) + ).toJSON() + ).toEqual({ suspended: false, delegated: [typedHexDID(dock.api, issuerDID2)], }); }); - it('Updates schemas in the existing Trust Registry', async () => { + it("Updates schemas metadata in the existing Trust Registry", async () => { // Create a random trust registry id const schemaId = randomAsHex(32); await dock.trustRegistry.initOrUpdate( convenerDID, trustRegistryId, - 'Test Registry', - 'Gov framework', + "Test Registry", + "Gov framework", convenerPair, - dock, + dock ); const verifiers = new BTreeSet(); @@ -317,9 +477,9 @@ buildTest('Trust Registry', () => { let issuers = new BTreeMap(); let issuerPrices = new BTreeMap(); - issuerPrices.set('A', 20); + issuerPrices.set("A", 20); let issuer2Prices = new BTreeMap(); - issuer2Prices.set('A', 20); + issuer2Prices.set("A", 20); issuers.set(typedHexDID(dock.api, issuerDID), issuerPrices); issuers.set(typedHexDID(dock.api, issuerDID2), issuer2Prices); @@ -330,77 +490,97 @@ buildTest('Trust Registry', () => { verifiers, }); - await dock.trustRegistry.addSchemaMetadata( + let schemasUpdate = new BTreeMap(); + schemasUpdate.set(schemaId, { + Add: { + issuers, + verifiers, + }, + }); + + await dock.trustRegistry.setSchemasMetadata( convenerDID, trustRegistryId, - schemas, + schemasUpdate, convenerPair, - dock, + dock ); expect( ( await dock.api.query.trustRegistry.trustRegistrySchemasMetadata( schemaId, - trustRegistryId, + trustRegistryId ) - ).toJSON(), + ).toJSON() ).toEqual({ issuers: expectedFormattedIssuers(issuers), verifiers: expectedFormattedVerifiers(verifiers), }); - let schemasUpdate = new BTreeMap(); + schemasUpdate = new BTreeMap(); issuers = new BTreeMap(); issuerPrices = new BTreeMap(); - issuerPrices.set('A', 65); + issuerPrices.set("A", 65); issuer2Prices = new BTreeMap(); - issuer2Prices.set('A', 75); + issuer2Prices.set("A", 75); issuers.set(typedHexDID(dock.api, issuerDID), issuerPrices); issuers.set(typedHexDID(dock.api, issuerDID2), issuer2Prices); schemasUpdate.set(schemaId, { - issuers: { - Set: issuers, + Modify: { + issuers: { + Set: issuers, + }, }, }); - await dock.trustRegistry.updateSchemaMetadata( + await dock.trustRegistry.setSchemasMetadata( convenerDID, trustRegistryId, schemasUpdate, convenerPair, - dock, + dock ); expect( ( await dock.api.query.trustRegistry.trustRegistrySchemasMetadata( schemaId, - trustRegistryId, + trustRegistryId ) - ).toJSON(), + ).toJSON() ).toEqual({ issuers: expectedFormattedIssuers(issuers), verifiers: expectedFormattedVerifiers(verifiers), }); schemasUpdate = new BTreeMap(); + + let issuer2PricesUpdate = new BTreeMap(); + issuer2PricesUpdate.set("C", { Add: 25 }); + issuer2PricesUpdate.set("B", { Set: 36 }); + issuer2PricesUpdate.set("A", 'Remove'); + issuer2Prices = new BTreeMap(); - issuer2Prices.set('A', 25); - issuer2Prices.set('B', 36); + issuer2Prices.set("C", 25); + issuer2Prices.set("B", 36); const issuersUpdate = new BTreeMap(); issuersUpdate.set(typedHexDID(dock.api, issuerDID2), { - Set: issuer2Prices, + Modify: issuer2PricesUpdate, }); + schemasUpdate.set(schemaId, { - issuers: { - Modify: issuersUpdate, + Modify: { + issuers: { + Modify: issuersUpdate, + }, }, }); + issuers = new BTreeMap(); issuers.set(typedHexDID(dock.api, issuerDID2), issuer2Prices); issuers.set(typedHexDID(dock.api, issuerDID), issuerPrices); @@ -409,21 +589,21 @@ buildTest('Trust Registry', () => { verifiers: schemas.get(schemaId).verifiers, }); - await dock.trustRegistry.updateSchemaMetadata( + await dock.trustRegistry.setSchemasMetadata( issuerDID2, trustRegistryId, schemasUpdate, issuerPair2, - dock, + dock ); expect( ( await dock.api.query.trustRegistry.trustRegistrySchemasMetadata( schemaId, - trustRegistryId, + trustRegistryId ) - ).toJSON(), + ).toJSON() ).toEqual({ issuers: expectedFormattedIssuers(issuers), verifiers: expectedFormattedVerifiers(verifiers), @@ -431,21 +611,22 @@ buildTest('Trust Registry', () => { schemasUpdate = new BTreeMap(); const verifiersUpdate = new BTreeMap(); - const innerUpdate = new BTreeSet(); - innerUpdate.add(verifierDIDMethodKey); - verifiersUpdate.set(verifierDIDMethodKey, { Remove: innerUpdate }); + verifiersUpdate.set(verifierDIDMethodKey, "Remove"); + // verifiersUpdate.set(issuerDID2, "Add"); schemasUpdate.set(schemaId, { - verifiers: { - Modify: verifiersUpdate, + Modify: { + verifiers: { + Modify: verifiersUpdate, + }, }, }); - await dock.trustRegistry.updateSchemaMetadata( + await dock.trustRegistry.setSchemasMetadata( verifierDIDMethodKey, trustRegistryId, schemasUpdate, verifierDIDMethodKeyPair, - dock, + dock ); verifiers.delete(verifierDIDMethodKey); @@ -453,9 +634,9 @@ buildTest('Trust Registry', () => { ( await dock.api.query.trustRegistry.trustRegistrySchemasMetadata( schemaId, - trustRegistryId, + trustRegistryId ) - ).toJSON(), + ).toJSON() ).toEqual({ issuers: expectedFormattedIssuers(issuers), verifiers: expectedFormattedVerifiers(verifiers), @@ -469,15 +650,19 @@ function expectedFormattedIssuers(issuers) { JSON.stringify( issuer.isDid ? { did: issuer.asDid } - : { didMethodKey: issuer.asDidMethodKey }, + : { didMethodKey: issuer.asDidMethodKey } ), Object.fromEntries([...prices.entries()]), - ]), + ]) ); } function expectedFormattedVerifiers(verifiers) { return [...verifiers.values()] - .map((verifier) => (verifier.isDid ? { did: verifier.asDid } : { didMethodKey: verifier.asDidMethodKey })) + .map((verifier) => + verifier.isDid + ? { did: verifier.asDid } + : { didMethodKey: verifier.asDidMethodKey } + ) .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); } diff --git a/tests/unit/__snapshots__/pattern.test.js.snap b/tests/unit/__snapshots__/pattern.test.js.snap new file mode 100644 index 000000000..8995c9fa8 --- /dev/null +++ b/tests/unit/__snapshots__/pattern.test.js.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ensureMatchesStructurePattern $anyOf 1`] = `"Neither of pattern succeeded for \`3\`: [\\"Unknown value \`3\`, expected 1, path: \`\`.\\",\\"Unknown value \`3\`, expected 2, path: \`\`.\\"], path: \`\`."`; + +exports[`ensureMatchesStructurePattern $instanceOf 1`] = `"Invalid value provided, expected instance of \`Function\`, received instance of \`Object\`, path: \`\`."`; + +exports[`ensureMatchesStructurePattern $iterableOf 1`] = `"Unknown value \`3\`, expected 1, path: \`@item#0\`."`; + +exports[`ensureMatchesStructurePattern $iterableOf 2`] = `"Unknown value \`3\`, expected 1, path: \`@item#1\`."`; + +exports[`ensureMatchesStructurePattern $iterableOf 3`] = `"Unknown value \`2\`, expected 1, path: \`@item#0\`."`; + +exports[`ensureMatchesStructurePattern $iterableOf 4`] = `"Unknown value \`2\`, expected 1, path: \`@item#1\`."`; + +exports[`ensureMatchesStructurePattern $mapOf 1`] = `"Unknown value \`3\`, expected 2, path: \`1\`."`; + +exports[`ensureMatchesStructurePattern $mapOf 2`] = `"Unknown value \`3\`, expected 1, path: \`3#key\`."`; + +exports[`ensureMatchesStructurePattern $mapOf 3`] = `"Unknown value \`3\`, expected 1, path: \`3#key\`."`; + +exports[`ensureMatchesStructurePattern $mapOf 4`] = `"Unknown value \`5\`, expected 1, path: \`A\`."`; + +exports[`ensureMatchesStructurePattern $mapOf 5`] = `"Invalid value key provided, expected one of \`\`[A]\`\`, received \`B\`, path: \`\`."`; + +exports[`ensureMatchesStructurePattern $mapOf 6`] = `"Expected a single key"`; + +exports[`ensureMatchesStructurePattern $matchIterable 1`] = `"Unknown value \`3\`, expected 1, path: \`@item#0\`."`; + +exports[`ensureMatchesStructurePattern $matchIterable 2`] = `"Unknown value \`3\`, expected 2, path: \`@item#1\`."`; + +exports[`ensureMatchesStructurePattern $matchIterable 3`] = `"Value iterable is shorter than expected, received: , path: \`\`."`; + +exports[`ensureMatchesStructurePattern $matchIterable 4`] = `"Value iterable is shorter than expected, received: 1, path: \`\`."`; + +exports[`ensureMatchesStructurePattern $matchIterable 5`] = `"Unknown value \`2\`, expected 1, path: \`@item#0\`."`; + +exports[`ensureMatchesStructurePattern $matchObject 1`] = `"Invalid property \`c\`, expected keys: \`\`[a, b]\`\`, path: \`\`, pattern: \`{\\"$matchObject\\":{\\"a\\":{\\"$matchValue\\":1},\\"b\\":{\\"$matchValue\\":2}}}\`"`; + +exports[`ensureMatchesStructurePattern $matchObject 2`] = `"Unknown value \`3\`, expected 1, path: \`a\`."`; + +exports[`ensureMatchesStructurePattern $matchObject 3`] = `"Unknown value \`3\`, expected 2, path: \`b\`."`; + +exports[`ensureMatchesStructurePattern $matchType 1`] = `"Invalid value provided, expected value with type \`number\`, received value with type \`string\`, path: \`\`."`; + +exports[`ensureMatchesStructurePattern $matchValue 1`] = `"Unknown value \`10\`, expected 11, path: \`\`."`; diff --git a/tests/unit/pattern.test.js b/tests/unit/pattern.test.js new file mode 100644 index 000000000..438c84925 --- /dev/null +++ b/tests/unit/pattern.test.js @@ -0,0 +1,140 @@ +import { ensureMatchesStructurePattern } from "../../src/utils/misc"; + +describe("ensureMatchesStructurePattern", () => { + test("$matchType", () => { + const pat = { $matchType: "number" }; + expect(() => + ensureMatchesStructurePattern(pat, "sasda") + ).toThrowErrorMatchingSnapshot(); + ensureMatchesStructurePattern(pat, 10); + }); + + test("$matchValue", () => { + const pat = { $matchValue: 11 }; + + expect(() => + ensureMatchesStructurePattern(pat, 10) + ).toThrowErrorMatchingSnapshot(); + ensureMatchesStructurePattern(pat, 11); + }); + + test("$matchObject", () => { + const pat = { + $matchObject: { a: { $matchValue: 1 }, b: { $matchValue: 2 } }, + }; + + expect(() => + ensureMatchesStructurePattern(pat, { c: 3 }) + ).toThrowErrorMatchingSnapshot(); + expect(() => + ensureMatchesStructurePattern(pat, { a: 3 }) + ).toThrowErrorMatchingSnapshot(); + expect(() => + ensureMatchesStructurePattern(pat, { a: 1, b: 3 }) + ).toThrowErrorMatchingSnapshot(); + + ensureMatchesStructurePattern(pat, { a: 1 }); + ensureMatchesStructurePattern(pat, { b: 2 }); + ensureMatchesStructurePattern(pat, { a: 1, b: 2 }); + }); + + test("$matchIterable", () => { + const pat = { $matchIterable: [{ $matchValue: 1 }, { $matchValue: 2 }] }; + expect(() => + ensureMatchesStructurePattern(pat, [3]) + ).toThrowErrorMatchingSnapshot(); + expect(() => + ensureMatchesStructurePattern(pat, [1, 3]) + ).toThrowErrorMatchingSnapshot(); + expect(() => + ensureMatchesStructurePattern(pat, []) + ).toThrowErrorMatchingSnapshot(); + expect(() => + ensureMatchesStructurePattern(pat, [1]) + ).toThrowErrorMatchingSnapshot(); + expect(() => + ensureMatchesStructurePattern(pat, [2]) + ).toThrowErrorMatchingSnapshot(); + + expect(ensureMatchesStructurePattern(pat, [1, 2])); + }); + + test("$instanceOf", () => { + expect(() => + ensureMatchesStructurePattern({ $instanceOf: Function }, {}) + ).toThrowErrorMatchingSnapshot(); + ensureMatchesStructurePattern({ $instanceOf: Object }, {}); + }); + + test("$iterableOf", () => { + const pat = { $iterableOf: { $matchValue: 1 } }; + + expect(() => + ensureMatchesStructurePattern(pat, [3]) + ).toThrowErrorMatchingSnapshot(); + expect(() => + ensureMatchesStructurePattern(pat, [1, 3]) + ).toThrowErrorMatchingSnapshot(); + + expect(() => + ensureMatchesStructurePattern(pat, [2]) + ).toThrowErrorMatchingSnapshot(); + + expect(() => + ensureMatchesStructurePattern(pat, [1, 2]) + ).toThrowErrorMatchingSnapshot(); + + ensureMatchesStructurePattern(pat, []); + ensureMatchesStructurePattern(pat, [1]); + ensureMatchesStructurePattern(pat, [1, 1, 1, 1, 1]); + }); + + test("$mapOf", () => { + const pat = { $mapOf: [{ $matchValue: 1 }, { $matchValue: 2 }] }; + + expect(() => + ensureMatchesStructurePattern(pat, new Map([[1, 3]])) + ).toThrowErrorMatchingSnapshot(); + expect(() => + ensureMatchesStructurePattern(pat, new Map([[3, 2]])) + ).toThrowErrorMatchingSnapshot(); + expect(() => + ensureMatchesStructurePattern( + pat, + new Map([ + [1, 2], + [3, 1], + ]) + ) + ).toThrowErrorMatchingSnapshot(); + + ensureMatchesStructurePattern(pat, new Map([])); + ensureMatchesStructurePattern(pat, new Map([[1, 2]])); + }); + + test("$anyOf", () => { + const pat = { $anyOf: [{ $matchValue: 1 }, { $matchValue: 2 }] }; + + expect(() => + ensureMatchesStructurePattern(pat, 3) + ).toThrowErrorMatchingSnapshot(); + ensureMatchesStructurePattern(pat, 1); + ensureMatchesStructurePattern(pat, 2); + }); + + test("$mapOf", () => { + const pat = { $objOf: { A: { $matchValue: 1 } } }; + + expect(() => + ensureMatchesStructurePattern(pat, { A: 5 }) + ).toThrowErrorMatchingSnapshot(); + expect(() => + ensureMatchesStructurePattern(pat, { B: 1 }) + ).toThrowErrorMatchingSnapshot(); + expect(() => + ensureMatchesStructurePattern(pat, { A: 1, B: 3 }) + ).toThrowErrorMatchingSnapshot(); + + ensureMatchesStructurePattern(pat, { A: 1 }); + }); +}); From 2158cd27e31b9fa61a5164f119bf1a2ff51d8ff0 Mon Sep 17 00:00:00 2001 From: olegnn Date: Wed, 7 Feb 2024 14:02:55 +0400 Subject: [PATCH 08/12] Refactor a bit --- src/modules/trust-registry.js | 4 +- src/utils/misc.js | 294 +++++++++------ tests/unit/__snapshots__/pattern.test.js.snap | 357 ++++++++++++++++-- tests/unit/pattern.test.js | 102 ++--- 4 files changed, 584 insertions(+), 173 deletions(-) diff --git a/src/modules/trust-registry.js b/src/modules/trust-registry.js index 673ad4918..39ad4374a 100644 --- a/src/modules/trust-registry.js +++ b/src/modules/trust-registry.js @@ -1,6 +1,6 @@ import { BTreeSet, BTreeMap } from '@polkadot/types'; import { DidMethodKey, DockDid, typedHexDID } from '../utils/did'; -import { getDidNonce, ensureMatchesStructurePattern } from '../utils/misc'; +import { getDidNonce, ensureMatchesPattern } from '../utils/misc'; /** * `Trust Registry` module. @@ -91,7 +91,7 @@ export default class TrustRegistryModule { nonce, didModule, }); - ensureMatchesStructurePattern( + ensureMatchesPattern( this.constructor.SchemasUpdatePattern, schemas, ); diff --git a/src/utils/misc.js b/src/utils/misc.js index 1a964a8a5..e846e2342 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ import elliptic from 'elliptic'; import { blake2AsHex, randomAsHex } from '@polkadot/util-crypto'; @@ -231,81 +232,136 @@ export async function getDidNonce( */ export const fmtIter = (iter) => `\`[${[...iter].map((item) => item.toString()).join(', ')}]\``; -const PatternAttrs = new Set([ - '$matchType', - '$matchValue', - '$matchObject', - '$matchIterable', - '$instanceOf', - '$iterableOf', - '$mapOf', - '$anyOf', - '$objOf', -]); - /** - * Ensures that provided value matches supplied pattern, throws an error otherwise. + * Pattern matching error. * - * @param pattern - * @param value + * @param message * @param path + * @param pattern + * @param errors */ -// eslint-disable-next-line sonarjs/cognitive-complexity -export const ensureMatchesStructurePattern = (pattern, value, path = []) => { - const patternAttrs = new Set(Object.keys(pattern)); +export class PatternError extends Error { + constructor(message, path, pattern, errors = []) { + super(message); + + this.message = message; + this.path = path; + this.pattern = pattern; + this.errors = errors; + } +} - for (const key of patternAttrs) { - if (!PatternAttrs.has(key)) { - throw new Error( - `Invalid pattern modifier: ${key}, path: \`${path.join('.')}\`.`, - ); +/** + * Entity used to ensure that provided value matches supplied descriptor(s), throws error(s) otherwise. + */ +export class Pattern { + /** + * Ensures that provided value matches supplied pattern descriptor(s), throws an error otherwise. + * + * @param descriptor + * @param value + * @param path + */ + static check(pattern, value, path = []) { + for (const key of Object.keys(pattern)) { + if (!key.startsWith('$') || this[key] == null) { + throw new PatternError( + `Invalid pattern key \`${key}\`, expected one of \`${fmtIter( + Object.getOwnPropertyNames(this).filter((prop) => prop.startsWith('$')), + )}\``, + ); + } + + try { + this[key](pattern, value, path); + } catch (error) { + if (error instanceof PatternError) { + throw error; + } else { + const message = path.length > 0 + ? `${error.message}, path: \`${path.join('.')}\`` + : error.message; + + throw new PatternError( + message, + path, + pattern, + error.errors, + ); + } + } } } - // eslint-disable-next-line valid-typeof - if (patternAttrs.has('$matchType') && typeof value !== pattern.$matchType) { - throw new Error( - `Invalid value provided, expected value with type \`${ - pattern.$matchType - }\`, received value with type \`${typeof value}\`, path: \`${path.join( - '.', - )}\`.`, - ); + /** + * Returns all an array of supported pattern matchers. + */ + static matchers() { + return Object.getOwnPropertyNames(this).filter((prop) => prop.startsWith('$')); } - if (patternAttrs.has('$matchValue') && value !== pattern.$matchValue) { - throw new Error( - `Unknown value \`${value}\`, expected ${ - pattern.$matchValue - }, path: \`${path.join('.')}\`.`, - ); + /** + * Supplied value matches pattern's type. + * + * @param pattern + * @param value + */ + static $matchType(pattern, value) { + // eslint-disable-next-line valid-typeof + if (typeof value !== pattern.$matchType) { + throw new Error( + `Invalid value provided, expected value with type \`${ + pattern.$matchType + }\`, received value with type \`${typeof value}\``, + ); + } } - if (patternAttrs.has('$matchObject')) { + /** + * Supplied value matches pattern's value. + * + * @param pattern + * @param value + */ + static $matchValue(pattern, value) { + if (value !== pattern.$matchValue) { + throw new Error( + `Unknown value \`${value}\`, expected ${pattern.$matchValue}`, + ); + } + } + + /** + * Supplied value is an object that matches pattern's object patterns. + * + * @param pattern + * @param value + * @param path + */ + static $matchObject(pattern, value, path) { for (const key of Object.keys(value)) { if (!Object.hasOwnProperty.call(pattern.$matchObject, key)) { throw new Error( `Invalid property \`${key}\`, expected keys: \`${fmtIter( Object.keys(pattern.$matchObject), - )}\`, path: \`${path.join('.')}\`, pattern: \`${JSON.stringify( - pattern, )}\``, ); } - ensureMatchesStructurePattern( - pattern.$matchObject[key], - value[key], - path.concat(key), - ); + this.check(pattern.$matchObject[key], value[key], path.concat(key)); } } - if (patternAttrs.has('$matchIterable')) { + /** + * Supplied value is an iterable that matches the pattern's iterable's patterns. + * + * @param pattern + * @param value + * @param path + */ + static $matchIterable(pattern, value, path) { if (typeof value[Symbol.iterator] !== 'function') { - throw new Error( - `Iterable expected, received: ${value}, path: \`${path.join('.')}\`.`, - ); + throw new Error(`Iterable expected, received: ${value}`); } const objectIter = value[Symbol.iterator](); @@ -314,108 +370,134 @@ export const ensureMatchesStructurePattern = (pattern, value, path = []) => { const { value: item, done } = objectIter.next(); if (done) { throw new Error( - `Value iterable is shorter than expected, received: ${fmtIter( - value, - )}, path: \`${path.join('.')}\`.`, + `Value iterable is shorter than expected, received: ${fmtIter(value)}`, ); } - ensureMatchesStructurePattern(pat, item, path.concat(`@item#${idx++}`)); + this.check(pat, item, path.concat(idx++)); } } - if ( - patternAttrs.has('$instanceOf') - && !(value instanceof pattern.$instanceOf) - ) { - throw new Error( - `Invalid value provided, expected instance of \`${ - pattern.$instanceOf.name - }\`, received instance of \`${ - value?.constructor?.name - }\`, path: \`${path.join('.')}\`.`, - ); + /** + * Supplied value is an instance of the pattern's specified constructor. + * + * @param pattern + * @param value + */ + static $instanceOf(pattern, value) { + if (!(value instanceof pattern.$instanceOf)) { + throw new Error( + `Invalid value provided, expected instance of \`${pattern.$instanceOf.name}\`, received instance of \`${value?.constructor?.name}\``, + ); + } } - if (patternAttrs.has('$iterableOf')) { + /** + * Supplied value is an iterable each item of which matches `pattern`'s pattern. + * + * @param pattern + * @param value + * @param path + */ + static $iterableOf(pattern, value, path) { if (typeof value?.[Symbol.iterator] !== 'function') { - throw new Error( - `Iterable expected, received \`${value}\`, path: \`${path.join('.')}\`.`, - ); + throw new Error(`Iterable expected, received \`${value}\``); } let idx = 0; for (const entry of value) { - ensureMatchesStructurePattern( - pattern.$iterableOf, - entry, - path.concat(`@item#${idx++}`), - ); + this.check(pattern.$iterableOf, entry, path.concat(idx++)); } } - if (patternAttrs.has('$mapOf')) { + /** + * Supplied value is a map in which keys and values match `pattern`'s patterns. + * + * @param pattern + * @param value + * @param path + */ + static $mapOf(pattern, value, path) { if (typeof value?.entries !== 'function') { - throw new Error( - `Map expected, received \`${value}\`, path: \`${path.join('.')}\`.`, - ); + throw new Error(`Map expected, received \`${value}\``); } if (!Array.isArray(pattern.$mapOf) || pattern.$mapOf.length !== 2) { throw new Error( `\`$mapOf\` pattern should be an array with two items, received \`${JSON.stringify( pattern, - )}\`, path: \`${path.join('.')}\``, + )}\``, ); } for (const [key, item] of value.entries()) { - ensureMatchesStructurePattern( - pattern.$mapOf[0], - key, - path.concat(`${key}#key`), - ); - ensureMatchesStructurePattern(pattern.$mapOf[1], item, path.concat(key)); + this.check(pattern.$mapOf[0], key, path.concat(`${key}#key`)); + this.check(pattern.$mapOf[1], item, path.concat(key)); } } - if (patternAttrs.has('$anyOf')) { + /** + * Supplied value matches at least one of `pattern`'s patterns. + * + * @param pattern + * @param value + * @param path + */ + static $anyOf(pattern, value, path) { let anySucceeded = false; const errors = []; - for (const patEntry of pattern.$anyOf) { - try { - ensureMatchesStructurePattern(patEntry, value, path); - anySucceeded = true; - } catch (err) { - errors.push(err); + + for (const pat of pattern.$anyOf) { + if (anySucceeded) { + break; + } else { + try { + this.check(pat, value, path); + anySucceeded = true; + } catch (err) { + errors.push(err); + } } } if (!anySucceeded) { - throw new Error( - `Neither of pattern succeeded for \`${value}\`: ${JSON.stringify( - errors.map((err) => err.message), - )}, path: \`${path.join('.')}\`.`, - ); + const error = new Error(`Neither of patterns succeeded for \`${value}\``); + error.errors = errors; + + throw error; } } - if (patternAttrs.has('$objOf')) { + /** + * Supplied value is an object with one key existing in `pattern` that matches the pattern under pattern key. + * + * @param pattern + * @param value + * @param path + */ + static $objOf(pattern, value, path) { const keys = Object.keys(value); if (keys.length !== 1) { throw new Error('Expected a single key'); } - if (!Object.hasOwnProperty.call(pattern.$objOf, keys[0])) { + const [key] = keys; + if (!Object.hasOwnProperty.call(pattern.$objOf, key)) { throw new Error( `Invalid value key provided, expected one of \`${fmtIter( Object.keys(pattern.$objOf), - )}\`, received \`${keys[0]}\`, path: \`${path.join('.')}\`.`, + )}\`, received \`${key}\``, ); } - ensureMatchesStructurePattern( - pattern.$objOf[keys[0]], - value[keys[0]], - path.concat(keys[0]), - ); + + this.check(pattern.$objOf[key], value[key], path.concat(key)); } -}; +} + +/** + * Ensures that provided value matches supplied pattern descriptor(s), throws an error otherwise. + * + * @param pattern + * @param value + * @param path + */ +export const ensureMatchesPattern = Pattern.check.bind(Pattern); diff --git a/tests/unit/__snapshots__/pattern.test.js.snap b/tests/unit/__snapshots__/pattern.test.js.snap index 23e65db5d..a1f655d0d 100644 --- a/tests/unit/__snapshots__/pattern.test.js.snap +++ b/tests/unit/__snapshots__/pattern.test.js.snap @@ -1,45 +1,358 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ensureMatchesStructurePattern $anyOf 1`] = `"Neither of pattern succeeded for \`3\`: [\\"Unknown value \`3\`, expected 1, path: \`\`.\\",\\"Unknown value \`3\`, expected 2, path: \`\`.\\"], path: \`\`."`; +exports[`ensureMatchesPattern $anyOf 1`] = `"Neither of patterns succeeded for \`3\`"`; -exports[`ensureMatchesStructurePattern $instanceOf 1`] = `"Invalid value provided, expected instance of \`Function\`, received instance of \`Object\`, path: \`\`."`; +exports[`ensureMatchesPattern $anyOf 2`] = `Array []`; -exports[`ensureMatchesStructurePattern $iterableOf 1`] = `"Unknown value \`3\`, expected 1, path: \`@item#0\`."`; +exports[`ensureMatchesPattern $anyOf 3`] = ` +Object { + "$anyOf": Array [ + Object { + "$matchValue": 1, + }, + Object { + "$matchValue": 2, + }, + ], +} +`; -exports[`ensureMatchesStructurePattern $iterableOf 2`] = `"Unknown value \`3\`, expected 1, path: \`@item#1\`."`; +exports[`ensureMatchesPattern $anyOf 4`] = ` +Array [ + [Error: Unknown value \`3\`, expected 1], + [Error: Unknown value \`3\`, expected 2], +] +`; -exports[`ensureMatchesStructurePattern $iterableOf 3`] = `"Unknown value \`2\`, expected 1, path: \`@item#0\`."`; +exports[`ensureMatchesPattern $instanceOf 1`] = `"Invalid value provided, expected instance of \`Function\`, received instance of \`Object\`"`; -exports[`ensureMatchesStructurePattern $iterableOf 4`] = `"Unknown value \`2\`, expected 1, path: \`@item#1\`."`; +exports[`ensureMatchesPattern $instanceOf 2`] = `Array []`; -exports[`ensureMatchesStructurePattern $mapOf 1`] = `"Unknown value \`3\`, expected 2, path: \`1\`."`; +exports[`ensureMatchesPattern $instanceOf 3`] = ` +Object { + "$instanceOf": [Function], +} +`; -exports[`ensureMatchesStructurePattern $mapOf 2`] = `"Unknown value \`3\`, expected 1, path: \`3#key\`."`; +exports[`ensureMatchesPattern $instanceOf 4`] = `Array []`; -exports[`ensureMatchesStructurePattern $mapOf 3`] = `"Unknown value \`3\`, expected 1, path: \`3#key\`."`; +exports[`ensureMatchesPattern $iterableOf 1`] = `"Unknown value \`3\`, expected 1, path: \`0\`"`; -exports[`ensureMatchesStructurePattern $mapOf 4`] = `"Unknown value \`5\`, expected 1, path: \`A\`."`; +exports[`ensureMatchesPattern $iterableOf 2`] = ` +Array [ + 0, +] +`; -exports[`ensureMatchesStructurePattern $mapOf 5`] = `"Invalid value key provided, expected one of \`\`[A]\`\`, received \`B\`, path: \`\`."`; +exports[`ensureMatchesPattern $iterableOf 3`] = ` +Object { + "$matchValue": 1, +} +`; -exports[`ensureMatchesStructurePattern $mapOf 6`] = `"Expected a single key"`; +exports[`ensureMatchesPattern $iterableOf 4`] = `Array []`; -exports[`ensureMatchesStructurePattern $matchIterable 1`] = `"Unknown value \`3\`, expected 1, path: \`@item#0\`."`; +exports[`ensureMatchesPattern $iterableOf 5`] = `"Unknown value \`3\`, expected 1, path: \`1\`"`; -exports[`ensureMatchesStructurePattern $matchIterable 2`] = `"Unknown value \`3\`, expected 2, path: \`@item#1\`."`; +exports[`ensureMatchesPattern $iterableOf 6`] = ` +Array [ + 1, +] +`; -exports[`ensureMatchesStructurePattern $matchIterable 3`] = `"Value iterable is shorter than expected, received: \`[]\`, path: \`\`."`; +exports[`ensureMatchesPattern $iterableOf 7`] = ` +Object { + "$matchValue": 1, +} +`; -exports[`ensureMatchesStructurePattern $matchIterable 4`] = `"Value iterable is shorter than expected, received: \`[1]\`, path: \`\`."`; +exports[`ensureMatchesPattern $iterableOf 8`] = `Array []`; -exports[`ensureMatchesStructurePattern $matchIterable 5`] = `"Unknown value \`2\`, expected 1, path: \`@item#0\`."`; +exports[`ensureMatchesPattern $iterableOf 9`] = `"Unknown value \`2\`, expected 1, path: \`0\`"`; -exports[`ensureMatchesStructurePattern $matchObject 1`] = `"Invalid property \`c\`, expected keys: \`\`[a, b]\`\`, path: \`\`, pattern: \`{\\"$matchObject\\":{\\"a\\":{\\"$matchValue\\":1},\\"b\\":{\\"$matchValue\\":2}}}\`"`; +exports[`ensureMatchesPattern $iterableOf 10`] = ` +Array [ + 0, +] +`; -exports[`ensureMatchesStructurePattern $matchObject 2`] = `"Unknown value \`3\`, expected 1, path: \`a\`."`; +exports[`ensureMatchesPattern $iterableOf 11`] = ` +Object { + "$matchValue": 1, +} +`; -exports[`ensureMatchesStructurePattern $matchObject 3`] = `"Unknown value \`3\`, expected 2, path: \`b\`."`; +exports[`ensureMatchesPattern $iterableOf 12`] = `Array []`; -exports[`ensureMatchesStructurePattern $matchType 1`] = `"Invalid value provided, expected value with type \`number\`, received value with type \`string\`, path: \`\`."`; +exports[`ensureMatchesPattern $iterableOf 13`] = `"Unknown value \`2\`, expected 1, path: \`1\`"`; -exports[`ensureMatchesStructurePattern $matchValue 1`] = `"Unknown value \`10\`, expected 11, path: \`\`."`; +exports[`ensureMatchesPattern $iterableOf 14`] = ` +Array [ + 1, +] +`; + +exports[`ensureMatchesPattern $iterableOf 15`] = ` +Object { + "$matchValue": 1, +} +`; + +exports[`ensureMatchesPattern $iterableOf 16`] = `Array []`; + +exports[`ensureMatchesPattern $mapOf 1`] = `"Unknown value \`3\`, expected 2, path: \`1\`"`; + +exports[`ensureMatchesPattern $mapOf 2`] = ` +Array [ + 1, +] +`; + +exports[`ensureMatchesPattern $mapOf 3`] = ` +Object { + "$matchValue": 2, +} +`; + +exports[`ensureMatchesPattern $mapOf 4`] = `Array []`; + +exports[`ensureMatchesPattern $mapOf 5`] = `"Unknown value \`3\`, expected 1, path: \`3#key\`"`; + +exports[`ensureMatchesPattern $mapOf 6`] = ` +Array [ + "3#key", +] +`; + +exports[`ensureMatchesPattern $mapOf 7`] = ` +Object { + "$matchValue": 1, +} +`; + +exports[`ensureMatchesPattern $mapOf 8`] = `Array []`; + +exports[`ensureMatchesPattern $mapOf 9`] = `"Unknown value \`3\`, expected 1, path: \`3#key\`"`; + +exports[`ensureMatchesPattern $mapOf 10`] = ` +Array [ + "3#key", +] +`; + +exports[`ensureMatchesPattern $mapOf 11`] = ` +Object { + "$matchValue": 1, +} +`; + +exports[`ensureMatchesPattern $mapOf 12`] = `Array []`; + +exports[`ensureMatchesPattern $mapOf 13`] = `"Unknown value \`5\`, expected 1, path: \`A\`"`; + +exports[`ensureMatchesPattern $mapOf 14`] = ` +Array [ + "A", +] +`; + +exports[`ensureMatchesPattern $mapOf 15`] = ` +Object { + "$matchValue": 1, +} +`; + +exports[`ensureMatchesPattern $mapOf 16`] = `Array []`; + +exports[`ensureMatchesPattern $mapOf 17`] = `"Invalid value key provided, expected one of \`\`[A]\`\`, received \`B\`"`; + +exports[`ensureMatchesPattern $mapOf 18`] = `Array []`; + +exports[`ensureMatchesPattern $mapOf 19`] = ` +Object { + "$objOf": Object { + "A": Object { + "$matchValue": 1, + }, + }, +} +`; + +exports[`ensureMatchesPattern $mapOf 20`] = `Array []`; + +exports[`ensureMatchesPattern $mapOf 21`] = `"Expected a single key"`; + +exports[`ensureMatchesPattern $mapOf 22`] = `Array []`; + +exports[`ensureMatchesPattern $mapOf 23`] = ` +Object { + "$objOf": Object { + "A": Object { + "$matchValue": 1, + }, + }, +} +`; + +exports[`ensureMatchesPattern $mapOf 24`] = `Array []`; + +exports[`ensureMatchesPattern $matchIterable 1`] = `"Unknown value \`3\`, expected 1, path: \`0\`"`; + +exports[`ensureMatchesPattern $matchIterable 2`] = ` +Array [ + 0, +] +`; + +exports[`ensureMatchesPattern $matchIterable 3`] = ` +Object { + "$matchValue": 1, +} +`; + +exports[`ensureMatchesPattern $matchIterable 4`] = `Array []`; + +exports[`ensureMatchesPattern $matchIterable 5`] = `"Unknown value \`3\`, expected 2, path: \`1\`"`; + +exports[`ensureMatchesPattern $matchIterable 6`] = ` +Array [ + 1, +] +`; + +exports[`ensureMatchesPattern $matchIterable 7`] = ` +Object { + "$matchValue": 2, +} +`; + +exports[`ensureMatchesPattern $matchIterable 8`] = `Array []`; + +exports[`ensureMatchesPattern $matchIterable 9`] = `"Value iterable is shorter than expected, received: \`[]\`"`; + +exports[`ensureMatchesPattern $matchIterable 10`] = `Array []`; + +exports[`ensureMatchesPattern $matchIterable 11`] = ` +Object { + "$matchIterable": Array [ + Object { + "$matchValue": 1, + }, + Object { + "$matchValue": 2, + }, + ], +} +`; + +exports[`ensureMatchesPattern $matchIterable 12`] = `Array []`; + +exports[`ensureMatchesPattern $matchIterable 13`] = `"Value iterable is shorter than expected, received: \`[1]\`"`; + +exports[`ensureMatchesPattern $matchIterable 14`] = `Array []`; + +exports[`ensureMatchesPattern $matchIterable 15`] = ` +Object { + "$matchIterable": Array [ + Object { + "$matchValue": 1, + }, + Object { + "$matchValue": 2, + }, + ], +} +`; + +exports[`ensureMatchesPattern $matchIterable 16`] = `Array []`; + +exports[`ensureMatchesPattern $matchIterable 17`] = `"Unknown value \`2\`, expected 1, path: \`0\`"`; + +exports[`ensureMatchesPattern $matchIterable 18`] = ` +Array [ + 0, +] +`; + +exports[`ensureMatchesPattern $matchIterable 19`] = ` +Object { + "$matchValue": 1, +} +`; + +exports[`ensureMatchesPattern $matchIterable 20`] = `Array []`; + +exports[`ensureMatchesPattern $matchObject 1`] = `"Invalid property \`c\`, expected keys: \`\`[a, b]\`\`"`; + +exports[`ensureMatchesPattern $matchObject 2`] = `Array []`; + +exports[`ensureMatchesPattern $matchObject 3`] = ` +Object { + "$matchObject": Object { + "a": Object { + "$matchValue": 1, + }, + "b": Object { + "$matchValue": 2, + }, + }, +} +`; + +exports[`ensureMatchesPattern $matchObject 4`] = `Array []`; + +exports[`ensureMatchesPattern $matchObject 5`] = `"Unknown value \`3\`, expected 1, path: \`a\`"`; + +exports[`ensureMatchesPattern $matchObject 6`] = ` +Array [ + "a", +] +`; + +exports[`ensureMatchesPattern $matchObject 7`] = ` +Object { + "$matchValue": 1, +} +`; + +exports[`ensureMatchesPattern $matchObject 8`] = `Array []`; + +exports[`ensureMatchesPattern $matchObject 9`] = `"Unknown value \`3\`, expected 2, path: \`b\`"`; + +exports[`ensureMatchesPattern $matchObject 10`] = ` +Array [ + "b", +] +`; + +exports[`ensureMatchesPattern $matchObject 11`] = ` +Object { + "$matchValue": 2, +} +`; + +exports[`ensureMatchesPattern $matchObject 12`] = `Array []`; + +exports[`ensureMatchesPattern $matchType 1`] = `"Invalid value provided, expected value with type \`number\`, received value with type \`string\`"`; + +exports[`ensureMatchesPattern $matchType 2`] = `Array []`; + +exports[`ensureMatchesPattern $matchType 3`] = ` +Object { + "$matchType": "number", +} +`; + +exports[`ensureMatchesPattern $matchType 4`] = `Array []`; + +exports[`ensureMatchesPattern $matchValue 1`] = `"Unknown value \`10\`, expected 11"`; + +exports[`ensureMatchesPattern $matchValue 2`] = `Array []`; + +exports[`ensureMatchesPattern $matchValue 3`] = ` +Object { + "$matchValue": 11, +} +`; + +exports[`ensureMatchesPattern $matchValue 4`] = `Array []`; diff --git a/tests/unit/pattern.test.js b/tests/unit/pattern.test.js index fc9155b35..cbef94dcf 100644 --- a/tests/unit/pattern.test.js +++ b/tests/unit/pattern.test.js @@ -1,17 +1,34 @@ -import { ensureMatchesStructurePattern } from '../../src/utils/misc'; - -describe('ensureMatchesStructurePattern', () => { +import { ensureMatchesPattern } from '../../src/utils/misc'; + +const checkError = fn => { + let thrown = false; + + try { + fn() + } catch(err) { + for (const key of ['message', 'path', 'pattern', 'errors']) { + expect(err[key]).toMatchSnapshot() + } + thrown = true; + } + + if (!thrown) { + throw new Error('Expected an error to be thrown') + } +} + +describe('ensureMatchesPattern', () => { test('$matchType', () => { const pat = { $matchType: 'number' }; - expect(() => ensureMatchesStructurePattern(pat, 'sasda')).toThrowErrorMatchingSnapshot(); - ensureMatchesStructurePattern(pat, 10); + checkError(() => ensureMatchesPattern(pat, 'sasda')); + ensureMatchesPattern(pat, 10); }); test('$matchValue', () => { const pat = { $matchValue: 11 }; - expect(() => ensureMatchesStructurePattern(pat, 10)).toThrowErrorMatchingSnapshot(); - ensureMatchesStructurePattern(pat, 11); + checkError(() => ensureMatchesPattern(pat, 10)); + ensureMatchesPattern(pat, 11); }); test('$matchObject', () => { @@ -19,78 +36,77 @@ describe('ensureMatchesStructurePattern', () => { $matchObject: { a: { $matchValue: 1 }, b: { $matchValue: 2 } }, }; - expect(() => ensureMatchesStructurePattern(pat, { c: 3 })).toThrowErrorMatchingSnapshot(); - expect(() => ensureMatchesStructurePattern(pat, { a: 3 })).toThrowErrorMatchingSnapshot(); - expect(() => ensureMatchesStructurePattern(pat, { a: 1, b: 3 })).toThrowErrorMatchingSnapshot(); + checkError(() => ensureMatchesPattern(pat, { c: 3 })); + checkError(() => ensureMatchesPattern(pat, { a: 3 })); + checkError(() => ensureMatchesPattern(pat, { a: 1, b: 3 })); - ensureMatchesStructurePattern(pat, { a: 1 }); - ensureMatchesStructurePattern(pat, { b: 2 }); - ensureMatchesStructurePattern(pat, { a: 1, b: 2 }); + ensureMatchesPattern(pat, { a: 1 }); + ensureMatchesPattern(pat, { b: 2 }); + ensureMatchesPattern(pat, { a: 1, b: 2 }); }); test('$matchIterable', () => { const pat = { $matchIterable: [{ $matchValue: 1 }, { $matchValue: 2 }] }; - expect(() => ensureMatchesStructurePattern(pat, [3])).toThrowErrorMatchingSnapshot(); - expect(() => ensureMatchesStructurePattern(pat, [1, 3])).toThrowErrorMatchingSnapshot(); - expect(() => ensureMatchesStructurePattern(pat, [])).toThrowErrorMatchingSnapshot(); - expect(() => ensureMatchesStructurePattern(pat, [1])).toThrowErrorMatchingSnapshot(); - expect(() => ensureMatchesStructurePattern(pat, [2])).toThrowErrorMatchingSnapshot(); + checkError(() => ensureMatchesPattern(pat, [3])); + checkError(() => ensureMatchesPattern(pat, [1, 3])); + checkError(() => ensureMatchesPattern(pat, [])); + checkError(() => ensureMatchesPattern(pat, [1])); + checkError(() => ensureMatchesPattern(pat, [2])); - expect(ensureMatchesStructurePattern(pat, [1, 2])); + expect(ensureMatchesPattern(pat, [1, 2])); }); test('$instanceOf', () => { - expect(() => ensureMatchesStructurePattern({ $instanceOf: Function }, {})).toThrowErrorMatchingSnapshot(); - ensureMatchesStructurePattern({ $instanceOf: Object }, {}); + checkError(() => ensureMatchesPattern({ $instanceOf: Function }, {})); + ensureMatchesPattern({ $instanceOf: Object }, {}); }); test('$iterableOf', () => { const pat = { $iterableOf: { $matchValue: 1 } }; - expect(() => ensureMatchesStructurePattern(pat, [3])).toThrowErrorMatchingSnapshot(); - expect(() => ensureMatchesStructurePattern(pat, [1, 3])).toThrowErrorMatchingSnapshot(); + checkError(() => ensureMatchesPattern(pat, [3])); + checkError(() => ensureMatchesPattern(pat, [1, 3])); - expect(() => ensureMatchesStructurePattern(pat, [2])).toThrowErrorMatchingSnapshot(); + checkError(() => ensureMatchesPattern(pat, [2])); - expect(() => ensureMatchesStructurePattern(pat, [1, 2])).toThrowErrorMatchingSnapshot(); + checkError(() => ensureMatchesPattern(pat, [1, 2])); - ensureMatchesStructurePattern(pat, []); - ensureMatchesStructurePattern(pat, [1]); - ensureMatchesStructurePattern(pat, [1, 1, 1, 1, 1]); + ensureMatchesPattern(pat, []); + ensureMatchesPattern(pat, [1]); + ensureMatchesPattern(pat, [1, 1, 1, 1, 1]); }); test('$mapOf', () => { const pat = { $mapOf: [{ $matchValue: 1 }, { $matchValue: 2 }] }; - expect(() => ensureMatchesStructurePattern(pat, new Map([[1, 3]]))).toThrowErrorMatchingSnapshot(); - expect(() => ensureMatchesStructurePattern(pat, new Map([[3, 2]]))).toThrowErrorMatchingSnapshot(); - expect(() => ensureMatchesStructurePattern( + checkError(() => ensureMatchesPattern(pat, new Map([[1, 3]]))); + checkError(() => ensureMatchesPattern(pat, new Map([[3, 2]]))); + checkError(() => ensureMatchesPattern( pat, new Map([ [1, 2], [3, 1], - ]), - )).toThrowErrorMatchingSnapshot(); - - ensureMatchesStructurePattern(pat, new Map([])); - ensureMatchesStructurePattern(pat, new Map([[1, 2]])); + ]) + )); + ensureMatchesPattern(pat, new Map([])); + ensureMatchesPattern(pat, new Map([[1, 2]])); }); test('$anyOf', () => { const pat = { $anyOf: [{ $matchValue: 1 }, { $matchValue: 2 }] }; - expect(() => ensureMatchesStructurePattern(pat, 3)).toThrowErrorMatchingSnapshot(); - ensureMatchesStructurePattern(pat, 1); - ensureMatchesStructurePattern(pat, 2); + checkError(() => ensureMatchesPattern(pat, 3)); + ensureMatchesPattern(pat, 1); + ensureMatchesPattern(pat, 2); }); test('$mapOf', () => { const pat = { $objOf: { A: { $matchValue: 1 } } }; - expect(() => ensureMatchesStructurePattern(pat, { A: 5 })).toThrowErrorMatchingSnapshot(); - expect(() => ensureMatchesStructurePattern(pat, { B: 1 })).toThrowErrorMatchingSnapshot(); - expect(() => ensureMatchesStructurePattern(pat, { A: 1, B: 3 })).toThrowErrorMatchingSnapshot(); + checkError(() => ensureMatchesPattern(pat, { A: 5 })); + checkError(() => ensureMatchesPattern(pat, { B: 1 })); + checkError(() => ensureMatchesPattern(pat, { A: 1, B: 3 })); - ensureMatchesStructurePattern(pat, { A: 1 }); + ensureMatchesPattern(pat, { A: 1 }); }); }); From 5962125145374f98ab4f664138a8205994296942 Mon Sep 17 00:00:00 2001 From: olegnn Date: Wed, 7 Feb 2024 14:10:29 +0400 Subject: [PATCH 09/12] Doc tweak --- src/utils/misc.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/misc.js b/src/utils/misc.js index e846e2342..6e16770ba 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -469,7 +469,7 @@ export class Pattern { } /** - * Supplied value is an object with one key existing in `pattern` that matches the pattern under pattern key. + * Supplied value is an object with one key existing in `pattern` that matches the pattern under this key. * * @param pattern * @param value @@ -481,6 +481,7 @@ export class Pattern { throw new Error('Expected a single key'); } const [key] = keys; + if (!Object.hasOwnProperty.call(pattern.$objOf, key)) { throw new Error( `Invalid value key provided, expected one of \`${fmtIter( From e469704976e13a7bc2dc24f9ddf5fc7008db671c Mon Sep 17 00:00:00 2001 From: olegnn Date: Wed, 7 Feb 2024 15:30:52 +0400 Subject: [PATCH 10/12] Minor tweaks --- src/utils/misc.js | 52 ++++++++++++++++------------------------------- 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/src/utils/misc.js b/src/utils/misc.js index 6e16770ba..81f09c1f5 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -252,24 +252,20 @@ export class PatternError extends Error { } /** - * Entity used to ensure that provided value matches supplied descriptor(s), throws error(s) otherwise. + * Entity used to ensure that provided value matches supplied pattern(s), throws error(s) otherwise. */ -export class Pattern { +export class PatternMatcher { /** - * Ensures that provided value matches supplied pattern descriptor(s), throws an error otherwise. + * Ensures that provided value matches supplied pattern(s), throws an error otherwise. * - * @param descriptor + * @param pattern * @param value * @param path */ - static check(pattern, value, path = []) { + check(pattern, value, path = []) { for (const key of Object.keys(pattern)) { if (!key.startsWith('$') || this[key] == null) { - throw new PatternError( - `Invalid pattern key \`${key}\`, expected one of \`${fmtIter( - Object.getOwnPropertyNames(this).filter((prop) => prop.startsWith('$')), - )}\``, - ); + throw new PatternError(`Invalid pattern key \`${key}\``, [], pattern); } try { @@ -282,31 +278,19 @@ export class Pattern { ? `${error.message}, path: \`${path.join('.')}\`` : error.message; - throw new PatternError( - message, - path, - pattern, - error.errors, - ); + throw new PatternError(message, path, pattern, error.errors); } } } } - /** - * Returns all an array of supported pattern matchers. - */ - static matchers() { - return Object.getOwnPropertyNames(this).filter((prop) => prop.startsWith('$')); - } - /** * Supplied value matches pattern's type. * * @param pattern * @param value */ - static $matchType(pattern, value) { + $matchType(pattern, value) { // eslint-disable-next-line valid-typeof if (typeof value !== pattern.$matchType) { throw new Error( @@ -323,7 +307,7 @@ export class Pattern { * @param pattern * @param value */ - static $matchValue(pattern, value) { + $matchValue(pattern, value) { if (value !== pattern.$matchValue) { throw new Error( `Unknown value \`${value}\`, expected ${pattern.$matchValue}`, @@ -338,7 +322,7 @@ export class Pattern { * @param value * @param path */ - static $matchObject(pattern, value, path) { + $matchObject(pattern, value, path) { for (const key of Object.keys(value)) { if (!Object.hasOwnProperty.call(pattern.$matchObject, key)) { throw new Error( @@ -359,7 +343,7 @@ export class Pattern { * @param value * @param path */ - static $matchIterable(pattern, value, path) { + $matchIterable(pattern, value, path) { if (typeof value[Symbol.iterator] !== 'function') { throw new Error(`Iterable expected, received: ${value}`); } @@ -384,7 +368,7 @@ export class Pattern { * @param pattern * @param value */ - static $instanceOf(pattern, value) { + $instanceOf(pattern, value) { if (!(value instanceof pattern.$instanceOf)) { throw new Error( `Invalid value provided, expected instance of \`${pattern.$instanceOf.name}\`, received instance of \`${value?.constructor?.name}\``, @@ -399,7 +383,7 @@ export class Pattern { * @param value * @param path */ - static $iterableOf(pattern, value, path) { + $iterableOf(pattern, value, path) { if (typeof value?.[Symbol.iterator] !== 'function') { throw new Error(`Iterable expected, received \`${value}\``); } @@ -417,7 +401,7 @@ export class Pattern { * @param value * @param path */ - static $mapOf(pattern, value, path) { + $mapOf(pattern, value, path) { if (typeof value?.entries !== 'function') { throw new Error(`Map expected, received \`${value}\``); } @@ -443,7 +427,7 @@ export class Pattern { * @param value * @param path */ - static $anyOf(pattern, value, path) { + $anyOf(pattern, value, path) { let anySucceeded = false; const errors = []; @@ -475,7 +459,7 @@ export class Pattern { * @param value * @param path */ - static $objOf(pattern, value, path) { + $objOf(pattern, value, path) { const keys = Object.keys(value); if (keys.length !== 1) { throw new Error('Expected a single key'); @@ -495,10 +479,10 @@ export class Pattern { } /** - * Ensures that provided value matches supplied pattern descriptor(s), throws an error otherwise. + * Ensures that provided value matches supplied pattern(s), throws an error otherwise. * * @param pattern * @param value * @param path */ -export const ensureMatchesPattern = Pattern.check.bind(Pattern); +export const ensureMatchesPattern = (pattern, value, path) => new PatternMatcher().check(pattern, value, path); From 9a48a085b76904b006ae1a4e1e7532e690378f35 Mon Sep 17 00:00:00 2001 From: olegnn Date: Wed, 7 Feb 2024 18:33:44 +0400 Subject: [PATCH 11/12] Don\'t log current time for the stash payouts --- scripts/validator_stash_payouts.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/validator_stash_payouts.js b/scripts/validator_stash_payouts.js index bd6c89c03..1511c56a3 100644 --- a/scripts/validator_stash_payouts.js +++ b/scripts/validator_stash_payouts.js @@ -5,7 +5,6 @@ import { notNilAnd, finiteNumber, parseBool, - timestampLogger, binarySearchFirstSatisfyingBlock, } from "./helpers"; import { o, defaultTo, unless, either, curry, __, sum } from "ramda"; @@ -153,7 +152,7 @@ const main = withDockAPI( .sort(([i1], [i2]) => i1 - i2) .reduce( (acc, [index, { total, staking, commission, blocks, prefs }]) => { - timestampLogger.log( + console.log( `Era ${index}: paid = \`${formatDock( total )}\` (staking = ${formatDock(staking)}, commission = ${formatDock( @@ -178,7 +177,7 @@ const main = withDockAPI( } ); - timestampLogger.log( + console.log( `Summarised stash payout for ${Stash} in ${StartEra}-${ StartEra + ErasCount - 1 } eras - total = \`${formatDock(total, { @@ -390,4 +389,4 @@ const validatorStashPayout = ( }; }; -main().catch(timestampLogger.error); +main().catch(console.error); From 41fc099fa3a15efe5f8ab1f2625a176d4429573d Mon Sep 17 00:00:00 2001 From: olegnn Date: Wed, 7 Feb 2024 20:29:52 +0400 Subject: [PATCH 12/12] Change `ensureMatchesPattern` signature --- src/utils/misc.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils/misc.js b/src/utils/misc.js index 81f09c1f5..522bcb57f 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -260,7 +260,7 @@ export class PatternMatcher { * * @param pattern * @param value - * @param path + * @param {?Array} path */ check(pattern, value, path = []) { for (const key of Object.keys(pattern)) { @@ -483,6 +483,5 @@ export class PatternMatcher { * * @param pattern * @param value - * @param path */ -export const ensureMatchesPattern = (pattern, value, path) => new PatternMatcher().check(pattern, value, path); +export const ensureMatchesPattern = (pattern, value) => new PatternMatcher().check(pattern, value);