diff --git a/packages/ua-utils-evm/.eslintignore b/packages/ua-utils-evm/.eslintignore new file mode 100644 index 000000000..db4c6d9b6 --- /dev/null +++ b/packages/ua-utils-evm/.eslintignore @@ -0,0 +1,2 @@ +dist +node_modules \ No newline at end of file diff --git a/packages/ua-utils-evm/.mocharc.json b/packages/ua-utils-evm/.mocharc.json new file mode 100644 index 000000000..d4ee538e6 --- /dev/null +++ b/packages/ua-utils-evm/.mocharc.json @@ -0,0 +1,5 @@ +{ + "extensions": ["ts"], + "spec": ["**/*.test.*"], + "loader": "ts-node/esm" +} diff --git a/packages/ua-utils-evm/.prettierignore b/packages/ua-utils-evm/.prettierignore new file mode 100644 index 000000000..763301fc0 --- /dev/null +++ b/packages/ua-utils-evm/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ \ No newline at end of file diff --git a/packages/ua-utils-evm/README.md b/packages/ua-utils-evm/README.md new file mode 100644 index 000000000..309a5701f --- /dev/null +++ b/packages/ua-utils-evm/README.md @@ -0,0 +1,27 @@ +

+ + LayerZero + +

+ +

@layerzerolabs/ua-utils-evm

+ + +

+ + NPM Version + + Downloads + + NPM License +

+ +## Installation + +```bash +yarn add @layerzerolabs/ua-utils-evm + +pnpm add @layerzerolabs/ua-utils-evm + +npm install @layerzerolabs/ua-utils-evm +``` diff --git a/packages/ua-utils-evm/package.json b/packages/ua-utils-evm/package.json new file mode 100644 index 000000000..e70596212 --- /dev/null +++ b/packages/ua-utils-evm/package.json @@ -0,0 +1,49 @@ +{ + "name": "@layerzerolabs/ua-utils-evm", + "description": "Utilities for working with LayerZero EVM projects", + "version": "0.0.1", + "license": "MIT", + "private": true, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "module": "./dist/index.mjs", + "exports": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.mjs" + }, + "files": [ + "./dist/index.*" + ], + "scripts": { + "build": "npx tsup", + "clean": "rm -rf dist", + "dev": "npx tsup --watch", + "lint": "npx eslint '**/*.{js,ts,json}'", + "prebuild": "npx tsc --noEmit -p tsconfig.build.json", + "test": "mocha --parallel" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/LayerZero-Labs/lz-utils.git", + "directory": "packages/ua-utils-evm" + }, + "devDependencies": { + "@ethersproject/contracts": "5.7.0", + "@layerzerolabs/lz-definitions": "~1.5.58", + "@layerzerolabs/ua-utils": "~0.1.0", + "@types/mocha": "^10.0.1", + "@types/sinon": "^17.0.2", + "chai": "^4.3.10", + "mocha": "^10.2.0", + "sinon": "^17.0.1", + "ts-node": "^10.9.1", + "tsup": "~7.2.0", + "typescript": "^5.2.2" + }, + "peerDependencies": { + "@ethersproject/contracts": "5.7.0", + "@layerzerolabs/lz-definitions": "~1.5.58", + "@layerzerolabs/ua-utils": "~0.1.0" + } +} diff --git a/packages/ua-utils-evm/src/index.ts b/packages/ua-utils-evm/src/index.ts new file mode 100644 index 000000000..9e4b10a93 --- /dev/null +++ b/packages/ua-utils-evm/src/index.ts @@ -0,0 +1 @@ +export * from "./oapp" diff --git a/packages/ua-utils-evm/src/oapp.ts b/packages/ua-utils-evm/src/oapp.ts new file mode 100644 index 000000000..c424baf6f --- /dev/null +++ b/packages/ua-utils-evm/src/oapp.ts @@ -0,0 +1,14 @@ +import { Contract } from "@ethersproject/contracts" +import { GetPropertyValue, createProperty } from "@layerzerolabs/ua-utils" +import { EndpointId } from "@layerzerolabs/lz-definitions" + +type SetPeerConfigurableContext = [oapp: Contract, endpointId: EndpointId] + +type SetPeerConfigurableValue = string + +export const createSetPeerProperty = (desired: GetPropertyValue) => + createProperty({ + desired, + get: (oapp, endpointId) => oapp.peers(endpointId), + set: (oapp, endpointId, peer) => oapp.setPeer(endpointId, peer), + }) diff --git a/packages/ua-utils-evm/test/oapp.test.ts b/packages/ua-utils-evm/test/oapp.test.ts new file mode 100644 index 000000000..8fc3e4d94 --- /dev/null +++ b/packages/ua-utils-evm/test/oapp.test.ts @@ -0,0 +1,30 @@ +import { Contract } from "@ethersproject/contracts" +import { EndpointId } from "@layerzerolabs/lz-definitions" +import { expect } from "chai" +import { describe } from "mocha" +import sinon from "sinon" +import { createSetPeerProperty } from "../src/oapp" +import { isMisconfigured } from "@layerzerolabs/ua-utils" + +describe("oapp", () => { + describe("createSetPeerProperty", () => { + it("should check peers and return Misconfigured if they don't match", async () => { + const peers = sinon.stub().resolves("peer-not-set") + const setPeer = sinon.stub().resolves("okay") + const oapp = { peers, setPeer } as unknown as Contract + + const desired = (oapp: Contract, endpointId: EndpointId) => `peer-on-${endpointId}` + const configurable = createSetPeerProperty(desired) + const state = await configurable(oapp, EndpointId.AAVEGOTCHI_TESTNET) + + expect(isMisconfigured(state)).to.be.true + expect(state.value).to.eql("peer-not-set") + expect(state.desiredValue).to.eql(`peer-on-${EndpointId.AAVEGOTCHI_TESTNET}`) + + const result = await state.configure?.() + + expect(result).to.eql("okay") + expect(setPeer.calledOnceWith(EndpointId.AAVEGOTCHI_TESTNET, `peer-on-${EndpointId.AAVEGOTCHI_TESTNET}`)).to.be.true + }) + }) +}) diff --git a/packages/ua-utils-evm/tsconfig.build.json b/packages/ua-utils-evm/tsconfig.build.json new file mode 100644 index 000000000..1e8f7493d --- /dev/null +++ b/packages/ua-utils-evm/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "test"] +} diff --git a/packages/ua-utils-evm/tsconfig.json b/packages/ua-utils-evm/tsconfig.json new file mode 100644 index 000000000..084db5958 --- /dev/null +++ b/packages/ua-utils-evm/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "exclude": ["dist", "node_modules"], + "include": ["src", "test", "*.config.ts"], + "compilerOptions": { + "module": "commonjs", + "types": ["node", "mocha"] + } +} diff --git a/packages/ua-utils-evm/tsup.config.ts b/packages/ua-utils-evm/tsup.config.ts new file mode 100644 index 000000000..16d909226 --- /dev/null +++ b/packages/ua-utils-evm/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup" + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "./dist", + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ["esm", "cjs"], +}) diff --git a/packages/ua-utils/.eslintignore b/packages/ua-utils/.eslintignore new file mode 100644 index 000000000..db4c6d9b6 --- /dev/null +++ b/packages/ua-utils/.eslintignore @@ -0,0 +1,2 @@ +dist +node_modules \ No newline at end of file diff --git a/packages/ua-utils/.mocharc.json b/packages/ua-utils/.mocharc.json new file mode 100644 index 000000000..d4ee538e6 --- /dev/null +++ b/packages/ua-utils/.mocharc.json @@ -0,0 +1,5 @@ +{ + "extensions": ["ts"], + "spec": ["**/*.test.*"], + "loader": "ts-node/esm" +} diff --git a/packages/ua-utils/.prettierignore b/packages/ua-utils/.prettierignore new file mode 100644 index 000000000..763301fc0 --- /dev/null +++ b/packages/ua-utils/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ \ No newline at end of file diff --git a/packages/ua-utils/README.md b/packages/ua-utils/README.md new file mode 100644 index 000000000..51f38cdfd --- /dev/null +++ b/packages/ua-utils/README.md @@ -0,0 +1,161 @@ +

+ + LayerZero + +

+ +

@layerzerolabs/ua-utils

+ + +

+ + NPM Version + + Downloads + + NPM License +

+ +## Installation + +```bash +yarn add @layerzerolabs/ua-utils + +pnpm add @layerzerolabs/ua-utils + +npm install @layerzerolabs/ua-utils +``` + +## Usage + +### `createProperty` + +Creates a `Property` object - an abstract wrapper around a getter and setter of a (typically contract) property. + +`Property` objects can be evaluated - this evaluation results in a `PropertyState` object which comes in two varieties: + +- `Configured` object represents a property whose desired value matches the current, actual value +- `Misconfigured` object represents a property whose desired value does not match the current value. It can be further executed using its `configure` method to set the desired property value + +```typescript +import type { Contract } from "@etherspropject/contracts" +import { createProperty, isMisconfigured } from "@layerzerolabs/ua-utils" + +// In this example we'll creating a property that is executed within a context of a contract +// +// What this means is that the property requires a contract to be evaluated. This context +// is then passed to the getter, setter and the desired value getter +const myContractProperty = createProperty({ + get: (contract: Contract) => contract.getFavouriteAddress(), + set: (contract: Contract, favouriteAddress: string) => contract.setFavouriteAddress(favouriteAddress), + desired: (contract: Contract) => "0x00000000219ab540356cbb839cbe05303d7705fa", +}) + +// Let's pretend we have a contract at hand +declare const myContract: Contract + +// We'll evaluate this property by passing myContract in. This contract will then be passed +// to the getter, setter and desired value getter +// +// The result of this evaluation is a PropertyState object +const state = await myContractProperty(myContract) + +// This package comes with two type narrowing utilities for working with PropertyState objects: +// +// - isConfigured +// - isMisconfigured +// +// Using these we can discern between the two varieties of PropertyState +if (isMisconfigured(state)) { + await state.configure() +} +``` + +In the example above we used a simple `Contract` object as our context. We can use arbitrary context as the following example shows: + +```typescript +import { createProperty, isMisconfigured } from "@layerzerolabs/ua-utils" + +const myContractPropertyWithParams = createProperty({ + get: (contract: Contract, when: number, where: string) => contract.getFavouriteAddress(when, where), + set: (contract: Contract, when: number, where: string, favouriteAddress: string) => + contract.setFavouriteAddress(when, where, favouriteAddress), + desired: (contract: Contract, when: number, where: string) => "0x00000000219ab540356cbb839cbe05303d7705fa", +}) + +// We'll again pretend we have a contract at hand +declare const myContract: Contract +const when = Date.now() +const where = "Antractica" + +// We'll evaluate this property by passing the required context in - in this case +// the context consists of a contract, a numeric value and a string value +// +// The result of this evaluation is a PropertyState object +const state = await myContractPropertyWithParams(myContract, when, where) + +// The execution goes just like before +if (isMisconfigured(state)) { + await state.configure() +} +``` + +The `createProperty` is completely abstract though and does not require us to get a single contract property or set it directly. in the following, completely made-up example we'll get multiple properties at once and instead of setting them, we'll just populate the transactions for further executions: + +```typescript +import { createProperty, isMisconfigured } from "@layerzerolabs/ua-utils" + +const myContractPropertyWithParams = createProperty({ + get: (contract: Contract) => Promise.all([contract.getA(), contract.getB()]), + set: (contract: Contract, [a, b, c]) => [ + contract.pupulateTransaction.setA(a), + contract.pupulateTransaction.setB(b), + contract.pupulateTransaction.setC(c), + ], + desired: (contract: Contract) => [7, 11, 17], +}) + +// We'll again pretend we have a contract at hand +declare const myContract: Contract + +// We'll evaluate this property by passing the required context in - in this case +// the context consists of a contract, a numeric value and a string value +// +// The result of this evaluation is a PropertyState object +const state = await myContractPropertyWithParams(myContract, when, where) + +// The execution goes just like before +if (isMisconfigured(state)) { + const transactions = await state.configure() + + // We now have a list of populated transactions to execute +} +``` + +### `isConfigured` + +Helper type assertion utility that narrows down the `PropertyState` type to `Configured`: + +```typescript +import { PropertyState, isConfigured } from "@layerzerolabs/ua-utils" + +declare const state: PropertyState + +if (isConfigured(state)) { + // state is now Configured, no action is needed as the property is in its desired state +} +``` + +### `isMisconfigured` + +Helper type assertion utility that narrows down the `PropertyState` type to `Misconfigured`: + +```typescript +import { PropertyState, isMisconfigured } from "@layerzerolabs/ua-utils" + +declare const state: PropertyState + +if (isMisconfigured(state)) { + // state is now Misconfigured, we can e.g. call .configure +} +``` diff --git a/packages/ua-utils/package.json b/packages/ua-utils/package.json new file mode 100644 index 000000000..859afb4c6 --- /dev/null +++ b/packages/ua-utils/package.json @@ -0,0 +1,41 @@ +{ + "name": "@layerzerolabs/ua-utils", + "description": "Utilities for working with LayerZero projects", + "version": "0.1.0", + "license": "MIT", + "private": true, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "module": "./dist/index.mjs", + "exports": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.mjs" + }, + "files": [ + "./dist/index.*" + ], + "scripts": { + "build": "npx tsup", + "clean": "rm -rf dist", + "dev": "npx tsup --watch", + "lint": "npx eslint '**/*.{js,ts,json}'", + "prebuild": "npx tsc --noEmit -p tsconfig.build.json", + "test": "mocha --parallel" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/LayerZero-Labs/lz-utils.git", + "directory": "packages/ua-utils" + }, + "devDependencies": { + "@types/mocha": "^10.0.1", + "@types/sinon": "^17.0.2", + "chai": "^4.3.10", + "mocha": "^10.2.0", + "sinon": "^17.0.1", + "ts-node": "^10.9.1", + "tsup": "~7.2.0", + "typescript": "^5.2.2" + } +} diff --git a/packages/ua-utils/src/index.ts b/packages/ua-utils/src/index.ts new file mode 100644 index 000000000..611bab50c --- /dev/null +++ b/packages/ua-utils/src/index.ts @@ -0,0 +1 @@ +export * from "./property" diff --git a/packages/ua-utils/src/property.ts b/packages/ua-utils/src/property.ts new file mode 100644 index 000000000..c08c735cd --- /dev/null +++ b/packages/ua-utils/src/property.ts @@ -0,0 +1,116 @@ +import assert from "assert" + +/** + * The central concept of this module - a structure that can be evaluated into a `PropertyState` + * + * `Property` has three basic functions: + * + * - Property getter that gets the current value of the property + * - Property setter that sets value of the property + * - Property desired value getter that grabs the value this property should be set to + */ +export type Property = ( + ...context: TContext +) => Promise> + +/** + * Type encapsulating two states of a configurable property: `Configured` and `Misconfigured` + * + * Property property is understood as anything that has a getter and setter + * and its value needs to match a desired value (coming from some sort of a configuration). + */ +export type PropertyState = + | Configured + | Misconfigured + +/** + * Interface for configured state of a configurable property. + * + * In configured state, the current value of the property matches its desired state + * and no action is necessary. + */ +export interface Configured { + context: TContext + value: TValue + desiredValue?: never + configure?: never +} + +/** + * Interface for misconfigured state of a configurable property. + * + * In misconfigured state, the current value of the property does not match its desired state + * and an action needs to be taken to synchronize these two. + */ +export interface Misconfigured { + context: TContext + value: TValue + desiredValue: TValue + configure: () => TResult | Promise +} + +export type GetPropertyValue = (...context: TContext) => TValue | Promise + +export type SetPropertyValue = ( + ...params: [...TContext, TValue] +) => TResult | Promise + +export interface PropertyOptions { + desired: GetPropertyValue + get: GetPropertyValue + set: SetPropertyValue +} + +/** + * Property factory, the central functional piece of this module. + * + * @param `PropertyOptions` + * + * @returns `Property` + */ +export const createProperty = + ({ + get, + set, + desired, + }: PropertyOptions): Property => + async (...context) => { + // First we grab the current & desired states of the property + const [value, desiredValue] = await Promise.all([get(...context), desired(...context)]) + + // Now we compare the current & desired states using value equality (i.e. values + // with the same shape will be considered equal) + // + // We'll use the native deep equality function that throws an AssertionError + // when things don't match so we need to try/catch and understand the catch branch + // as inequality + try { + assert.deepStrictEqual(value, desiredValue) + + // The values matched, we return a Configured + return { context, value } + } catch { + // The values did not match, we'll return a Misconfigured + return { context, value, desiredValue, configure: async () => set(...context, desiredValue) } + } + } + +/** + * Type assertion utility for narrowing the `PropertyState` type to `Misconfigured` type + * + * @param value `PropertyState` + * @returns `value is Misconfigured` + */ +export const isMisconfigured = ( + value: PropertyState +): value is Misconfigured => "configure" in value && "desiredValue" in value && typeof value.configure === "function" + +/** + * Type assertion utility for narrowing the `PropertyState` type to `Configured` type + * + * @param value `PropertyState` + * @returns `value is Configured` + */ +export const isConfigured = ( + value: PropertyState +): value is Configured => !isMisconfigured(value) diff --git a/packages/ua-utils/test/property.test.ts b/packages/ua-utils/test/property.test.ts new file mode 100644 index 000000000..c709002d8 --- /dev/null +++ b/packages/ua-utils/test/property.test.ts @@ -0,0 +1,103 @@ +import { expect } from "chai" +import { describe } from "mocha" +import sinon from "sinon" +import { createProperty, isConfigured, isMisconfigured } from "../src/property" + +describe("property", () => { + describe("isMisconfigured", () => { + it("should return true if value is Misconfigured", () => { + expect(isMisconfigured({ context: [], value: false, desiredValue: true, configure: () => {} })).to.be.true + expect(isMisconfigured({ context: [], value: null, desiredValue: null, configure: () => {} })).to.be.true + expect(isMisconfigured({ context: [], value: 0, desiredValue: 0, configure: () => {} })).to.be.true + }) + + it("should return false if value is Configured", () => { + expect(isMisconfigured({ context: [], value: false })).to.be.false + expect(isMisconfigured({ context: [], value: true })).to.be.false + expect(isMisconfigured({ context: [], value: 1 })).to.be.false + }) + }) + + describe("isConfigured", () => { + it("should return false if value is Configured", () => { + expect(isConfigured({ context: [], value: false, desiredValue: true, configure: () => {} })).to.be.false + expect(isConfigured({ context: [], value: null, desiredValue: null, configure: () => {} })).to.be.false + expect(isConfigured({ context: [], value: 0, desiredValue: 0, configure: () => {} })).to.be.false + }) + + it("should return true if value is Misconfigured", () => { + expect(isConfigured({ context: [], value: false })).to.be.true + expect(isConfigured({ context: [], value: true })).to.be.true + expect(isConfigured({ context: [], value: 1 })).to.be.true + }) + }) + + describe("createProperty", () => { + it("should return Configured if the current and desired values match", async () => { + const currentValue = [1, "two", { three: true }] + const desiredValue = [1, "two", { three: true }] + + const property = createProperty({ + desired: async () => desiredValue, + get: () => currentValue, + set: () => {}, + }) + + const state = await property() + + expect(isMisconfigured(state)).to.be.false + expect(state.context).to.eql([]) + }) + + it("should return Misconfigured if the current and desired don't match", async () => { + const currentValue = [1, "two", { three: true }] + const desiredValue = [1, "two", { three: false }] + + const property = createProperty({ + desired: async () => desiredValue, + get: () => currentValue, + set: () => {}, + }) + + const state = await property() + expect(isMisconfigured(state)).to.be.true + expect(state.context).to.eql([]) + }) + + it("should call the setter with desired value when executed", async () => { + const currentValue = [1, "two", { three: true }] + const desiredValue = [1, "two", { three: false }] + + const set = sinon.spy() + const property = createProperty({ + desired: () => desiredValue, + get: () => currentValue, + set, + }) + + const state = await property() + await state.configure?.() + + expect(set.calledOnceWith(desiredValue)).to.be.true + expect(state.context).to.eql([]) + }) + + it("should call the setter with context when executed", async () => { + const currentValue = [1, "two", { three: true }] + const desiredValue = [1, "two", { three: false }] + + const set = sinon.spy() + const property = createProperty({ + desired: (context: string) => desiredValue, + get: (context: string) => currentValue, + set, + }) + + const state = await property("context") + await state.configure?.() + + expect(set.calledOnceWith("context", desiredValue)).to.be.true + expect(state.context).to.eql(["context"]) + }) + }) +}) diff --git a/packages/ua-utils/tsconfig.build.json b/packages/ua-utils/tsconfig.build.json new file mode 100644 index 000000000..1e8f7493d --- /dev/null +++ b/packages/ua-utils/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "test"] +} diff --git a/packages/ua-utils/tsconfig.json b/packages/ua-utils/tsconfig.json new file mode 100644 index 000000000..084db5958 --- /dev/null +++ b/packages/ua-utils/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "exclude": ["dist", "node_modules"], + "include": ["src", "test", "*.config.ts"], + "compilerOptions": { + "module": "commonjs", + "types": ["node", "mocha"] + } +} diff --git a/packages/ua-utils/tsup.config.ts b/packages/ua-utils/tsup.config.ts new file mode 100644 index 000000000..16d909226 --- /dev/null +++ b/packages/ua-utils/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup" + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "./dist", + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ["esm", "cjs"], +}) diff --git a/yarn.lock b/yarn.lock index aadb9bfcb..c77161157 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1622,6 +1622,18 @@ resolved "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz" integrity sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg== +"@types/sinon@^17.0.2": + version "17.0.2" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.2.tgz#9a769f67e62b45b7233f1fe01cb1f231d2393e1c" + integrity sha512-Zt6heIGsdqERkxctIpvN5Pv3edgBrhoeb3yHyxffd4InN0AX2SVNKSrhdDZKGQICVOxWP/q4DyhpfPNMSrpIiA== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "8.1.5" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz#5fd3592ff10c1e9695d377020c033116cc2889f2" + integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ== + "@types/tinycolor2@*", "@types/tinycolor2@^1.4.0": version "1.4.6" resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz#670cbc0caf4e58dd61d1e3a6f26386e473087f06" @@ -5286,7 +5298,7 @@ mnemonist@^0.38.0: dependencies: obliterator "^2.0.0" -mocha@^10.0.0: +mocha@^10.0.0, mocha@^10.2.0: version "10.2.0" resolved "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz" integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==