From 07043122b4018c7031c2fcf70c385f300dc42144 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 11 Nov 2022 14:12:22 -0700 Subject: [PATCH] Convert to monorepo (#831) Currently, this repo and everything within it is published as a single package. Using and maintaining this package, however, is problematic for a few reasons: 1. Even if your library uses a couple of controllers, you must add the entire package and all of its dependencies to your library's dependency tree. 2. Because this package is used by many teams, when we make a new release, even if that release contains changes to one controller, we must coordinate with all teams to ensure that nothing has broken. In addition, we want to be able to maintain our existing libraries more easily, as right now it is difficult due to code being split across multiple repositories. To solve this problem, this commit converts the existing structure to a monorepo structure, assigning controllers to packages which we can then publish individually. (A full list of packages is contained in the README.) Along with a monorepo structure comes with a litany of changes: * **TypeScript:** We have a "master" TypeScript config file, which is used by developers' code editors, but each package also has its own TypeScript config files. We are also using TypeScript project references, which allows us to inform TypeScript how all packages are connected to each other dependency-wise; this allows TypeScript to know which packages to build first. * **Jest:** Each package has its own Jest config file, and we use Yarn workspaces to run the tests for each package in parallel. * **ESLint:** We are able to lint the monorepo in the same way as we linted before. * **TypeDoc:** We've added TypeDoc to each package and use Yarn workspaces to generate docs for each package in parallel. * **Yarn:** A bunch of Yarn constraints have been added that verify that both the root package and each package has a well-formed `package.json`. * **Other notes:** * Some packages depend on other packages within the monorepo. In other words, we might have an import for `@metamask/base-controller` within a controller file. Out of the box both TypeScript and Jest won't know what to do with this. Although Yarn will add a symlink in `node_modules` to the proper directory in `packages` for the package in question, TypeScript expects the code for the package to be compiled (i.e. for `dist/` to be populated), and Jest, as it has custom resolver logic, doesn't know what to do at all. To make this possible we have to add a custom mapping for both TypeScript and Jest that will tell them what to do when it sees a `@metamask/*` import. * The GitHub Action workflow files have been standardized against the newest changes to the module template. Co-authored-by: Mark Stacey Co-authored-by: Maarten Zuidhoorn --- .github/workflows/create-release-pr.yml | 43 - .github/workflows/lint-build-test.yml | 103 +- .github/workflows/main.yml | 51 + .github/workflows/publish-release.yml | 61 +- .gitignore | 3 + .yarn/plugins/@yarnpkg/plugin-constraints.cjs | 52 + .../@yarnpkg/plugin-workspace-tools.cjs | 28 + .yarnrc.yml | 4 + CHANGELOG.md => CHANGELOG.old.md | 0 README.md | 70 +- __mocks__/uuid.js | 14 - constraints.pro | 248 +++ jest.config.js | 35 - jest.config.packages.js | 202 +++ package.json | 100 +- packages/address-book-controller/CHANGELOG.md | 9 + .../address-book-controller/LICENSE | 0 packages/address-book-controller/README.md | 15 + .../address-book-controller/jest.config.js | 25 + packages/address-book-controller/package.json | 50 + .../src}/AddressBookController.test.ts | 0 .../src}/AddressBookController.ts | 8 +- packages/address-book-controller/src/index.ts | 1 + .../tsconfig.build.json | 13 + .../address-book-controller/tsconfig.json | 11 + .../address-book-controller/typedoc.json | 3 +- packages/announcement-controller/CHANGELOG.md | 9 + packages/announcement-controller/LICENSE | 20 + packages/announcement-controller/README.md | 15 + .../announcement-controller/jest.config.js | 25 + packages/announcement-controller/package.json | 49 + .../src}/AnnouncementController.test.ts | 0 .../src}/AnnouncementController.ts | 6 +- packages/announcement-controller/src/index.ts | 1 + .../tsconfig.build.json | 10 + .../announcement-controller/tsconfig.json | 8 + packages/announcement-controller/typedoc.json | 7 + packages/approval-controller/CHANGELOG.md | 9 + packages/approval-controller/LICENSE | 20 + packages/approval-controller/README.md | 15 + packages/approval-controller/jest.config.js | 25 + packages/approval-controller/package.json | 53 + .../src}/ApprovalController.test.ts | 247 ++- .../src}/ApprovalController.ts | 10 +- .../approval-controller/src}/errors.ts | 0 .../approval-controller/src}/index.ts | 0 .../approval-controller/tsconfig.build.json | 10 + packages/approval-controller/tsconfig.json | 8 + packages/approval-controller/typedoc.json | 7 + packages/assets-controllers/CHANGELOG.md | 9 + packages/assets-controllers/LICENSE | 20 + packages/assets-controllers/README.md | 30 + packages/assets-controllers/jest.config.js | 28 + packages/assets-controllers/package.json | 74 + .../src}/AccountTrackerController.test.ts | 25 +- .../src}/AccountTrackerController.ts | 16 +- .../src}/AssetsContractController.test.ts | 10 +- .../src}/AssetsContractController.ts | 14 +- .../src}/CurrencyRateController.test.ts | 6 +- .../src}/CurrencyRateController.ts | 21 +- .../src}/NftController.test.ts | 14 +- .../assets-controllers/src}/NftController.ts | 20 +- .../src}/NftDetectionController.test.ts | 10 +- .../src}/NftDetectionController.ts | 23 +- .../src}/Standards/ERC20Standard.test.ts | 4 +- .../src}/Standards/ERC20Standard.ts | 2 +- .../ERC1155/ERC1155Standard.test.ts | 3 +- .../NftStandards/ERC1155/ERC1155Standard.ts | 6 +- .../ERC721/ERC721Standard.test.ts | 3 +- .../NftStandards/ERC721/ERC721Standard.ts | 7 +- .../src}/Standards/standards-types.ts | 0 .../src}/TokenBalancesController.test.ts | 8 +- .../src}/TokenBalancesController.ts | 12 +- .../src}/TokenDetectionController.test.ts | 22 +- .../src}/TokenDetectionController.ts | 16 +- .../src}/TokenListController.test.ts | 56 +- .../src}/TokenListController.ts | 30 +- .../src}/TokenRatesController.test.ts | 8 +- .../src}/TokenRatesController.ts | 21 +- .../src}/TokensController.test.ts | 47 +- .../src}/TokensController.ts | 38 +- .../assets-controllers/src/assetsUtil.test.ts | 435 ++++++ packages/assets-controllers/src/assetsUtil.ts | 250 ++++ .../src}/crypto-compare.test.ts | 1 - .../assets-controllers/src}/crypto-compare.ts | 2 +- packages/assets-controllers/src/index.ts | 11 + .../src}/token-service.test.ts | 74 +- .../assets-controllers/src}/token-service.ts | 3 +- .../assets-controllers/tsconfig.build.json | 15 + packages/assets-controllers/tsconfig.json | 13 + packages/assets-controllers/typedoc.json | 7 + packages/base-controller/CHANGELOG.md | 9 + packages/base-controller/LICENSE | 20 + packages/base-controller/README.md | 15 + packages/base-controller/jest.config.js | 25 + packages/base-controller/package.json | 51 + .../src}/BaseController.test.ts | 2 +- .../base-controller/src}/BaseController.ts | 0 .../src}/BaseControllerV2.test.ts | 3 +- .../base-controller/src}/BaseControllerV2.ts | 0 .../src}/ControllerMessenger.test.ts | 3 +- .../src}/ControllerMessenger.ts | 0 packages/base-controller/src/index.ts | 17 + packages/base-controller/tsconfig.build.json | 9 + packages/base-controller/tsconfig.json | 7 + packages/base-controller/typedoc.json | 7 + packages/composable-controller/CHANGELOG.md | 9 + packages/composable-controller/LICENSE | 20 + packages/composable-controller/README.md | 15 + packages/composable-controller/jest.config.js | 25 + packages/composable-controller/package.json | 57 + .../src}/ComposableController.test.ts | 33 +- .../src}/ComposableController.ts | 6 +- packages/composable-controller/src/index.ts | 1 + .../composable-controller/tsconfig.build.json | 29 + packages/composable-controller/tsconfig.json | 27 + packages/composable-controller/typedoc.json | 7 + packages/controller-utils/CHANGELOG.md | 9 + packages/controller-utils/LICENSE | 20 + packages/controller-utils/README.md | 15 + packages/controller-utils/jest.config.js | 28 + packages/controller-utils/package.json | 57 + .../controller-utils/src}/constants.ts | 5 +- packages/controller-utils/src/index.ts | 5 + packages/controller-utils/src/types.ts | 21 + packages/controller-utils/src/util.test.ts | 560 +++++++ packages/controller-utils/src/util.ts | 528 +++++++ packages/controller-utils/tsconfig.build.json | 15 + packages/controller-utils/tsconfig.json | 13 + packages/controller-utils/typedoc.json | 7 + packages/ens-controller/CHANGELOG.md | 9 + packages/ens-controller/LICENSE | 20 + packages/ens-controller/README.md | 15 + packages/ens-controller/jest.config.js | 25 + packages/ens-controller/package.json | 50 + .../ens-controller/src}/EnsController.test.ts | 2 +- .../ens-controller/src}/EnsController.ts | 8 +- packages/ens-controller/src/index.ts | 1 + packages/ens-controller/tsconfig.build.json | 13 + packages/ens-controller/tsconfig.json | 11 + packages/ens-controller/typedoc.json | 7 + packages/gas-fee-controller/CHANGELOG.md | 9 + packages/gas-fee-controller/LICENSE | 20 + packages/gas-fee-controller/README.md | 15 + packages/gas-fee-controller/jest.config.js | 25 + packages/gas-fee-controller/package.json | 62 + .../src}/GasFeeController.test.ts | 18 +- .../src}/GasFeeController.ts | 15 +- .../src}/determineGasFeeCalculations.test.ts | 34 +- .../src}/determineGasFeeCalculations.ts | 0 .../src}/fetchBlockFeeHistory.test.ts | 72 +- .../src}/fetchBlockFeeHistory.ts | 2 +- .../fetchGasEstimatesViaEthFeeHistory.test.ts | 20 +- .../src}/fetchGasEstimatesViaEthFeeHistory.ts | 2 +- ...teGasFeeEstimatesForPriorityLevels.test.ts | 0 ...lculateGasFeeEstimatesForPriorityLevels.ts | 2 +- .../fetchLatestBlock.ts | 2 +- .../medianOf.ts | 0 .../types.ts | 0 .../gas-fee-controller/src}/gas-util.test.ts | 0 .../gas-fee-controller/src}/gas-util.ts | 7 +- packages/gas-fee-controller/src/index.ts | 1 + .../gas-fee-controller/tsconfig.build.json | 14 + packages/gas-fee-controller/tsconfig.json | 12 + packages/gas-fee-controller/typedoc.json | 7 + packages/keyring-controller/CHANGELOG.md | 9 + packages/keyring-controller/LICENSE | 20 + packages/keyring-controller/README.md | 15 + packages/keyring-controller/jest.config.js | 25 + packages/keyring-controller/package.json | 63 + .../src}/KeyringController.test.ts | 19 +- .../src}/KeyringController.ts | 12 +- packages/keyring-controller/src/index.ts | 1 + .../tests}/mocks/mockEncryptor.ts | 0 .../keyring-controller/tsconfig.build.json | 23 + packages/keyring-controller/tsconfig.json | 21 + packages/keyring-controller/typedoc.json | 7 + packages/message-manager/CHANGELOG.md | 9 + packages/message-manager/LICENSE | 20 + packages/message-manager/README.md | 15 + packages/message-manager/jest.config.js | 25 + packages/message-manager/package.json | 55 + .../src}/AbstractMessageManager.test.ts | 0 .../src}/AbstractMessageManager.ts | 6 +- .../src}/MessageManager.test.ts | 0 .../message-manager/src}/MessageManager.ts | 2 +- .../src}/PersonalMessageManager.test.ts | 0 .../src}/PersonalMessageManager.ts | 2 +- .../src}/TypedMessageManager.test.ts | 0 .../src}/TypedMessageManager.ts | 2 +- packages/message-manager/src/index.ts | 3 + packages/message-manager/src/utils.test.ts | 188 +++ packages/message-manager/src/utils.ts | 118 ++ packages/message-manager/tsconfig.build.json | 13 + packages/message-manager/tsconfig.json | 11 + packages/message-manager/typedoc.json | 7 + packages/network-controller/CHANGELOG.md | 9 + packages/network-controller/LICENSE | 20 + packages/network-controller/README.md | 15 + packages/network-controller/jest.config.js | 29 + packages/network-controller/package.json | 57 + .../src}/NetworkController.test.ts | 7 +- .../src}/NetworkController.ts | 34 +- packages/network-controller/src/index.ts | 1 + .../network-controller/tsconfig.build.json | 13 + packages/network-controller/tsconfig.json | 11 + packages/network-controller/typedoc.json | 7 + packages/notification-controller/CHANGELOG.md | 9 + packages/notification-controller/LICENSE | 20 + packages/notification-controller/README.md | 15 + .../notification-controller/jest.config.js | 25 + packages/notification-controller/package.json | 52 + .../src}/NotificationController.test.ts | 2 +- .../src}/NotificationController.ts | 12 +- packages/notification-controller/src/index.ts | 1 + .../tsconfig.build.json | 13 + .../notification-controller/tsconfig.json | 11 + packages/notification-controller/typedoc.json | 7 + .../permission-controller/ARCHITECTURE.md | 2 +- packages/permission-controller/CHANGELOG.md | 9 + packages/permission-controller/LICENSE | 20 + packages/permission-controller/README.md | 19 + packages/permission-controller/jest.config.js | 25 + packages/permission-controller/package.json | 58 + .../permission-controller/src}/Caveat.test.ts | 0 .../permission-controller/src}/Caveat.ts | 2 +- .../src}/Permission.test.ts | 0 .../permission-controller/src}/Permission.ts | 2 +- .../src}/PermissionController.test.ts | 8 +- .../src}/PermissionController.ts | 19 +- .../permission-controller/src}/errors.test.ts | 0 .../permission-controller/src}/errors.ts | 0 .../permission-controller/src}/index.ts | 0 .../src}/permission-middleware.ts | 0 .../src}/rpc-methods/getPermissions.test.ts | 0 .../src}/rpc-methods/getPermissions.ts | 0 .../src}/rpc-methods/index.ts | 0 .../rpc-methods/requestPermissions.test.ts | 0 .../src}/rpc-methods/requestPermissions.ts | 3 +- .../permission-controller/src}/utils.ts | 0 .../permission-controller/tsconfig.build.json | 14 + packages/permission-controller/tsconfig.json | 12 + packages/permission-controller/typedoc.json | 7 + packages/phishing-controller/CHANGELOG.md | 9 + packages/phishing-controller/LICENSE | 20 + packages/phishing-controller/README.md | 15 + packages/phishing-controller/jest.config.js | 25 + packages/phishing-controller/package.json | 56 + .../src}/PhishingController.test.ts | 2 +- .../src}/PhishingController.ts | 8 +- packages/phishing-controller/src/index.ts | 3 + .../phishing-controller/tsconfig.build.json | 13 + packages/phishing-controller/tsconfig.json | 11 + packages/phishing-controller/typedoc.json | 7 + packages/preferences-controller/CHANGELOG.md | 9 + packages/preferences-controller/LICENSE | 20 + packages/preferences-controller/README.md | 15 + .../preferences-controller/jest.config.js | 25 + packages/preferences-controller/package.json | 50 + .../src}/PreferencesController.test.ts | 0 .../src}/PreferencesController.ts | 22 +- packages/preferences-controller/src/index.ts | 1 + .../tsconfig.build.json | 13 + packages/preferences-controller/tsconfig.json | 11 + packages/preferences-controller/typedoc.json | 7 + packages/rate-limit-controller/CHANGELOG.md | 9 + packages/rate-limit-controller/LICENSE | 20 + packages/rate-limit-controller/README.md | 15 + packages/rate-limit-controller/jest.config.js | 25 + packages/rate-limit-controller/package.json | 51 + .../src}/RateLimitController.test.ts | 2 +- .../src}/RateLimitController.ts | 8 +- packages/rate-limit-controller/src/index.ts | 1 + .../rate-limit-controller/tsconfig.build.json | 10 + packages/rate-limit-controller/tsconfig.json | 8 + packages/rate-limit-controller/typedoc.json | 7 + .../subject-metadata-controller/CHANGELOG.md | 9 + packages/subject-metadata-controller/LICENSE | 20 + .../subject-metadata-controller/README.md | 15 + .../jest.config.js | 25 + .../subject-metadata-controller/package.json | 52 + .../src}/SubjectMetadataController.test.ts | 5 +- .../src}/SubjectMetadataController.ts | 11 +- .../subject-metadata-controller/src}/index.ts | 0 .../tsconfig.build.json | 13 + .../subject-metadata-controller/tsconfig.json | 11 + .../subject-metadata-controller/typedoc.json | 7 + packages/transaction-controller/CHANGELOG.md | 9 + packages/transaction-controller/LICENSE | 20 + packages/transaction-controller/README.md | 15 + .../transaction-controller/jest.config.js | 28 + packages/transaction-controller/package.json | 64 + .../src}/TransactionController.test.ts | 67 +- .../src}/TransactionController.ts | 24 +- packages/transaction-controller/src/index.ts | 1 + .../src}/mocks/txsMock.ts | 0 .../transaction-controller/src/utils.test.ts | 284 ++++ packages/transaction-controller/src/utils.ts | 263 ++++ .../tsconfig.build.json | 14 + packages/transaction-controller/tsconfig.json | 12 + packages/transaction-controller/typedoc.json | 7 + release.config.json | 3 + scripts/validate-changelog.sh | 16 + src/approval/ApprovalController.test.js | 282 ---- src/assets/assetsUtil.test.ts | 125 -- src/assets/assetsUtil.ts | 93 -- src/index.ts | 43 - src/util.test.ts | 1333 ----------------- src/util.ts | 1053 ------------- tests/{setupTests.ts => setup.ts} | 0 tsconfig.build.json | 66 +- tsconfig.json | 67 +- tsconfig.packages.build.json | 9 + tsconfig.packages.json | 21 + yarn.lock | 952 +++++++----- 315 files changed, 8061 insertions(+), 4034 deletions(-) delete mode 100644 .github/workflows/create-release-pr.yml create mode 100644 .github/workflows/main.yml create mode 100644 .yarn/plugins/@yarnpkg/plugin-constraints.cjs create mode 100644 .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs rename CHANGELOG.md => CHANGELOG.old.md (100%) delete mode 100644 __mocks__/uuid.js create mode 100644 constraints.pro delete mode 100644 jest.config.js create mode 100644 jest.config.packages.js create mode 100644 packages/address-book-controller/CHANGELOG.md rename LICENSE => packages/address-book-controller/LICENSE (100%) create mode 100644 packages/address-book-controller/README.md create mode 100644 packages/address-book-controller/jest.config.js create mode 100644 packages/address-book-controller/package.json rename {src/user => packages/address-book-controller/src}/AddressBookController.test.ts (100%) rename {src/user => packages/address-book-controller/src}/AddressBookController.ts (96%) create mode 100644 packages/address-book-controller/src/index.ts create mode 100644 packages/address-book-controller/tsconfig.build.json create mode 100644 packages/address-book-controller/tsconfig.json rename typedoc.json => packages/address-book-controller/typedoc.json (62%) create mode 100644 packages/announcement-controller/CHANGELOG.md create mode 100644 packages/announcement-controller/LICENSE create mode 100644 packages/announcement-controller/README.md create mode 100644 packages/announcement-controller/jest.config.js create mode 100644 packages/announcement-controller/package.json rename {src/announcement => packages/announcement-controller/src}/AnnouncementController.test.ts (100%) rename {src/announcement => packages/announcement-controller/src}/AnnouncementController.ts (96%) create mode 100644 packages/announcement-controller/src/index.ts create mode 100644 packages/announcement-controller/tsconfig.build.json create mode 100644 packages/announcement-controller/tsconfig.json create mode 100644 packages/announcement-controller/typedoc.json create mode 100644 packages/approval-controller/CHANGELOG.md create mode 100644 packages/approval-controller/LICENSE create mode 100644 packages/approval-controller/README.md create mode 100644 packages/approval-controller/jest.config.js create mode 100644 packages/approval-controller/package.json rename {src/approval => packages/approval-controller/src}/ApprovalController.test.ts (76%) rename {src/approval => packages/approval-controller/src}/ApprovalController.ts (98%) rename {src/approval => packages/approval-controller/src}/errors.ts (100%) rename {src/approval => packages/approval-controller/src}/index.ts (100%) create mode 100644 packages/approval-controller/tsconfig.build.json create mode 100644 packages/approval-controller/tsconfig.json create mode 100644 packages/approval-controller/typedoc.json create mode 100644 packages/assets-controllers/CHANGELOG.md create mode 100644 packages/assets-controllers/LICENSE create mode 100644 packages/assets-controllers/README.md create mode 100644 packages/assets-controllers/jest.config.js create mode 100644 packages/assets-controllers/package.json rename {src/assets => packages/assets-controllers/src}/AccountTrackerController.test.ts (88%) rename {src/assets => packages/assets-controllers/src}/AccountTrackerController.ts (94%) rename {src/assets => packages/assets-controllers/src}/AssetsContractController.test.ts (97%) rename {src/assets => packages/assets-controllers/src}/AssetsContractController.ts (97%) rename {src/assets => packages/assets-controllers/src}/CurrencyRateController.test.ts (98%) rename {src/assets => packages/assets-controllers/src}/CurrencyRateController.ts (95%) rename {src/assets => packages/assets-controllers/src}/NftController.test.ts (99%) rename {src/assets => packages/assets-controllers/src}/NftController.ts (99%) rename {src/assets => packages/assets-controllers/src}/NftDetectionController.test.ts (98%) rename {src/assets => packages/assets-controllers/src}/NftDetectionController.ts (96%) rename {src/assets => packages/assets-controllers/src}/Standards/ERC20Standard.test.ts (98%) rename {src/assets => packages/assets-controllers/src}/Standards/ERC20Standard.ts (98%) rename {src/assets => packages/assets-controllers/src}/Standards/NftStandards/ERC1155/ERC1155Standard.test.ts (96%) rename {src/assets => packages/assets-controllers/src}/Standards/NftStandards/ERC1155/ERC1155Standard.ts (98%) rename {src/assets => packages/assets-controllers/src}/Standards/NftStandards/ERC721/ERC721Standard.test.ts (99%) rename {src/assets => packages/assets-controllers/src}/Standards/NftStandards/ERC721/ERC721Standard.ts (98%) rename {src/assets => packages/assets-controllers/src}/Standards/standards-types.ts (100%) rename {src/assets => packages/assets-controllers/src}/TokenBalancesController.test.ts (97%) rename {src/assets => packages/assets-controllers/src}/TokenBalancesController.ts (93%) rename {src/assets => packages/assets-controllers/src}/TokenDetectionController.test.ts (97%) rename {src/assets => packages/assets-controllers/src}/TokenDetectionController.ts (95%) rename {src/assets => packages/assets-controllers/src}/TokenListController.test.ts (96%) rename {src/assets => packages/assets-controllers/src}/TokenListController.ts (93%) rename {src/assets => packages/assets-controllers/src}/TokenRatesController.test.ts (98%) rename {src/assets => packages/assets-controllers/src}/TokenRatesController.ts (96%) rename {src/assets => packages/assets-controllers/src}/TokensController.test.ts (97%) rename {src/assets => packages/assets-controllers/src}/TokensController.ts (97%) create mode 100644 packages/assets-controllers/src/assetsUtil.test.ts create mode 100644 packages/assets-controllers/src/assetsUtil.ts rename {src/apis => packages/assets-controllers/src}/crypto-compare.test.ts (99%) rename {src/apis => packages/assets-controllers/src}/crypto-compare.ts (97%) create mode 100644 packages/assets-controllers/src/index.ts rename {src/apis => packages/assets-controllers/src}/token-service.test.ts (78%) rename {src/apis => packages/assets-controllers/src}/token-service.ts (97%) create mode 100644 packages/assets-controllers/tsconfig.build.json create mode 100644 packages/assets-controllers/tsconfig.json create mode 100644 packages/assets-controllers/typedoc.json create mode 100644 packages/base-controller/CHANGELOG.md create mode 100644 packages/base-controller/LICENSE create mode 100644 packages/base-controller/README.md create mode 100644 packages/base-controller/jest.config.js create mode 100644 packages/base-controller/package.json rename {src => packages/base-controller/src}/BaseController.test.ts (98%) rename {src => packages/base-controller/src}/BaseController.ts (100%) rename {src => packages/base-controller/src}/BaseControllerV2.test.ts (99%) rename {src => packages/base-controller/src}/BaseControllerV2.ts (100%) rename {src => packages/base-controller/src}/ControllerMessenger.test.ts (99%) rename {src => packages/base-controller/src}/ControllerMessenger.ts (100%) create mode 100644 packages/base-controller/src/index.ts create mode 100644 packages/base-controller/tsconfig.build.json create mode 100644 packages/base-controller/tsconfig.json create mode 100644 packages/base-controller/typedoc.json create mode 100644 packages/composable-controller/CHANGELOG.md create mode 100644 packages/composable-controller/LICENSE create mode 100644 packages/composable-controller/README.md create mode 100644 packages/composable-controller/jest.config.js create mode 100644 packages/composable-controller/package.json rename {src => packages/composable-controller/src}/ComposableController.test.ts (96%) rename {src => packages/composable-controller/src}/ComposableController.ts (95%) create mode 100644 packages/composable-controller/src/index.ts create mode 100644 packages/composable-controller/tsconfig.build.json create mode 100644 packages/composable-controller/tsconfig.json create mode 100644 packages/composable-controller/typedoc.json create mode 100644 packages/controller-utils/CHANGELOG.md create mode 100644 packages/controller-utils/LICENSE create mode 100644 packages/controller-utils/README.md create mode 100644 packages/controller-utils/jest.config.js create mode 100644 packages/controller-utils/package.json rename {src => packages/controller-utils/src}/constants.ts (90%) create mode 100644 packages/controller-utils/src/index.ts create mode 100644 packages/controller-utils/src/types.ts create mode 100644 packages/controller-utils/src/util.test.ts create mode 100644 packages/controller-utils/src/util.ts create mode 100644 packages/controller-utils/tsconfig.build.json create mode 100644 packages/controller-utils/tsconfig.json create mode 100644 packages/controller-utils/typedoc.json create mode 100644 packages/ens-controller/CHANGELOG.md create mode 100644 packages/ens-controller/LICENSE create mode 100644 packages/ens-controller/README.md create mode 100644 packages/ens-controller/jest.config.js create mode 100644 packages/ens-controller/package.json rename {src/third-party => packages/ens-controller/src}/EnsController.test.ts (99%) rename {src/third-party => packages/ens-controller/src}/EnsController.ts (97%) create mode 100644 packages/ens-controller/src/index.ts create mode 100644 packages/ens-controller/tsconfig.build.json create mode 100644 packages/ens-controller/tsconfig.json create mode 100644 packages/ens-controller/typedoc.json create mode 100644 packages/gas-fee-controller/CHANGELOG.md create mode 100644 packages/gas-fee-controller/LICENSE create mode 100644 packages/gas-fee-controller/README.md create mode 100644 packages/gas-fee-controller/jest.config.js create mode 100644 packages/gas-fee-controller/package.json rename {src/gas => packages/gas-fee-controller/src}/GasFeeController.test.ts (98%) rename {src/gas => packages/gas-fee-controller/src}/GasFeeController.ts (98%) rename {src/gas => packages/gas-fee-controller/src}/determineGasFeeCalculations.test.ts (93%) rename {src/gas => packages/gas-fee-controller/src}/determineGasFeeCalculations.ts (100%) rename {src/gas => packages/gas-fee-controller/src}/fetchBlockFeeHistory.test.ts (87%) rename {src/gas => packages/gas-fee-controller/src}/fetchBlockFeeHistory.ts (99%) rename {src/gas => packages/gas-fee-controller/src}/fetchGasEstimatesViaEthFeeHistory.test.ts (82%) rename {src/gas => packages/gas-fee-controller/src}/fetchGasEstimatesViaEthFeeHistory.ts (97%) rename {src/gas => packages/gas-fee-controller/src}/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.test.ts (100%) rename {src/gas => packages/gas-fee-controller/src}/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.ts (98%) rename {src/gas => packages/gas-fee-controller/src}/fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock.ts (92%) rename {src/gas => packages/gas-fee-controller/src}/fetchGasEstimatesViaEthFeeHistory/medianOf.ts (100%) rename {src/gas => packages/gas-fee-controller/src}/fetchGasEstimatesViaEthFeeHistory/types.ts (100%) rename {src/gas => packages/gas-fee-controller/src}/gas-util.test.ts (100%) rename {src/gas => packages/gas-fee-controller/src}/gas-util.ts (98%) create mode 100644 packages/gas-fee-controller/src/index.ts create mode 100644 packages/gas-fee-controller/tsconfig.build.json create mode 100644 packages/gas-fee-controller/tsconfig.json create mode 100644 packages/gas-fee-controller/typedoc.json create mode 100644 packages/keyring-controller/CHANGELOG.md create mode 100644 packages/keyring-controller/LICENSE create mode 100644 packages/keyring-controller/README.md create mode 100644 packages/keyring-controller/jest.config.js create mode 100644 packages/keyring-controller/package.json rename {src/keyring => packages/keyring-controller/src}/KeyringController.test.ts (98%) rename {src/keyring => packages/keyring-controller/src}/KeyringController.ts (98%) create mode 100644 packages/keyring-controller/src/index.ts rename {tests => packages/keyring-controller/tests}/mocks/mockEncryptor.ts (100%) create mode 100644 packages/keyring-controller/tsconfig.build.json create mode 100644 packages/keyring-controller/tsconfig.json create mode 100644 packages/keyring-controller/typedoc.json create mode 100644 packages/message-manager/CHANGELOG.md create mode 100644 packages/message-manager/LICENSE create mode 100644 packages/message-manager/README.md create mode 100644 packages/message-manager/jest.config.js create mode 100644 packages/message-manager/package.json rename {src/message-manager => packages/message-manager/src}/AbstractMessageManager.test.ts (100%) rename {src/message-manager => packages/message-manager/src}/AbstractMessageManager.ts (98%) rename {src/message-manager => packages/message-manager/src}/MessageManager.test.ts (100%) rename {src/message-manager => packages/message-manager/src}/MessageManager.ts (98%) rename {src/message-manager => packages/message-manager/src}/PersonalMessageManager.test.ts (100%) rename {src/message-manager => packages/message-manager/src}/PersonalMessageManager.ts (98%) rename {src/message-manager => packages/message-manager/src}/TypedMessageManager.test.ts (100%) rename {src/message-manager => packages/message-manager/src}/TypedMessageManager.ts (99%) create mode 100644 packages/message-manager/src/index.ts create mode 100644 packages/message-manager/src/utils.test.ts create mode 100644 packages/message-manager/src/utils.ts create mode 100644 packages/message-manager/tsconfig.build.json create mode 100644 packages/message-manager/tsconfig.json create mode 100644 packages/message-manager/typedoc.json create mode 100644 packages/network-controller/CHANGELOG.md create mode 100644 packages/network-controller/LICENSE create mode 100644 packages/network-controller/README.md create mode 100644 packages/network-controller/jest.config.js create mode 100644 packages/network-controller/package.json rename {src/network => packages/network-controller/src}/NetworkController.test.ts (97%) rename {src/network => packages/network-controller/src}/NetworkController.ts (95%) create mode 100644 packages/network-controller/src/index.ts create mode 100644 packages/network-controller/tsconfig.build.json create mode 100644 packages/network-controller/tsconfig.json create mode 100644 packages/network-controller/typedoc.json create mode 100644 packages/notification-controller/CHANGELOG.md create mode 100644 packages/notification-controller/LICENSE create mode 100644 packages/notification-controller/README.md create mode 100644 packages/notification-controller/jest.config.js create mode 100644 packages/notification-controller/package.json rename {src/notification => packages/notification-controller/src}/NotificationController.test.ts (98%) rename {src/notification => packages/notification-controller/src}/NotificationController.ts (95%) create mode 100644 packages/notification-controller/src/index.ts create mode 100644 packages/notification-controller/tsconfig.build.json create mode 100644 packages/notification-controller/tsconfig.json create mode 100644 packages/notification-controller/typedoc.json rename src/permissions/README.md => packages/permission-controller/ARCHITECTURE.md (99%) create mode 100644 packages/permission-controller/CHANGELOG.md create mode 100644 packages/permission-controller/LICENSE create mode 100644 packages/permission-controller/README.md create mode 100644 packages/permission-controller/jest.config.js create mode 100644 packages/permission-controller/package.json rename {src/permissions => packages/permission-controller/src}/Caveat.test.ts (100%) rename {src/permissions => packages/permission-controller/src}/Caveat.ts (99%) rename {src/permissions => packages/permission-controller/src}/Permission.test.ts (100%) rename {src/permissions => packages/permission-controller/src}/Permission.ts (99%) rename {src/permissions => packages/permission-controller/src}/PermissionController.test.ts (99%) rename {src/permissions => packages/permission-controller/src}/PermissionController.ts (99%) rename {src/permissions => packages/permission-controller/src}/errors.test.ts (100%) rename {src/permissions => packages/permission-controller/src}/errors.ts (100%) rename {src/permissions => packages/permission-controller/src}/index.ts (100%) rename {src/permissions => packages/permission-controller/src}/permission-middleware.ts (100%) rename {src/permissions => packages/permission-controller/src}/rpc-methods/getPermissions.test.ts (100%) rename {src/permissions => packages/permission-controller/src}/rpc-methods/getPermissions.ts (100%) rename {src/permissions => packages/permission-controller/src}/rpc-methods/index.ts (100%) rename {src/permissions => packages/permission-controller/src}/rpc-methods/requestPermissions.test.ts (100%) rename {src/permissions => packages/permission-controller/src}/rpc-methods/requestPermissions.ts (97%) rename {src/permissions => packages/permission-controller/src}/utils.ts (100%) create mode 100644 packages/permission-controller/tsconfig.build.json create mode 100644 packages/permission-controller/tsconfig.json create mode 100644 packages/permission-controller/typedoc.json create mode 100644 packages/phishing-controller/CHANGELOG.md create mode 100644 packages/phishing-controller/LICENSE create mode 100644 packages/phishing-controller/README.md create mode 100644 packages/phishing-controller/jest.config.js create mode 100644 packages/phishing-controller/package.json rename {src/third-party => packages/phishing-controller/src}/PhishingController.test.ts (99%) rename {src/third-party => packages/phishing-controller/src}/PhishingController.ts (98%) create mode 100644 packages/phishing-controller/src/index.ts create mode 100644 packages/phishing-controller/tsconfig.build.json create mode 100644 packages/phishing-controller/tsconfig.json create mode 100644 packages/phishing-controller/typedoc.json create mode 100644 packages/preferences-controller/CHANGELOG.md create mode 100644 packages/preferences-controller/LICENSE create mode 100644 packages/preferences-controller/README.md create mode 100644 packages/preferences-controller/jest.config.js create mode 100644 packages/preferences-controller/package.json rename {src/user => packages/preferences-controller/src}/PreferencesController.test.ts (100%) rename {src/user => packages/preferences-controller/src}/PreferencesController.ts (94%) create mode 100644 packages/preferences-controller/src/index.ts create mode 100644 packages/preferences-controller/tsconfig.build.json create mode 100644 packages/preferences-controller/tsconfig.json create mode 100644 packages/preferences-controller/typedoc.json create mode 100644 packages/rate-limit-controller/CHANGELOG.md create mode 100644 packages/rate-limit-controller/LICENSE create mode 100644 packages/rate-limit-controller/README.md create mode 100644 packages/rate-limit-controller/jest.config.js create mode 100644 packages/rate-limit-controller/package.json rename {src/ratelimit => packages/rate-limit-controller/src}/RateLimitController.test.ts (98%) rename {src/ratelimit => packages/rate-limit-controller/src}/RateLimitController.ts (97%) create mode 100644 packages/rate-limit-controller/src/index.ts create mode 100644 packages/rate-limit-controller/tsconfig.build.json create mode 100644 packages/rate-limit-controller/tsconfig.json create mode 100644 packages/rate-limit-controller/typedoc.json create mode 100644 packages/subject-metadata-controller/CHANGELOG.md create mode 100644 packages/subject-metadata-controller/LICENSE create mode 100644 packages/subject-metadata-controller/README.md create mode 100644 packages/subject-metadata-controller/jest.config.js create mode 100644 packages/subject-metadata-controller/package.json rename {src/subject-metadata => packages/subject-metadata-controller/src}/SubjectMetadataController.test.ts (98%) rename {src/subject-metadata => packages/subject-metadata-controller/src}/SubjectMetadataController.ts (97%) rename {src/subject-metadata => packages/subject-metadata-controller/src}/index.ts (100%) create mode 100644 packages/subject-metadata-controller/tsconfig.build.json create mode 100644 packages/subject-metadata-controller/tsconfig.json create mode 100644 packages/subject-metadata-controller/typedoc.json create mode 100644 packages/transaction-controller/CHANGELOG.md create mode 100644 packages/transaction-controller/LICENSE create mode 100644 packages/transaction-controller/README.md create mode 100644 packages/transaction-controller/jest.config.js create mode 100644 packages/transaction-controller/package.json rename {src/transaction => packages/transaction-controller/src}/TransactionController.test.ts (96%) rename {src/transaction => packages/transaction-controller/src}/TransactionController.ts (99%) create mode 100644 packages/transaction-controller/src/index.ts rename {src/transaction => packages/transaction-controller/src}/mocks/txsMock.ts (100%) create mode 100644 packages/transaction-controller/src/utils.test.ts create mode 100644 packages/transaction-controller/src/utils.ts create mode 100644 packages/transaction-controller/tsconfig.build.json create mode 100644 packages/transaction-controller/tsconfig.json create mode 100644 packages/transaction-controller/typedoc.json create mode 100644 release.config.json create mode 100755 scripts/validate-changelog.sh delete mode 100644 src/approval/ApprovalController.test.js delete mode 100644 src/assets/assetsUtil.test.ts delete mode 100644 src/assets/assetsUtil.ts delete mode 100644 src/index.ts delete mode 100644 src/util.test.ts delete mode 100644 src/util.ts rename tests/{setupTests.ts => setup.ts} (100%) create mode 100644 tsconfig.packages.build.json create mode 100644 tsconfig.packages.json diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml deleted file mode 100644 index 2308632ee6..0000000000 --- a/.github/workflows/create-release-pr.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Create Release Pull Request - -on: - workflow_dispatch: - inputs: - base-branch: - description: 'The base branch for git operations and the pull request.' - default: 'main' - required: true - release-type: - description: 'A SemVer version diff, i.e. major, minor, patch, prerelease etc. Mutually exclusive with "release-version".' - required: false - release-version: - description: 'A specific version to bump to. Mutually exclusive with "release-type".' - required: false - -jobs: - create-release-pr: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - uses: actions/checkout@v3 - with: - # This is to guarantee that the most recent tag is fetched. - # This can be configured to a more reasonable value by consumers. - fetch-depth: 0 - # We check out the specified branch, which will be used as the base - # branch for all git operations and the release PR. - ref: ${{ github.event.inputs.base-branch }} - - name: Get Node.js version - id: nvm - run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT - - uses: actions/setup-node@v3 - with: - node-version: ${{ steps.nvm.outputs.NODE_VERSION }} - - uses: MetaMask/action-create-release-pr@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - release-type: ${{ github.event.inputs.release-type }} - release-version: ${{ github.event.inputs.release-version }} diff --git a/.github/workflows/lint-build-test.yml b/.github/workflows/lint-build-test.yml index ac28fc25ed..e3724d352a 100644 --- a/.github/workflows/lint-build-test.yml +++ b/.github/workflows/lint-build-test.yml @@ -1,14 +1,12 @@ name: Lint, Build, and Test on: - push: - branches: [main] - pull_request: + workflow_call: jobs: - lint-build-test: - name: Lint, Build, and Test - runs-on: ubuntu-20.04 + prepare: + name: Prepare + runs-on: ubuntu-latest strategy: matrix: node-version: [14.x, 16.x] @@ -18,31 +16,78 @@ jobs: uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - - name: Get Yarn cache directory - run: echo "YARN_CACHE_DIR=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT - id: yarn-cache-dir - - name: Get Yarn version - run: echo "YARN_VERSION=$(yarn --version)" >> $GITHUB_OUTPUT - id: yarn-version - - name: Cache yarn dependencies - uses: actions/cache@v3 + cache: yarn + - run: yarn --immutable + + lint: + name: Lint + runs-on: ubuntu-latest + needs: prepare + strategy: + matrix: + node-version: [14.x, 16.x] + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 with: - path: ${{ steps.yarn-cache-dir.outputs.YARN_CACHE_DIR }} - key: yarn-cache-${{ runner.os }}-${{ steps.yarn-version.outputs.YARN_VERSION }}-${{ hashFiles('yarn.lock') }} + node-version: ${{ matrix.node-version }} + cache: yarn - run: yarn --immutable - run: yarn lint - - name: Validate RC changelog - if: ${{ startsWith(github.ref, 'release/') }} - run: yarn auto-changelog validate --rc - - name: Validate changelog - if: ${{ !startsWith(github.ref, 'release/') }} - run: yarn auto-changelog validate + - run: yarn changelog:validate + - name: Require clean working directory + shell: bash + run: | + if ! git diff --exit-code; then + echo "Working tree dirty at end of job" + exit 1 + fi + + build: + name: Build + runs-on: ubuntu-latest + needs: prepare + strategy: + matrix: + node-version: [14.x, 16.x] + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: yarn + - run: yarn --immutable - run: yarn build - - run: yarn test --maxWorkers=1 - all-jobs-pass: - name: All jobs pass - runs-on: ubuntu-20.04 - needs: - - lint-build-test + - name: Require clean working directory + shell: bash + run: | + if ! git diff --exit-code; then + echo "Working tree dirty at end of job" + exit 1 + fi + + test: + name: Test + runs-on: ubuntu-latest + needs: prepare + strategy: + matrix: + node-version: [14.x, 16.x] steps: - - run: echo "Great success!" + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: yarn + - run: yarn --immutable + - run: yarn test + - name: Require clean working directory + shell: bash + run: | + if ! git diff --exit-code; then + echo "Working tree dirty at end of job" + exit 1 + fi diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..01d15bdb6e --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,51 @@ +name: Main + +on: + push: + branches: [main] + pull_request: + +jobs: + check-workflows: + name: Check workflows + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Download actionlint + id: download-actionlint + run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/7fdc9630cc360ea1a469eed64ac6d78caeda1234/scripts/download-actionlint.bash) 1.6.22 + shell: bash + - name: Check workflow files + run: ${{ steps.download-actionlint.outputs.executable }} -color + shell: bash + + lint-build-test: + name: Lint, build, and test + needs: check-workflows + uses: ./.github/workflows/lint-build-test.yml + + is-release: + name: Determine whether this is a release merge commit + needs: lint-build-test + if: startsWith(github.event.commits[0].author.name, 'github-actions') + runs-on: ubuntu-latest + outputs: + IS_RELEASE: ${{ steps.is-release.outputs.IS_RELEASE }} + steps: + - uses: MetaMask/action-is-release@v1 + id: is-release + + publish-release: + name: Publish release + needs: is-release + if: needs.is-release.outputs.IS_RELEASE == 'true' + permissions: + contents: write + uses: ./.github/workflows/publish-release.yml + + all-jobs-pass: + name: All jobs pass + runs-on: ubuntu-latest + needs: lint-build-test + steps: + - run: echo "Great success!" diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 15b114687e..4dad7bc993 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,58 +1,33 @@ name: Publish Release on: - push: - branches: [main] + workflow_call: jobs: - is-release: - # release merge commits come from github-actions - if: startsWith(github.event.commits[0].author.name, 'github-actions') - outputs: - IS_RELEASE: ${{ steps.is-release.outputs.IS_RELEASE }} - runs-on: ubuntu-latest - steps: - - uses: MetaMask/action-is-release@v1 - id: is-release - publish-release: permissions: contents: write - if: needs.is-release.outputs.IS_RELEASE == 'true' runs-on: ubuntu-latest - needs: is-release steps: - uses: actions/checkout@v3 with: ref: ${{ github.sha }} - - name: Get Node.js version - id: nvm - run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT - name: Setup Node uses: actions/setup-node@v3 with: - node-version: ${{ steps.nvm.outputs.NODE_VERSION }} + node-version-file: '.nvmrc' + cache: yarn + - uses: actions/cache@v3 + with: + path: | + ./packages/**/dist + ./node_modules/.yarn-state.yml + key: ${{ github.sha }} - uses: MetaMask/action-publish-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Get Yarn cache directory - run: echo "YARN_CACHE_DIR=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT - id: yarn-cache-dir - - name: Get Yarn version - run: echo "YARN_VERSION=$(yarn --version)" >> $GITHUB_OUTPUT - id: yarn-version - - name: Cache yarn dependencies - uses: actions/cache@v3 - with: - path: ${{ steps.yarn-cache-dir.outputs.YARN_CACHE_DIR }} - key: yarn-cache-${{ runner.os }}-${{ steps.yarn-version.outputs.YARN_VERSION }}-${{ hashFiles('yarn.lock') }} - run: yarn --immutable - run: yarn build - - uses: actions/cache@v3 - id: restore-build - with: - path: ./dist - key: ${{ github.sha }} publish-npm-dry-run: runs-on: ubuntu-latest @@ -62,15 +37,14 @@ jobs: with: ref: ${{ github.sha }} - uses: actions/cache@v3 - id: restore-build with: - path: ./dist + path: | + ./packages/**/dist + ./node_modules/.yarn-state.yml key: ${{ github.sha }} - # Set `ignore-scripts` to skip `prepublishOnly` because the release was built already in the previous job - - run: npm config set ignore-scripts true - name: Dry Run Publish # omit npm-token token to perform dry run publish - uses: MetaMask/action-npm-publish@v1 + uses: MetaMask/action-npm-publish@v2 env: SKIP_PREPACK: true @@ -83,14 +57,13 @@ jobs: with: ref: ${{ github.sha }} - uses: actions/cache@v3 - id: restore-build with: - path: ./dist + path: | + ./packages/**/dist + ./node_modules/.yarn-state.yml key: ${{ github.sha }} - # Set `ignore-scripts` to skip `prepublishOnly` because the release was built already in the previous job - - run: npm config set ignore-scripts true - name: Publish - uses: MetaMask/action-npm-publish@v1 + uses: MetaMask/action-npm-publish@v2 with: npm-token: ${{ secrets.NPM_TOKEN }} env: diff --git a/.gitignore b/.gitignore index 654ab7b4c5..e66d256a96 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ docs !.yarn/releases !.yarn/sdks !.yarn/versions + +# typescript +*.tsbuildinfo diff --git a/.yarn/plugins/@yarnpkg/plugin-constraints.cjs b/.yarn/plugins/@yarnpkg/plugin-constraints.cjs new file mode 100644 index 0000000000..f3b0db0c02 --- /dev/null +++ b/.yarn/plugins/@yarnpkg/plugin-constraints.cjs @@ -0,0 +1,52 @@ +/* eslint-disable */ +//prettier-ignore +module.exports = { +name: "@yarnpkg/plugin-constraints", +factory: function (require) { +var plugin=(()=>{var Li=Object.create,Je=Object.defineProperty;var Hi=Object.getOwnPropertyDescriptor;var Gi=Object.getOwnPropertyNames;var Yi=Object.getPrototypeOf,Ui=Object.prototype.hasOwnProperty;var Zi=r=>Je(r,"__esModule",{value:!0});var I=(r,u)=>()=>(u||r((u={exports:{}}).exports,u),u.exports),Qi=(r,u)=>{for(var p in u)Je(r,p,{get:u[p],enumerable:!0})},Ji=(r,u,p)=>{if(u&&typeof u=="object"||typeof u=="function")for(let c of Gi(u))!Ui.call(r,c)&&c!=="default"&&Je(r,c,{get:()=>u[c],enumerable:!(p=Hi(u,c))||p.enumerable});return r},G=r=>Ji(Zi(Je(r!=null?Li(Yi(r)):{},"default",r&&r.__esModule&&"default"in r?{get:()=>r.default,enumerable:!0}:{value:r,enumerable:!0})),r);var Xr=I((Nu,_r)=>{var Ki;(function(r){var u=function(){return{"append/2":[new r.type.Rule(new r.type.Term("append",[new r.type.Var("X"),new r.type.Var("L")]),new r.type.Term("foldl",[new r.type.Term("append",[]),new r.type.Var("X"),new r.type.Term("[]",[]),new r.type.Var("L")]))],"append/3":[new r.type.Rule(new r.type.Term("append",[new r.type.Term("[]",[]),new r.type.Var("X"),new r.type.Var("X")]),null),new r.type.Rule(new r.type.Term("append",[new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("T")]),new r.type.Var("X"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("S")])]),new r.type.Term("append",[new r.type.Var("T"),new r.type.Var("X"),new r.type.Var("S")]))],"member/2":[new r.type.Rule(new r.type.Term("member",[new r.type.Var("X"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("_")])]),null),new r.type.Rule(new r.type.Term("member",[new r.type.Var("X"),new r.type.Term(".",[new r.type.Var("_"),new r.type.Var("Xs")])]),new r.type.Term("member",[new r.type.Var("X"),new r.type.Var("Xs")]))],"permutation/2":[new r.type.Rule(new r.type.Term("permutation",[new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("permutation",[new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("T")]),new r.type.Var("S")]),new r.type.Term(",",[new r.type.Term("permutation",[new r.type.Var("T"),new r.type.Var("P")]),new r.type.Term(",",[new r.type.Term("append",[new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("P")]),new r.type.Term("append",[new r.type.Var("X"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("Y")]),new r.type.Var("S")])])]))],"maplist/2":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("X")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("Xs")])]))],"maplist/3":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("A"),new r.type.Var("As")]),new r.type.Term(".",[new r.type.Var("B"),new r.type.Var("Bs")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("A"),new r.type.Var("B")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("As"),new r.type.Var("Bs")])]))],"maplist/4":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("A"),new r.type.Var("As")]),new r.type.Term(".",[new r.type.Var("B"),new r.type.Var("Bs")]),new r.type.Term(".",[new r.type.Var("C"),new r.type.Var("Cs")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("A"),new r.type.Var("B"),new r.type.Var("C")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("As"),new r.type.Var("Bs"),new r.type.Var("Cs")])]))],"maplist/5":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("A"),new r.type.Var("As")]),new r.type.Term(".",[new r.type.Var("B"),new r.type.Var("Bs")]),new r.type.Term(".",[new r.type.Var("C"),new r.type.Var("Cs")]),new r.type.Term(".",[new r.type.Var("D"),new r.type.Var("Ds")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("A"),new r.type.Var("B"),new r.type.Var("C"),new r.type.Var("D")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("As"),new r.type.Var("Bs"),new r.type.Var("Cs"),new r.type.Var("Ds")])]))],"maplist/6":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("A"),new r.type.Var("As")]),new r.type.Term(".",[new r.type.Var("B"),new r.type.Var("Bs")]),new r.type.Term(".",[new r.type.Var("C"),new r.type.Var("Cs")]),new r.type.Term(".",[new r.type.Var("D"),new r.type.Var("Ds")]),new r.type.Term(".",[new r.type.Var("E"),new r.type.Var("Es")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("A"),new r.type.Var("B"),new r.type.Var("C"),new r.type.Var("D"),new r.type.Var("E")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("As"),new r.type.Var("Bs"),new r.type.Var("Cs"),new r.type.Var("Ds"),new r.type.Var("Es")])]))],"maplist/7":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("A"),new r.type.Var("As")]),new r.type.Term(".",[new r.type.Var("B"),new r.type.Var("Bs")]),new r.type.Term(".",[new r.type.Var("C"),new r.type.Var("Cs")]),new r.type.Term(".",[new r.type.Var("D"),new r.type.Var("Ds")]),new r.type.Term(".",[new r.type.Var("E"),new r.type.Var("Es")]),new r.type.Term(".",[new r.type.Var("F"),new r.type.Var("Fs")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("A"),new r.type.Var("B"),new r.type.Var("C"),new r.type.Var("D"),new r.type.Var("E"),new r.type.Var("F")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("As"),new r.type.Var("Bs"),new r.type.Var("Cs"),new r.type.Var("Ds"),new r.type.Var("Es"),new r.type.Var("Fs")])]))],"maplist/8":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("A"),new r.type.Var("As")]),new r.type.Term(".",[new r.type.Var("B"),new r.type.Var("Bs")]),new r.type.Term(".",[new r.type.Var("C"),new r.type.Var("Cs")]),new r.type.Term(".",[new r.type.Var("D"),new r.type.Var("Ds")]),new r.type.Term(".",[new r.type.Var("E"),new r.type.Var("Es")]),new r.type.Term(".",[new r.type.Var("F"),new r.type.Var("Fs")]),new r.type.Term(".",[new r.type.Var("G"),new r.type.Var("Gs")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("A"),new r.type.Var("B"),new r.type.Var("C"),new r.type.Var("D"),new r.type.Var("E"),new r.type.Var("F"),new r.type.Var("G")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("As"),new r.type.Var("Bs"),new r.type.Var("Cs"),new r.type.Var("Ds"),new r.type.Var("Es"),new r.type.Var("Fs"),new r.type.Var("Gs")])]))],"include/3":[new r.type.Rule(new r.type.Term("include",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("include",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("T")]),new r.type.Var("L")]),new r.type.Term(",",[new r.type.Term("=..",[new r.type.Var("P"),new r.type.Var("A")]),new r.type.Term(",",[new r.type.Term("append",[new r.type.Var("A"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Term("[]",[])]),new r.type.Var("B")]),new r.type.Term(",",[new r.type.Term("=..",[new r.type.Var("F"),new r.type.Var("B")]),new r.type.Term(",",[new r.type.Term(";",[new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("F")]),new r.type.Term(",",[new r.type.Term("=",[new r.type.Var("L"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("S")])]),new r.type.Term("!",[])])]),new r.type.Term("=",[new r.type.Var("L"),new r.type.Var("S")])]),new r.type.Term("include",[new r.type.Var("P"),new r.type.Var("T"),new r.type.Var("S")])])])])]))],"exclude/3":[new r.type.Rule(new r.type.Term("exclude",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("exclude",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("T")]),new r.type.Var("S")]),new r.type.Term(",",[new r.type.Term("exclude",[new r.type.Var("P"),new r.type.Var("T"),new r.type.Var("E")]),new r.type.Term(",",[new r.type.Term("=..",[new r.type.Var("P"),new r.type.Var("L")]),new r.type.Term(",",[new r.type.Term("append",[new r.type.Var("L"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Term("[]",[])]),new r.type.Var("Q")]),new r.type.Term(",",[new r.type.Term("=..",[new r.type.Var("R"),new r.type.Var("Q")]),new r.type.Term(";",[new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("R")]),new r.type.Term(",",[new r.type.Term("!",[]),new r.type.Term("=",[new r.type.Var("S"),new r.type.Var("E")])])]),new r.type.Term("=",[new r.type.Var("S"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("E")])])])])])])]))],"foldl/4":[new r.type.Rule(new r.type.Term("foldl",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Var("I"),new r.type.Var("I")]),null),new r.type.Rule(new r.type.Term("foldl",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("T")]),new r.type.Var("I"),new r.type.Var("R")]),new r.type.Term(",",[new r.type.Term("=..",[new r.type.Var("P"),new r.type.Var("L")]),new r.type.Term(",",[new r.type.Term("append",[new r.type.Var("L"),new r.type.Term(".",[new r.type.Var("I"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Term("[]",[])])])]),new r.type.Var("L2")]),new r.type.Term(",",[new r.type.Term("=..",[new r.type.Var("P2"),new r.type.Var("L2")]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P2")]),new r.type.Term("foldl",[new r.type.Var("P"),new r.type.Var("T"),new r.type.Var("X"),new r.type.Var("R")])])])])]))],"select/3":[new r.type.Rule(new r.type.Term("select",[new r.type.Var("E"),new r.type.Term(".",[new r.type.Var("E"),new r.type.Var("Xs")]),new r.type.Var("Xs")]),null),new r.type.Rule(new r.type.Term("select",[new r.type.Var("E"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Ys")])]),new r.type.Term("select",[new r.type.Var("E"),new r.type.Var("Xs"),new r.type.Var("Ys")]))],"sum_list/2":[new r.type.Rule(new r.type.Term("sum_list",[new r.type.Term("[]",[]),new r.type.Num(0,!1)]),null),new r.type.Rule(new r.type.Term("sum_list",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Var("S")]),new r.type.Term(",",[new r.type.Term("sum_list",[new r.type.Var("Xs"),new r.type.Var("Y")]),new r.type.Term("is",[new r.type.Var("S"),new r.type.Term("+",[new r.type.Var("X"),new r.type.Var("Y")])])]))],"max_list/2":[new r.type.Rule(new r.type.Term("max_list",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Term("[]",[])]),new r.type.Var("X")]),null),new r.type.Rule(new r.type.Term("max_list",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Var("S")]),new r.type.Term(",",[new r.type.Term("max_list",[new r.type.Var("Xs"),new r.type.Var("Y")]),new r.type.Term(";",[new r.type.Term(",",[new r.type.Term(">=",[new r.type.Var("X"),new r.type.Var("Y")]),new r.type.Term(",",[new r.type.Term("=",[new r.type.Var("S"),new r.type.Var("X")]),new r.type.Term("!",[])])]),new r.type.Term("=",[new r.type.Var("S"),new r.type.Var("Y")])])]))],"min_list/2":[new r.type.Rule(new r.type.Term("min_list",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Term("[]",[])]),new r.type.Var("X")]),null),new r.type.Rule(new r.type.Term("min_list",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Var("S")]),new r.type.Term(",",[new r.type.Term("min_list",[new r.type.Var("Xs"),new r.type.Var("Y")]),new r.type.Term(";",[new r.type.Term(",",[new r.type.Term("=<",[new r.type.Var("X"),new r.type.Var("Y")]),new r.type.Term(",",[new r.type.Term("=",[new r.type.Var("S"),new r.type.Var("X")]),new r.type.Term("!",[])])]),new r.type.Term("=",[new r.type.Var("S"),new r.type.Var("Y")])])]))],"prod_list/2":[new r.type.Rule(new r.type.Term("prod_list",[new r.type.Term("[]",[]),new r.type.Num(1,!1)]),null),new r.type.Rule(new r.type.Term("prod_list",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Var("S")]),new r.type.Term(",",[new r.type.Term("prod_list",[new r.type.Var("Xs"),new r.type.Var("Y")]),new r.type.Term("is",[new r.type.Var("S"),new r.type.Term("*",[new r.type.Var("X"),new r.type.Var("Y")])])]))],"last/2":[new r.type.Rule(new r.type.Term("last",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Term("[]",[])]),new r.type.Var("X")]),null),new r.type.Rule(new r.type.Term("last",[new r.type.Term(".",[new r.type.Var("_"),new r.type.Var("Xs")]),new r.type.Var("X")]),new r.type.Term("last",[new r.type.Var("Xs"),new r.type.Var("X")]))],"prefix/2":[new r.type.Rule(new r.type.Term("prefix",[new r.type.Var("Part"),new r.type.Var("Whole")]),new r.type.Term("append",[new r.type.Var("Part"),new r.type.Var("_"),new r.type.Var("Whole")]))],"nth0/3":[new r.type.Rule(new r.type.Term("nth0",[new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z")]),new r.type.Term(";",[new r.type.Term("->",[new r.type.Term("var",[new r.type.Var("X")]),new r.type.Term("nth",[new r.type.Num(0,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("_")])]),new r.type.Term(",",[new r.type.Term(">=",[new r.type.Var("X"),new r.type.Num(0,!1)]),new r.type.Term(",",[new r.type.Term("nth",[new r.type.Num(0,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("_")]),new r.type.Term("!",[])])])]))],"nth1/3":[new r.type.Rule(new r.type.Term("nth1",[new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z")]),new r.type.Term(";",[new r.type.Term("->",[new r.type.Term("var",[new r.type.Var("X")]),new r.type.Term("nth",[new r.type.Num(1,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("_")])]),new r.type.Term(",",[new r.type.Term(">",[new r.type.Var("X"),new r.type.Num(0,!1)]),new r.type.Term(",",[new r.type.Term("nth",[new r.type.Num(1,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("_")]),new r.type.Term("!",[])])])]))],"nth0/4":[new r.type.Rule(new r.type.Term("nth0",[new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("W")]),new r.type.Term(";",[new r.type.Term("->",[new r.type.Term("var",[new r.type.Var("X")]),new r.type.Term("nth",[new r.type.Num(0,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("W")])]),new r.type.Term(",",[new r.type.Term(">=",[new r.type.Var("X"),new r.type.Num(0,!1)]),new r.type.Term(",",[new r.type.Term("nth",[new r.type.Num(0,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("W")]),new r.type.Term("!",[])])])]))],"nth1/4":[new r.type.Rule(new r.type.Term("nth1",[new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("W")]),new r.type.Term(";",[new r.type.Term("->",[new r.type.Term("var",[new r.type.Var("X")]),new r.type.Term("nth",[new r.type.Num(1,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("W")])]),new r.type.Term(",",[new r.type.Term(">",[new r.type.Var("X"),new r.type.Num(0,!1)]),new r.type.Term(",",[new r.type.Term("nth",[new r.type.Num(1,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("W")]),new r.type.Term("!",[])])])]))],"nth/5":[new r.type.Rule(new r.type.Term("nth",[new r.type.Var("N"),new r.type.Var("N"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Var("X"),new r.type.Var("Xs")]),null),new r.type.Rule(new r.type.Term("nth",[new r.type.Var("N"),new r.type.Var("O"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Var("Y"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Ys")])]),new r.type.Term(",",[new r.type.Term("is",[new r.type.Var("M"),new r.type.Term("+",[new r.type.Var("N"),new r.type.Num(1,!1)])]),new r.type.Term("nth",[new r.type.Var("M"),new r.type.Var("O"),new r.type.Var("Xs"),new r.type.Var("Y"),new r.type.Var("Ys")])]))],"length/2":function(c,w,_){var v=_.args[0],g=_.args[1];if(!r.type.is_variable(g)&&!r.type.is_integer(g))c.throw_error(r.error.type("integer",g,_.indicator));else if(r.type.is_integer(g)&&g.value<0)c.throw_error(r.error.domain("not_less_than_zero",g,_.indicator));else{var h=new r.type.Term("length",[v,new r.type.Num(0,!1),g]);r.type.is_integer(g)&&(h=new r.type.Term(",",[h,new r.type.Term("!",[])])),c.prepend([new r.type.State(w.goal.replace(h),w.substitution,w)])}},"length/3":[new r.type.Rule(new r.type.Term("length",[new r.type.Term("[]",[]),new r.type.Var("N"),new r.type.Var("N")]),null),new r.type.Rule(new r.type.Term("length",[new r.type.Term(".",[new r.type.Var("_"),new r.type.Var("X")]),new r.type.Var("A"),new r.type.Var("N")]),new r.type.Term(",",[new r.type.Term("succ",[new r.type.Var("A"),new r.type.Var("B")]),new r.type.Term("length",[new r.type.Var("X"),new r.type.Var("B"),new r.type.Var("N")])]))],"replicate/3":function(c,w,_){var v=_.args[0],g=_.args[1],h=_.args[2];if(r.type.is_variable(g))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_integer(g))c.throw_error(r.error.type("integer",g,_.indicator));else if(g.value<0)c.throw_error(r.error.domain("not_less_than_zero",g,_.indicator));else if(!r.type.is_variable(h)&&!r.type.is_list(h))c.throw_error(r.error.type("list",h,_.indicator));else{for(var x=new r.type.Term("[]"),T=0;T0;b--)T[b].equals(T[b-1])&&T.splice(b,1);for(var C=new r.type.Term("[]"),b=T.length-1;b>=0;b--)C=new r.type.Term(".",[T[b],C]);c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[C,g])),w.substitution,w)])}}},"msort/2":function(c,w,_){var v=_.args[0],g=_.args[1];if(r.type.is_variable(v))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_variable(g)&&!r.type.is_fully_list(g))c.throw_error(r.error.type("list",g,_.indicator));else{for(var h=[],x=v;x.indicator==="./2";)h.push(x.args[0]),x=x.args[1];if(r.type.is_variable(x))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_empty_list(x))c.throw_error(r.error.type("list",v,_.indicator));else{for(var T=h.sort(r.compare),b=new r.type.Term("[]"),C=T.length-1;C>=0;C--)b=new r.type.Term(".",[T[C],b]);c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[b,g])),w.substitution,w)])}}},"keysort/2":function(c,w,_){var v=_.args[0],g=_.args[1];if(r.type.is_variable(v))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_variable(g)&&!r.type.is_fully_list(g))c.throw_error(r.error.type("list",g,_.indicator));else{for(var h=[],x,T=v;T.indicator==="./2";){if(x=T.args[0],r.type.is_variable(x)){c.throw_error(r.error.instantiation(_.indicator));return}else if(!r.type.is_term(x)||x.indicator!=="-/2"){c.throw_error(r.error.type("pair",x,_.indicator));return}x.args[0].pair=x.args[1],h.push(x.args[0]),T=T.args[1]}if(r.type.is_variable(T))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_empty_list(T))c.throw_error(r.error.type("list",v,_.indicator));else{for(var b=h.sort(r.compare),C=new r.type.Term("[]"),N=b.length-1;N>=0;N--)C=new r.type.Term(".",[new r.type.Term("-",[b[N],b[N].pair]),C]),delete b[N].pair;c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[C,g])),w.substitution,w)])}}},"take/3":function(c,w,_){var v=_.args[0],g=_.args[1],h=_.args[2];if(r.type.is_variable(g)||r.type.is_variable(v))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_list(g))c.throw_error(r.error.type("list",g,_.indicator));else if(!r.type.is_integer(v))c.throw_error(r.error.type("integer",v,_.indicator));else if(!r.type.is_variable(h)&&!r.type.is_list(h))c.throw_error(r.error.type("list",h,_.indicator));else{for(var x=v.value,T=[],b=g;x>0&&b.indicator==="./2";)T.push(b.args[0]),b=b.args[1],x--;if(x===0){for(var C=new r.type.Term("[]"),x=T.length-1;x>=0;x--)C=new r.type.Term(".",[T[x],C]);c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[C,h])),w.substitution,w)])}}},"drop/3":function(c,w,_){var v=_.args[0],g=_.args[1],h=_.args[2];if(r.type.is_variable(g)||r.type.is_variable(v))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_list(g))c.throw_error(r.error.type("list",g,_.indicator));else if(!r.type.is_integer(v))c.throw_error(r.error.type("integer",v,_.indicator));else if(!r.type.is_variable(h)&&!r.type.is_list(h))c.throw_error(r.error.type("list",h,_.indicator));else{for(var x=v.value,T=[],b=g;x>0&&b.indicator==="./2";)T.push(b.args[0]),b=b.args[1],x--;x===0&&c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[b,h])),w.substitution,w)])}},"reverse/2":function(c,w,_){var v=_.args[0],g=_.args[1],h=r.type.is_instantiated_list(v),x=r.type.is_instantiated_list(g);if(r.type.is_variable(v)&&r.type.is_variable(g))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_variable(v)&&!r.type.is_fully_list(v))c.throw_error(r.error.type("list",v,_.indicator));else if(!r.type.is_variable(g)&&!r.type.is_fully_list(g))c.throw_error(r.error.type("list",g,_.indicator));else if(!h&&!x)c.throw_error(r.error.instantiation(_.indicator));else{for(var T=h?v:g,b=new r.type.Term("[]",[]);T.indicator==="./2";)b=new r.type.Term(".",[T.args[0],b]),T=T.args[1];c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[b,h?g:v])),w.substitution,w)])}},"list_to_set/2":function(c,w,_){var v=_.args[0],g=_.args[1];if(r.type.is_variable(v))c.throw_error(r.error.instantiation(_.indicator));else{for(var h=v,x=[];h.indicator==="./2";)x.push(h.args[0]),h=h.args[1];if(r.type.is_variable(h))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_term(h)||h.indicator!=="[]/0")c.throw_error(r.error.type("list",v,_.indicator));else{for(var T=[],b=new r.type.Term("[]",[]),C,N=0;N=0;N--)b=new r.type.Term(".",[T[N],b]);c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[g,b])),w.substitution,w)])}}}}},p=["append/2","append/3","member/2","permutation/2","maplist/2","maplist/3","maplist/4","maplist/5","maplist/6","maplist/7","maplist/8","include/3","exclude/3","foldl/4","sum_list/2","max_list/2","min_list/2","prod_list/2","last/2","prefix/2","nth0/3","nth1/3","nth0/4","nth1/4","length/2","replicate/3","select/3","sort/2","msort/2","keysort/2","take/3","drop/3","reverse/2","list_to_set/2"];typeof _r!="undefined"?_r.exports=function(c){r=c,new r.type.Module("lists",u(),p)}:new r.type.Module("lists",u(),p)})(Ki)});var et=I(M=>{"use strict";var Ve=process.platform==="win32",wr="aes-256-cbc",ji="sha256",Br="The current environment doesn't support interactive reading from TTY.",z=require("fs"),Fr=process.binding("tty_wrap").TTY,gr=require("child_process"),_e=require("path"),dr={prompt:"> ",hideEchoBack:!1,mask:"*",limit:[],limitMessage:"Input another, please.$<( [)limit(])>",defaultInput:"",trueValue:[],falseValue:[],caseSensitive:!1,keepWhitespace:!1,encoding:"utf8",bufferSize:1024,print:void 0,history:!0,cd:!1,phContent:void 0,preCheck:void 0},fe="none",oe,Ce,zr=!1,we,Ke,vr,es=0,hr="",Se=[],je,Wr=!1,mr=!1,$e=!1;function Lr(r){function u(p){return p.replace(/[^\w\u0080-\uFFFF]/g,function(c){return"#"+c.charCodeAt(0)+";"})}return Ke.concat(function(p){var c=[];return Object.keys(p).forEach(function(w){p[w]==="boolean"?r[w]&&c.push("--"+w):p[w]==="string"&&r[w]&&c.push("--"+w,u(r[w]))}),c}({display:"string",displayOnly:"boolean",keyIn:"boolean",hideEchoBack:"boolean",mask:"string",limit:"string",caseSensitive:"boolean"}))}function rs(r,u){function p(j){var U,Ue="",Ze;for(vr=vr||require("os").tmpdir();;){U=_e.join(vr,j+Ue);try{Ze=z.openSync(U,"wx")}catch(Qe){if(Qe.code==="EEXIST"){Ue++;continue}else throw Qe}z.closeSync(Ze);break}return U}var c,w,_,v={},g,h,x=p("readline-sync.stdout"),T=p("readline-sync.stderr"),b=p("readline-sync.exit"),C=p("readline-sync.done"),N=require("crypto"),W,ee,te;W=N.createHash(ji),W.update(""+process.pid+es+++Math.random()),te=W.digest("hex"),ee=N.createDecipher(wr,te),c=Lr(r),Ve?(w=process.env.ComSpec||"cmd.exe",process.env.Q='"',_=["/V:ON","/S","/C","(%Q%"+w+"%Q% /V:ON /S /C %Q%%Q%"+we+"%Q%"+c.map(function(j){return" %Q%"+j+"%Q%"}).join("")+" & (echo !ERRORLEVEL!)>%Q%"+b+"%Q%%Q%) 2>%Q%"+T+"%Q% |%Q%"+process.execPath+"%Q% %Q%"+__dirname+"\\encrypt.js%Q% %Q%"+wr+"%Q% %Q%"+te+"%Q% >%Q%"+x+"%Q% & (echo 1)>%Q%"+C+"%Q%"]):(w="/bin/sh",_=["-c",'("'+we+'"'+c.map(function(j){return" '"+j.replace(/'/g,"'\\''")+"'"}).join("")+'; echo $?>"'+b+'") 2>"'+T+'" |"'+process.execPath+'" "'+__dirname+'/encrypt.js" "'+wr+'" "'+te+'" >"'+x+'"; echo 1 >"'+C+'"']),$e&&$e("_execFileSync",c);try{gr.spawn(w,_,u)}catch(j){v.error=new Error(j.message),v.error.method="_execFileSync - spawn",v.error.program=w,v.error.args=_}for(;z.readFileSync(C,{encoding:r.encoding}).trim()!=="1";);return(g=z.readFileSync(b,{encoding:r.encoding}).trim())==="0"?v.input=ee.update(z.readFileSync(x,{encoding:"binary"}),"hex",r.encoding)+ee.final(r.encoding):(h=z.readFileSync(T,{encoding:r.encoding}).trim(),v.error=new Error(Br+(h?` +`+h:"")),v.error.method="_execFileSync",v.error.program=w,v.error.args=_,v.error.extMessage=h,v.error.exitCode=+g),z.unlinkSync(x),z.unlinkSync(T),z.unlinkSync(b),z.unlinkSync(C),v}function ts(r){var u,p={},c,w={env:process.env,encoding:r.encoding};if(we||(Ve?process.env.PSModulePath?(we="powershell.exe",Ke=["-ExecutionPolicy","Bypass","-File",__dirname+"\\read.ps1"]):(we="cscript.exe",Ke=["//nologo",__dirname+"\\read.cs.js"]):(we="/bin/sh",Ke=[__dirname+"/read.sh"])),Ve&&!process.env.PSModulePath&&(w.stdio=[process.stdin]),gr.execFileSync){u=Lr(r),$e&&$e("execFileSync",u);try{p.input=gr.execFileSync(we,u,w)}catch(_){c=_.stderr?(_.stderr+"").trim():"",p.error=new Error(Br+(c?` +`+c:"")),p.error.method="execFileSync",p.error.program=we,p.error.args=u,p.error.extMessage=c,p.error.exitCode=_.status,p.error.code=_.code,p.error.signal=_.signal}}else p=rs(r,w);return p.error||(p.input=p.input.replace(/^\s*'|'\s*$/g,""),r.display=""),p}function br(r){var u="",p=r.display,c=!r.display&&r.keyIn&&r.hideEchoBack&&!r.mask;function w(){var _=ts(r);if(_.error)throw _.error;return _.input}return mr&&mr(r),function(){var _,v,g;function h(){return _||(_=process.binding("fs"),v=process.binding("constants")),_}if(typeof fe=="string")if(fe=null,Ve){if(g=function(x){var T=x.replace(/^\D+/,"").split("."),b=0;return(T[0]=+T[0])&&(b+=T[0]*1e4),(T[1]=+T[1])&&(b+=T[1]*100),(T[2]=+T[2])&&(b+=T[2]),b}(process.version),!(g>=20302&&g<40204||g>=5e4&&g<50100||g>=50600&&g<60200)&&process.stdin.isTTY)process.stdin.pause(),fe=process.stdin.fd,Ce=process.stdin._handle;else try{fe=h().open("CONIN$",v.O_RDWR,parseInt("0666",8)),Ce=new Fr(fe,!0)}catch(x){}if(process.stdout.isTTY)oe=process.stdout.fd;else{try{oe=z.openSync("\\\\.\\CON","w")}catch(x){}if(typeof oe!="number")try{oe=h().open("CONOUT$",v.O_RDWR,parseInt("0666",8))}catch(x){}}}else{if(process.stdin.isTTY){process.stdin.pause();try{fe=z.openSync("/dev/tty","r"),Ce=process.stdin._handle}catch(x){}}else try{fe=z.openSync("/dev/tty","r"),Ce=new Fr(fe,!1)}catch(x){}if(process.stdout.isTTY)oe=process.stdout.fd;else try{oe=z.openSync("/dev/tty","w")}catch(x){}}}(),function(){var _,v,g=!r.hideEchoBack&&!r.keyIn,h,x,T,b,C;je="";function N(W){return W===zr?!0:Ce.setRawMode(W)!==0?!1:(zr=W,!0)}if(Wr||!Ce||typeof oe!="number"&&(r.display||!g)){u=w();return}if(r.display&&(z.writeSync(oe,r.display),r.display=""),!r.displayOnly){if(!N(!g)){u=w();return}for(x=r.keyIn?1:r.bufferSize,h=Buffer.allocUnsafe&&Buffer.alloc?Buffer.alloc(x):new Buffer(x),r.keyIn&&r.limit&&(v=new RegExp("[^"+r.limit+"]","g"+(r.caseSensitive?"":"i")));;){T=0;try{T=z.readSync(fe,h,0,x)}catch(W){if(W.code!=="EOF"){N(!1),u+=w();return}}if(T>0?(b=h.toString(r.encoding,0,T),je+=b):(b=` +`,je+=String.fromCharCode(0)),b&&typeof(C=(b.match(/^(.*?)[\r\n]/)||[])[1])=="string"&&(b=C,_=!0),b&&(b=b.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g,"")),b&&v&&(b=b.replace(v,"")),b&&(g||(r.hideEchoBack?r.mask&&z.writeSync(oe,new Array(b.length+1).join(r.mask)):z.writeSync(oe,b)),u+=b),!r.keyIn&&_||r.keyIn&&u.length>=x)break}!g&&!c&&z.writeSync(oe,` +`),N(!1)}}(),r.print&&!c&&r.print(p+(r.displayOnly?"":(r.hideEchoBack?new Array(u.length+1).join(r.mask):u)+` +`),r.encoding),r.displayOnly?"":hr=r.keepWhitespace||r.keyIn?u:u.trim()}function ns(r,u){var p=[];function c(w){w!=null&&(Array.isArray(w)?w.forEach(c):(!u||u(w))&&p.push(w))}return c(r),p}function Tr(r){return r.replace(/[\x00-\x7f]/g,function(u){return"\\x"+("00"+u.charCodeAt().toString(16)).substr(-2)})}function Z(){var r=Array.prototype.slice.call(arguments),u,p;return r.length&&typeof r[0]=="boolean"&&(p=r.shift(),p&&(u=Object.keys(dr),r.unshift(dr))),r.reduce(function(c,w){return w==null||(w.hasOwnProperty("noEchoBack")&&!w.hasOwnProperty("hideEchoBack")&&(w.hideEchoBack=w.noEchoBack,delete w.noEchoBack),w.hasOwnProperty("noTrim")&&!w.hasOwnProperty("keepWhitespace")&&(w.keepWhitespace=w.noTrim,delete w.noTrim),p||(u=Object.keys(w)),u.forEach(function(_){var v;if(!!w.hasOwnProperty(_))switch(v=w[_],_){case"mask":case"limitMessage":case"defaultInput":case"encoding":v=v!=null?v+"":"",v&&_!=="limitMessage"&&(v=v.replace(/[\r\n]/g,"")),c[_]=v;break;case"bufferSize":!isNaN(v=parseInt(v,10))&&typeof v=="number"&&(c[_]=v);break;case"displayOnly":case"keyIn":case"hideEchoBack":case"caseSensitive":case"keepWhitespace":case"history":case"cd":c[_]=!!v;break;case"limit":case"trueValue":case"falseValue":c[_]=ns(v,function(g){var h=typeof g;return h==="string"||h==="number"||h==="function"||g instanceof RegExp}).map(function(g){return typeof g=="string"?g.replace(/[\r\n]/g,""):g});break;case"print":case"phContent":case"preCheck":c[_]=typeof v=="function"?v:void 0;break;case"prompt":case"display":c[_]=v!=null?v:"";break}})),c},{})}function xr(r,u,p){return u.some(function(c){var w=typeof c;return w==="string"?p?r===c:r.toLowerCase()===c.toLowerCase():w==="number"?parseFloat(r)===c:w==="function"?c(r):c instanceof RegExp?c.test(r):!1})}function Vr(r,u){var p=_e.normalize(Ve?(process.env.HOMEDRIVE||"")+(process.env.HOMEPATH||""):process.env.HOME||"").replace(/[\/\\]+$/,"");return r=_e.normalize(r),u?r.replace(/^~(?=\/|\\|$)/,p):r.replace(new RegExp("^"+Tr(p)+"(?=\\/|\\\\|$)",Ve?"i":""),"~")}function Oe(r,u){var p="(?:\\(([\\s\\S]*?)\\))?(\\w+|.-.)(?:\\(([\\s\\S]*?)\\))?",c=new RegExp("(\\$)?(\\$<"+p+">)","g"),w=new RegExp("(\\$)?(\\$\\{"+p+"\\})","g");function _(v,g,h,x,T,b){var C;return g||typeof(C=u(T))!="string"?h:C?(x||"")+C+(b||""):""}return r.replace(c,_).replace(w,_)}function Hr(r,u,p){var c,w=[],_=-1,v=0,g="",h;function x(T,b){return b.length>3?(T.push(b[0]+"..."+b[b.length-1]),h=!0):b.length&&(T=T.concat(b)),T}return c=r.reduce(function(T,b){return T.concat((b+"").split(""))},[]).reduce(function(T,b){var C,N;return u||(b=b.toLowerCase()),C=/^\d$/.test(b)?1:/^[A-Z]$/.test(b)?2:/^[a-z]$/.test(b)?3:0,p&&C===0?g+=b:(N=b.charCodeAt(0),C&&C===_&&N===v+1?w.push(b):(T=x(T,w),w=[b],_=C),v=N),T},[]),c=x(c,w),g&&(c.push(g),h=!0),{values:c,suppressed:h}}function Gr(r,u){return r.join(r.length>2?", ":u?" / ":"/")}function Yr(r,u){var p,c,w={},_;if(u.phContent&&(p=u.phContent(r,u)),typeof p!="string")switch(r){case"hideEchoBack":case"mask":case"defaultInput":case"caseSensitive":case"keepWhitespace":case"encoding":case"bufferSize":case"history":case"cd":p=u.hasOwnProperty(r)?typeof u[r]=="boolean"?u[r]?"on":"off":u[r]+"":"";break;case"limit":case"trueValue":case"falseValue":c=u[u.hasOwnProperty(r+"Src")?r+"Src":r],u.keyIn?(w=Hr(c,u.caseSensitive),c=w.values):c=c.filter(function(v){var g=typeof v;return g==="string"||g==="number"}),p=Gr(c,w.suppressed);break;case"limitCount":case"limitCountNotZero":p=u[u.hasOwnProperty("limitSrc")?"limitSrc":"limit"].length,p=p||r!=="limitCountNotZero"?p+"":"";break;case"lastInput":p=hr;break;case"cwd":case"CWD":case"cwdHome":p=process.cwd(),r==="CWD"?p=_e.basename(p):r==="cwdHome"&&(p=Vr(p));break;case"date":case"time":case"localeDate":case"localeTime":p=new Date()["to"+r.replace(/^./,function(v){return v.toUpperCase()})+"String"]();break;default:typeof(_=(r.match(/^history_m(\d+)$/)||[])[1])=="string"&&(p=Se[Se.length-_]||"")}return p}function Ur(r){var u=/^(.)-(.)$/.exec(r),p="",c,w,_,v;if(!u)return null;for(c=u[1].charCodeAt(0),w=u[2].charCodeAt(0),v=c +And the length must be: $`,trueValue:null,falseValue:null,caseSensitive:!0},u,{history:!1,cd:!1,phContent:function(N){return N==="charlist"?p.text:N==="length"?c+"..."+w:null}}),v,g,h,x,T,b,C;for(u=u||{},v=Oe(u.charlist?u.charlist+"":"$",Ur),(isNaN(c=parseInt(u.min,10))||typeof c!="number")&&(c=12),(isNaN(w=parseInt(u.max,10))||typeof w!="number")&&(w=24),x=new RegExp("^["+Tr(v)+"]{"+c+","+w+"}$"),p=Hr([v],_.caseSensitive,!0),p.text=Gr(p.values,p.suppressed),g=u.confirmMessage!=null?u.confirmMessage:"Reinput a same one to confirm it: ",h=u.unmatchMessage!=null?u.unmatchMessage:"It differs from first one. Hit only the Enter key if you want to retry from first one.",r==null&&(r="Input new password: "),T=_.limitMessage;!C;)_.limit=x,_.limitMessage=T,b=M.question(r,_),_.limit=[b,""],_.limitMessage=h,C=M.question(g,_);return b};function Jr(r,u,p){var c;function w(_){return c=p(_),!isNaN(c)&&typeof c=="number"}return M.question(r,Z({limitMessage:"Input valid number, please."},u,{limit:w,cd:!1})),c}M.questionInt=function(r,u){return Jr(r,u,function(p){return parseInt(p,10)})};M.questionFloat=function(r,u){return Jr(r,u,parseFloat)};M.questionPath=function(r,u){var p,c="",w=Z({hideEchoBack:!1,limitMessage:`$Input valid path, please.$<( Min:)min>$<( Max:)max>`,history:!0,cd:!0},u,{keepWhitespace:!1,limit:function(_){var v,g,h;_=Vr(_,!0),c="";function x(T){T.split(/\/|\\/).reduce(function(b,C){var N=_e.resolve(b+=C+_e.sep);if(!z.existsSync(N))z.mkdirSync(N);else if(!z.statSync(N).isDirectory())throw new Error("Non directory already exists: "+N);return b},"")}try{if(v=z.existsSync(_),p=v?z.realpathSync(_):_e.resolve(_),!u.hasOwnProperty("exists")&&!v||typeof u.exists=="boolean"&&u.exists!==v)return c=(v?"Already exists":"No such file or directory")+": "+p,!1;if(!v&&u.create&&(u.isDirectory?x(p):(x(_e.dirname(p)),z.closeSync(z.openSync(p,"w"))),p=z.realpathSync(p)),v&&(u.min||u.max||u.isFile||u.isDirectory)){if(g=z.statSync(p),u.isFile&&!g.isFile())return c="Not file: "+p,!1;if(u.isDirectory&&!g.isDirectory())return c="Not directory: "+p,!1;if(u.min&&g.size<+u.min||u.max&&g.size>+u.max)return c="Size "+g.size+" is out of range: "+p,!1}if(typeof u.validate=="function"&&(h=u.validate(p))!==!0)return typeof h=="string"&&(c=h),!1}catch(T){return c=T+"",!1}return!0},phContent:function(_){return _==="error"?c:_!=="min"&&_!=="max"?null:u.hasOwnProperty(_)?u[_]+"":""}});return u=u||{},r==null&&(r='Input path (you can "cd" and "pwd"): '),M.question(r,w),p};function Kr(r,u){var p={},c={};return typeof r=="object"?(Object.keys(r).forEach(function(w){typeof r[w]=="function"&&(c[u.caseSensitive?w:w.toLowerCase()]=r[w])}),p.preCheck=function(w){var _;return p.args=Sr(w),_=p.args[0]||"",u.caseSensitive||(_=_.toLowerCase()),p.hRes=_!=="_"&&c.hasOwnProperty(_)?c[_].apply(w,p.args.slice(1)):c.hasOwnProperty("_")?c._.apply(w,p.args):null,{res:w,forceNext:!1}},c.hasOwnProperty("_")||(p.limit=function(){var w=p.args[0]||"";return u.caseSensitive||(w=w.toLowerCase()),c.hasOwnProperty(w)})):p.preCheck=function(w){return p.args=Sr(w),p.hRes=typeof r=="function"?r.apply(w,p.args):!0,{res:w,forceNext:!1}},p}M.promptCL=function(r,u){var p=Z({hideEchoBack:!1,limitMessage:"Requested command is not available.",caseSensitive:!1,history:!0},u),c=Kr(r,p);return p.limit=c.limit,p.preCheck=c.preCheck,M.prompt(p),c.args};M.promptLoop=function(r,u){for(var p=Z({hideEchoBack:!1,trueValue:null,falseValue:null,caseSensitive:!1,history:!0},u);!r(M.prompt(p)););};M.promptCLLoop=function(r,u){var p=Z({hideEchoBack:!1,limitMessage:"Requested command is not available.",caseSensitive:!1,history:!0},u),c=Kr(r,p);for(p.limit=c.limit,p.preCheck=c.preCheck;M.prompt(p),!c.hRes;);};M.promptSimShell=function(r){return M.prompt(Z({hideEchoBack:!1,history:!0},r,{prompt:function(){return Ve?"$>":(process.env.USER||"")+(process.env.HOSTNAME?"@"+process.env.HOSTNAME.replace(/\..*$/,""):"")+":$$ "}()}))};function jr(r,u,p){var c;return r==null&&(r="Are you sure? "),(!u||u.guide!==!1)&&(r+="")&&(r=r.replace(/\s*:?\s*$/,"")+" [y/n]: "),c=M.keyIn(r,Z(u,{hideEchoBack:!1,limit:p,trueValue:"y",falseValue:"n",caseSensitive:!1})),typeof c=="boolean"?c:""}M.keyInYN=function(r,u){return jr(r,u)};M.keyInYNStrict=function(r,u){return jr(r,u,"yn")};M.keyInPause=function(r,u){r==null&&(r="Continue..."),(!u||u.guide!==!1)&&(r+="")&&(r=r.replace(/\s+$/,"")+" (Hit any key)"),M.keyIn(r,Z({limit:null},u,{hideEchoBack:!0,mask:""}))};M.keyInSelect=function(r,u,p){var c=Z({hideEchoBack:!1},p,{trueValue:null,falseValue:null,caseSensitive:!1,phContent:function(h){return h==="itemsCount"?r.length+"":h==="firstItem"?(r[0]+"").trim():h==="lastItem"?(r[r.length-1]+"").trim():null}}),w="",_={},v=49,g=` +`;if(!Array.isArray(r)||!r.length||r.length>35)throw"`items` must be Array (max length: 35).";return r.forEach(function(h,x){var T=String.fromCharCode(v);w+=T,_[T]=x,g+="["+T+"] "+(h+"").trim()+` +`,v=v===57?97:v+1}),(!p||p.cancel!==!1)&&(w+="0",_["0"]=-1,g+="[0] "+(p&&p.cancel!=null&&typeof p.cancel!="boolean"?(p.cancel+"").trim():"CANCEL")+` +`),c.limit=w,g+=` +`,u==null&&(u="Choose one from list: "),(u+="")&&((!p||p.guide!==!1)&&(u=u.replace(/\s*:?\s*$/,"")+" [$]: "),g+=u),_[M.keyIn(g,c).toLowerCase()]};M.getRawInput=function(){return je};function De(r,u){var p;return u.length&&(p={},p[r]=u[0]),M.setDefaultOptions(p)[r]}M.setPrint=function(){return De("print",arguments)};M.setPrompt=function(){return De("prompt",arguments)};M.setEncoding=function(){return De("encoding",arguments)};M.setMask=function(){return De("mask",arguments)};M.setBufferSize=function(){return De("bufferSize",arguments)}});var kr=I((Mu,ie)=>{(function(){var r={major:0,minor:2,patch:66,status:"beta"};tau_file_system={files:{},open:function(e,n,t){var s=tau_file_system.files[e];if(!s){if(t==="read")return null;s={path:e,text:"",type:n,get:function(a,l){return l===this.text.length||l>this.text.length?"end_of_file":this.text.substring(l,l+a)},put:function(a,l){return l==="end_of_file"?(this.text+=a,!0):l==="past_end_of_file"?null:(this.text=this.text.substring(0,l)+a+this.text.substring(l+a.length),!0)},get_byte:function(a){if(a==="end_of_stream")return-1;var l=Math.floor(a/2);if(this.text.length<=l)return-1;var f=_(this.text[Math.floor(a/2)],0);return a%2==0?f&255:f/256>>>0},put_byte:function(a,l){var f=l==="end_of_stream"?this.text.length:Math.floor(l/2);if(this.text.length>>0,y=(y&255)<<8|a&255):(y=y&255,y=(a&255)<<8|y&255),this.text.length===f?this.text+=v(y):this.text=this.text.substring(0,f)+v(y)+this.text.substring(f+1),!0},flush:function(){return!0},close:function(){var a=tau_file_system.files[this.path];return a?!0:null}},tau_file_system.files[e]=s}return t==="write"&&(s.text=""),s}},tau_user_input={buffer:"",get:function(e,n){for(var t;tau_user_input.buffer.length\?\@\^\~\\]+|'(?:[^']*?(?:\\(?:x?\d+)?\\)*(?:'')*(?:\\')*)*')/,number:/^(?:0o[0-7]+|0x[0-9a-fA-F]+|0b[01]+|0'(?:''|\\[abfnrtv\\'"`]|\\x?\d+\\|[^\\])|\d+(?:\.\d+(?:[eE][+-]?\d+)?)?)/,string:/^(?:"([^"]|""|\\")*"|`([^`]|``|\\`)*`)/,l_brace:/^(?:\[)/,r_brace:/^(?:\])/,l_bracket:/^(?:\{)/,r_bracket:/^(?:\})/,bar:/^(?:\|)/,l_paren:/^(?:\()/,r_paren:/^(?:\))/};function te(e,n){return e.get_flag("char_conversion").id==="on"?n.replace(/./g,function(t){return e.get_char_conversion(t)}):n}function j(e){this.thread=e,this.text="",this.tokens=[]}j.prototype.set_last_tokens=function(e){return this.tokens=e},j.prototype.new_text=function(e){this.text=e,this.tokens=[]},j.prototype.get_tokens=function(e){var n,t=0,s=0,a=0,l=[],f=!1;if(e){var y=this.tokens[e-1];t=y.len,n=te(this.thread,this.text.substr(y.len)),s=y.line,a=y.start}else n=this.text;if(/^\s*$/.test(n))return null;for(;n!=="";){var d=[],m=!1;if(/^\n/.exec(n)!==null){s++,a=0,t++,n=n.replace(/\n/,""),f=!0;continue}for(var S in ee)if(ee.hasOwnProperty(S)){var P=ee[S].exec(n);P&&d.push({value:P[0],name:S,matches:P})}if(!d.length)return this.set_last_tokens([{value:n,matches:[],name:"lexical",line:s,start:a}]);var y=p(d,function(B,q){return B.value.length>=q.value.length?B:q});switch(y.start=a,y.line=s,n=n.replace(y.value,""),a+=y.value.length,t+=y.value.length,y.name){case"atom":y.raw=y.value,y.value.charAt(0)==="'"&&(y.value=C(y.value.substr(1,y.value.length-2),"'"),y.value===null&&(y.name="lexical",y.value="unknown escape sequence"));break;case"number":y.float=y.value.substring(0,2)!=="0x"&&y.value.match(/[.eE]/)!==null&&y.value!=="0'.",y.value=W(y.value),y.blank=m;break;case"string":var A=y.value.charAt(0);y.value=C(y.value.substr(1,y.value.length-2),A),y.value===null&&(y.name="lexical",y.value="unknown escape sequence");break;case"whitespace":var R=l[l.length-1];R&&(R.space=!0),m=!0;continue;case"r_bracket":l.length>0&&l[l.length-1].name==="l_bracket"&&(y=l.pop(),y.name="atom",y.value="{}",y.raw="{}",y.space=!1);break;case"r_brace":l.length>0&&l[l.length-1].name==="l_brace"&&(y=l.pop(),y.name="atom",y.value="[]",y.raw="[]",y.space=!1);break}y.len=t,l.push(y),m=!1}var k=this.set_last_tokens(l);return k.length===0?null:k};function U(e,n,t,s,a){if(!n[t])return{type:g,value:i.error.syntax(n[t-1],"expression expected",!0)};var l;if(s==="0"){var f=n[t];switch(f.name){case"number":return{type:h,len:t+1,value:new i.type.Num(f.value,f.float)};case"variable":return{type:h,len:t+1,value:new i.type.Var(f.value)};case"string":var y;switch(e.get_flag("double_quotes").id){case"atom":y=new o(f.value,[]);break;case"codes":y=new o("[]",[]);for(var d=f.value.length-1;d>=0;d--)y=new o(".",[new i.type.Num(_(f.value,d),!1),y]);break;case"chars":y=new o("[]",[]);for(var d=f.value.length-1;d>=0;d--)y=new o(".",[new i.type.Term(f.value.charAt(d),[]),y]);break}return{type:h,len:t+1,value:y};case"l_paren":var k=U(e,n,t+1,e.__get_max_priority(),!0);return k.type!==h?k:n[k.len]&&n[k.len].name==="r_paren"?(k.len++,k):{type:g,derived:!0,value:i.error.syntax(n[k.len]?n[k.len]:n[k.len-1],") or operator expected",!n[k.len])};case"l_bracket":var k=U(e,n,t+1,e.__get_max_priority(),!0);return k.type!==h?k:n[k.len]&&n[k.len].name==="r_bracket"?(k.len++,k.value=new o("{}",[k.value]),k):{type:g,derived:!0,value:i.error.syntax(n[k.len]?n[k.len]:n[k.len-1],"} or operator expected",!n[k.len])}}var m=Ue(e,n,t,a);return m.type===h||m.derived||(m=Ze(e,n,t),m.type===h||m.derived)?m:{type:g,derived:!1,value:i.error.syntax(n[t],"unexpected token")}}var S=e.__get_max_priority(),P=e.__get_next_priority(s),A=t;if(n[t].name==="atom"&&n[t+1]&&(n[t].space||n[t+1].name!=="l_paren")){var f=n[t++],R=e.__lookup_operator_classes(s,f.value);if(R&&R.indexOf("fy")>-1){var k=U(e,n,t,s,a);if(k.type!==g)return f.value==="-"&&!f.space&&i.type.is_number(k.value)?{value:new i.type.Num(-k.value.value,k.value.is_float),len:k.len,type:h}:{value:new i.type.Term(f.value,[k.value]),len:k.len,type:h};l=k}else if(R&&R.indexOf("fx")>-1){var k=U(e,n,t,P,a);if(k.type!==g)return{value:new i.type.Term(f.value,[k.value]),len:k.len,type:h};l=k}}t=A;var k=U(e,n,t,P,a);if(k.type===h){t=k.len;var f=n[t];if(n[t]&&(n[t].name==="atom"&&e.__lookup_operator_classes(s,f.value)||n[t].name==="bar"&&e.__lookup_operator_classes(s,"|"))){var L=P,B=s,R=e.__lookup_operator_classes(s,f.value);if(R.indexOf("xf")>-1)return{value:new i.type.Term(f.value,[k.value]),len:++k.len,type:h};if(R.indexOf("xfx")>-1){var q=U(e,n,t+1,L,a);return q.type===h?{value:new i.type.Term(f.value,[k.value,q.value]),len:q.len,type:h}:(q.derived=!0,q)}else if(R.indexOf("xfy")>-1){var q=U(e,n,t+1,B,a);return q.type===h?{value:new i.type.Term(f.value,[k.value,q.value]),len:q.len,type:h}:(q.derived=!0,q)}else if(k.type!==g)for(;;){t=k.len;var f=n[t];if(f&&f.name==="atom"&&e.__lookup_operator_classes(s,f.value)){var R=e.__lookup_operator_classes(s,f.value);if(R.indexOf("yf")>-1)k={value:new i.type.Term(f.value,[k.value]),len:++t,type:h};else if(R.indexOf("yfx")>-1){var q=U(e,n,++t,L,a);if(q.type===g)return q.derived=!0,q;t=q.len,k={value:new i.type.Term(f.value,[k.value,q.value]),len:t,type:h}}else break}else break}}else l={type:g,value:i.error.syntax(n[k.len-1],"operator expected")};return k}return k}function Ue(e,n,t,s){if(!n[t]||n[t].name==="atom"&&n[t].raw==="."&&!s&&(n[t].space||!n[t+1]||n[t+1].name!=="l_paren"))return{type:g,derived:!1,value:i.error.syntax(n[t-1],"unfounded token")};var a=n[t],l=[];if(n[t].name==="atom"&&n[t].raw!==","){if(t++,n[t-1].space)return{type:h,len:t,value:new i.type.Term(a.value,l)};if(n[t]&&n[t].name==="l_paren"){if(n[t+1]&&n[t+1].name==="r_paren")return{type:g,derived:!0,value:i.error.syntax(n[t+1],"argument expected")};var f=U(e,n,++t,"999",!0);if(f.type===g)return f.derived?f:{type:g,derived:!0,value:i.error.syntax(n[t]?n[t]:n[t-1],"argument expected",!n[t])};for(l.push(f.value),t=f.len;n[t]&&n[t].name==="atom"&&n[t].value===",";){if(f=U(e,n,t+1,"999",!0),f.type===g)return f.derived?f:{type:g,derived:!0,value:i.error.syntax(n[t+1]?n[t+1]:n[t],"argument expected",!n[t+1])};l.push(f.value),t=f.len}if(n[t]&&n[t].name==="r_paren")t++;else return{type:g,derived:!0,value:i.error.syntax(n[t]?n[t]:n[t-1],", or ) expected",!n[t])}}return{type:h,len:t,value:new i.type.Term(a.value,l)}}return{type:g,derived:!1,value:i.error.syntax(n[t],"term expected")}}function Ze(e,n,t){if(!n[t])return{type:g,derived:!1,value:i.error.syntax(n[t-1],"[ expected")};if(n[t]&&n[t].name==="l_brace"){var s=U(e,n,++t,"999",!0),a=[s.value],l=void 0;if(s.type===g)return n[t]&&n[t].name==="r_brace"?{type:h,len:t+1,value:new i.type.Term("[]",[])}:{type:g,derived:!0,value:i.error.syntax(n[t],"] expected")};for(t=s.len;n[t]&&n[t].name==="atom"&&n[t].value===",";){if(s=U(e,n,t+1,"999",!0),s.type===g)return s.derived?s:{type:g,derived:!0,value:i.error.syntax(n[t+1]?n[t+1]:n[t],"argument expected",!n[t+1])};a.push(s.value),t=s.len}var f=!1;if(n[t]&&n[t].name==="bar"){if(f=!0,s=U(e,n,t+1,"999",!0),s.type===g)return s.derived?s:{type:g,derived:!0,value:i.error.syntax(n[t+1]?n[t+1]:n[t],"argument expected",!n[t+1])};l=s.value,t=s.len}return n[t]&&n[t].name==="r_brace"?{type:h,len:t+1,value:he(a,l)}:{type:g,derived:!0,value:i.error.syntax(n[t]?n[t]:n[t-1],f?"] expected":", or | or ] expected",!n[t])}}return{type:g,derived:!1,value:i.error.syntax(n[t],"list expected")}}function Qe(e,n,t){var s=n[t].line,a=U(e,n,t,e.__get_max_priority(),!1),l=null,f;if(a.type!==g)if(t=a.len,n[t]&&n[t].name==="atom"&&n[t].raw===".")if(t++,i.type.is_term(a.value)){if(a.value.indicator===":-/2"?(l=new i.type.Rule(a.value.args[0],ve(a.value.args[1])),f={value:l,len:t,type:h}):a.value.indicator==="-->/2"?(l=Bi(new i.type.Rule(a.value.args[0],a.value.args[1]),e),l.body=ve(l.body),f={value:l,len:t,type:i.type.is_rule(l)?h:g}):(l=new i.type.Rule(a.value,null),f={value:l,len:t,type:h}),l){var y=l.singleton_variables();y.length>0&&e.throw_warning(i.warning.singleton(y,l.head.indicator,s))}return f}else return{type:g,value:i.error.syntax(n[t],"callable expected")};else return{type:g,value:i.error.syntax(n[t]?n[t]:n[t-1],". or operator expected")};return a}function Di(e,n,t){t=t||{},t.from=t.from?t.from:"$tau-js",t.reconsult=t.reconsult!==void 0?t.reconsult:!0;var s=new j(e),a={},l;s.new_text(n);var f=0,y=s.get_tokens(f);do{if(y===null||!y[f])break;var d=Qe(e,y,f);if(d.type===g)return new o("throw",[d.value]);if(d.value.body===null&&d.value.head.indicator==="?-/1"){var m=new X(e.session);m.add_goal(d.value.head.args[0]),m.answer(function(P){i.type.is_error(P)?e.throw_warning(P.args[0]):(P===!1||P===null)&&e.throw_warning(i.warning.failed_goal(d.value.head.args[0],d.len))}),f=d.len;var S=!0}else if(d.value.body===null&&d.value.head.indicator===":-/1"){var S=e.run_directive(d.value.head.args[0]);f=d.len,d.value.head.args[0].indicator==="char_conversion/2"&&(y=s.get_tokens(f),f=0)}else{l=d.value.head.indicator,t.reconsult!==!1&&a[l]!==!0&&!e.is_multifile_predicate(l)&&(e.session.rules[l]=w(e.session.rules[l]||[],function(A){return A.dynamic}),a[l]=!0);var S=e.add_rule(d.value,t);f=d.len}if(!S)return S}while(!0);return!0}function Xi(e,n){var t=new j(e);t.new_text(n);var s=0;do{var a=t.get_tokens(s);if(a===null)break;var l=U(e,a,0,e.__get_max_priority(),!1);if(l.type!==g){var f=l.len,y=f;if(a[f]&&a[f].name==="atom"&&a[f].raw===".")e.add_goal(ve(l.value));else{var d=a[f];return new o("throw",[i.error.syntax(d||a[f-1],". or operator expected",!d)])}s=l.len+1}else return new o("throw",[l.value])}while(!0);return!0}function Bi(e,n){e=e.rename(n);var t=n.next_free_variable(),s=pr(e.body,t,n);return s.error?s.value:(e.body=s.value,e.head.args=e.head.args.concat([t,s.variable]),e.head=new o(e.head.id,e.head.args),e)}function pr(e,n,t){var s;if(i.type.is_term(e)&&e.indicator==="!/0")return{value:e,variable:n,error:!1};if(i.type.is_term(e)&&e.indicator===",/2"){var a=pr(e.args[0],n,t);if(a.error)return a;var l=pr(e.args[1],a.variable,t);return l.error?l:{value:new o(",",[a.value,l.value]),variable:l.variable,error:!1}}else{if(i.type.is_term(e)&&e.indicator==="{}/1")return{value:e.args[0],variable:n,error:!1};if(i.type.is_empty_list(e))return{value:new o("true",[]),variable:n,error:!1};if(i.type.is_list(e)){s=t.next_free_variable();for(var f=e,y;f.indicator==="./2";)y=f,f=f.args[1];return i.type.is_variable(f)?{value:i.error.instantiation("DCG"),variable:n,error:!0}:i.type.is_empty_list(f)?(y.args[1]=s,{value:new o("=",[n,e]),variable:s,error:!1}):{value:i.error.type("list",e,"DCG"),variable:n,error:!0}}else return i.type.is_callable(e)?(s=t.next_free_variable(),e.args=e.args.concat([n,s]),e=new o(e.id,e.args),{value:e,variable:s,error:!1}):{value:i.error.type("callable",e,"DCG"),variable:n,error:!0}}}function ve(e){return i.type.is_variable(e)?new o("call",[e]):i.type.is_term(e)&&[",/2",";/2","->/2"].indexOf(e.indicator)!==-1?new o(e.id,[ve(e.args[0]),ve(e.args[1])]):e}function he(e,n){for(var t=n||new i.type.Term("[]",[]),s=e.length-1;s>=0;s--)t=new i.type.Term(".",[e[s],t]);return t}function Fi(e,n){for(var t=e.length-1;t>=0;t--)e[t]===n&&e.splice(t,1)}function yr(e){for(var n={},t=[],s=0;s=0;n--)if(e.charAt(n)==="/")return new o("/",[new o(e.substring(0,n)),new E(parseInt(e.substring(n+1)),!1)])}function O(e){this.id=e}function E(e,n){this.is_float=n!==void 0?n:parseInt(e)!==e,this.value=this.is_float?e:parseInt(e)}var $r=0;function o(e,n,t){this.ref=t||++$r,this.id=e,this.args=n||[],this.indicator=e+"/"+this.args.length}var Wi=0;function ne(e,n,t,s,a,l){this.id=Wi++,this.stream=e,this.mode=n,this.alias=t,this.type=s!==void 0?s:"text",this.reposition=a!==void 0?a:!0,this.eof_action=l!==void 0?l:"eof_code",this.position=this.mode==="append"?"end_of_stream":0,this.output=this.mode==="write"||this.mode==="append",this.input=this.mode==="read"}function Y(e){e=e||{},this.links=e}function V(e,n,t){n=n||new Y,t=t||null,this.goal=e,this.substitution=n,this.parent=t}function Q(e,n,t){this.head=e,this.body=n,this.dynamic=t||!1}function D(e){e=e===void 0||e<=0?1e3:e,this.rules={},this.src_predicates={},this.rename=0,this.modules=[],this.thread=new X(this),this.total_threads=1,this.renamed_variables={},this.public_predicates={},this.multifile_predicates={},this.limit=e,this.streams={user_input:new ne(typeof ie!="undefined"&&ie.exports?nodejs_user_input:tau_user_input,"read","user_input","text",!1,"reset"),user_output:new ne(typeof ie!="undefined"&&ie.exports?nodejs_user_output:tau_user_output,"write","user_output","text",!1,"eof_code")},this.file_system=typeof ie!="undefined"&&ie.exports?nodejs_file_system:tau_file_system,this.standard_input=this.streams.user_input,this.standard_output=this.streams.user_output,this.current_input=this.streams.user_input,this.current_output=this.streams.user_output,this.format_success=function(n){return n.substitution},this.format_error=function(n){return n.goal},this.flag={bounded:i.flag.bounded.value,max_integer:i.flag.max_integer.value,min_integer:i.flag.min_integer.value,integer_rounding_function:i.flag.integer_rounding_function.value,char_conversion:i.flag.char_conversion.value,debug:i.flag.debug.value,max_arity:i.flag.max_arity.value,unknown:i.flag.unknown.value,double_quotes:i.flag.double_quotes.value,occurs_check:i.flag.occurs_check.value,dialect:i.flag.dialect.value,version_data:i.flag.version_data.value,nodejs:i.flag.nodejs.value},this.__loaded_modules=[],this.__char_conversion={},this.__operators={1200:{":-":["fx","xfx"],"-->":["xfx"],"?-":["fx"]},1100:{";":["xfy"]},1050:{"->":["xfy"]},1e3:{",":["xfy"]},900:{"\\+":["fy"]},700:{"=":["xfx"],"\\=":["xfx"],"==":["xfx"],"\\==":["xfx"],"@<":["xfx"],"@=<":["xfx"],"@>":["xfx"],"@>=":["xfx"],"=..":["xfx"],is:["xfx"],"=:=":["xfx"],"=\\=":["xfx"],"<":["xfx"],"=<":["xfx"],">":["xfx"],">=":["xfx"]},600:{":":["xfy"]},500:{"+":["yfx"],"-":["yfx"],"/\\":["yfx"],"\\/":["yfx"]},400:{"*":["yfx"],"/":["yfx"],"//":["yfx"],rem:["yfx"],mod:["yfx"],"<<":["yfx"],">>":["yfx"]},200:{"**":["xfx"],"^":["xfy"],"-":["fy"],"+":["fy"],"\\":["fy"]}}}function X(e){this.epoch=Date.now(),this.session=e,this.session.total_threads++,this.total_steps=0,this.cpu_time=0,this.cpu_time_last=0,this.points=[],this.debugger=!1,this.debugger_states=[],this.level="top_level/0",this.__calls=[],this.current_limit=this.session.limit,this.warnings=[]}function Dr(e,n,t){this.id=e,this.rules=n,this.exports=t,i.module[e]=this}Dr.prototype.exports_predicate=function(e){return this.exports.indexOf(e)!==-1},O.prototype.unify=function(e,n){if(n&&u(e.variables(),this.id)!==-1&&!i.type.is_variable(e))return null;var t={};return t[this.id]=e,new Y(t)},E.prototype.unify=function(e,n){return i.type.is_number(e)&&this.value===e.value&&this.is_float===e.is_float?new Y:null},o.prototype.unify=function(e,n){if(i.type.is_term(e)&&this.indicator===e.indicator){for(var t=new Y,s=0;s=0){var s=this.args[0].value,a=Math.floor(s/26),l=s%26;return"ABCDEFGHIJKLMNOPQRSTUVWXYZ"[l]+(a!==0?a:"")}switch(this.indicator){case"[]/0":case"{}/0":case"!/0":return this.id;case"{}/1":return"{"+this.args[0].toString(e)+"}";case"./2":for(var f="["+this.args[0].toString(e),y=this.args[1];y.indicator==="./2";)f+=", "+y.args[0].toString(e),y=y.args[1];return y.indicator!=="[]/0"&&(f+="|"+y.toString(e)),f+="]",f;case",/2":return"("+this.args[0].toString(e)+", "+this.args[1].toString(e)+")";default:var d=this.id,m=e.session?e.session.lookup_operator(this.id,this.args.length):null;if(e.session===void 0||e.ignore_ops||m===null)return e.quoted&&!/^(!|,|;|[a-z][0-9a-zA-Z_]*)$/.test(d)&&d!=="{}"&&d!=="[]"&&(d="'"+N(d)+"'"),d+(this.args.length?"("+c(this.args,function(R){return R.toString(e)}).join(", ")+")":"");var S=m.priority>n.priority||m.priority===n.priority&&(m.class==="xfy"&&this.indicator!==n.indicator||m.class==="yfx"&&this.indicator!==n.indicator||this.indicator===n.indicator&&m.class==="yfx"&&t==="right"||this.indicator===n.indicator&&m.class==="xfy"&&t==="left");m.indicator=this.indicator;var P=S?"(":"",A=S?")":"";return this.args.length===0?"("+this.id+")":["fy","fx"].indexOf(m.class)!==-1?P+d+" "+this.args[0].toString(e,m)+A:["yf","xf"].indexOf(m.class)!==-1?P+this.args[0].toString(e,m)+" "+d+A:P+this.args[0].toString(e,m,"left")+" "+this.id+" "+this.args[1].toString(e,m,"right")+A}},ne.prototype.toString=function(e){return"("+this.id+")"},Y.prototype.toString=function(e){var n="{";for(var t in this.links)!this.links.hasOwnProperty(t)||(n!=="{"&&(n+=", "),n+=t+"/"+this.links[t].toString(e));return n+="}",n},V.prototype.toString=function(e){return this.goal===null?"<"+this.substitution.toString(e)+">":"<"+this.goal.toString(e)+", "+this.substitution.toString(e)+">"},Q.prototype.toString=function(e){return this.body?this.head.toString(e)+" :- "+this.body.toString(e)+".":this.head.toString(e)+"."},D.prototype.toString=function(e){for(var n="",t=0;t=0;a--)s=new o(".",[n[a],s]);return s}return new o(this.id,c(this.args,function(l){return l.apply(e)}),this.ref)},ne.prototype.apply=function(e){return this},Q.prototype.apply=function(e){return new Q(this.head.apply(e),this.body!==null?this.body.apply(e):null)},Y.prototype.apply=function(e){var n,t={};for(n in this.links)!this.links.hasOwnProperty(n)||(t[n]=this.links[n].apply(e));return new Y(t)},o.prototype.select=function(){for(var e=this;e.indicator===",/2";)e=e.args[0];return e},o.prototype.replace=function(e){return this.indicator===",/2"?this.args[0].indicator===",/2"?new o(",",[this.args[0].replace(e),this.args[1]]):e===null?this.args[1]:new o(",",[e,this.args[1]]):e},o.prototype.search=function(e){if(i.type.is_term(e)&&e.ref!==void 0&&this.ref===e.ref)return!0;for(var n=0;nn&&s0&&(n=this.head_point().substitution.domain());u(n,i.format_variable(this.session.rename))!==-1;)this.session.rename++;if(e.id==="_")return new O(i.format_variable(this.session.rename));this.session.renamed_variables[e.id]=i.format_variable(this.session.rename)}return new O(this.session.renamed_variables[e.id])},D.prototype.next_free_variable=function(){return this.thread.next_free_variable()},X.prototype.next_free_variable=function(){this.session.rename++;var e=[];for(this.points.length>0&&(e=this.head_point().substitution.domain());u(e,i.format_variable(this.session.rename))!==-1;)this.session.rename++;return new O(i.format_variable(this.session.rename))},D.prototype.is_public_predicate=function(e){return!this.public_predicates.hasOwnProperty(e)||this.public_predicates[e]===!0},X.prototype.is_public_predicate=function(e){return this.session.is_public_predicate(e)},D.prototype.is_multifile_predicate=function(e){return this.multifile_predicates.hasOwnProperty(e)&&this.multifile_predicates[e]===!0},X.prototype.is_multifile_predicate=function(e){return this.session.is_multifile_predicate(e)},D.prototype.prepend=function(e){return this.thread.prepend(e)},X.prototype.prepend=function(e){for(var n=e.length-1;n>=0;n--)this.points.push(e[n])},D.prototype.success=function(e,n){return this.thread.success(e,n)},X.prototype.success=function(e,n){var n=typeof n=="undefined"?e:n;this.prepend([new V(e.goal.replace(null),e.substitution,n)])},D.prototype.throw_error=function(e){return this.thread.throw_error(e)},X.prototype.throw_error=function(e){this.prepend([new V(new o("throw",[e]),new Y,null,null)])},D.prototype.step_rule=function(e,n){return this.thread.step_rule(e,n)},X.prototype.step_rule=function(e,n){var t=n.indicator;if(e==="user"&&(e=null),e===null&&this.session.rules.hasOwnProperty(t))return this.session.rules[t];for(var s=e===null?this.session.modules:u(this.session.modules,e)===-1?[]:[e],a=0;a1)&&this.again()},D.prototype.answers=function(e,n,t){return this.thread.answers(e,n,t)},X.prototype.answers=function(e,n,t){var s=n||1e3,a=this;if(n<=0){t&&t();return}this.answer(function(l){e(l),l!==!1?setTimeout(function(){a.answers(e,n-1,t)},1):t&&t()})},D.prototype.again=function(e){return this.thread.again(e)},X.prototype.again=function(e){for(var n,t=Date.now();this.__calls.length>0;){for(this.warnings=[],e!==!1&&(this.current_limit=this.session.limit);this.current_limit>0&&this.points.length>0&&this.head_point().goal!==null&&!i.type.is_error(this.head_point().goal);)if(this.current_limit--,this.step()===!0)return;var s=Date.now();this.cpu_time_last=s-t,this.cpu_time+=this.cpu_time_last;var a=this.__calls.shift();this.current_limit<=0?a(null):this.points.length===0?a(!1):i.type.is_error(this.head_point().goal)?(n=this.session.format_error(this.points.pop()),this.points=[],a(n)):(this.debugger&&this.debugger_states.push(this.head_point()),n=this.session.format_success(this.points.pop()),a(n))}},D.prototype.unfold=function(e){if(e.body===null)return!1;var n=e.head,t=e.body,s=t.select(),a=new X(this),l=[];a.add_goal(s),a.step();for(var f=a.points.length-1;f>=0;f--){var y=a.points[f],d=n.apply(y.substitution),m=t.replace(y.goal);m!==null&&(m=m.apply(y.substitution)),l.push(new Q(d,m))}var S=this.rules[n.indicator],P=u(S,e);return l.length>0&&P!==-1?(S.splice.apply(S,[P,1].concat(l)),!0):!1},X.prototype.unfold=function(e){return this.session.unfold(e)},O.prototype.interpret=function(e){return i.error.instantiation(e.level)},E.prototype.interpret=function(e){return this},o.prototype.interpret=function(e){return i.type.is_unitary_list(this)?this.args[0].interpret(e):i.operate(e,this)},O.prototype.compare=function(e){return this.ide.id?1:0},E.prototype.compare=function(e){if(this.value===e.value&&this.is_float===e.is_float)return 0;if(this.valuee.value)return 1},o.prototype.compare=function(e){if(this.args.lengthe.args.length||this.args.length===e.args.length&&this.id>e.id)return 1;for(var n=0;ns)return 1;if(e.constructor===E){if(e.is_float&&n.is_float)return 0;if(e.is_float)return-1;if(n.is_float)return 1}return 0},is_substitution:function(e){return e instanceof Y},is_state:function(e){return e instanceof V},is_rule:function(e){return e instanceof Q},is_variable:function(e){return e instanceof O},is_stream:function(e){return e instanceof ne},is_anonymous_var:function(e){return e instanceof O&&e.id==="_"},is_callable:function(e){return e instanceof o},is_number:function(e){return e instanceof E},is_integer:function(e){return e instanceof E&&!e.is_float},is_float:function(e){return e instanceof E&&e.is_float},is_term:function(e){return e instanceof o},is_atom:function(e){return e instanceof o&&e.args.length===0},is_ground:function(e){if(e instanceof O)return!1;if(e instanceof o){for(var n=0;n0},is_list:function(e){return e instanceof o&&(e.indicator==="[]/0"||e.indicator==="./2")},is_empty_list:function(e){return e instanceof o&&e.indicator==="[]/0"},is_non_empty_list:function(e){return e instanceof o&&e.indicator==="./2"},is_fully_list:function(e){for(;e instanceof o&&e.indicator==="./2";)e=e.args[1];return e instanceof O||e instanceof o&&e.indicator==="[]/0"},is_instantiated_list:function(e){for(;e instanceof o&&e.indicator==="./2";)e=e.args[1];return e instanceof o&&e.indicator==="[]/0"},is_unitary_list:function(e){return e instanceof o&&e.indicator==="./2"&&e.args[1]instanceof o&&e.args[1].indicator==="[]/0"},is_character:function(e){return e instanceof o&&(e.id.length===1||e.id.length>0&&e.id.length<=2&&_(e.id,0)>=65536)},is_character_code:function(e){return e instanceof E&&!e.is_float&&e.value>=0&&e.value<=1114111},is_byte:function(e){return e instanceof E&&!e.is_float&&e.value>=0&&e.value<=255},is_operator:function(e){return e instanceof o&&i.arithmetic.evaluation[e.indicator]},is_directive:function(e){return e instanceof o&&i.directive[e.indicator]!==void 0},is_builtin:function(e){return e instanceof o&&i.predicate[e.indicator]!==void 0},is_error:function(e){return e instanceof o&&e.indicator==="throw/1"},is_predicate_indicator:function(e){return e instanceof o&&e.indicator==="//2"&&e.args[0]instanceof o&&e.args[0].args.length===0&&e.args[1]instanceof E&&e.args[1].is_float===!1},is_flag:function(e){return e instanceof o&&e.args.length===0&&i.flag[e.id]!==void 0},is_value_flag:function(e,n){if(!i.type.is_flag(e))return!1;for(var t in i.flag[e.id].allowed)if(!!i.flag[e.id].allowed.hasOwnProperty(t)&&i.flag[e.id].allowed[t].equals(n))return!0;return!1},is_io_mode:function(e){return i.type.is_atom(e)&&["read","write","append"].indexOf(e.id)!==-1},is_stream_option:function(e){return i.type.is_term(e)&&(e.indicator==="alias/1"&&i.type.is_atom(e.args[0])||e.indicator==="reposition/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="true"||e.args[0].id==="false")||e.indicator==="type/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="text"||e.args[0].id==="binary")||e.indicator==="eof_action/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="error"||e.args[0].id==="eof_code"||e.args[0].id==="reset"))},is_stream_position:function(e){return i.type.is_integer(e)&&e.value>=0||i.type.is_atom(e)&&(e.id==="end_of_stream"||e.id==="past_end_of_stream")},is_stream_property:function(e){return i.type.is_term(e)&&(e.indicator==="input/0"||e.indicator==="output/0"||e.indicator==="alias/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0]))||e.indicator==="file_name/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0]))||e.indicator==="position/1"&&(i.type.is_variable(e.args[0])||i.type.is_stream_position(e.args[0]))||e.indicator==="reposition/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0])&&(e.args[0].id==="true"||e.args[0].id==="false"))||e.indicator==="type/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0])&&(e.args[0].id==="text"||e.args[0].id==="binary"))||e.indicator==="mode/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0])&&(e.args[0].id==="read"||e.args[0].id==="write"||e.args[0].id==="append"))||e.indicator==="eof_action/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0])&&(e.args[0].id==="error"||e.args[0].id==="eof_code"||e.args[0].id==="reset"))||e.indicator==="end_of_stream/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0])&&(e.args[0].id==="at"||e.args[0].id==="past"||e.args[0].id==="not")))},is_streamable:function(e){return e.__proto__.stream!==void 0},is_read_option:function(e){return i.type.is_term(e)&&["variables/1","variable_names/1","singletons/1"].indexOf(e.indicator)!==-1},is_write_option:function(e){return i.type.is_term(e)&&(e.indicator==="quoted/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="true"||e.args[0].id==="false")||e.indicator==="ignore_ops/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="true"||e.args[0].id==="false")||e.indicator==="numbervars/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="true"||e.args[0].id==="false"))},is_close_option:function(e){return i.type.is_term(e)&&e.indicator==="force/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="true"||e.args[0].id==="false")},is_modifiable_flag:function(e){return i.type.is_flag(e)&&i.flag[e.id].changeable},is_module:function(e){return e instanceof o&&e.indicator==="library/1"&&e.args[0]instanceof o&&e.args[0].args.length===0&&i.module[e.args[0].id]!==void 0}},arithmetic:{evaluation:{"e/0":{type_args:null,type_result:!0,fn:function(e){return Math.E}},"pi/0":{type_args:null,type_result:!0,fn:function(e){return Math.PI}},"tau/0":{type_args:null,type_result:!0,fn:function(e){return 2*Math.PI}},"epsilon/0":{type_args:null,type_result:!0,fn:function(e){return Number.EPSILON}},"+/1":{type_args:null,type_result:null,fn:function(e,n){return e}},"-/1":{type_args:null,type_result:null,fn:function(e,n){return-e}},"\\/1":{type_args:!1,type_result:!1,fn:function(e,n){return~e}},"abs/1":{type_args:null,type_result:null,fn:function(e,n){return Math.abs(e)}},"sign/1":{type_args:null,type_result:null,fn:function(e,n){return Math.sign(e)}},"float_integer_part/1":{type_args:!0,type_result:!1,fn:function(e,n){return parseInt(e)}},"float_fractional_part/1":{type_args:!0,type_result:!0,fn:function(e,n){return e-parseInt(e)}},"float/1":{type_args:null,type_result:!0,fn:function(e,n){return parseFloat(e)}},"floor/1":{type_args:!0,type_result:!1,fn:function(e,n){return Math.floor(e)}},"truncate/1":{type_args:!0,type_result:!1,fn:function(e,n){return parseInt(e)}},"round/1":{type_args:!0,type_result:!1,fn:function(e,n){return Math.round(e)}},"ceiling/1":{type_args:!0,type_result:!1,fn:function(e,n){return Math.ceil(e)}},"sin/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.sin(e)}},"cos/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.cos(e)}},"tan/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.tan(e)}},"asin/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.asin(e)}},"acos/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.acos(e)}},"atan/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.atan(e)}},"atan2/2":{type_args:null,type_result:!0,fn:function(e,n,t){return Math.atan2(e,n)}},"exp/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.exp(e)}},"sqrt/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.sqrt(e)}},"log/1":{type_args:null,type_result:!0,fn:function(e,n){return e>0?Math.log(e):i.error.evaluation("undefined",n.__call_indicator)}},"+/2":{type_args:null,type_result:null,fn:function(e,n,t){return e+n}},"-/2":{type_args:null,type_result:null,fn:function(e,n,t){return e-n}},"*/2":{type_args:null,type_result:null,fn:function(e,n,t){return e*n}},"//2":{type_args:null,type_result:!0,fn:function(e,n,t){return n?e/n:i.error.evaluation("zero_division",t.__call_indicator)}},"///2":{type_args:!1,type_result:!1,fn:function(e,n,t){return n?parseInt(e/n):i.error.evaluation("zero_division",t.__call_indicator)}},"**/2":{type_args:null,type_result:!0,fn:function(e,n,t){return Math.pow(e,n)}},"^/2":{type_args:null,type_result:null,fn:function(e,n,t){return Math.pow(e,n)}},"<>/2":{type_args:!1,type_result:!1,fn:function(e,n,t){return e>>n}},"/\\/2":{type_args:!1,type_result:!1,fn:function(e,n,t){return e&n}},"\\//2":{type_args:!1,type_result:!1,fn:function(e,n,t){return e|n}},"xor/2":{type_args:!1,type_result:!1,fn:function(e,n,t){return e^n}},"rem/2":{type_args:!1,type_result:!1,fn:function(e,n,t){return n?e%n:i.error.evaluation("zero_division",t.__call_indicator)}},"mod/2":{type_args:!1,type_result:!1,fn:function(e,n,t){return n?e-parseInt(e/n)*n:i.error.evaluation("zero_division",t.__call_indicator)}},"max/2":{type_args:null,type_result:null,fn:function(e,n,t){return Math.max(e,n)}},"min/2":{type_args:null,type_result:null,fn:function(e,n,t){return Math.min(e,n)}}}},directive:{"dynamic/1":function(e,n){var t=n.args[0];if(i.type.is_variable(t))e.throw_error(i.error.instantiation(n.indicator));else if(!i.type.is_compound(t)||t.indicator!=="//2")e.throw_error(i.error.type("predicate_indicator",t,n.indicator));else if(i.type.is_variable(t.args[0])||i.type.is_variable(t.args[1]))e.throw_error(i.error.instantiation(n.indicator));else if(!i.type.is_atom(t.args[0]))e.throw_error(i.error.type("atom",t.args[0],n.indicator));else if(!i.type.is_integer(t.args[1]))e.throw_error(i.error.type("integer",t.args[1],n.indicator));else{var s=n.args[0].args[0].id+"/"+n.args[0].args[1].value;e.session.public_predicates[s]=!0,e.session.rules[s]||(e.session.rules[s]=[])}},"multifile/1":function(e,n){var t=n.args[0];i.type.is_variable(t)?e.throw_error(i.error.instantiation(n.indicator)):!i.type.is_compound(t)||t.indicator!=="//2"?e.throw_error(i.error.type("predicate_indicator",t,n.indicator)):i.type.is_variable(t.args[0])||i.type.is_variable(t.args[1])?e.throw_error(i.error.instantiation(n.indicator)):i.type.is_atom(t.args[0])?i.type.is_integer(t.args[1])?e.session.multifile_predicates[n.args[0].args[0].id+"/"+n.args[0].args[1].value]=!0:e.throw_error(i.error.type("integer",t.args[1],n.indicator)):e.throw_error(i.error.type("atom",t.args[0],n.indicator))},"set_prolog_flag/2":function(e,n){var t=n.args[0],s=n.args[1];i.type.is_variable(t)||i.type.is_variable(s)?e.throw_error(i.error.instantiation(n.indicator)):i.type.is_atom(t)?i.type.is_flag(t)?i.type.is_value_flag(t,s)?i.type.is_modifiable_flag(t)?e.session.flag[t.id]=s:e.throw_error(i.error.permission("modify","flag",t)):e.throw_error(i.error.domain("flag_value",new o("+",[t,s]),n.indicator)):e.throw_error(i.error.domain("prolog_flag",t,n.indicator)):e.throw_error(i.error.type("atom",t,n.indicator))},"use_module/1":function(e,n){var t=n.args[0];if(i.type.is_variable(t))e.throw_error(i.error.instantiation(n.indicator));else if(!i.type.is_term(t))e.throw_error(i.error.type("term",t,n.indicator));else if(i.type.is_module(t)){var s=t.args[0].id;u(e.session.modules,s)===-1&&e.session.modules.push(s)}},"char_conversion/2":function(e,n){var t=n.args[0],s=n.args[1];i.type.is_variable(t)||i.type.is_variable(s)?e.throw_error(i.error.instantiation(n.indicator)):i.type.is_character(t)?i.type.is_character(s)?t.id===s.id?delete e.session.__char_conversion[t.id]:e.session.__char_conversion[t.id]=s.id:e.throw_error(i.error.type("character",s,n.indicator)):e.throw_error(i.error.type("character",t,n.indicator))},"op/3":function(e,n){var t=n.args[0],s=n.args[1],a=n.args[2];if(i.type.is_variable(t)||i.type.is_variable(s)||i.type.is_variable(a))e.throw_error(i.error.instantiation(n.indicator));else if(!i.type.is_integer(t))e.throw_error(i.error.type("integer",t,n.indicator));else if(!i.type.is_atom(s))e.throw_error(i.error.type("atom",s,n.indicator));else if(!i.type.is_atom(a))e.throw_error(i.error.type("atom",a,n.indicator));else if(t.value<0||t.value>1200)e.throw_error(i.error.domain("operator_priority",t,n.indicator));else if(a.id===",")e.throw_error(i.error.permission("modify","operator",a,n.indicator));else if(a.id==="|"&&(t.value<1001||s.id.length!==3))e.throw_error(i.error.permission("modify","operator",a,n.indicator));else if(["fy","fx","yf","xf","xfx","yfx","xfy"].indexOf(s.id)===-1)e.throw_error(i.error.domain("operator_specifier",s,n.indicator));else{var l={prefix:null,infix:null,postfix:null};for(var f in e.session.__operators)if(!!e.session.__operators.hasOwnProperty(f)){var y=e.session.__operators[f][a.id];y&&(u(y,"fx")!==-1&&(l.prefix={priority:f,type:"fx"}),u(y,"fy")!==-1&&(l.prefix={priority:f,type:"fy"}),u(y,"xf")!==-1&&(l.postfix={priority:f,type:"xf"}),u(y,"yf")!==-1&&(l.postfix={priority:f,type:"yf"}),u(y,"xfx")!==-1&&(l.infix={priority:f,type:"xfx"}),u(y,"xfy")!==-1&&(l.infix={priority:f,type:"xfy"}),u(y,"yfx")!==-1&&(l.infix={priority:f,type:"yfx"}))}var d;switch(s.id){case"fy":case"fx":d="prefix";break;case"yf":case"xf":d="postfix";break;default:d="infix";break}if(((l.prefix&&d==="prefix"||l.postfix&&d==="postfix"||l.infix&&d==="infix")&&l[d].type!==s.id||l.infix&&d==="postfix"||l.postfix&&d==="infix")&&t.value!==0)e.throw_error(i.error.permission("create","operator",a,n.indicator));else return l[d]&&(Fi(e.session.__operators[l[d].priority][a.id],s.id),e.session.__operators[l[d].priority][a.id].length===0&&delete e.session.__operators[l[d].priority][a.id]),t.value>0&&(e.session.__operators[t.value]||(e.session.__operators[t.value.toString()]={}),e.session.__operators[t.value][a.id]||(e.session.__operators[t.value][a.id]=[]),e.session.__operators[t.value][a.id].push(s.id)),!0}}},predicate:{"op/3":function(e,n,t){i.directive["op/3"](e,t)&&e.success(n)},"current_op/3":function(e,n,t){var s=t.args[0],a=t.args[1],l=t.args[2],f=[];for(var y in e.session.__operators)for(var d in e.session.__operators[y])for(var m=0;m/2"){var s=e.points,a=e.session.format_success,l=e.session.format_error;e.session.format_success=function(m){return m.substitution},e.session.format_error=function(m){return m.goal},e.points=[new V(t.args[0].args[0],n.substitution,n)];var f=function(m){e.points=s,e.session.format_success=a,e.session.format_error=l,m===!1?e.prepend([new V(n.goal.replace(t.args[1]),n.substitution,n)]):i.type.is_error(m)?e.throw_error(m.args[0]):m===null?(e.prepend([n]),e.__calls.shift()(null)):e.prepend([new V(n.goal.replace(t.args[0].args[1]).apply(m),n.substitution.apply(m),n)])};e.__calls.unshift(f)}else{var y=new V(n.goal.replace(t.args[0]),n.substitution,n),d=new V(n.goal.replace(t.args[1]),n.substitution,n);e.prepend([y,d])}},"!/0":function(e,n,t){var s,a,l=[];for(s=n,a=null;s.parent!==null&&s.parent.goal.search(t);)if(a=s,s=s.parent,s.goal!==null){var f=s.goal.select();if(f&&f.id==="call"&&f.search(t)){s=a;break}}for(var y=e.points.length-1;y>=0;y--){for(var d=e.points[y],m=d.parent;m!==null&&m!==s.parent;)m=m.parent;m===null&&m!==s.parent&&l.push(d)}e.points=l.reverse(),e.success(n)},"\\+/1":function(e,n,t){var s=t.args[0];i.type.is_variable(s)?e.throw_error(i.error.instantiation(e.level)):i.type.is_callable(s)?e.prepend([new V(n.goal.replace(new o(",",[new o(",",[new o("call",[s]),new o("!",[])]),new o("fail",[])])),n.substitution,n),new V(n.goal.replace(null),n.substitution,n)]):e.throw_error(i.error.type("callable",s,e.level))},"->/2":function(e,n,t){var s=n.goal.replace(new o(",",[t.args[0],new o(",",[new o("!"),t.args[1]])]));e.prepend([new V(s,n.substitution,n)])},"fail/0":function(e,n,t){},"false/0":function(e,n,t){},"true/0":function(e,n,t){e.success(n)},"call/1":ye(1),"call/2":ye(2),"call/3":ye(3),"call/4":ye(4),"call/5":ye(5),"call/6":ye(6),"call/7":ye(7),"call/8":ye(8),"once/1":function(e,n,t){var s=t.args[0];e.prepend([new V(n.goal.replace(new o(",",[new o("call",[s]),new o("!",[])])),n.substitution,n)])},"forall/2":function(e,n,t){var s=t.args[0],a=t.args[1];e.prepend([new V(n.goal.replace(new o("\\+",[new o(",",[new o("call",[s]),new o("\\+",[new o("call",[a])])])])),n.substitution,n)])},"repeat/0":function(e,n,t){e.prepend([new V(n.goal.replace(null),n.substitution,n),n])},"throw/1":function(e,n,t){i.type.is_variable(t.args[0])?e.throw_error(i.error.instantiation(e.level)):e.throw_error(t.args[0])},"catch/3":function(e,n,t){var s=e.points;e.points=[],e.prepend([new V(t.args[0],n.substitution,n)]);var a=e.session.format_success,l=e.session.format_error;e.session.format_success=function(y){return y.substitution},e.session.format_error=function(y){return y.goal};var f=function(y){var d=e.points;if(e.points=s,e.session.format_success=a,e.session.format_error=l,i.type.is_error(y)){for(var m=[],S=e.points.length-1;S>=0;S--){for(var R=e.points[S],P=R.parent;P!==null&&P!==n.parent;)P=P.parent;P===null&&P!==n.parent&&m.push(R)}e.points=m;var A=e.get_flag("occurs_check").indicator==="true/0",R=new V,k=i.unify(y.args[0],t.args[1],A);k!==null?(R.substitution=n.substitution.apply(k),R.goal=n.goal.replace(t.args[2]).apply(k),R.parent=n,e.prepend([R])):e.throw_error(y.args[0])}else if(y!==!1){for(var L=y===null?[]:[new V(n.goal.apply(y).replace(null),n.substitution.apply(y),n)],B=[],S=d.length-1;S>=0;S--){B.push(d[S]);var q=d[S].goal!==null?d[S].goal.select():null;if(i.type.is_term(q)&&q.indicator==="!/0")break}var F=c(B,function(H){return H.goal===null&&(H.goal=new o("true",[])),H=new V(n.goal.replace(new o("catch",[H.goal,t.args[1],t.args[2]])),n.substitution.apply(H.substitution),H.parent),H.exclude=t.args[0].variables(),H}).reverse();e.prepend(F),e.prepend(L),y===null&&(this.current_limit=0,e.__calls.shift()(null))}};e.__calls.unshift(f)},"=/2":function(e,n,t){var s=e.get_flag("occurs_check").indicator==="true/0",a=new V,l=i.unify(t.args[0],t.args[1],s);l!==null&&(a.goal=n.goal.apply(l).replace(null),a.substitution=n.substitution.apply(l),a.parent=n,e.prepend([a]))},"unify_with_occurs_check/2":function(e,n,t){var s=new V,a=i.unify(t.args[0],t.args[1],!0);a!==null&&(s.goal=n.goal.apply(a).replace(null),s.substitution=n.substitution.apply(a),s.parent=n,e.prepend([s]))},"\\=/2":function(e,n,t){var s=e.get_flag("occurs_check").indicator==="true/0",a=i.unify(t.args[0],t.args[1],s);a===null&&e.success(n)},"subsumes_term/2":function(e,n,t){var s=e.get_flag("occurs_check").indicator==="true/0",a=i.unify(t.args[1],t.args[0],s);a!==null&&t.args[1].apply(a).equals(t.args[1])&&e.success(n)},"findall/3":function(e,n,t){var s=t.args[0],a=t.args[1],l=t.args[2];if(i.type.is_variable(a))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(a))e.throw_error(i.error.type("callable",a,t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_list(l))e.throw_error(i.error.type("list",l,t.indicator));else{var f=e.next_free_variable(),y=new o(",",[a,new o("=",[f,s])]),d=e.points,m=e.session.limit,S=e.session.format_success;e.session.format_success=function(R){return R.substitution},e.add_goal(y,!0,n);var P=[],A=function(R){if(R!==!1&&R!==null&&!i.type.is_error(R))e.__calls.unshift(A),P.push(R.links[f.id]),e.session.limit=e.current_limit;else if(e.points=d,e.session.limit=m,e.session.format_success=S,i.type.is_error(R))e.throw_error(R.args[0]);else if(e.current_limit>0){for(var k=new o("[]"),L=P.length-1;L>=0;L--)k=new o(".",[P[L],k]);e.prepend([new V(n.goal.replace(new o("=",[l,k])),n.substitution,n)])}};e.__calls.unshift(A)}},"bagof/3":function(e,n,t){var s,a=t.args[0],l=t.args[1],f=t.args[2];if(i.type.is_variable(l))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(l))e.throw_error(i.error.type("callable",l,t.indicator));else if(!i.type.is_variable(f)&&!i.type.is_list(f))e.throw_error(i.error.type("list",f,t.indicator));else{var y=e.next_free_variable(),d;l.indicator==="^/2"?(d=l.args[0].variables(),l=l.args[1]):d=[],d=d.concat(a.variables());for(var m=l.variables().filter(function(F){return u(d,F)===-1}),S=new o("[]"),P=m.length-1;P>=0;P--)S=new o(".",[new O(m[P]),S]);var A=new o(",",[l,new o("=",[y,new o(",",[S,a])])]),R=e.points,k=e.session.limit,L=e.session.format_success;e.session.format_success=function(F){return F.substitution},e.add_goal(A,!0,n);var B=[],q=function(F){if(F!==!1&&F!==null&&!i.type.is_error(F)){e.__calls.unshift(q);var H=!1,J=F.links[y.id].args[0],me=F.links[y.id].args[1];for(var be in B)if(!!B.hasOwnProperty(be)){var Me=B[be];if(Me.variables.equals(J)){Me.answers.push(me),H=!0;break}}H||B.push({variables:J,answers:[me]}),e.session.limit=e.current_limit}else if(e.points=R,e.session.limit=k,e.session.format_success=L,i.type.is_error(F))e.throw_error(F.args[0]);else if(e.current_limit>0){for(var qe=[],ce=0;ce=0;xe--)Te=new o(".",[F[xe],Te]);qe.push(new V(n.goal.replace(new o(",",[new o("=",[S,B[ce].variables]),new o("=",[f,Te])])),n.substitution,n))}e.prepend(qe)}};e.__calls.unshift(q)}},"setof/3":function(e,n,t){var s,a=t.args[0],l=t.args[1],f=t.args[2];if(i.type.is_variable(l))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(l))e.throw_error(i.error.type("callable",l,t.indicator));else if(!i.type.is_variable(f)&&!i.type.is_list(f))e.throw_error(i.error.type("list",f,t.indicator));else{var y=e.next_free_variable(),d;l.indicator==="^/2"?(d=l.args[0].variables(),l=l.args[1]):d=[],d=d.concat(a.variables());for(var m=l.variables().filter(function(F){return u(d,F)===-1}),S=new o("[]"),P=m.length-1;P>=0;P--)S=new o(".",[new O(m[P]),S]);var A=new o(",",[l,new o("=",[y,new o(",",[S,a])])]),R=e.points,k=e.session.limit,L=e.session.format_success;e.session.format_success=function(F){return F.substitution},e.add_goal(A,!0,n);var B=[],q=function(F){if(F!==!1&&F!==null&&!i.type.is_error(F)){e.__calls.unshift(q);var H=!1,J=F.links[y.id].args[0],me=F.links[y.id].args[1];for(var be in B)if(!!B.hasOwnProperty(be)){var Me=B[be];if(Me.variables.equals(J)){Me.answers.push(me),H=!0;break}}H||B.push({variables:J,answers:[me]}),e.session.limit=e.current_limit}else if(e.points=R,e.session.limit=k,e.session.format_success=L,i.type.is_error(F))e.throw_error(F.args[0]);else if(e.current_limit>0){for(var qe=[],ce=0;ce=0;xe--)Te=new o(".",[F[xe],Te]);qe.push(new V(n.goal.replace(new o(",",[new o("=",[S,B[ce].variables]),new o("=",[f,Te])])),n.substitution,n))}e.prepend(qe)}};e.__calls.unshift(q)}},"functor/3":function(e,n,t){var s,a=t.args[0],l=t.args[1],f=t.args[2];if(i.type.is_variable(a)&&(i.type.is_variable(l)||i.type.is_variable(f)))e.throw_error(i.error.instantiation("functor/3"));else if(!i.type.is_variable(f)&&!i.type.is_integer(f))e.throw_error(i.error.type("integer",t.args[2],"functor/3"));else if(!i.type.is_variable(l)&&!i.type.is_atomic(l))e.throw_error(i.error.type("atomic",t.args[1],"functor/3"));else if(i.type.is_integer(l)&&i.type.is_integer(f)&&f.value!==0)e.throw_error(i.error.type("atom",t.args[1],"functor/3"));else if(i.type.is_variable(a)){if(t.args[2].value>=0){for(var y=[],d=0;d0&&s<=t.args[1].args.length){var a=new o("=",[t.args[1].args[s-1],t.args[2]]);e.prepend([new V(n.goal.replace(a),n.substitution,n)])}}},"=../2":function(e,n,t){var s;if(i.type.is_variable(t.args[0])&&(i.type.is_variable(t.args[1])||i.type.is_non_empty_list(t.args[1])&&i.type.is_variable(t.args[1].args[0])))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_fully_list(t.args[1]))e.throw_error(i.error.type("list",t.args[1],t.indicator));else if(i.type.is_variable(t.args[0])){if(!i.type.is_variable(t.args[1])){var l=[];for(s=t.args[1].args[1];s.indicator==="./2";)l.push(s.args[0]),s=s.args[1];i.type.is_variable(t.args[0])&&i.type.is_variable(s)?e.throw_error(i.error.instantiation(t.indicator)):l.length===0&&i.type.is_compound(t.args[1].args[0])?e.throw_error(i.error.type("atomic",t.args[1].args[0],t.indicator)):l.length>0&&(i.type.is_compound(t.args[1].args[0])||i.type.is_number(t.args[1].args[0]))?e.throw_error(i.error.type("atom",t.args[1].args[0],t.indicator)):l.length===0?e.prepend([new V(n.goal.replace(new o("=",[t.args[1].args[0],t.args[0]],n)),n.substitution,n)]):e.prepend([new V(n.goal.replace(new o("=",[new o(t.args[1].args[0].id,l),t.args[0]])),n.substitution,n)])}}else{if(i.type.is_atomic(t.args[0]))s=new o(".",[t.args[0],new o("[]")]);else{s=new o("[]");for(var a=t.args[0].args.length-1;a>=0;a--)s=new o(".",[t.args[0].args[a],s]);s=new o(".",[new o(t.args[0].id),s])}e.prepend([new V(n.goal.replace(new o("=",[s,t.args[1]])),n.substitution,n)])}},"copy_term/2":function(e,n,t){var s=t.args[0].rename(e);e.prepend([new V(n.goal.replace(new o("=",[s,t.args[1]])),n.substitution,n.parent)])},"term_variables/2":function(e,n,t){var s=t.args[0],a=t.args[1];if(!i.type.is_fully_list(a))e.throw_error(i.error.type("list",a,t.indicator));else{var l=he(c(yr(s.variables()),function(f){return new O(f)}));e.prepend([new V(n.goal.replace(new o("=",[a,l])),n.substitution,n)])}},"clause/2":function(e,n,t){if(i.type.is_variable(t.args[0]))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(t.args[0]))e.throw_error(i.error.type("callable",t.args[0],t.indicator));else if(!i.type.is_variable(t.args[1])&&!i.type.is_callable(t.args[1]))e.throw_error(i.error.type("callable",t.args[1],t.indicator));else if(e.session.rules[t.args[0].indicator]!==void 0)if(e.is_public_predicate(t.args[0].indicator)){var s=[];for(var a in e.session.rules[t.args[0].indicator])if(!!e.session.rules[t.args[0].indicator].hasOwnProperty(a)){var l=e.session.rules[t.args[0].indicator][a];e.session.renamed_variables={},l=l.rename(e),l.body===null&&(l.body=new o("true"));var f=new o(",",[new o("=",[l.head,t.args[0]]),new o("=",[l.body,t.args[1]])]);s.push(new V(n.goal.replace(f),n.substitution,n))}e.prepend(s)}else e.throw_error(i.error.permission("access","private_procedure",t.args[0].indicator,t.indicator))},"current_predicate/1":function(e,n,t){var s=t.args[0];if(!i.type.is_variable(s)&&(!i.type.is_compound(s)||s.indicator!=="//2"))e.throw_error(i.error.type("predicate_indicator",s,t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_variable(s.args[0])&&!i.type.is_atom(s.args[0]))e.throw_error(i.error.type("atom",s.args[0],t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_variable(s.args[1])&&!i.type.is_integer(s.args[1]))e.throw_error(i.error.type("integer",s.args[1],t.indicator));else{var a=[];for(var l in e.session.rules)if(!!e.session.rules.hasOwnProperty(l)){var f=l.lastIndexOf("/"),y=l.substr(0,f),d=parseInt(l.substr(f+1,l.length-(f+1))),m=new o("/",[new o(y),new E(d,!1)]),S=new o("=",[m,s]);a.push(new V(n.goal.replace(S),n.substitution,n))}e.prepend(a)}},"asserta/1":function(e,n,t){if(i.type.is_variable(t.args[0]))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(t.args[0]))e.throw_error(i.error.type("callable",t.args[0],t.indicator));else{var s,a;t.args[0].indicator===":-/2"?(s=t.args[0].args[0],a=ve(t.args[0].args[1])):(s=t.args[0],a=null),i.type.is_callable(s)?a!==null&&!i.type.is_callable(a)?e.throw_error(i.error.type("callable",a,t.indicator)):e.is_public_predicate(s.indicator)?(e.session.rules[s.indicator]===void 0&&(e.session.rules[s.indicator]=[]),e.session.public_predicates[s.indicator]=!0,e.session.rules[s.indicator]=[new Q(s,a,!0)].concat(e.session.rules[s.indicator]),e.success(n)):e.throw_error(i.error.permission("modify","static_procedure",s.indicator,t.indicator)):e.throw_error(i.error.type("callable",s,t.indicator))}},"assertz/1":function(e,n,t){if(i.type.is_variable(t.args[0]))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(t.args[0]))e.throw_error(i.error.type("callable",t.args[0],t.indicator));else{var s,a;t.args[0].indicator===":-/2"?(s=t.args[0].args[0],a=ve(t.args[0].args[1])):(s=t.args[0],a=null),i.type.is_callable(s)?a!==null&&!i.type.is_callable(a)?e.throw_error(i.error.type("callable",a,t.indicator)):e.is_public_predicate(s.indicator)?(e.session.rules[s.indicator]===void 0&&(e.session.rules[s.indicator]=[]),e.session.public_predicates[s.indicator]=!0,e.session.rules[s.indicator].push(new Q(s,a,!0)),e.success(n)):e.throw_error(i.error.permission("modify","static_procedure",s.indicator,t.indicator)):e.throw_error(i.error.type("callable",s,t.indicator))}},"retract/1":function(e,n,t){if(i.type.is_variable(t.args[0]))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(t.args[0]))e.throw_error(i.error.type("callable",t.args[0],t.indicator));else{var s,a;if(t.args[0].indicator===":-/2"?(s=t.args[0].args[0],a=t.args[0].args[1]):(s=t.args[0],a=new o("true")),typeof n.retract=="undefined")if(e.is_public_predicate(s.indicator)){if(e.session.rules[s.indicator]!==void 0){for(var l=[],f=0;fe.get_flag("max_arity").value)e.throw_error(i.error.representation("max_arity",t.indicator));else{var s=t.args[0].args[0].id+"/"+t.args[0].args[1].value;e.is_public_predicate(s)?(delete e.session.rules[s],e.success(n)):e.throw_error(i.error.permission("modify","static_procedure",s,t.indicator))}},"atom_length/2":function(e,n,t){if(i.type.is_variable(t.args[0]))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_atom(t.args[0]))e.throw_error(i.error.type("atom",t.args[0],t.indicator));else if(!i.type.is_variable(t.args[1])&&!i.type.is_integer(t.args[1]))e.throw_error(i.error.type("integer",t.args[1],t.indicator));else if(i.type.is_integer(t.args[1])&&t.args[1].value<0)e.throw_error(i.error.domain("not_less_than_zero",t.args[1],t.indicator));else{var s=new E(t.args[0].id.length,!1);e.prepend([new V(n.goal.replace(new o("=",[s,t.args[1]])),n.substitution,n)])}},"atom_concat/3":function(e,n,t){var s,a,l=t.args[0],f=t.args[1],y=t.args[2];if(i.type.is_variable(y)&&(i.type.is_variable(l)||i.type.is_variable(f)))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_atom(l))e.throw_error(i.error.type("atom",l,t.indicator));else if(!i.type.is_variable(f)&&!i.type.is_atom(f))e.throw_error(i.error.type("atom",f,t.indicator));else if(!i.type.is_variable(y)&&!i.type.is_atom(y))e.throw_error(i.error.type("atom",y,t.indicator));else{var d=i.type.is_variable(l),m=i.type.is_variable(f);if(!d&&!m)a=new o("=",[y,new o(l.id+f.id)]),e.prepend([new V(n.goal.replace(a),n.substitution,n)]);else if(d&&!m)s=y.id.substr(0,y.id.length-f.id.length),s+f.id===y.id&&(a=new o("=",[l,new o(s)]),e.prepend([new V(n.goal.replace(a),n.substitution,n)]));else if(m&&!d)s=y.id.substr(l.id.length),l.id+s===y.id&&(a=new o("=",[f,new o(s)]),e.prepend([new V(n.goal.replace(a),n.substitution,n)]));else{for(var S=[],P=0;P<=y.id.length;P++){var A=new o(y.id.substr(0,P)),R=new o(y.id.substr(P));a=new o(",",[new o("=",[A,l]),new o("=",[R,f])]),S.push(new V(n.goal.replace(a),n.substitution,n))}e.prepend(S)}}},"sub_atom/5":function(e,n,t){var s,a=t.args[0],l=t.args[1],f=t.args[2],y=t.args[3],d=t.args[4];if(i.type.is_variable(a))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_integer(l))e.throw_error(i.error.type("integer",l,t.indicator));else if(!i.type.is_variable(f)&&!i.type.is_integer(f))e.throw_error(i.error.type("integer",f,t.indicator));else if(!i.type.is_variable(y)&&!i.type.is_integer(y))e.throw_error(i.error.type("integer",y,t.indicator));else if(i.type.is_integer(l)&&l.value<0)e.throw_error(i.error.domain("not_less_than_zero",l,t.indicator));else if(i.type.is_integer(f)&&f.value<0)e.throw_error(i.error.domain("not_less_than_zero",f,t.indicator));else if(i.type.is_integer(y)&&y.value<0)e.throw_error(i.error.domain("not_less_than_zero",y,t.indicator));else{var m=[],S=[],P=[];if(i.type.is_variable(l))for(s=0;s<=a.id.length;s++)m.push(s);else m.push(l.value);if(i.type.is_variable(f))for(s=0;s<=a.id.length;s++)S.push(s);else S.push(f.value);if(i.type.is_variable(y))for(s=0;s<=a.id.length;s++)P.push(s);else P.push(y.value);var A=[];for(var R in m)if(!!m.hasOwnProperty(R)){s=m[R];for(var k in S)if(!!S.hasOwnProperty(k)){var L=S[k],B=a.id.length-s-L;if(u(P,B)!==-1&&s+L+B===a.id.length){var q=a.id.substr(s,L);if(a.id===a.id.substr(0,s)+q+a.id.substr(s+L,B)){var F=new o("=",[new o(q),d]),H=new o("=",[l,new E(s)]),J=new o("=",[f,new E(L)]),me=new o("=",[y,new E(B)]),be=new o(",",[new o(",",[new o(",",[H,J]),me]),F]);A.push(new V(n.goal.replace(be),n.substitution,n))}}}}e.prepend(A)}},"atom_chars/2":function(e,n,t){var s=t.args[0],a=t.args[1];if(i.type.is_variable(s)&&i.type.is_variable(a))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_atom(s))e.throw_error(i.error.type("atom",s,t.indicator));else if(i.type.is_variable(s)){for(var y=a,d=i.type.is_variable(s),m="";y.indicator==="./2";){if(i.type.is_character(y.args[0]))m+=y.args[0].id;else if(i.type.is_variable(y.args[0])&&d){e.throw_error(i.error.instantiation(t.indicator));return}else if(!i.type.is_variable(y.args[0])){e.throw_error(i.error.type("character",y.args[0],t.indicator));return}y=y.args[1]}i.type.is_variable(y)&&d?e.throw_error(i.error.instantiation(t.indicator)):!i.type.is_empty_list(y)&&!i.type.is_variable(y)?e.throw_error(i.error.type("list",a,t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[new o(m),s])),n.substitution,n)])}else{for(var l=new o("[]"),f=s.id.length-1;f>=0;f--)l=new o(".",[new o(s.id.charAt(f)),l]);e.prepend([new V(n.goal.replace(new o("=",[a,l])),n.substitution,n)])}},"atom_codes/2":function(e,n,t){var s=t.args[0],a=t.args[1];if(i.type.is_variable(s)&&i.type.is_variable(a))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_atom(s))e.throw_error(i.error.type("atom",s,t.indicator));else if(i.type.is_variable(s)){for(var y=a,d=i.type.is_variable(s),m="";y.indicator==="./2";){if(i.type.is_character_code(y.args[0]))m+=v(y.args[0].value);else if(i.type.is_variable(y.args[0])&&d){e.throw_error(i.error.instantiation(t.indicator));return}else if(!i.type.is_variable(y.args[0])){e.throw_error(i.error.representation("character_code",t.indicator));return}y=y.args[1]}i.type.is_variable(y)&&d?e.throw_error(i.error.instantiation(t.indicator)):!i.type.is_empty_list(y)&&!i.type.is_variable(y)?e.throw_error(i.error.type("list",a,t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[new o(m),s])),n.substitution,n)])}else{for(var l=new o("[]"),f=s.id.length-1;f>=0;f--)l=new o(".",[new E(_(s.id,f),!1),l]);e.prepend([new V(n.goal.replace(new o("=",[a,l])),n.substitution,n)])}},"char_code/2":function(e,n,t){var s=t.args[0],a=t.args[1];if(i.type.is_variable(s)&&i.type.is_variable(a))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_character(s))e.throw_error(i.error.type("character",s,t.indicator));else if(!i.type.is_variable(a)&&!i.type.is_integer(a))e.throw_error(i.error.type("integer",a,t.indicator));else if(!i.type.is_variable(a)&&!i.type.is_character_code(a))e.throw_error(i.error.representation("character_code",t.indicator));else if(i.type.is_variable(a)){var l=new E(_(s.id,0),!1);e.prepend([new V(n.goal.replace(new o("=",[l,a])),n.substitution,n)])}else{var f=new o(v(a.value));e.prepend([new V(n.goal.replace(new o("=",[f,s])),n.substitution,n)])}},"number_chars/2":function(e,n,t){var s,a=t.args[0],l=t.args[1];if(i.type.is_variable(a)&&i.type.is_variable(l))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(a)&&!i.type.is_number(a))e.throw_error(i.error.type("number",a,t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_list(l))e.throw_error(i.error.type("list",l,t.indicator));else{var f=i.type.is_variable(a);if(!i.type.is_variable(l)){var y=l,d=!0;for(s="";y.indicator==="./2";){if(i.type.is_character(y.args[0]))s+=y.args[0].id;else if(i.type.is_variable(y.args[0]))d=!1;else if(!i.type.is_variable(y.args[0])){e.throw_error(i.error.type("character",y.args[0],t.indicator));return}y=y.args[1]}if(d=d&&i.type.is_empty_list(y),!i.type.is_empty_list(y)&&!i.type.is_variable(y)){e.throw_error(i.error.type("list",l,t.indicator));return}if(!d&&f){e.throw_error(i.error.instantiation(t.indicator));return}else if(d)if(i.type.is_variable(y)&&f){e.throw_error(i.error.instantiation(t.indicator));return}else{var m=e.parse(s),S=m.value;!i.type.is_number(S)||m.tokens[m.tokens.length-1].space?e.throw_error(i.error.syntax_by_predicate("parseable_number",t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[a,S])),n.substitution,n)]);return}}if(!f){s=a.toString();for(var P=new o("[]"),A=s.length-1;A>=0;A--)P=new o(".",[new o(s.charAt(A)),P]);e.prepend([new V(n.goal.replace(new o("=",[l,P])),n.substitution,n)])}}},"number_codes/2":function(e,n,t){var s,a=t.args[0],l=t.args[1];if(i.type.is_variable(a)&&i.type.is_variable(l))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(a)&&!i.type.is_number(a))e.throw_error(i.error.type("number",a,t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_list(l))e.throw_error(i.error.type("list",l,t.indicator));else{var f=i.type.is_variable(a);if(!i.type.is_variable(l)){var y=l,d=!0;for(s="";y.indicator==="./2";){if(i.type.is_character_code(y.args[0]))s+=v(y.args[0].value);else if(i.type.is_variable(y.args[0]))d=!1;else if(!i.type.is_variable(y.args[0])){e.throw_error(i.error.type("character_code",y.args[0],t.indicator));return}y=y.args[1]}if(d=d&&i.type.is_empty_list(y),!i.type.is_empty_list(y)&&!i.type.is_variable(y)){e.throw_error(i.error.type("list",l,t.indicator));return}if(!d&&f){e.throw_error(i.error.instantiation(t.indicator));return}else if(d)if(i.type.is_variable(y)&&f){e.throw_error(i.error.instantiation(t.indicator));return}else{var m=e.parse(s),S=m.value;!i.type.is_number(S)||m.tokens[m.tokens.length-1].space?e.throw_error(i.error.syntax_by_predicate("parseable_number",t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[a,S])),n.substitution,n)]);return}}if(!f){s=a.toString();for(var P=new o("[]"),A=s.length-1;A>=0;A--)P=new o(".",[new E(_(s,A),!1),P]);e.prepend([new V(n.goal.replace(new o("=",[l,P])),n.substitution,n)])}}},"upcase_atom/2":function(e,n,t){var s=t.args[0],a=t.args[1];i.type.is_variable(s)?e.throw_error(i.error.instantiation(t.indicator)):i.type.is_atom(s)?!i.type.is_variable(a)&&!i.type.is_atom(a)?e.throw_error(i.error.type("atom",a,t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[a,new o(s.id.toUpperCase(),[])])),n.substitution,n)]):e.throw_error(i.error.type("atom",s,t.indicator))},"downcase_atom/2":function(e,n,t){var s=t.args[0],a=t.args[1];i.type.is_variable(s)?e.throw_error(i.error.instantiation(t.indicator)):i.type.is_atom(s)?!i.type.is_variable(a)&&!i.type.is_atom(a)?e.throw_error(i.error.type("atom",a,t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[a,new o(s.id.toLowerCase(),[])])),n.substitution,n)]):e.throw_error(i.error.type("atom",s,t.indicator))},"atomic_list_concat/2":function(e,n,t){var s=t.args[0],a=t.args[1];e.prepend([new V(n.goal.replace(new o("atomic_list_concat",[s,new o("",[]),a])),n.substitution,n)])},"atomic_list_concat/3":function(e,n,t){var s=t.args[0],a=t.args[1],l=t.args[2];if(i.type.is_variable(a)||i.type.is_variable(s)&&i.type.is_variable(l))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_list(s))e.throw_error(i.error.type("list",s,t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_atom(l))e.throw_error(i.error.type("atom",l,t.indicator));else if(i.type.is_variable(l)){for(var y="",d=s;i.type.is_term(d)&&d.indicator==="./2";){if(!i.type.is_atom(d.args[0])&&!i.type.is_number(d.args[0])){e.throw_error(i.error.type("atomic",d.args[0],t.indicator));return}y!==""&&(y+=a.id),i.type.is_atom(d.args[0])?y+=d.args[0].id:y+=""+d.args[0].value,d=d.args[1]}y=new o(y,[]),i.type.is_variable(d)?e.throw_error(i.error.instantiation(t.indicator)):!i.type.is_term(d)||d.indicator!=="[]/0"?e.throw_error(i.error.type("list",s,t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[y,l])),n.substitution,n)])}else{var f=he(c(l.id.split(a.id),function(m){return new o(m,[])}));e.prepend([new V(n.goal.replace(new o("=",[f,s])),n.substitution,n)])}},"@=/2":function(e,n,t){i.compare(t.args[0],t.args[1])>0&&e.success(n)},"@>=/2":function(e,n,t){i.compare(t.args[0],t.args[1])>=0&&e.success(n)},"compare/3":function(e,n,t){var s=t.args[0],a=t.args[1],l=t.args[2];if(!i.type.is_variable(s)&&!i.type.is_atom(s))e.throw_error(i.error.type("atom",s,t.indicator));else if(i.type.is_atom(s)&&["<",">","="].indexOf(s.id)===-1)e.throw_error(i.type.domain("order",s,t.indicator));else{var f=i.compare(a,l);f=f===0?"=":f===-1?"<":">",e.prepend([new V(n.goal.replace(new o("=",[s,new o(f,[])])),n.substitution,n)])}},"is/2":function(e,n,t){var s=t.args[1].interpret(e);i.type.is_number(s)?e.prepend([new V(n.goal.replace(new o("=",[t.args[0],s],e.level)),n.substitution,n)]):e.throw_error(s)},"between/3":function(e,n,t){var s=t.args[0],a=t.args[1],l=t.args[2];if(i.type.is_variable(s)||i.type.is_variable(a))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_integer(s))e.throw_error(i.error.type("integer",s,t.indicator));else if(!i.type.is_integer(a))e.throw_error(i.error.type("integer",a,t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_integer(l))e.throw_error(i.error.type("integer",l,t.indicator));else if(i.type.is_variable(l)){var f=[new V(n.goal.replace(new o("=",[l,s])),n.substitution,n)];s.value=l.value&&e.success(n)},"succ/2":function(e,n,t){var s=t.args[0],a=t.args[1];i.type.is_variable(s)&&i.type.is_variable(a)?e.throw_error(i.error.instantiation(t.indicator)):!i.type.is_variable(s)&&!i.type.is_integer(s)?e.throw_error(i.error.type("integer",s,t.indicator)):!i.type.is_variable(a)&&!i.type.is_integer(a)?e.throw_error(i.error.type("integer",a,t.indicator)):!i.type.is_variable(s)&&s.value<0?e.throw_error(i.error.domain("not_less_than_zero",s,t.indicator)):!i.type.is_variable(a)&&a.value<0?e.throw_error(i.error.domain("not_less_than_zero",a,t.indicator)):(i.type.is_variable(a)||a.value>0)&&(i.type.is_variable(s)?e.prepend([new V(n.goal.replace(new o("=",[s,new E(a.value-1,!1)])),n.substitution,n)]):e.prepend([new V(n.goal.replace(new o("=",[a,new E(s.value+1,!1)])),n.substitution,n)]))},"=:=/2":function(e,n,t){var s=i.arithmetic_compare(e,t.args[0],t.args[1]);i.type.is_term(s)?e.throw_error(s):s===0&&e.success(n)},"=\\=/2":function(e,n,t){var s=i.arithmetic_compare(e,t.args[0],t.args[1]);i.type.is_term(s)?e.throw_error(s):s!==0&&e.success(n)},"/2":function(e,n,t){var s=i.arithmetic_compare(e,t.args[0],t.args[1]);i.type.is_term(s)?e.throw_error(s):s>0&&e.success(n)},">=/2":function(e,n,t){var s=i.arithmetic_compare(e,t.args[0],t.args[1]);i.type.is_term(s)?e.throw_error(s):s>=0&&e.success(n)},"var/1":function(e,n,t){i.type.is_variable(t.args[0])&&e.success(n)},"atom/1":function(e,n,t){i.type.is_atom(t.args[0])&&e.success(n)},"atomic/1":function(e,n,t){i.type.is_atomic(t.args[0])&&e.success(n)},"compound/1":function(e,n,t){i.type.is_compound(t.args[0])&&e.success(n)},"integer/1":function(e,n,t){i.type.is_integer(t.args[0])&&e.success(n)},"float/1":function(e,n,t){i.type.is_float(t.args[0])&&e.success(n)},"number/1":function(e,n,t){i.type.is_number(t.args[0])&&e.success(n)},"nonvar/1":function(e,n,t){i.type.is_variable(t.args[0])||e.success(n)},"ground/1":function(e,n,t){t.variables().length===0&&e.success(n)},"acyclic_term/1":function(e,n,t){for(var s=n.substitution.apply(n.substitution),a=t.args[0].variables(),l=0;l0?k[k.length-1]:null,k!==null&&(A=U(e,k,0,e.__get_max_priority(),!1))}if(A.type===h&&A.len===k.length-1&&L.value==="."){A=A.value.rename(e);var B=new o("=",[a,A]);if(y.variables){var q=he(c(yr(A.variables()),function(F){return new O(F)}));B=new o(",",[B,new o("=",[y.variables,q])])}if(y.variable_names){var q=he(c(yr(A.variables()),function(H){var J;for(J in e.session.renamed_variables)if(e.session.renamed_variables.hasOwnProperty(J)&&e.session.renamed_variables[J]===H)break;return new o("=",[new o(J,[]),new O(H)])}));B=new o(",",[B,new o("=",[y.variable_names,q])])}if(y.singletons){var q=he(c(new Q(A,null).singleton_variables(),function(H){var J;for(J in e.session.renamed_variables)if(e.session.renamed_variables.hasOwnProperty(J)&&e.session.renamed_variables[J]===H)break;return new o("=",[new o(J,[]),new O(H)])}));B=new o(",",[B,new o("=",[y.singletons,q])])}e.prepend([new V(n.goal.replace(B),n.substitution,n)])}else A.type===h?e.throw_error(i.error.syntax(k[A.len],"unexpected token",!1)):e.throw_error(A.value)}}},"write/1":function(e,n,t){var s=t.args[0];e.prepend([new V(n.goal.replace(new o(",",[new o("current_output",[new O("S")]),new o("write",[new O("S"),s])])),n.substitution,n)])},"write/2":function(e,n,t){var s=t.args[0],a=t.args[1];e.prepend([new V(n.goal.replace(new o("write_term",[s,a,new o(".",[new o("quoted",[new o("false",[])]),new o(".",[new o("ignore_ops",[new o("false")]),new o(".",[new o("numbervars",[new o("true")]),new o("[]",[])])])])])),n.substitution,n)])},"writeq/1":function(e,n,t){var s=t.args[0];e.prepend([new V(n.goal.replace(new o(",",[new o("current_output",[new O("S")]),new o("writeq",[new O("S"),s])])),n.substitution,n)])},"writeq/2":function(e,n,t){var s=t.args[0],a=t.args[1];e.prepend([new V(n.goal.replace(new o("write_term",[s,a,new o(".",[new o("quoted",[new o("true",[])]),new o(".",[new o("ignore_ops",[new o("false")]),new o(".",[new o("numbervars",[new o("true")]),new o("[]",[])])])])])),n.substitution,n)])},"write_canonical/1":function(e,n,t){var s=t.args[0];e.prepend([new V(n.goal.replace(new o(",",[new o("current_output",[new O("S")]),new o("write_canonical",[new O("S"),s])])),n.substitution,n)])},"write_canonical/2":function(e,n,t){var s=t.args[0],a=t.args[1];e.prepend([new V(n.goal.replace(new o("write_term",[s,a,new o(".",[new o("quoted",[new o("true",[])]),new o(".",[new o("ignore_ops",[new o("true")]),new o(".",[new o("numbervars",[new o("false")]),new o("[]",[])])])])])),n.substitution,n)])},"write_term/2":function(e,n,t){var s=t.args[0],a=t.args[1];e.prepend([new V(n.goal.replace(new o(",",[new o("current_output",[new O("S")]),new o("write_term",[new O("S"),s,a])])),n.substitution,n)])},"write_term/3":function(e,n,t){var s=t.args[0],a=t.args[1],l=t.args[2],f=i.type.is_stream(s)?s:e.get_stream_by_alias(s.id);if(i.type.is_variable(s)||i.type.is_variable(l))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_list(l))e.throw_error(i.error.type("list",l,t.indicator));else if(!i.type.is_stream(s)&&!i.type.is_atom(s))e.throw_error(i.error.domain("stream_or_alias",s,t.indicator));else if(!i.type.is_stream(f)||f.stream===null)e.throw_error(i.error.existence("stream",s,t.indicator));else if(f.input)e.throw_error(i.error.permission("output","stream",s,t.indicator));else if(f.type==="binary")e.throw_error(i.error.permission("output","binary_stream",s,t.indicator));else if(f.position==="past_end_of_stream"&&f.eof_action==="error")e.throw_error(i.error.permission("output","past_end_of_stream",s,t.indicator));else{for(var y={},d=l,m;i.type.is_term(d)&&d.indicator==="./2";){if(m=d.args[0],i.type.is_variable(m)){e.throw_error(i.error.instantiation(t.indicator));return}else if(!i.type.is_write_option(m)){e.throw_error(i.error.domain("write_option",m,t.indicator));return}y[m.id]=m.args[0].id==="true",d=d.args[1]}if(d.indicator!=="[]/0"){i.type.is_variable(d)?e.throw_error(i.error.instantiation(t.indicator)):e.throw_error(i.error.type("list",l,t.indicator));return}else{y.session=e.session;var S=a.toString(y);f.stream.put(S,f.position),typeof f.position=="number"&&(f.position+=S.length),e.success(n)}}},"halt/0":function(e,n,t){e.points=[]},"halt/1":function(e,n,t){var s=t.args[0];i.type.is_variable(s)?e.throw_error(i.error.instantiation(t.indicator)):i.type.is_integer(s)?e.points=[]:e.throw_error(i.error.type("integer",s,t.indicator))},"current_prolog_flag/2":function(e,n,t){var s=t.args[0],a=t.args[1];if(!i.type.is_variable(s)&&!i.type.is_atom(s))e.throw_error(i.error.type("atom",s,t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_flag(s))e.throw_error(i.error.domain("prolog_flag",s,t.indicator));else{var l=[];for(var f in i.flag)if(!!i.flag.hasOwnProperty(f)){var y=new o(",",[new o("=",[new o(f),s]),new o("=",[e.get_flag(f),a])]);l.push(new V(n.goal.replace(y),n.substitution,n))}e.prepend(l)}},"set_prolog_flag/2":function(e,n,t){var s=t.args[0],a=t.args[1];i.type.is_variable(s)||i.type.is_variable(a)?e.throw_error(i.error.instantiation(t.indicator)):i.type.is_atom(s)?i.type.is_flag(s)?i.type.is_value_flag(s,a)?i.type.is_modifiable_flag(s)?(e.session.flag[s.id]=a,e.success(n)):e.throw_error(i.error.permission("modify","flag",s)):e.throw_error(i.error.domain("flag_value",new o("+",[s,a]),t.indicator)):e.throw_error(i.error.domain("prolog_flag",s,t.indicator)):e.throw_error(i.error.type("atom",s,t.indicator))}},flag:{bounded:{allowed:[new o("true"),new o("false")],value:new o("true"),changeable:!1},max_integer:{allowed:[new E(Number.MAX_SAFE_INTEGER)],value:new E(Number.MAX_SAFE_INTEGER),changeable:!1},min_integer:{allowed:[new E(Number.MIN_SAFE_INTEGER)],value:new E(Number.MIN_SAFE_INTEGER),changeable:!1},integer_rounding_function:{allowed:[new o("down"),new o("toward_zero")],value:new o("toward_zero"),changeable:!1},char_conversion:{allowed:[new o("on"),new o("off")],value:new o("on"),changeable:!0},debug:{allowed:[new o("on"),new o("off")],value:new o("off"),changeable:!0},max_arity:{allowed:[new o("unbounded")],value:new o("unbounded"),changeable:!1},unknown:{allowed:[new o("error"),new o("fail"),new o("warning")],value:new o("error"),changeable:!0},double_quotes:{allowed:[new o("chars"),new o("codes"),new o("atom")],value:new o("codes"),changeable:!0},occurs_check:{allowed:[new o("false"),new o("true")],value:new o("false"),changeable:!0},dialect:{allowed:[new o("tau")],value:new o("tau"),changeable:!1},version_data:{allowed:[new o("tau",[new E(r.major,!1),new E(r.minor,!1),new E(r.patch,!1),new o(r.status)])],value:new o("tau",[new E(r.major,!1),new E(r.minor,!1),new E(r.patch,!1),new o(r.status)]),changeable:!1},nodejs:{allowed:[new o("yes"),new o("no")],value:new o(typeof ie!="undefined"&&ie.exports?"yes":"no"),changeable:!1}},unify:function(e,n,t){t=t===void 0?!1:t;for(var s=[{left:e,right:n}],a={};s.length!==0;){var l=s.pop();if(e=l.left,n=l.right,i.type.is_term(e)&&i.type.is_term(n)){if(e.indicator!==n.indicator)return null;for(var f=0;fa.value?1:0:a}else return s},operate:function(e,n){if(i.type.is_operator(n)){for(var t=i.type.is_operator(n),s=[],a,l=!1,f=0;fe.get_flag("max_integer").value||a0?e.start+e.matches[0].length:e.start,a=t?new o("token_not_found"):new o("found",[new o(e.value.toString())]),l=new o(".",[new o("line",[new E(e.line+1)]),new o(".",[new o("column",[new E(s+1)]),new o(".",[a,new o("[]",[])])])]);return new o("error",[new o("syntax_error",[new o(n)]),l])},syntax_by_predicate:function(e,n){return new o("error",[new o("syntax_error",[new o(e)]),ae(n)])}},warning:{singleton:function(e,n,t){for(var s=new o("[]"),a=e.length-1;a>=0;a--)s=new o(".",[new O(e[a]),s]);return new o("warning",[new o("singleton_variables",[s,ae(n)]),new o(".",[new o("line",[new E(t,!1)]),new o("[]")])])},failed_goal:function(e,n){return new o("warning",[new o("failed_goal",[e]),new o(".",[new o("line",[new E(n,!1)]),new o("[]")])])}},format_variable:function(e){return"_"+e},format_answer:function(e,n,t){n instanceof D&&(n=n.thread);var t=t||{};if(t.session=n?n.session:void 0,i.type.is_error(e))return"uncaught exception: "+e.args[0].toString();if(e===!1)return"false.";if(e===null)return"limit exceeded ;";var s=0,a="";if(i.type.is_substitution(e)){var l=e.domain(!0);e=e.filter(function(d,m){return!i.type.is_variable(m)||l.indexOf(m.id)!==-1&&d!==m.id})}for(var f in e.links)!e.links.hasOwnProperty(f)||(s++,a!==""&&(a+=", "),a+=f.toString(t)+" = "+e.links[f].toString(t));var y=typeof n=="undefined"||n.points.length>0?" ;":".";return s===0?"true"+y:a+y},flatten_error:function(e){if(!i.type.is_error(e))return null;e=e.args[0];var n={};return n.type=e.args[0].id,n.thrown=n.type==="syntax_error"?null:e.args[1].id,n.expected=null,n.found=null,n.representation=null,n.existence=null,n.existence_type=null,n.line=null,n.column=null,n.permission_operation=null,n.permission_type=null,n.evaluation_type=null,n.type==="type_error"||n.type==="domain_error"?(n.expected=e.args[0].args[0].id,n.found=e.args[0].args[1].toString()):n.type==="syntax_error"?e.args[1].indicator==="./2"?(n.expected=e.args[0].args[0].id,n.found=e.args[1].args[1].args[1].args[0],n.found=n.found.id==="token_not_found"?n.found.id:n.found.args[0].id,n.line=e.args[1].args[0].args[0].value,n.column=e.args[1].args[1].args[0].args[0].value):n.thrown=e.args[1].id:n.type==="permission_error"?(n.found=e.args[0].args[2].toString(),n.permission_operation=e.args[0].args[0].id,n.permission_type=e.args[0].args[1].id):n.type==="evaluation_error"?n.evaluation_type=e.args[0].args[0].id:n.type==="representation_error"?n.representation=e.args[0].args[0].id:n.type==="existence_error"&&(n.existence=e.args[0].args[1].toString(),n.existence_type=e.args[0].args[0].id),n},create:function(e){return new i.type.Session(e)}};typeof ie!="undefined"?ie.exports=i:window.pl=i})()});var er=I((qu,rt)=>{var is=Array.isArray;rt.exports=is});var nt=I(($u,tt)=>{var ss=typeof global=="object"&&global&&global.Object===Object&&global;tt.exports=ss});var rr=I((Du,it)=>{var as=nt(),os=typeof self=="object"&&self&&self.Object===Object&&self,us=as||os||Function("return this")();it.exports=us});var tr=I((Xu,st)=>{var ls=rr(),cs=ls.Symbol;st.exports=cs});var lt=I((Bu,at)=>{var ot=tr(),ut=Object.prototype,fs=ut.hasOwnProperty,ps=ut.toString,Xe=ot?ot.toStringTag:void 0;function ys(r){var u=fs.call(r,Xe),p=r[Xe];try{r[Xe]=void 0;var c=!0}catch(_){}var w=ps.call(r);return c&&(u?r[Xe]=p:delete r[Xe]),w}at.exports=ys});var ft=I((Fu,ct)=>{var _s=Object.prototype,ws=_s.toString;function gs(r){return ws.call(r)}ct.exports=gs});var Pr=I((zu,pt)=>{var yt=tr(),ds=lt(),vs=ft(),hs="[object Null]",ms="[object Undefined]",_t=yt?yt.toStringTag:void 0;function bs(r){return r==null?r===void 0?ms:hs:_t&&_t in Object(r)?ds(r):vs(r)}pt.exports=bs});var gt=I((Wu,wt)=>{function Ts(r){return r!=null&&typeof r=="object"}wt.exports=Ts});var nr=I((Lu,dt)=>{var xs=Pr(),Vs=gt(),Ss="[object Symbol]";function ks(r){return typeof r=="symbol"||Vs(r)&&xs(r)==Ss}dt.exports=ks});var ht=I((Hu,vt)=>{var Ps=er(),Cs=nr(),Os=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,Is=/^\w*$/;function Es(r,u){if(Ps(r))return!1;var p=typeof r;return p=="number"||p=="symbol"||p=="boolean"||r==null||Cs(r)?!0:Is.test(r)||!Os.test(r)||u!=null&&r in Object(u)}vt.exports=Es});var ir=I((Gu,mt)=>{function As(r){var u=typeof r;return r!=null&&(u=="object"||u=="function")}mt.exports=As});var Tt=I((Yu,bt)=>{var Ns=Pr(),Rs=ir(),Ms="[object AsyncFunction]",qs="[object Function]",$s="[object GeneratorFunction]",Ds="[object Proxy]";function Xs(r){if(!Rs(r))return!1;var u=Ns(r);return u==qs||u==$s||u==Ms||u==Ds}bt.exports=Xs});var Vt=I((Uu,xt)=>{var Bs=rr(),Fs=Bs["__core-js_shared__"];xt.exports=Fs});var Pt=I((Zu,St)=>{var Cr=Vt(),kt=function(){var r=/[^.]+$/.exec(Cr&&Cr.keys&&Cr.keys.IE_PROTO||"");return r?"Symbol(src)_1."+r:""}();function zs(r){return!!kt&&kt in r}St.exports=zs});var Ot=I((Qu,Ct)=>{var Ws=Function.prototype,Ls=Ws.toString;function Hs(r){if(r!=null){try{return Ls.call(r)}catch(u){}try{return r+""}catch(u){}}return""}Ct.exports=Hs});var Et=I((Ju,It)=>{var Gs=Tt(),Ys=Pt(),Us=ir(),Zs=Ot(),Qs=/[\\^$.*+?()[\]{}|]/g,Js=/^\[object .+?Constructor\]$/,Ks=Function.prototype,js=Object.prototype,ea=Ks.toString,ra=js.hasOwnProperty,ta=RegExp("^"+ea.call(ra).replace(Qs,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function na(r){if(!Us(r)||Ys(r))return!1;var u=Gs(r)?ta:Js;return u.test(Zs(r))}It.exports=na});var Nt=I((Ku,At)=>{function ia(r,u){return r==null?void 0:r[u]}At.exports=ia});var sr=I((ju,Rt)=>{var sa=Et(),aa=Nt();function oa(r,u){var p=aa(r,u);return sa(p)?p:void 0}Rt.exports=oa});var Be=I((el,Mt)=>{var ua=sr(),la=ua(Object,"create");Mt.exports=la});var Dt=I((rl,qt)=>{var $t=Be();function ca(){this.__data__=$t?$t(null):{},this.size=0}qt.exports=ca});var Bt=I((tl,Xt)=>{function fa(r){var u=this.has(r)&&delete this.__data__[r];return this.size-=u?1:0,u}Xt.exports=fa});var zt=I((nl,Ft)=>{var pa=Be(),ya="__lodash_hash_undefined__",_a=Object.prototype,wa=_a.hasOwnProperty;function ga(r){var u=this.__data__;if(pa){var p=u[r];return p===ya?void 0:p}return wa.call(u,r)?u[r]:void 0}Ft.exports=ga});var Lt=I((il,Wt)=>{var da=Be(),va=Object.prototype,ha=va.hasOwnProperty;function ma(r){var u=this.__data__;return da?u[r]!==void 0:ha.call(u,r)}Wt.exports=ma});var Gt=I((sl,Ht)=>{var ba=Be(),Ta="__lodash_hash_undefined__";function xa(r,u){var p=this.__data__;return this.size+=this.has(r)?0:1,p[r]=ba&&u===void 0?Ta:u,this}Ht.exports=xa});var Ut=I((al,Yt)=>{var Va=Dt(),Sa=Bt(),ka=zt(),Pa=Lt(),Ca=Gt();function Ie(r){var u=-1,p=r==null?0:r.length;for(this.clear();++u{function Oa(){this.__data__=[],this.size=0}Zt.exports=Oa});var Or=I((ul,Jt)=>{function Ia(r,u){return r===u||r!==r&&u!==u}Jt.exports=Ia});var Fe=I((ll,Kt)=>{var Ea=Or();function Aa(r,u){for(var p=r.length;p--;)if(Ea(r[p][0],u))return p;return-1}Kt.exports=Aa});var en=I((cl,jt)=>{var Na=Fe(),Ra=Array.prototype,Ma=Ra.splice;function qa(r){var u=this.__data__,p=Na(u,r);if(p<0)return!1;var c=u.length-1;return p==c?u.pop():Ma.call(u,p,1),--this.size,!0}jt.exports=qa});var tn=I((fl,rn)=>{var $a=Fe();function Da(r){var u=this.__data__,p=$a(u,r);return p<0?void 0:u[p][1]}rn.exports=Da});var sn=I((pl,nn)=>{var Xa=Fe();function Ba(r){return Xa(this.__data__,r)>-1}nn.exports=Ba});var on=I((yl,an)=>{var Fa=Fe();function za(r,u){var p=this.__data__,c=Fa(p,r);return c<0?(++this.size,p.push([r,u])):p[c][1]=u,this}an.exports=za});var ln=I((_l,un)=>{var Wa=Qt(),La=en(),Ha=tn(),Ga=sn(),Ya=on();function Ee(r){var u=-1,p=r==null?0:r.length;for(this.clear();++u{var Ua=sr(),Za=rr(),Qa=Ua(Za,"Map");cn.exports=Qa});var _n=I((gl,pn)=>{var yn=Ut(),Ja=ln(),Ka=fn();function ja(){this.size=0,this.__data__={hash:new yn,map:new(Ka||Ja),string:new yn}}pn.exports=ja});var gn=I((dl,wn)=>{function eo(r){var u=typeof r;return u=="string"||u=="number"||u=="symbol"||u=="boolean"?r!=="__proto__":r===null}wn.exports=eo});var ze=I((vl,dn)=>{var ro=gn();function to(r,u){var p=r.__data__;return ro(u)?p[typeof u=="string"?"string":"hash"]:p.map}dn.exports=to});var hn=I((hl,vn)=>{var no=ze();function io(r){var u=no(this,r).delete(r);return this.size-=u?1:0,u}vn.exports=io});var bn=I((ml,mn)=>{var so=ze();function ao(r){return so(this,r).get(r)}mn.exports=ao});var xn=I((bl,Tn)=>{var oo=ze();function uo(r){return oo(this,r).has(r)}Tn.exports=uo});var Sn=I((Tl,Vn)=>{var lo=ze();function co(r,u){var p=lo(this,r),c=p.size;return p.set(r,u),this.size+=p.size==c?0:1,this}Vn.exports=co});var Pn=I((xl,kn)=>{var fo=_n(),po=hn(),yo=bn(),_o=xn(),wo=Sn();function Ae(r){var u=-1,p=r==null?0:r.length;for(this.clear();++u{var On=Pn(),go="Expected a function";function Ir(r,u){if(typeof r!="function"||u!=null&&typeof u!="function")throw new TypeError(go);var p=function(){var c=arguments,w=u?u.apply(this,c):c[0],_=p.cache;if(_.has(w))return _.get(w);var v=r.apply(this,c);return p.cache=_.set(w,v)||_,v};return p.cache=new(Ir.Cache||On),p}Ir.Cache=On;Cn.exports=Ir});var An=I((Sl,En)=>{var vo=In(),ho=500;function mo(r){var u=vo(r,function(c){return p.size===ho&&p.clear(),c}),p=u.cache;return u}En.exports=mo});var Rn=I((kl,Nn)=>{var bo=An(),To=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,xo=/\\(\\)?/g,Vo=bo(function(r){var u=[];return r.charCodeAt(0)===46&&u.push(""),r.replace(To,function(p,c,w,_){u.push(w?_.replace(xo,"$1"):c||p)}),u});Nn.exports=Vo});var qn=I((Pl,Mn)=>{function So(r,u){for(var p=-1,c=r==null?0:r.length,w=Array(c);++p{var Dn=tr(),ko=qn(),Po=er(),Co=nr(),Oo=1/0,Xn=Dn?Dn.prototype:void 0,Bn=Xn?Xn.toString:void 0;function Fn(r){if(typeof r=="string")return r;if(Po(r))return ko(r,Fn)+"";if(Co(r))return Bn?Bn.call(r):"";var u=r+"";return u=="0"&&1/r==-Oo?"-0":u}$n.exports=Fn});var Ln=I((Ol,Wn)=>{var Io=zn();function Eo(r){return r==null?"":Io(r)}Wn.exports=Eo});var ar=I((Il,Hn)=>{var Ao=er(),No=ht(),Ro=Rn(),Mo=Ln();function qo(r,u){return Ao(r)?r:No(r,u)?[r]:Ro(Mo(r))}Hn.exports=qo});var or=I((El,Gn)=>{var $o=nr(),Do=1/0;function Xo(r){if(typeof r=="string"||$o(r))return r;var u=r+"";return u=="0"&&1/r==-Do?"-0":u}Gn.exports=Xo});var Er=I((Al,Yn)=>{var Bo=ar(),Fo=or();function zo(r,u){u=Bo(u,r);for(var p=0,c=u.length;r!=null&&p{var Wo=Er();function Lo(r,u,p){var c=r==null?void 0:Wo(r,u);return c===void 0?p:c}Un.exports=Lo});var li=I((Ul,ui)=>{var Jo=sr(),Ko=function(){try{var r=Jo(Object,"defineProperty");return r({},"",{}),r}catch(u){}}();ui.exports=Ko});var pi=I((Zl,ci)=>{var fi=li();function jo(r,u,p){u=="__proto__"&&fi?fi(r,u,{configurable:!0,enumerable:!0,value:p,writable:!0}):r[u]=p}ci.exports=jo});var _i=I((Ql,yi)=>{var eu=pi(),ru=Or(),tu=Object.prototype,nu=tu.hasOwnProperty;function iu(r,u,p){var c=r[u];(!(nu.call(r,u)&&ru(c,p))||p===void 0&&!(u in r))&&eu(r,u,p)}yi.exports=iu});var gi=I((Jl,wi)=>{var su=9007199254740991,au=/^(?:0|[1-9]\d*)$/;function ou(r,u){var p=typeof r;return u=u==null?su:u,!!u&&(p=="number"||p!="symbol"&&au.test(r))&&r>-1&&r%1==0&&r{var uu=_i(),lu=ar(),cu=gi(),vi=ir(),fu=or();function pu(r,u,p,c){if(!vi(r))return r;u=lu(u,r);for(var w=-1,_=u.length,v=_-1,g=r;g!=null&&++w<_;){var h=fu(u[w]),x=p;if(h==="__proto__"||h==="constructor"||h==="prototype")return r;if(w!=v){var T=g[h];x=c?c(T,h,g):void 0,x===void 0&&(x=vi(T)?T:cu(u[w+1])?[]:{})}uu(g,h,x),g=g[h]}return r}di.exports=pu});var bi=I((jl,mi)=>{var yu=hi();function _u(r,u,p){return r==null?r:yu(r,u,p)}mi.exports=_u});var xi=I((ec,Ti)=>{function wu(r){var u=r==null?0:r.length;return u?r[u-1]:void 0}Ti.exports=wu});var Si=I((rc,Vi)=>{function gu(r,u,p){var c=-1,w=r.length;u<0&&(u=-u>w?0:w+u),p=p>w?w:p,p<0&&(p+=w),w=u>p?0:p-u>>>0,u>>>=0;for(var _=Array(w);++c{var du=Er(),vu=Si();function hu(r,u){return u.length<2?r:du(r,vu(u,0,-1))}ki.exports=hu});var Oi=I((nc,Ci)=>{var mu=ar(),bu=xi(),Tu=Pi(),xu=or();function Vu(r,u){return u=mu(u,r),r=Tu(r,u),r==null||delete r[xu(bu(u))]}Ci.exports=Vu});var Ei=I((ic,Ii)=>{var Su=Oi();function ku(r,u){return r==null?!0:Su(r,u)}Ii.exports=ku});var Ou={};Qi(Ou,{default:()=>Eu});var $i=G(require("@yarnpkg/core"));var ni=G(require("@yarnpkg/cli")),ur=G(require("@yarnpkg/core")),ii=G(require("@yarnpkg/core")),Le=G(require("clipanion"));var ue=G(require("@yarnpkg/core")),le=G(require("@yarnpkg/core")),Ne=G(require("@yarnpkg/fslib")),jn=G(Xr()),Re=G(kr());var Nr=G(require("@yarnpkg/core")),Rr=G(Ar()),re=G(kr()),Zn=G(require("vm")),{is_atom:ge,is_variable:Ho,is_instantiated_list:Go}=re.default.type;function Qn(r,u,p){r.prepend(p.map(c=>new re.default.type.State(u.goal.replace(c),u.substitution,u)))}var Jn=new WeakMap;function Mr(r){let u=Jn.get(r.session);if(u==null)throw new Error("Assertion failed: A project should have been registered for the active session");return u}var Yo=new re.default.type.Module("constraints",{["project_workspaces_by_descriptor/3"]:(r,u,p)=>{let[c,w,_]=p.args;if(!ge(c)||!ge(w)){r.throw_error(re.default.error.instantiation(p.indicator));return}let v=Nr.structUtils.parseIdent(c.id),g=Nr.structUtils.makeDescriptor(v,w.id),x=Mr(r).tryWorkspaceByDescriptor(g);Ho(_)&&x!==null&&Qn(r,u,[new re.default.type.Term("=",[_,new re.default.type.Term(String(x.relativeCwd))])]),ge(_)&&x!==null&&x.relativeCwd===_.id&&r.success(u)},["workspace_field/3"]:(r,u,p)=>{let[c,w,_]=p.args;if(!ge(c)||!ge(w)){r.throw_error(re.default.error.instantiation(p.indicator));return}let g=Mr(r).tryWorkspaceByCwd(c.id);if(g==null)return;let h=(0,Rr.default)(g.manifest.raw,w.id);typeof h!="undefined"&&Qn(r,u,[new re.default.type.Term("=",[_,new re.default.type.Term(typeof h=="object"?JSON.stringify(h):h)])])},["workspace_field_test/3"]:(r,u,p)=>{let[c,w,_]=p.args;r.prepend([new re.default.type.State(u.goal.replace(new re.default.type.Term("workspace_field_test",[c,w,_,new re.default.type.Term("[]",[])])),u.substitution,u)])},["workspace_field_test/4"]:(r,u,p)=>{let[c,w,_,v]=p.args;if(!ge(c)||!ge(w)||!ge(_)||!Go(v)){r.throw_error(re.default.error.instantiation(p.indicator));return}let h=Mr(r).tryWorkspaceByCwd(c.id);if(h==null)return;let x=(0,Rr.default)(h.manifest.raw,w.id);if(typeof x=="undefined")return;let T={$$:x};for(let[C,N]of v.toJavaScript().entries())T[`$${C}`]=N;Zn.default.runInNewContext(_.id,T)&&r.success(u)}},["project_workspaces_by_descriptor/3","workspace_field/3","workspace_field_test/3","workspace_field_test/4"]);function Kn(r,u){Jn.set(r,u),r.consult(`:- use_module(library(${Yo.id})).`)}(0,jn.default)(Re.default);var We;(function(c){c.Dependencies="dependencies",c.DevDependencies="devDependencies",c.PeerDependencies="peerDependencies"})(We||(We={}));var ei=[We.Dependencies,We.DevDependencies,We.PeerDependencies];function K(r){if(r instanceof Re.default.type.Num)return r.value;if(r instanceof Re.default.type.Term)switch(r.indicator){case"throw/1":return K(r.args[0]);case"error/1":return K(r.args[0]);case"error/2":if(r.args[0]instanceof Re.default.type.Term&&r.args[0].indicator==="syntax_error/1")return Object.assign(K(r.args[0]),...K(r.args[1]));{let u=K(r.args[0]);return u.message+=` (in ${K(r.args[1])})`,u}case"syntax_error/1":return new ue.ReportError(ue.MessageName.PROLOG_SYNTAX_ERROR,`Syntax error: ${K(r.args[0])}`);case"existence_error/2":return new ue.ReportError(ue.MessageName.PROLOG_EXISTENCE_ERROR,`Existence error: ${K(r.args[0])} ${K(r.args[1])} not found`);case"instantiation_error/0":return new ue.ReportError(ue.MessageName.PROLOG_INSTANTIATION_ERROR,"Instantiation error: an argument is variable when an instantiated argument was expected");case"line/1":return{line:K(r.args[0])};case"column/1":return{column:K(r.args[0])};case"found/1":return{found:K(r.args[0])};case"./2":return[K(r.args[0])].concat(K(r.args[1]));case"//2":return`${K(r.args[0])}/${K(r.args[1])}`;default:return r.id}throw`couldn't pretty print because of unsupported node ${r}`}function ri(r){let u;try{u=K(r)}catch(p){throw typeof p=="string"?new ue.ReportError(ue.MessageName.PROLOG_UNKNOWN_ERROR,`Unknown error: ${r} (note: ${p})`):p}return typeof u.line!="undefined"&&typeof u.column!="undefined"&&(u.message+=` at line ${u.line}, column ${u.column}`),u}var ti=class{constructor(u,p){this.session=Re.default.create(),Kn(this.session,u),this.session.consult(":- use_module(library(lists))."),this.session.consult(p)}fetchNextAnswer(){return new Promise(u=>{this.session.answer(p=>{u(p)})})}async*makeQuery(u){let p=this.session.query(u);if(p!==!0)throw ri(p);for(;;){let c=await this.fetchNextAnswer();if(!c)break;if(c.id==="throw")throw ri(c);yield c}}};function ke(r){return r.id==="null"?null:`${r.toJavaScript()}`}function Uo(r){if(r.id==="null")return null;{let u=r.toJavaScript();if(typeof u!="string")return JSON.stringify(u);try{return JSON.stringify(JSON.parse(u))}catch{return JSON.stringify(u)}}}var pe=class{constructor(u){this.source="";this.project=u;let p=u.configuration.get("constraintsPath");Ne.xfs.existsSync(p)&&(this.source=Ne.xfs.readFileSync(p,"utf8"))}static async find(u){return new pe(u)}getProjectDatabase(){let u="";for(let p of ei)u+=`dependency_type(${p}). +`;for(let p of this.project.workspacesByCwd.values()){let c=p.relativeCwd;u+=`workspace(${de(c)}). +`,u+=`workspace_ident(${de(c)}, ${de(le.structUtils.stringifyIdent(p.locator))}). +`,u+=`workspace_version(${de(c)}, ${de(p.manifest.version)}). +`;for(let w of ei)for(let _ of p.manifest[w].values())u+=`workspace_has_dependency(${de(c)}, ${de(le.structUtils.stringifyIdent(_))}, ${de(_.range)}, ${w}). +`}return u+=`workspace(_) :- false. +`,u+=`workspace_ident(_, _) :- false. +`,u+=`workspace_version(_, _) :- false. +`,u+=`workspace_has_dependency(_, _, _, _) :- false. +`,u}getDeclarations(){let u="";return u+=`gen_enforced_dependency(_, _, _, _) :- false. +`,u+=`gen_enforced_field(_, _, _) :- false. +`,u}get fullSource(){return`${this.getProjectDatabase()} +${this.source} +${this.getDeclarations()}`}createSession(){return new ti(this.project,this.fullSource)}async process(){let u=this.createSession();return{enforcedDependencies:await this.genEnforcedDependencies(u),enforcedFields:await this.genEnforcedFields(u)}}async genEnforcedDependencies(u){let p=[];for await(let c of u.makeQuery("workspace(WorkspaceCwd), dependency_type(DependencyType), gen_enforced_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType).")){let w=Ne.ppath.resolve(this.project.cwd,ke(c.links.WorkspaceCwd)),_=ke(c.links.DependencyIdent),v=ke(c.links.DependencyRange),g=ke(c.links.DependencyType);if(w===null||_===null)throw new Error("Invalid rule");let h=this.project.getWorkspaceByCwd(w),x=le.structUtils.parseIdent(_);p.push({workspace:h,dependencyIdent:x,dependencyRange:v,dependencyType:g})}return le.miscUtils.sortMap(p,[({dependencyRange:c})=>c!==null?"0":"1",({workspace:c})=>le.structUtils.stringifyIdent(c.locator),({dependencyIdent:c})=>le.structUtils.stringifyIdent(c)])}async genEnforcedFields(u){let p=[];for await(let c of u.makeQuery("workspace(WorkspaceCwd), gen_enforced_field(WorkspaceCwd, FieldPath, FieldValue).")){let w=Ne.ppath.resolve(this.project.cwd,ke(c.links.WorkspaceCwd)),_=ke(c.links.FieldPath),v=Uo(c.links.FieldValue);if(w===null||_===null)throw new Error("Invalid rule");let g=this.project.getWorkspaceByCwd(w);p.push({workspace:g,fieldPath:_,fieldValue:v})}return le.miscUtils.sortMap(p,[({workspace:c})=>le.structUtils.stringifyIdent(c.locator),({fieldPath:c})=>c])}async*query(u){let p=this.createSession();for await(let c of p.makeQuery(u)){let w={};for(let[_,v]of Object.entries(c.links))_!=="_"&&(w[_]=ke(v));yield w}}};function de(r){return typeof r=="string"?`'${r}'`:"[]"}var He=class extends ni.BaseCommand{constructor(){super(...arguments);this.json=Le.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.query=Le.Option.String()}async execute(){let u=await ur.Configuration.find(this.context.cwd,this.context.plugins),{project:p}=await ur.Project.find(u,this.context.cwd),c=await pe.find(p),w=this.query;return w.endsWith(".")||(w=`${w}.`),(await ii.StreamReport.start({configuration:u,json:this.json,stdout:this.context.stdout},async v=>{for await(let g of c.query(w)){let h=Array.from(Object.entries(g)),x=h.length,T=h.reduce((b,[C])=>Math.max(b,C.length),0);for(let b=0;b{let v=new Set,g=[];for(let h=0,x=this.fix?10:1;h{await h.persistManifest()}));for(let[h,x]of g)_.reportError(h,x)});return w.hasErrors()?w.exitCode():0}};Ye.paths=[["constraints"]],Ye.usage=fr.Command.Usage({category:"Constraints-related commands",description:"check that the project constraints are met",details:` + This command will run constraints on your project and emit errors for each one that is found but isn't met. If any error is emitted the process will exit with a non-zero exit code. + + If the \`--fix\` flag is used, Yarn will attempt to automatically fix the issues the best it can, following a multi-pass process (with a maximum of 10 iterations). Some ambiguous patterns cannot be autofixed, in which case you'll have to manually specify the right resolution. + + For more information as to how to write constraints, please consult our dedicated page on our website: https://yarnpkg.com/features/constraints. + `,examples:[["Check that all constraints are satisfied","yarn constraints"],["Autofix all unmet constraints","yarn constraints --fix"]]});var qi=Ye;async function Pu(r,u,p,{configuration:c,fix:w}){let _=new Map,v=new Map;for(let{workspace:g,dependencyIdent:h,dependencyRange:x,dependencyType:T}of p){let b=v.get(g);typeof b=="undefined"&&v.set(g,b=new Map);let C=b.get(h.identHash);typeof C=="undefined"&&b.set(h.identHash,C=new Map);let N=C.get(T);typeof N=="undefined"&&C.set(T,N=new Set),_.set(h.identHash,h),N.add(x)}for(let[g,h]of v)for(let[x,T]of h){let b=_.get(x);if(typeof b=="undefined")throw new Error("Assertion failed: The ident should have been registered");for(let[C,N]of T){let W=N.has(null)?[null]:[...N];if(W.length>2)u.push([se.MessageName.CONSTRAINTS_AMBIGUITY,`${$.structUtils.prettyWorkspace(c,g)} must depend on ${$.structUtils.prettyIdent(c,b)} via conflicting ranges ${W.slice(0,-1).map(ee=>$.structUtils.prettyRange(c,String(ee))).join(", ")}, and ${$.structUtils.prettyRange(c,String(W[W.length-1]))} (in ${C})`]);else if(W.length>1)u.push([se.MessageName.CONSTRAINTS_AMBIGUITY,`${$.structUtils.prettyWorkspace(c,g)} must depend on ${$.structUtils.prettyIdent(c,b)} via conflicting ranges ${$.structUtils.prettyRange(c,String(W[0]))} and ${$.structUtils.prettyRange(c,String(W[1]))} (in ${C})`]);else{let ee=g.manifest[C].get(b.identHash),[te]=W;te!==null?ee?ee.range!==te&&(w?(g.manifest[C].set(b.identHash,$.structUtils.makeDescriptor(b,te)),r.add(g)):u.push([se.MessageName.CONSTRAINTS_INCOMPATIBLE_DEPENDENCY,`${$.structUtils.prettyWorkspace(c,g)} must depend on ${$.structUtils.prettyIdent(c,b)} via ${$.structUtils.prettyRange(c,te)}, but uses ${$.structUtils.prettyRange(c,ee.range)} instead (in ${C})`])):w?(g.manifest[C].set(b.identHash,$.structUtils.makeDescriptor(b,te)),r.add(g)):u.push([se.MessageName.CONSTRAINTS_MISSING_DEPENDENCY,`${$.structUtils.prettyWorkspace(c,g)} must depend on ${$.structUtils.prettyIdent(c,b)} (via ${$.structUtils.prettyRange(c,te)}), but doesn't (in ${C})`]):ee&&(w?(g.manifest[C].delete(b.identHash),r.add(g)):u.push([se.MessageName.CONSTRAINTS_EXTRANEOUS_DEPENDENCY,`${$.structUtils.prettyWorkspace(c,g)} has an extraneous dependency on ${$.structUtils.prettyIdent(c,b)} (in ${C})`]))}}}}async function Cu(r,u,p,{configuration:c,fix:w}){let _=new Map;for(let{workspace:v,fieldPath:g,fieldValue:h}of p){let x=Pe.miscUtils.getMapWithDefault(_,v);Pe.miscUtils.getSetWithDefault(x,g).add(h)}for(let[v,g]of _)for(let[h,x]of g){let T=[...x];if(T.length>2)u.push([se.MessageName.CONSTRAINTS_AMBIGUITY,`${$.structUtils.prettyWorkspace(c,v)} must have a field ${$.formatUtils.pretty(c,h,"cyan")} set to conflicting values ${T.slice(0,-1).map(b=>$.formatUtils.pretty(c,String(b),"magenta")).join(", ")}, or ${$.formatUtils.pretty(c,String(T[T.length-1]),"magenta")}`]);else if(T.length>1)u.push([se.MessageName.CONSTRAINTS_AMBIGUITY,`${$.structUtils.prettyWorkspace(c,v)} must have a field ${$.formatUtils.pretty(c,h,"cyan")} set to conflicting values ${$.formatUtils.pretty(c,String(T[0]),"magenta")} or ${$.formatUtils.pretty(c,String(T[1]),"magenta")}`]);else{let b=(0,Ni.default)(v.manifest.raw,h),[C]=T;C!==null?b===void 0?w?(await qr(v,h,C),r.add(v)):u.push([se.MessageName.CONSTRAINTS_MISSING_FIELD,`${$.structUtils.prettyWorkspace(c,v)} must have a field ${$.formatUtils.pretty(c,h,"cyan")} set to ${$.formatUtils.pretty(c,String(C),"magenta")}, but doesn't`]):JSON.stringify(b)!==C&&(w?(await qr(v,h,C),r.add(v)):u.push([se.MessageName.CONSTRAINTS_INCOMPATIBLE_FIELD,`${$.structUtils.prettyWorkspace(c,v)} must have a field ${$.formatUtils.pretty(c,h,"cyan")} set to ${$.formatUtils.pretty(c,String(C),"magenta")}, but is set to ${$.formatUtils.pretty(c,JSON.stringify(b),"magenta")} instead`])):b!=null&&(w?(await qr(v,h,null),r.add(v)):u.push([se.MessageName.CONSTRAINTS_EXTRANEOUS_FIELD,`${$.structUtils.prettyWorkspace(c,v)} has an extraneous field ${$.formatUtils.pretty(c,h,"cyan")} set to ${$.formatUtils.pretty(c,JSON.stringify(b),"magenta")}`]))}}}async function qr(r,u,p){p===null?(0,Mi.default)(r.manifest.raw,u):(0,Ri.default)(r.manifest.raw,u,JSON.parse(p))}var Iu={configuration:{constraintsPath:{description:"The path of the constraints file.",type:$i.SettingsType.ABSOLUTE_PATH,default:"./constraints.pro"}},commands:[si,oi,qi]},Eu=Iu;return Ou;})(); +return plugin; +} +}; diff --git a/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs b/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs new file mode 100644 index 0000000000..b9044a0144 --- /dev/null +++ b/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs @@ -0,0 +1,28 @@ +/* eslint-disable */ +//prettier-ignore +module.exports = { +name: "@yarnpkg/plugin-workspace-tools", +factory: function (require) { +var plugin=(()=>{var wr=Object.create,me=Object.defineProperty,Sr=Object.defineProperties,vr=Object.getOwnPropertyDescriptor,Hr=Object.getOwnPropertyDescriptors,$r=Object.getOwnPropertyNames,et=Object.getOwnPropertySymbols,kr=Object.getPrototypeOf,tt=Object.prototype.hasOwnProperty,Tr=Object.prototype.propertyIsEnumerable;var rt=(e,t,r)=>t in e?me(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,B=(e,t)=>{for(var r in t||(t={}))tt.call(t,r)&&rt(e,r,t[r]);if(et)for(var r of et(t))Tr.call(t,r)&&rt(e,r,t[r]);return e},Q=(e,t)=>Sr(e,Hr(t)),Lr=e=>me(e,"__esModule",{value:!0});var K=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Or=(e,t)=>{for(var r in t)me(e,r,{get:t[r],enumerable:!0})},Nr=(e,t,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of $r(t))!tt.call(e,n)&&n!=="default"&&me(e,n,{get:()=>t[n],enumerable:!(r=vr(t,n))||r.enumerable});return e},X=e=>Nr(Lr(me(e!=null?wr(kr(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var $e=K(te=>{"use strict";te.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;te.find=(e,t)=>e.nodes.find(r=>r.type===t);te.exceedsLimit=(e,t,r=1,n)=>n===!1||!te.isInteger(e)||!te.isInteger(t)?!1:(Number(t)-Number(e))/Number(r)>=n;te.escapeNode=(e,t=0,r)=>{let n=e.nodes[t];!n||(r&&n.type===r||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0)};te.encloseBrace=e=>e.type!=="brace"?!1:e.commas>>0+e.ranges>>0==0?(e.invalid=!0,!0):!1;te.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:e.commas>>0+e.ranges>>0==0||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;te.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;te.reduce=e=>e.reduce((t,r)=>(r.type==="text"&&t.push(r.value),r.type==="range"&&(r.type="text"),t),[]);te.flatten=(...e)=>{let t=[],r=n=>{for(let s=0;s{"use strict";var it=$e();at.exports=(e,t={})=>{let r=(n,s={})=>{let a=t.escapeInvalid&&it.isInvalidBrace(s),i=n.invalid===!0&&t.escapeInvalid===!0,o="";if(n.value)return(a||i)&&it.isOpenOrClose(n)?"\\"+n.value:n.value;if(n.value)return n.value;if(n.nodes)for(let h of n.nodes)o+=r(h);return o};return r(e)}});var ct=K((os,ot)=>{"use strict";ot.exports=function(e){return typeof e=="number"?e-e==0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var At=K((cs,ut)=>{"use strict";var lt=ct(),pe=(e,t,r)=>{if(lt(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(t===void 0||e===t)return String(e);if(lt(t)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let n=B({relaxZeros:!0},r);typeof n.strictZeros=="boolean"&&(n.relaxZeros=n.strictZeros===!1);let s=String(n.relaxZeros),a=String(n.shorthand),i=String(n.capture),o=String(n.wrap),h=e+":"+t+"="+s+a+i+o;if(pe.cache.hasOwnProperty(h))return pe.cache[h].result;let g=Math.min(e,t),f=Math.max(e,t);if(Math.abs(g-f)===1){let R=e+"|"+t;return n.capture?`(${R})`:n.wrap===!1?R:`(?:${R})`}let A=ft(e)||ft(t),p={min:e,max:t,a:g,b:f},k=[],y=[];if(A&&(p.isPadded=A,p.maxLen=String(p.max).length),g<0){let R=f<0?Math.abs(f):1;y=pt(R,Math.abs(g),p,n),g=p.a=0}return f>=0&&(k=pt(g,f,p,n)),p.negatives=y,p.positives=k,p.result=Ir(y,k,n),n.capture===!0?p.result=`(${p.result})`:n.wrap!==!1&&k.length+y.length>1&&(p.result=`(?:${p.result})`),pe.cache[h]=p,p.result};function Ir(e,t,r){let n=Pe(e,t,"-",!1,r)||[],s=Pe(t,e,"",!1,r)||[],a=Pe(e,t,"-?",!0,r)||[];return n.concat(a).concat(s).join("|")}function Mr(e,t){let r=1,n=1,s=ht(e,r),a=new Set([t]);for(;e<=s&&s<=t;)a.add(s),r+=1,s=ht(e,r);for(s=dt(t+1,n)-1;e1&&o.count.pop(),o.count.push(f.count[0]),o.string=o.pattern+gt(o.count),i=g+1;continue}r.isPadded&&(A=Gr(g,r,n)),f.string=A+f.pattern+gt(f.count),a.push(f),i=g+1,o=f}return a}function Pe(e,t,r,n,s){let a=[];for(let i of e){let{string:o}=i;!n&&!mt(t,"string",o)&&a.push(r+o),n&&mt(t,"string",o)&&a.push(r+o)}return a}function Pr(e,t){let r=[];for(let n=0;nt?1:t>e?-1:0}function mt(e,t,r){return e.some(n=>n[t]===r)}function ht(e,t){return Number(String(e).slice(0,-t)+"9".repeat(t))}function dt(e,t){return e-e%Math.pow(10,t)}function gt(e){let[t=0,r=""]=e;return r||t>1?`{${t+(r?","+r:"")}}`:""}function Dr(e,t,r){return`[${e}${t-e==1?"":"-"}${t}]`}function ft(e){return/^-?(0+)\d/.test(e)}function Gr(e,t,r){if(!t.isPadded)return e;let n=Math.abs(t.maxLen-String(e).length),s=r.relaxZeros!==!1;switch(n){case 0:return"";case 1:return s?"0?":"0";case 2:return s?"0{0,2}":"00";default:return s?`0{0,${n}}`:`0{${n}}`}}pe.cache={};pe.clearCache=()=>pe.cache={};ut.exports=pe});var Ge=K((us,Rt)=>{"use strict";var qr=require("util"),yt=At(),bt=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Kr=e=>t=>e===!0?Number(t):String(t),De=e=>typeof e=="number"||typeof e=="string"&&e!=="",Re=e=>Number.isInteger(+e),Ue=e=>{let t=`${e}`,r=-1;if(t[0]==="-"&&(t=t.slice(1)),t==="0")return!1;for(;t[++r]==="0";);return r>0},Wr=(e,t,r)=>typeof e=="string"||typeof t=="string"?!0:r.stringify===!0,jr=(e,t,r)=>{if(t>0){let n=e[0]==="-"?"-":"";n&&(e=e.slice(1)),e=n+e.padStart(n?t-1:t,"0")}return r===!1?String(e):e},_t=(e,t)=>{let r=e[0]==="-"?"-":"";for(r&&(e=e.slice(1),t--);e.length{e.negatives.sort((i,o)=>io?1:0),e.positives.sort((i,o)=>io?1:0);let r=t.capture?"":"?:",n="",s="",a;return e.positives.length&&(n=e.positives.join("|")),e.negatives.length&&(s=`-(${r}${e.negatives.join("|")})`),n&&s?a=`${n}|${s}`:a=n||s,t.wrap?`(${r}${a})`:a},Et=(e,t,r,n)=>{if(r)return yt(e,t,B({wrap:!1},n));let s=String.fromCharCode(e);if(e===t)return s;let a=String.fromCharCode(t);return`[${s}-${a}]`},xt=(e,t,r)=>{if(Array.isArray(e)){let n=r.wrap===!0,s=r.capture?"":"?:";return n?`(${s}${e.join("|")})`:e.join("|")}return yt(e,t,r)},Ct=(...e)=>new RangeError("Invalid range arguments: "+qr.inspect(...e)),wt=(e,t,r)=>{if(r.strictRanges===!0)throw Ct([e,t]);return[]},Qr=(e,t)=>{if(t.strictRanges===!0)throw new TypeError(`Expected step "${e}" to be a number`);return[]},Xr=(e,t,r=1,n={})=>{let s=Number(e),a=Number(t);if(!Number.isInteger(s)||!Number.isInteger(a)){if(n.strictRanges===!0)throw Ct([e,t]);return[]}s===0&&(s=0),a===0&&(a=0);let i=s>a,o=String(e),h=String(t),g=String(r);r=Math.max(Math.abs(r),1);let f=Ue(o)||Ue(h)||Ue(g),A=f?Math.max(o.length,h.length,g.length):0,p=f===!1&&Wr(e,t,n)===!1,k=n.transform||Kr(p);if(n.toRegex&&r===1)return Et(_t(e,A),_t(t,A),!0,n);let y={negatives:[],positives:[]},R=T=>y[T<0?"negatives":"positives"].push(Math.abs(T)),_=[],x=0;for(;i?s>=a:s<=a;)n.toRegex===!0&&r>1?R(s):_.push(jr(k(s,x),A,p)),s=i?s-r:s+r,x++;return n.toRegex===!0?r>1?Fr(y,n):xt(_,null,B({wrap:!1},n)):_},Zr=(e,t,r=1,n={})=>{if(!Re(e)&&e.length>1||!Re(t)&&t.length>1)return wt(e,t,n);let s=n.transform||(p=>String.fromCharCode(p)),a=`${e}`.charCodeAt(0),i=`${t}`.charCodeAt(0),o=a>i,h=Math.min(a,i),g=Math.max(a,i);if(n.toRegex&&r===1)return Et(h,g,!1,n);let f=[],A=0;for(;o?a>=i:a<=i;)f.push(s(a,A)),a=o?a-r:a+r,A++;return n.toRegex===!0?xt(f,null,{wrap:!1,options:n}):f},Te=(e,t,r,n={})=>{if(t==null&&De(e))return[e];if(!De(e)||!De(t))return wt(e,t,n);if(typeof r=="function")return Te(e,t,1,{transform:r});if(bt(r))return Te(e,t,0,r);let s=B({},n);return s.capture===!0&&(s.wrap=!0),r=r||s.step||1,Re(r)?Re(e)&&Re(t)?Xr(e,t,r,s):Zr(e,t,Math.max(Math.abs(r),1),s):r!=null&&!bt(r)?Qr(r,s):Te(e,t,1,r)};Rt.exports=Te});var Ht=K((ls,St)=>{"use strict";var Yr=Ge(),vt=$e(),zr=(e,t={})=>{let r=(n,s={})=>{let a=vt.isInvalidBrace(s),i=n.invalid===!0&&t.escapeInvalid===!0,o=a===!0||i===!0,h=t.escapeInvalid===!0?"\\":"",g="";if(n.isOpen===!0||n.isClose===!0)return h+n.value;if(n.type==="open")return o?h+n.value:"(";if(n.type==="close")return o?h+n.value:")";if(n.type==="comma")return n.prev.type==="comma"?"":o?n.value:"|";if(n.value)return n.value;if(n.nodes&&n.ranges>0){let f=vt.reduce(n.nodes),A=Yr(...f,Q(B({},t),{wrap:!1,toRegex:!0}));if(A.length!==0)return f.length>1&&A.length>1?`(${A})`:A}if(n.nodes)for(let f of n.nodes)g+=r(f,n);return g};return r(e)};St.exports=zr});var Tt=K((ps,$t)=>{"use strict";var Vr=Ge(),kt=ke(),he=$e(),fe=(e="",t="",r=!1)=>{let n=[];if(e=[].concat(e),t=[].concat(t),!t.length)return e;if(!e.length)return r?he.flatten(t).map(s=>`{${s}}`):t;for(let s of e)if(Array.isArray(s))for(let a of s)n.push(fe(a,t,r));else for(let a of t)r===!0&&typeof a=="string"&&(a=`{${a}}`),n.push(Array.isArray(a)?fe(s,a,r):s+a);return he.flatten(n)},Jr=(e,t={})=>{let r=t.rangeLimit===void 0?1e3:t.rangeLimit,n=(s,a={})=>{s.queue=[];let i=a,o=a.queue;for(;i.type!=="brace"&&i.type!=="root"&&i.parent;)i=i.parent,o=i.queue;if(s.invalid||s.dollar){o.push(fe(o.pop(),kt(s,t)));return}if(s.type==="brace"&&s.invalid!==!0&&s.nodes.length===2){o.push(fe(o.pop(),["{}"]));return}if(s.nodes&&s.ranges>0){let A=he.reduce(s.nodes);if(he.exceedsLimit(...A,t.step,r))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let p=Vr(...A,t);p.length===0&&(p=kt(s,t)),o.push(fe(o.pop(),p)),s.nodes=[];return}let h=he.encloseBrace(s),g=s.queue,f=s;for(;f.type!=="brace"&&f.type!=="root"&&f.parent;)f=f.parent,g=f.queue;for(let A=0;A{"use strict";Lt.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` +`,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Pt=K((hs,Nt)=>{"use strict";var en=ke(),{MAX_LENGTH:It,CHAR_BACKSLASH:qe,CHAR_BACKTICK:tn,CHAR_COMMA:rn,CHAR_DOT:nn,CHAR_LEFT_PARENTHESES:sn,CHAR_RIGHT_PARENTHESES:an,CHAR_LEFT_CURLY_BRACE:on,CHAR_RIGHT_CURLY_BRACE:cn,CHAR_LEFT_SQUARE_BRACKET:Bt,CHAR_RIGHT_SQUARE_BRACKET:Mt,CHAR_DOUBLE_QUOTE:un,CHAR_SINGLE_QUOTE:ln,CHAR_NO_BREAK_SPACE:pn,CHAR_ZERO_WIDTH_NOBREAK_SPACE:fn}=Ot(),hn=(e,t={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let r=t||{},n=typeof r.maxLength=="number"?Math.min(It,r.maxLength):It;if(e.length>n)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${n})`);let s={type:"root",input:e,nodes:[]},a=[s],i=s,o=s,h=0,g=e.length,f=0,A=0,p,k={},y=()=>e[f++],R=_=>{if(_.type==="text"&&o.type==="dot"&&(o.type="text"),o&&o.type==="text"&&_.type==="text"){o.value+=_.value;return}return i.nodes.push(_),_.parent=i,_.prev=o,o=_,_};for(R({type:"bos"});f0){if(i.ranges>0){i.ranges=0;let _=i.nodes.shift();i.nodes=[_,{type:"text",value:en(i)}]}R({type:"comma",value:p}),i.commas++;continue}if(p===nn&&A>0&&i.commas===0){let _=i.nodes;if(A===0||_.length===0){R({type:"text",value:p});continue}if(o.type==="dot"){if(i.range=[],o.value+=p,o.type="range",i.nodes.length!==3&&i.nodes.length!==5){i.invalid=!0,i.ranges=0,o.type="text";continue}i.ranges++,i.args=[];continue}if(o.type==="range"){_.pop();let x=_[_.length-1];x.value+=o.value+p,o=x,i.ranges--;continue}R({type:"dot",value:p});continue}R({type:"text",value:p})}do if(i=a.pop(),i.type!=="root"){i.nodes.forEach(T=>{T.nodes||(T.type==="open"&&(T.isOpen=!0),T.type==="close"&&(T.isClose=!0),T.nodes||(T.type="text"),T.invalid=!0)});let _=a[a.length-1],x=_.nodes.indexOf(i);_.nodes.splice(x,1,...i.nodes)}while(a.length>0);return R({type:"eos"}),s};Nt.exports=hn});var Gt=K((ds,Dt)=>{"use strict";var Ut=ke(),dn=Ht(),gn=Tt(),mn=Pt(),V=(e,t={})=>{let r=[];if(Array.isArray(e))for(let n of e){let s=V.create(n,t);Array.isArray(s)?r.push(...s):r.push(s)}else r=[].concat(V.create(e,t));return t&&t.expand===!0&&t.nodupes===!0&&(r=[...new Set(r)]),r};V.parse=(e,t={})=>mn(e,t);V.stringify=(e,t={})=>typeof e=="string"?Ut(V.parse(e,t),t):Ut(e,t);V.compile=(e,t={})=>(typeof e=="string"&&(e=V.parse(e,t)),dn(e,t));V.expand=(e,t={})=>{typeof e=="string"&&(e=V.parse(e,t));let r=gn(e,t);return t.noempty===!0&&(r=r.filter(Boolean)),t.nodupes===!0&&(r=[...new Set(r)]),r};V.create=(e,t={})=>e===""||e.length<3?[e]:t.expand!==!0?V.compile(e,t):V.expand(e,t);Dt.exports=V});var ye=K((gs,qt)=>{"use strict";var An=require("path"),ie="\\\\/",Kt=`[^${ie}]`,ce="\\.",Rn="\\+",yn="\\?",Le="\\/",bn="(?=.)",Wt="[^/]",Ke=`(?:${Le}|$)`,jt=`(?:^|${Le})`,We=`${ce}{1,2}${Ke}`,_n=`(?!${ce})`,En=`(?!${jt}${We})`,xn=`(?!${ce}{0,1}${Ke})`,Cn=`(?!${We})`,wn=`[^.${Le}]`,Sn=`${Wt}*?`,Ft={DOT_LITERAL:ce,PLUS_LITERAL:Rn,QMARK_LITERAL:yn,SLASH_LITERAL:Le,ONE_CHAR:bn,QMARK:Wt,END_ANCHOR:Ke,DOTS_SLASH:We,NO_DOT:_n,NO_DOTS:En,NO_DOT_SLASH:xn,NO_DOTS_SLASH:Cn,QMARK_NO_DOT:wn,STAR:Sn,START_ANCHOR:jt},vn=Q(B({},Ft),{SLASH_LITERAL:`[${ie}]`,QMARK:Kt,STAR:`${Kt}*?`,DOTS_SLASH:`${ce}{1,2}(?:[${ie}]|$)`,NO_DOT:`(?!${ce})`,NO_DOTS:`(?!(?:^|[${ie}])${ce}{1,2}(?:[${ie}]|$))`,NO_DOT_SLASH:`(?!${ce}{0,1}(?:[${ie}]|$))`,NO_DOTS_SLASH:`(?!${ce}{1,2}(?:[${ie}]|$))`,QMARK_NO_DOT:`[^.${ie}]`,START_ANCHOR:`(?:^|[${ie}])`,END_ANCHOR:`(?:[${ie}]|$)`}),Hn={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};qt.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:Hn,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:An.sep,extglobChars(e){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${e.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(e){return e===!0?vn:Ft}}});var be=K(Z=>{"use strict";var $n=require("path"),kn=process.platform==="win32",{REGEX_BACKSLASH:Tn,REGEX_REMOVE_BACKSLASH:Ln,REGEX_SPECIAL_CHARS:On,REGEX_SPECIAL_CHARS_GLOBAL:Nn}=ye();Z.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);Z.hasRegexChars=e=>On.test(e);Z.isRegexChar=e=>e.length===1&&Z.hasRegexChars(e);Z.escapeRegex=e=>e.replace(Nn,"\\$1");Z.toPosixSlashes=e=>e.replace(Tn,"/");Z.removeBackslashes=e=>e.replace(Ln,t=>t==="\\"?"":t);Z.supportsLookbehinds=()=>{let e=process.version.slice(1).split(".").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};Z.isWindows=e=>e&&typeof e.windows=="boolean"?e.windows:kn===!0||$n.sep==="\\";Z.escapeLast=(e,t,r)=>{let n=e.lastIndexOf(t,r);return n===-1?e:e[n-1]==="\\"?Z.escapeLast(e,t,n-1):`${e.slice(0,n)}\\${e.slice(n)}`};Z.removePrefix=(e,t={})=>{let r=e;return r.startsWith("./")&&(r=r.slice(2),t.prefix="./"),r};Z.wrapOutput=(e,t={},r={})=>{let n=r.contains?"":"^",s=r.contains?"":"$",a=`${n}(?:${e})${s}`;return t.negated===!0&&(a=`(?:^(?!${a}).*$)`),a}});var er=K((As,Qt)=>{"use strict";var Xt=be(),{CHAR_ASTERISK:je,CHAR_AT:In,CHAR_BACKWARD_SLASH:_e,CHAR_COMMA:Bn,CHAR_DOT:Fe,CHAR_EXCLAMATION_MARK:Qe,CHAR_FORWARD_SLASH:Zt,CHAR_LEFT_CURLY_BRACE:Xe,CHAR_LEFT_PARENTHESES:Ze,CHAR_LEFT_SQUARE_BRACKET:Mn,CHAR_PLUS:Pn,CHAR_QUESTION_MARK:Yt,CHAR_RIGHT_CURLY_BRACE:Dn,CHAR_RIGHT_PARENTHESES:zt,CHAR_RIGHT_SQUARE_BRACKET:Un}=ye(),Vt=e=>e===Zt||e===_e,Jt=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?Infinity:1)},Gn=(e,t)=>{let r=t||{},n=e.length-1,s=r.parts===!0||r.scanToEnd===!0,a=[],i=[],o=[],h=e,g=-1,f=0,A=0,p=!1,k=!1,y=!1,R=!1,_=!1,x=!1,T=!1,O=!1,W=!1,G=!1,ne=0,E,b,C={value:"",depth:0,isGlob:!1},M=()=>g>=n,l=()=>h.charCodeAt(g+1),H=()=>(E=b,h.charCodeAt(++g));for(;g0&&(j=h.slice(0,f),h=h.slice(f),A-=f),w&&y===!0&&A>0?(w=h.slice(0,A),c=h.slice(A)):y===!0?(w="",c=h):w=h,w&&w!==""&&w!=="/"&&w!==h&&Vt(w.charCodeAt(w.length-1))&&(w=w.slice(0,-1)),r.unescape===!0&&(c&&(c=Xt.removeBackslashes(c)),w&&T===!0&&(w=Xt.removeBackslashes(w)));let u={prefix:j,input:e,start:f,base:w,glob:c,isBrace:p,isBracket:k,isGlob:y,isExtglob:R,isGlobstar:_,negated:O,negatedExtglob:W};if(r.tokens===!0&&(u.maxDepth=0,Vt(b)||i.push(C),u.tokens=i),r.parts===!0||r.tokens===!0){let I;for(let $=0;${"use strict";var Oe=ye(),J=be(),{MAX_LENGTH:Ne,POSIX_REGEX_SOURCE:qn,REGEX_NON_SPECIAL_CHARS:Kn,REGEX_SPECIAL_CHARS_BACKREF:Wn,REPLACEMENTS:rr}=Oe,jn=(e,t)=>{if(typeof t.expandRange=="function")return t.expandRange(...e,t);e.sort();let r=`[${e.join("-")}]`;try{new RegExp(r)}catch(n){return e.map(s=>J.escapeRegex(s)).join("..")}return r},de=(e,t)=>`Missing ${e}: "${t}" - use "\\\\${t}" to match literal characters`,nr=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");e=rr[e]||e;let r=B({},t),n=typeof r.maxLength=="number"?Math.min(Ne,r.maxLength):Ne,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);let a={type:"bos",value:"",output:r.prepend||""},i=[a],o=r.capture?"":"?:",h=J.isWindows(t),g=Oe.globChars(h),f=Oe.extglobChars(g),{DOT_LITERAL:A,PLUS_LITERAL:p,SLASH_LITERAL:k,ONE_CHAR:y,DOTS_SLASH:R,NO_DOT:_,NO_DOT_SLASH:x,NO_DOTS_SLASH:T,QMARK:O,QMARK_NO_DOT:W,STAR:G,START_ANCHOR:ne}=g,E=m=>`(${o}(?:(?!${ne}${m.dot?R:A}).)*?)`,b=r.dot?"":_,C=r.dot?O:W,M=r.bash===!0?E(r):G;r.capture&&(M=`(${M})`),typeof r.noext=="boolean"&&(r.noextglob=r.noext);let l={input:e,index:-1,start:0,dot:r.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:i};e=J.removePrefix(e,l),s=e.length;let H=[],w=[],j=[],c=a,u,I=()=>l.index===s-1,$=l.peek=(m=1)=>e[l.index+m],ee=l.advance=()=>e[++l.index]||"",se=()=>e.slice(l.index+1),z=(m="",L=0)=>{l.consumed+=m,l.index+=L},Ce=m=>{l.output+=m.output!=null?m.output:m.value,z(m.value)},xr=()=>{let m=1;for(;$()==="!"&&($(2)!=="("||$(3)==="?");)ee(),l.start++,m++;return m%2==0?!1:(l.negated=!0,l.start++,!0)},we=m=>{l[m]++,j.push(m)},ue=m=>{l[m]--,j.pop()},v=m=>{if(c.type==="globstar"){let L=l.braces>0&&(m.type==="comma"||m.type==="brace"),d=m.extglob===!0||H.length&&(m.type==="pipe"||m.type==="paren");m.type!=="slash"&&m.type!=="paren"&&!L&&!d&&(l.output=l.output.slice(0,-c.output.length),c.type="star",c.value="*",c.output=M,l.output+=c.output)}if(H.length&&m.type!=="paren"&&(H[H.length-1].inner+=m.value),(m.value||m.output)&&Ce(m),c&&c.type==="text"&&m.type==="text"){c.value+=m.value,c.output=(c.output||"")+m.value;return}m.prev=c,i.push(m),c=m},Se=(m,L)=>{let d=Q(B({},f[L]),{conditions:1,inner:""});d.prev=c,d.parens=l.parens,d.output=l.output;let S=(r.capture?"(":"")+d.open;we("parens"),v({type:m,value:L,output:l.output?"":y}),v({type:"paren",extglob:!0,value:ee(),output:S}),H.push(d)},Cr=m=>{let L=m.close+(r.capture?")":""),d;if(m.type==="negate"){let S=M;m.inner&&m.inner.length>1&&m.inner.includes("/")&&(S=E(r)),(S!==M||I()||/^\)+$/.test(se()))&&(L=m.close=`)$))${S}`),m.inner.includes("*")&&(d=se())&&/^\.[^\\/.]+$/.test(d)&&(L=m.close=`)${d})${S})`),m.prev.type==="bos"&&(l.negatedExtglob=!0)}v({type:"paren",extglob:!0,value:u,output:L}),ue("parens")};if(r.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(e)){let m=!1,L=e.replace(Wn,(d,S,P,F,q,Me)=>F==="\\"?(m=!0,d):F==="?"?S?S+F+(q?O.repeat(q.length):""):Me===0?C+(q?O.repeat(q.length):""):O.repeat(P.length):F==="."?A.repeat(P.length):F==="*"?S?S+F+(q?M:""):M:S?d:`\\${d}`);return m===!0&&(r.unescape===!0?L=L.replace(/\\/g,""):L=L.replace(/\\+/g,d=>d.length%2==0?"\\\\":d?"\\":"")),L===e&&r.contains===!0?(l.output=e,l):(l.output=J.wrapOutput(L,l,t),l)}for(;!I();){if(u=ee(),u==="\0")continue;if(u==="\\"){let d=$();if(d==="/"&&r.bash!==!0||d==="."||d===";")continue;if(!d){u+="\\",v({type:"text",value:u});continue}let S=/^\\+/.exec(se()),P=0;if(S&&S[0].length>2&&(P=S[0].length,l.index+=P,P%2!=0&&(u+="\\")),r.unescape===!0?u=ee():u+=ee(),l.brackets===0){v({type:"text",value:u});continue}}if(l.brackets>0&&(u!=="]"||c.value==="["||c.value==="[^")){if(r.posix!==!1&&u===":"){let d=c.value.slice(1);if(d.includes("[")&&(c.posix=!0,d.includes(":"))){let S=c.value.lastIndexOf("["),P=c.value.slice(0,S),F=c.value.slice(S+2),q=qn[F];if(q){c.value=P+q,l.backtrack=!0,ee(),!a.output&&i.indexOf(c)===1&&(a.output=y);continue}}}(u==="["&&$()!==":"||u==="-"&&$()==="]")&&(u=`\\${u}`),u==="]"&&(c.value==="["||c.value==="[^")&&(u=`\\${u}`),r.posix===!0&&u==="!"&&c.value==="["&&(u="^"),c.value+=u,Ce({value:u});continue}if(l.quotes===1&&u!=='"'){u=J.escapeRegex(u),c.value+=u,Ce({value:u});continue}if(u==='"'){l.quotes=l.quotes===1?0:1,r.keepQuotes===!0&&v({type:"text",value:u});continue}if(u==="("){we("parens"),v({type:"paren",value:u});continue}if(u===")"){if(l.parens===0&&r.strictBrackets===!0)throw new SyntaxError(de("opening","("));let d=H[H.length-1];if(d&&l.parens===d.parens+1){Cr(H.pop());continue}v({type:"paren",value:u,output:l.parens?")":"\\)"}),ue("parens");continue}if(u==="["){if(r.nobracket===!0||!se().includes("]")){if(r.nobracket!==!0&&r.strictBrackets===!0)throw new SyntaxError(de("closing","]"));u=`\\${u}`}else we("brackets");v({type:"bracket",value:u});continue}if(u==="]"){if(r.nobracket===!0||c&&c.type==="bracket"&&c.value.length===1){v({type:"text",value:u,output:`\\${u}`});continue}if(l.brackets===0){if(r.strictBrackets===!0)throw new SyntaxError(de("opening","["));v({type:"text",value:u,output:`\\${u}`});continue}ue("brackets");let d=c.value.slice(1);if(c.posix!==!0&&d[0]==="^"&&!d.includes("/")&&(u=`/${u}`),c.value+=u,Ce({value:u}),r.literalBrackets===!1||J.hasRegexChars(d))continue;let S=J.escapeRegex(c.value);if(l.output=l.output.slice(0,-c.value.length),r.literalBrackets===!0){l.output+=S,c.value=S;continue}c.value=`(${o}${S}|${c.value})`,l.output+=c.value;continue}if(u==="{"&&r.nobrace!==!0){we("braces");let d={type:"brace",value:u,output:"(",outputIndex:l.output.length,tokensIndex:l.tokens.length};w.push(d),v(d);continue}if(u==="}"){let d=w[w.length-1];if(r.nobrace===!0||!d){v({type:"text",value:u,output:u});continue}let S=")";if(d.dots===!0){let P=i.slice(),F=[];for(let q=P.length-1;q>=0&&(i.pop(),P[q].type!=="brace");q--)P[q].type!=="dots"&&F.unshift(P[q].value);S=jn(F,r),l.backtrack=!0}if(d.comma!==!0&&d.dots!==!0){let P=l.output.slice(0,d.outputIndex),F=l.tokens.slice(d.tokensIndex);d.value=d.output="\\{",u=S="\\}",l.output=P;for(let q of F)l.output+=q.output||q.value}v({type:"brace",value:u,output:S}),ue("braces"),w.pop();continue}if(u==="|"){H.length>0&&H[H.length-1].conditions++,v({type:"text",value:u});continue}if(u===","){let d=u,S=w[w.length-1];S&&j[j.length-1]==="braces"&&(S.comma=!0,d="|"),v({type:"comma",value:u,output:d});continue}if(u==="/"){if(c.type==="dot"&&l.index===l.start+1){l.start=l.index+1,l.consumed="",l.output="",i.pop(),c=a;continue}v({type:"slash",value:u,output:k});continue}if(u==="."){if(l.braces>0&&c.type==="dot"){c.value==="."&&(c.output=A);let d=w[w.length-1];c.type="dots",c.output+=u,c.value+=u,d.dots=!0;continue}if(l.braces+l.parens===0&&c.type!=="bos"&&c.type!=="slash"){v({type:"text",value:u,output:A});continue}v({type:"dot",value:u,output:A});continue}if(u==="?"){if(!(c&&c.value==="(")&&r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Se("qmark",u);continue}if(c&&c.type==="paren"){let S=$(),P=u;if(S==="<"&&!J.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(c.value==="("&&!/[!=<:]/.test(S)||S==="<"&&!/<([!=]|\w+>)/.test(se()))&&(P=`\\${u}`),v({type:"text",value:u,output:P});continue}if(r.dot!==!0&&(c.type==="slash"||c.type==="bos")){v({type:"qmark",value:u,output:W});continue}v({type:"qmark",value:u,output:O});continue}if(u==="!"){if(r.noextglob!==!0&&$()==="("&&($(2)!=="?"||!/[!=<:]/.test($(3)))){Se("negate",u);continue}if(r.nonegate!==!0&&l.index===0){xr();continue}}if(u==="+"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Se("plus",u);continue}if(c&&c.value==="("||r.regex===!1){v({type:"plus",value:u,output:p});continue}if(c&&(c.type==="bracket"||c.type==="paren"||c.type==="brace")||l.parens>0){v({type:"plus",value:u});continue}v({type:"plus",value:p});continue}if(u==="@"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){v({type:"at",extglob:!0,value:u,output:""});continue}v({type:"text",value:u});continue}if(u!=="*"){(u==="$"||u==="^")&&(u=`\\${u}`);let d=Kn.exec(se());d&&(u+=d[0],l.index+=d[0].length),v({type:"text",value:u});continue}if(c&&(c.type==="globstar"||c.star===!0)){c.type="star",c.star=!0,c.value+=u,c.output=M,l.backtrack=!0,l.globstar=!0,z(u);continue}let m=se();if(r.noextglob!==!0&&/^\([^?]/.test(m)){Se("star",u);continue}if(c.type==="star"){if(r.noglobstar===!0){z(u);continue}let d=c.prev,S=d.prev,P=d.type==="slash"||d.type==="bos",F=S&&(S.type==="star"||S.type==="globstar");if(r.bash===!0&&(!P||m[0]&&m[0]!=="/")){v({type:"star",value:u,output:""});continue}let q=l.braces>0&&(d.type==="comma"||d.type==="brace"),Me=H.length&&(d.type==="pipe"||d.type==="paren");if(!P&&d.type!=="paren"&&!q&&!Me){v({type:"star",value:u,output:""});continue}for(;m.slice(0,3)==="/**";){let ve=e[l.index+4];if(ve&&ve!=="/")break;m=m.slice(3),z("/**",3)}if(d.type==="bos"&&I()){c.type="globstar",c.value+=u,c.output=E(r),l.output=c.output,l.globstar=!0,z(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&!F&&I()){l.output=l.output.slice(0,-(d.output+c.output).length),d.output=`(?:${d.output}`,c.type="globstar",c.output=E(r)+(r.strictSlashes?")":"|$)"),c.value+=u,l.globstar=!0,l.output+=d.output+c.output,z(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&m[0]==="/"){let ve=m[1]!==void 0?"|$":"";l.output=l.output.slice(0,-(d.output+c.output).length),d.output=`(?:${d.output}`,c.type="globstar",c.output=`${E(r)}${k}|${k}${ve})`,c.value+=u,l.output+=d.output+c.output,l.globstar=!0,z(u+ee()),v({type:"slash",value:"/",output:""});continue}if(d.type==="bos"&&m[0]==="/"){c.type="globstar",c.value+=u,c.output=`(?:^|${k}|${E(r)}${k})`,l.output=c.output,l.globstar=!0,z(u+ee()),v({type:"slash",value:"/",output:""});continue}l.output=l.output.slice(0,-c.output.length),c.type="globstar",c.output=E(r),c.value+=u,l.output+=c.output,l.globstar=!0,z(u);continue}let L={type:"star",value:u,output:M};if(r.bash===!0){L.output=".*?",(c.type==="bos"||c.type==="slash")&&(L.output=b+L.output),v(L);continue}if(c&&(c.type==="bracket"||c.type==="paren")&&r.regex===!0){L.output=u,v(L);continue}(l.index===l.start||c.type==="slash"||c.type==="dot")&&(c.type==="dot"?(l.output+=x,c.output+=x):r.dot===!0?(l.output+=T,c.output+=T):(l.output+=b,c.output+=b),$()!=="*"&&(l.output+=y,c.output+=y)),v(L)}for(;l.brackets>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing","]"));l.output=J.escapeLast(l.output,"["),ue("brackets")}for(;l.parens>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing",")"));l.output=J.escapeLast(l.output,"("),ue("parens")}for(;l.braces>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing","}"));l.output=J.escapeLast(l.output,"{"),ue("braces")}if(r.strictSlashes!==!0&&(c.type==="star"||c.type==="bracket")&&v({type:"maybe_slash",value:"",output:`${k}?`}),l.backtrack===!0){l.output="";for(let m of l.tokens)l.output+=m.output!=null?m.output:m.value,m.suffix&&(l.output+=m.suffix)}return l};nr.fastpaths=(e,t)=>{let r=B({},t),n=typeof r.maxLength=="number"?Math.min(Ne,r.maxLength):Ne,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);e=rr[e]||e;let a=J.isWindows(t),{DOT_LITERAL:i,SLASH_LITERAL:o,ONE_CHAR:h,DOTS_SLASH:g,NO_DOT:f,NO_DOTS:A,NO_DOTS_SLASH:p,STAR:k,START_ANCHOR:y}=Oe.globChars(a),R=r.dot?A:f,_=r.dot?p:f,x=r.capture?"":"?:",T={negated:!1,prefix:""},O=r.bash===!0?".*?":k;r.capture&&(O=`(${O})`);let W=b=>b.noglobstar===!0?O:`(${x}(?:(?!${y}${b.dot?g:i}).)*?)`,G=b=>{switch(b){case"*":return`${R}${h}${O}`;case".*":return`${i}${h}${O}`;case"*.*":return`${R}${O}${i}${h}${O}`;case"*/*":return`${R}${O}${o}${h}${_}${O}`;case"**":return R+W(r);case"**/*":return`(?:${R}${W(r)}${o})?${_}${h}${O}`;case"**/*.*":return`(?:${R}${W(r)}${o})?${_}${O}${i}${h}${O}`;case"**/.*":return`(?:${R}${W(r)}${o})?${i}${h}${O}`;default:{let C=/^(.*?)\.(\w+)$/.exec(b);if(!C)return;let M=G(C[1]);return M?M+i+C[2]:void 0}}},ne=J.removePrefix(e,T),E=G(ne);return E&&r.strictSlashes!==!0&&(E+=`${o}?`),E};tr.exports=nr});var ir=K((ys,ar)=>{"use strict";var Fn=require("path"),Qn=er(),Ye=sr(),ze=be(),Xn=ye(),Zn=e=>e&&typeof e=="object"&&!Array.isArray(e),D=(e,t,r=!1)=>{if(Array.isArray(e)){let f=e.map(p=>D(p,t,r));return p=>{for(let k of f){let y=k(p);if(y)return y}return!1}}let n=Zn(e)&&e.tokens&&e.input;if(e===""||typeof e!="string"&&!n)throw new TypeError("Expected pattern to be a non-empty string");let s=t||{},a=ze.isWindows(t),i=n?D.compileRe(e,t):D.makeRe(e,t,!1,!0),o=i.state;delete i.state;let h=()=>!1;if(s.ignore){let f=Q(B({},t),{ignore:null,onMatch:null,onResult:null});h=D(s.ignore,f,r)}let g=(f,A=!1)=>{let{isMatch:p,match:k,output:y}=D.test(f,i,t,{glob:e,posix:a}),R={glob:e,state:o,regex:i,posix:a,input:f,output:y,match:k,isMatch:p};return typeof s.onResult=="function"&&s.onResult(R),p===!1?(R.isMatch=!1,A?R:!1):h(f)?(typeof s.onIgnore=="function"&&s.onIgnore(R),R.isMatch=!1,A?R:!1):(typeof s.onMatch=="function"&&s.onMatch(R),A?R:!0)};return r&&(g.state=o),g};D.test=(e,t,r,{glob:n,posix:s}={})=>{if(typeof e!="string")throw new TypeError("Expected input to be a string");if(e==="")return{isMatch:!1,output:""};let a=r||{},i=a.format||(s?ze.toPosixSlashes:null),o=e===n,h=o&&i?i(e):e;return o===!1&&(h=i?i(e):e,o=h===n),(o===!1||a.capture===!0)&&(a.matchBase===!0||a.basename===!0?o=D.matchBase(e,t,r,s):o=t.exec(h)),{isMatch:Boolean(o),match:o,output:h}};D.matchBase=(e,t,r,n=ze.isWindows(r))=>(t instanceof RegExp?t:D.makeRe(t,r)).test(Fn.basename(e));D.isMatch=(e,t,r)=>D(t,r)(e);D.parse=(e,t)=>Array.isArray(e)?e.map(r=>D.parse(r,t)):Ye(e,Q(B({},t),{fastpaths:!1}));D.scan=(e,t)=>Qn(e,t);D.compileRe=(e,t,r=!1,n=!1)=>{if(r===!0)return e.output;let s=t||{},a=s.contains?"":"^",i=s.contains?"":"$",o=`${a}(?:${e.output})${i}`;e&&e.negated===!0&&(o=`^(?!${o}).*$`);let h=D.toRegex(o,t);return n===!0&&(h.state=e),h};D.makeRe=(e,t={},r=!1,n=!1)=>{if(!e||typeof e!="string")throw new TypeError("Expected a non-empty string");let s={negated:!1,fastpaths:!0};return t.fastpaths!==!1&&(e[0]==="."||e[0]==="*")&&(s.output=Ye.fastpaths(e,t)),s.output||(s=Ye(e,t)),D.compileRe(s,t,r,n)};D.toRegex=(e,t)=>{try{let r=t||{};return new RegExp(e,r.flags||(r.nocase?"i":""))}catch(r){if(t&&t.debug===!0)throw r;return/$^/}};D.constants=Xn;ar.exports=D});var cr=K((bs,or)=>{"use strict";or.exports=ir()});var hr=K((_s,ur)=>{"use strict";var lr=require("util"),pr=Gt(),oe=cr(),Ve=be(),fr=e=>e===""||e==="./",N=(e,t,r)=>{t=[].concat(t),e=[].concat(e);let n=new Set,s=new Set,a=new Set,i=0,o=f=>{a.add(f.output),r&&r.onResult&&r.onResult(f)};for(let f=0;f!n.has(f));if(r&&g.length===0){if(r.failglob===!0)throw new Error(`No matches found for "${t.join(", ")}"`);if(r.nonull===!0||r.nullglob===!0)return r.unescape?t.map(f=>f.replace(/\\/g,"")):t}return g};N.match=N;N.matcher=(e,t)=>oe(e,t);N.isMatch=(e,t,r)=>oe(t,r)(e);N.any=N.isMatch;N.not=(e,t,r={})=>{t=[].concat(t).map(String);let n=new Set,s=[],a=o=>{r.onResult&&r.onResult(o),s.push(o.output)},i=N(e,t,Q(B({},r),{onResult:a}));for(let o of s)i.includes(o)||n.add(o);return[...n]};N.contains=(e,t,r)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${lr.inspect(e)}"`);if(Array.isArray(t))return t.some(n=>N.contains(e,n,r));if(typeof t=="string"){if(fr(e)||fr(t))return!1;if(e.includes(t)||e.startsWith("./")&&e.slice(2).includes(t))return!0}return N.isMatch(e,t,Q(B({},r),{contains:!0}))};N.matchKeys=(e,t,r)=>{if(!Ve.isObject(e))throw new TypeError("Expected the first argument to be an object");let n=N(Object.keys(e),t,r),s={};for(let a of n)s[a]=e[a];return s};N.some=(e,t,r)=>{let n=[].concat(e);for(let s of[].concat(t)){let a=oe(String(s),r);if(n.some(i=>a(i)))return!0}return!1};N.every=(e,t,r)=>{let n=[].concat(e);for(let s of[].concat(t)){let a=oe(String(s),r);if(!n.every(i=>a(i)))return!1}return!0};N.all=(e,t,r)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${lr.inspect(e)}"`);return[].concat(t).every(n=>oe(n,r)(e))};N.capture=(e,t,r)=>{let n=Ve.isWindows(r),a=oe.makeRe(String(e),Q(B({},r),{capture:!0})).exec(n?Ve.toPosixSlashes(t):t);if(a)return a.slice(1).map(i=>i===void 0?"":i)};N.makeRe=(...e)=>oe.makeRe(...e);N.scan=(...e)=>oe.scan(...e);N.parse=(e,t)=>{let r=[];for(let n of[].concat(e||[]))for(let s of pr(String(n),t))r.push(oe.parse(s,t));return r};N.braces=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return t&&t.nobrace===!0||!/\{.*\}/.test(e)?[e]:pr(e,t)};N.braceExpand=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return N.braces(e,Q(B({},t),{expand:!0}))};ur.exports=N});var gr=K((Es,dr)=>{"use strict";dr.exports=(e,...t)=>new Promise(r=>{r(e(...t))})});var Ar=K((xs,Je)=>{"use strict";var Yn=gr(),mr=e=>{if(e<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let t=[],r=0,n=()=>{r--,t.length>0&&t.shift()()},s=(o,h,...g)=>{r++;let f=Yn(o,...g);h(f),f.then(n,n)},a=(o,h,...g)=>{rnew Promise(g=>a(o,g,...h));return Object.defineProperties(i,{activeCount:{get:()=>r},pendingCount:{get:()=>t.length}}),i};Je.exports=mr;Je.exports.default=mr});var Vn={};Or(Vn,{default:()=>es});var He=X(require("@yarnpkg/cli")),ae=X(require("@yarnpkg/core")),nt=X(require("@yarnpkg/core")),le=X(require("clipanion")),Ae=class extends He.BaseCommand{constructor(){super(...arguments);this.json=le.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.production=le.Option.Boolean("--production",!1,{description:"Only install regular dependencies by omitting dev dependencies"});this.all=le.Option.Boolean("-A,--all",!1,{description:"Install the entire project"});this.workspaces=le.Option.Rest()}async execute(){let t=await ae.Configuration.find(this.context.cwd,this.context.plugins),{project:r,workspace:n}=await ae.Project.find(t,this.context.cwd),s=await ae.Cache.find(t);await r.restoreInstallState({restoreResolutions:!1});let a;if(this.all)a=new Set(r.workspaces);else if(this.workspaces.length===0){if(!n)throw new He.WorkspaceRequiredError(r.cwd,this.context.cwd);a=new Set([n])}else a=new Set(this.workspaces.map(o=>r.getWorkspaceByIdent(nt.structUtils.parseIdent(o))));for(let o of a)for(let h of this.production?["dependencies"]:ae.Manifest.hardDependencies)for(let g of o.manifest.getForScope(h).values()){let f=r.tryWorkspaceByDescriptor(g);f!==null&&a.add(f)}for(let o of r.workspaces)a.has(o)?this.production&&o.manifest.devDependencies.clear():(o.manifest.installConfig=o.manifest.installConfig||{},o.manifest.installConfig.selfReferences=!1,o.manifest.dependencies.clear(),o.manifest.devDependencies.clear(),o.manifest.peerDependencies.clear(),o.manifest.scripts.clear());return(await ae.StreamReport.start({configuration:t,json:this.json,stdout:this.context.stdout,includeLogs:!0},async o=>{await r.install({cache:s,report:o,persistProject:!1})})).exitCode()}};Ae.paths=[["workspaces","focus"]],Ae.usage=le.Command.Usage({category:"Workspace-related commands",description:"install a single workspace and its dependencies",details:"\n This command will run an install as if the specified workspaces (and all other workspaces they depend on) were the only ones in the project. If no workspaces are explicitly listed, the active one will be assumed.\n\n Note that this command is only very moderately useful when using zero-installs, since the cache will contain all the packages anyway - meaning that the only difference between a full install and a focused install would just be a few extra lines in the `.pnp.cjs` file, at the cost of introducing an extra complexity.\n\n If the `-A,--all` flag is set, the entire project will be installed. Combine with `--production` to replicate the old `yarn install --production`.\n "});var st=Ae;var Ie=X(require("@yarnpkg/cli")),ge=X(require("@yarnpkg/core")),Ee=X(require("@yarnpkg/core")),Y=X(require("@yarnpkg/core")),Rr=X(require("@yarnpkg/plugin-git")),U=X(require("clipanion")),Be=X(hr()),yr=X(require("os")),br=X(Ar()),re=X(require("typanion")),xe=class extends Ie.BaseCommand{constructor(){super(...arguments);this.recursive=U.Option.Boolean("-R,--recursive",!1,{description:"Find packages via dependencies/devDependencies instead of using the workspaces field"});this.from=U.Option.Array("--from",[],{description:"An array of glob pattern idents from which to base any recursion"});this.all=U.Option.Boolean("-A,--all",!1,{description:"Run the command on all workspaces of a project"});this.verbose=U.Option.Boolean("-v,--verbose",!1,{description:"Prefix each output line with the name of the originating workspace"});this.parallel=U.Option.Boolean("-p,--parallel",!1,{description:"Run the commands in parallel"});this.interlaced=U.Option.Boolean("-i,--interlaced",!1,{description:"Print the output of commands in real-time instead of buffering it"});this.jobs=U.Option.String("-j,--jobs",{description:"The maximum number of parallel tasks that the execution will be limited to; or `unlimited`",validator:re.isOneOf([re.isEnum(["unlimited"]),re.applyCascade(re.isNumber(),[re.isInteger(),re.isAtLeast(1)])])});this.topological=U.Option.Boolean("-t,--topological",!1,{description:"Run the command after all workspaces it depends on (regular) have finished"});this.topologicalDev=U.Option.Boolean("--topological-dev",!1,{description:"Run the command after all workspaces it depends on (regular + dev) have finished"});this.include=U.Option.Array("--include",[],{description:"An array of glob pattern idents; only matching workspaces will be traversed"});this.exclude=U.Option.Array("--exclude",[],{description:"An array of glob pattern idents; matching workspaces won't be traversed"});this.publicOnly=U.Option.Boolean("--no-private",{description:"Avoid running the command on private workspaces"});this.since=U.Option.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.commandName=U.Option.String();this.args=U.Option.Proxy()}async execute(){let t=await ge.Configuration.find(this.context.cwd,this.context.plugins),{project:r,workspace:n}=await ge.Project.find(t,this.context.cwd);if(!this.all&&!n)throw new Ie.WorkspaceRequiredError(r.cwd,this.context.cwd);await r.restoreInstallState();let s=this.cli.process([this.commandName,...this.args]),a=s.path.length===1&&s.path[0]==="run"&&typeof s.scriptName!="undefined"?s.scriptName:null;if(s.path.length===0)throw new U.UsageError("Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script");let i=this.all?r.topLevelWorkspace:n,o=this.since?Array.from(await Rr.gitUtils.fetchChangedWorkspaces({ref:this.since,project:r})):[i,...this.from.length>0?i.getRecursiveWorkspaceChildren():[]],h=E=>Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.from),g=this.from.length>0?o.filter(h):o,f=new Set([...g,...g.map(E=>[...this.recursive?this.since?E.getRecursiveWorkspaceDependents():E.getRecursiveWorkspaceDependencies():E.getRecursiveWorkspaceChildren()]).flat()]),A=[],p=!1;if(a==null?void 0:a.includes(":")){for(let E of r.workspaces)if(E.manifest.scripts.has(a)&&(p=!p,p===!1))break}for(let E of f)a&&!E.manifest.scripts.has(a)&&!p&&!(await ge.scriptUtils.getWorkspaceAccessibleBinaries(E)).has(a)||a===process.env.npm_lifecycle_event&&E.cwd===n.cwd||this.include.length>0&&!Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.include)||this.exclude.length>0&&Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.exclude)||this.publicOnly&&E.manifest.private===!0||A.push(E);let k=this.parallel?this.jobs==="unlimited"?Infinity:this.jobs||Math.max(1,(0,yr.cpus)().length/2):1,y=k===1?!1:this.parallel,R=y?this.interlaced:!0,_=(0,br.default)(k),x=new Map,T=new Set,O=0,W=null,G=!1,ne=await Ee.StreamReport.start({configuration:t,stdout:this.context.stdout},async E=>{let b=async(C,{commandIndex:M})=>{if(G)return-1;!y&&this.verbose&&M>1&&E.reportSeparator();let l=zn(C,{configuration:t,verbose:this.verbose,commandIndex:M}),[H,w]=_r(E,{prefix:l,interlaced:R}),[j,c]=_r(E,{prefix:l,interlaced:R});try{this.verbose&&E.reportInfo(null,`${l} Process started`);let u=Date.now(),I=await this.cli.run([this.commandName,...this.args],{cwd:C.cwd,stdout:H,stderr:j})||0;H.end(),j.end(),await w,await c;let $=Date.now();if(this.verbose){let ee=t.get("enableTimers")?`, completed in ${Y.formatUtils.pretty(t,$-u,Y.formatUtils.Type.DURATION)}`:"";E.reportInfo(null,`${l} Process exited (exit code ${I})${ee}`)}return I===130&&(G=!0,W=I),I}catch(u){throw H.end(),j.end(),await w,await c,u}};for(let C of A)x.set(C.anchoredLocator.locatorHash,C);for(;x.size>0&&!E.hasErrors();){let C=[];for(let[H,w]of x){if(T.has(w.anchoredDescriptor.descriptorHash))continue;let j=!0;if(this.topological||this.topologicalDev){let c=this.topologicalDev?new Map([...w.manifest.dependencies,...w.manifest.devDependencies]):w.manifest.dependencies;for(let u of c.values()){let I=r.tryWorkspaceByDescriptor(u);if(j=I===null||!x.has(I.anchoredLocator.locatorHash),!j)break}}if(!!j&&(T.add(w.anchoredDescriptor.descriptorHash),C.push(_(async()=>{let c=await b(w,{commandIndex:++O});return x.delete(H),T.delete(w.anchoredDescriptor.descriptorHash),c})),!y))break}if(C.length===0){let H=Array.from(x.values()).map(w=>Y.structUtils.prettyLocator(t,w.anchoredLocator)).join(", ");E.reportError(Ee.MessageName.CYCLIC_DEPENDENCIES,`Dependency cycle detected (${H})`);return}let l=(await Promise.all(C)).find(H=>H!==0);W===null&&(W=typeof l!="undefined"?1:W),(this.topological||this.topologicalDev)&&typeof l!="undefined"&&E.reportError(Ee.MessageName.UNNAMED,"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph")}});return W!==null?W:ne.exitCode()}};xe.paths=[["workspaces","foreach"]],xe.usage=U.Command.Usage({category:"Workspace-related commands",description:"run a command on all workspaces",details:"\n This command will run a given sub-command on current and all its descendant workspaces. Various flags can alter the exact behavior of the command:\n\n - If `-p,--parallel` is set, the commands will be ran in parallel; they'll by default be limited to a number of parallel tasks roughly equal to half your core number, but that can be overridden via `-j,--jobs`, or disabled by setting `-j unlimited`.\n\n - If `-p,--parallel` and `-i,--interlaced` are both set, Yarn will print the lines from the output as it receives them. If `-i,--interlaced` wasn't set, it would instead buffer the output from each process and print the resulting buffers only after their source processes have exited.\n\n - If `-t,--topological` is set, Yarn will only run the command after all workspaces that it depends on through the `dependencies` field have successfully finished executing. If `--topological-dev` is set, both the `dependencies` and `devDependencies` fields will be considered when figuring out the wait points.\n\n - If `-A,--all` is set, Yarn will run the command on all the workspaces of a project. By default yarn runs the command only on current and all its descendant workspaces.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `--from` is set, Yarn will use the packages matching the 'from' glob as the starting point for any recursive search.\n\n - If `--since` is set, Yarn will only run the command on workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - The command may apply to only some workspaces through the use of `--include` which acts as a whitelist. The `--exclude` flag will do the opposite and will be a list of packages that mustn't execute the script. Both flags accept glob patterns (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n Adding the `-v,--verbose` flag will cause Yarn to print more information; in particular the name of the workspace that generated the output will be printed at the front of each line.\n\n If the command is `run` and the script being run does not exist the child workspace will be skipped without error.\n ",examples:[["Publish current and all descendant packages","yarn workspaces foreach npm publish --tolerate-republish"],["Run build script on current and all descendant packages","yarn workspaces foreach run build"],["Run build script on current and all descendant packages in parallel, building package dependencies first","yarn workspaces foreach -pt run build"],["Run build script on several packages and all their dependencies, building dependencies first","yarn workspaces foreach -ptR --from '{workspace-a,workspace-b}' run build"]]});var Er=xe;function _r(e,{prefix:t,interlaced:r}){let n=e.createStreamReporter(t),s=new Y.miscUtils.DefaultStream;s.pipe(n,{end:!1}),s.on("finish",()=>{n.end()});let a=new Promise(o=>{n.on("finish",()=>{o(s.active)})});if(r)return[s,a];let i=new Y.miscUtils.BufferStream;return i.pipe(s,{end:!1}),i.on("finish",()=>{s.end()}),[i,a]}function zn(e,{configuration:t,commandIndex:r,verbose:n}){if(!n)return null;let s=Y.structUtils.convertToIdent(e.locator),i=`[${Y.structUtils.stringifyIdent(s)}]:`,o=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],h=o[r%o.length];return Y.formatUtils.pretty(t,i,h)}var Jn={commands:[st,Er]},es=Jn;return Vn;})(); +/*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + */ +/*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + */ +/*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + */ +return plugin; +} +}; diff --git a/.yarnrc.yml b/.yarnrc.yml index a8e3efbd72..aae00fefcc 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -9,5 +9,9 @@ nodeLinker: node-modules plugins: - path: .yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs spec: "https://raw.githubusercontent.com/LavaMoat/LavaMoat/main/packages/yarn-plugin-allow-scripts/bundles/@yarnpkg/plugin-allow-scripts.js" + - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs + spec: "@yarnpkg/plugin-workspace-tools" + - path: .yarn/plugins/@yarnpkg/plugin-constraints.cjs + spec: "@yarnpkg/plugin-constraints" yarnPath: .yarn/releases/yarn-3.2.1.cjs diff --git a/CHANGELOG.md b/CHANGELOG.old.md similarity index 100% rename from CHANGELOG.md rename to CHANGELOG.old.md diff --git a/README.md b/README.md index 2abd81da06..bea88e0723 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,30 @@ -# `@metamask/controllers` +# Controllers A collection of platform-agnostic modules for creating secure data models for cryptocurrency wallets. -## Installation - -```sh -yarn add @metamask/controllers -``` - -or - -```sh -npm install @metamask/controllers -``` +## Modules + +This is a monorepo that houses the following packages. Please refer to the READMEs for these packages for installation and usage instructions: + +- [`@metamask/address-book-controller`](packages/address-book-controller) +- [`@metamask/announcement-controller`](packages/announcement-controller) +- [`@metamask/approval-controller`](packages/approval-controller) +- [`@metamask/assets-controller`](packages/assets-controller) +- [`@metamask/base-controller`](packages/base-controller) +- [`@metamask/composable-controller`](packages/composable-controller) +- [`@metamask/controller-utils`](packages/controller-utils) +- [`@metamask/ens-controller`](packages/ens-controller) +- [`@metamask/gas-fee-controller`](packages/gas-fee-controller) +- [`@metamask/keyring-controller`](packages/keyring-controller) +- [`@metamask/message-manager`](packages/message-manager) +- [`@metamask/network-controller`](packages/network-controller) +- [`@metamask/notification-controller`](packages/notification-controller) +- [`@metamask/permission-controller`](packages/permission-controller) +- [`@metamask/phishing-controller`](packages/phishing-controller) +- [`@metamask/preferences-controller`](packages/preferences-controller) +- [`@metamask/rate-limit-controller`](packages/rate-limit-controller) +- [`@metamask/subject-metadata-controller`](packages/subject-metadata-controller) +- [`@metamask/transaction-controller`](packages/transaction-controller) ## Contributing @@ -26,50 +38,32 @@ npm install @metamask/controllers ### Testing and Linting -Run `yarn lint` to run the linter, or run `yarn lint:fix` to run the linter and fix any automatically fixable issues. - -Run `yarn test` to run the tests once. To run tests on file changes, run `yarn test:watch`. - -To enable debugger [via Chrome DevTools](https://jestjs.io/docs/troubleshooting#tests-are-failing-and-you-dont-know-why): +Run `yarn test` to run tests for all packages. Run `yarn workspace run test` to run tests for a single package. -1. run `yarn test:debug` -2. navigate to `chrome://inspect` -3. click "Open Dedicated DevTools for Node". Keep the DevTools window open -4. stop/rerun the tests as needed +Run `yarn lint` to lint all files and show possible violations, or run `yarn lint:fix` to fix any automatically fixable violations. ### Release & Publishing -The project follows the same release process as the other libraries in the MetaMask organization. The GitHub Actions [`action-create-release-pr`](https://github.com/MetaMask/action-create-release-pr) and [`action-publish-release`](https://github.com/MetaMask/action-publish-release) are used to automate the release process; see those repositories for more information about how they work. - -1. Choose a release version. - - - The release version should be chosen according to SemVer. Analyze the changes to see whether they include any breaking changes, new features, or deprecations, then choose the appropriate SemVer version. See [the SemVer specification](https://semver.org/) for more information. - -2. If this release is backporting changes onto a previous release, then ensure there is a major version branch for that version (e.g. `1.x` for a `v1` backport release). - - - The major version branch should be set to the most recent release with that major version. For example, when backporting a `v1.0.2` release, you'd want to ensure there was a `1.x` branch that was set to the `v1.0.1` tag. - -3. Trigger the [`workflow_dispatch`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch) event [manually](https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow) for the `Create Release Pull Request` action to create the release PR. +This project follows a unique release process. The [`create-release-branch`](https://github.com/MetaMask/create-release-branch) tool and [`action-publish-release`](https://github.com/MetaMask/action-publish-release) GitHub action are used to automate the release process; see those repositories for more information about how they work. - - For a backport release, the base branch should be the major version branch that you ensured existed in step 2. For a normal release, the base branch should be the main branch for that repository (which should be the default value). - - This should trigger the [`action-create-release-pr`](https://github.com/MetaMask/action-create-release-pr) workflow to create the release PR. +1. To begin the release process, run `create-release-branch`, specifying the packages you want to release. This tool will bump versions and update changelogs across the monorepo automatically, then create a new branch for you. -4. Update the changelog to move each change entry into the appropriate change category ([See here](https://keepachangelog.com/en/1.0.0/#types) for the full list of change categories, and the correct ordering), and edit them to be more easily understood by users of the package. +2. Once you have a new release branch, review the set of package changelogs that the tool has updated. For each changelog, update it to move each change entry into the appropriate change category ([see here](https://keepachangelog.com/en/1.0.0/#types) for the full list of change categories and the correct ordering), and edit them to be more easily understood by users of the package. - Generally any changes that don't affect consumers of the package (e.g. lockfile changes or development environment changes) are omitted. Exceptions may be made for changes that might be of interest despite not having an effect upon the published package (e.g. major test improvements, security improvements, improved documentation, etc.). - Try to explain each change in terms that users of the package would understand (e.g. avoid referencing internal variables/concepts). - Consolidate related changes into one change entry if it makes it easier to explain. - Run `yarn auto-changelog validate --rc` to check that the changelog is correctly formatted. -5. Review and QA the release. +3. Submit a pull request for the release branch, so that it can be reviewed and tested. - If changes are made to the base branch, the release branch will need to be updated with these changes and review/QA will need to restart again. As such, it's probably best to avoid merging other PRs into the base branch while review is underway. -6. Squash & Merge the release. +4. Squash & Merge the release. - This should trigger the [`action-publish-release`](https://github.com/MetaMask/action-publish-release) workflow to tag the final release commit and publish the release on GitHub. -7. Publish the release on npm. +5. Publish the release on npm. - Wait for the `publish-release` GitHub Action workflow to finish. This should trigger a second job (`publish-npm`), which will wait for a run approval by the [`npm publishers`](https://github.com/orgs/MetaMask/teams/npm-publishers) team. - Approve the `publish-npm` job (or ask somebody on the npm publishers team to approve it for you). diff --git a/__mocks__/uuid.js b/__mocks__/uuid.js deleted file mode 100644 index 5f17ea96d7..0000000000 --- a/__mocks__/uuid.js +++ /dev/null @@ -1,14 +0,0 @@ -const uuid = require('uuid'); - -// mock the v4 function of uuid lib to make sure it returns the fixed id for testing -const v4 = () => '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'; - -module.exports.NIL = uuid.NIL; -module.exports.v1 = uuid.v1; -module.exports.v3 = uuid.v3; -module.exports.v5 = uuid.v5; -module.exports.parse = uuid.parse; -module.exports.validate = uuid.validate; -module.exports.stringify = uuid.stringify; - -module.exports.v4 = v4; diff --git a/constraints.pro b/constraints.pro new file mode 100644 index 0000000000..3e20e004c8 --- /dev/null +++ b/constraints.pro @@ -0,0 +1,248 @@ +%%%%% +% Utility predicates +%%%%% + +% True if and only if VersionRange is a value that we would expect to see +% following a package in a "*dependencies" field within a `package.json`. +is_valid_version_range(VersionRange) :- + VersionRange = 'workspace:^'; + VersionRange = 'workspace:~'; + parse_version_range(VersionRange, _, _, _, _). + +% Succeeds if Number can be unified with Atom converted to a number; throws if +% not. +atom_to_number(Atom, Number) :- + atom_chars(Atom, Chars), + number_chars(Number, Chars). + +% True if and only if Atom can be converted to a number. +is_atom_number(Atom) :- + catch(atom_to_number(Atom, _), _, false). + +% True if and only if Modifier can be unified with the leading character of the +% version range ("^" or "~" if present, or "" if not present), Major can be +% unified with the major part of the version string, Minor with the minor, and +% Patch with the patch. +parse_version_range(VersionRange, Modifier, Major, Minor, Patch) :- + % Identify and extract the modifier (^ or ~) from the version string + atom_chars(VersionRange, Chars), + Chars = [PossibleModifier | CharsWithoutPossibleModifier], + ( + ( + PossibleModifier = '^'; + PossibleModifier = '~' + ) -> + ( + Modifier = PossibleModifier, + CharsWithoutModifier = CharsWithoutPossibleModifier + ) ; + ( + is_atom_number(PossibleModifier) -> + ( + Modifier = '', + CharsWithoutModifier = Chars + ) ; + false + ) + ), + atomic_list_concat(CharsWithoutModifier, '', VersionRangeWithoutModifier), + atomic_list_concat(VersionParts, '.', VersionRangeWithoutModifier), + % Validate version string while extracting each part + length(VersionParts, 3), + nth0(0, VersionParts, MajorAtom), + nth0(1, VersionParts, MinorAtom), + nth0(2, VersionParts, PatchAtom), + atom_to_number(MajorAtom, Major), + atom_to_number(MinorAtom, Minor), + atom_to_number(PatchAtom, Patch). + +% True if and only if the first SemVer version range is greater than the second +% SemVer version range. Such a range must match "^MAJOR.MINOR.PATCH", +% "~MAJOR.MINOR.PATCH", "MAJOR.MINOR.PATCH". If two ranges do not have the same +% modifier ("^" or "~"), then they cannot be compared and the first cannot be +% considered as less than the second. +% +% Borrowed from: +npm_version_range_out_of_sync(VersionRange1, VersionRange2) :- + parse_version_range(VersionRange1, VersionRange1Modifier, VersionRange1Major, VersionRange1Minor, VersionRange1Patch), + parse_version_range(VersionRange2, VersionRange2Modifier, VersionRange2Major, VersionRange2Minor, VersionRange2Patch), + VersionRange1Modifier == VersionRange2Modifier, + ( + % 2.0.0 > 1.0.0 + % 2.0.0 > 1.1.0 + % 2.0.0 > 1.0.1 + VersionRange1Major @> VersionRange2Major ; + ( + VersionRange1Major == VersionRange2Major , + ( + % 1.1.0 > 1.0.0 + % 1.1.0 > 1.0.1 + VersionRange1Minor @> VersionRange2Minor ; + ( + VersionRange1Minor == VersionRange2Minor , + % 1.0.1 > 1.0.0 + VersionRange1Patch @> VersionRange2Patch + ) + ) + ) + ). + +% True if and only if WorkspaceBasename can unify with the part of the given +% workspace directory name that results from removing all leading directories. +workspace_basename(WorkspaceCwd, WorkspaceBasename) :- + atomic_list_concat(Parts, '/', WorkspaceCwd), + last(Parts, WorkspaceBasename). + +% True if and only if WorkspacePackageName can unify with the name of the +% package which the workspace represents (which comes from the directory where +% the package is located). Assumes that the package is not in a sub-workspace +% and is not private. +workspace_package_name(WorkspaceCwd, WorkspacePackageName) :- + workspace_basename(WorkspaceCwd, WorkspaceBasename), + atom_concat('@metamask/', WorkspaceBasename, WorkspacePackageName). + +%%%%% +% Constraints +%%%%% + +% "name" is required for all workspaces (including the root). +\+ gen_enforced_field(WorkspaceCwd, 'name', null). + +% The name of the root package can be anything, but the name of a workspace +% package must match its directory (e.g., a package located in "packages/foo" +% must be called "@metamask/foo"). +gen_enforced_field(WorkspaceCwd, 'name', WorkspacePackageName) :- + \+ workspace_field(WorkspaceCwd, 'private', true), + workspace_package_name(WorkspaceCwd, WorkspacePackageName). + +% "description" is required for all packages. +\+ gen_enforced_field(WorkspaceCwd, 'description', null). + +% The value of "description" cannot end with a period. +gen_enforced_field(WorkspaceCwd, 'description', DescriptionWithoutTrailingPeriod) :- + workspace_field(WorkspaceCwd, 'description', Description), + atom_length(Description, Length), + LengthLessOne is Length - 1, + sub_atom(Description, LengthLessOne, 1, 0, LastCharacter), + sub_atom(Description, 0, LengthLessOne, 1, DescriptionWithoutPossibleTrailingPeriod), + ( + LastCharacter == '.' -> + DescriptionWithoutTrailingPeriod = DescriptionWithoutPossibleTrailingPeriod ; + DescriptionWithoutTrailingPeriod = Description + ). + +% "keywords" must be the same across all workspace packages +% (and must be unset for the root). +gen_enforced_field(WorkspaceCwd, 'keywords', ['MetaMask', 'Ethereum']) :- + \+ workspace_field(WorkspaceCwd, 'private', true). +gen_enforced_field(WorkspaceCwd, 'keywords', null) :- + workspace_field(WorkspaceCwd, 'private', true). + +% "homepage" must match the name of the package (based on the workspace +% directory name) across all workspace packages (and must be unset for the +% root). +gen_enforced_field(WorkspaceCwd, 'homepage', CorrectHomepageUrl) :- + \+ workspace_field(WorkspaceCwd, 'private', true), + workspace_basename(WorkspaceCwd, WorkspaceBasename), + atomic_list_concat(['https://github.com/MetaMask/controllers/tree/main/packages/', WorkspaceBasename, '#readme'], CorrectHomepageUrl). +gen_enforced_field(WorkspaceCwd, 'homepage', null) :- + workspace_field(WorkspaceCwd, 'private', true). + +% "repository.type" must be "git" for all packages. +gen_enforced_field(WorkspaceCwd, 'repository.type', 'git'). + +% "repository.url" must be "https://github.com/MetaMask/controllers.git" for all +% packages. +gen_enforced_field(WorkspaceCwd, 'repository.url', 'https://github.com/MetaMask/controllers.git'). + +% "license" must be "MIT" for all workspace packages and unset for the root. +gen_enforced_field(WorkspaceCwd, 'license', 'MIT') :- + \+ workspace_field(WorkspaceCwd, 'private', true). +gen_enforced_field(WorkspaceCwd, 'license', null) :- + workspace_field(WorkspaceCwd, 'private', true). + +% "main" must be "dist/index.js" for workspace packages and unset for the +% root. +gen_enforced_field(WorkspaceCwd, 'main', './dist/index.js') :- + \+ workspace_field(WorkspaceCwd, 'private', true). +gen_enforced_field(WorkspaceCwd, 'main', null) :- + workspace_field(WorkspaceCwd, 'private', true). + +% "types" must be "dist/index.d.ts" for workspace packages and unset for the +% root. +gen_enforced_field(WorkspaceCwd, 'types', './dist/index.d.ts') :- + \+ workspace_field(WorkspaceCwd, 'private', true). +gen_enforced_field(WorkspaceCwd, 'types', null) :- + workspace_field(WorkspaceCwd, 'private', true). + +% "files" must be ["dist/"] for workspace packages and unset for the root. +gen_enforced_field(WorkspaceCwd, 'files', ['dist/']) :- + \+ workspace_field(WorkspaceCwd, 'private', true). +gen_enforced_field(WorkspaceCwd, 'files', null) :- + workspace_field(WorkspaceCwd, 'private', true). + +% All workspace packages must have the same "build:docs" script. +gen_enforced_field(WorkspaceCwd, 'scripts.build:docs', 'typedoc') :- + \+ workspace_field(WorkspaceCwd, 'private', true). + +% The "changelog:validate" script for each package must follow a specific +% format. +gen_enforced_field(WorkspaceCwd, 'scripts.changelog:validate', ProperChangelogValidationScript) :- + \+ workspace_field(WorkspaceCwd, 'private', true), + workspace_package_name(WorkspaceCwd, WorkspacePackageName), + atomic_list_concat(['../../scripts/validate-changelog.sh ', WorkspacePackageName], ProperChangelogValidationScript). + +% All workspace packages must have the same "test" script. +gen_enforced_field(WorkspaceCwd, 'scripts.test', 'jest') :- + \+ workspace_field(WorkspaceCwd, 'private', true). + +% All workspace packages must have the same "test:watch" script. +gen_enforced_field(WorkspaceCwd, 'scripts.test:watch', 'jest --watch') :- + \+ workspace_field(WorkspaceCwd, 'private', true). + +% All dependency ranges must be recognizable. +gen_enforced_dependency(WorkspaceCwd, DependencyIdent, 'a range optionally starting with ^ or ~', DependencyType) :- + workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType), + \+ is_valid_version_range(DependencyRange). + +% All dependency ranges for a package must be synchronized across the monorepo +% (the least version range wins), regardless of which "*dependencies" the +% package appears. +gen_enforced_dependency(WorkspaceCwd, DependencyIdent, OtherDependencyRange, DependencyType) :- + workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType), + workspace_has_dependency(OtherWorkspaceCwd, DependencyIdent, OtherDependencyRange, OtherDependencyType), + WorkspaceCwd \= OtherWorkspaceCwd, + DependencyRange \= OtherDependencyRange, + npm_version_range_out_of_sync(DependencyRange, OtherDependencyRange). + +% If a dependency is listed under "dependencies", it should not be listed under +% any other "*dependencies" lists. We match on the same dependency range so that +% if a dependency is listed twice in the same manifest, their versions are +% synchronized and then this constraint will apply and remove the "right" +% duplicate. +gen_enforced_dependency(WorkspaceCwd, DependencyIdent, null, DependencyType) :- + workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, 'dependencies'), + workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType), + DependencyType \= 'dependencies'. + +% eth-query has an unlisted dependency on babel-runtime, so that package needs +% to be present if eth-query is present. +gen_enforced_dependency(WorkspaceCwd, 'babel-runtime', '^6.26.0', DependencyType) :- + workspace_has_dependency(WorkspaceCwd, 'eth-query', _, DependencyType). + +% "engines.node" must be ">=14.0.0" for all packages. +gen_enforced_field(WorkspaceCwd, 'engines.node', '>=14.0.0'). + +% "publishConfig.access" must be "public" for workspace packages and unset +% for the root. +gen_enforced_field(WorkspaceCwd, 'publishConfig.access', 'public') :- + \+ workspace_field(WorkspaceCwd, 'private', true). +gen_enforced_field(WorkspaceCwd, 'publishConfig.access', null) :- + workspace_field(WorkspaceCwd, 'private', true). + +% "publishConfig.registry" must be "https://registry.npmjs.org" for all +% workspace packages and unset for the root. +gen_enforced_field(WorkspaceCwd, 'publishConfig.registry', 'https://registry.npmjs.org/') :- + \+ workspace_field(WorkspaceCwd, 'private', true). +gen_enforced_field(WorkspaceCwd, 'publishConfig.registry', null) :- + workspace_field(WorkspaceCwd, 'private', true). diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 2eb2fe4143..0000000000 --- a/jest.config.js +++ /dev/null @@ -1,35 +0,0 @@ -module.exports = { - collectCoverage: true, - // Ensures that we collect coverage from all source files, not just tested - // ones. - collectCoverageFrom: ['./src/**/*.ts'], - // TODO: Test index.ts - coveragePathIgnorePatterns: ['./src/index.ts'], - coverageReporters: ['text', 'html'], - coverageThreshold: { - global: { - branches: 90, - functions: 96, - lines: 95, - statements: 95, - }, - }, - moduleFileExtensions: ['js', 'json', 'ts', 'node'], - preset: 'ts-jest', - // TODO: Enable resetMocks - // "resetMocks" resets all mocks, including mocked modules, to jest.fn(), - // between each test case. - // resetMocks: true, - // "restoreMocks" restores all mocks created using jest.spyOn to their - // original implementations, between each test. It does not affect mocked - // modules. - restoreMocks: true, - setupFiles: ['./tests/setupTests.ts'], - setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'], - testEnvironment: 'jsdom', - testRegex: ['\\.test\\.(ts|js)$'], - testTimeout: 5000, - transform: { - '^.+\\.tsx?$': 'ts-jest', - }, -}; diff --git a/jest.config.packages.js b/jest.config.packages.js new file mode 100644 index 0000000000..569deb2598 --- /dev/null +++ b/jest.config.packages.js @@ -0,0 +1,202 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/fk/c3y07g0576j8_2s9m01pk4qw0000gn/T/jest_dx", + + // Automatically clear mock calls, instances and results before every test. + // This does not remove any mock implementation that may have been provided, + // so we disable it. + // clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: ['./src/**/*.ts'], + + // The directory where Jest should output its coverage files + coverageDirectory: 'coverage', + + // An array of regexp pattern strings used to skip coverage collection + coveragePathIgnorePatterns: ['./src/index.ts'], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: 'babel', + + // A list of reporter names that Jest uses when writing coverage reports + coverageReporters: ['text', 'html', 'json-summary'], + + // An object that configures minimum threshold enforcement for coverage results + // (Each package defines this separately) + // coverageThreshold: undefined + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // Here we ensure that Jest resolves `@metamask/*` imports to the uncompiled source code for packages that live in this repo. + // NOTE: This must be synchronized with the `paths` option in `tsconfig.packages.json`. + moduleNameMapper: { + '^@metamask/(.+)$': [ + '/../$1/src', + // Some @metamask/* packages we are referencing aren't in this monorepo, + // so in that case use their published versions + '/../../node_modules/@metamask/$1', + ], + }, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + preset: 'ts-jest', + + // Run tests from one or more projects + // projects: undefined + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // "resetMocks" resets all mocks, including mocked modules, to jest.fn(), + // between each test case. + // TODO: Enable + // resetMocks: true, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // "restoreMocks" restores all mocks created using jest.spyOn to their + // original implementations, between each test. It does not affect mocked + // modules. + restoreMocks: true, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + setupFiles: ['../../tests/setup.ts'], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + setupFilesAfterEnv: ['../../tests/setupAfterEnv.ts'], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: 'node', + + // Options that will be passed to the testEnvironment + testEnvironmentOptions: { + customExportConditions: ['node', 'node-addons'], + }, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", + + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: undefined + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; diff --git a/package.json b/package.json index 48407c8152..534824c278 100644 --- a/package.json +++ b/package.json @@ -1,97 +1,39 @@ { - "name": "@metamask/controllers", + "name": "@metamask/controllers-monorepo", "version": "33.0.0", + "private": true, "description": "Collection of platform-agnostic modules for creating secure data models for cryptocurrency wallets", - "keywords": [ - "MetaMask", - "Ethereum" - ], - "homepage": "https://github.com/MetaMask/controllers#readme", - "bugs": { - "url": "https://github.com/MetaMask/controllers/issues" - }, "repository": { "type": "git", "url": "https://github.com/MetaMask/controllers.git" }, - "license": "MIT", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist/" + "workspaces": [ + "packages/*" ], "scripts": { - "build": "rimraf dist && tsc --project tsconfig.build.json", - "build:link": "yarn build && cd dist && yarn link && rm -rf node_modules && cd ..", - "build:watch": "yarn build --watch", - "doc": "typedoc && touch docs/.nojekyll", - "lint": "yarn lint:eslint && yarn lint:misc --check", + "build": "tsc --build tsconfig.build.json --verbose", + "build:clean": "rimraf dist '**/*.tsbuildinfo' && yarn build", + "build:docs": "yarn workspaces foreach --parallel --interlaced --verbose run build:docs", + "build:watch": "yarn run build --watch", + "changelog:validate": "yarn workspaces foreach --parallel --interlaced --verbose run changelog:validate", + "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints", "lint:eslint": "eslint . --cache --ext js,ts", - "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write", - "lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' '**/*.yml' '!.yarnrc.yml' --ignore-path .gitignore", + "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write && yarn constraints --fix", + "lint:misc": "prettier '**/*.json' '**/*.md' '!**/CHANGELOG.md' '!**/CHANGELOG.old.md' '**/*.yml' '!.yarnrc.yml' --ignore-path .gitignore", "prepack": "./scripts/prepack.sh", "setup": "yarn install", - "test": "jest", - "test:debug": "yarn run --inspect-brk jest --runInBand", - "test:watch": "jest --watch" + "test": "yarn workspaces foreach --parallel --verbose run test" }, "simple-git-hooks": { "pre-push": "yarn lint" }, "dependencies": { - "@ethereumjs/common": "^2.3.1", - "@ethereumjs/tx": "^3.2.1", - "@ethersproject/abi": "^5.7.0", - "@ethersproject/contracts": "^5.7.0", - "@ethersproject/providers": "^5.7.0", - "@keystonehq/metamask-airgapped-keyring": "^0.6.1", - "@metamask/contract-metadata": "^1.35.0", - "@metamask/metamask-eth-abis": "3.0.0", - "@metamask/types": "^1.1.0", - "@types/uuid": "^8.3.0", - "abort-controller": "^3.0.0", - "async-mutex": "^0.2.6", - "babel-runtime": "^6.26.0", - "deep-freeze-strict": "^1.1.1", - "eth-ens-namehash": "^2.0.8", - "eth-json-rpc-infura": "^5.1.0", - "eth-keyring-controller": "^7.0.2", - "eth-method-registry": "1.1.0", - "eth-phishing-detect": "^1.2.0", - "eth-query": "^2.1.2", - "eth-rpc-errors": "^4.0.0", - "eth-sig-util": "^3.0.0", - "ethereumjs-util": "^7.0.10", - "ethereumjs-wallet": "^1.0.1", - "ethjs-unit": "^0.1.6", - "fast-deep-equal": "^3.1.3", - "immer": "^9.0.6", - "isomorphic-fetch": "^3.0.0", - "json-rpc-engine": "^6.1.0", - "jsonschema": "^1.2.4", - "multiformats": "^9.5.2", - "nanoid": "^3.1.31", - "punycode": "^2.1.1", - "single-call-balance-checker-abi": "^1.0.0", - "uuid": "^8.3.2", - "web3": "^0.20.7", - "web3-provider-engine": "^16.0.3" - }, - "devDependencies": { - "@keystonehq/bc-ur-registry-eth": "^0.9.0", "@lavamoat/allow-scripts": "^2.0.2", - "@metamask/auto-changelog": "^2.6.0", + "@metamask/create-release-branch": "^1.0.0", "@metamask/eslint-config": "^9.0.0", "@metamask/eslint-config-jest": "^9.0.0", "@metamask/eslint-config-nodejs": "^9.0.0", "@metamask/eslint-config-typescript": "^9.0.1", - "@types/deep-freeze-strict": "^1.1.0", - "@types/jest": "^26.0.22", - "@types/jest-when": "^2.7.3", - "@types/node": "^14.14.31", - "@types/punycode": "^2.1.0", - "@types/sinon": "^9.0.10", - "@types/web3": "^1.0.6", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "eslint": "^7.24.0", @@ -102,29 +44,17 @@ "eslint-plugin-jsdoc": "^36.1.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.4.1", - "ethjs-provider-http": "^0.1.6", - "jest": "^26.4.2", - "jest-environment-jsdom": "^25.0.0", - "jest-when": "^3.4.2", - "nock": "^13.0.7", + "isomorphic-fetch": "^3.0.0", "prettier": "^2.6.2", "prettier-plugin-packagejson": "^2.2.17", "rimraf": "^3.0.2", "simple-git-hooks": "^2.8.0", - "sinon": "^9.2.4", - "ts-jest": "^26.5.2", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", "typescript": "~4.6.3" }, "packageManager": "yarn@3.2.1", "engines": { "node": ">=14.0.0" }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, "lavamoat": { "allowScripts": { "@lavamoat/preinstall-always-fail": false, diff --git a/packages/address-book-controller/CHANGELOG.md b/packages/address-book-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/address-book-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/LICENSE b/packages/address-book-controller/LICENSE similarity index 100% rename from LICENSE rename to packages/address-book-controller/LICENSE diff --git a/packages/address-book-controller/README.md b/packages/address-book-controller/README.md new file mode 100644 index 0000000000..dbb6108c73 --- /dev/null +++ b/packages/address-book-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/address-book-controller` + +Manages a list of recipient addresses associated with nicknames. + +## Installation + +`yarn add @metamask/address-book-controller` + +or + +`npm install @metamask/address-book-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/address-book-controller/jest.config.js b/packages/address-book-controller/jest.config.js new file mode 100644 index 0000000000..a7ae2b4b47 --- /dev/null +++ b/packages/address-book-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/address-book-controller/package.json b/packages/address-book-controller/package.json new file mode 100644 index 0000000000..169e0a6f3d --- /dev/null +++ b/packages/address-book-controller/package.json @@ -0,0 +1,50 @@ +{ + "name": "@metamask/address-book-controller", + "version": "0.0.0", + "description": "Manages a list of recipient addresses associated with nicknames", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/address-book-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/address-book-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~", + "@metamask/controller-utils": "workspace:~" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/user/AddressBookController.test.ts b/packages/address-book-controller/src/AddressBookController.test.ts similarity index 100% rename from src/user/AddressBookController.test.ts rename to packages/address-book-controller/src/AddressBookController.test.ts diff --git a/src/user/AddressBookController.ts b/packages/address-book-controller/src/AddressBookController.ts similarity index 96% rename from src/user/AddressBookController.ts rename to packages/address-book-controller/src/AddressBookController.ts index 7b917181c9..0bbdeca40c 100644 --- a/src/user/AddressBookController.ts +++ b/packages/address-book-controller/src/AddressBookController.ts @@ -2,8 +2,12 @@ import { normalizeEnsName, isValidHexAddress, toChecksumHexAddress, -} from '../util'; -import { BaseController, BaseConfig, BaseState } from '../BaseController'; +} from '@metamask/controller-utils'; +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; /** * @type ContactEntry diff --git a/packages/address-book-controller/src/index.ts b/packages/address-book-controller/src/index.ts new file mode 100644 index 0000000000..e75d24a939 --- /dev/null +++ b/packages/address-book-controller/src/index.ts @@ -0,0 +1 @@ +export * from './AddressBookController'; diff --git a/packages/address-book-controller/tsconfig.build.json b/packages/address-book-controller/tsconfig.build.json new file mode 100644 index 0000000000..bbfe057a20 --- /dev/null +++ b/packages/address-book-controller/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/address-book-controller/tsconfig.json b/packages/address-book-controller/tsconfig.json new file mode 100644 index 0000000000..7ee9852347 --- /dev/null +++ b/packages/address-book-controller/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" } + ], + "include": ["../../types", "./src"] +} diff --git a/typedoc.json b/packages/address-book-controller/typedoc.json similarity index 62% rename from typedoc.json rename to packages/address-book-controller/typedoc.json index b527b62572..c9da015dbf 100644 --- a/typedoc.json +++ b/packages/address-book-controller/typedoc.json @@ -2,5 +2,6 @@ "entryPoints": ["./src/index.ts"], "excludePrivate": true, "hideGenerator": true, - "out": "docs" + "out": "docs", + "tsconfig": "./tsconfig.build.json" } diff --git a/packages/announcement-controller/CHANGELOG.md b/packages/announcement-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/announcement-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/announcement-controller/LICENSE b/packages/announcement-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/announcement-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/announcement-controller/README.md b/packages/announcement-controller/README.md new file mode 100644 index 0000000000..8cfc38dabb --- /dev/null +++ b/packages/announcement-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/announcement-controller` + +Manages in-app announcements. + +## Installation + +`yarn add @metamask/announcement-controller` + +or + +`npm install @metamask/announcement-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/announcement-controller/jest.config.js b/packages/announcement-controller/jest.config.js new file mode 100644 index 0000000000..a7ae2b4b47 --- /dev/null +++ b/packages/announcement-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/announcement-controller/package.json b/packages/announcement-controller/package.json new file mode 100644 index 0000000000..695ccea1a1 --- /dev/null +++ b/packages/announcement-controller/package.json @@ -0,0 +1,49 @@ +{ + "name": "@metamask/announcement-controller", + "version": "0.0.0", + "description": "Manages in-app announcements", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/announcement-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/announcement-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/announcement/AnnouncementController.test.ts b/packages/announcement-controller/src/AnnouncementController.test.ts similarity index 100% rename from src/announcement/AnnouncementController.test.ts rename to packages/announcement-controller/src/AnnouncementController.test.ts diff --git a/src/announcement/AnnouncementController.ts b/packages/announcement-controller/src/AnnouncementController.ts similarity index 96% rename from src/announcement/AnnouncementController.ts rename to packages/announcement-controller/src/AnnouncementController.ts index ab0e371621..2bf002795e 100644 --- a/src/announcement/AnnouncementController.ts +++ b/packages/announcement-controller/src/AnnouncementController.ts @@ -1,4 +1,8 @@ -import { BaseController, BaseConfig, BaseState } from '../BaseController'; +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; interface ViewedAnnouncement { [id: number]: boolean; diff --git a/packages/announcement-controller/src/index.ts b/packages/announcement-controller/src/index.ts new file mode 100644 index 0000000000..f3ce26e85e --- /dev/null +++ b/packages/announcement-controller/src/index.ts @@ -0,0 +1 @@ +export * from './AnnouncementController'; diff --git a/packages/announcement-controller/tsconfig.build.json b/packages/announcement-controller/tsconfig.build.json new file mode 100644 index 0000000000..e5fd7422b9 --- /dev/null +++ b/packages/announcement-controller/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [{ "path": "../base-controller/tsconfig.build.json" }], + "include": ["../../types", "./src"] +} diff --git a/packages/announcement-controller/tsconfig.json b/packages/announcement-controller/tsconfig.json new file mode 100644 index 0000000000..34354c4b09 --- /dev/null +++ b/packages/announcement-controller/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [{ "path": "../base-controller" }], + "include": ["../../types", "./src"] +} diff --git a/packages/announcement-controller/typedoc.json b/packages/announcement-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/announcement-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/approval-controller/CHANGELOG.md b/packages/approval-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/approval-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/approval-controller/LICENSE b/packages/approval-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/approval-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/approval-controller/README.md b/packages/approval-controller/README.md new file mode 100644 index 0000000000..90ab2f6728 --- /dev/null +++ b/packages/approval-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/approval-controller` + +Manages requests that require user approval. + +## Installation + +`yarn add @metamask/approval-controller` + +or + +`npm install @metamask/approval-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/approval-controller/jest.config.js b/packages/approval-controller/jest.config.js new file mode 100644 index 0000000000..a7ae2b4b47 --- /dev/null +++ b/packages/approval-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/approval-controller/package.json b/packages/approval-controller/package.json new file mode 100644 index 0000000000..2526cd2e8e --- /dev/null +++ b/packages/approval-controller/package.json @@ -0,0 +1,53 @@ +{ + "name": "@metamask/approval-controller", + "version": "0.0.0", + "description": "Manages requests that require user approval", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/approval-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/approval-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~", + "eth-rpc-errors": "^4.0.0", + "immer": "^9.0.6", + "nanoid": "^3.1.31" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "sinon": "^9.2.4", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/approval/ApprovalController.test.ts b/packages/approval-controller/src/ApprovalController.test.ts similarity index 76% rename from src/approval/ApprovalController.test.ts rename to packages/approval-controller/src/ApprovalController.test.ts index f443b261d3..e976c538c4 100644 --- a/src/approval/ApprovalController.test.ts +++ b/packages/approval-controller/src/ApprovalController.test.ts @@ -1,6 +1,6 @@ import { errorCodes, EthereumRpcError } from 'eth-rpc-errors'; -import sinon from 'sinon'; -import { ControllerMessenger } from '../ControllerMessenger'; +import * as sinon from 'sinon'; +import { ControllerMessenger } from '@metamask/base-controller'; import { ApprovalController, ApprovalControllerActions, @@ -55,6 +55,45 @@ describe('approval controller', () => { }); }); + it('validates input', () => { + expect(() => + approvalController.add({ id: null, origin: 'bar.baz' } as any), + ).toThrow(getInvalidIdError()); + + expect(() => approvalController.add({ id: 'foo' } as any)).toThrow( + getInvalidOriginError(), + ); + + expect(() => + approvalController.add({ id: 'foo', origin: true } as any), + ).toThrow(getInvalidOriginError()); + + expect(() => + approvalController.add({ + id: 'foo', + origin: 'bar.baz', + type: {}, + } as any), + ).toThrow(getInvalidTypeError(errorCodes.rpc.internal)); + + expect(() => + approvalController.add({ + id: 'foo', + origin: 'bar.baz', + type: '', + } as any), + ).toThrow(getInvalidTypeError(errorCodes.rpc.internal)); + + expect(() => + approvalController.add({ + id: 'foo', + origin: 'bar.baz', + type: 'type', + requestData: 'foo', + } as any), + ).toThrow(getInvalidRequestDataError()); + }); + it('adds correctly specified entry', () => { expect(() => approvalController.add({ id: 'foo', origin: 'bar.baz', type: TYPE }), @@ -194,6 +233,21 @@ describe('approval controller', () => { time: 1, }); }); + + it('returns undefined for non-existing entry', () => { + const approvalController = new ApprovalController({ + messenger: getRestrictedMessenger(), + showApprovalRequest: sinon.spy(), + }); + + approvalController.add({ id: 'foo', origin: 'bar.baz', type: 'type' }); + + expect(approvalController.get('fizz')).toBeUndefined(); + + expect((approvalController as any).get()).toBeUndefined(); + + expect(approvalController.get({} as any)).toBeUndefined(); + }); }); describe('getApprovalCount', () => { @@ -210,6 +264,24 @@ describe('approval controller', () => { approvalController.add(args).catch(() => undefined); }); + it('validates input', () => { + expect(() => approvalController.getApprovalCount()).toThrow( + getApprovalCountParamsError(), + ); + + expect(() => approvalController.getApprovalCount({})).toThrow( + getApprovalCountParamsError(), + ); + + expect(() => + approvalController.getApprovalCount({ origin: null } as any), + ).toThrow(getApprovalCountParamsError()); + + expect(() => + approvalController.getApprovalCount({ type: false } as any), + ).toThrow(getApprovalCountParamsError()); + }); + it('gets the count when specifying origin and type', () => { addWithCatch({ id: '1', origin: 'origin1', type: TYPE }); addWithCatch({ id: '2', origin: 'origin1', type: 'type1' }); @@ -345,6 +417,32 @@ describe('approval controller', () => { }); }); + it('validates input', () => { + expect(() => approvalController.has()).toThrow( + getInvalidHasParamsError(), + ); + + expect(() => approvalController.has({})).toThrow( + getInvalidHasParamsError(), + ); + + expect(() => approvalController.has({ id: true } as any)).toThrow( + getInvalidHasIdError(), + ); + + expect(() => approvalController.has({ origin: true } as any)).toThrow( + getInvalidHasOriginError(), + ); + + expect(() => approvalController.has({ type: true } as any)).toThrow( + getInvalidHasTypeError(), + ); + + expect(() => + approvalController.has({ origin: 'foo', type: true } as any), + ).toThrow(getInvalidHasTypeError()); + }); + it('returns true for existing entry by id', () => { approvalController.add({ id: 'foo', origin: 'bar.baz', type: TYPE }); @@ -630,6 +728,63 @@ describe('approval controller', () => { }); }); + // We test this internal function before resolve, reject, and clear because + // they are heavily dependent upon it. + // TODO: Stop using private methods in tests + describe('_delete', () => { + let approvalController: ApprovalController; + + beforeEach(() => { + approvalController = new ApprovalController({ + messenger: getRestrictedMessenger(), + showApprovalRequest: sinon.spy(), + }); + }); + + it('deletes entry', () => { + approvalController.add({ id: 'foo', origin: 'bar.baz', type: 'type' }); + + (approvalController as any)._delete('foo'); + + expect( + !approvalController.has({ id: 'foo' }) && + !approvalController.has({ type: 'type' }) && + !approvalController.has({ origin: 'bar.baz' }) && + !approvalController.state[STORE_KEY].foo, + ).toStrictEqual(true); + }); + + it('deletes one entry out of many without side-effects', () => { + approvalController.add({ id: 'foo', origin: 'bar.baz', type: 'type1' }); + approvalController.add({ id: 'fizz', origin: 'bar.baz', type: 'type2' }); + + (approvalController as any)._delete('fizz'); + + expect( + !approvalController.has({ id: 'fizz' }) && + !approvalController.has({ origin: 'bar.baz', type: 'type2' }), + ).toStrictEqual(true); + + expect( + approvalController.has({ id: 'foo' }) && + approvalController.has({ origin: 'bar.baz' }), + ).toStrictEqual(true); + }); + }); + + // TODO: Stop using private methods in tests + describe('_isEmptyOrigin', () => { + it('handles non-existing origin', () => { + const approvalController = new ApprovalController({ + messenger: getRestrictedMessenger(), + showApprovalRequest: sinon.spy(), + }); + expect(() => + (approvalController as any)._isEmptyOrigin('kaplar'), + ).not.toThrow(); + }); + }); + describe('actions', () => { it('addApprovalRequest: shouldShowRequest = true', async () => { const messenger = new ControllerMessenger< @@ -706,6 +861,15 @@ function getOriginTypeCollisionError(origin: string, type = TYPE) { return getError(message, errorCodes.rpc.resourceUnavailable); } +/** + * Get an invalid ID error. + * + * @returns An invalid ID error. + */ +function getInvalidIdError() { + return getError('Must specify non-empty string id.', errorCodes.rpc.internal); +} + /** * Get an "ID not found" error. * @@ -716,6 +880,85 @@ function getIdNotFoundError(id: string) { return getError(`Approval request with id '${id}' not found.`); } +/** + * Get an invalid ID type error. + * + * @returns An invalid ID type error. + */ +function getInvalidHasIdError() { + return getError('May not specify non-string id.'); +} + +/** + * Get an invalid origin type error. + * + * @returns The invalid origin type error. + */ +function getInvalidHasOriginError() { + return getError('May not specify non-string origin.'); +} + +/** + * Get an invalid type error. + * + * @returns The invalid type error. + */ +function getInvalidHasTypeError() { + return getError('May not specify non-string type.'); +} + +/** + * Get an invalid origin error. + * + * @returns The invalid origin error. + */ +function getInvalidOriginError() { + return getError( + 'Must specify non-empty string origin.', + errorCodes.rpc.internal, + ); +} + +/** + * Get an invalid request data error. + * + * @returns The invalid request data error. + */ +function getInvalidRequestDataError() { + return getError( + 'Request data must be a plain object if specified.', + errorCodes.rpc.internal, + ); +} + +/** + * Get an invalid type error. + * + * @param code - The error code. + * @returns The invalid type error. + */ +function getInvalidTypeError(code: number) { + return getError('Must specify non-empty string type.', code); +} + +/** + * Get an invalid params error. + * + * @returns The invalid params error. + */ +function getInvalidHasParamsError() { + return getError('Must specify a valid combination of id, origin, and type.'); +} + +/** + * Get an invalid approval count params error. + * + * @returns The invalid approval count params error. + */ +function getApprovalCountParamsError() { + return getError('Must specify origin, type, or both.'); +} + /** * Get an error. * diff --git a/src/approval/ApprovalController.ts b/packages/approval-controller/src/ApprovalController.ts similarity index 98% rename from src/approval/ApprovalController.ts rename to packages/approval-controller/src/ApprovalController.ts index 3be6d4c3b8..b77aec7a4a 100644 --- a/src/approval/ApprovalController.ts +++ b/packages/approval-controller/src/ApprovalController.ts @@ -1,9 +1,11 @@ import type { Patch } from 'immer'; import { EthereumRpcError, ethErrors } from 'eth-rpc-errors'; import { nanoid } from 'nanoid'; - -import { BaseController, Json } from '../BaseControllerV2'; -import type { RestrictedControllerMessenger } from '../ControllerMessenger'; +import { + BaseControllerV2, + RestrictedControllerMessenger, + Json, +} from '@metamask/base-controller'; import { ApprovalRequestNotFoundError } from './errors'; const controllerName = 'ApprovalController'; @@ -146,7 +148,7 @@ type ApprovalControllerOptions = { * Adding a request returns a promise that resolves or rejects when the request * is approved or denied, respectively. */ -export class ApprovalController extends BaseController< +export class ApprovalController extends BaseControllerV2< typeof controllerName, ApprovalControllerState, ApprovalControllerMessenger diff --git a/src/approval/errors.ts b/packages/approval-controller/src/errors.ts similarity index 100% rename from src/approval/errors.ts rename to packages/approval-controller/src/errors.ts diff --git a/src/approval/index.ts b/packages/approval-controller/src/index.ts similarity index 100% rename from src/approval/index.ts rename to packages/approval-controller/src/index.ts diff --git a/packages/approval-controller/tsconfig.build.json b/packages/approval-controller/tsconfig.build.json new file mode 100644 index 0000000000..e5fd7422b9 --- /dev/null +++ b/packages/approval-controller/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [{ "path": "../base-controller/tsconfig.build.json" }], + "include": ["../../types", "./src"] +} diff --git a/packages/approval-controller/tsconfig.json b/packages/approval-controller/tsconfig.json new file mode 100644 index 0000000000..34354c4b09 --- /dev/null +++ b/packages/approval-controller/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [{ "path": "../base-controller" }], + "include": ["../../types", "./src"] +} diff --git a/packages/approval-controller/typedoc.json b/packages/approval-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/approval-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/assets-controllers/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/assets-controllers/LICENSE b/packages/assets-controllers/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/assets-controllers/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/assets-controllers/README.md b/packages/assets-controllers/README.md new file mode 100644 index 0000000000..cac5b35e8e --- /dev/null +++ b/packages/assets-controllers/README.md @@ -0,0 +1,30 @@ +# `@metamask/assets-controllers` + +Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs). + +## Installation + +`yarn add @metamask/assets-controllers` + +or + +`npm install @metamask/assets-controllers` + +## Controllers + +This package features the following controllers: + +- [**AccountTrackerController**](src/AccountTrackerController.ts) keeps a updated list of the accounts in the currently selected keychain which is updated automatically on a schedule or on demand. +- [**AssetsContractController**](src/AssetsContractController.ts) provides a set of convenience methods that use contracts to retrieve information about tokens, read token balances, and transfer tokens. +- [**CollectibleDetectionController**](src/CollectibleDetectionController.ts) keeps a periodically updated list of ERC-721 tokens assigned to the currently selected address. +- [**CollectiblesController**](src/CollectiblesController.ts) tracks ERC-721 and ERC-1155 tokens assigned to the currently selected address, using OpenSea to retrieve token information. +- [**CurrencyRateController**](src/CurrencyRateController.ts) keeps a periodically updated value of the exchange rate from the currently selected "native" currency to another (handling testnet tokens specially). +- [**TokenBalancesController**](src/TokenBalancesController.ts) keeps a periodically updated set of balances for the current set of ERC-20 tokens. +- [**TokenDetectionController**](src/TokenDetectionController.ts) keeps a periodically updated list of ERC-20 tokens assigned to the currently selected address. +- [**TokenListController**](src/TokenListController.ts) uses the MetaSwap API to keep a periodically updated list of known ERC-20 tokens along with their metadata. +- [**TokenRatesController**](src/TokenRatesController.ts) keeps a periodically updated list of exchange rates for known ERC-20 tokens relative to the currently selected native currency. +- [**TokensController**](src/TokensController.ts) stores the ERC-20 and ERC-721 tokens, along with their metadata, that are listed in the wallet under the currently selected address on the currently selected chain. + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/assets-controllers/jest.config.js b/packages/assets-controllers/jest.config.js new file mode 100644 index 0000000000..a080d34818 --- /dev/null +++ b/packages/assets-controllers/jest.config.js @@ -0,0 +1,28 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 88.86, + functions: 96.71, + lines: 96.62, + statements: 96.69, + }, + }, + + // We rely on `window` to make requests + testEnvironment: 'jsdom', +}); diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json new file mode 100644 index 0000000000..0d4db1c52a --- /dev/null +++ b/packages/assets-controllers/package.json @@ -0,0 +1,74 @@ +{ + "name": "@metamask/assets-controllers", + "version": "0.0.0", + "description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/assets-controllers#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/assets-controllers", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@ethersproject/abi": "^5.7.0", + "@ethersproject/contracts": "^5.7.0", + "@ethersproject/providers": "^5.7.0", + "@metamask/base-controller": "workspace:~", + "@metamask/contract-metadata": "^1.35.0", + "@metamask/controller-utils": "workspace:~", + "@metamask/metamask-eth-abis": "3.0.0", + "@metamask/network-controller": "workspace:~", + "@metamask/preferences-controller": "workspace:~", + "@types/uuid": "^8.3.0", + "abort-controller": "^3.0.0", + "async-mutex": "^0.2.6", + "babel-runtime": "^6.26.0", + "eth-query": "^2.1.2", + "eth-rpc-errors": "^4.0.0", + "ethereumjs-util": "^7.0.10", + "immer": "^9.0.6", + "multiformats": "^9.5.2", + "single-call-balance-checker-abi": "^1.0.0", + "uuid": "^8.3.2", + "web3": "^0.20.7" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "@types/node": "^14.14.31", + "@types/web3": "^1.0.6", + "deepmerge": "^4.2.2", + "ethjs-provider-http": "^0.1.6", + "jest": "^26.4.2", + "nock": "^13.0.7", + "sinon": "^9.2.4", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/assets/AccountTrackerController.test.ts b/packages/assets-controllers/src/AccountTrackerController.test.ts similarity index 88% rename from src/assets/AccountTrackerController.test.ts rename to packages/assets-controllers/src/AccountTrackerController.test.ts index 0e7b3dbd18..c2c99a9306 100644 --- a/src/assets/AccountTrackerController.test.ts +++ b/packages/assets-controllers/src/AccountTrackerController.test.ts @@ -1,10 +1,24 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import HttpProvider from 'ethjs-provider-http'; -import type { ContactEntry } from '../user/AddressBookController'; -import { PreferencesController } from '../user/PreferencesController'; -import * as utils from '../util'; +import { + ContactEntry, + PreferencesController, +} from '@metamask/preferences-controller'; +import { query } from '@metamask/controller-utils'; import { AccountTrackerController } from './AccountTrackerController'; +jest.mock('@metamask/controller-utils', () => { + return { + ...jest.requireActual('@metamask/controller-utils'), + query: jest.fn(), + }; +}); + +const mockedQuery = query as jest.Mock< + ReturnType, + Parameters +>; + const provider = new HttpProvider( 'https://ropsten.infura.io/v3/341eacb578dd44a1a049cbc5f6fd4035', ); @@ -51,7 +65,6 @@ describe('AccountTrackerController', () => { it('should sync balance with addresses', async () => { const address = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; - const queryStub = sinon.stub(utils, 'query'); const controller = new AccountTrackerController( { onPreferencesStateChange: sinon.stub(), @@ -61,7 +74,7 @@ describe('AccountTrackerController', () => { }, { provider }, ); - queryStub.returns(Promise.resolve('0x10')); + mockedQuery.mockReturnValue(Promise.resolve('0x10')); const result = await controller.syncBalanceWithAddresses([address]); expect(result[address].balance).toBe('0x10'); }); diff --git a/src/assets/AccountTrackerController.ts b/packages/assets-controllers/src/AccountTrackerController.ts similarity index 94% rename from src/assets/AccountTrackerController.ts rename to packages/assets-controllers/src/AccountTrackerController.ts index 6471416a1f..a18e8b6b82 100644 --- a/src/assets/AccountTrackerController.ts +++ b/packages/assets-controllers/src/AccountTrackerController.ts @@ -1,8 +1,16 @@ import EthQuery from 'eth-query'; import { Mutex } from 'async-mutex'; -import { BaseConfig, BaseController, BaseState } from '../BaseController'; -import { PreferencesState } from '../user/PreferencesController'; -import { BNToHex, query, safelyExecuteWithTimeout } from '../util'; +import { + BaseConfig, + BaseController, + BaseState, +} from '@metamask/base-controller'; +import { PreferencesState } from '@metamask/preferences-controller'; +import { + BNToHex, + query, + safelyExecuteWithTimeout, +} from '@metamask/controller-utils'; /** * @type AccountInformation @@ -46,7 +54,7 @@ export class AccountTrackerController extends BaseController< private mutex = new Mutex(); - private handle?: NodeJS.Timer; + private handle?: ReturnType; private syncAccounts() { const { accounts } = this.state; diff --git a/src/assets/AssetsContractController.test.ts b/packages/assets-controllers/src/AssetsContractController.test.ts similarity index 97% rename from src/assets/AssetsContractController.test.ts rename to packages/assets-controllers/src/AssetsContractController.test.ts index 3f45988e26..cd5a035d94 100644 --- a/src/assets/AssetsContractController.test.ts +++ b/packages/assets-controllers/src/AssetsContractController.test.ts @@ -1,16 +1,16 @@ import HttpProvider from 'ethjs-provider-http'; -import { IPFS_DEFAULT_GATEWAY_URL } from '../constants'; -import { SupportedTokenDetectionNetworks } from '../util'; -import { PreferencesController } from '../user/PreferencesController'; +import { IPFS_DEFAULT_GATEWAY_URL } from '@metamask/controller-utils'; +import { PreferencesController } from '@metamask/preferences-controller'; import { NetworkController, NetworkControllerMessenger, -} from '../network/NetworkController'; -import { ControllerMessenger } from '../ControllerMessenger'; +} from '@metamask/network-controller'; +import { ControllerMessenger } from '@metamask/base-controller'; import { AssetsContractController, MISSING_PROVIDER_ERROR, } from './AssetsContractController'; +import { SupportedTokenDetectionNetworks } from './assetsUtil'; const MAINNET_PROVIDER = new HttpProvider( 'https://mainnet.infura.io/v3/341eacb578dd44a1a049cbc5f6fd4035', diff --git a/src/assets/AssetsContractController.ts b/packages/assets-controllers/src/AssetsContractController.ts similarity index 97% rename from src/assets/AssetsContractController.ts rename to packages/assets-controllers/src/AssetsContractController.ts index ef79c38756..dcf1504c29 100644 --- a/src/assets/AssetsContractController.ts +++ b/packages/assets-controllers/src/AssetsContractController.ts @@ -1,11 +1,15 @@ import { BN } from 'ethereumjs-util'; import Web3 from 'web3'; import abiSingleCallBalancesContract from 'single-call-balance-checker-abi'; -import { BaseController, BaseConfig, BaseState } from '../BaseController'; -import type { PreferencesState } from '../user/PreferencesController'; -import { IPFS_DEFAULT_GATEWAY_URL } from '../constants'; -import { SupportedTokenDetectionNetworks } from '../util'; -import { NetworkState } from '../network/NetworkController'; +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; +import type { PreferencesState } from '@metamask/preferences-controller'; +import { IPFS_DEFAULT_GATEWAY_URL } from '@metamask/controller-utils'; +import { NetworkState } from '@metamask/network-controller'; +import { SupportedTokenDetectionNetworks } from './assetsUtil'; import { ERC721Standard } from './Standards/NftStandards/ERC721/ERC721Standard'; import { ERC1155Standard } from './Standards/NftStandards/ERC1155/ERC1155Standard'; import { ERC20Standard } from './Standards/ERC20Standard'; diff --git a/src/assets/CurrencyRateController.test.ts b/packages/assets-controllers/src/CurrencyRateController.test.ts similarity index 98% rename from src/assets/CurrencyRateController.test.ts rename to packages/assets-controllers/src/CurrencyRateController.test.ts index 4baa9c82d8..0e744d32f4 100644 --- a/src/assets/CurrencyRateController.test.ts +++ b/packages/assets-controllers/src/CurrencyRateController.test.ts @@ -1,7 +1,7 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import nock from 'nock'; -import { ControllerMessenger } from '../ControllerMessenger'; -import { TESTNET_TICKER_SYMBOLS } from '../constants'; +import { ControllerMessenger } from '@metamask/base-controller'; +import { TESTNET_TICKER_SYMBOLS } from '@metamask/controller-utils'; import { CurrencyRateController, CurrencyRateStateChange, diff --git a/src/assets/CurrencyRateController.ts b/packages/assets-controllers/src/CurrencyRateController.ts similarity index 95% rename from src/assets/CurrencyRateController.ts rename to packages/assets-controllers/src/CurrencyRateController.ts index f10d81c878..3bb8d4a31b 100644 --- a/src/assets/CurrencyRateController.ts +++ b/packages/assets-controllers/src/CurrencyRateController.ts @@ -1,12 +1,15 @@ import { Mutex } from 'async-mutex'; import type { Patch } from 'immer'; - -import { BaseController } from '../BaseControllerV2'; -import { safelyExecute } from '../util'; -import { fetchExchangeRate as defaultFetchExchangeRate } from '../apis/crypto-compare'; - -import type { RestrictedControllerMessenger } from '../ControllerMessenger'; -import { TESTNET_TICKER_SYMBOLS, FALL_BACK_VS_CURRENCY } from '../constants'; +import { + BaseControllerV2, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { + TESTNET_TICKER_SYMBOLS, + FALL_BACK_VS_CURRENCY, + safelyExecute, +} from '@metamask/controller-utils'; +import { fetchExchangeRate as defaultFetchExchangeRate } from './crypto-compare'; /** * @type CurrencyRateState @@ -72,14 +75,14 @@ const defaultState = { * Controller that passively polls on a set interval for an exchange rate from the current network * asset to the user's preferred currency. */ -export class CurrencyRateController extends BaseController< +export class CurrencyRateController extends BaseControllerV2< typeof name, CurrencyRateState, CurrencyRateMessenger > { private mutex = new Mutex(); - private intervalId?: NodeJS.Timeout; + private intervalId?: ReturnType; private intervalDelay; diff --git a/src/assets/NftController.test.ts b/packages/assets-controllers/src/NftController.test.ts similarity index 99% rename from src/assets/NftController.test.ts rename to packages/assets-controllers/src/NftController.test.ts index bacbd839d6..ab8b99dcb0 100644 --- a/src/assets/NftController.test.ts +++ b/packages/assets-controllers/src/NftController.test.ts @@ -1,23 +1,23 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import nock from 'nock'; import HttpProvider from 'ethjs-provider-http'; -import { PreferencesController } from '../user/PreferencesController'; +import { PreferencesController } from '@metamask/preferences-controller'; import { NetworkController, - NetworksChainId, NetworkControllerMessenger, -} from '../network/NetworkController'; -import { getFormattedIpfsUrl } from '../util'; +} from '@metamask/network-controller'; import { OPENSEA_PROXY_URL, IPFS_DEFAULT_GATEWAY_URL, ERC1155, OPENSEA_API_URL, ERC721, -} from '../constants'; -import { ControllerMessenger } from '../ControllerMessenger'; + NetworksChainId, +} from '@metamask/controller-utils'; +import { ControllerMessenger } from '@metamask/base-controller'; import { AssetsContractController } from './AssetsContractController'; import { NftController } from './NftController'; +import { getFormattedIpfsUrl } from './assetsUtil'; const CRYPTOPUNK_ADDRESS = '0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB'; const ERC721_KUDOSADDRESS = '0x2aEa4Add166EBf38b63d09a75dE1a7b94Aa24163'; diff --git a/src/assets/NftController.ts b/packages/assets-controllers/src/NftController.ts similarity index 99% rename from src/assets/NftController.ts rename to packages/assets-controllers/src/NftController.ts index 6bc1fc42cb..52e1f99685 100644 --- a/src/assets/NftController.ts +++ b/packages/assets-controllers/src/NftController.ts @@ -1,19 +1,19 @@ import { EventEmitter } from 'events'; import { BN, stripHexPrefix } from 'ethereumjs-util'; import { Mutex } from 'async-mutex'; - -import { BaseController, BaseConfig, BaseState } from '../BaseController'; -import type { PreferencesState } from '../user/PreferencesController'; -import type { NetworkState, NetworkType } from '../network/NetworkController'; +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; +import type { PreferencesState } from '@metamask/preferences-controller'; +import type { NetworkState } from '@metamask/network-controller'; import { safelyExecute, handleFetch, toChecksumHexAddress, BNToHex, - getFormattedIpfsUrl, fetchWithErrorHandling, -} from '../util'; -import { MAINNET, RINKEBY_CHAIN_ID, IPFS_DEFAULT_GATEWAY_URL, @@ -22,8 +22,8 @@ import { OPENSEA_API_URL, OPENSEA_PROXY_URL, OPENSEA_TEST_API_URL, -} from '../constants'; - + NetworkType, +} from '@metamask/controller-utils'; import type { ApiNft, ApiNftCreator, @@ -31,7 +31,7 @@ import type { ApiNftLastSale, } from './NftDetectionController'; import type { AssetsContractController } from './AssetsContractController'; -import { compareNftMetadata } from './assetsUtil'; +import { compareNftMetadata, getFormattedIpfsUrl } from './assetsUtil'; /** * @type Nft diff --git a/src/assets/NftDetectionController.test.ts b/packages/assets-controllers/src/NftDetectionController.test.ts similarity index 98% rename from src/assets/NftDetectionController.test.ts rename to packages/assets-controllers/src/NftDetectionController.test.ts index 480838f7af..eace03070e 100644 --- a/src/assets/NftDetectionController.test.ts +++ b/packages/assets-controllers/src/NftDetectionController.test.ts @@ -1,12 +1,12 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import nock from 'nock'; +import { ControllerMessenger } from '@metamask/base-controller'; import { NetworkController, NetworkControllerMessenger, -} from '../network/NetworkController'; -import { PreferencesController } from '../user/PreferencesController'; -import { OPENSEA_PROXY_URL } from '../constants'; -import { ControllerMessenger } from '../ControllerMessenger'; +} from '@metamask/network-controller'; +import { PreferencesController } from '@metamask/preferences-controller'; +import { OPENSEA_PROXY_URL } from '@metamask/controller-utils'; import { NftController } from './NftController'; import { AssetsContractController } from './AssetsContractController'; import { NftDetectionController } from './NftDetectionController'; diff --git a/src/assets/NftDetectionController.ts b/packages/assets-controllers/src/NftDetectionController.ts similarity index 96% rename from src/assets/NftDetectionController.ts rename to packages/assets-controllers/src/NftDetectionController.ts index 019222bbcd..223afcecf3 100644 --- a/src/assets/NftDetectionController.ts +++ b/packages/assets-controllers/src/NftDetectionController.ts @@ -1,9 +1,18 @@ -import { BaseController, BaseConfig, BaseState } from '../BaseController'; -import type { NetworkState, NetworkType } from '../network/NetworkController'; -import type { PreferencesState } from '../user/PreferencesController'; -import { fetchWithErrorHandling, toChecksumHexAddress } from '../util'; -import { MAINNET, OPENSEA_PROXY_URL, OPENSEA_API_URL } from '../constants'; - +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; +import type { NetworkState } from '@metamask/network-controller'; +import type { PreferencesState } from '@metamask/preferences-controller'; +import { + MAINNET, + OPENSEA_PROXY_URL, + OPENSEA_API_URL, + NetworkType, + fetchWithErrorHandling, + toChecksumHexAddress, +} from '@metamask/controller-utils'; import type { NftController, NftState, NftMetadata } from './NftController'; const DEFAULT_INTERVAL = 180000; @@ -126,7 +135,7 @@ export class NftDetectionController extends BaseController< NftDetectionConfig, BaseState > { - private intervalId?: NodeJS.Timeout; + private intervalId?: ReturnType; private getOwnerNftApi({ address, diff --git a/src/assets/Standards/ERC20Standard.test.ts b/packages/assets-controllers/src/Standards/ERC20Standard.test.ts similarity index 98% rename from src/assets/Standards/ERC20Standard.test.ts rename to packages/assets-controllers/src/Standards/ERC20Standard.test.ts index 86aba64642..f4cd501bbd 100644 --- a/src/assets/Standards/ERC20Standard.test.ts +++ b/packages/assets-controllers/src/Standards/ERC20Standard.test.ts @@ -13,15 +13,15 @@ const AMBIRE_ADDRESS = '0xa07D75aacEFd11b425AF7181958F0F85c312f143'; describe('ERC20Standard', () => { let erc20Standard: ERC20Standard; let web3: any; - nock.disableNetConnect(); beforeAll(() => { web3 = new Web3(MAINNET_PROVIDER); erc20Standard = new ERC20Standard(web3); + nock.disableNetConnect(); }); afterAll(() => { - nock.restore(); + nock.enableNetConnect(); }); it('should get correct token symbol for a given ERC20 contract address', async () => { diff --git a/src/assets/Standards/ERC20Standard.ts b/packages/assets-controllers/src/Standards/ERC20Standard.ts similarity index 98% rename from src/assets/Standards/ERC20Standard.ts rename to packages/assets-controllers/src/Standards/ERC20Standard.ts index c6fa243f65..531745bead 100644 --- a/src/assets/Standards/ERC20Standard.ts +++ b/packages/assets-controllers/src/Standards/ERC20Standard.ts @@ -1,7 +1,7 @@ import { abiERC20 } from '@metamask/metamask-eth-abis'; import { BN, toUtf8 } from 'ethereumjs-util'; import { AbiCoder } from '@ethersproject/abi'; -import { ERC20 } from '../../constants'; +import { ERC20 } from '@metamask/controller-utils'; import { Web3 } from './standards-types'; export class ERC20Standard { diff --git a/src/assets/Standards/NftStandards/ERC1155/ERC1155Standard.test.ts b/packages/assets-controllers/src/Standards/NftStandards/ERC1155/ERC1155Standard.test.ts similarity index 96% rename from src/assets/Standards/NftStandards/ERC1155/ERC1155Standard.test.ts rename to packages/assets-controllers/src/Standards/NftStandards/ERC1155/ERC1155Standard.test.ts index 01869b2a03..6e2dccd0c7 100644 --- a/src/assets/Standards/NftStandards/ERC1155/ERC1155Standard.test.ts +++ b/packages/assets-controllers/src/Standards/NftStandards/ERC1155/ERC1155Standard.test.ts @@ -12,15 +12,16 @@ const ERC1155_ADDRESS = '0xfaaFDc07907ff5120a76b34b731b278c38d6043C'; describe('ERC1155Standard', () => { let erc1155Standard: ERC1155Standard; let web3: any; - nock.disableNetConnect(); beforeAll(() => { web3 = new Web3(MAINNET_PROVIDER); erc1155Standard = new ERC1155Standard(web3); + nock.disableNetConnect(); }); afterAll(() => { nock.restore(); + nock.enableNetConnect(); }); it('should determine if contract supports URI metadata interface correctly', async () => { diff --git a/src/assets/Standards/NftStandards/ERC1155/ERC1155Standard.ts b/packages/assets-controllers/src/Standards/NftStandards/ERC1155/ERC1155Standard.ts similarity index 98% rename from src/assets/Standards/NftStandards/ERC1155/ERC1155Standard.ts rename to packages/assets-controllers/src/Standards/NftStandards/ERC1155/ERC1155Standard.ts index 9bee70ace4..12fd21ed06 100644 --- a/src/assets/Standards/NftStandards/ERC1155/ERC1155Standard.ts +++ b/packages/assets-controllers/src/Standards/NftStandards/ERC1155/ERC1155Standard.ts @@ -4,8 +4,10 @@ import { ERC1155_INTERFACE_ID, ERC1155_METADATA_URI_INTERFACE_ID, ERC1155_TOKEN_RECEIVER_INTERFACE_ID, -} from '../../../../constants'; -import { getFormattedIpfsUrl, timeoutFetch } from '../../../../util'; + timeoutFetch, +} from '@metamask/controller-utils'; +import { getFormattedIpfsUrl } from '../../../assetsUtil'; + import { Web3 } from '../../standards-types'; export class ERC1155Standard { diff --git a/src/assets/Standards/NftStandards/ERC721/ERC721Standard.test.ts b/packages/assets-controllers/src/Standards/NftStandards/ERC721/ERC721Standard.test.ts similarity index 99% rename from src/assets/Standards/NftStandards/ERC721/ERC721Standard.test.ts rename to packages/assets-controllers/src/Standards/NftStandards/ERC721/ERC721Standard.test.ts index af3670c217..0a10403d8f 100644 --- a/src/assets/Standards/NftStandards/ERC721/ERC721Standard.test.ts +++ b/packages/assets-controllers/src/Standards/NftStandards/ERC721/ERC721Standard.test.ts @@ -1,7 +1,7 @@ import Web3 from 'web3'; import HttpProvider from 'ethjs-provider-http'; import nock from 'nock'; -import { IPFS_DEFAULT_GATEWAY_URL } from '../../../../constants'; +import { IPFS_DEFAULT_GATEWAY_URL } from '@metamask/controller-utils'; import { ERC721Standard } from './ERC721Standard'; const MAINNET_PROVIDER = new HttpProvider( @@ -25,6 +25,7 @@ describe('ERC721Standard', () => { afterAll(() => { nock.restore(); + nock.enableNetConnect(); }); it('should determine if contract supports interface correctly', async () => { diff --git a/src/assets/Standards/NftStandards/ERC721/ERC721Standard.ts b/packages/assets-controllers/src/Standards/NftStandards/ERC721/ERC721Standard.ts similarity index 98% rename from src/assets/Standards/NftStandards/ERC721/ERC721Standard.ts rename to packages/assets-controllers/src/Standards/NftStandards/ERC721/ERC721Standard.ts index 048ea19905..f01156c024 100644 --- a/src/assets/Standards/NftStandards/ERC721/ERC721Standard.ts +++ b/packages/assets-controllers/src/Standards/NftStandards/ERC721/ERC721Standard.ts @@ -1,12 +1,13 @@ import { abiERC721 } from '@metamask/metamask-eth-abis'; -import { Web3 } from '../../standards-types'; -import { getFormattedIpfsUrl, timeoutFetch } from '../../../../util'; import { + timeoutFetch, ERC721_INTERFACE_ID, ERC721_METADATA_INTERFACE_ID, ERC721_ENUMERABLE_INTERFACE_ID, ERC721, -} from '../../../../constants'; +} from '@metamask/controller-utils'; +import { getFormattedIpfsUrl } from '../../../assetsUtil'; +import { Web3 } from '../../standards-types'; export class ERC721Standard { private web3: Web3; diff --git a/src/assets/Standards/standards-types.ts b/packages/assets-controllers/src/Standards/standards-types.ts similarity index 100% rename from src/assets/Standards/standards-types.ts rename to packages/assets-controllers/src/Standards/standards-types.ts diff --git a/src/assets/TokenBalancesController.test.ts b/packages/assets-controllers/src/TokenBalancesController.test.ts similarity index 97% rename from src/assets/TokenBalancesController.test.ts rename to packages/assets-controllers/src/TokenBalancesController.test.ts index a6c1a5392f..054608c112 100644 --- a/src/assets/TokenBalancesController.test.ts +++ b/packages/assets-controllers/src/TokenBalancesController.test.ts @@ -1,11 +1,11 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import { BN } from 'ethereumjs-util'; import { NetworkController, NetworkControllerMessenger, -} from '../network/NetworkController'; -import { PreferencesController } from '../user/PreferencesController'; -import { ControllerMessenger } from '../ControllerMessenger'; +} from '@metamask/network-controller'; +import { PreferencesController } from '@metamask/preferences-controller'; +import { ControllerMessenger } from '@metamask/base-controller'; import { TokensController } from './TokensController'; import { Token } from './TokenRatesController'; import { AssetsContractController } from './AssetsContractController'; diff --git a/src/assets/TokenBalancesController.ts b/packages/assets-controllers/src/TokenBalancesController.ts similarity index 93% rename from src/assets/TokenBalancesController.ts rename to packages/assets-controllers/src/TokenBalancesController.ts index 7913781e7c..bf891c49ac 100644 --- a/src/assets/TokenBalancesController.ts +++ b/packages/assets-controllers/src/TokenBalancesController.ts @@ -1,7 +1,11 @@ import { BN } from 'ethereumjs-util'; -import { BaseController, BaseConfig, BaseState } from '../BaseController'; -import { safelyExecute } from '../util'; -import type { PreferencesState } from '../user/PreferencesController'; +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; +import { safelyExecute } from '@metamask/controller-utils'; +import type { PreferencesState } from '@metamask/preferences-controller'; import { Token } from './TokenRatesController'; import { TokensState } from './TokensController'; import type { AssetsContractController } from './AssetsContractController'; @@ -39,7 +43,7 @@ export class TokenBalancesController extends BaseController< TokenBalancesConfig, TokenBalancesState > { - private handle?: NodeJS.Timer; + private handle?: ReturnType; /** * Name of this controller used during composition diff --git a/src/assets/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts similarity index 97% rename from src/assets/TokenDetectionController.test.ts rename to packages/assets-controllers/src/TokenDetectionController.test.ts index 3ffc01dce3..14999fdceb 100644 --- a/src/assets/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -1,4 +1,4 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import nock from 'nock'; import { BN } from 'ethereumjs-util'; import { @@ -6,15 +6,10 @@ import { NetworkControllerMessenger, NetworkControllerProviderChangeEvent, NetworkControllerStateChangeEvent, - NetworksChainId, -} from '../network/NetworkController'; -import { PreferencesController } from '../user/PreferencesController'; -import { ControllerMessenger } from '../ControllerMessenger'; -import { - isTokenDetectionSupportedForNetwork, - SupportedTokenDetectionNetworks, -} from '../util'; -import { TOKEN_END_POINT_API } from '../apis/token-service'; +} from '@metamask/network-controller'; +import { NetworksChainId } from '@metamask/controller-utils'; +import { PreferencesController } from '@metamask/preferences-controller'; +import { ControllerMessenger } from '@metamask/base-controller'; import { TokensController } from './TokensController'; import { TokenDetectionController } from './TokenDetectionController'; import { @@ -24,8 +19,13 @@ import { TokenListToken, } from './TokenListController'; import { AssetsContractController } from './AssetsContractController'; -import { formatAggregatorNames } from './assetsUtil'; +import { + formatAggregatorNames, + isTokenDetectionSupportedForNetwork, + SupportedTokenDetectionNetworks, +} from './assetsUtil'; import { Token } from './TokenRatesController'; +import { TOKEN_END_POINT_API } from './token-service'; const DEFAULT_INTERVAL = 180000; diff --git a/src/assets/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts similarity index 95% rename from src/assets/TokenDetectionController.ts rename to packages/assets-controllers/src/TokenDetectionController.ts index a96be20e9e..210b0e7238 100644 --- a/src/assets/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -1,11 +1,15 @@ -import { BaseController, BaseConfig, BaseState } from '../BaseController'; -import type { NetworkState } from '../network/NetworkController'; -import type { PreferencesState } from '../user/PreferencesController'; +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; +import type { NetworkState } from '@metamask/network-controller'; +import type { PreferencesState } from '@metamask/preferences-controller'; import { safelyExecute, toChecksumHexAddress, - isTokenDetectionSupportedForNetwork, -} from '../util'; +} from '@metamask/controller-utils'; +import { isTokenDetectionSupportedForNetwork } from './assetsUtil'; import type { TokensController, TokensState } from './TokensController'; import type { AssetsContractController } from './AssetsContractController'; import { Token } from './TokenRatesController'; @@ -38,7 +42,7 @@ export class TokenDetectionController extends BaseController< TokenDetectionConfig, BaseState > { - private intervalId?: NodeJS.Timeout; + private intervalId?: ReturnType; /** * Name of this controller used during composition diff --git a/src/assets/TokenListController.test.ts b/packages/assets-controllers/src/TokenListController.test.ts similarity index 96% rename from src/assets/TokenListController.test.ts rename to packages/assets-controllers/src/TokenListController.test.ts index 82991887bc..b3689cbd17 100644 --- a/src/assets/TokenListController.test.ts +++ b/packages/assets-controllers/src/TokenListController.test.ts @@ -1,12 +1,11 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import nock from 'nock'; -import { TOKEN_END_POINT_API } from '../apis/token-service'; -import { ControllerMessenger } from '../ControllerMessenger'; +import { ControllerMessenger } from '@metamask/base-controller'; import { NetworkController, NetworkControllerProviderChangeEvent, - NetworksChainId, -} from '../network/NetworkController'; +} from '@metamask/network-controller'; +import { NetworksChainId } from '@metamask/controller-utils'; import { TokenListController, TokenListStateChange, @@ -14,6 +13,7 @@ import { TokenListMap, TokenListState, } from './TokenListController'; +import { TOKEN_END_POINT_API } from './token-service'; const name = 'TokenListController'; const timestamp = Date.now(); @@ -829,28 +829,34 @@ describe('TokenListController', () => { chainId: NetworksChainId.mainnet, preventPollingOnNetworkRestart: false, messenger, + interval: 750, }); await controller.start(); - expect(controller.state.tokenList).toStrictEqual( - sampleSingleChainState.tokenList, - ); - - expect( - controller.state.tokensChainsCache[NetworksChainId.mainnet].data, - ).toStrictEqual( - sampleSingleChainState.tokensChainsCache[NetworksChainId.mainnet].data, - ); - - expect( - controller.state.tokensChainsCache[NetworksChainId.mainnet].timestamp, - ).toBeGreaterThanOrEqual( - sampleSingleChainState.tokensChainsCache[NetworksChainId.mainnet] - .timestamp, - ); - controller.destroy(); - controllerMessenger.clearEventSubscriptions( - 'NetworkController:providerChange', - ); + try { + await new Promise((resolve) => setTimeout(resolve, 1000)); + expect(controller.state.tokenList).toStrictEqual( + sampleSingleChainState.tokenList, + ); + + expect( + controller.state.tokensChainsCache[NetworksChainId.mainnet].data, + ).toStrictEqual( + sampleSingleChainState.tokensChainsCache[NetworksChainId.mainnet].data, + ); + + expect( + controller.state.tokensChainsCache[NetworksChainId.mainnet].timestamp, + ).toBeGreaterThanOrEqual( + sampleSingleChainState.tokensChainsCache[NetworksChainId.mainnet] + .timestamp, + ); + controller.destroy(); + controllerMessenger.clearEventSubscriptions( + 'NetworkController:providerChange', + ); + } finally { + controller.destroy(); + } }); it('should update the cache before threshold time if the current data is undefined', async () => { diff --git a/src/assets/TokenListController.ts b/packages/assets-controllers/src/TokenListController.ts similarity index 93% rename from src/assets/TokenListController.ts rename to packages/assets-controllers/src/TokenListController.ts index 252ae063b0..675bb23a22 100644 --- a/src/assets/TokenListController.ts +++ b/packages/assets-controllers/src/TokenListController.ts @@ -1,16 +1,22 @@ import type { Patch } from 'immer'; import { Mutex } from 'async-mutex'; -import { AbortController } from 'abort-controller'; -import { BaseController } from '../BaseControllerV2'; -import type { RestrictedControllerMessenger } from '../ControllerMessenger'; -import { safelyExecute, isTokenListSupportedForNetwork } from '../util'; -import { fetchTokenList } from '../apis/token-service'; +import { AbortController as WhatwgAbortController } from 'abort-controller'; +import { + BaseControllerV2, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { safelyExecute } from '@metamask/controller-utils'; import { NetworkControllerProviderChangeEvent, NetworkState, ProviderConfig, -} from '../network/NetworkController'; -import { formatAggregatorNames, formatIconUrlWithProxy } from './assetsUtil'; +} from '@metamask/network-controller'; +import { + isTokenListSupportedForNetwork, + formatAggregatorNames, + formatIconUrlWithProxy, +} from './assetsUtil'; +import { fetchTokenList } from './token-service'; const DEFAULT_INTERVAL = 24 * 60 * 60 * 1000; const DEFAULT_THRESHOLD = 24 * 60 * 60 * 1000; @@ -76,14 +82,14 @@ const defaultState: TokenListState = { /** * Controller that passively polls on a set interval for the list of tokens from metaswaps api */ -export class TokenListController extends BaseController< +export class TokenListController extends BaseControllerV2< typeof name, TokenListState, TokenListMessenger > { private mutex = new Mutex(); - private intervalId?: NodeJS.Timeout; + private intervalId?: ReturnType; private intervalDelay: number; @@ -91,7 +97,7 @@ export class TokenListController extends BaseController< private chainId: string; - private abortController: AbortController; + private abortController: WhatwgAbortController; /** * Creates a TokenListController instance. @@ -134,7 +140,7 @@ export class TokenListController extends BaseController< this.cacheRefreshThreshold = cacheRefreshThreshold; this.chainId = chainId; this.updatePreventPollingOnNetworkRestart(preventPollingOnNetworkRestart); - this.abortController = new AbortController(); + this.abortController = new WhatwgAbortController(); if (onNetworkStateChange) { onNetworkStateChange(async (networkStateOrProviderConfig) => { // this check for "provider" is for testing purposes, since in the extension this callback will receive @@ -168,7 +174,7 @@ export class TokenListController extends BaseController< async #onNetworkStateChangeCallback(providerConfig: ProviderConfig) { if (this.chainId !== providerConfig.chainId) { this.abortController.abort(); - this.abortController = new AbortController(); + this.abortController = new WhatwgAbortController(); this.chainId = providerConfig.chainId; if (this.state.preventPollingOnNetworkRestart) { this.clearingTokenListData(); diff --git a/src/assets/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts similarity index 98% rename from src/assets/TokenRatesController.test.ts rename to packages/assets-controllers/src/TokenRatesController.test.ts index 941aab0ad8..cc4f3d12b7 100644 --- a/src/assets/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -1,11 +1,11 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import nock from 'nock'; -import { PreferencesController } from '../user/PreferencesController'; +import { PreferencesController } from '@metamask/preferences-controller'; import { NetworkController, NetworkControllerMessenger, -} from '../network/NetworkController'; -import { ControllerMessenger } from '../ControllerMessenger'; +} from '@metamask/network-controller'; +import { ControllerMessenger } from '@metamask/base-controller'; import { TokenRatesController } from './TokenRatesController'; import { TokensController } from './TokensController'; diff --git a/src/assets/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts similarity index 96% rename from src/assets/TokenRatesController.ts rename to packages/assets-controllers/src/TokenRatesController.ts index 7e96889543..68eb27e8dd 100644 --- a/src/assets/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -1,9 +1,16 @@ -import { BaseController, BaseConfig, BaseState } from '../BaseController'; -import { safelyExecute, handleFetch, toChecksumHexAddress } from '../util'; - -import type { NetworkState } from '../network/NetworkController'; -import { FALL_BACK_VS_CURRENCY } from '../constants'; -import { fetchExchangeRate as fetchNativeExchangeRate } from '../apis/crypto-compare'; +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; +import { + safelyExecute, + handleFetch, + toChecksumHexAddress, + FALL_BACK_VS_CURRENCY, +} from '@metamask/controller-utils'; +import type { NetworkState } from '@metamask/network-controller'; +import { fetchExchangeRate as fetchNativeExchangeRate } from './crypto-compare'; import type { TokensState } from './TokensController'; import type { CurrencyRateState } from './CurrencyRateController'; @@ -134,7 +141,7 @@ export class TokenRatesController extends BaseController< TokenRatesConfig, TokenRatesState > { - private handle?: NodeJS.Timer; + private handle?: ReturnType; private tokenList: Token[] = []; diff --git a/src/assets/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts similarity index 97% rename from src/assets/TokensController.test.ts rename to packages/assets-controllers/src/TokensController.test.ts index 41c11a1a06..4144d24e8f 100644 --- a/src/assets/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -1,17 +1,23 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import nock from 'nock'; import contractMaps from '@metamask/contract-metadata'; -import { PreferencesController } from '../user/PreferencesController'; -import { TOKEN_END_POINT_API } from '../apis/token-service'; +import { PreferencesController } from '@metamask/preferences-controller'; import { NetworkController, NetworkControllerMessenger, - NetworksChainId, - NetworkType, -} from '../network/NetworkController'; -import { ControllerMessenger } from '../ControllerMessenger'; +} from '@metamask/network-controller'; +import { NetworksChainId, NetworkType } from '@metamask/controller-utils'; +import { ControllerMessenger } from '@metamask/base-controller'; import { TokensController } from './TokensController'; import { Token } from './TokenRatesController'; +import { TOKEN_END_POINT_API } from './token-service'; + +jest.mock('uuid', () => { + return { + ...jest.requireActual('uuid'), + v1: () => '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d', + }; +}); const stubCreateEthers = (ctrl: TokensController, res: boolean) => { return sinon.stub(ctrl, '_createEthersContract').callsFake(() => { @@ -942,23 +948,16 @@ describe('TokensController', () => { }); it('should fail an invalid type suggested asset via watchAsset', async () => { - await new Promise(async (resolve) => { - await tokensController - .watchAsset( - { - address: '0xe9f786dfdd9ae4d57e830acb52296837765f0e5b', - decimals: 18, - symbol: 'TKN', - }, - 'ERC721', - ) - .catch((error) => { - expect(error.message).toContain( - 'Asset of type ERC721 not supported', - ); - resolve(''); - }); - }); + await expect( + tokensController.watchAsset( + { + address: '0xe9f786dfdd9ae4d57e830acb52296837765f0e5b', + decimals: 18, + symbol: 'TKN', + }, + 'ERC721', + ), + ).rejects.toThrow('Asset of type ERC721 not supported'); }); it('should reject a valid suggested asset via watchAsset', async () => { diff --git a/src/assets/TokensController.ts b/packages/assets-controllers/src/TokensController.ts similarity index 97% rename from src/assets/TokensController.ts rename to packages/assets-controllers/src/TokensController.ts index d4dcbddf11..cee7daf3f0 100644 --- a/src/assets/TokensController.ts +++ b/packages/assets-controllers/src/TokensController.ts @@ -5,19 +5,31 @@ import { v1 as random } from 'uuid'; import { Mutex } from 'async-mutex'; import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; -import { AbortController } from 'abort-controller'; -import { BaseController, BaseConfig, BaseState } from '../BaseController'; -import type { PreferencesState } from '../user/PreferencesController'; -import type { NetworkState, NetworkType } from '../network/NetworkController'; -import { validateTokenToWatch, toChecksumHexAddress } from '../util'; -import { MAINNET, ERC721_INTERFACE_ID } from '../constants'; +import { AbortController as WhatwgAbortController } from 'abort-controller'; import { - fetchTokenMetadata, - TOKEN_METADATA_NO_SUPPORT_ERROR, -} from '../apis/token-service'; + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; +import type { PreferencesState } from '@metamask/preferences-controller'; +import type { NetworkState } from '@metamask/network-controller'; +import { + NetworkType, + toChecksumHexAddress, + MAINNET, + ERC721_INTERFACE_ID, +} from '@metamask/controller-utils'; import type { Token } from './TokenRatesController'; import { TokenListToken } from './TokenListController'; -import { formatAggregatorNames, formatIconUrlWithProxy } from './assetsUtil'; +import { + formatAggregatorNames, + formatIconUrlWithProxy, + validateTokenToWatch, +} from './assetsUtil'; +import { + fetchTokenMetadata, + TOKEN_METADATA_NO_SUPPORT_ERROR, +} from './token-service'; /** * @type TokensConfig @@ -113,7 +125,7 @@ export class TokensController extends BaseController< private ethersProvider: any; - private abortController: AbortController; + private abortController: WhatwgAbortController; private failSuggestedAsset( suggestedAssetMeta: SuggestedAssetMeta, @@ -213,7 +225,7 @@ export class TokensController extends BaseController< }; this.initialize(); - this.abortController = new AbortController(); + this.abortController = new WhatwgAbortController(); onPreferencesStateChange(({ selectedAddress }) => { const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; @@ -231,7 +243,7 @@ export class TokensController extends BaseController< const { selectedAddress } = this.config; const { chainId } = provider; this.abortController.abort(); - this.abortController = new AbortController(); + this.abortController = new WhatwgAbortController(); this.configure({ chainId }); this.ethersProvider = this._instantiateNewEthersProvider(); this.update({ diff --git a/packages/assets-controllers/src/assetsUtil.test.ts b/packages/assets-controllers/src/assetsUtil.test.ts new file mode 100644 index 0000000000..e224a7c07e --- /dev/null +++ b/packages/assets-controllers/src/assetsUtil.test.ts @@ -0,0 +1,435 @@ +import { GANACHE_CHAIN_ID, NetworksChainId } from '@metamask/controller-utils'; +import * as assetsUtil from './assetsUtil'; +import { Nft, NftMetadata } from './NftController'; + +const DEFAULT_IPFS_URL_FORMAT = 'ipfs://'; +const ALTERNATIVE_IPFS_URL_FORMAT = 'ipfs://ipfs/'; +const IPFS_CID_V0 = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n'; +const IPFS_CID_V1 = + 'bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku'; + +const IFPS_GATEWAY = 'dweb.link'; + +const SOME_API = 'https://someapi.com'; + +describe('assetsUtil', () => { + describe('compareNftMetadata', () => { + it('should resolve true if any key is different', () => { + const nftMetadata: NftMetadata = { + name: 'name', + image: 'image', + description: 'description', + standard: 'standard', + backgroundColor: 'backgroundColor', + imagePreview: 'imagePreview', + imageThumbnail: 'imageThumbnail', + imageOriginal: 'imageOriginal', + animation: 'animation', + animationOriginal: 'animationOriginal', + externalLink: 'externalLink-123', + }; + const nft: Nft = { + address: 'address', + tokenId: '123', + name: 'name', + image: 'image', + description: 'description', + standard: 'standard', + backgroundColor: 'backgroundColor', + imagePreview: 'imagePreview', + imageThumbnail: 'imageThumbnail', + imageOriginal: 'imageOriginal', + animation: 'animation', + animationOriginal: 'animationOriginal', + externalLink: 'externalLink', + }; + const different = assetsUtil.compareNftMetadata(nftMetadata, nft); + expect(different).toStrictEqual(true); + }); + + it('should resolve true if any key is different as always as metadata is not undefined', () => { + const nftMetadata: NftMetadata = { + name: 'name', + image: 'image', + description: 'description', + standard: 'standard', + externalLink: 'externalLink', + }; + const nft: Nft = { + address: 'address', + tokenId: '123', + name: 'name', + image: 'image', + standard: 'standard', + description: 'description', + backgroundColor: 'backgroundColor', + externalLink: 'externalLink', + }; + const different = assetsUtil.compareNftMetadata(nftMetadata, nft); + expect(different).toStrictEqual(false); + }); + + it('should resolve false if no key is different', () => { + const nftMetadata: NftMetadata = { + name: 'name', + image: 'image', + description: 'description', + standard: 'standard', + backgroundColor: 'backgroundColor', + imagePreview: 'imagePreview', + imageThumbnail: 'imageThumbnail', + imageOriginal: 'imageOriginal', + animation: 'animation', + animationOriginal: 'animationOriginal', + externalLink: 'externalLink', + }; + const nft: Nft = { + address: 'address', + tokenId: '123', + name: 'name', + image: 'image', + standard: 'standard', + description: 'description', + backgroundColor: 'backgroundColor', + imagePreview: 'imagePreview', + imageThumbnail: 'imageThumbnail', + imageOriginal: 'imageOriginal', + animation: 'animation', + animationOriginal: 'animationOriginal', + externalLink: 'externalLink', + }; + const different = assetsUtil.compareNftMetadata(nftMetadata, nft); + expect(different).toStrictEqual(false); + }); + + it('should format aggregator names', () => { + const formattedAggregatorNames = assetsUtil.formatAggregatorNames([ + 'bancor', + 'aave', + 'coinGecko', + ]); + const expectedValue = ['Bancor', 'Aave', 'CoinGecko']; + expect(formattedAggregatorNames).toStrictEqual(expectedValue); + }); + + it('should format icon url with Codefi proxy correctly when passed chainId as a decimal string', () => { + const linkTokenAddress = '0x514910771af9ca656af840dff83e8264ecf986ca'; + const formattedIconUrl = assetsUtil.formatIconUrlWithProxy({ + chainId: NetworksChainId.mainnet, + tokenAddress: linkTokenAddress, + }); + const expectedValue = `https://static.metaswap.codefi.network/api/v1/tokenIcons/${NetworksChainId.mainnet}/${linkTokenAddress}.png`; + expect(formattedIconUrl).toStrictEqual(expectedValue); + }); + + it('should format icon url with Codefi proxy correctly when passed chainId as a hexadecimal string', () => { + const linkTokenAddress = '0x514910771af9ca656af840dff83e8264ecf986ca'; + const formattedIconUrl = assetsUtil.formatIconUrlWithProxy({ + chainId: `0x${Number(NetworksChainId.mainnet).toString(16)}`, + tokenAddress: linkTokenAddress, + }); + const expectedValue = `https://static.metaswap.codefi.network/api/v1/tokenIcons/${NetworksChainId.mainnet}/${linkTokenAddress}.png`; + expect(formattedIconUrl).toStrictEqual(expectedValue); + }); + }); + + describe('validateTokenToWatch', () => { + it('should throw if undefined token atrributes', () => { + expect(() => + assetsUtil.validateTokenToWatch({ + address: undefined, + decimals: 0, + symbol: 'TKN', + } as any), + ).toThrow('Must specify address, symbol, and decimals.'); + + expect(() => + assetsUtil.validateTokenToWatch({ + address: '0x1', + decimals: 0, + symbol: undefined, + } as any), + ).toThrow('Must specify address, symbol, and decimals.'); + + expect(() => + assetsUtil.validateTokenToWatch({ + address: '0x1', + decimals: undefined, + symbol: 'TKN', + } as any), + ).toThrow('Must specify address, symbol, and decimals.'); + }); + + it('should throw if symbol is not a string', () => { + expect(() => + assetsUtil.validateTokenToWatch({ + address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', + decimals: 0, + symbol: { foo: 'bar' }, + } as any), + ).toThrow('Invalid symbol: not a string.'); + }); + + it('should throw if symbol is an empty string', () => { + expect(() => + assetsUtil.validateTokenToWatch({ + address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', + decimals: 0, + symbol: '', + } as any), + ).toThrow('Must specify address, symbol, and decimals.'); + }); + + it('should not throw if symbol is exactly 1 character long', () => { + expect(() => + assetsUtil.validateTokenToWatch({ + address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', + decimals: 0, + symbol: 'T', + } as any), + ).not.toThrow(); + }); + + it('should not throw if symbol is exactly 11 characters long', () => { + expect(() => + assetsUtil.validateTokenToWatch({ + address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', + decimals: 0, + symbol: 'TKNTKNTKNTK', + } as any), + ).not.toThrow(); + }); + + it('should throw if symbol is more than 11 characters long', () => { + expect(() => + assetsUtil.validateTokenToWatch({ + address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', + decimals: 0, + symbol: 'TKNTKNTKNTKN', + } as any), + ).toThrow('Invalid symbol "TKNTKNTKNTKN": longer than 11 characters.'); + }); + + it('should throw if invalid decimals', () => { + expect(() => + assetsUtil.validateTokenToWatch({ + address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', + decimals: 0, + symbol: 'TKN', + } as any), + ).not.toThrow(); + + expect(() => + assetsUtil.validateTokenToWatch({ + address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', + decimals: 38, + symbol: 'TKN', + } as any), + ).toThrow('Invalid decimals "38": must be 0 <= 36.'); + + expect(() => + assetsUtil.validateTokenToWatch({ + address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', + decimals: -1, + symbol: 'TKN', + } as any), + ).toThrow('Invalid decimals "-1": must be 0 <= 36.'); + }); + + it('should throw if invalid address', () => { + expect(() => + assetsUtil.validateTokenToWatch({ + address: '0xe9', + decimals: 0, + symbol: 'TKN', + } as any), + ).toThrow('Invalid address "0xe9".'); + }); + }); + + describe('isTokenDetectionSupportedForNetwork', () => { + it('returns true for Mainnet', () => { + expect( + assetsUtil.isTokenDetectionSupportedForNetwork( + assetsUtil.SupportedTokenDetectionNetworks.mainnet, + ), + ).toBe(true); + }); + + it('returns true for custom network such as BSC', () => { + expect( + assetsUtil.isTokenDetectionSupportedForNetwork( + assetsUtil.SupportedTokenDetectionNetworks.bsc, + ), + ).toBe(true); + }); + + it('returns false for testnets such as Ropsten', () => { + expect(assetsUtil.isTokenDetectionSupportedForNetwork('3')).toBe(false); + }); + }); + + describe('isTokenListSupportedForNetwork', () => { + it('returns true for Mainnet when chainId is passed as a decimal string', () => { + expect( + assetsUtil.isTokenListSupportedForNetwork( + assetsUtil.SupportedTokenDetectionNetworks.mainnet, + ), + ).toBe(true); + }); + + it('returns true for Mainnet when chainId is passed as a hexadecimal string', () => { + expect(assetsUtil.isTokenListSupportedForNetwork('0x1')).toBe(true); + }); + + it('returns true for ganache local network', () => { + expect(assetsUtil.isTokenListSupportedForNetwork(GANACHE_CHAIN_ID)).toBe( + true, + ); + }); + + it('returns true for custom network such as Polygon', () => { + expect( + assetsUtil.isTokenListSupportedForNetwork( + assetsUtil.SupportedTokenDetectionNetworks.polygon, + ), + ).toBe(true); + }); + + it('returns false for testnets such as Ropsten', () => { + expect( + assetsUtil.isTokenListSupportedForNetwork(NetworksChainId.ropsten), + ).toBe(false); + }); + }); + + describe('removeIpfsProtocolPrefix', () => { + it('should return content identifier and path combined string from default ipfs url format', () => { + expect( + assetsUtil.removeIpfsProtocolPrefix( + `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V0}/test`, + ), + ).toStrictEqual(`${IPFS_CID_V0}/test`); + }); + + it('should return content identifier string from default ipfs url format if no path preset', () => { + expect( + assetsUtil.removeIpfsProtocolPrefix( + `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V0}`, + ), + ).toStrictEqual(IPFS_CID_V0); + }); + + it('should return content identifier string from alternate ipfs url format', () => { + expect( + assetsUtil.removeIpfsProtocolPrefix( + `${ALTERNATIVE_IPFS_URL_FORMAT}${IPFS_CID_V0}`, + ), + ).toStrictEqual(IPFS_CID_V0); + }); + + it('should throw error if passed a non ipfs url', () => { + expect(() => assetsUtil.removeIpfsProtocolPrefix(SOME_API)).toThrow( + 'this method should not be used with non ipfs urls', + ); + }); + }); + + describe('getIpfsCIDv1AndPath', () => { + it('should return content identifier from default ipfs url format', () => { + expect( + assetsUtil.getIpfsCIDv1AndPath( + `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V0}`, + ), + ).toStrictEqual({ cid: IPFS_CID_V1, path: undefined }); + }); + + it('should return content identifier from alternative ipfs url format', () => { + expect( + assetsUtil.getIpfsCIDv1AndPath( + `${ALTERNATIVE_IPFS_URL_FORMAT}${IPFS_CID_V0}`, + ), + ).toStrictEqual({ cid: IPFS_CID_V1, path: undefined }); + }); + + it('should return unchanged content identifier if already v1', () => { + expect( + assetsUtil.getIpfsCIDv1AndPath( + `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V1}`, + ), + ).toStrictEqual({ cid: IPFS_CID_V1, path: undefined }); + }); + + it('should return a path when url contains one', () => { + expect( + assetsUtil.getIpfsCIDv1AndPath( + `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V1}/test/test/test`, + ), + ).toStrictEqual({ cid: IPFS_CID_V1, path: '/test/test/test' }); + }); + }); + + describe('getFormattedIpfsUrl', () => { + it('should return a correctly formatted subdomained ipfs url when passed ipfsGateway without protocol prefix, no path and subdomainSupported argument set to true', () => { + expect( + assetsUtil.getFormattedIpfsUrl( + IFPS_GATEWAY, + `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V1}`, + true, + ), + ).toStrictEqual(`https://${IPFS_CID_V1}.ipfs.${IFPS_GATEWAY}`); + }); + + it('should return a correctly formatted subdomained ipfs url when passed ipfsGateway with protocol prefix, a cidv0 and no path and subdomainSupported argument set to true', () => { + expect( + assetsUtil.getFormattedIpfsUrl( + `https://${IFPS_GATEWAY}`, + `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V0}`, + true, + ), + ).toStrictEqual(`https://${IPFS_CID_V1}.ipfs.${IFPS_GATEWAY}`); + }); + + it('should return a correctly formatted subdomained ipfs url when passed ipfsGateway with protocol prefix, a path at the end of the url, and subdomainSupported argument set to true', () => { + expect( + assetsUtil.getFormattedIpfsUrl( + `https://${IFPS_GATEWAY}`, + `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V1}/test`, + true, + ), + ).toStrictEqual(`https://${IPFS_CID_V1}.ipfs.${IFPS_GATEWAY}/test`); + }); + + it('should return a correctly formatted non-subdomained ipfs url when passed ipfsGateway with no "/ipfs/" appended, a path at the end of the url, and subdomainSupported argument set to false', () => { + expect( + assetsUtil.getFormattedIpfsUrl( + `https://${IFPS_GATEWAY}`, + `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V1}/test`, + false, + ), + ).toStrictEqual(`https://${IFPS_GATEWAY}/ipfs/${IPFS_CID_V1}/test`); + }); + + it('should return a correctly formatted non-subdomained ipfs url when passed an ipfsGateway with "/ipfs/" appended, a path at the end of the url, subdomainSupported argument set to false', () => { + expect( + assetsUtil.getFormattedIpfsUrl( + `https://${IFPS_GATEWAY}/ipfs/`, + `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V1}/test`, + false, + ), + ).toStrictEqual(`https://${IFPS_GATEWAY}/ipfs/${IPFS_CID_V1}/test`); + }); + }); + + describe('addUrlProtocolPrefix', () => { + it('should return a URL with https:// prepended if input URL does not already have it', () => { + expect(assetsUtil.addUrlProtocolPrefix(IFPS_GATEWAY)).toStrictEqual( + `https://${IFPS_GATEWAY}`, + ); + }); + + it('should return a URL as is if https:// is already prepended', () => { + expect(assetsUtil.addUrlProtocolPrefix(SOME_API)).toStrictEqual(SOME_API); + }); + }); +}); diff --git a/packages/assets-controllers/src/assetsUtil.ts b/packages/assets-controllers/src/assetsUtil.ts new file mode 100644 index 0000000000..c6a5ea7665 --- /dev/null +++ b/packages/assets-controllers/src/assetsUtil.ts @@ -0,0 +1,250 @@ +import { ethErrors } from 'eth-rpc-errors'; +import { CID } from 'multiformats/cid'; +import { + convertHexToDecimal, + isValidHexAddress, + GANACHE_CHAIN_ID, +} from '@metamask/controller-utils'; +import { Nft, NftMetadata } from './NftController'; +import { Token } from './TokenRatesController'; + +/** + * Compares nft metadata entries to any nft entry. + * We need this method when comparing a new fetched nft metadata, in case a entry changed to a defined value, + * there's a need to update the nft in state. + * + * @param newNftMetadata - Nft metadata object. + * @param nft - Nft object to compare with. + * @returns Whether there are differences. + */ +export function compareNftMetadata(newNftMetadata: NftMetadata, nft: Nft) { + const keys: (keyof NftMetadata)[] = [ + 'image', + 'backgroundColor', + 'imagePreview', + 'imageThumbnail', + 'imageOriginal', + 'animation', + 'animationOriginal', + 'externalLink', + ]; + const differentValues = keys.reduce((value, key) => { + if (newNftMetadata[key] && newNftMetadata[key] !== nft[key]) { + return value + 1; + } + return value; + }, 0); + return differentValues > 0; +} + +const aggregatorNameByKey: Record = { + aave: 'Aave', + bancor: 'Bancor', + cmc: 'CMC', + cryptocom: 'Crypto.com', + coinGecko: 'CoinGecko', + oneInch: '1inch', + paraswap: 'Paraswap', + pmm: 'PMM', + zapper: 'Zapper', + zerion: 'Zerion', + zeroEx: '0x', + synthetix: 'Synthetix', + yearn: 'Yearn', + apeswap: 'ApeSwap', + binanceDex: 'BinanceDex', + pancakeTop100: 'PancakeTop100', + pancakeExtended: 'PancakeExtended', + balancer: 'Balancer', + quickswap: 'QuickSwap', + matcha: 'Matcha', + pangolinDex: 'PangolinDex', + pangolinDexStableCoin: 'PangolinDexStableCoin', + pangolinDexAvaxBridge: 'PangolinDexAvaxBridge', + traderJoe: 'TraderJoe', + airswapLight: 'AirswapLight', + kleros: 'Kleros', +}; + +/** + * Formats aggregator names to presentable format. + * + * @param aggregators - List of token list names in camelcase. + * @returns Formatted aggregator names. + */ +export const formatAggregatorNames = (aggregators: string[]) => { + return aggregators.map( + (key) => + aggregatorNameByKey[key] || + `${key[0].toUpperCase()}${key.substring(1, key.length)}`, + ); +}; + +/** + * Format token list assets to use image proxy from Codefi. + * + * @param params - Object that contains chainID and tokenAddress. + * @param params.chainId - ChainID of network in decimal or hexadecimal format. + * @param params.tokenAddress - Address of token in mixed or lowercase. + * @returns Formatted image url + */ +export const formatIconUrlWithProxy = ({ + chainId, + tokenAddress, +}: { + chainId: string; + tokenAddress: string; +}) => { + const chainIdDecimal = convertHexToDecimal(chainId).toString(); + return `https://static.metaswap.codefi.network/api/v1/tokenIcons/${chainIdDecimal}/${tokenAddress.toLowerCase()}.png`; +}; + +/** + * Validates a ERC20 token to be added with EIP747. + * + * @param token - Token object to validate. + */ +export function validateTokenToWatch(token: Token) { + const { address, symbol, decimals } = token; + if (!address || !symbol || typeof decimals === 'undefined') { + throw ethErrors.rpc.invalidParams( + `Must specify address, symbol, and decimals.`, + ); + } + + if (typeof symbol !== 'string') { + throw ethErrors.rpc.invalidParams(`Invalid symbol: not a string.`); + } + + if (symbol.length > 11) { + throw ethErrors.rpc.invalidParams( + `Invalid symbol "${symbol}": longer than 11 characters.`, + ); + } + const numDecimals = parseInt(decimals as unknown as string, 10); + if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) { + throw ethErrors.rpc.invalidParams( + `Invalid decimals "${decimals}": must be 0 <= 36.`, + ); + } + + if (!isValidHexAddress(address)) { + throw ethErrors.rpc.invalidParams(`Invalid address "${address}".`); + } +} + +/** + * Networks where token detection is supported - Values are in decimal format + */ +export enum SupportedTokenDetectionNetworks { + mainnet = '1', + bsc = '56', + polygon = '137', + avax = '43114', +} + +/** + * Check if token detection is enabled for certain networks. + * + * @param chainId - ChainID of network + * @returns Whether the current network supports token detection + */ +export function isTokenDetectionSupportedForNetwork(chainId: string): boolean { + return Object.values(SupportedTokenDetectionNetworks).includes( + chainId, + ); +} + +/** + * Check if token list polling is enabled for a given network. + * Currently this method is used to support e2e testing for consumers of this package. + * + * @param chainId - ChainID of network + * @returns Whether the current network supports tokenlists + */ +export function isTokenListSupportedForNetwork(chainId: string): boolean { + const chainIdDecimal = convertHexToDecimal(chainId).toString(); + return ( + isTokenDetectionSupportedForNetwork(chainIdDecimal) || + chainIdDecimal === GANACHE_CHAIN_ID + ); +} + +/** + * Removes IPFS protocol prefix from input string. + * + * @param ipfsUrl - An IPFS url (e.g. ipfs://{content id}) + * @returns IPFS content identifier and (possibly) path in a string + * @throws Will throw if the url passed is not IPFS. + */ +export function removeIpfsProtocolPrefix(ipfsUrl: string) { + if (ipfsUrl.startsWith('ipfs://ipfs/')) { + return ipfsUrl.replace('ipfs://ipfs/', ''); + } else if (ipfsUrl.startsWith('ipfs://')) { + return ipfsUrl.replace('ipfs://', ''); + } + // this method should not be used with non-ipfs urls (i.e. startsWith('ipfs://') === true) + throw new Error('this method should not be used with non ipfs urls'); +} + +/** + * Extracts content identifier and path from an input string. + * + * @param ipfsUrl - An IPFS URL minus the IPFS protocol prefix + * @returns IFPS content identifier (cid) and sub path as string. + * @throws Will throw if the url passed is not ipfs. + */ +export function getIpfsCIDv1AndPath(ipfsUrl: string): { + cid: string; + path?: string; +} { + const url = removeIpfsProtocolPrefix(ipfsUrl); + + // check if there is a path + // (CID is everything preceding first forward slash, path is everything after) + const index = url.indexOf('/'); + const cid = index !== -1 ? url.substring(0, index) : url; + const path = index !== -1 ? url.substring(index) : undefined; + + // We want to ensure that the CID is v1 (https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) + // because most cid v0s appear to be incompatible with IPFS subdomains + return { + cid: CID.parse(cid).toV1().toString(), + path, + }; +} + +/** + * Formats URL correctly for use retrieving assets hosted on IPFS. + * + * @param ipfsGateway - The users preferred IPFS gateway (full URL or just host). + * @param ipfsUrl - The IFPS URL pointed at the asset. + * @param subdomainSupported - Boolean indicating whether the URL should be formatted with subdomains or not. + * @returns A formatted URL, with the user's preferred IPFS gateway and format (subdomain or not), pointing to an asset hosted on IPFS. + */ +export function getFormattedIpfsUrl( + ipfsGateway: string, + ipfsUrl: string, + subdomainSupported: boolean, +): string { + const { host, protocol, origin } = new URL(addUrlProtocolPrefix(ipfsGateway)); + if (subdomainSupported) { + const { cid, path } = getIpfsCIDv1AndPath(ipfsUrl); + return `${protocol}//${cid}.ipfs.${host}${path ?? ''}`; + } + const cidAndPath = removeIpfsProtocolPrefix(ipfsUrl); + return `${origin}/ipfs/${cidAndPath}`; +} + +/** + * Adds URL protocol prefix to input URL string if missing. + * + * @param urlString - An IPFS URL. + * @returns A URL with a https:// prepended. + */ +export function addUrlProtocolPrefix(urlString: string): string { + if (!urlString.match(/(^http:\/\/)|(^https:\/\/)/u)) { + return `https://${urlString}`; + } + return urlString; +} diff --git a/src/apis/crypto-compare.test.ts b/packages/assets-controllers/src/crypto-compare.test.ts similarity index 99% rename from src/apis/crypto-compare.test.ts rename to packages/assets-controllers/src/crypto-compare.test.ts index fb78846a09..3a11ee8ec9 100644 --- a/src/apis/crypto-compare.test.ts +++ b/packages/assets-controllers/src/crypto-compare.test.ts @@ -1,5 +1,4 @@ import nock from 'nock'; - import { fetchExchangeRate } from './crypto-compare'; const cryptoCompareHost = 'https://min-api.cryptocompare.com'; diff --git a/src/apis/crypto-compare.ts b/packages/assets-controllers/src/crypto-compare.ts similarity index 97% rename from src/apis/crypto-compare.ts rename to packages/assets-controllers/src/crypto-compare.ts index 0266bd24e6..619484131e 100644 --- a/src/apis/crypto-compare.ts +++ b/packages/assets-controllers/src/crypto-compare.ts @@ -1,4 +1,4 @@ -import { handleFetch } from '../util'; +import { handleFetch } from '@metamask/controller-utils'; /** * Get the CryptoCompare API URL for getting the conversion rate from the given native currency to diff --git a/packages/assets-controllers/src/index.ts b/packages/assets-controllers/src/index.ts new file mode 100644 index 0000000000..a22a755cd6 --- /dev/null +++ b/packages/assets-controllers/src/index.ts @@ -0,0 +1,11 @@ +export * from './AccountTrackerController'; +export * from './AssetsContractController'; +export * from './CurrencyRateController'; +export * from './NftController'; +export * from './NftDetectionController'; +export * from './TokenBalancesController'; +export * from './TokenDetectionController'; +export * from './TokenListController'; +export * from './TokenRatesController'; +export * from './TokensController'; +export { formatIconUrlWithProxy } from './assetsUtil'; diff --git a/src/apis/token-service.test.ts b/packages/assets-controllers/src/token-service.test.ts similarity index 78% rename from src/apis/token-service.test.ts rename to packages/assets-controllers/src/token-service.test.ts index 4ae75bfe76..99267029a0 100644 --- a/src/apis/token-service.test.ts +++ b/packages/assets-controllers/src/token-service.test.ts @@ -1,5 +1,5 @@ import nock from 'nock'; -import { NetworksChainId } from '../network/NetworkController'; +import { AbortController as WhatwgAbortController } from 'abort-controller'; import { fetchTokenList, fetchTokenMetadata, @@ -132,6 +132,8 @@ const sampleToken = { name: 'Chainlink', }; +const sampleChainId = '1'; + describe('Token service', () => { beforeAll(() => { nock.disableNetConnect(); @@ -147,28 +149,28 @@ describe('Token service', () => { describe('fetchTokenList', () => { it('should call the tokens api and return the list of tokens', async () => { - const { signal } = new AbortController(); + const { signal } = new WhatwgAbortController(); nock(TOKEN_END_POINT_API) - .get(`/tokens/${NetworksChainId.mainnet}`) + .get(`/tokens/${sampleChainId}`) .reply(200, sampleTokenList) .persist(); - const tokens = await fetchTokenList(NetworksChainId.mainnet, signal); + const tokens = await fetchTokenList(sampleChainId, signal); expect(tokens).toStrictEqual(sampleTokenList); }); it('should return undefined if the fetch is aborted', async () => { - const abortController = new AbortController(); + const abortController = new WhatwgAbortController(); nock(TOKEN_END_POINT_API) - .get(`/tokens/${NetworksChainId.mainnet}`) + .get(`/tokens/${sampleChainId}`) // well beyond time it will take to abort .delay(ONE_SECOND_IN_MILLISECONDS) .reply(200, sampleTokenList) .persist(); const fetchPromise = fetchTokenList( - NetworksChainId.mainnet, + sampleChainId, abortController.signal, ); abortController.abort(); @@ -177,39 +179,39 @@ describe('Token service', () => { }); it('should return undefined if the fetch fails with a network error', async () => { - const { signal } = new AbortController(); + const { signal } = new WhatwgAbortController(); nock(TOKEN_END_POINT_API) - .get(`/tokens/${NetworksChainId.mainnet}`) + .get(`/tokens/${sampleChainId}`) .replyWithError('Example network error') .persist(); - const result = await fetchTokenList(NetworksChainId.mainnet, signal); + const result = await fetchTokenList(sampleChainId, signal); expect(result).toBeUndefined(); }); it('should return undefined if the fetch fails with an unsuccessful status code', async () => { - const { signal } = new AbortController(); + const { signal } = new WhatwgAbortController(); nock(TOKEN_END_POINT_API) - .get(`/tokens/${NetworksChainId.mainnet}`) + .get(`/tokens/${sampleChainId}`) .reply(500) .persist(); - const result = await fetchTokenList(NetworksChainId.mainnet, signal); + const result = await fetchTokenList(sampleChainId, signal); expect(result).toBeUndefined(); }); it('should return undefined if the fetch fails with a timeout', async () => { - const { signal } = new AbortController(); + const { signal } = new WhatwgAbortController(); nock(TOKEN_END_POINT_API) - .get(`/tokens/${NetworksChainId.mainnet}`) + .get(`/tokens/${sampleChainId}`) // well beyond timeout .delay(ONE_SECOND_IN_MILLISECONDS) .reply(200, sampleTokenList) .persist(); - const result = await fetchTokenList(NetworksChainId.mainnet, signal, { + const result = await fetchTokenList(sampleChainId, signal, { timeout: ONE_MILLISECOND, }); @@ -219,16 +221,16 @@ describe('Token service', () => { describe('fetchTokenMetadata', () => { it('should call the api to return the token metadata for eth address provided', async () => { - const { signal } = new AbortController(); + const { signal } = new WhatwgAbortController(); nock(TOKEN_END_POINT_API) .get( - `/token/${NetworksChainId.mainnet}?address=0x514910771af9ca656af840dff83e8264ecf986ca`, + `/token/${sampleChainId}?address=0x514910771af9ca656af840dff83e8264ecf986ca`, ) .reply(200, sampleToken) .persist(); const token = await fetchTokenMetadata( - NetworksChainId.mainnet, + sampleChainId, '0x514910771af9ca656af840dff83e8264ecf986ca', signal, ); @@ -237,16 +239,16 @@ describe('Token service', () => { }); it('should return undefined if the fetch is aborted', async () => { - const abortController = new AbortController(); + const abortController = new WhatwgAbortController(); nock(TOKEN_END_POINT_API) - .get(`/tokens/${NetworksChainId.mainnet}`) + .get(`/tokens/${sampleChainId}`) // well beyond time it will take to abort .delay(ONE_SECOND_IN_MILLISECONDS) .reply(200, sampleTokenList) .persist(); const fetchPromise = fetchTokenMetadata( - NetworksChainId.mainnet, + sampleChainId, '0x514910771af9ca656af840dff83e8264ecf986ca', abortController.signal, ); @@ -256,14 +258,14 @@ describe('Token service', () => { }); it('should return undefined if the fetch fails with a network error', async () => { - const { signal } = new AbortController(); + const { signal } = new WhatwgAbortController(); nock(TOKEN_END_POINT_API) - .get(`/tokens/${NetworksChainId.mainnet}`) + .get(`/tokens/${sampleChainId}`) .replyWithError('Example network error') .persist(); const tokenMetadata = await fetchTokenMetadata( - NetworksChainId.mainnet, + sampleChainId, '0x514910771af9ca656af840dff83e8264ecf986ca', signal, ); @@ -272,14 +274,14 @@ describe('Token service', () => { }); it('should return undefined if the fetch fails with an unsuccessful status code', async () => { - const { signal } = new AbortController(); + const { signal } = new WhatwgAbortController(); nock(TOKEN_END_POINT_API) - .get(`/tokens/${NetworksChainId.mainnet}`) + .get(`/tokens/${sampleChainId}`) .reply(500) .persist(); const tokenMetadata = await fetchTokenMetadata( - NetworksChainId.mainnet, + sampleChainId, '0x514910771af9ca656af840dff83e8264ecf986ca', signal, ); @@ -288,16 +290,16 @@ describe('Token service', () => { }); it('should return undefined if the fetch fails with a timeout', async () => { - const { signal } = new AbortController(); + const { signal } = new WhatwgAbortController(); nock(TOKEN_END_POINT_API) - .get(`/tokens/${NetworksChainId.mainnet}`) + .get(`/tokens/${sampleChainId}`) // well beyond timeout .delay(ONE_SECOND_IN_MILLISECONDS) .reply(200, sampleTokenList) .persist(); const tokenMetadata = await fetchTokenMetadata( - NetworksChainId.mainnet, + sampleChainId, '0x514910771af9ca656af840dff83e8264ecf986ca', signal, { timeout: ONE_MILLISECOND }, @@ -307,10 +309,10 @@ describe('Token service', () => { }); it('should throw error if fetching from non supported network', async () => { - const { signal } = new AbortController(); + const { signal } = new WhatwgAbortController(); await expect( fetchTokenMetadata( - NetworksChainId.goerli, + '5', '0x514910771af9ca656af840dff83e8264ecf986ca', signal, ), @@ -319,13 +321,13 @@ describe('Token service', () => { }); it('should call the tokens api and return undefined', async () => { - const { signal } = new AbortController(); + const { signal } = new WhatwgAbortController(); nock(TOKEN_END_POINT_API) - .get(`/tokens/${NetworksChainId.mainnet}`) + .get(`/tokens/${sampleChainId}`) .reply(404, undefined) .persist(); - const tokens = await fetchTokenList(NetworksChainId.mainnet, signal); + const tokens = await fetchTokenList(sampleChainId, signal); expect(tokens).toBeUndefined(); }); diff --git a/src/apis/token-service.ts b/packages/assets-controllers/src/token-service.ts similarity index 97% rename from src/apis/token-service.ts rename to packages/assets-controllers/src/token-service.ts index 368946a450..4a9d118374 100644 --- a/src/apis/token-service.ts +++ b/packages/assets-controllers/src/token-service.ts @@ -1,4 +1,5 @@ -import { isTokenListSupportedForNetwork, timeoutFetch } from '../util'; +import { timeoutFetch } from '@metamask/controller-utils'; +import { isTokenListSupportedForNetwork } from './assetsUtil'; export const TOKEN_END_POINT_API = 'https://token-api.metaswap.codefi.network'; export const TOKEN_METADATA_NO_SUPPORT_ERROR = diff --git a/packages/assets-controllers/tsconfig.build.json b/packages/assets-controllers/tsconfig.build.json new file mode 100644 index 0000000000..e819874528 --- /dev/null +++ b/packages/assets-controllers/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" }, + { "path": "../preferences-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/assets-controllers/tsconfig.json b/packages/assets-controllers/tsconfig.json new file mode 100644 index 0000000000..b88c89c2a2 --- /dev/null +++ b/packages/assets-controllers/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" }, + { "path": "../network-controller" }, + { "path": "../preferences-controller" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/assets-controllers/typedoc.json b/packages/assets-controllers/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/assets-controllers/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/base-controller/CHANGELOG.md b/packages/base-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/base-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/base-controller/LICENSE b/packages/base-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/base-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/base-controller/README.md b/packages/base-controller/README.md new file mode 100644 index 0000000000..bf251a0363 --- /dev/null +++ b/packages/base-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/base-controller` + +Provides scaffolding for controllers as well a communication system for all controllers. + +## Installation + +`yarn add @metamask/base-controller` + +or + +`npm install @metamask/base-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/base-controller/jest.config.js b/packages/base-controller/jest.config.js new file mode 100644 index 0000000000..37bb024274 --- /dev/null +++ b/packages/base-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 98, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/base-controller/package.json b/packages/base-controller/package.json new file mode 100644 index 0000000000..db02c2c365 --- /dev/null +++ b/packages/base-controller/package.json @@ -0,0 +1,51 @@ +{ + "name": "@metamask/base-controller", + "version": "0.0.0", + "description": "Provides scaffolding for controllers as well a communication system for all controllers", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/base-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/base-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "immer": "^9.0.6" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "@types/sinon": "^9.0.10", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "sinon": "^9.2.4", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/BaseController.test.ts b/packages/base-controller/src/BaseController.test.ts similarity index 98% rename from src/BaseController.test.ts rename to packages/base-controller/src/BaseController.test.ts index d5a86fb4ba..dca6e9d220 100644 --- a/src/BaseController.test.ts +++ b/packages/base-controller/src/BaseController.test.ts @@ -1,4 +1,4 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import { BaseController, BaseConfig, BaseState } from './BaseController'; const STATE = { name: 'foo' }; diff --git a/src/BaseController.ts b/packages/base-controller/src/BaseController.ts similarity index 100% rename from src/BaseController.ts rename to packages/base-controller/src/BaseController.ts diff --git a/src/BaseControllerV2.test.ts b/packages/base-controller/src/BaseControllerV2.test.ts similarity index 99% rename from src/BaseControllerV2.test.ts rename to packages/base-controller/src/BaseControllerV2.test.ts index d640db639f..f199aa9ae5 100644 --- a/src/BaseControllerV2.test.ts +++ b/packages/base-controller/src/BaseControllerV2.test.ts @@ -1,6 +1,5 @@ import type { Draft, Patch } from 'immer'; -import sinon from 'sinon'; - +import * as sinon from 'sinon'; import { BaseController, getAnonymizedState, diff --git a/src/BaseControllerV2.ts b/packages/base-controller/src/BaseControllerV2.ts similarity index 100% rename from src/BaseControllerV2.ts rename to packages/base-controller/src/BaseControllerV2.ts diff --git a/src/ControllerMessenger.test.ts b/packages/base-controller/src/ControllerMessenger.test.ts similarity index 99% rename from src/ControllerMessenger.test.ts rename to packages/base-controller/src/ControllerMessenger.test.ts index 55759315d3..3a8e66eada 100644 --- a/src/ControllerMessenger.test.ts +++ b/packages/base-controller/src/ControllerMessenger.test.ts @@ -1,6 +1,5 @@ import type { Patch } from 'immer'; -import sinon from 'sinon'; - +import * as sinon from 'sinon'; import { ControllerMessenger } from './ControllerMessenger'; describe('ControllerMessenger', () => { diff --git a/src/ControllerMessenger.ts b/packages/base-controller/src/ControllerMessenger.ts similarity index 100% rename from src/ControllerMessenger.ts rename to packages/base-controller/src/ControllerMessenger.ts diff --git a/packages/base-controller/src/index.ts b/packages/base-controller/src/index.ts new file mode 100644 index 0000000000..76d70d9a2c --- /dev/null +++ b/packages/base-controller/src/index.ts @@ -0,0 +1,17 @@ +export { + BaseConfig, + BaseController, + BaseState, + Listener, +} from './BaseController'; +export { + BaseController as BaseControllerV2, + Listener as ListenerV2, + StateDeriver, + StateMetadata, + StatePropertyMetadata, + Json, + getAnonymizedState, + getPersistentState, +} from './BaseControllerV2'; +export * from './ControllerMessenger'; diff --git a/packages/base-controller/tsconfig.build.json b/packages/base-controller/tsconfig.build.json new file mode 100644 index 0000000000..0df910b215 --- /dev/null +++ b/packages/base-controller/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["../../types", "./src"] +} diff --git a/packages/base-controller/tsconfig.json b/packages/base-controller/tsconfig.json new file mode 100644 index 0000000000..ee9de925a2 --- /dev/null +++ b/packages/base-controller/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "include": ["../../types", "./src"] +} diff --git a/packages/base-controller/typedoc.json b/packages/base-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/base-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/composable-controller/CHANGELOG.md b/packages/composable-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/composable-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/composable-controller/LICENSE b/packages/composable-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/composable-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/composable-controller/README.md b/packages/composable-controller/README.md new file mode 100644 index 0000000000..a195b1e644 --- /dev/null +++ b/packages/composable-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/composable-controller` + +Consolidates the state from multiple controllers into one. + +## Installation + +`yarn add @metamask/composable-controller` + +or + +`npm install @metamask/composable-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/composable-controller/jest.config.js b/packages/composable-controller/jest.config.js new file mode 100644 index 0000000000..a7ae2b4b47 --- /dev/null +++ b/packages/composable-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/composable-controller/package.json b/packages/composable-controller/package.json new file mode 100644 index 0000000000..20c0e57699 --- /dev/null +++ b/packages/composable-controller/package.json @@ -0,0 +1,57 @@ +{ + "name": "@metamask/composable-controller", + "version": "0.0.0", + "description": "Consolidates the state from multiple controllers into one", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/composable-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/composable-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~" + }, + "devDependencies": { + "@metamask/address-book-controller": "workspace:~", + "@metamask/assets-controllers": "workspace:~", + "@metamask/auto-changelog": "^3.0.0", + "@metamask/controller-utils": "workspace:~", + "@metamask/ens-controller": "workspace:~", + "@metamask/network-controller": "workspace:~", + "@metamask/preferences-controller": "workspace:~", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "immer": "^9.0.6", + "jest": "^26.4.2", + "sinon": "^9.2.4", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/ComposableController.test.ts b/packages/composable-controller/src/ComposableController.test.ts similarity index 96% rename from src/ComposableController.test.ts rename to packages/composable-controller/src/ComposableController.test.ts index 0f1570e7f3..08accbc5cf 100644 --- a/src/ComposableController.test.ts +++ b/packages/composable-controller/src/ComposableController.test.ts @@ -1,27 +1,30 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import type { Patch } from 'immer'; -import { TokensController } from './assets/TokensController'; -import { NftController } from './assets/NftController'; -import { AddressBookController } from './user/AddressBookController'; -import { EnsController } from './third-party/EnsController'; import { - ComposableController, - ComposableControllerRestrictedMessenger, -} from './ComposableController'; -import { BaseController, BaseState } from './BaseController'; -import { BaseController as BaseControllerV2 } from './BaseControllerV2'; + TokensController, + NftController, + AssetsContractController, +} from '@metamask/assets-controllers'; +import { AddressBookController } from '@metamask/address-book-controller'; +import { EnsController } from '@metamask/ens-controller'; import { + BaseController, + BaseState, + BaseControllerV2, ControllerMessenger, RestrictedControllerMessenger, -} from './ControllerMessenger'; -import { PreferencesController } from './user/PreferencesController'; +} from '@metamask/base-controller'; +import { PreferencesController } from '@metamask/preferences-controller'; import { NetworkController, NetworkControllerMessenger, NetworkControllerStateChangeEvent, - NetworksChainId, -} from './network/NetworkController'; -import { AssetsContractController } from './assets/AssetsContractController'; +} from '@metamask/network-controller'; +import { NetworksChainId } from '@metamask/controller-utils'; +import { + ComposableController, + ComposableControllerRestrictedMessenger, +} from './ComposableController'; // Mock BaseControllerV2 classes diff --git a/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts similarity index 95% rename from src/ComposableController.ts rename to packages/composable-controller/src/ComposableController.ts index 9f40ffb02d..6fc44931d6 100644 --- a/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -1,5 +1,7 @@ -import { BaseController } from './BaseController'; -import { RestrictedControllerMessenger } from './ControllerMessenger'; +import { + BaseController, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; /** * List of child controller instances diff --git a/packages/composable-controller/src/index.ts b/packages/composable-controller/src/index.ts new file mode 100644 index 0000000000..b1a9e5a4b1 --- /dev/null +++ b/packages/composable-controller/src/index.ts @@ -0,0 +1 @@ +export * from './ComposableController'; diff --git a/packages/composable-controller/tsconfig.build.json b/packages/composable-controller/tsconfig.build.json new file mode 100644 index 0000000000..218c76b2cd --- /dev/null +++ b/packages/composable-controller/tsconfig.build.json @@ -0,0 +1,29 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { + "path": "../address-book-controller/tsconfig.build.json" + }, + { + "path": "../assets-controllers/tsconfig.build.json" + }, + { + "path": "../base-controller/tsconfig.build.json" + }, + { + "path": "../ens-controller/tsconfig.build.json" + }, + { + "path": "../network-controller/tsconfig.build.json" + }, + { + "path": "../preferences-controller/tsconfig.build.json" + } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/composable-controller/tsconfig.json b/packages/composable-controller/tsconfig.json new file mode 100644 index 0000000000..372239daaa --- /dev/null +++ b/packages/composable-controller/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { + "path": "../address-book-controller" + }, + { + "path": "../assets-controllers" + }, + { + "path": "../base-controller" + }, + { + "path": "../ens-controller" + }, + { + "path": "../network-controller" + }, + { + "path": "../preferences-controller" + } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/composable-controller/typedoc.json b/packages/composable-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/composable-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/controller-utils/CHANGELOG.md b/packages/controller-utils/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/controller-utils/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/controller-utils/LICENSE b/packages/controller-utils/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/controller-utils/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/controller-utils/README.md b/packages/controller-utils/README.md new file mode 100644 index 0000000000..0d9ed16334 --- /dev/null +++ b/packages/controller-utils/README.md @@ -0,0 +1,15 @@ +# `@metamask/controller-utils` + +Data and convenience functions shared by multiple packages. + +## Installation + +`yarn add @metamask/controller-utils` + +or + +`npm install @metamask/controller-utils` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/controller-utils/jest.config.js b/packages/controller-utils/jest.config.js new file mode 100644 index 0000000000..52278f2baf --- /dev/null +++ b/packages/controller-utils/jest.config.js @@ -0,0 +1,28 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 68.05, + functions: 80.55, + lines: 69.82, + statements: 70.17, + }, + }, + + // We rely on `window` to make requests + testEnvironment: 'jsdom', +}); diff --git a/packages/controller-utils/package.json b/packages/controller-utils/package.json new file mode 100644 index 0000000000..8594283223 --- /dev/null +++ b/packages/controller-utils/package.json @@ -0,0 +1,57 @@ +{ + "name": "@metamask/controller-utils", + "version": "0.0.0", + "description": "Data and convenience functions shared by multiple packages", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/controller-utils#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/controller-utils", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~", + "eth-ens-namehash": "^2.0.8", + "eth-rpc-errors": "^4.0.0", + "ethereumjs-util": "^7.0.10", + "ethjs-unit": "^0.1.6", + "fast-deep-equal": "^3.1.3", + "isomorphic-fetch": "^3.0.0" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "abort-controller": "^3.0.0", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "nock": "^13.0.7", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/constants.ts b/packages/controller-utils/src/constants.ts similarity index 90% rename from src/constants.ts rename to packages/controller-utils/src/constants.ts index e1a16dd919..24c4dfb0ba 100644 --- a/src/constants.ts +++ b/packages/controller-utils/src/constants.ts @@ -1,4 +1,4 @@ -import { NetworkType } from './network/NetworkController'; +import { NetworkType } from './types'; export const MAINNET = 'mainnet'; export const RPC = 'rpc'; @@ -25,9 +25,6 @@ export const ERC1155_TOKEN_RECEIVER_INTERFACE_ID = '0x4e2312e0'; // UNITS export const GWEI = 'gwei'; -// TRANSACTION CONTROLLER ERRORS -export const ESTIMATE_GAS_ERROR = 'eth_estimateGas rpc method error'; - // ASSET TYPES export const ASSET_TYPES = { NATIVE: 'NATIVE', diff --git a/packages/controller-utils/src/index.ts b/packages/controller-utils/src/index.ts new file mode 100644 index 0000000000..e0a2d81dc4 --- /dev/null +++ b/packages/controller-utils/src/index.ts @@ -0,0 +1,5 @@ +import 'isomorphic-fetch'; + +export * from './constants'; +export * from './util'; +export * from './types'; diff --git a/packages/controller-utils/src/types.ts b/packages/controller-utils/src/types.ts new file mode 100644 index 0000000000..6ee96c1cc6 --- /dev/null +++ b/packages/controller-utils/src/types.ts @@ -0,0 +1,21 @@ +/** + * Human-readable network name + */ +export type NetworkType = + | 'kovan' + | 'localhost' + | 'mainnet' + | 'rinkeby' + | 'goerli' + | 'ropsten' + | 'rpc'; + +export enum NetworksChainId { + mainnet = '1', + kovan = '42', + rinkeby = '4', + goerli = '5', + ropsten = '3', + localhost = '', + rpc = '', +} diff --git a/packages/controller-utils/src/util.test.ts b/packages/controller-utils/src/util.test.ts new file mode 100644 index 0000000000..eb2b27e057 --- /dev/null +++ b/packages/controller-utils/src/util.test.ts @@ -0,0 +1,560 @@ +import { BN } from 'ethereumjs-util'; +import nock from 'nock'; +import * as util from './util'; + +const VALID = '4e1fF7229BDdAf0A73DF183a88d9c3a04cc975e0'; +const SOME_API = 'https://someapi.com'; +const SOME_FAILING_API = 'https://somefailingapi.com'; + +describe('util', () => { + beforeEach(() => { + nock.cleanAll(); + }); + + it('bNToHex', () => { + expect(util.BNToHex(new BN('1337'))).toBe('0x539'); + }); + + it('fractionBN', () => { + expect(util.fractionBN(new BN('1337'), 9, 10).toNumber()).toBe(1203); + }); + + it('getBuyURL', () => { + expect(util.getBuyURL(undefined, 'foo', 1337)).toBe( + 'https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=1337&address=foo&crypto_currency=ETH', + ); + + expect(util.getBuyURL('1', 'foo', 1337)).toBe( + 'https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=1337&address=foo&crypto_currency=ETH', + ); + expect(util.getBuyURL('3')).toBe('https://faucet.metamask.io/'); + expect(util.getBuyURL('4')).toBe('https://www.rinkeby.io/'); + expect(util.getBuyURL('5')).toBe('https://goerli-faucet.slock.it/'); + expect(util.getBuyURL('42')).toBe( + 'https://github.com/kovan-testnet/faucet', + ); + expect(util.getBuyURL('unrecognized network ID')).toBeUndefined(); + }); + + it('hexToBN', () => { + expect(util.hexToBN('0x1337').toNumber()).toBe(4919); + }); + + describe('fromHex', () => { + it('converts a string that represents a number in hexadecimal format with leading "0x" into a BN', () => { + expect(util.fromHex('0x1337')).toStrictEqual(new BN(4919)); + }); + + it('converts a string that represents a number in hexadecimal format without leading "0x" into a BN', () => { + expect(util.fromHex('1337')).toStrictEqual(new BN(4919)); + }); + + it('does nothing to a BN', () => { + const bn = new BN(4919); + expect(util.fromHex(bn)).toBe(bn); + }); + }); + + describe('toHex', () => { + it('converts a BN to a hex string prepended with "0x"', () => { + expect(util.toHex(new BN(4919))).toStrictEqual('0x1337'); + }); + + it('parses a string as a number in decimal format and converts it to a hex string prepended with "0x"', () => { + expect(util.toHex('4919')).toStrictEqual('0x1337'); + }); + + it('throws an error if given a string with decimals', () => { + expect(() => util.toHex('4919.3')).toThrow('Invalid character'); + }); + + it('converts a number to a hex string prepended with "0x"', () => { + expect(util.toHex(4919)).toStrictEqual('0x1337'); + }); + + it('throws an error if given a float', () => { + expect(() => util.toHex(4919.3)).toThrow('Invalid character'); + }); + + it('does nothing to a string that is already a "0x"-prepended hex value', () => { + expect(util.toHex('0x1337')).toStrictEqual('0x1337'); + }); + + it('throws an error if given a non-"0x"-prepended string that is not a valid hex value', () => { + expect(() => util.toHex('zzzz')).toThrow('Invalid character'); + }); + }); + + describe('gweiDecToWEIBN', () => { + it('should convert a whole number to WEI', () => { + expect(util.gweiDecToWEIBN(1).toNumber()).toBe(1000000000); + expect(util.gweiDecToWEIBN(123).toNumber()).toBe(123000000000); + expect(util.gweiDecToWEIBN(101).toNumber()).toBe(101000000000); + expect(util.gweiDecToWEIBN(1234).toNumber()).toBe(1234000000000); + expect(util.gweiDecToWEIBN(1000).toNumber()).toBe(1000000000000); + }); + + it('should convert a number with a decimal part to WEI', () => { + expect(util.gweiDecToWEIBN(1.1).toNumber()).toBe(1100000000); + expect(util.gweiDecToWEIBN(123.01).toNumber()).toBe(123010000000); + expect(util.gweiDecToWEIBN(101.001).toNumber()).toBe(101001000000); + expect(util.gweiDecToWEIBN(100.001).toNumber()).toBe(100001000000); + expect(util.gweiDecToWEIBN(1234.567).toNumber()).toBe(1234567000000); + }); + + it('should convert a number < 1 to WEI', () => { + expect(util.gweiDecToWEIBN(0.1).toNumber()).toBe(100000000); + expect(util.gweiDecToWEIBN(0.01).toNumber()).toBe(10000000); + expect(util.gweiDecToWEIBN(0.001).toNumber()).toBe(1000000); + expect(util.gweiDecToWEIBN(0.567).toNumber()).toBe(567000000); + }); + + it('should round to whole WEI numbers', () => { + expect(util.gweiDecToWEIBN(0.1001).toNumber()).toBe(100100000); + expect(util.gweiDecToWEIBN(0.0109).toNumber()).toBe(10900000); + expect(util.gweiDecToWEIBN(0.0014).toNumber()).toBe(1400000); + expect(util.gweiDecToWEIBN(0.5676).toNumber()).toBe(567600000); + }); + + it('should handle inputs with more than 9 decimal places', () => { + expect(util.gweiDecToWEIBN(1.0000000162).toNumber()).toBe(1000000016); + expect(util.gweiDecToWEIBN(1.0000000165).toNumber()).toBe(1000000017); + expect(util.gweiDecToWEIBN(1.0000000199).toNumber()).toBe(1000000020); + expect(util.gweiDecToWEIBN(1.9999999999).toNumber()).toBe(2000000000); + expect(util.gweiDecToWEIBN(1.0000005998).toNumber()).toBe(1000000600); + expect(util.gweiDecToWEIBN(123456.0000005998).toNumber()).toBe( + 123456000000600, + ); + expect(util.gweiDecToWEIBN(1.000000016025).toNumber()).toBe(1000000016); + expect(util.gweiDecToWEIBN(1.0000000160000028).toNumber()).toBe( + 1000000016, + ); + expect(util.gweiDecToWEIBN(1.000000016522).toNumber()).toBe(1000000017); + expect(util.gweiDecToWEIBN(1.000000016800022).toNumber()).toBe( + 1000000017, + ); + }); + + it('should work if there are extraneous trailing decimal zeroes', () => { + expect(util.gweiDecToWEIBN('0.5000').toNumber()).toBe(500000000); + expect(util.gweiDecToWEIBN('123.002300').toNumber()).toBe(123002300000); + expect(util.gweiDecToWEIBN('123.002300000000').toNumber()).toBe( + 123002300000, + ); + expect(util.gweiDecToWEIBN('0.00000200000').toNumber()).toBe(2000); + }); + + it('should work if there is no whole number specified', () => { + expect(util.gweiDecToWEIBN('.1').toNumber()).toBe(100000000); + expect(util.gweiDecToWEIBN('.01').toNumber()).toBe(10000000); + expect(util.gweiDecToWEIBN('.001').toNumber()).toBe(1000000); + expect(util.gweiDecToWEIBN('.567').toNumber()).toBe(567000000); + }); + + it('should handle NaN', () => { + expect(util.gweiDecToWEIBN(NaN).toNumber()).toBe(0); + }); + }); + + describe('weiHexToGweiDec', () => { + it('should convert a whole number to WEI', () => { + const testData = [ + { + input: '3b9aca00', + expectedResult: '1', + }, + { + input: '1ca35f0e00', + expectedResult: '123', + }, + { + input: '178411b200', + expectedResult: '101', + }, + { + input: '11f5021b400', + expectedResult: '1234', + }, + ]; + testData.forEach(({ input, expectedResult }) => { + expect(util.weiHexToGweiDec(input)).toBe(expectedResult); + }); + }); + + it('should convert a number with a decimal part to WEI', () => { + const testData = [ + { + input: '4190ab00', + expectedResult: '1.1', + }, + { + input: '1ca3f7a480', + expectedResult: '123.01', + }, + { + input: '178420f440', + expectedResult: '101.001', + }, + { + input: '11f71ed6fc0', + expectedResult: '1234.567', + }, + ]; + + testData.forEach(({ input, expectedResult }) => { + expect(util.weiHexToGweiDec(input)).toBe(expectedResult); + }); + }); + + it('should convert a number < 1 to WEI', () => { + const testData = [ + { + input: '5f5e100', + expectedResult: '0.1', + }, + { + input: '989680', + expectedResult: '0.01', + }, + { + input: 'f4240', + expectedResult: '0.001', + }, + { + input: '21cbbbc0', + expectedResult: '0.567', + }, + ]; + + testData.forEach(({ input, expectedResult }) => { + expect(util.weiHexToGweiDec(input)).toBe(expectedResult); + }); + }); + + it('should work with 0x prefixed values', () => { + expect(util.weiHexToGweiDec('0x5f48b0f7')).toBe('1.598599415'); + }); + }); + + describe('safelyExecute', () => { + it('should swallow errors', async () => { + expect( + await util.safelyExecute(() => { + throw new Error('ahh'); + }), + ).toBeUndefined(); + }); + }); + + describe('safelyExecuteWithTimeout', () => { + it('should swallow errors', async () => { + expect( + await util.safelyExecuteWithTimeout(() => { + throw new Error('ahh'); + }), + ).toBeUndefined(); + }); + + it('should resolve', async () => { + const response = await util.safelyExecuteWithTimeout(() => { + return new Promise((res) => setTimeout(() => res('response'), 200)); + }); + expect(response).toStrictEqual('response'); + }); + + it('should timeout', async () => { + expect( + await util.safelyExecuteWithTimeout(() => { + return new Promise((res) => setTimeout(res, 800)); + }), + ).toBeUndefined(); + }); + }); + + describe('toChecksumHexAddress', () => { + const fullAddress = `0x${VALID}`; + it('should return address for valid address', () => { + expect(util.toChecksumHexAddress(fullAddress)).toBe(fullAddress); + }); + + it('should return address for non prefix address', () => { + expect(util.toChecksumHexAddress(VALID)).toBe(fullAddress); + }); + }); + + describe('isValidHexAddress', () => { + it('should return true for valid address', () => { + expect(util.isValidHexAddress(VALID)).toBe(true); + }); + + it('should return false for invalid address', () => { + expect(util.isValidHexAddress('0x00')).toBe(false); + }); + + it('should allow allowNonPrefixed to be false', () => { + expect(util.isValidHexAddress('0x00', { allowNonPrefixed: false })).toBe( + false, + ); + }); + }); + + it('messageHexToString', () => { + const str = util.hexToText('68656c6c6f207468657265'); + expect(str).toStrictEqual('hello there'); + }); + + it('isSmartContractCode', () => { + const toSmartContract1 = util.isSmartContractCode(''); + const toSmartContract2 = util.isSmartContractCode('0x'); + const toSmartContract3 = util.isSmartContractCode('0x0'); + const toSmartContract4 = util.isSmartContractCode('0x01234'); + expect(toSmartContract1).toBe(false); + expect(toSmartContract2).toBe(false); + expect(toSmartContract3).toBe(false); + expect(toSmartContract4).toBe(true); + }); + + describe('successfulFetch', () => { + beforeEach(() => { + nock(SOME_API).get(/.+/u).reply(200, { foo: 'bar' }).persist(); + nock(SOME_FAILING_API).get(/.+/u).reply(500).persist(); + }); + + it('should return successful fetch response', async () => { + const res = await util.successfulFetch(SOME_API); + const parsed = await res.json(); + expect(parsed).toStrictEqual({ foo: 'bar' }); + }); + + it('should throw error for an unsuccessful fetch', async () => { + await expect(util.successfulFetch(SOME_FAILING_API)).rejects.toThrow( + `Fetch failed with status '500' for request '${SOME_FAILING_API}'`, + ); + }); + }); + + describe('timeoutFetch', () => { + beforeEach(() => { + nock(SOME_API).get(/.+/u).delay(300).reply(200, {}).persist(); + }); + + it('should fetch first if response is faster than timeout', async () => { + const res = await util.timeoutFetch(SOME_API); + const parsed = await res.json(); + expect(parsed).toStrictEqual({}); + }); + + it('should fail fetch with timeout', async () => { + await expect(util.timeoutFetch(SOME_API, {}, 100)).rejects.toThrow( + 'timeout', + ); + }); + }); + + describe('normalizeEnsName', () => { + it('should normalize with valid 2LD', async () => { + let valid = util.normalizeEnsName('metamask.eth'); + expect(valid).toStrictEqual('metamask.eth'); + valid = util.normalizeEnsName('foobar1.eth'); + expect(valid).toStrictEqual('foobar1.eth'); + valid = util.normalizeEnsName('foo-bar.eth'); + expect(valid).toStrictEqual('foo-bar.eth'); + valid = util.normalizeEnsName('1-foo-bar.eth'); + expect(valid).toStrictEqual('1-foo-bar.eth'); + }); + + it('should normalize with valid 2LD and "test" TLD', async () => { + const valid = util.normalizeEnsName('metamask.test'); + expect(valid).toStrictEqual('metamask.test'); + }); + + it('should normalize with valid 2LD and 3LD', async () => { + let valid = util.normalizeEnsName('a.metamask.eth'); + expect(valid).toStrictEqual('a.metamask.eth'); + valid = util.normalizeEnsName('aa.metamask.eth'); + expect(valid).toStrictEqual('aa.metamask.eth'); + valid = util.normalizeEnsName('a-a.metamask.eth'); + expect(valid).toStrictEqual('a-a.metamask.eth'); + valid = util.normalizeEnsName('1-a.metamask.eth'); + expect(valid).toStrictEqual('1-a.metamask.eth'); + valid = util.normalizeEnsName('1-2.metamask.eth'); + expect(valid).toStrictEqual('1-2.metamask.eth'); + }); + + it('should return null with invalid 2LD', async () => { + let invalid = util.normalizeEnsName('me.eth'); + expect(invalid).toBeNull(); + invalid = util.normalizeEnsName('metamask-.eth'); + expect(invalid).toBeNull(); + invalid = util.normalizeEnsName('-metamask.eth'); + expect(invalid).toBeNull(); + invalid = util.normalizeEnsName('@metamask.eth'); + expect(invalid).toBeNull(); + invalid = util.normalizeEnsName('foobar.eth'); + expect(invalid).toBeNull(); + }); + + it('should return null with valid 2LD and invalid 3LD', async () => { + let invalid = util.normalizeEnsName('-.metamask.eth'); + expect(invalid).toBeNull(); + invalid = util.normalizeEnsName('abc-.metamask.eth'); + expect(invalid).toBeNull(); + invalid = util.normalizeEnsName('-abc.metamask.eth'); + expect(invalid).toBeNull(); + invalid = util.normalizeEnsName('.metamask.eth'); + expect(invalid).toBeNull(); + invalid = util.normalizeEnsName('f@o.metamask.eth'); + expect(invalid).toBeNull(); + }); + + it('should return null with invalid 2LD and valid 3LD', async () => { + const invalid = util.normalizeEnsName('foo.barbaz.eth'); + expect(invalid).toBeNull(); + }); + + it('should return null with invalid TLD', async () => { + const invalid = util.normalizeEnsName('a.metamask.com'); + expect(invalid).toBeNull(); + }); + + it('should return null with repeated periods', async () => { + let invalid = util.normalizeEnsName('foo..metamask.eth'); + expect(invalid).toBeNull(); + invalid = util.normalizeEnsName('foo.metamask..eth'); + expect(invalid).toBeNull(); + }); + + it('should return null with empty string', async () => { + const invalid = util.normalizeEnsName(''); + expect(invalid).toBeNull(); + }); + }); + + describe('query', () => { + describe('when the given method exists directly on the EthQuery', () => { + it('should call the method on the EthQuery and, if it is successful, return a promise that resolves to the result', async () => { + const ethQuery = { + getBlockByHash: (blockId: any, cb: any) => cb(null, { id: blockId }), + }; + const result = await util.query(ethQuery, 'getBlockByHash', ['0x1234']); + expect(result).toStrictEqual({ id: '0x1234' }); + }); + + it('should call the method on the EthQuery and, if it errors, return a promise that is rejected with the error', async () => { + const ethQuery = { + getBlockByHash: (_blockId: any, cb: any) => + cb(new Error('uh oh'), null), + }; + await expect( + util.query(ethQuery, 'getBlockByHash', ['0x1234']), + ).rejects.toThrow('uh oh'); + }); + }); + + describe('when the given method does not exist directly on the EthQuery', () => { + it('should use sendAsync to call the RPC endpoint and, if it is successful, return a promise that resolves to the result', async () => { + const ethQuery = { + sendAsync: ({ method, params }: any, cb: any) => { + if (method === 'eth_getBlockByHash') { + return cb(null, { id: params[0] }); + } + throw new Error(`Unsupported method ${method}`); + }, + }; + const result = await util.query(ethQuery, 'eth_getBlockByHash', [ + '0x1234', + ]); + expect(result).toStrictEqual({ id: '0x1234' }); + }); + + it('should use sendAsync to call the RPC endpoint and, if it errors, return a promise that is rejected with the error', async () => { + const ethQuery = { + sendAsync: (_args: any, cb: any) => { + cb(new Error('uh oh'), null); + }, + }; + await expect( + util.query(ethQuery, 'eth_getBlockByHash', ['0x1234']), + ).rejects.toThrow('uh oh'); + }); + }); + }); + + describe('convertHexToDecimal', () => { + it('should convert hex price to decimal', () => { + expect(util.convertHexToDecimal('0x50fd51da')).toStrictEqual(1358778842); + }); + + it('should return zero when undefined', () => { + expect(util.convertHexToDecimal(undefined)).toStrictEqual(0); + }); + + it('should return a decimal string as the same decimal number', () => { + expect(util.convertHexToDecimal('1611')).toStrictEqual(1611); + }); + + it('should return 0 when passed an invalid hex string', () => { + expect(util.convertHexToDecimal('0x12398u12')).toStrictEqual(0); + }); + }); + + describe('isPlainObject', () => { + it('returns false for null values', () => { + expect(util.isPlainObject(null)).toBe(false); + expect(util.isPlainObject(undefined)).toBe(false); + }); + + it('returns false for non objects', () => { + expect(util.isPlainObject(5)).toBe(false); + expect(util.isPlainObject('foo')).toBe(false); + }); + + it('returns false for arrays', () => { + expect(util.isPlainObject(['foo'])).toBe(false); + expect(util.isPlainObject([{}])).toBe(false); + }); + + it('returns true for objects', () => { + expect(util.isPlainObject({ foo: 'bar' })).toBe(true); + expect(util.isPlainObject({ foo: 'bar', test: { num: 5 } })).toBe(true); + }); + }); + + describe('hasProperty', () => { + it('returns false for non existing properties', () => { + expect(util.hasProperty({ foo: 'bar' }, 'property')).toBe(false); + }); + + it('returns true for existing properties', () => { + expect(util.hasProperty({ foo: 'bar' }, 'foo')).toBe(true); + }); + }); + + describe('isNonEmptyArray', () => { + it('returns false non arrays', () => { + // @ts-expect-error Invalid type for testing purposes + expect(util.isNonEmptyArray(null)).toBe(false); + // @ts-expect-error Invalid type for testing purposes + expect(util.isNonEmptyArray(undefined)).toBe(false); + }); + + it('returns false for empty array', () => { + expect(util.isNonEmptyArray([])).toBe(false); + }); + + it('returns true arrays with at least one item', () => { + expect(util.isNonEmptyArray([1])).toBe(true); + expect(util.isNonEmptyArray([1, 2, 3, 4])).toBe(true); + }); + }); + + describe('isValidJson', () => { + it('returns false for class instances', () => { + expect(util.isValidJson(new Map())).toBe(false); + }); + + it('returns true for valid JSON', () => { + expect(util.isValidJson({ foo: 'bar', test: { num: 5 } })).toBe(true); + }); + }); +}); diff --git a/packages/controller-utils/src/util.ts b/packages/controller-utils/src/util.ts new file mode 100644 index 0000000000..e47ed77d03 --- /dev/null +++ b/packages/controller-utils/src/util.ts @@ -0,0 +1,528 @@ +import { + addHexPrefix, + isValidAddress, + isHexString, + BN, + toChecksumAddress, + stripHexPrefix, +} from 'ethereumjs-util'; +import { fromWei, toWei } from 'ethjs-unit'; +import ensNamehash from 'eth-ens-namehash'; +import deepEqual from 'fast-deep-equal'; +import { Json } from '@metamask/base-controller'; + +const TIMEOUT_ERROR = new Error('timeout'); + +/** + * Converts a BN object to a hex string with a '0x' prefix. + * + * @param inputBn - BN instance to convert to a hex string. + * @returns A '0x'-prefixed hex string. + */ +export function BNToHex(inputBn: any) { + return addHexPrefix(inputBn.toString(16)); +} + +/** + * Used to multiply a BN by a fraction. + * + * @param targetBN - Number to multiply by a fraction. + * @param numerator - Numerator of the fraction multiplier. + * @param denominator - Denominator of the fraction multiplier. + * @returns Product of the multiplication. + */ +export function fractionBN( + targetBN: any, + numerator: number | string, + denominator: number | string, +) { + const numBN = new BN(numerator); + const denomBN = new BN(denominator); + return targetBN.mul(numBN).div(denomBN); +} + +/** + * Used to convert a base-10 number from GWEI to WEI. Can handle numbers with decimal parts. + * + * @param n - The base 10 number to convert to WEI. + * @returns The number in WEI, as a BN. + */ +export function gweiDecToWEIBN(n: number | string) { + if (Number.isNaN(n)) { + return new BN(0); + } + + const parts = n.toString().split('.'); + const wholePart = parts[0] || '0'; + let decimalPart = parts[1] || ''; + + if (!decimalPart) { + return toWei(wholePart, 'gwei'); + } + + if (decimalPart.length <= 9) { + return toWei(`${wholePart}.${decimalPart}`, 'gwei'); + } + + const decimalPartToRemove = decimalPart.slice(9); + const decimalRoundingDigit = decimalPartToRemove[0]; + + decimalPart = decimalPart.slice(0, 9); + let wei = toWei(`${wholePart}.${decimalPart}`, 'gwei'); + + if (Number(decimalRoundingDigit) >= 5) { + wei = wei.add(new BN(1)); + } + + return wei; +} + +/** + * Used to convert values from wei hex format to dec gwei format. + * + * @param hex - The value in hex wei. + * @returns The value in dec gwei as string. + */ +export function weiHexToGweiDec(hex: string) { + const hexWei = new BN(stripHexPrefix(hex), 16); + return fromWei(hexWei, 'gwei').toString(10); +} + +/** + * Return a URL that can be used to obtain ETH for a given network. + * + * @param networkCode - Network code of desired network. + * @param address - Address to deposit obtained ETH. + * @param amount - How much ETH is desired. + * @returns URL to buy ETH based on network. + */ +export function getBuyURL( + networkCode = '1', + address?: string, + amount = 5, +): string | undefined { + switch (networkCode) { + case '1': + return `https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=${amount}&address=${address}&crypto_currency=ETH`; + case '3': + return 'https://faucet.metamask.io/'; + case '4': + return 'https://www.rinkeby.io/'; + case '5': + return 'https://goerli-faucet.slock.it/'; + case '42': + return 'https://github.com/kovan-testnet/faucet'; + default: + return undefined; + } +} + +/** + * Converts a hex string to a BN object. + * + * @param inputHex - Number represented as a hex string. + * @returns A BN instance. + */ +export function hexToBN(inputHex: string) { + return new BN(stripHexPrefix(inputHex), 16); +} + +/** + * A helper function that converts hex data to human readable string. + * + * @param hex - The hex string to convert to string. + * @returns A human readable string conversion. + */ +export function hexToText(hex: string) { + try { + const stripped = stripHexPrefix(hex); + const buff = Buffer.from(stripped, 'hex'); + return buff.toString('utf8'); + } catch (e) { + /* istanbul ignore next */ + return hex; + } +} + +/** + * Parses a hex string and converts it into a number that can be operated on in a bignum-safe, + * base-10 way. + * + * @param value - A base-16 number encoded as a string. + * @returns The number as a BN object in base-16 mode. + */ +export function fromHex(value: string | BN): BN { + if (BN.isBN(value)) { + return value; + } + return new BN(hexToBN(value).toString(10)); +} + +/** + * Converts an integer to a hexadecimal representation. + * + * @param value - An integer, an integer encoded as a base-10 string, or a BN. + * @returns The integer encoded as a hex string. + */ +export function toHex(value: number | string | BN): string { + if (typeof value === 'string' && isHexString(value)) { + return value; + } + const hexString = BN.isBN(value) + ? value.toString(16) + : new BN(value.toString(), 10).toString(16); + return `0x${hexString}`; +} + +/** + * Execute and return an asynchronous operation without throwing errors. + * + * @param operation - Function returning a Promise. + * @param logError - Determines if the error should be logged. + * @returns Promise resolving to the result of the async operation. + */ +export async function safelyExecute( + operation: () => Promise, + logError = false, +) { + try { + return await operation(); + } catch (error: any) { + /* istanbul ignore next */ + if (logError) { + console.error(error); + } + return undefined; + } +} + +/** + * Execute and return an asynchronous operation with a timeout. + * + * @param operation - Function returning a Promise. + * @param logError - Determines if the error should be logged. + * @param timeout - Timeout to fail the operation. + * @returns Promise resolving to the result of the async operation. + */ +export async function safelyExecuteWithTimeout( + operation: () => Promise, + logError = false, + timeout = 500, +) { + try { + return await Promise.race([ + operation(), + new Promise((_, reject) => + setTimeout(() => { + reject(TIMEOUT_ERROR); + }, timeout), + ), + ]); + } catch (error) { + /* istanbul ignore next */ + if (logError) { + console.error(error); + } + return undefined; + } +} + +/** + * Convert an address to a checksummed hexidecimal address. + * + * @param address - The address to convert. + * @returns A 0x-prefixed hexidecimal checksummed address. + */ +export function toChecksumHexAddress(address: string) { + const hexPrefixed = addHexPrefix(address); + if (!isHexString(hexPrefixed)) { + // Version 5.1 of ethereumjs-utils would have returned '0xY' for input 'y' + // but we shouldn't waste effort trying to change case on a clearly invalid + // string. Instead just return the hex prefixed original string which most + // closely mimics the original behavior. + return hexPrefixed; + } + return toChecksumAddress(hexPrefixed); +} + +/** + * Validates that the input is a hex address. This utility method is a thin + * wrapper around ethereumjs-util.isValidAddress, with the exception that it + * does not throw an error when provided values that are not hex strings. In + * addition, and by default, this method will return true for hex strings that + * meet the length requirement of a hex address, but are not prefixed with `0x` + * Finally, if the mixedCaseUseChecksum flag is true and a mixed case string is + * provided this method will validate it has the proper checksum formatting. + * + * @param possibleAddress - Input parameter to check against. + * @param options - The validation options. + * @param options.allowNonPrefixed - If true will first ensure '0x' is prepended to the string. + * @returns Whether or not the input is a valid hex address. + */ +export function isValidHexAddress( + possibleAddress: string, + { allowNonPrefixed = true } = {}, +) { + const addressToCheck = allowNonPrefixed + ? addHexPrefix(possibleAddress) + : possibleAddress; + if (!isHexString(addressToCheck)) { + return false; + } + + return isValidAddress(addressToCheck); +} + +/** + * Returns whether the given code corresponds to a smart contract. + * + * @param code - The potential smart contract code. + * @returns Whether the code was smart contract code or not. + */ +export function isSmartContractCode(code: string) { + /* istanbul ignore if */ + if (!code) { + return false; + } + // Geth will return '0x', and ganache-core v2.2.1 will return '0x0' + const smartContractCode = code !== '0x' && code !== '0x0'; + return smartContractCode; +} + +/** + * Execute fetch and verify that the response was successful. + * + * @param request - Request information. + * @param options - Fetch options. + * @returns The fetch response. + */ +export async function successfulFetch(request: string, options?: RequestInit) { + const response = await fetch(request, options); + if (!response.ok) { + throw new Error( + `Fetch failed with status '${response.status}' for request '${request}'`, + ); + } + return response; +} + +/** + * Execute fetch and return object response. + * + * @param request - The request information. + * @param options - The fetch options. + * @returns The fetch response JSON data. + */ +export async function handleFetch(request: string, options?: RequestInit) { + const response = await successfulFetch(request, options); + const object = await response.json(); + return object; +} + +/** + * Execute fetch and return object response, log if known error thrown, otherwise rethrow error. + * + * @param request - the request options object + * @param request.url - The request url to query. + * @param request.options - The fetch options. + * @param request.timeout - Timeout to fail request + * @param request.errorCodesToCatch - array of error codes for errors we want to catch in a particular context + * @returns The fetch response JSON data or undefined (if error occurs). + */ +export async function fetchWithErrorHandling({ + url, + options, + timeout, + errorCodesToCatch, +}: { + url: string; + options?: RequestInit; + timeout?: number; + errorCodesToCatch?: number[]; +}) { + let result; + try { + if (timeout) { + result = Promise.race([ + await handleFetch(url, options), + new Promise((_, reject) => + setTimeout(() => { + reject(TIMEOUT_ERROR); + }, timeout), + ), + ]); + } else { + result = await handleFetch(url, options); + } + } catch (e) { + logOrRethrowError(e, errorCodesToCatch); + } + return result; +} + +/** + * Fetch that fails after timeout. + * + * @param url - Url to fetch. + * @param options - Options to send with the request. + * @param timeout - Timeout to fail request. + * @returns Promise resolving the request. + */ +export async function timeoutFetch( + url: string, + options?: RequestInit, + timeout = 500, +): Promise { + return Promise.race([ + successfulFetch(url, options), + new Promise((_, reject) => + setTimeout(() => { + reject(TIMEOUT_ERROR); + }, timeout), + ), + ]); +} + +/** + * Normalizes the given ENS name. + * + * @param ensName - The ENS name. + * @returns The normalized ENS name string. + */ +export function normalizeEnsName(ensName: string): string | null { + if (ensName && typeof ensName === 'string') { + try { + const normalized = ensNamehash.normalize(ensName.trim()); + // this regex is only sufficient with the above call to ensNamehash.normalize + // TODO: change 7 in regex to 3 when shorter ENS domains are live + if (normalized.match(/^(([\w\d-]+)\.)*[\w\d-]{7,}\.(eth|test)$/u)) { + return normalized; + } + } catch (_) { + // do nothing + } + } + return null; +} + +/** + * Wrapper method to handle EthQuery requests. + * + * @param ethQuery - EthQuery object initialized with a provider. + * @param method - Method to request. + * @param args - Arguments to send. + * @returns Promise resolving the request. + */ +export function query( + ethQuery: any, + method: string, + args: any[] = [], +): Promise { + return new Promise((resolve, reject) => { + const cb = (error: Error, result: any) => { + if (error) { + reject(error); + return; + } + resolve(result); + }; + + if (typeof ethQuery[method] === 'function') { + ethQuery[method](...args, cb); + } else { + ethQuery.sendAsync({ method, params: args }, cb); + } + }); +} + +/** + * Converts valid hex strings to decimal numbers, and handles unexpected arg types. + * + * @param value - a string that is either a hexadecimal with `0x` prefix or a decimal string. + * @returns a decimal number. + */ +export const convertHexToDecimal = ( + value: string | undefined = '0x0', +): number => { + if (isHexString(value)) { + return parseInt(value, 16); + } + + return Number(value) ? Number(value) : 0; +}; + +type PlainObject = Record; + +/** + * Determines whether a value is a "plain" object. + * + * @param value - A value to check + * @returns True if the passed value is a plain object + */ +export function isPlainObject(value: unknown): value is PlainObject { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +export const hasProperty = ( + object: PlainObject, + key: string | number | symbol, +) => Reflect.hasOwnProperty.call(object, key); + +/** + * Like {@link Array}, but always non-empty. + * + * @template T - The non-empty array member type. + */ +export type NonEmptyArray = [T, ...T[]]; + +/** + * Type guard for {@link NonEmptyArray}. + * + * @template T - The non-empty array member type. + * @param value - The value to check. + * @returns Whether the value is a non-empty array. + */ +export function isNonEmptyArray(value: T[]): value is NonEmptyArray { + return Array.isArray(value) && value.length > 0; +} + +/** + * Type guard for {@link Json}. + * + * @param value - The value to check. + * @returns Whether the value is valid JSON. + */ +export function isValidJson(value: unknown): value is Json { + try { + return deepEqual(value, JSON.parse(JSON.stringify(value))); + } catch (_) { + return false; + } +} + +/** + * Utility method to log if error is a common fetch error and otherwise rethrow it. + * + * @param error - Caught error that we should either rethrow or log to console + * @param codesToCatch - array of error codes for errors we want to catch and log in a particular context + */ +function logOrRethrowError(error: any, codesToCatch: number[] = []) { + if (!error) { + return; + } + + const includesErrorCodeToCatch = codesToCatch.some((code) => + error.message?.includes(`Fetch failed with status '${code}'`), + ); + + if ( + error instanceof Error && + (includesErrorCodeToCatch || + error.message?.includes('Failed to fetch') || + error === TIMEOUT_ERROR) + ) { + console.error(error); + } else { + throw error; + } +} diff --git a/packages/controller-utils/tsconfig.build.json b/packages/controller-utils/tsconfig.build.json new file mode 100644 index 0000000000..1767c84df2 --- /dev/null +++ b/packages/controller-utils/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "lib": ["ES2017", "DOM"], + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { + "path": "../base-controller/tsconfig.build.json" + } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/controller-utils/tsconfig.json b/packages/controller-utils/tsconfig.json new file mode 100644 index 0000000000..bb8db8272e --- /dev/null +++ b/packages/controller-utils/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./", + "lib": ["ES2017", "DOM"] + }, + "references": [ + { + "path": "../base-controller" + } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/controller-utils/typedoc.json b/packages/controller-utils/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/controller-utils/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/ens-controller/CHANGELOG.md b/packages/ens-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/ens-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/ens-controller/LICENSE b/packages/ens-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/ens-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/ens-controller/README.md b/packages/ens-controller/README.md new file mode 100644 index 0000000000..68a5b8ed3e --- /dev/null +++ b/packages/ens-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/ens-controller` + +Maps ENS names to their resolved addresses by chain id. + +## Installation + +`yarn add @metamask/ens-controller` + +or + +`npm install @metamask/ens-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/ens-controller/jest.config.js b/packages/ens-controller/jest.config.js new file mode 100644 index 0000000000..a7ae2b4b47 --- /dev/null +++ b/packages/ens-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/ens-controller/package.json b/packages/ens-controller/package.json new file mode 100644 index 0000000000..20731fb348 --- /dev/null +++ b/packages/ens-controller/package.json @@ -0,0 +1,50 @@ +{ + "name": "@metamask/ens-controller", + "version": "0.0.0", + "description": "Maps ENS names to their resolved addresses by chain id", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/ens-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/ens-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~", + "@metamask/controller-utils": "workspace:~" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/third-party/EnsController.test.ts b/packages/ens-controller/src/EnsController.test.ts similarity index 99% rename from src/third-party/EnsController.test.ts rename to packages/ens-controller/src/EnsController.test.ts index 980587232f..db1fbdaa51 100644 --- a/src/third-party/EnsController.test.ts +++ b/packages/ens-controller/src/EnsController.test.ts @@ -1,4 +1,4 @@ -import { toChecksumHexAddress } from '../util'; +import { toChecksumHexAddress } from '@metamask/controller-utils'; import { EnsController } from './EnsController'; const address1 = '0x32Be343B94f860124dC4fEe278FDCBD38C102D88'; diff --git a/src/third-party/EnsController.ts b/packages/ens-controller/src/EnsController.ts similarity index 97% rename from src/third-party/EnsController.ts rename to packages/ens-controller/src/EnsController.ts index 9a8eace70f..be98d5751f 100644 --- a/src/third-party/EnsController.ts +++ b/packages/ens-controller/src/EnsController.ts @@ -1,9 +1,13 @@ -import { BaseController, BaseConfig, BaseState } from '../BaseController'; +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; import { normalizeEnsName, isValidHexAddress, toChecksumHexAddress, -} from '../util'; +} from '@metamask/controller-utils'; /** * @type EnsEntry diff --git a/packages/ens-controller/src/index.ts b/packages/ens-controller/src/index.ts new file mode 100644 index 0000000000..14cbf704a6 --- /dev/null +++ b/packages/ens-controller/src/index.ts @@ -0,0 +1 @@ +export * from './EnsController'; diff --git a/packages/ens-controller/tsconfig.build.json b/packages/ens-controller/tsconfig.build.json new file mode 100644 index 0000000000..bbfe057a20 --- /dev/null +++ b/packages/ens-controller/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/ens-controller/tsconfig.json b/packages/ens-controller/tsconfig.json new file mode 100644 index 0000000000..7ee9852347 --- /dev/null +++ b/packages/ens-controller/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/ens-controller/typedoc.json b/packages/ens-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/ens-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/gas-fee-controller/CHANGELOG.md b/packages/gas-fee-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/gas-fee-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/gas-fee-controller/LICENSE b/packages/gas-fee-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/gas-fee-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/gas-fee-controller/README.md b/packages/gas-fee-controller/README.md new file mode 100644 index 0000000000..45b45d71ec --- /dev/null +++ b/packages/gas-fee-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/gas-fee-controller` + +Periodically calculates gas fee estimates based on various gas limits as well as other data displayed on transaction confirm screens. + +## Installation + +`yarn add @metamask/gas-fee-controller` + +or + +`npm install @metamask/gas-fee-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/gas-fee-controller/jest.config.js b/packages/gas-fee-controller/jest.config.js new file mode 100644 index 0000000000..93b3a1692f --- /dev/null +++ b/packages/gas-fee-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 65.31, + functions: 76.59, + lines: 75.83, + statements: 75.91, + }, + }, +}); diff --git a/packages/gas-fee-controller/package.json b/packages/gas-fee-controller/package.json new file mode 100644 index 0000000000..fdc3839819 --- /dev/null +++ b/packages/gas-fee-controller/package.json @@ -0,0 +1,62 @@ +{ + "name": "@metamask/gas-fee-controller", + "version": "0.0.0", + "description": "Periodically calculates gas fee estimates based on various gas limits as well as other data displayed on transaction confirm screens", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/gas-fee-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/gas-fee-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~", + "@metamask/controller-utils": "workspace:~", + "@metamask/network-controller": "workspace:~", + "@types/uuid": "^8.3.0", + "babel-runtime": "^6.26.0", + "eth-query": "^2.1.2", + "ethereumjs-util": "^7.0.10", + "ethjs-unit": "^0.1.6", + "immer": "^9.0.6", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "@types/jest-when": "^2.7.3", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "jest-when": "^3.4.2", + "nock": "^13.0.7", + "sinon": "^9.2.4", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/gas/GasFeeController.test.ts b/packages/gas-fee-controller/src/GasFeeController.test.ts similarity index 98% rename from src/gas/GasFeeController.test.ts rename to packages/gas-fee-controller/src/GasFeeController.test.ts index 78df654d2a..1e0b178d6d 100644 --- a/src/gas/GasFeeController.test.ts +++ b/packages/gas-fee-controller/src/GasFeeController.test.ts @@ -1,12 +1,11 @@ -import sinon, { SinonFakeTimers } from 'sinon'; -import { mocked } from 'ts-jest/utils'; -import { ControllerMessenger } from '../ControllerMessenger'; +import * as sinon from 'sinon'; +import { ControllerMessenger } from '@metamask/base-controller'; import { NetworkController, NetworkControllerGetEthQueryAction, NetworkControllerGetProviderConfigAction, NetworkControllerProviderChangeEvent, -} from '../network/NetworkController'; +} from '@metamask/network-controller'; import { GAS_ESTIMATE_TYPES, GasFeeController, @@ -21,10 +20,11 @@ import determineGasFeeCalculations from './determineGasFeeCalculations'; jest.mock('./determineGasFeeCalculations'); -const mockedDetermineGasFeeCalculations = mocked( - determineGasFeeCalculations, - true, -); +const mockedDetermineGasFeeCalculations = + determineGasFeeCalculations as jest.Mock< + ReturnType, + Parameters + >; const name = 'GasFeeController'; @@ -174,7 +174,7 @@ function buildMockGasFeeStateEthGasPrice({ } describe('GasFeeController', () => { - let clock: SinonFakeTimers; + let clock: sinon.SinonFakeTimers; let gasFeeController: GasFeeController; /** diff --git a/src/gas/GasFeeController.ts b/packages/gas-fee-controller/src/GasFeeController.ts similarity index 98% rename from src/gas/GasFeeController.ts rename to packages/gas-fee-controller/src/GasFeeController.ts index 4c890bffb1..e1cd5b5186 100644 --- a/src/gas/GasFeeController.ts +++ b/packages/gas-fee-controller/src/GasFeeController.ts @@ -1,18 +1,19 @@ import type { Patch } from 'immer'; - import EthQuery from 'eth-query'; import { v1 as random } from 'uuid'; import { isHexString } from 'ethereumjs-util'; -import { BaseController } from '../BaseControllerV2'; -import { safelyExecute } from '../util'; -import type { RestrictedControllerMessenger } from '../ControllerMessenger'; +import { + BaseControllerV2, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { safelyExecute } from '@metamask/controller-utils'; import type { NetworkControllerGetEthQueryAction, NetworkControllerGetProviderConfigAction, NetworkControllerProviderChangeEvent, NetworkController, NetworkState, -} from '../network/NetworkController'; +} from '@metamask/network-controller'; import { fetchGasEstimates, fetchLegacyGasPriceEstimates, @@ -232,12 +233,12 @@ export type ChainID = `0x${string}` | `${number}` | number; /** * Controller that retrieves gas fee estimate data and polls for updated data on a set interval */ -export class GasFeeController extends BaseController< +export class GasFeeController extends BaseControllerV2< typeof name, GasFeeState, GasFeeMessenger > { - private intervalId?: NodeJS.Timeout; + private intervalId?: ReturnType; private intervalDelay; diff --git a/src/gas/determineGasFeeCalculations.test.ts b/packages/gas-fee-controller/src/determineGasFeeCalculations.test.ts similarity index 93% rename from src/gas/determineGasFeeCalculations.test.ts rename to packages/gas-fee-controller/src/determineGasFeeCalculations.test.ts index 4ca928dbbd..99858e448a 100644 --- a/src/gas/determineGasFeeCalculations.test.ts +++ b/packages/gas-fee-controller/src/determineGasFeeCalculations.test.ts @@ -1,4 +1,3 @@ -import { mocked } from 'ts-jest/utils'; import determineGasFeeCalculations from './determineGasFeeCalculations'; import { unknownString, @@ -18,17 +17,28 @@ import fetchGasEstimatesViaEthFeeHistory from './fetchGasEstimatesViaEthFeeHisto jest.mock('./gas-util'); jest.mock('./fetchGasEstimatesViaEthFeeHistory'); -const mockedFetchGasEstimates = mocked(fetchGasEstimates, true); -const mockedFetchLegacyGasPriceEstimates = mocked( - fetchLegacyGasPriceEstimates, - true, -); -const mockedFetchEthGasPriceEstimate = mocked(fetchEthGasPriceEstimate, true); -const mockedCalculateTimeEstimate = mocked(calculateTimeEstimate, true); -const mockedFetchGasEstimatesViaEthFeeHistory = mocked( - fetchGasEstimatesViaEthFeeHistory, - true, -); +const mockedFetchGasEstimates = fetchGasEstimates as jest.Mock< + ReturnType, + Parameters +>; +const mockedFetchLegacyGasPriceEstimates = + fetchLegacyGasPriceEstimates as jest.Mock< + ReturnType, + Parameters + >; +const mockedFetchEthGasPriceEstimate = fetchEthGasPriceEstimate as jest.Mock< + ReturnType, + Parameters +>; +const mockedCalculateTimeEstimate = calculateTimeEstimate as jest.Mock< + ReturnType, + Parameters +>; +const mockedFetchGasEstimatesViaEthFeeHistory = + fetchGasEstimatesViaEthFeeHistory as jest.Mock< + ReturnType, + Parameters + >; /** * Builds mock data for the `fetchGasEstimates` function. All of the data here is filled in to make diff --git a/src/gas/determineGasFeeCalculations.ts b/packages/gas-fee-controller/src/determineGasFeeCalculations.ts similarity index 100% rename from src/gas/determineGasFeeCalculations.ts rename to packages/gas-fee-controller/src/determineGasFeeCalculations.ts diff --git a/src/gas/fetchBlockFeeHistory.test.ts b/packages/gas-fee-controller/src/fetchBlockFeeHistory.test.ts similarity index 87% rename from src/gas/fetchBlockFeeHistory.test.ts rename to packages/gas-fee-controller/src/fetchBlockFeeHistory.test.ts index 162ea61bb3..5fb8da5083 100644 --- a/src/gas/fetchBlockFeeHistory.test.ts +++ b/packages/gas-fee-controller/src/fetchBlockFeeHistory.test.ts @@ -1,18 +1,20 @@ import { BN } from 'ethereumjs-util'; -import { mocked } from 'ts-jest/utils'; import { when } from 'jest-when'; -import { query, fromHex, toHex } from '../util'; +import { query, fromHex, toHex } from '@metamask/controller-utils'; import fetchBlockFeeHistory from './fetchBlockFeeHistory'; -jest.mock('../util', () => { +jest.mock('@metamask/controller-utils', () => { return { - ...jest.requireActual('../util'), + ...jest.requireActual('@metamask/controller-utils'), __esModule: true, query: jest.fn(), }; }); -const mockedQuery = mocked(query, true); +const mockedQuery = query as jest.Mock< + ReturnType, + Parameters +>; /** * Calls the given function the given number of times, collecting the results from each call. @@ -71,20 +73,20 @@ describe('fetchBlockFeeHistory', () => { expect(feeHistory).toStrictEqual([ { - number: new BN(1), - baseFeePerGas: new BN(10_000_000_000), + number: fromHex(toHex(1)), + baseFeePerGas: fromHex(toHex(10_000_000_000)), gasUsedRatio: 0.1, priorityFeesByPercentile: {}, }, { - number: new BN(2), - baseFeePerGas: new BN(20_000_000_000), + number: fromHex(toHex(2)), + baseFeePerGas: fromHex(toHex(20_000_000_000)), gasUsedRatio: 0.2, priorityFeesByPercentile: {}, }, { - number: new BN(3), - baseFeePerGas: new BN(30_000_000_000), + number: fromHex(toHex(3)), + baseFeePerGas: fromHex(toHex(30_000_000_000)), gasUsedRatio: 0.3, priorityFeesByPercentile: {}, }, @@ -183,7 +185,7 @@ describe('fetchBlockFeeHistory', () => { expect(feeHistory).toStrictEqual( expectedBlocks.map((block) => { return { - number: new BN(block.number), + number: fromHex(toHex(block.number)), baseFeePerGas: fromHex(block.baseFeePerGas), gasUsedRatio: block.gasUsedRatio, priorityFeesByPercentile: {}, @@ -273,33 +275,33 @@ describe('fetchBlockFeeHistory', () => { expect(feeHistory).toStrictEqual([ { - number: new BN(1), - baseFeePerGas: new BN(100_000_000_000), + number: fromHex(toHex(1)), + baseFeePerGas: fromHex(toHex(100_000_000_000)), gasUsedRatio: 0.1, priorityFeesByPercentile: { - 10: new BN(10_000_000_000), - 20: new BN(15_000_000_000), - 30: new BN(20_000_000_000), + 10: fromHex(toHex(10_000_000_000)), + 20: fromHex(toHex(15_000_000_000)), + 30: fromHex(toHex(20_000_000_000)), }, }, { - number: new BN(2), - baseFeePerGas: new BN(200_000_000_000), + number: fromHex(toHex(2)), + baseFeePerGas: fromHex(toHex(200_000_000_000)), gasUsedRatio: 0.2, priorityFeesByPercentile: { - 10: new BN(0), - 20: new BN(10_000_000_000), - 30: new BN(15_000_000_000), + 10: fromHex(toHex(0)), + 20: fromHex(toHex(10_000_000_000)), + 30: fromHex(toHex(15_000_000_000)), }, }, { - number: new BN(3), - baseFeePerGas: new BN(300_000_000_000), + number: fromHex(toHex(3)), + baseFeePerGas: fromHex(toHex(300_000_000_000)), gasUsedRatio: 0.3, priorityFeesByPercentile: { - 10: new BN(20_000_000_000), - 20: new BN(20_000_000_000), - 30: new BN(30_000_000_000), + 10: fromHex(toHex(20_000_000_000)), + 20: fromHex(toHex(20_000_000_000)), + 30: fromHex(toHex(30_000_000_000)), }, }, ]); @@ -363,26 +365,26 @@ describe('fetchBlockFeeHistory', () => { expect(feeHistory).toStrictEqual([ { - number: new BN(1), - baseFeePerGas: new BN(10_000_000_000), + number: fromHex(toHex(1)), + baseFeePerGas: fromHex(toHex(10_000_000_000)), gasUsedRatio: 0.1, priorityFeesByPercentile: {}, }, { - number: new BN(2), - baseFeePerGas: new BN(20_000_000_000), + number: fromHex(toHex(2)), + baseFeePerGas: fromHex(toHex(20_000_000_000)), gasUsedRatio: 0.2, priorityFeesByPercentile: {}, }, { - number: new BN(3), - baseFeePerGas: new BN(30_000_000_000), + number: fromHex(toHex(3)), + baseFeePerGas: fromHex(toHex(30_000_000_000)), gasUsedRatio: 0.3, priorityFeesByPercentile: {}, }, { - number: new BN(4), - baseFeePerGas: new BN(40_000_000_000), + number: fromHex(toHex(4)), + baseFeePerGas: fromHex(toHex(40_000_000_000)), gasUsedRatio: null, priorityFeesByPercentile: null, }, diff --git a/src/gas/fetchBlockFeeHistory.ts b/packages/gas-fee-controller/src/fetchBlockFeeHistory.ts similarity index 99% rename from src/gas/fetchBlockFeeHistory.ts rename to packages/gas-fee-controller/src/fetchBlockFeeHistory.ts index 553e7b01f4..fd80040b85 100644 --- a/src/gas/fetchBlockFeeHistory.ts +++ b/packages/gas-fee-controller/src/fetchBlockFeeHistory.ts @@ -1,5 +1,5 @@ import { BN } from 'ethereumjs-util'; -import { query, fromHex, toHex } from '../util'; +import { query, fromHex, toHex } from '@metamask/controller-utils'; type EthQuery = any; diff --git a/src/gas/fetchGasEstimatesViaEthFeeHistory.test.ts b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.test.ts similarity index 82% rename from src/gas/fetchGasEstimatesViaEthFeeHistory.test.ts rename to packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.test.ts index c82a737730..c9086ab4fc 100644 --- a/src/gas/fetchGasEstimatesViaEthFeeHistory.test.ts +++ b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.test.ts @@ -1,5 +1,4 @@ import { BN } from 'ethereumjs-util'; -import { mocked } from 'ts-jest/utils'; import { when } from 'jest-when'; import fetchBlockFeeHistory from './fetchBlockFeeHistory'; import calculateGasFeeEstimatesForPriorityLevels from './fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels'; @@ -12,12 +11,19 @@ jest.mock( ); jest.mock('./fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock'); -const mockedFetchBlockFeeHistory = mocked(fetchBlockFeeHistory, true); -const mockedCalculateGasFeeEstimatesForPriorityLevels = mocked( - calculateGasFeeEstimatesForPriorityLevels, - true, -); -const mockedFetchLatestBlock = mocked(fetchLatestBlock, true); +const mockedFetchBlockFeeHistory = fetchBlockFeeHistory as jest.Mock< + ReturnType, + Parameters +>; +const mockedCalculateGasFeeEstimatesForPriorityLevels = + calculateGasFeeEstimatesForPriorityLevels as jest.Mock< + ReturnType, + Parameters + >; +const mockedFetchLatestBlock = fetchLatestBlock as jest.Mock< + ReturnType, + Parameters +>; describe('fetchGasEstimatesViaEthFeeHistory', () => { const latestBlock = { diff --git a/src/gas/fetchGasEstimatesViaEthFeeHistory.ts b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.ts similarity index 97% rename from src/gas/fetchGasEstimatesViaEthFeeHistory.ts rename to packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.ts index 85db2fdac9..13baabf398 100644 --- a/src/gas/fetchGasEstimatesViaEthFeeHistory.ts +++ b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory.ts @@ -1,5 +1,5 @@ import { fromWei } from 'ethjs-unit'; -import { GWEI } from '../constants'; +import { GWEI } from '@metamask/controller-utils'; import { GasFeeEstimates } from './GasFeeController'; import { EthQuery } from './fetchGasEstimatesViaEthFeeHistory/types'; import fetchBlockFeeHistory from './fetchBlockFeeHistory'; diff --git a/src/gas/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.test.ts b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.test.ts similarity index 100% rename from src/gas/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.test.ts rename to packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.test.ts diff --git a/src/gas/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.ts b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.ts similarity index 98% rename from src/gas/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.ts rename to packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.ts index 7eedae2c2c..8897ff4eec 100644 --- a/src/gas/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.ts +++ b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.ts @@ -1,8 +1,8 @@ import { BN } from 'ethereumjs-util'; import { fromWei } from 'ethjs-unit'; +import { GWEI } from '@metamask/controller-utils'; import { Eip1559GasFee, GasFeeEstimates } from '../GasFeeController'; import { FeeHistoryBlock } from '../fetchBlockFeeHistory'; -import { GWEI } from '../../constants'; import medianOf from './medianOf'; export type PriorityLevel = typeof PRIORITY_LEVELS[number]; diff --git a/src/gas/fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock.ts b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock.ts similarity index 92% rename from src/gas/fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock.ts rename to packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock.ts index 01def5bfd5..a3d686f284 100644 --- a/src/gas/fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock.ts +++ b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock.ts @@ -1,4 +1,4 @@ -import { query, fromHex } from '../../util'; +import { query, fromHex } from '@metamask/controller-utils'; import { EthBlock, EthQuery } from './types'; /** diff --git a/src/gas/fetchGasEstimatesViaEthFeeHistory/medianOf.ts b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/medianOf.ts similarity index 100% rename from src/gas/fetchGasEstimatesViaEthFeeHistory/medianOf.ts rename to packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/medianOf.ts diff --git a/src/gas/fetchGasEstimatesViaEthFeeHistory/types.ts b/packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/types.ts similarity index 100% rename from src/gas/fetchGasEstimatesViaEthFeeHistory/types.ts rename to packages/gas-fee-controller/src/fetchGasEstimatesViaEthFeeHistory/types.ts diff --git a/src/gas/gas-util.test.ts b/packages/gas-fee-controller/src/gas-util.test.ts similarity index 100% rename from src/gas/gas-util.test.ts rename to packages/gas-fee-controller/src/gas-util.test.ts diff --git a/src/gas/gas-util.ts b/packages/gas-fee-controller/src/gas-util.ts similarity index 98% rename from src/gas/gas-util.ts rename to packages/gas-fee-controller/src/gas-util.ts index 98b82f16a0..01dce95267 100644 --- a/src/gas/gas-util.ts +++ b/packages/gas-fee-controller/src/gas-util.ts @@ -1,5 +1,10 @@ import { BN } from 'ethereumjs-util'; -import { query, handleFetch, gweiDecToWEIBN, weiHexToGweiDec } from '../util'; +import { + query, + handleFetch, + gweiDecToWEIBN, + weiHexToGweiDec, +} from '@metamask/controller-utils'; import { GasFeeEstimates, EthGasPriceEstimate, diff --git a/packages/gas-fee-controller/src/index.ts b/packages/gas-fee-controller/src/index.ts new file mode 100644 index 0000000000..bb3be201ce --- /dev/null +++ b/packages/gas-fee-controller/src/index.ts @@ -0,0 +1 @@ +export * from './GasFeeController'; diff --git a/packages/gas-fee-controller/tsconfig.build.json b/packages/gas-fee-controller/tsconfig.build.json new file mode 100644 index 0000000000..ac0df4920c --- /dev/null +++ b/packages/gas-fee-controller/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/gas-fee-controller/tsconfig.json b/packages/gas-fee-controller/tsconfig.json new file mode 100644 index 0000000000..4bbb0be81b --- /dev/null +++ b/packages/gas-fee-controller/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" }, + { "path": "../network-controller" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/gas-fee-controller/typedoc.json b/packages/gas-fee-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/gas-fee-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/keyring-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/keyring-controller/LICENSE b/packages/keyring-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/keyring-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/keyring-controller/README.md b/packages/keyring-controller/README.md new file mode 100644 index 0000000000..0f932bc7ca --- /dev/null +++ b/packages/keyring-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/keyring-controller` + +Stores identities seen in the wallet and manages interactions such as signing. + +## Installation + +`yarn add @metamask/keyring-controller` + +or + +`npm install @metamask/keyring-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/keyring-controller/jest.config.js b/packages/keyring-controller/jest.config.js new file mode 100644 index 0000000000..ff05eddccd --- /dev/null +++ b/packages/keyring-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 98, + functions: 100, + lines: 99.49, + statements: 99.49, + }, + }, +}); diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json new file mode 100644 index 0000000000..d636d7b6d4 --- /dev/null +++ b/packages/keyring-controller/package.json @@ -0,0 +1,63 @@ +{ + "name": "@metamask/keyring-controller", + "version": "0.0.0", + "description": "Stores identities seen in the wallet and manages interactions such as signing", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/keyring-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/keyring-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@keystonehq/metamask-airgapped-keyring": "^0.6.1", + "@metamask/base-controller": "workspace:~", + "@metamask/controller-utils": "workspace:~", + "@metamask/message-manager": "workspace:~", + "@metamask/preferences-controller": "workspace:~", + "async-mutex": "^0.2.6", + "eth-keyring-controller": "^7.0.2", + "eth-sig-util": "^3.0.0", + "ethereumjs-util": "^7.0.10", + "ethereumjs-wallet": "^1.0.1" + }, + "devDependencies": { + "@ethereumjs/common": "^2.3.1", + "@ethereumjs/tx": "^3.2.1", + "@keystonehq/bc-ur-registry-eth": "^0.9.0", + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "sinon": "^9.2.4", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/keyring/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts similarity index 98% rename from src/keyring/KeyringController.test.ts rename to packages/keyring-controller/src/KeyringController.test.ts index 0cb47319fe..0cf6400ef5 100644 --- a/src/keyring/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -5,15 +5,15 @@ import { recoverTypedSignature_v4, recoverTypedSignatureLegacy, } from 'eth-sig-util'; -import sinon, { SinonStub } from 'sinon'; +import * as sinon from 'sinon'; import Common from '@ethereumjs/common'; import { TransactionFactory } from '@ethereumjs/tx'; import { MetaMaskKeyring as QRKeyring } from '@keystonehq/metamask-airgapped-keyring'; import { CryptoHDKey, ETHSignature } from '@keystonehq/bc-ur-registry-eth'; import * as uuid from 'uuid'; -import MockEncryptor from '../../tests/mocks/mockEncryptor'; -import { PreferencesController } from '../user/PreferencesController'; -import { MAINNET } from '../constants'; +import { PreferencesController } from '@metamask/preferences-controller'; +import { MAINNET } from '@metamask/controller-utils'; +import MockEncryptor from '../tests/mocks/mockEncryptor'; import { AccountImportStrategy, Keyring, @@ -23,6 +23,13 @@ import { SignTypedDataVersion, } from './KeyringController'; +jest.mock('uuid', () => { + return { + ...jest.requireActual('uuid'), + v4: () => '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d', + }; +}); + const input = '{"version":3,"id":"534e0199-53f6-41a9-a8fe-d504702ee5e8","address":"b97c80fab7a3793bbe746864db80d236f1345ea7",' + '"crypto":{"ciphertext":"974fec42023c2d6340d9710863aa82a2961aa03b9d7e5dd19aa77ab4aab1f344",' + @@ -722,8 +729,8 @@ describe('KeyringController', () => { let signProcessKeyringController: KeyringController; preferences = new PreferencesController(); - let requestSignatureStub: SinonStub; - let readAccountSub: SinonStub; + let requestSignatureStub: sinon.SinonStub; + let readAccountSub: sinon.SinonStub; const setupQRKeyring = async () => { readAccountSub.resolves( diff --git a/src/keyring/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts similarity index 98% rename from src/keyring/KeyringController.ts rename to packages/keyring-controller/src/KeyringController.ts index faa0d9a2f3..143542495e 100644 --- a/src/keyring/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -23,11 +23,13 @@ import { BaseConfig, BaseState, Listener, -} from '../BaseController'; -import { PreferencesController } from '../user/PreferencesController'; -import { PersonalMessageParams } from '../message-manager/PersonalMessageManager'; -import { TypedMessageParams } from '../message-manager/TypedMessageManager'; -import { toChecksumHexAddress } from '../util'; +} from '@metamask/base-controller'; +import { PreferencesController } from '@metamask/preferences-controller'; +import { + PersonalMessageParams, + TypedMessageParams, +} from '@metamask/message-manager'; +import { toChecksumHexAddress } from '@metamask/controller-utils'; /** * Available keyring types diff --git a/packages/keyring-controller/src/index.ts b/packages/keyring-controller/src/index.ts new file mode 100644 index 0000000000..9b98ad6fd7 --- /dev/null +++ b/packages/keyring-controller/src/index.ts @@ -0,0 +1 @@ +export * from './KeyringController'; diff --git a/tests/mocks/mockEncryptor.ts b/packages/keyring-controller/tests/mocks/mockEncryptor.ts similarity index 100% rename from tests/mocks/mockEncryptor.ts rename to packages/keyring-controller/tests/mocks/mockEncryptor.ts diff --git a/packages/keyring-controller/tsconfig.build.json b/packages/keyring-controller/tsconfig.build.json new file mode 100644 index 0000000000..093088e762 --- /dev/null +++ b/packages/keyring-controller/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { + "path": "../base-controller/tsconfig.build.json" + }, + { + "path": "../controller-utils/tsconfig.build.json" + }, + { + "path": "../message-manager/tsconfig.build.json" + }, + { + "path": "../preferences-controller/tsconfig.build.json" + } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/keyring-controller/tsconfig.json b/packages/keyring-controller/tsconfig.json new file mode 100644 index 0000000000..8d413c2652 --- /dev/null +++ b/packages/keyring-controller/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { + "path": "../base-controller" + }, + { + "path": "../controller-utils" + }, + { + "path": "../message-manager" + }, + { + "path": "../preferences-controller" + } + ], + "include": ["../../types", "./src", "./tests"] +} diff --git a/packages/keyring-controller/typedoc.json b/packages/keyring-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/keyring-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/message-manager/CHANGELOG.md b/packages/message-manager/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/message-manager/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/message-manager/LICENSE b/packages/message-manager/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/message-manager/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/message-manager/README.md b/packages/message-manager/README.md new file mode 100644 index 0000000000..fd18756a58 --- /dev/null +++ b/packages/message-manager/README.md @@ -0,0 +1,15 @@ +# `@metamask/message-manager` + +Stores and manages interactions with signing requests. + +## Installation + +`yarn add @metamask/message-manager` + +or + +`npm install @metamask/message-manager` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/message-manager/jest.config.js b/packages/message-manager/jest.config.js new file mode 100644 index 0000000000..a7ae2b4b47 --- /dev/null +++ b/packages/message-manager/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/message-manager/package.json b/packages/message-manager/package.json new file mode 100644 index 0000000000..523a630493 --- /dev/null +++ b/packages/message-manager/package.json @@ -0,0 +1,55 @@ +{ + "name": "@metamask/message-manager", + "version": "0.0.0", + "description": "Stores and manages interactions with signing requests", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/message-manager#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/message-manager", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~", + "@metamask/controller-utils": "workspace:~", + "@types/uuid": "^8.3.0", + "eth-sig-util": "^3.0.0", + "ethereumjs-util": "^7.0.10", + "jsonschema": "^1.2.4", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/message-manager/AbstractMessageManager.test.ts b/packages/message-manager/src/AbstractMessageManager.test.ts similarity index 100% rename from src/message-manager/AbstractMessageManager.test.ts rename to packages/message-manager/src/AbstractMessageManager.test.ts diff --git a/src/message-manager/AbstractMessageManager.ts b/packages/message-manager/src/AbstractMessageManager.ts similarity index 98% rename from src/message-manager/AbstractMessageManager.ts rename to packages/message-manager/src/AbstractMessageManager.ts index 7b51eafd7f..3aed006051 100644 --- a/src/message-manager/AbstractMessageManager.ts +++ b/packages/message-manager/src/AbstractMessageManager.ts @@ -1,5 +1,9 @@ import { EventEmitter } from 'events'; -import { BaseController, BaseConfig, BaseState } from '../BaseController'; +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; /** * @type OriginalRequest diff --git a/src/message-manager/MessageManager.test.ts b/packages/message-manager/src/MessageManager.test.ts similarity index 100% rename from src/message-manager/MessageManager.test.ts rename to packages/message-manager/src/MessageManager.test.ts diff --git a/src/message-manager/MessageManager.ts b/packages/message-manager/src/MessageManager.ts similarity index 98% rename from src/message-manager/MessageManager.ts rename to packages/message-manager/src/MessageManager.ts index dfd533fc39..ab83931a7f 100644 --- a/src/message-manager/MessageManager.ts +++ b/packages/message-manager/src/MessageManager.ts @@ -1,5 +1,4 @@ import { v1 as random } from 'uuid'; -import { validateSignMessageData, normalizeMessageData } from '../util'; import { AbstractMessageManager, AbstractMessage, @@ -7,6 +6,7 @@ import { AbstractMessageParamsMetamask, OriginalRequest, } from './AbstractMessageManager'; +import { normalizeMessageData, validateSignMessageData } from './utils'; /** * @type Message diff --git a/src/message-manager/PersonalMessageManager.test.ts b/packages/message-manager/src/PersonalMessageManager.test.ts similarity index 100% rename from src/message-manager/PersonalMessageManager.test.ts rename to packages/message-manager/src/PersonalMessageManager.test.ts diff --git a/src/message-manager/PersonalMessageManager.ts b/packages/message-manager/src/PersonalMessageManager.ts similarity index 98% rename from src/message-manager/PersonalMessageManager.ts rename to packages/message-manager/src/PersonalMessageManager.ts index 114412b044..e7e4e04168 100644 --- a/src/message-manager/PersonalMessageManager.ts +++ b/packages/message-manager/src/PersonalMessageManager.ts @@ -1,5 +1,5 @@ import { v1 as random } from 'uuid'; -import { validateSignMessageData, normalizeMessageData } from '../util'; +import { normalizeMessageData, validateSignMessageData } from './utils'; import { AbstractMessageManager, AbstractMessage, diff --git a/src/message-manager/TypedMessageManager.test.ts b/packages/message-manager/src/TypedMessageManager.test.ts similarity index 100% rename from src/message-manager/TypedMessageManager.test.ts rename to packages/message-manager/src/TypedMessageManager.test.ts diff --git a/src/message-manager/TypedMessageManager.ts b/packages/message-manager/src/TypedMessageManager.ts similarity index 99% rename from src/message-manager/TypedMessageManager.ts rename to packages/message-manager/src/TypedMessageManager.ts index 1b9fe91d86..cc6ff49307 100644 --- a/src/message-manager/TypedMessageManager.ts +++ b/packages/message-manager/src/TypedMessageManager.ts @@ -2,7 +2,7 @@ import { v1 as random } from 'uuid'; import { validateTypedSignMessageDataV3, validateTypedSignMessageDataV1, -} from '../util'; +} from './utils'; import { AbstractMessageManager, AbstractMessage, diff --git a/packages/message-manager/src/index.ts b/packages/message-manager/src/index.ts new file mode 100644 index 0000000000..239569e41c --- /dev/null +++ b/packages/message-manager/src/index.ts @@ -0,0 +1,3 @@ +export * from './MessageManager'; +export * from './PersonalMessageManager'; +export * from './TypedMessageManager'; diff --git a/packages/message-manager/src/utils.test.ts b/packages/message-manager/src/utils.test.ts new file mode 100644 index 0000000000..f07ff97ff9 --- /dev/null +++ b/packages/message-manager/src/utils.test.ts @@ -0,0 +1,188 @@ +import * as util from './utils'; + +describe('utils', () => { + it('normalizeMessageData', () => { + const firstNormalized = util.normalizeMessageData( + '879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', + ); + const secondNormalized = util.normalizeMessageData('somedata'); + expect(firstNormalized).toStrictEqual( + '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', + ); + expect(secondNormalized).toStrictEqual('0x736f6d6564617461'); + }); + + describe('validateSignMessageData', () => { + it('should throw if no from address', () => { + expect(() => + util.validateSignMessageData({ + data: '0x879a05', + } as any), + ).toThrow('Invalid "from" address: undefined must be a valid string.'); + }); + + it('should throw if invalid from address', () => { + expect(() => + util.validateSignMessageData({ + data: '0x879a05', + from: '01', + } as any), + ).toThrow('Invalid "from" address: 01 must be a valid string.'); + }); + + it('should throw if invalid type from address', () => { + expect(() => + util.validateSignMessageData({ + data: '0x879a05', + from: 123, + } as any), + ).toThrow('Invalid "from" address: 123 must be a valid string.'); + }); + + it('should throw if no data', () => { + expect(() => + util.validateSignMessageData({ + data: '0x879a05', + } as any), + ).toThrow('Invalid "from" address: undefined must be a valid string.'); + }); + + it('should throw if invalid tyoe data', () => { + expect(() => + util.validateSignMessageData({ + data: 123, + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + } as any), + ).toThrow('Invalid message "data": 123 must be a valid string.'); + }); + }); + + describe('validateTypedMessageDataV1', () => { + it('should throw if no from address legacy', () => { + expect(() => + util.validateTypedSignMessageDataV1({ + data: [], + } as any), + ).toThrow('Invalid "from" address:'); + }); + + it('should throw if invalid from address', () => { + expect(() => + util.validateTypedSignMessageDataV1({ + data: [], + from: '3244e191f1b4903970224322180f1fbbc415696b', + } as any), + ).toThrow('Expected EIP712 typed data.'); + }); + + it('should throw if invalid type from address', () => { + expect(() => + util.validateTypedSignMessageDataV1({ + data: [], + from: 123, + } as any), + ).toThrow('Invalid "from" address:'); + }); + + it('should throw if incorrect data', () => { + expect(() => + util.validateTypedSignMessageDataV1({ + data: '0x879a05', + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + } as any), + ).toThrow('Invalid message "data":'); + }); + + it('should throw if no data', () => { + expect(() => + util.validateTypedSignMessageDataV1({ + data: '0x879a05', + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + } as any), + ).toThrow('Invalid message "data":'); + }); + + it('should throw if invalid type data', () => { + expect(() => + util.validateTypedSignMessageDataV1({ + data: [], + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + } as any), + ).toThrow('Expected EIP712 typed data.'); + }); + }); + + describe('validateTypedMessageDataV3', () => { + const dataTyped = + '{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Person":[{"name":"name","type":"string"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","domain":{"name":"Ether Mail","version":"1","chainId":1,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},"contents":"Hello, Bob!"}}'; + it('should throw if no from address', () => { + expect(() => + util.validateTypedSignMessageDataV3({ + data: '0x879a05', + } as any), + ).toThrow('Invalid "from" address:'); + }); + + it('should throw if invalid from address', () => { + expect(() => + util.validateTypedSignMessageDataV3({ + data: '0x879a05', + from: '3244e191f1b4903970224322180f1fbbc415696b', + } as any), + ).toThrow('Data must be passed as a valid JSON string.'); + }); + + it('should throw if invalid type from address', () => { + expect(() => + util.validateTypedSignMessageDataV3({ + data: '0x879a05', + from: 123, + } as any), + ).toThrow('Invalid "from" address:'); + }); + + it('should throw if array data', () => { + expect(() => + util.validateTypedSignMessageDataV3({ + data: [], + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + } as any), + ).toThrow('Invalid message "data":'); + }); + + it('should throw if no array data', () => { + expect(() => + util.validateTypedSignMessageDataV3({ + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + } as any), + ).toThrow('Invalid message "data":'); + }); + + it('should throw if no json valid data', () => { + expect(() => + util.validateTypedSignMessageDataV3({ + data: 'uh oh', + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + } as any), + ).toThrow('Data must be passed as a valid JSON string.'); + }); + + it('should throw if data not in typed message schema', () => { + expect(() => + util.validateTypedSignMessageDataV3({ + data: '{"greetings":"I am Alice"}', + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + } as any), + ).toThrow('Data must conform to EIP-712 schema.'); + }); + + it('should not throw if data is correct', () => { + expect(() => + util.validateTypedSignMessageDataV3({ + data: dataTyped, + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + } as any), + ).not.toThrow(); + }); + }); +}); diff --git a/packages/message-manager/src/utils.ts b/packages/message-manager/src/utils.ts new file mode 100644 index 0000000000..454f2f8c10 --- /dev/null +++ b/packages/message-manager/src/utils.ts @@ -0,0 +1,118 @@ +import { addHexPrefix, bufferToHex, stripHexPrefix } from 'ethereumjs-util'; +import { TYPED_MESSAGE_SCHEMA, typedSignatureHash } from 'eth-sig-util'; +import { validate } from 'jsonschema'; +import { isValidHexAddress } from '@metamask/controller-utils'; +import { MessageParams } from './MessageManager'; +import { PersonalMessageParams } from './PersonalMessageManager'; +import { TypedMessageParams } from './TypedMessageManager'; + +const hexRe = /^[0-9A-Fa-f]+$/gu; + +/** + * A helper function that converts rawmessageData buffer data to a hex, or just returns the data if + * it is already formatted as a hex. + * + * @param data - The buffer data to convert to a hex. + * @returns A hex string conversion of the buffer data. + */ +export function normalizeMessageData(data: string) { + try { + const stripped = stripHexPrefix(data); + if (stripped.match(hexRe)) { + return addHexPrefix(stripped); + } + } catch (e) { + /* istanbul ignore next */ + } + return bufferToHex(Buffer.from(data, 'utf8')); +} + +/** + * Validates a PersonalMessageParams and MessageParams objects for required properties and throws in + * the event of any validation error. + * + * @param messageData - PersonalMessageParams object to validate. + */ +export function validateSignMessageData( + messageData: PersonalMessageParams | MessageParams, +) { + const { from, data } = messageData; + if (!from || typeof from !== 'string' || !isValidHexAddress(from)) { + throw new Error(`Invalid "from" address: ${from} must be a valid string.`); + } + + if (!data || typeof data !== 'string') { + throw new Error(`Invalid message "data": ${data} must be a valid string.`); + } +} + +/** + * Validates a TypedMessageParams object for required properties and throws in + * the event of any validation error for eth_signTypedMessage_V1. + * + * @param messageData - TypedMessageParams object to validate. + */ +export function validateTypedSignMessageDataV1( + messageData: TypedMessageParams, +) { + if ( + !messageData.from || + typeof messageData.from !== 'string' || + !isValidHexAddress(messageData.from) + ) { + throw new Error( + `Invalid "from" address: ${messageData.from} must be a valid string.`, + ); + } + + if (!messageData.data || !Array.isArray(messageData.data)) { + throw new Error( + `Invalid message "data": ${messageData.data} must be a valid array.`, + ); + } + + try { + // typedSignatureHash will throw if the data is invalid. + typedSignatureHash(messageData.data as any); + } catch (e) { + throw new Error(`Expected EIP712 typed data.`); + } +} + +/** + * Validates a TypedMessageParams object for required properties and throws in + * the event of any validation error for eth_signTypedMessage_V3. + * + * @param messageData - TypedMessageParams object to validate. + */ +export function validateTypedSignMessageDataV3( + messageData: TypedMessageParams, +) { + if ( + !messageData.from || + typeof messageData.from !== 'string' || + !isValidHexAddress(messageData.from) + ) { + throw new Error( + `Invalid "from" address: ${messageData.from} must be a valid string.`, + ); + } + + if (!messageData.data || typeof messageData.data !== 'string') { + throw new Error( + `Invalid message "data": ${messageData.data} must be a valid array.`, + ); + } + let data; + try { + data = JSON.parse(messageData.data); + } catch (e) { + throw new Error('Data must be passed as a valid JSON string.'); + } + const validation = validate(data, TYPED_MESSAGE_SCHEMA); + if (validation.errors.length > 0) { + throw new Error( + 'Data must conform to EIP-712 schema. See https://git.io/fNtcx.', + ); + } +} diff --git a/packages/message-manager/tsconfig.build.json b/packages/message-manager/tsconfig.build.json new file mode 100644 index 0000000000..bbfe057a20 --- /dev/null +++ b/packages/message-manager/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/message-manager/tsconfig.json b/packages/message-manager/tsconfig.json new file mode 100644 index 0000000000..7ee9852347 --- /dev/null +++ b/packages/message-manager/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/message-manager/typedoc.json b/packages/message-manager/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/message-manager/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/network-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/network-controller/LICENSE b/packages/network-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/network-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/network-controller/README.md b/packages/network-controller/README.md new file mode 100644 index 0000000000..09b69ee910 --- /dev/null +++ b/packages/network-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/network-controller` + +Provides an interface to the currently selected network via a MetaMask-compatible provider object. + +## Installation + +`yarn add @metamask/network-controller` + +or + +`npm install @metamask/network-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/network-controller/jest.config.js b/packages/network-controller/jest.config.js new file mode 100644 index 0000000000..58c9787c32 --- /dev/null +++ b/packages/network-controller/jest.config.js @@ -0,0 +1,29 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 71.11, + functions: 78.57, + lines: 85.1, + statements: 85.1, + }, + }, + + // Currently the tests for NetworkController have a race condition which + // causes intermittent failures. This seems to fix it. + testEnvironment: 'jsdom', +}); diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json new file mode 100644 index 0000000000..61555bfbea --- /dev/null +++ b/packages/network-controller/package.json @@ -0,0 +1,57 @@ +{ + "name": "@metamask/network-controller", + "version": "0.0.0", + "description": "Provides an interface to the currently selected network via a MetaMask-compatible provider object", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/network-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/network-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~", + "@metamask/controller-utils": "workspace:~", + "async-mutex": "^0.2.6", + "babel-runtime": "^6.26.0", + "eth-json-rpc-infura": "^5.1.0", + "eth-query": "^2.1.2", + "immer": "^9.0.6", + "web3-provider-engine": "^16.0.3" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "sinon": "^9.2.4", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/network/NetworkController.test.ts b/packages/network-controller/src/NetworkController.test.ts similarity index 97% rename from src/network/NetworkController.test.ts rename to packages/network-controller/src/NetworkController.test.ts index 7e60305164..a49fbfa1bf 100644 --- a/src/network/NetworkController.test.ts +++ b/packages/network-controller/src/NetworkController.test.ts @@ -1,12 +1,11 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import Web3ProviderEngine from 'web3-provider-engine'; -import { ControllerMessenger } from '../ControllerMessenger'; +import { ControllerMessenger } from '@metamask/base-controller'; +import { NetworkType, NetworksChainId } from '@metamask/controller-utils'; import { NetworkController, NetworkControllerMessenger, NetworkControllerOptions, - NetworksChainId, - NetworkType, ProviderConfig, } from './NetworkController'; diff --git a/src/network/NetworkController.ts b/packages/network-controller/src/NetworkController.ts similarity index 95% rename from src/network/NetworkController.ts rename to packages/network-controller/src/NetworkController.ts index 23114baf3d..a0f9aaccef 100644 --- a/src/network/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -4,35 +4,17 @@ import createInfuraProvider from 'eth-json-rpc-infura/src/createProvider'; import createMetamaskProvider from 'web3-provider-engine/zero'; import { Mutex } from 'async-mutex'; import type { Patch } from 'immer'; -import { BaseController } from '../BaseControllerV2'; +import { + BaseControllerV2, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; import { MAINNET, RPC, TESTNET_NETWORK_TYPE_TO_TICKER_SYMBOL, -} from '../constants'; -import { RestrictedControllerMessenger } from '../ControllerMessenger'; - -/** - * Human-readable network name - */ -export type NetworkType = - | 'kovan' - | 'localhost' - | 'mainnet' - | 'rinkeby' - | 'goerli' - | 'ropsten' - | 'rpc'; - -export enum NetworksChainId { - mainnet = '1', - kovan = '42', - rinkeby = '4', - goerli = '5', - ropsten = '3', - localhost = '', - rpc = '', -} + NetworksChainId, + NetworkType, +} from '@metamask/controller-utils'; /** * @type ProviderConfig @@ -125,7 +107,7 @@ const defaultState: NetworkState = { /** * Controller that creates and manages an Ethereum network provider. */ -export class NetworkController extends BaseController< +export class NetworkController extends BaseControllerV2< typeof name, NetworkState, NetworkControllerMessenger diff --git a/packages/network-controller/src/index.ts b/packages/network-controller/src/index.ts new file mode 100644 index 0000000000..102992b2fc --- /dev/null +++ b/packages/network-controller/src/index.ts @@ -0,0 +1 @@ +export * from './NetworkController'; diff --git a/packages/network-controller/tsconfig.build.json b/packages/network-controller/tsconfig.build.json new file mode 100644 index 0000000000..bbfe057a20 --- /dev/null +++ b/packages/network-controller/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/network-controller/tsconfig.json b/packages/network-controller/tsconfig.json new file mode 100644 index 0000000000..7ee9852347 --- /dev/null +++ b/packages/network-controller/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/network-controller/typedoc.json b/packages/network-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/network-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/notification-controller/CHANGELOG.md b/packages/notification-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/notification-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/notification-controller/LICENSE b/packages/notification-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/notification-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/notification-controller/README.md b/packages/notification-controller/README.md new file mode 100644 index 0000000000..a893285678 --- /dev/null +++ b/packages/notification-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/notification-controller` + +Manages display of the "What's New" messages in MetaMask. + +## Installation + +`yarn add @metamask/notification-controller` + +or + +`npm install @metamask/notification-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/notification-controller/jest.config.js b/packages/notification-controller/jest.config.js new file mode 100644 index 0000000000..a7ae2b4b47 --- /dev/null +++ b/packages/notification-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/notification-controller/package.json b/packages/notification-controller/package.json new file mode 100644 index 0000000000..7344098c47 --- /dev/null +++ b/packages/notification-controller/package.json @@ -0,0 +1,52 @@ +{ + "name": "@metamask/notification-controller", + "version": "0.0.0", + "description": "Manages display of the \"What's New\" messages in MetaMask", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/notification-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/notification-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~", + "@metamask/controller-utils": "workspace:~", + "immer": "^9.0.6", + "nanoid": "^3.1.31" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/notification/NotificationController.test.ts b/packages/notification-controller/src/NotificationController.test.ts similarity index 98% rename from src/notification/NotificationController.test.ts rename to packages/notification-controller/src/NotificationController.test.ts index da2e135154..e1c30a6781 100644 --- a/src/notification/NotificationController.test.ts +++ b/packages/notification-controller/src/NotificationController.test.ts @@ -1,4 +1,4 @@ -import { ControllerMessenger } from '../ControllerMessenger'; +import { ControllerMessenger } from '@metamask/base-controller'; import { NotificationControllerActions, NotificationController, diff --git a/src/notification/NotificationController.ts b/packages/notification-controller/src/NotificationController.ts similarity index 95% rename from src/notification/NotificationController.ts rename to packages/notification-controller/src/NotificationController.ts index 5cc264dfe0..aec9117002 100644 --- a/src/notification/NotificationController.ts +++ b/packages/notification-controller/src/NotificationController.ts @@ -1,10 +1,10 @@ import type { Patch } from 'immer'; import { nanoid } from 'nanoid'; - -import { hasProperty } from '../util'; -import { BaseController } from '../BaseControllerV2'; - -import type { RestrictedControllerMessenger } from '../ControllerMessenger'; +import { hasProperty } from '@metamask/controller-utils'; +import { + BaseControllerV2, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; /** * @typedef NotificationControllerState @@ -88,7 +88,7 @@ const defaultState = { /** * Controller that handles storing notifications and showing them to the user */ -export class NotificationController extends BaseController< +export class NotificationController extends BaseControllerV2< typeof name, NotificationControllerState, NotificationControllerMessenger diff --git a/packages/notification-controller/src/index.ts b/packages/notification-controller/src/index.ts new file mode 100644 index 0000000000..6c896d4826 --- /dev/null +++ b/packages/notification-controller/src/index.ts @@ -0,0 +1 @@ +export * from './NotificationController'; diff --git a/packages/notification-controller/tsconfig.build.json b/packages/notification-controller/tsconfig.build.json new file mode 100644 index 0000000000..bbfe057a20 --- /dev/null +++ b/packages/notification-controller/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/notification-controller/tsconfig.json b/packages/notification-controller/tsconfig.json new file mode 100644 index 0000000000..7ee9852347 --- /dev/null +++ b/packages/notification-controller/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/notification-controller/typedoc.json b/packages/notification-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/notification-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/src/permissions/README.md b/packages/permission-controller/ARCHITECTURE.md similarity index 99% rename from src/permissions/README.md rename to packages/permission-controller/ARCHITECTURE.md index 8481df6200..09d8b6a7c8 100644 --- a/src/permissions/README.md +++ b/packages/permission-controller/ARCHITECTURE.md @@ -1,4 +1,4 @@ -# PermissionController +# Architecture The `PermissionController` is the heart of an object capability-inspired permission system. It is the successor of the original MetaMask permission system, [`rpc-cap`](https://github.com/MetaMask/rpc-cap). diff --git a/packages/permission-controller/CHANGELOG.md b/packages/permission-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/permission-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/permission-controller/LICENSE b/packages/permission-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/permission-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/permission-controller/README.md b/packages/permission-controller/README.md new file mode 100644 index 0000000000..e3a5f17e5d --- /dev/null +++ b/packages/permission-controller/README.md @@ -0,0 +1,19 @@ +# `@metamask/permission-controller` + +Mediates access to JSON-RPC methods, used to interact with pieces of the MetaMask stack, via middleware for `json-rpc-engine`. + +## Installation + +`yarn add @metamask/permission-controller` + +or + +`npm install @metamask/permission-controller` + +## Understanding + +Please read the [Architecture][./architecture.md] document for more on how PermissionController works and how to use it. + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/permission-controller/jest.config.js b/packages/permission-controller/jest.config.js new file mode 100644 index 0000000000..0f1508348b --- /dev/null +++ b/packages/permission-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 99.6, + functions: 100, + lines: 99.78, + statements: 99.78, + }, + }, +}); diff --git a/packages/permission-controller/package.json b/packages/permission-controller/package.json new file mode 100644 index 0000000000..26bf7584ce --- /dev/null +++ b/packages/permission-controller/package.json @@ -0,0 +1,58 @@ +{ + "name": "@metamask/permission-controller", + "version": "0.0.0", + "description": "Mediates access to JSON-RPC methods, used to interact with pieces of the MetaMask stack, via middleware for json-rpc-engine", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/permission-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/permission-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/approval-controller": "workspace:~", + "@metamask/base-controller": "workspace:~", + "@metamask/controller-utils": "workspace:~", + "@metamask/types": "^1.1.0", + "@types/deep-freeze-strict": "^1.1.0", + "deep-freeze-strict": "^1.1.1", + "eth-rpc-errors": "^4.0.0", + "immer": "^9.0.6", + "json-rpc-engine": "^6.1.0", + "nanoid": "^3.1.31" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/permissions/Caveat.test.ts b/packages/permission-controller/src/Caveat.test.ts similarity index 100% rename from src/permissions/Caveat.test.ts rename to packages/permission-controller/src/Caveat.test.ts diff --git a/src/permissions/Caveat.ts b/packages/permission-controller/src/Caveat.ts similarity index 99% rename from src/permissions/Caveat.ts rename to packages/permission-controller/src/Caveat.ts index 875bbb23d1..45ff3b02a2 100644 --- a/src/permissions/Caveat.ts +++ b/packages/permission-controller/src/Caveat.ts @@ -1,5 +1,5 @@ import { Json } from '@metamask/types'; -import { hasProperty } from '../util'; +import { hasProperty } from '@metamask/controller-utils'; import { CaveatSpecificationMismatchError, UnrecognizedCaveatTypeError, diff --git a/src/permissions/Permission.test.ts b/packages/permission-controller/src/Permission.test.ts similarity index 100% rename from src/permissions/Permission.test.ts rename to packages/permission-controller/src/Permission.test.ts diff --git a/src/permissions/Permission.ts b/packages/permission-controller/src/Permission.ts similarity index 99% rename from src/permissions/Permission.ts rename to packages/permission-controller/src/Permission.ts index c53794d9dc..50a00820b4 100644 --- a/src/permissions/Permission.ts +++ b/packages/permission-controller/src/Permission.ts @@ -1,6 +1,6 @@ import { Json } from '@metamask/types'; import { nanoid } from 'nanoid'; -import { NonEmptyArray } from '../util'; +import { NonEmptyArray } from '@metamask/controller-utils'; import { CaveatConstraint } from './Caveat'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import type { PermissionController } from './PermissionController'; diff --git a/src/permissions/PermissionController.test.ts b/packages/permission-controller/src/PermissionController.test.ts similarity index 99% rename from src/permissions/PermissionController.test.ts rename to packages/permission-controller/src/PermissionController.test.ts index 1e9b69d447..34aab312cb 100644 --- a/src/permissions/PermissionController.test.ts +++ b/packages/permission-controller/src/PermissionController.test.ts @@ -5,10 +5,9 @@ import { AddApprovalRequest, HasApprovalRequest, RejectRequest as RejectApprovalRequest, -} from '../approval/ApprovalController'; -import { Json } from '../BaseControllerV2'; -import { ControllerMessenger } from '../ControllerMessenger'; -import { hasProperty, isPlainObject } from '../util'; +} from '@metamask/approval-controller'; +import { Json, ControllerMessenger } from '@metamask/base-controller'; +import { hasProperty, isPlainObject } from '@metamask/controller-utils'; import * as errors from './errors'; import { EndowmentGetterParams } from './Permission'; import { @@ -30,6 +29,7 @@ import { RestrictedMethodParameters, ValidPermission, } from '.'; + // Caveat types and specifications const CaveatTypes = { diff --git a/src/permissions/PermissionController.ts b/packages/permission-controller/src/PermissionController.ts similarity index 99% rename from src/permissions/PermissionController.ts rename to packages/permission-controller/src/PermissionController.ts index 3388c871d8..659413e5ba 100644 --- a/src/permissions/PermissionController.ts +++ b/packages/permission-controller/src/PermissionController.ts @@ -9,16 +9,20 @@ import { AddApprovalRequest, HasApprovalRequest, RejectRequest as RejectApprovalRequest, -} from '../approval/ApprovalController'; -import { BaseController, Json, StateMetadata } from '../BaseControllerV2'; -import { RestrictedControllerMessenger } from '../ControllerMessenger'; +} from '@metamask/approval-controller'; +import { + BaseControllerV2, + Json, + StateMetadata, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; import { hasProperty, isNonEmptyArray, isPlainObject, isValidJson, NonEmptyArray, -} from '../util'; +} from '@metamask/controller-utils'; import { CaveatConstraint, CaveatSpecificationConstraint, @@ -456,7 +460,8 @@ export type PermissionControllerOptions< }; /** - * The permission controller. See the README for details. + * The permission controller. See the [Architecture](../ARCHITECTURE.md) + * document for details. * * Assumes the existence of an {@link ApprovalController} reachable via the * {@link ControllerMessenger}. @@ -470,7 +475,7 @@ export type PermissionControllerOptions< export class PermissionController< ControllerPermissionSpecification extends PermissionSpecificationConstraint, ControllerCaveatSpecification extends CaveatSpecificationConstraint, -> extends BaseController< +> extends BaseControllerV2< typeof controllerName, PermissionControllerState< ExtractPermission< @@ -524,7 +529,7 @@ export class PermissionController< * @param options.unrestrictedMethods - The callable names of all JSON-RPC * methods ignored by the new controller. * @param options.messenger - The controller messenger. See - * {@link BaseController} for more information. + * {@link BaseControllerV2} for more information. * @param options.state - Existing state to hydrate the controller with at * initialization. */ diff --git a/src/permissions/errors.test.ts b/packages/permission-controller/src/errors.test.ts similarity index 100% rename from src/permissions/errors.test.ts rename to packages/permission-controller/src/errors.test.ts diff --git a/src/permissions/errors.ts b/packages/permission-controller/src/errors.ts similarity index 100% rename from src/permissions/errors.ts rename to packages/permission-controller/src/errors.ts diff --git a/src/permissions/index.ts b/packages/permission-controller/src/index.ts similarity index 100% rename from src/permissions/index.ts rename to packages/permission-controller/src/index.ts diff --git a/src/permissions/permission-middleware.ts b/packages/permission-controller/src/permission-middleware.ts similarity index 100% rename from src/permissions/permission-middleware.ts rename to packages/permission-controller/src/permission-middleware.ts diff --git a/src/permissions/rpc-methods/getPermissions.test.ts b/packages/permission-controller/src/rpc-methods/getPermissions.test.ts similarity index 100% rename from src/permissions/rpc-methods/getPermissions.test.ts rename to packages/permission-controller/src/rpc-methods/getPermissions.test.ts diff --git a/src/permissions/rpc-methods/getPermissions.ts b/packages/permission-controller/src/rpc-methods/getPermissions.ts similarity index 100% rename from src/permissions/rpc-methods/getPermissions.ts rename to packages/permission-controller/src/rpc-methods/getPermissions.ts diff --git a/src/permissions/rpc-methods/index.ts b/packages/permission-controller/src/rpc-methods/index.ts similarity index 100% rename from src/permissions/rpc-methods/index.ts rename to packages/permission-controller/src/rpc-methods/index.ts diff --git a/src/permissions/rpc-methods/requestPermissions.test.ts b/packages/permission-controller/src/rpc-methods/requestPermissions.test.ts similarity index 100% rename from src/permissions/rpc-methods/requestPermissions.test.ts rename to packages/permission-controller/src/rpc-methods/requestPermissions.test.ts diff --git a/src/permissions/rpc-methods/requestPermissions.ts b/packages/permission-controller/src/rpc-methods/requestPermissions.ts similarity index 97% rename from src/permissions/rpc-methods/requestPermissions.ts rename to packages/permission-controller/src/rpc-methods/requestPermissions.ts index 6f9b88a752..5509d1ebfb 100644 --- a/src/permissions/rpc-methods/requestPermissions.ts +++ b/packages/permission-controller/src/rpc-methods/requestPermissions.ts @@ -5,11 +5,10 @@ import type { PendingJsonRpcResponse, PermittedHandlerExport, } from '@metamask/types'; +import { isPlainObject } from '@metamask/controller-utils'; import { MethodNames } from '../utils'; - import { invalidParams } from '../errors'; import type { PermissionConstraint, RequestedPermissions } from '../Permission'; -import { isPlainObject } from '../../util'; export const requestPermissionsHandler: PermittedHandlerExport< RequestPermissionsHooks, diff --git a/src/permissions/utils.ts b/packages/permission-controller/src/utils.ts similarity index 100% rename from src/permissions/utils.ts rename to packages/permission-controller/src/utils.ts diff --git a/packages/permission-controller/tsconfig.build.json b/packages/permission-controller/tsconfig.build.json new file mode 100644 index 0000000000..072ac3ed13 --- /dev/null +++ b/packages/permission-controller/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../approval-controller/tsconfig.build.json" }, + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/permission-controller/tsconfig.json b/packages/permission-controller/tsconfig.json new file mode 100644 index 0000000000..32e1ee560c --- /dev/null +++ b/packages/permission-controller/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../approval-controller" }, + { "path": "../base-controller" }, + { "path": "../controller-utils" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/permission-controller/typedoc.json b/packages/permission-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/permission-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/phishing-controller/CHANGELOG.md b/packages/phishing-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/phishing-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/phishing-controller/LICENSE b/packages/phishing-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/phishing-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/phishing-controller/README.md b/packages/phishing-controller/README.md new file mode 100644 index 0000000000..575686ec46 --- /dev/null +++ b/packages/phishing-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/phishing-controller` + +Maintains a periodically updated list of approved and unapproved website origins. + +## Installation + +`yarn add @metamask/phishing-controller` + +or + +`npm install @metamask/phishing-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/phishing-controller/jest.config.js b/packages/phishing-controller/jest.config.js new file mode 100644 index 0000000000..a7ae2b4b47 --- /dev/null +++ b/packages/phishing-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/phishing-controller/package.json b/packages/phishing-controller/package.json new file mode 100644 index 0000000000..b4d4ff6996 --- /dev/null +++ b/packages/phishing-controller/package.json @@ -0,0 +1,56 @@ +{ + "name": "@metamask/phishing-controller", + "version": "0.0.0", + "description": "Maintains a periodically updated list of approved and unapproved website origins", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/phishing-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/phishing-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~", + "@metamask/controller-utils": "workspace:~", + "@types/punycode": "^2.1.0", + "eth-phishing-detect": "^1.2.0", + "isomorphic-fetch": "^3.0.0", + "punycode": "^2.1.1" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "nock": "^13.0.7", + "sinon": "^9.2.4", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/third-party/PhishingController.test.ts b/packages/phishing-controller/src/PhishingController.test.ts similarity index 99% rename from src/third-party/PhishingController.test.ts rename to packages/phishing-controller/src/PhishingController.test.ts index 0ac4c7e4d8..348137d2e8 100644 --- a/src/third-party/PhishingController.test.ts +++ b/packages/phishing-controller/src/PhishingController.test.ts @@ -1,5 +1,5 @@ import { strict as assert } from 'assert'; -import sinon from 'sinon'; +import * as sinon from 'sinon'; import nock from 'nock'; import DEFAULT_PHISHING_RESPONSE from 'eth-phishing-detect/src/config.json'; import { diff --git a/src/third-party/PhishingController.ts b/packages/phishing-controller/src/PhishingController.ts similarity index 98% rename from src/third-party/PhishingController.ts rename to packages/phishing-controller/src/PhishingController.ts index 46ce870437..e78686ddd4 100644 --- a/src/third-party/PhishingController.ts +++ b/packages/phishing-controller/src/PhishingController.ts @@ -1,8 +1,12 @@ import { toASCII } from 'punycode/'; import DEFAULT_PHISHING_RESPONSE from 'eth-phishing-detect/src/config.json'; import PhishingDetector from 'eth-phishing-detect/src/detector'; -import { BaseController, BaseConfig, BaseState } from '../BaseController'; -import { safelyExecute } from '../util'; +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; +import { safelyExecute } from '@metamask/controller-utils'; /** * @type EthPhishingResponse diff --git a/packages/phishing-controller/src/index.ts b/packages/phishing-controller/src/index.ts new file mode 100644 index 0000000000..d462ec3090 --- /dev/null +++ b/packages/phishing-controller/src/index.ts @@ -0,0 +1,3 @@ +import 'isomorphic-fetch'; + +export * from './PhishingController'; diff --git a/packages/phishing-controller/tsconfig.build.json b/packages/phishing-controller/tsconfig.build.json new file mode 100644 index 0000000000..bbfe057a20 --- /dev/null +++ b/packages/phishing-controller/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/phishing-controller/tsconfig.json b/packages/phishing-controller/tsconfig.json new file mode 100644 index 0000000000..7ee9852347 --- /dev/null +++ b/packages/phishing-controller/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/phishing-controller/typedoc.json b/packages/phishing-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/phishing-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/preferences-controller/CHANGELOG.md b/packages/preferences-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/preferences-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/preferences-controller/LICENSE b/packages/preferences-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/preferences-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/preferences-controller/README.md b/packages/preferences-controller/README.md new file mode 100644 index 0000000000..3ac749db59 --- /dev/null +++ b/packages/preferences-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/preferences-controller` + +Manages user-configurable settings for MetaMask. + +## Installation + +`yarn add @metamask/preferences-controller` + +or + +`npm install @metamask/preferences-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/preferences-controller/jest.config.js b/packages/preferences-controller/jest.config.js new file mode 100644 index 0000000000..5fd4fda362 --- /dev/null +++ b/packages/preferences-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 88.23, + functions: 95, + lines: 93.82, + statements: 93.82, + }, + }, +}); diff --git a/packages/preferences-controller/package.json b/packages/preferences-controller/package.json new file mode 100644 index 0000000000..1576e43289 --- /dev/null +++ b/packages/preferences-controller/package.json @@ -0,0 +1,50 @@ +{ + "name": "@metamask/preferences-controller", + "version": "0.0.0", + "description": "Manages user-configurable settings for MetaMask", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/preferences-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/preferences-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~", + "@metamask/controller-utils": "workspace:~" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/user/PreferencesController.test.ts b/packages/preferences-controller/src/PreferencesController.test.ts similarity index 100% rename from src/user/PreferencesController.test.ts rename to packages/preferences-controller/src/PreferencesController.test.ts diff --git a/src/user/PreferencesController.ts b/packages/preferences-controller/src/PreferencesController.ts similarity index 94% rename from src/user/PreferencesController.ts rename to packages/preferences-controller/src/PreferencesController.ts index 6d13bc6cd1..c902bd0232 100644 --- a/src/user/PreferencesController.ts +++ b/packages/preferences-controller/src/PreferencesController.ts @@ -1,6 +1,22 @@ -import { BaseController, BaseConfig, BaseState } from '../BaseController'; -import { toChecksumHexAddress } from '../util'; -import { ContactEntry } from './AddressBookController'; +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; +import { toChecksumHexAddress } from '@metamask/controller-utils'; + +/** + * ContactEntry representation. + * + * @property address - Hex address of a recipient account + * @property name - Nickname associated with this address + * @property importTime - Data time when an account as created/imported + */ +export interface ContactEntry { + address: string; + name: string; + importTime?: number; +} /** * Custom RPC network information diff --git a/packages/preferences-controller/src/index.ts b/packages/preferences-controller/src/index.ts new file mode 100644 index 0000000000..8032ff1591 --- /dev/null +++ b/packages/preferences-controller/src/index.ts @@ -0,0 +1 @@ +export * from './PreferencesController'; diff --git a/packages/preferences-controller/tsconfig.build.json b/packages/preferences-controller/tsconfig.build.json new file mode 100644 index 0000000000..bbfe057a20 --- /dev/null +++ b/packages/preferences-controller/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/preferences-controller/tsconfig.json b/packages/preferences-controller/tsconfig.json new file mode 100644 index 0000000000..7ee9852347 --- /dev/null +++ b/packages/preferences-controller/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/preferences-controller/typedoc.json b/packages/preferences-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/preferences-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/rate-limit-controller/CHANGELOG.md b/packages/rate-limit-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/rate-limit-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/rate-limit-controller/LICENSE b/packages/rate-limit-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/rate-limit-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/rate-limit-controller/README.md b/packages/rate-limit-controller/README.md new file mode 100644 index 0000000000..3190239e4f --- /dev/null +++ b/packages/rate-limit-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/rate-limit-controller` + +Contains logic for rate-limiting API endpoints by requesting origin. + +## Installation + +`yarn add @metamask/rate-limit-controller` + +or + +`npm install @metamask/rate-limit-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/rate-limit-controller/jest.config.js b/packages/rate-limit-controller/jest.config.js new file mode 100644 index 0000000000..e1d82f7a5a --- /dev/null +++ b/packages/rate-limit-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 88.88, + functions: 100, + lines: 96.55, + statements: 96.55, + }, + }, +}); diff --git a/packages/rate-limit-controller/package.json b/packages/rate-limit-controller/package.json new file mode 100644 index 0000000000..d752020328 --- /dev/null +++ b/packages/rate-limit-controller/package.json @@ -0,0 +1,51 @@ +{ + "name": "@metamask/rate-limit-controller", + "version": "0.0.0", + "description": "Contains logic for rate-limiting API endpoints by requesting origin", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/rate-limit-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/rate-limit-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~", + "eth-rpc-errors": "^4.0.0", + "immer": "^9.0.6" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/ratelimit/RateLimitController.test.ts b/packages/rate-limit-controller/src/RateLimitController.test.ts similarity index 98% rename from src/ratelimit/RateLimitController.test.ts rename to packages/rate-limit-controller/src/RateLimitController.test.ts index 5a0f7c076d..fc63790ad7 100644 --- a/src/ratelimit/RateLimitController.test.ts +++ b/packages/rate-limit-controller/src/RateLimitController.test.ts @@ -1,4 +1,4 @@ -import { ControllerMessenger } from '../ControllerMessenger'; +import { ControllerMessenger } from '@metamask/base-controller'; import { RateLimitControllerActions, RateLimitStateChange, diff --git a/src/ratelimit/RateLimitController.ts b/packages/rate-limit-controller/src/RateLimitController.ts similarity index 97% rename from src/ratelimit/RateLimitController.ts rename to packages/rate-limit-controller/src/RateLimitController.ts index af2c41dd71..221ba8ec50 100644 --- a/src/ratelimit/RateLimitController.ts +++ b/packages/rate-limit-controller/src/RateLimitController.ts @@ -1,9 +1,9 @@ import { ethErrors } from 'eth-rpc-errors'; import type { Patch } from 'immer'; - -import { BaseController } from '../BaseControllerV2'; - -import type { RestrictedControllerMessenger } from '../ControllerMessenger'; +import { + BaseControllerV2 as BaseController, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; /** * @type RateLimitState diff --git a/packages/rate-limit-controller/src/index.ts b/packages/rate-limit-controller/src/index.ts new file mode 100644 index 0000000000..50645817b7 --- /dev/null +++ b/packages/rate-limit-controller/src/index.ts @@ -0,0 +1 @@ +export * from './RateLimitController'; diff --git a/packages/rate-limit-controller/tsconfig.build.json b/packages/rate-limit-controller/tsconfig.build.json new file mode 100644 index 0000000000..e5fd7422b9 --- /dev/null +++ b/packages/rate-limit-controller/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [{ "path": "../base-controller/tsconfig.build.json" }], + "include": ["../../types", "./src"] +} diff --git a/packages/rate-limit-controller/tsconfig.json b/packages/rate-limit-controller/tsconfig.json new file mode 100644 index 0000000000..34354c4b09 --- /dev/null +++ b/packages/rate-limit-controller/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [{ "path": "../base-controller" }], + "include": ["../../types", "./src"] +} diff --git a/packages/rate-limit-controller/typedoc.json b/packages/rate-limit-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/rate-limit-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/subject-metadata-controller/CHANGELOG.md b/packages/subject-metadata-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/subject-metadata-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/subject-metadata-controller/LICENSE b/packages/subject-metadata-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/subject-metadata-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/subject-metadata-controller/README.md b/packages/subject-metadata-controller/README.md new file mode 100644 index 0000000000..b977286b06 --- /dev/null +++ b/packages/subject-metadata-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/subject-metadata-controller` + +Caches metadata associated with permission subjects. + +## Installation + +`yarn add @metamask/subject-metadata-controller` + +or + +`npm install @metamask/subject-metadata-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/subject-metadata-controller/jest.config.js b/packages/subject-metadata-controller/jest.config.js new file mode 100644 index 0000000000..a7ae2b4b47 --- /dev/null +++ b/packages/subject-metadata-controller/jest.config.js @@ -0,0 +1,25 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/subject-metadata-controller/package.json b/packages/subject-metadata-controller/package.json new file mode 100644 index 0000000000..a128681099 --- /dev/null +++ b/packages/subject-metadata-controller/package.json @@ -0,0 +1,52 @@ +{ + "name": "@metamask/subject-metadata-controller", + "version": "0.0.0", + "description": "Caches metadata associated with permission subjects", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/subject-metadata-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/subject-metadata-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "workspace:~", + "@metamask/permission-controller": "workspace:~", + "@metamask/types": "^1.1.0", + "immer": "^9.0.6" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "deepmerge": "^4.2.2", + "jest": "^26.4.2", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/subject-metadata/SubjectMetadataController.test.ts b/packages/subject-metadata-controller/src/SubjectMetadataController.test.ts similarity index 98% rename from src/subject-metadata/SubjectMetadataController.test.ts rename to packages/subject-metadata-controller/src/SubjectMetadataController.test.ts index 1760fe7508..408e5664bd 100644 --- a/src/subject-metadata/SubjectMetadataController.test.ts +++ b/packages/subject-metadata-controller/src/SubjectMetadataController.test.ts @@ -1,6 +1,5 @@ -import { Json } from '../BaseControllerV2'; -import { ControllerMessenger } from '../ControllerMessenger'; -import { HasPermissions } from '../permissions'; +import { Json, ControllerMessenger } from '@metamask/base-controller'; +import { HasPermissions } from '@metamask/permission-controller'; import { SubjectMetadataController, SubjectMetadataControllerActions, diff --git a/src/subject-metadata/SubjectMetadataController.ts b/packages/subject-metadata-controller/src/SubjectMetadataController.ts similarity index 97% rename from src/subject-metadata/SubjectMetadataController.ts rename to packages/subject-metadata-controller/src/SubjectMetadataController.ts index e01bdb7847..1ff9d19944 100644 --- a/src/subject-metadata/SubjectMetadataController.ts +++ b/packages/subject-metadata-controller/src/SubjectMetadataController.ts @@ -1,13 +1,14 @@ import type { Patch } from 'immer'; import { Json } from '@metamask/types'; -import { BaseController } from '../BaseControllerV2'; -import { RestrictedControllerMessenger } from '../ControllerMessenger'; - +import { + BaseControllerV2, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; import type { GenericPermissionController, PermissionSubjectMetadata, HasPermissions, -} from '../permissions'; +} from '@metamask/permission-controller'; const controllerName = 'SubjectMetadataController'; @@ -94,7 +95,7 @@ type SubjectMetadataControllerOptions = { * A controller for storing metadata associated with permission subjects. More * or less, a cache. */ -export class SubjectMetadataController extends BaseController< +export class SubjectMetadataController extends BaseControllerV2< typeof controllerName, SubjectMetadataControllerState, SubjectMetadataControllerMessenger diff --git a/src/subject-metadata/index.ts b/packages/subject-metadata-controller/src/index.ts similarity index 100% rename from src/subject-metadata/index.ts rename to packages/subject-metadata-controller/src/index.ts diff --git a/packages/subject-metadata-controller/tsconfig.build.json b/packages/subject-metadata-controller/tsconfig.build.json new file mode 100644 index 0000000000..7e0b87ab2f --- /dev/null +++ b/packages/subject-metadata-controller/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../permission-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/subject-metadata-controller/tsconfig.json b/packages/subject-metadata-controller/tsconfig.json new file mode 100644 index 0000000000..087b00ab75 --- /dev/null +++ b/packages/subject-metadata-controller/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../permission-controller" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/subject-metadata-controller/typedoc.json b/packages/subject-metadata-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/subject-metadata-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md new file mode 100644 index 0000000000..6afa80d26d --- /dev/null +++ b/packages/transaction-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/controllers/ diff --git a/packages/transaction-controller/LICENSE b/packages/transaction-controller/LICENSE new file mode 100644 index 0000000000..ddfbecf902 --- /dev/null +++ b/packages/transaction-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/transaction-controller/README.md b/packages/transaction-controller/README.md new file mode 100644 index 0000000000..2f43447fa7 --- /dev/null +++ b/packages/transaction-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/transaction-controller` + +Stores transactions alongside their periodically updated statuses and manages interactions such as approval and cancellation. + +## Installation + +`yarn add @metamask/transaction-controller` + +or + +`npm install @metamask/transaction-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/controllers#readme). diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js new file mode 100644 index 0000000000..43c200d268 --- /dev/null +++ b/packages/transaction-controller/jest.config.js @@ -0,0 +1,28 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const path = require('path'); +const merge = require('deepmerge'); +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 81.51, + functions: 98.86, + lines: 95.79, + statements: 95.97, + }, + }, + + // We rely on `XMLHttpRequest` to make requests + testEnvironment: 'jsdom', +}); diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json new file mode 100644 index 0000000000..22074b11c0 --- /dev/null +++ b/packages/transaction-controller/package.json @@ -0,0 +1,64 @@ +{ + "name": "@metamask/transaction-controller", + "version": "0.0.0", + "description": "Stores transactions alongside their periodically updated statuses and manages interactions such as approval and cancellation", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/controllers/tree/main/packages/transaction-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/controllers/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/controllers.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/transaction-controller", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@ethereumjs/common": "^2.3.1", + "@ethereumjs/tx": "^3.2.1", + "@metamask/base-controller": "workspace:~", + "@metamask/controller-utils": "workspace:~", + "@metamask/network-controller": "workspace:~", + "async-mutex": "^0.2.6", + "babel-runtime": "^6.26.0", + "eth-method-registry": "1.1.0", + "eth-query": "^2.1.2", + "eth-rpc-errors": "^4.0.0", + "ethereumjs-util": "^7.0.10", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.0.0", + "@types/jest": "^26.0.22", + "@types/node": "^14.14.31", + "deepmerge": "^4.2.2", + "ethjs-provider-http": "^0.1.6", + "isomorphic-fetch": "^3.0.0", + "jest": "^26.4.2", + "sinon": "^9.2.4", + "ts-jest": "^26.5.2", + "typedoc": "^0.22.15", + "typedoc-plugin-missing-exports": "^0.22.6", + "typescript": "~4.6.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/transaction/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts similarity index 96% rename from src/transaction/TransactionController.test.ts rename to packages/transaction-controller/src/TransactionController.test.ts index 27e2eb5efc..3901ceb506 100644 --- a/src/transaction/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -1,11 +1,8 @@ -import sinon from 'sinon'; +import * as sinon from 'sinon'; import HttpProvider from 'ethjs-provider-http'; -import { - NetworksChainId, - NetworkType, - NetworkState, -} from '../network/NetworkController'; -import { ESTIMATE_GAS_ERROR } from '../constants'; +import { NetworksChainId, NetworkType } from '@metamask/controller-utils'; +import type { NetworkState } from '@metamask/network-controller'; +import { ESTIMATE_GAS_ERROR } from './utils'; import { TransactionController, TransactionStatus, @@ -20,7 +17,12 @@ import { txsInStateWithOutdatedStatusAndGasDataMock, } from './mocks/txsMock'; -const globalAny: any = global; +jest.mock('uuid', () => { + return { + ...jest.requireActual('uuid'), + v1: () => '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d', + }; +}); const mockFlags: { [key: string]: any } = { estimateGasError: null, @@ -96,28 +98,27 @@ jest.mock('eth-query', () => * @param data - The mock data to return. * @returns The mock `fetch` implementation. */ -function mockFetch(data: any) { - return jest.fn().mockImplementation(() => - Promise.resolve({ - json: () => data, - ok: true, - }), - ); +function mockFetchWithStaticResponse(data: any) { + return jest + .spyOn(global, 'fetch') + .mockImplementation(() => + Promise.resolve(new Response(JSON.stringify(data))), + ); } /** - * Create a mock implementation of `fetch` that returns different mock data for each URL. + * Mocks the global `fetch` to return the different mock data for each URL + * requested. * - * @param data - A map of mock data, keyed by URL. + * @param dataForUrl - A map of mock data, keyed by URL. * @returns The mock `fetch` implementation. */ -function mockFetchs(data: any) { - return jest.fn().mockImplementation((key) => - Promise.resolve({ - json: () => data[key], - ok: true, - }), - ); +function mockFetchWithDynamicResponse(dataForUrl: any) { + return jest + .spyOn(global, 'fetch') + .mockImplementation((key) => + Promise.resolve(new Response(JSON.stringify(dataForUrl[key.toString()]))), + ); } const MOCK_PRFERENCES = { state: { selectedAddress: 'foo' } }; @@ -960,7 +961,7 @@ describe('TransactionController', () => { }); it('should fetch all the transactions from an address, including incoming transactions, in ropsten', async () => { - globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); + mockFetchWithDynamicResponse(MOCK_FETCH_TX_HISTORY_DATA_OK); const controller = new TransactionController({ getNetworkState: () => MOCK_NETWORK.state, onNetworkStateChange: MOCK_NETWORK.subscribe, @@ -977,7 +978,7 @@ describe('TransactionController', () => { }); it('should fetch all the transactions from an address, including incoming token transactions, in mainnet', async () => { - globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); + mockFetchWithDynamicResponse(MOCK_FETCH_TX_HISTORY_DATA_OK); const controller = new TransactionController({ getNetworkState: () => MOCK_MAINNET_NETWORK.state, onNetworkStateChange: MOCK_MAINNET_NETWORK.subscribe, @@ -994,7 +995,7 @@ describe('TransactionController', () => { }); it('should fetch all the transactions from an address, including incoming token transactions without modifying transactions that have the same data in local and remote', async () => { - globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); + mockFetchWithDynamicResponse(MOCK_FETCH_TX_HISTORY_DATA_OK); const controller = new TransactionController({ getNetworkState: () => MOCK_MAINNET_NETWORK.state, onNetworkStateChange: MOCK_MAINNET_NETWORK.subscribe, @@ -1016,7 +1017,7 @@ describe('TransactionController', () => { }); it('should fetch all the transactions from an address, including incoming transactions, in mainnet from block', async () => { - globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); + mockFetchWithDynamicResponse(MOCK_FETCH_TX_HISTORY_DATA_OK); const controller = new TransactionController({ getNetworkState: () => MOCK_MAINNET_NETWORK.state, onNetworkStateChange: MOCK_MAINNET_NETWORK.subscribe, @@ -1033,7 +1034,7 @@ describe('TransactionController', () => { }); it('should fetch and updated all transactions with outdated status regarding the data provided by the remote source in mainnet', async () => { - globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); + mockFetchWithDynamicResponse(MOCK_FETCH_TX_HISTORY_DATA_OK); const controller = new TransactionController({ getNetworkState: () => MOCK_MAINNET_NETWORK.state, onNetworkStateChange: MOCK_MAINNET_NETWORK.subscribe, @@ -1059,7 +1060,7 @@ describe('TransactionController', () => { }); it('should fetch and updated all transactions with outdated gas data regarding the data provided by the remote source in mainnet', async () => { - globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); + mockFetchWithDynamicResponse(MOCK_FETCH_TX_HISTORY_DATA_OK); const controller = new TransactionController({ getNetworkState: () => MOCK_MAINNET_NETWORK.state, onNetworkStateChange: MOCK_MAINNET_NETWORK.subscribe, @@ -1086,7 +1087,7 @@ describe('TransactionController', () => { }); it('should fetch and updated all transactions with outdated status and gas data regarding the data provided by the remote source in mainnet', async () => { - globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); + mockFetchWithDynamicResponse(MOCK_FETCH_TX_HISTORY_DATA_OK); const controller = new TransactionController({ getNetworkState: () => MOCK_MAINNET_NETWORK.state, onNetworkStateChange: MOCK_MAINNET_NETWORK.subscribe, @@ -1115,7 +1116,7 @@ describe('TransactionController', () => { }); it('should return', async () => { - globalAny.fetch = mockFetch(MOCK_FETCH_TX_HISTORY_DATA_ERROR); + mockFetchWithStaticResponse(MOCK_FETCH_TX_HISTORY_DATA_ERROR); const controller = new TransactionController({ getNetworkState: () => MOCK_NETWORK.state, onNetworkStateChange: MOCK_NETWORK.subscribe, @@ -1237,7 +1238,7 @@ describe('TransactionController', () => { it('should limit tx state to a length of 2', async () => { await new Promise(async (resolve) => { - globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); + mockFetchWithDynamicResponse(MOCK_FETCH_TX_HISTORY_DATA_OK); const controller = new TransactionController( { getNetworkState: () => MOCK_NETWORK.state, diff --git a/src/transaction/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts similarity index 99% rename from src/transaction/TransactionController.ts rename to packages/transaction-controller/src/TransactionController.ts index ffc2712960..30aa451a50 100644 --- a/src/transaction/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -7,29 +7,37 @@ import Common from '@ethereumjs/common'; import { TransactionFactory, TypedTransaction } from '@ethereumjs/tx'; import { v1 as random } from 'uuid'; import { Mutex } from 'async-mutex'; -import { BaseController, BaseConfig, BaseState } from '../BaseController'; +import { + BaseController, + BaseConfig, + BaseState, +} from '@metamask/base-controller'; import type { NetworkState, NetworkController, -} from '../network/NetworkController'; +} from '@metamask/network-controller'; import { BNToHex, fractionBN, hexToBN, - normalizeTransaction, safelyExecute, - validateTransaction, isSmartContractCode, - handleTransactionFetch, query, + MAINNET, + RPC, +} from '@metamask/controller-utils'; +import { + normalizeTransaction, + validateTransaction, + handleTransactionFetch, getIncreasedPriceFromExisting, isEIP1559Transaction, isGasPriceValue, isFeeMarketEIP1559Values, validateGasValues, validateMinimumIncrease, -} from '../util'; -import { ESTIMATE_GAS_ERROR, MAINNET, RPC } from '../constants'; + ESTIMATE_GAS_ERROR, +} from './utils'; const HARDFORK = 'london'; @@ -264,7 +272,7 @@ export class TransactionController extends BaseController< private registry: any; - private handle?: NodeJS.Timer; + private handle?: ReturnType; private mutex = new Mutex(); diff --git a/packages/transaction-controller/src/index.ts b/packages/transaction-controller/src/index.ts new file mode 100644 index 0000000000..9ecbc1c43a --- /dev/null +++ b/packages/transaction-controller/src/index.ts @@ -0,0 +1 @@ +export * from './TransactionController'; diff --git a/src/transaction/mocks/txsMock.ts b/packages/transaction-controller/src/mocks/txsMock.ts similarity index 100% rename from src/transaction/mocks/txsMock.ts rename to packages/transaction-controller/src/mocks/txsMock.ts diff --git a/packages/transaction-controller/src/utils.test.ts b/packages/transaction-controller/src/utils.test.ts new file mode 100644 index 0000000000..f1404784c5 --- /dev/null +++ b/packages/transaction-controller/src/utils.test.ts @@ -0,0 +1,284 @@ +import { + Transaction, + GasPriceValue, + FeeMarketEIP1559Values, +} from './TransactionController'; +import * as util from './utils'; + +const MAX_FEE_PER_GAS = 'maxFeePerGas'; +const MAX_PRIORITY_FEE_PER_GAS = 'maxPriorityFeePerGas'; +const GAS_PRICE = 'gasPrice'; +const FAIL = 'lol'; +const PASS = '0x1'; + +describe('utils', () => { + describe('getEtherscanApiUrl', () => { + const networkType = 'mainnet'; + const address = '0xC7D3BFDeA106B446Cf9f2Db354D496e6Dd8b2525'; + const action = 'txlist'; + + it('should return a correctly structured url', () => { + const url = util.getEtherscanApiUrl(networkType, { address, action }); + expect(url.indexOf(`&action=${action}`)).toBeGreaterThan(0); + }); + + it('should return a correctly structured url with from block', () => { + const fromBlock = 'xxxxxx'; + const url = util.getEtherscanApiUrl(networkType, { + address, + action, + startBlock: fromBlock, + }); + expect(url.indexOf(`&startBlock=${fromBlock}`)).toBeGreaterThan(0); + }); + + it('should return a correctly structured url with testnet subdomain', () => { + const ropsten = 'ropsten'; + const url = util.getEtherscanApiUrl(ropsten, { address, action }); + expect(url.indexOf(`https://api-${ropsten}`)).toBe(0); + }); + + it('should return a correctly structured url with apiKey', () => { + const apiKey = 'xxxxxx'; + const url = util.getEtherscanApiUrl(networkType, { + address, + action, + startBlock: 'xxxxxx', + apikey: apiKey, + }); + expect(url.indexOf(`&apikey=${apiKey}`)).toBeGreaterThan(0); + }); + }); + + it('normalizeTransaction', () => { + const normalized = util.normalizeTransaction({ + data: 'data', + from: 'FROM', + gas: 'gas', + gasPrice: 'gasPrice', + nonce: 'nonce', + to: 'TO', + value: 'value', + maxFeePerGas: 'maxFeePerGas', + maxPriorityFeePerGas: 'maxPriorityFeePerGas', + estimatedBaseFee: 'estimatedBaseFee', + }); + expect(normalized).toStrictEqual({ + data: '0xdata', + from: '0xfrom', + gas: '0xgas', + gasPrice: '0xgasPrice', + nonce: '0xnonce', + to: '0xto', + value: '0xvalue', + maxFeePerGas: '0xmaxFeePerGas', + maxPriorityFeePerGas: '0xmaxPriorityFeePerGas', + estimatedBaseFee: '0xestimatedBaseFee', + }); + }); + + describe('validateTransaction', () => { + it('should throw if no from address', () => { + expect(() => util.validateTransaction({} as any)).toThrow( + 'Invalid "from" address: undefined must be a valid string.', + ); + }); + + it('should throw if non-string from address', () => { + expect(() => util.validateTransaction({ from: 1337 } as any)).toThrow( + 'Invalid "from" address: 1337 must be a valid string.', + ); + }); + + it('should throw if invalid from address', () => { + expect(() => util.validateTransaction({ from: '1337' } as any)).toThrow( + 'Invalid "from" address: 1337 must be a valid string.', + ); + }); + + it('should throw if no data', () => { + expect(() => + util.validateTransaction({ + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + to: '0x', + } as any), + ).toThrow('Invalid "to" address: 0x must be a valid string.'); + + expect(() => + util.validateTransaction({ + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + } as any), + ).toThrow('Invalid "to" address: undefined must be a valid string.'); + }); + + it('should delete data', () => { + const transaction = { + data: 'foo', + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + to: '0x', + }; + util.validateTransaction(transaction); + expect(transaction.to).toBeUndefined(); + }); + + it('should throw if invalid to address', () => { + expect(() => + util.validateTransaction({ + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + to: '1337', + } as any), + ).toThrow('Invalid "to" address: 1337 must be a valid string.'); + }); + + it('should throw if value is invalid', () => { + expect(() => + util.validateTransaction({ + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + to: '0x3244e191f1b4903970224322180f1fbbc415696b', + value: '133-7', + } as any), + ).toThrow('Invalid "value": 133-7 is not a positive number.'); + + expect(() => + util.validateTransaction({ + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + to: '0x3244e191f1b4903970224322180f1fbbc415696b', + value: '133.7', + } as any), + ).toThrow('Invalid "value": 133.7 number must be denominated in wei.'); + + expect(() => + util.validateTransaction({ + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + to: '0x3244e191f1b4903970224322180f1fbbc415696b', + value: 'hello', + } as any), + ).toThrow('Invalid "value": hello number must be a valid number.'); + + expect(() => + util.validateTransaction({ + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + to: '0x3244e191f1b4903970224322180f1fbbc415696b', + value: 'one million dollar$', + } as any), + ).toThrow( + 'Invalid "value": one million dollar$ number must be a valid number.', + ); + + expect(() => + util.validateTransaction({ + from: '0x3244e191f1b4903970224322180f1fbbc415696b', + to: '0x3244e191f1b4903970224322180f1fbbc415696b', + value: '1', + } as any), + ).not.toThrow(); + }); + }); + + describe('isEIP1559Transaction', () => { + it('should detect EIP1559 transaction', () => { + const tx: Transaction = { from: '' }; + const eip1559tx: Transaction = { + ...tx, + maxFeePerGas: '2', + maxPriorityFeePerGas: '3', + }; + expect(util.isEIP1559Transaction(eip1559tx)).toBe(true); + expect(util.isEIP1559Transaction(tx)).toBe(false); + }); + }); + + describe('validateGasValues', () => { + it('should throw when provided invalid gas values', () => { + const gasValues: GasPriceValue = { + [GAS_PRICE]: FAIL, + }; + expect(() => util.validateGasValues(gasValues)).toThrow(TypeError); + expect(() => util.validateGasValues(gasValues)).toThrow( + `expected hex string for ${GAS_PRICE} but received: ${FAIL}`, + ); + }); + + it('should throw when any provided gas values are invalid', () => { + const gasValues: FeeMarketEIP1559Values = { + [MAX_PRIORITY_FEE_PER_GAS]: PASS, + [MAX_FEE_PER_GAS]: FAIL, + }; + expect(() => util.validateGasValues(gasValues)).toThrow(TypeError); + expect(() => util.validateGasValues(gasValues)).toThrow( + `expected hex string for ${MAX_FEE_PER_GAS} but received: ${FAIL}`, + ); + }); + + it('should return true when provided valid gas values', () => { + const gasValues: FeeMarketEIP1559Values = { + [MAX_FEE_PER_GAS]: PASS, + [MAX_PRIORITY_FEE_PER_GAS]: PASS, + }; + expect(() => util.validateGasValues(gasValues)).not.toThrow(TypeError); + }); + }); + + describe('isFeeMarketEIP1559Values', () => { + it('should detect if isFeeMarketEIP1559Values', () => { + const gasValues = { + [MAX_PRIORITY_FEE_PER_GAS]: PASS, + [MAX_FEE_PER_GAS]: FAIL, + }; + expect(util.isFeeMarketEIP1559Values(gasValues)).toBe(true); + expect(util.isGasPriceValue(gasValues)).toBe(false); + }); + }); + + describe('isGasPriceValue', () => { + it('should detect if isGasPriceValue', () => { + const gasValues: GasPriceValue = { + [GAS_PRICE]: PASS, + }; + expect(util.isGasPriceValue(gasValues)).toBe(true); + expect(util.isFeeMarketEIP1559Values(gasValues)).toBe(false); + }); + }); + + describe('getIncreasedPriceHex', () => { + it('should get increased price from number as hex', () => { + expect(util.getIncreasedPriceHex(1358778842, 1.1)).toStrictEqual( + '0x5916a6d6', + ); + }); + }); + + describe('getIncreasedPriceFromExisting', () => { + it('should get increased price from hex as hex', () => { + expect( + util.getIncreasedPriceFromExisting('0x50fd51da', 1.1), + ).toStrictEqual('0x5916a6d6'); + }); + }); + + describe('validateMinimumIncrease', () => { + it('should throw if increase does not meet minimum requirement', () => { + expect(() => + util.validateMinimumIncrease('0x50fd51da', '0x5916a6d6'), + ).toThrow(Error); + + expect(() => + util.validateMinimumIncrease('0x50fd51da', '0x5916a6d6'), + ).toThrow( + 'The proposed value: 1358778842 should meet or exceed the minimum value: 1494656726', + ); + }); + + it('should not throw if increase meets minimum requirement', () => { + expect(() => + util.validateMinimumIncrease('0x5916a6d6', '0x5916a6d6'), + ).not.toThrow(Error); + }); + + it('should not throw if increase exceeds minimum requirement', () => { + expect(() => + util.validateMinimumIncrease('0x7162a5ca', '0x5916a6d6'), + ).not.toThrow(Error); + }); + }); +}); diff --git a/packages/transaction-controller/src/utils.ts b/packages/transaction-controller/src/utils.ts new file mode 100644 index 0000000000..bf79199f74 --- /dev/null +++ b/packages/transaction-controller/src/utils.ts @@ -0,0 +1,263 @@ +import { addHexPrefix, isHexString } from 'ethereumjs-util'; +import { + MAINNET, + convertHexToDecimal, + handleFetch, + isValidHexAddress, +} from '@metamask/controller-utils'; +import { + Transaction, + FetchAllOptions, + GasPriceValue, + FeeMarketEIP1559Values, +} from './TransactionController'; + +export const ESTIMATE_GAS_ERROR = 'eth_estimateGas rpc method error'; + +const NORMALIZERS: { [param in keyof Transaction]: any } = { + data: (data: string) => addHexPrefix(data), + from: (from: string) => addHexPrefix(from).toLowerCase(), + gas: (gas: string) => addHexPrefix(gas), + gasPrice: (gasPrice: string) => addHexPrefix(gasPrice), + nonce: (nonce: string) => addHexPrefix(nonce), + to: (to: string) => addHexPrefix(to).toLowerCase(), + value: (value: string) => addHexPrefix(value), + maxFeePerGas: (maxFeePerGas: string) => addHexPrefix(maxFeePerGas), + maxPriorityFeePerGas: (maxPriorityFeePerGas: string) => + addHexPrefix(maxPriorityFeePerGas), + estimatedBaseFee: (maxPriorityFeePerGas: string) => + addHexPrefix(maxPriorityFeePerGas), +}; + +/** + * Return a URL that can be used to fetch ETH transactions. + * + * @param networkType - Network type of desired network. + * @param urlParams - The parameters used to construct the URL. + * @returns URL to fetch the access the endpoint. + */ +export function getEtherscanApiUrl( + networkType: string, + urlParams: any, +): string { + let etherscanSubdomain = 'api'; + if (networkType !== MAINNET) { + etherscanSubdomain = `api-${networkType}`; + } + const apiUrl = `https://${etherscanSubdomain}.etherscan.io`; + let url = `${apiUrl}/api?`; + + for (const paramKey in urlParams) { + if (urlParams[paramKey]) { + url += `${paramKey}=${urlParams[paramKey]}&`; + } + } + url += 'tag=latest&page=1'; + return url; +} + +/** + * Normalizes properties on a Transaction object. + * + * @param transaction - Transaction object to normalize. + * @returns Normalized Transaction object. + */ +export function normalizeTransaction(transaction: Transaction) { + const normalizedTransaction: Transaction = { from: '' }; + let key: keyof Transaction; + for (key in NORMALIZERS) { + if (transaction[key as keyof Transaction]) { + normalizedTransaction[key] = NORMALIZERS[key](transaction[key]) as never; + } + } + return normalizedTransaction; +} + +/** + * Validates a Transaction object for required properties and throws in + * the event of any validation error. + * + * @param transaction - Transaction object to validate. + */ +export function validateTransaction(transaction: Transaction) { + if ( + !transaction.from || + typeof transaction.from !== 'string' || + !isValidHexAddress(transaction.from) + ) { + throw new Error( + `Invalid "from" address: ${transaction.from} must be a valid string.`, + ); + } + + if (transaction.to === '0x' || transaction.to === undefined) { + if (transaction.data) { + delete transaction.to; + } else { + throw new Error( + `Invalid "to" address: ${transaction.to} must be a valid string.`, + ); + } + } else if ( + transaction.to !== undefined && + !isValidHexAddress(transaction.to) + ) { + throw new Error( + `Invalid "to" address: ${transaction.to} must be a valid string.`, + ); + } + + if (transaction.value !== undefined) { + const value = transaction.value.toString(); + if (value.includes('-')) { + throw new Error(`Invalid "value": ${value} is not a positive number.`); + } + + if (value.includes('.')) { + throw new Error( + `Invalid "value": ${value} number must be denominated in wei.`, + ); + } + const intValue = parseInt(transaction.value, 10); + const isValid = + Number.isFinite(intValue) && + !Number.isNaN(intValue) && + !isNaN(Number(value)) && + Number.isSafeInteger(intValue); + if (!isValid) { + throw new Error( + `Invalid "value": ${value} number must be a valid number.`, + ); + } + } +} + +/** + * Checks if a transaction is EIP-1559 by checking for the existence of + * maxFeePerGas and maxPriorityFeePerGas within its parameters. + * + * @param transaction - Transaction object to add. + * @returns Boolean that is true if the transaction is EIP-1559 (has maxFeePerGas and maxPriorityFeePerGas), otherwise returns false. + */ +export const isEIP1559Transaction = (transaction: Transaction): boolean => { + const hasOwnProp = (obj: Transaction, key: string) => + Object.prototype.hasOwnProperty.call(obj, key); + return ( + hasOwnProp(transaction, 'maxFeePerGas') && + hasOwnProp(transaction, 'maxPriorityFeePerGas') + ); +}; + +/** + * Handles the fetch of incoming transactions. + * + * @param networkType - Network type of desired network. + * @param address - Address to get the transactions from. + * @param txHistoryLimit - The maximum number of transactions to fetch. + * @param opt - Object that can contain fromBlock and Etherscan service API key. + * @returns Responses for both ETH and ERC20 token transactions. + */ +export async function handleTransactionFetch( + networkType: string, + address: string, + txHistoryLimit: number, + opt?: FetchAllOptions, +): Promise<[{ [result: string]: [] }, { [result: string]: [] }]> { + // transactions + const urlParams = { + module: 'account', + address, + startBlock: opt?.fromBlock, + apikey: opt?.etherscanApiKey, + offset: txHistoryLimit.toString(), + order: 'desc', + }; + const etherscanTxUrl = getEtherscanApiUrl(networkType, { + ...urlParams, + action: 'txlist', + }); + const etherscanTxResponsePromise = handleFetch(etherscanTxUrl); + + // tokens + const etherscanTokenUrl = getEtherscanApiUrl(networkType, { + ...urlParams, + action: 'tokentx', + }); + const etherscanTokenResponsePromise = handleFetch(etherscanTokenUrl); + + let [etherscanTxResponse, etherscanTokenResponse] = await Promise.all([ + etherscanTxResponsePromise, + etherscanTokenResponsePromise, + ]); + + if ( + etherscanTxResponse.status === '0' || + etherscanTxResponse.result.length <= 0 + ) { + etherscanTxResponse = { status: etherscanTxResponse.status, result: [] }; + } + + if ( + etherscanTokenResponse.status === '0' || + etherscanTokenResponse.result.length <= 0 + ) { + etherscanTokenResponse = { + status: etherscanTokenResponse.status, + result: [], + }; + } + + return [etherscanTxResponse, etherscanTokenResponse]; +} + +export const validateGasValues = ( + gasValues: GasPriceValue | FeeMarketEIP1559Values, +) => { + Object.keys(gasValues).forEach((key) => { + const value = (gasValues as any)[key]; + if (typeof value !== 'string' || !isHexString(value)) { + throw new TypeError( + `expected hex string for ${key} but received: ${value}`, + ); + } + }); +}; + +export const isFeeMarketEIP1559Values = ( + gasValues?: GasPriceValue | FeeMarketEIP1559Values, +): gasValues is FeeMarketEIP1559Values => + (gasValues as FeeMarketEIP1559Values)?.maxFeePerGas !== undefined || + (gasValues as FeeMarketEIP1559Values)?.maxPriorityFeePerGas !== undefined; + +export const isGasPriceValue = ( + gasValues?: GasPriceValue | FeeMarketEIP1559Values, +): gasValues is GasPriceValue => + (gasValues as GasPriceValue)?.gasPrice !== undefined; + +export const getIncreasedPriceHex = (value: number, rate: number): string => + addHexPrefix(`${parseInt(`${value * rate}`, 10).toString(16)}`); + +export const getIncreasedPriceFromExisting = ( + value: string | undefined, + rate: number, +): string => { + return getIncreasedPriceHex(convertHexToDecimal(value), rate); +}; + +/** + * Validates that the proposed value is greater than or equal to the minimum value. + * + * @param proposed - The proposed value. + * @param min - The minimum value. + * @returns The proposed value. + * @throws Will throw if the proposed value is too low. + */ +export function validateMinimumIncrease(proposed: string, min: string) { + const proposedDecimal = convertHexToDecimal(proposed); + const minDecimal = convertHexToDecimal(min); + if (proposedDecimal >= minDecimal) { + return proposed; + } + const errorMsg = `The proposed value: ${proposedDecimal} should meet or exceed the minimum value: ${minDecimal}`; + throw new Error(errorMsg); +} diff --git a/packages/transaction-controller/tsconfig.build.json b/packages/transaction-controller/tsconfig.build.json new file mode 100644 index 0000000000..ac0df4920c --- /dev/null +++ b/packages/transaction-controller/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/transaction-controller/tsconfig.json b/packages/transaction-controller/tsconfig.json new file mode 100644 index 0000000000..4bbb0be81b --- /dev/null +++ b/packages/transaction-controller/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" }, + { "path": "../network-controller" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/transaction-controller/typedoc.json b/packages/transaction-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/transaction-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/release.config.json b/release.config.json new file mode 100644 index 0000000000..9a672ef126 --- /dev/null +++ b/release.config.json @@ -0,0 +1,3 @@ +{ + "versioningStrategy": "independent" +} diff --git a/scripts/validate-changelog.sh b/scripts/validate-changelog.sh new file mode 100755 index 0000000000..7d5f0c11b8 --- /dev/null +++ b/scripts/validate-changelog.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -eq 0 ]]; then + echo "Missing package name." + exit 1 +fi + +package_name="$1" + +if [[ "${GITHUB_REF:-}" =~ '^release/' ]]; then + yarn auto-changelog validate --tag-prefix "${package_name}@" --rc +else + yarn auto-changelog validate --tag-prefix "${package_name}@" +fi diff --git a/src/approval/ApprovalController.test.js b/src/approval/ApprovalController.test.js deleted file mode 100644 index 6120c41281..0000000000 --- a/src/approval/ApprovalController.test.js +++ /dev/null @@ -1,282 +0,0 @@ -const { errorCodes } = require('eth-rpc-errors'); -const { ControllerMessenger } = require('../ControllerMessenger'); -const { ApprovalController } = require('./ApprovalController'); - -/** - * Constructs a restricted controller messenger. - * - * @returns {object} A restricted controller messenger. - */ -function getRestrictedMessenger() { - const controllerMessenger = new ControllerMessenger(); - const messenger = controllerMessenger.getRestricted({ - name: 'ApprovalController', - }); - return messenger; -} - -const getApprovalController = () => - new ApprovalController({ - messenger: getRestrictedMessenger(), - showApprovalRequest: () => undefined, - }); - -const STORE_KEY = 'pendingApprovals'; - -describe('ApprovalController: Input Validation', () => { - describe('add', () => { - it('validates input', () => { - const approvalController = getApprovalController(); - - expect(() => - approvalController.add({ id: null, origin: 'bar.baz' }), - ).toThrow(getInvalidIdError()); - - expect(() => approvalController.add({ id: 'foo' })).toThrow( - getInvalidOriginError(), - ); - - expect(() => approvalController.add({ id: 'foo', origin: true })).toThrow( - getInvalidOriginError(), - ); - - expect(() => - approvalController.add({ id: 'foo', origin: 'bar.baz', type: {} }), - ).toThrow(getInvalidTypeError(errorCodes.rpc.internal)); - - expect(() => - approvalController.add({ id: 'foo', origin: 'bar.baz', type: '' }), - ).toThrow(getInvalidTypeError(errorCodes.rpc.internal)); - - expect(() => - approvalController.add({ - id: 'foo', - origin: 'bar.baz', - type: 'type', - requestData: 'foo', - }), - ).toThrow(getInvalidRequestDataError()); - }); - }); - - describe('get', () => { - it('returns undefined for non-existing entry', () => { - const approvalController = getApprovalController(); - - approvalController.add({ id: 'foo', origin: 'bar.baz', type: 'type' }); - - expect(approvalController.get('fizz')).toBeUndefined(); - - expect(approvalController.get()).toBeUndefined(); - - expect(approvalController.get({})).toBeUndefined(); - }); - }); - - describe('getApprovalCount', () => { - it('validates input', () => { - const approvalController = getApprovalController(); - - expect(() => approvalController.getApprovalCount()).toThrow( - getApprovalCountParamsError(), - ); - - expect(() => approvalController.getApprovalCount({})).toThrow( - getApprovalCountParamsError(), - ); - - expect(() => - approvalController.getApprovalCount({ origin: null }), - ).toThrow(getApprovalCountParamsError()); - - expect(() => - approvalController.getApprovalCount({ type: false }), - ).toThrow(getApprovalCountParamsError()); - }); - }); - - describe('has', () => { - it('validates input', () => { - const approvalController = getApprovalController(); - - expect(() => approvalController.has()).toThrow( - getInvalidHasParamsError(), - ); - - expect(() => approvalController.has({})).toThrow( - getInvalidHasParamsError(), - ); - - expect(() => approvalController.has({ id: true })).toThrow( - getInvalidHasIdError(), - ); - - expect(() => approvalController.has({ origin: true })).toThrow( - getInvalidHasOriginError(), - ); - - expect(() => approvalController.has({ type: true })).toThrow( - getInvalidHasTypeError(), - ); - - expect(() => - approvalController.has({ origin: 'foo', type: true }), - ).toThrow(getInvalidHasTypeError()); - }); - }); - - // We test this internal function before resolve, reject, and clear because - // they are heavily dependent upon it. - describe('_delete', () => { - let approvalController; - - beforeEach(() => { - approvalController = getApprovalController(); - }); - - it('deletes entry', () => { - approvalController.add({ id: 'foo', origin: 'bar.baz', type: 'type' }); - - approvalController._delete('foo'); - - expect( - !approvalController.has({ id: 'foo' }) && - !approvalController.has({ type: 'type' }) && - !approvalController.has({ origin: 'bar.baz' }) && - !approvalController.state[STORE_KEY].foo, - ).toStrictEqual(true); - }); - - it('deletes one entry out of many without side-effects', () => { - approvalController.add({ id: 'foo', origin: 'bar.baz', type: 'type1' }); - approvalController.add({ id: 'fizz', origin: 'bar.baz', type: 'type2' }); - - approvalController._delete('fizz'); - - expect( - !approvalController.has({ id: 'fizz' }) && - !approvalController.has({ origin: 'bar.baz', type: 'type2' }), - ).toStrictEqual(true); - - expect( - approvalController.has({ id: 'foo' }) && - approvalController.has({ origin: 'bar.baz' }), - ).toStrictEqual(true); - }); - }); - - describe('miscellaneous', () => { - it('isEmptyOrigin: handles non-existing origin', () => { - const approvalController = getApprovalController(); - expect(() => approvalController._isEmptyOrigin('kaplar')).not.toThrow(); - }); - }); -}); - -// helpers - -/** - * Get an invalid ID error. - * - * @returns {Error} An invalid ID error. - */ -function getInvalidIdError() { - return getError('Must specify non-empty string id.', errorCodes.rpc.internal); -} - -/** - * Get an invalid ID type error. - * - * @returns {Error} An invalid ID type error. - */ -function getInvalidHasIdError() { - return getError('May not specify non-string id.'); -} - -/** - * Get an invalid origin type error. - * - * @returns {Error} The invalid origin type error. - */ -function getInvalidHasOriginError() { - return getError('May not specify non-string origin.'); -} - -/** - * Get an invalid type error. - * - * @returns {Error} The invalid type error. - */ -function getInvalidHasTypeError() { - return getError('May not specify non-string type.'); -} - -/** - * Get an invalid origin error. - * - * @returns {Error} The invalid origin error. - */ -function getInvalidOriginError() { - return getError( - 'Must specify non-empty string origin.', - errorCodes.rpc.internal, - ); -} - -/** - * Get an invalid request data error. - * - * @returns {Error} The invalid request data error. - */ -function getInvalidRequestDataError() { - return getError( - 'Request data must be a plain object if specified.', - errorCodes.rpc.internal, - ); -} - -/** - * Get an invalid type error. - * - * @param {number} code - The error code. - * @returns {Error} The invalid type error. - */ -function getInvalidTypeError(code) { - return getError('Must specify non-empty string type.', code); -} - -/** - * Get an invalid params error. - * - * @returns {Error} The invalid params error. - */ -function getInvalidHasParamsError() { - return getError('Must specify a valid combination of id, origin, and type.'); -} - -/** - * Get an invalid approval count params error. - * - * @returns {Error} The invalid approval count params error. - */ -function getApprovalCountParamsError() { - return getError('Must specify origin, type, or both.'); -} - -/** - * Get an Error. - * - * @param {string} message - The error message. - * @param {number} code - The error code. - * @returns {Error} An Error. - */ -function getError(message, code) { - const err = { - name: 'Error', - message, - }; - if (code !== undefined) { - err.code = code; - } - return err; -} diff --git a/src/assets/assetsUtil.test.ts b/src/assets/assetsUtil.test.ts deleted file mode 100644 index a67925867b..0000000000 --- a/src/assets/assetsUtil.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { NetworksChainId } from '../network/NetworkController'; -import * as assetsUtil from './assetsUtil'; -import { Nft, NftMetadata } from './NftController'; - -describe('assetsUtil', () => { - describe('compareNftMetadata', () => { - it('should resolve true if any key is different', () => { - const nftMetadata: NftMetadata = { - name: 'name', - image: 'image', - description: 'description', - standard: 'standard', - backgroundColor: 'backgroundColor', - imagePreview: 'imagePreview', - imageThumbnail: 'imageThumbnail', - imageOriginal: 'imageOriginal', - animation: 'animation', - animationOriginal: 'animationOriginal', - externalLink: 'externalLink-123', - }; - const nft: Nft = { - address: 'address', - tokenId: '123', - name: 'name', - image: 'image', - description: 'description', - standard: 'standard', - backgroundColor: 'backgroundColor', - imagePreview: 'imagePreview', - imageThumbnail: 'imageThumbnail', - imageOriginal: 'imageOriginal', - animation: 'animation', - animationOriginal: 'animationOriginal', - externalLink: 'externalLink', - }; - const different = assetsUtil.compareNftMetadata(nftMetadata, nft); - expect(different).toStrictEqual(true); - }); - - it('should resolve true if any key is different as always as metadata is not undefined', () => { - const nftMetadata: NftMetadata = { - name: 'name', - image: 'image', - description: 'description', - standard: 'standard', - externalLink: 'externalLink', - }; - const nft: Nft = { - address: 'address', - tokenId: '123', - name: 'name', - image: 'image', - standard: 'standard', - description: 'description', - backgroundColor: 'backgroundColor', - externalLink: 'externalLink', - }; - const different = assetsUtil.compareNftMetadata(nftMetadata, nft); - expect(different).toStrictEqual(false); - }); - - it('should resolve false if no key is different', () => { - const nftMetadata: NftMetadata = { - name: 'name', - image: 'image', - description: 'description', - standard: 'standard', - backgroundColor: 'backgroundColor', - imagePreview: 'imagePreview', - imageThumbnail: 'imageThumbnail', - imageOriginal: 'imageOriginal', - animation: 'animation', - animationOriginal: 'animationOriginal', - externalLink: 'externalLink', - }; - const nft: Nft = { - address: 'address', - tokenId: '123', - name: 'name', - image: 'image', - standard: 'standard', - description: 'description', - backgroundColor: 'backgroundColor', - imagePreview: 'imagePreview', - imageThumbnail: 'imageThumbnail', - imageOriginal: 'imageOriginal', - animation: 'animation', - animationOriginal: 'animationOriginal', - externalLink: 'externalLink', - }; - const different = assetsUtil.compareNftMetadata(nftMetadata, nft); - expect(different).toStrictEqual(false); - }); - - it('should format aggregator names', () => { - const formattedAggregatorNames = assetsUtil.formatAggregatorNames([ - 'bancor', - 'aave', - 'coinGecko', - ]); - const expectedValue = ['Bancor', 'Aave', 'CoinGecko']; - expect(formattedAggregatorNames).toStrictEqual(expectedValue); - }); - - it('should format icon url with Codefi proxy correctly when passed chainId as a decimal string', () => { - const linkTokenAddress = '0x514910771af9ca656af840dff83e8264ecf986ca'; - const formattedIconUrl = assetsUtil.formatIconUrlWithProxy({ - chainId: NetworksChainId.mainnet, - tokenAddress: linkTokenAddress, - }); - const expectedValue = `https://static.metaswap.codefi.network/api/v1/tokenIcons/${NetworksChainId.mainnet}/${linkTokenAddress}.png`; - expect(formattedIconUrl).toStrictEqual(expectedValue); - }); - - it('should format icon url with Codefi proxy correctly when passed chainId as a hexadecimal string', () => { - const linkTokenAddress = '0x514910771af9ca656af840dff83e8264ecf986ca'; - const formattedIconUrl = assetsUtil.formatIconUrlWithProxy({ - chainId: `0x${Number(NetworksChainId.mainnet).toString(16)}`, - tokenAddress: linkTokenAddress, - }); - const expectedValue = `https://static.metaswap.codefi.network/api/v1/tokenIcons/${NetworksChainId.mainnet}/${linkTokenAddress}.png`; - expect(formattedIconUrl).toStrictEqual(expectedValue); - }); - }); -}); diff --git a/src/assets/assetsUtil.ts b/src/assets/assetsUtil.ts deleted file mode 100644 index ee02087f62..0000000000 --- a/src/assets/assetsUtil.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { convertHexToDecimal } from '../util'; -import { Nft, NftMetadata } from './NftController'; - -/** - * Compares NFT metadata entries to any NFT entry. - * We need this method when comparing a new fetched NFT metadata, in case a entry changed to a defined value, - * there's a need to update the NFT in state. - * - * @param newNftMetadata - NFT metadata object. - * @param nft - NFT object to compare with. - * @returns Whether there are differences. - */ -export function compareNftMetadata(newNftMetadata: NftMetadata, nft: Nft) { - const keys: (keyof NftMetadata)[] = [ - 'image', - 'backgroundColor', - 'imagePreview', - 'imageThumbnail', - 'imageOriginal', - 'animation', - 'animationOriginal', - 'externalLink', - ]; - const differentValues = keys.reduce((value, key) => { - if (newNftMetadata[key] && newNftMetadata[key] !== nft[key]) { - return value + 1; - } - return value; - }, 0); - return differentValues > 0; -} - -const aggregatorNameByKey: Record = { - aave: 'Aave', - bancor: 'Bancor', - cmc: 'CMC', - cryptocom: 'Crypto.com', - coinGecko: 'CoinGecko', - oneInch: '1inch', - paraswap: 'Paraswap', - pmm: 'PMM', - zapper: 'Zapper', - zerion: 'Zerion', - zeroEx: '0x', - synthetix: 'Synthetix', - yearn: 'Yearn', - apeswap: 'ApeSwap', - binanceDex: 'BinanceDex', - pancakeTop100: 'PancakeTop100', - pancakeExtended: 'PancakeExtended', - balancer: 'Balancer', - quickswap: 'QuickSwap', - matcha: 'Matcha', - pangolinDex: 'PangolinDex', - pangolinDexStableCoin: 'PangolinDexStableCoin', - pangolinDexAvaxBridge: 'PangolinDexAvaxBridge', - traderJoe: 'TraderJoe', - airswapLight: 'AirswapLight', - kleros: 'Kleros', -}; - -/** - * Formats aggregator names to presentable format. - * - * @param aggregators - List of token list names in camelcase. - * @returns Formatted aggregator names. - */ -export const formatAggregatorNames = (aggregators: string[]) => { - return aggregators.map( - (key) => - aggregatorNameByKey[key] || - `${key[0].toUpperCase()}${key.substring(1, key.length)}`, - ); -}; - -/** - * Format token list assets to use image proxy from Codefi. - * - * @param params - Object that contains chainID and tokenAddress. - * @param params.chainId - ChainID of network in decimal or hexadecimal format. - * @param params.tokenAddress - Address of token in mixed or lowercase. - * @returns Formatted image url - */ -export const formatIconUrlWithProxy = ({ - chainId, - tokenAddress, -}: { - chainId: string; - tokenAddress: string; -}) => { - const chainIdDecimal = convertHexToDecimal(chainId).toString(); - return `https://static.metaswap.codefi.network/api/v1/tokenIcons/${chainIdDecimal}/${tokenAddress.toLowerCase()}.png`; -}; diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 816ed3b0f6..0000000000 --- a/src/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import 'isomorphic-fetch'; -import * as util from './util'; -import { formatIconUrlWithProxy } from './assets/assetsUtil'; - -export * from './assets/AccountTrackerController'; -export * from './user/AddressBookController'; -export * from './approval'; -export * from './assets/AssetsContractController'; -export * from './BaseController'; -export { - BaseController as BaseControllerV2, - getPersistentState, - getAnonymizedState, - Json, - StateDeriver, - StateMetadata, - StatePropertyMetadata, -} from './BaseControllerV2'; -export * from './ComposableController'; -export * from './ControllerMessenger'; -export * from './assets/CurrencyRateController'; -export * from './keyring/KeyringController'; -export * from './message-manager/MessageManager'; -export * from './network/NetworkController'; -export * from './third-party/PhishingController'; -export * from './user/PreferencesController'; -export * from './assets/TokenBalancesController'; -export * from './assets/TokenRatesController'; -export * from './transaction/TransactionController'; -export * from './message-manager/PersonalMessageManager'; -export * from './message-manager/TypedMessageManager'; -export * from './announcement/AnnouncementController'; -export * from './assets/TokenListController'; -export * from './gas/GasFeeController'; -export * from './assets/TokensController'; -export * from './assets/NftController'; -export * from './assets/TokenDetectionController'; -export * from './assets/NftDetectionController'; -export * from './permissions'; -export * from './subject-metadata'; -export * from './ratelimit/RateLimitController'; -export * from './notification/NotificationController'; -export { util, formatIconUrlWithProxy }; diff --git a/src/util.test.ts b/src/util.test.ts deleted file mode 100644 index fbbbc55b1d..0000000000 --- a/src/util.test.ts +++ /dev/null @@ -1,1333 +0,0 @@ -import 'isomorphic-fetch'; -import { BN } from 'ethereumjs-util'; -import nock from 'nock'; -import { GANACHE_CHAIN_ID } from './constants'; -import * as util from './util'; -import { - Transaction, - GasPriceValue, - FeeMarketEIP1559Values, -} from './transaction/TransactionController'; -import { NetworksChainId } from './network/NetworkController'; - -const VALID = '4e1fF7229BDdAf0A73DF183a88d9c3a04cc975e0'; -const SOME_API = 'https://someapi.com'; -const SOME_FAILING_API = 'https://somefailingapi.com'; - -const DEFAULT_IPFS_URL_FORMAT = 'ipfs://'; -const ALTERNATIVE_IPFS_URL_FORMAT = 'ipfs://ipfs/'; -const IPFS_CID_V0 = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n'; -const IPFS_CID_V1 = - 'bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku'; - -const IFPS_GATEWAY = 'dweb.link'; - -const MAX_FEE_PER_GAS = 'maxFeePerGas'; -const MAX_PRIORITY_FEE_PER_GAS = 'maxPriorityFeePerGas'; -const GAS_PRICE = 'gasPrice'; -const FAIL = 'lol'; -const PASS = '0x1'; - -describe('util', () => { - beforeEach(() => { - nock.cleanAll(); - }); - - it('bNToHex', () => { - expect(util.BNToHex(new BN('1337'))).toBe('0x539'); - }); - - it('fractionBN', () => { - expect(util.fractionBN(new BN('1337'), 9, 10).toNumber()).toBe(1203); - }); - - it('getBuyURL', () => { - expect(util.getBuyURL(undefined, 'foo', 1337)).toBe( - 'https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=1337&address=foo&crypto_currency=ETH', - ); - - expect(util.getBuyURL('1', 'foo', 1337)).toBe( - 'https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=1337&address=foo&crypto_currency=ETH', - ); - expect(util.getBuyURL('3')).toBe('https://faucet.metamask.io/'); - expect(util.getBuyURL('4')).toBe('https://www.rinkeby.io/'); - expect(util.getBuyURL('5')).toBe('https://goerli-faucet.slock.it/'); - expect(util.getBuyURL('42')).toBe( - 'https://github.com/kovan-testnet/faucet', - ); - expect(util.getBuyURL('unrecognized network ID')).toBeUndefined(); - }); - - it('hexToBN', () => { - expect(util.hexToBN('0x1337').toNumber()).toBe(4919); - }); - - describe('fromHex', () => { - it('converts a string that represents a number in hexadecimal format with leading "0x" into a BN', () => { - expect(util.fromHex('0x1337')).toStrictEqual(new BN(4919)); - }); - - it('converts a string that represents a number in hexadecimal format without leading "0x" into a BN', () => { - expect(util.fromHex('1337')).toStrictEqual(new BN(4919)); - }); - - it('does nothing to a BN', () => { - const bn = new BN(4919); - expect(util.fromHex(bn)).toBe(bn); - }); - }); - - describe('toHex', () => { - it('converts a BN to a hex string prepended with "0x"', () => { - expect(util.toHex(new BN(4919))).toStrictEqual('0x1337'); - }); - - it('parses a string as a number in decimal format and converts it to a hex string prepended with "0x"', () => { - expect(util.toHex('4919')).toStrictEqual('0x1337'); - }); - - it('throws an error if given a string with decimals', () => { - expect(() => util.toHex('4919.3')).toThrow('Invalid character'); - }); - - it('converts a number to a hex string prepended with "0x"', () => { - expect(util.toHex(4919)).toStrictEqual('0x1337'); - }); - - it('throws an error if given a float', () => { - expect(() => util.toHex(4919.3)).toThrow('Invalid character'); - }); - - it('does nothing to a string that is already a "0x"-prepended hex value', () => { - expect(util.toHex('0x1337')).toStrictEqual('0x1337'); - }); - - it('throws an error if given a non-"0x"-prepended string that is not a valid hex value', () => { - expect(() => util.toHex('zzzz')).toThrow('Invalid character'); - }); - }); - - it('normalizeTransaction', () => { - const normalized = util.normalizeTransaction({ - data: 'data', - from: 'FROM', - gas: 'gas', - gasPrice: 'gasPrice', - nonce: 'nonce', - to: 'TO', - value: 'value', - maxFeePerGas: 'maxFeePerGas', - maxPriorityFeePerGas: 'maxPriorityFeePerGas', - estimatedBaseFee: 'estimatedBaseFee', - }); - expect(normalized).toStrictEqual({ - data: '0xdata', - from: '0xfrom', - gas: '0xgas', - gasPrice: '0xgasPrice', - nonce: '0xnonce', - to: '0xto', - value: '0xvalue', - maxFeePerGas: '0xmaxFeePerGas', - maxPriorityFeePerGas: '0xmaxPriorityFeePerGas', - estimatedBaseFee: '0xestimatedBaseFee', - }); - }); - - describe('gweiDecToWEIBN', () => { - it('should convert a whole number to WEI', () => { - expect(util.gweiDecToWEIBN(1).toNumber()).toBe(1000000000); - expect(util.gweiDecToWEIBN(123).toNumber()).toBe(123000000000); - expect(util.gweiDecToWEIBN(101).toNumber()).toBe(101000000000); - expect(util.gweiDecToWEIBN(1234).toNumber()).toBe(1234000000000); - expect(util.gweiDecToWEIBN(1000).toNumber()).toBe(1000000000000); - }); - - it('should convert a number with a decimal part to WEI', () => { - expect(util.gweiDecToWEIBN(1.1).toNumber()).toBe(1100000000); - expect(util.gweiDecToWEIBN(123.01).toNumber()).toBe(123010000000); - expect(util.gweiDecToWEIBN(101.001).toNumber()).toBe(101001000000); - expect(util.gweiDecToWEIBN(100.001).toNumber()).toBe(100001000000); - expect(util.gweiDecToWEIBN(1234.567).toNumber()).toBe(1234567000000); - }); - - it('should convert a number < 1 to WEI', () => { - expect(util.gweiDecToWEIBN(0.1).toNumber()).toBe(100000000); - expect(util.gweiDecToWEIBN(0.01).toNumber()).toBe(10000000); - expect(util.gweiDecToWEIBN(0.001).toNumber()).toBe(1000000); - expect(util.gweiDecToWEIBN(0.567).toNumber()).toBe(567000000); - }); - - it('should round to whole WEI numbers', () => { - expect(util.gweiDecToWEIBN(0.1001).toNumber()).toBe(100100000); - expect(util.gweiDecToWEIBN(0.0109).toNumber()).toBe(10900000); - expect(util.gweiDecToWEIBN(0.0014).toNumber()).toBe(1400000); - expect(util.gweiDecToWEIBN(0.5676).toNumber()).toBe(567600000); - }); - - it('should handle inputs with more than 9 decimal places', () => { - expect(util.gweiDecToWEIBN(1.0000000162).toNumber()).toBe(1000000016); - expect(util.gweiDecToWEIBN(1.0000000165).toNumber()).toBe(1000000017); - expect(util.gweiDecToWEIBN(1.0000000199).toNumber()).toBe(1000000020); - expect(util.gweiDecToWEIBN(1.9999999999).toNumber()).toBe(2000000000); - expect(util.gweiDecToWEIBN(1.0000005998).toNumber()).toBe(1000000600); - expect(util.gweiDecToWEIBN(123456.0000005998).toNumber()).toBe( - 123456000000600, - ); - expect(util.gweiDecToWEIBN(1.000000016025).toNumber()).toBe(1000000016); - expect(util.gweiDecToWEIBN(1.0000000160000028).toNumber()).toBe( - 1000000016, - ); - expect(util.gweiDecToWEIBN(1.000000016522).toNumber()).toBe(1000000017); - expect(util.gweiDecToWEIBN(1.000000016800022).toNumber()).toBe( - 1000000017, - ); - }); - - it('should work if there are extraneous trailing decimal zeroes', () => { - expect(util.gweiDecToWEIBN('0.5000').toNumber()).toBe(500000000); - expect(util.gweiDecToWEIBN('123.002300').toNumber()).toBe(123002300000); - expect(util.gweiDecToWEIBN('123.002300000000').toNumber()).toBe( - 123002300000, - ); - expect(util.gweiDecToWEIBN('0.00000200000').toNumber()).toBe(2000); - }); - - it('should work if there is no whole number specified', () => { - expect(util.gweiDecToWEIBN('.1').toNumber()).toBe(100000000); - expect(util.gweiDecToWEIBN('.01').toNumber()).toBe(10000000); - expect(util.gweiDecToWEIBN('.001').toNumber()).toBe(1000000); - expect(util.gweiDecToWEIBN('.567').toNumber()).toBe(567000000); - }); - - it('should handle NaN', () => { - expect(util.gweiDecToWEIBN(NaN).toNumber()).toBe(0); - }); - }); - - describe('weiHexToGweiDec', () => { - it('should convert a whole number to WEI', () => { - const testData = [ - { - input: '3b9aca00', - expectedResult: '1', - }, - { - input: '1ca35f0e00', - expectedResult: '123', - }, - { - input: '178411b200', - expectedResult: '101', - }, - { - input: '11f5021b400', - expectedResult: '1234', - }, - ]; - testData.forEach(({ input, expectedResult }) => { - expect(util.weiHexToGweiDec(input)).toBe(expectedResult); - }); - }); - - it('should convert a number with a decimal part to WEI', () => { - const testData = [ - { - input: '4190ab00', - expectedResult: '1.1', - }, - { - input: '1ca3f7a480', - expectedResult: '123.01', - }, - { - input: '178420f440', - expectedResult: '101.001', - }, - { - input: '11f71ed6fc0', - expectedResult: '1234.567', - }, - ]; - - testData.forEach(({ input, expectedResult }) => { - expect(util.weiHexToGweiDec(input)).toBe(expectedResult); - }); - }); - - it('should convert a number < 1 to WEI', () => { - const testData = [ - { - input: '5f5e100', - expectedResult: '0.1', - }, - { - input: '989680', - expectedResult: '0.01', - }, - { - input: 'f4240', - expectedResult: '0.001', - }, - { - input: '21cbbbc0', - expectedResult: '0.567', - }, - ]; - - testData.forEach(({ input, expectedResult }) => { - expect(util.weiHexToGweiDec(input)).toBe(expectedResult); - }); - }); - - it('should work with 0x prefixed values', () => { - expect(util.weiHexToGweiDec('0x5f48b0f7')).toBe('1.598599415'); - }); - }); - - describe('safelyExecute', () => { - it('should swallow errors', async () => { - expect( - await util.safelyExecute(() => { - throw new Error('ahh'); - }), - ).toBeUndefined(); - }); - }); - - describe('safelyExecuteWithTimeout', () => { - it('should swallow errors', async () => { - expect( - await util.safelyExecuteWithTimeout(() => { - throw new Error('ahh'); - }), - ).toBeUndefined(); - }); - - it('should resolve', async () => { - const response = await util.safelyExecuteWithTimeout(() => { - return new Promise((res) => setTimeout(() => res('response'), 200)); - }); - expect(response).toStrictEqual('response'); - }); - - it('should timeout', async () => { - expect( - await util.safelyExecuteWithTimeout(() => { - return new Promise((res) => setTimeout(res, 800)); - }), - ).toBeUndefined(); - }); - }); - - describe('getEtherscanApiUrl', () => { - const networkType = 'mainnet'; - const address = '0xC7D3BFDeA106B446Cf9f2Db354D496e6Dd8b2525'; - const action = 'txlist'; - - it('should return a correctly structured url', () => { - const url = util.getEtherscanApiUrl(networkType, { address, action }); - expect(url.indexOf(`&action=${action}`)).toBeGreaterThan(0); - }); - - it('should return a correctly structured url with from block', () => { - const fromBlock = 'xxxxxx'; - const url = util.getEtherscanApiUrl(networkType, { - address, - action, - startBlock: fromBlock, - }); - expect(url.indexOf(`&startBlock=${fromBlock}`)).toBeGreaterThan(0); - }); - - it('should return a correctly structured url with testnet subdomain', () => { - const ropsten = 'ropsten'; - const url = util.getEtherscanApiUrl(ropsten, { address, action }); - expect(url.indexOf(`https://api-${ropsten}`)).toBe(0); - }); - - it('should return a correctly structured url with apiKey', () => { - const apiKey = 'xxxxxx'; - const url = util.getEtherscanApiUrl(networkType, { - address, - action, - startBlock: 'xxxxxx', - apikey: apiKey, - }); - expect(url.indexOf(`&apikey=${apiKey}`)).toBeGreaterThan(0); - }); - }); - - describe('toChecksumHexAddress', () => { - const fullAddress = `0x${VALID}`; - it('should return address for valid address', () => { - expect(util.toChecksumHexAddress(fullAddress)).toBe(fullAddress); - }); - - it('should return address for non prefix address', () => { - expect(util.toChecksumHexAddress(VALID)).toBe(fullAddress); - }); - }); - - describe('isValidHexAddress', () => { - it('should return true for valid address', () => { - expect(util.isValidHexAddress(VALID)).toBe(true); - }); - - it('should return false for invalid address', () => { - expect(util.isValidHexAddress('0x00')).toBe(false); - }); - - it('should allow allowNonPrefixed to be false', () => { - expect(util.isValidHexAddress('0x00', { allowNonPrefixed: false })).toBe( - false, - ); - }); - }); - - describe('validateTransaction', () => { - it('should throw if no from address', () => { - expect(() => util.validateTransaction({} as any)).toThrow( - 'Invalid "from" address: undefined must be a valid string.', - ); - }); - - it('should throw if non-string from address', () => { - expect(() => util.validateTransaction({ from: 1337 } as any)).toThrow( - 'Invalid "from" address: 1337 must be a valid string.', - ); - }); - - it('should throw if invalid from address', () => { - expect(() => util.validateTransaction({ from: '1337' } as any)).toThrow( - 'Invalid "from" address: 1337 must be a valid string.', - ); - }); - - it('should throw if no data', () => { - expect(() => - util.validateTransaction({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - to: '0x', - } as any), - ).toThrow('Invalid "to" address: 0x must be a valid string.'); - - expect(() => - util.validateTransaction({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - } as any), - ).toThrow('Invalid "to" address: undefined must be a valid string.'); - }); - - it('should delete data', () => { - const transaction = { - data: 'foo', - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - to: '0x', - }; - util.validateTransaction(transaction); - expect(transaction.to).toBeUndefined(); - }); - - it('should throw if invalid to address', () => { - expect(() => - util.validateTransaction({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - to: '1337', - } as any), - ).toThrow('Invalid "to" address: 1337 must be a valid string.'); - }); - - it('should throw if value is invalid', () => { - expect(() => - util.validateTransaction({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - to: '0x3244e191f1b4903970224322180f1fbbc415696b', - value: '133-7', - } as any), - ).toThrow('Invalid "value": 133-7 is not a positive number.'); - - expect(() => - util.validateTransaction({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - to: '0x3244e191f1b4903970224322180f1fbbc415696b', - value: '133.7', - } as any), - ).toThrow('Invalid "value": 133.7 number must be denominated in wei.'); - - expect(() => - util.validateTransaction({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - to: '0x3244e191f1b4903970224322180f1fbbc415696b', - value: 'hello', - } as any), - ).toThrow('Invalid "value": hello number must be a valid number.'); - - expect(() => - util.validateTransaction({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - to: '0x3244e191f1b4903970224322180f1fbbc415696b', - value: 'one million dollar$', - } as any), - ).toThrow( - 'Invalid "value": one million dollar$ number must be a valid number.', - ); - - expect(() => - util.validateTransaction({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - to: '0x3244e191f1b4903970224322180f1fbbc415696b', - value: '1', - } as any), - ).not.toThrow(); - }); - }); - - it('normalizeMessageData', () => { - const firstNormalized = util.normalizeMessageData( - '879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', - ); - const secondNormalized = util.normalizeMessageData('somedata'); - expect(firstNormalized).toStrictEqual( - '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', - ); - expect(secondNormalized).toStrictEqual('0x736f6d6564617461'); - }); - - it('messageHexToString', () => { - const str = util.hexToText('68656c6c6f207468657265'); - expect(str).toStrictEqual('hello there'); - }); - - describe('validateSignMessageData', () => { - it('should throw if no from address', () => { - expect(() => - util.validateSignMessageData({ - data: '0x879a05', - } as any), - ).toThrow('Invalid "from" address: undefined must be a valid string.'); - }); - - it('should throw if invalid from address', () => { - expect(() => - util.validateSignMessageData({ - data: '0x879a05', - from: '01', - } as any), - ).toThrow('Invalid "from" address: 01 must be a valid string.'); - }); - - it('should throw if invalid type from address', () => { - expect(() => - util.validateSignMessageData({ - data: '0x879a05', - from: 123, - } as any), - ).toThrow('Invalid "from" address: 123 must be a valid string.'); - }); - - it('should throw if no data', () => { - expect(() => - util.validateSignMessageData({ - data: '0x879a05', - } as any), - ).toThrow('Invalid "from" address: undefined must be a valid string.'); - }); - - it('should throw if invalid tyoe data', () => { - expect(() => - util.validateSignMessageData({ - data: 123, - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - } as any), - ).toThrow('Invalid message "data": 123 must be a valid string.'); - }); - }); - - describe('validateTypedMessageDataV1', () => { - it('should throw if no from address legacy', () => { - expect(() => - util.validateTypedSignMessageDataV1({ - data: [], - } as any), - ).toThrow('Invalid "from" address:'); - }); - - it('should throw if invalid from address', () => { - expect(() => - util.validateTypedSignMessageDataV1({ - data: [], - from: '3244e191f1b4903970224322180f1fbbc415696b', - } as any), - ).toThrow('Expected EIP712 typed data.'); - }); - - it('should throw if invalid type from address', () => { - expect(() => - util.validateTypedSignMessageDataV1({ - data: [], - from: 123, - } as any), - ).toThrow('Invalid "from" address:'); - }); - - it('should throw if incorrect data', () => { - expect(() => - util.validateTypedSignMessageDataV1({ - data: '0x879a05', - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - } as any), - ).toThrow('Invalid message "data":'); - }); - - it('should throw if no data', () => { - expect(() => - util.validateTypedSignMessageDataV1({ - data: '0x879a05', - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - } as any), - ).toThrow('Invalid message "data":'); - }); - - it('should throw if invalid type data', () => { - expect(() => - util.validateTypedSignMessageDataV1({ - data: [], - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - } as any), - ).toThrow('Expected EIP712 typed data.'); - }); - }); - - describe('validateTypedMessageDataV3', () => { - const dataTyped = - '{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Person":[{"name":"name","type":"string"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","domain":{"name":"Ether Mail","version":"1","chainId":1,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},"contents":"Hello, Bob!"}}'; - it('should throw if no from address', () => { - expect(() => - util.validateTypedSignMessageDataV3({ - data: '0x879a05', - } as any), - ).toThrow('Invalid "from" address:'); - }); - - it('should throw if invalid from address', () => { - expect(() => - util.validateTypedSignMessageDataV3({ - data: '0x879a05', - from: '3244e191f1b4903970224322180f1fbbc415696b', - } as any), - ).toThrow('Data must be passed as a valid JSON string.'); - }); - - it('should throw if invalid type from address', () => { - expect(() => - util.validateTypedSignMessageDataV3({ - data: '0x879a05', - from: 123, - } as any), - ).toThrow('Invalid "from" address:'); - }); - - it('should throw if array data', () => { - expect(() => - util.validateTypedSignMessageDataV3({ - data: [], - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - } as any), - ).toThrow('Invalid message "data":'); - }); - - it('should throw if no array data', () => { - expect(() => - util.validateTypedSignMessageDataV3({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - } as any), - ).toThrow('Invalid message "data":'); - }); - - it('should throw if no json valid data', () => { - expect(() => - util.validateTypedSignMessageDataV3({ - data: 'uh oh', - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - } as any), - ).toThrow('Data must be passed as a valid JSON string.'); - }); - - it('should throw if data not in typed message schema', () => { - expect(() => - util.validateTypedSignMessageDataV3({ - data: '{"greetings":"I am Alice"}', - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - } as any), - ).toThrow('Data must conform to EIP-712 schema.'); - }); - - it('should not throw if data is correct', () => { - expect(() => - util.validateTypedSignMessageDataV3({ - data: dataTyped, - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - } as any), - ).not.toThrow(); - }); - - it('should identify smart contract code', () => { - const toSmartContract1 = util.isSmartContractCode(''); - const toSmartContract2 = util.isSmartContractCode('0x'); - const toSmartContract3 = util.isSmartContractCode('0x0'); - const toSmartContract4 = util.isSmartContractCode('0x01234'); - expect(toSmartContract1).toBe(false); - expect(toSmartContract2).toBe(false); - expect(toSmartContract3).toBe(false); - expect(toSmartContract4).toBe(true); - }); - }); - - describe('validateTokenToWatch', () => { - it('should throw if undefined token atrributes', () => { - expect(() => - util.validateTokenToWatch({ - address: undefined, - decimals: 0, - symbol: 'TKN', - } as any), - ).toThrow('Must specify address, symbol, and decimals.'); - - expect(() => - util.validateTokenToWatch({ - address: '0x1', - decimals: 0, - symbol: undefined, - } as any), - ).toThrow('Must specify address, symbol, and decimals.'); - - expect(() => - util.validateTokenToWatch({ - address: '0x1', - decimals: undefined, - symbol: 'TKN', - } as any), - ).toThrow('Must specify address, symbol, and decimals.'); - }); - - it('should throw if symbol is not a string', () => { - expect(() => - util.validateTokenToWatch({ - address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', - decimals: 0, - symbol: { foo: 'bar' }, - } as any), - ).toThrow('Invalid symbol: not a string.'); - }); - - it('should throw if symbol is an empty string', () => { - expect(() => - util.validateTokenToWatch({ - address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', - decimals: 0, - symbol: '', - } as any), - ).toThrow('Must specify address, symbol, and decimals.'); - }); - - it('should not throw if symbol is exactly 1 character long', () => { - expect(() => - util.validateTokenToWatch({ - address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', - decimals: 0, - symbol: 'T', - } as any), - ).not.toThrow(); - }); - - it('should not throw if symbol is exactly 11 characters long', () => { - expect(() => - util.validateTokenToWatch({ - address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', - decimals: 0, - symbol: 'TKNTKNTKNTK', - } as any), - ).not.toThrow(); - }); - - it('should throw if symbol is more than 11 characters long', () => { - expect(() => - util.validateTokenToWatch({ - address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', - decimals: 0, - symbol: 'TKNTKNTKNTKN', - } as any), - ).toThrow('Invalid symbol "TKNTKNTKNTKN": longer than 11 characters.'); - }); - - it('should throw if invalid decimals', () => { - expect(() => - util.validateTokenToWatch({ - address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', - decimals: 0, - symbol: 'TKN', - } as any), - ).not.toThrow(); - - expect(() => - util.validateTokenToWatch({ - address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', - decimals: 38, - symbol: 'TKN', - } as any), - ).toThrow('Invalid decimals "38": must be 0 <= 36.'); - - expect(() => - util.validateTokenToWatch({ - address: '0xe9f786dfdd9be4d57e830acb52296837765f0e5b', - decimals: -1, - symbol: 'TKN', - } as any), - ).toThrow('Invalid decimals "-1": must be 0 <= 36.'); - }); - - it('should throw if invalid address', () => { - expect(() => - util.validateTokenToWatch({ - address: '0xe9', - decimals: 0, - symbol: 'TKN', - } as any), - ).toThrow('Invalid address "0xe9".'); - }); - }); - - describe('successfulFetch', () => { - beforeEach(() => { - nock(SOME_API).get(/.+/u).reply(200, { foo: 'bar' }).persist(); - nock(SOME_FAILING_API).get(/.+/u).reply(500).persist(); - }); - - it('should return successful fetch response', async () => { - const res = await util.successfulFetch(SOME_API); - const parsed = await res.json(); - expect(parsed).toStrictEqual({ foo: 'bar' }); - }); - - it('should throw error for an unsuccessful fetch', async () => { - await expect(util.successfulFetch(SOME_FAILING_API)).rejects.toThrow( - `Fetch failed with status '500' for request '${SOME_FAILING_API}'`, - ); - }); - }); - - describe('timeoutFetch', () => { - beforeEach(() => { - nock(SOME_API).get(/.+/u).delay(300).reply(200, {}).persist(); - }); - - it('should fetch first if response is faster than timeout', async () => { - const res = await util.timeoutFetch(SOME_API); - const parsed = await res.json(); - expect(parsed).toStrictEqual({}); - }); - - it('should fail fetch with timeout', async () => { - await expect(util.timeoutFetch(SOME_API, {}, 100)).rejects.toThrow( - 'timeout', - ); - }); - }); - - describe('normalizeEnsName', () => { - it('should normalize with valid 2LD', async () => { - let valid = util.normalizeEnsName('metamask.eth'); - expect(valid).toStrictEqual('metamask.eth'); - valid = util.normalizeEnsName('foobar1.eth'); - expect(valid).toStrictEqual('foobar1.eth'); - valid = util.normalizeEnsName('foo-bar.eth'); - expect(valid).toStrictEqual('foo-bar.eth'); - valid = util.normalizeEnsName('1-foo-bar.eth'); - expect(valid).toStrictEqual('1-foo-bar.eth'); - }); - - it('should normalize with valid 2LD and "test" TLD', async () => { - const valid = util.normalizeEnsName('metamask.test'); - expect(valid).toStrictEqual('metamask.test'); - }); - - it('should normalize with valid 2LD and 3LD', async () => { - let valid = util.normalizeEnsName('a.metamask.eth'); - expect(valid).toStrictEqual('a.metamask.eth'); - valid = util.normalizeEnsName('aa.metamask.eth'); - expect(valid).toStrictEqual('aa.metamask.eth'); - valid = util.normalizeEnsName('a-a.metamask.eth'); - expect(valid).toStrictEqual('a-a.metamask.eth'); - valid = util.normalizeEnsName('1-a.metamask.eth'); - expect(valid).toStrictEqual('1-a.metamask.eth'); - valid = util.normalizeEnsName('1-2.metamask.eth'); - expect(valid).toStrictEqual('1-2.metamask.eth'); - }); - - it('should return null with invalid 2LD', async () => { - let invalid = util.normalizeEnsName('me.eth'); - expect(invalid).toBeNull(); - invalid = util.normalizeEnsName('metamask-.eth'); - expect(invalid).toBeNull(); - invalid = util.normalizeEnsName('-metamask.eth'); - expect(invalid).toBeNull(); - invalid = util.normalizeEnsName('@metamask.eth'); - expect(invalid).toBeNull(); - invalid = util.normalizeEnsName('foobar.eth'); - expect(invalid).toBeNull(); - }); - - it('should return null with valid 2LD and invalid 3LD', async () => { - let invalid = util.normalizeEnsName('-.metamask.eth'); - expect(invalid).toBeNull(); - invalid = util.normalizeEnsName('abc-.metamask.eth'); - expect(invalid).toBeNull(); - invalid = util.normalizeEnsName('-abc.metamask.eth'); - expect(invalid).toBeNull(); - invalid = util.normalizeEnsName('.metamask.eth'); - expect(invalid).toBeNull(); - invalid = util.normalizeEnsName('f@o.metamask.eth'); - expect(invalid).toBeNull(); - }); - - it('should return null with invalid 2LD and valid 3LD', async () => { - const invalid = util.normalizeEnsName('foo.barbaz.eth'); - expect(invalid).toBeNull(); - }); - - it('should return null with invalid TLD', async () => { - const invalid = util.normalizeEnsName('a.metamask.com'); - expect(invalid).toBeNull(); - }); - - it('should return null with repeated periods', async () => { - let invalid = util.normalizeEnsName('foo..metamask.eth'); - expect(invalid).toBeNull(); - invalid = util.normalizeEnsName('foo.metamask..eth'); - expect(invalid).toBeNull(); - }); - - it('should return null with empty string', async () => { - const invalid = util.normalizeEnsName(''); - expect(invalid).toBeNull(); - }); - }); - - describe('query', () => { - describe('when the given method exists directly on the EthQuery', () => { - it('should call the method on the EthQuery and, if it is successful, return a promise that resolves to the result', async () => { - const ethQuery = { - getBlockByHash: (blockId: any, cb: any) => cb(null, { id: blockId }), - }; - const result = await util.query(ethQuery, 'getBlockByHash', ['0x1234']); - expect(result).toStrictEqual({ id: '0x1234' }); - }); - - it('should call the method on the EthQuery and, if it errors, return a promise that is rejected with the error', async () => { - const ethQuery = { - getBlockByHash: (_blockId: any, cb: any) => - cb(new Error('uh oh'), null), - }; - await expect( - util.query(ethQuery, 'getBlockByHash', ['0x1234']), - ).rejects.toThrow('uh oh'); - }); - }); - - describe('when the given method does not exist directly on the EthQuery', () => { - it('should use sendAsync to call the RPC endpoint and, if it is successful, return a promise that resolves to the result', async () => { - const ethQuery = { - sendAsync: ({ method, params }: any, cb: any) => { - if (method === 'eth_getBlockByHash') { - return cb(null, { id: params[0] }); - } - throw new Error(`Unsupported method ${method}`); - }, - }; - const result = await util.query(ethQuery, 'eth_getBlockByHash', [ - '0x1234', - ]); - expect(result).toStrictEqual({ id: '0x1234' }); - }); - - it('should use sendAsync to call the RPC endpoint and, if it errors, return a promise that is rejected with the error', async () => { - const ethQuery = { - sendAsync: (_args: any, cb: any) => { - cb(new Error('uh oh'), null); - }, - }; - await expect( - util.query(ethQuery, 'eth_getBlockByHash', ['0x1234']), - ).rejects.toThrow('uh oh'); - }); - }); - }); - - describe('convertHexToDecimal', () => { - it('should convert hex price to decimal', () => { - expect(util.convertHexToDecimal('0x50fd51da')).toStrictEqual(1358778842); - }); - - it('should return zero when undefined', () => { - expect(util.convertHexToDecimal(undefined)).toStrictEqual(0); - }); - - it('should return a decimal string as the same decimal number', () => { - expect(util.convertHexToDecimal('1611')).toStrictEqual(1611); - }); - - it('should return 0 when passed an invalid hex string', () => { - expect(util.convertHexToDecimal('0x12398u12')).toStrictEqual(0); - }); - }); - - describe('getIncreasedPriceHex', () => { - it('should get increased price from number as hex', () => { - expect(util.getIncreasedPriceHex(1358778842, 1.1)).toStrictEqual( - '0x5916a6d6', - ); - }); - }); - - describe('getIncreasedPriceFromExisting', () => { - it('should get increased price from hex as hex', () => { - expect( - util.getIncreasedPriceFromExisting('0x50fd51da', 1.1), - ).toStrictEqual('0x5916a6d6'); - }); - }); - - describe('isEIP1559Transaction', () => { - it('should detect EIP1559 transaction', () => { - const tx: Transaction = { from: '' }; - const eip1559tx: Transaction = { - ...tx, - maxFeePerGas: '2', - maxPriorityFeePerGas: '3', - }; - expect(util.isEIP1559Transaction(eip1559tx)).toBe(true); - expect(util.isEIP1559Transaction(tx)).toBe(false); - }); - }); - - describe('validateGasValues', () => { - it('should throw when provided invalid gas values', () => { - const gasValues: GasPriceValue = { - [GAS_PRICE]: FAIL, - }; - expect(() => util.validateGasValues(gasValues)).toThrow(TypeError); - expect(() => util.validateGasValues(gasValues)).toThrow( - `expected hex string for ${GAS_PRICE} but received: ${FAIL}`, - ); - }); - - it('should throw when any provided gas values are invalid', () => { - const gasValues: FeeMarketEIP1559Values = { - [MAX_PRIORITY_FEE_PER_GAS]: PASS, - [MAX_FEE_PER_GAS]: FAIL, - }; - expect(() => util.validateGasValues(gasValues)).toThrow(TypeError); - expect(() => util.validateGasValues(gasValues)).toThrow( - `expected hex string for ${MAX_FEE_PER_GAS} but received: ${FAIL}`, - ); - }); - - it('should return true when provided valid gas values', () => { - const gasValues: FeeMarketEIP1559Values = { - [MAX_FEE_PER_GAS]: PASS, - [MAX_PRIORITY_FEE_PER_GAS]: PASS, - }; - expect(() => util.validateGasValues(gasValues)).not.toThrow(TypeError); - }); - }); - - describe('isFeeMarketEIP1559Values', () => { - it('should detect if isFeeMarketEIP1559Values', () => { - const gasValues = { - [MAX_PRIORITY_FEE_PER_GAS]: PASS, - [MAX_FEE_PER_GAS]: FAIL, - }; - expect(util.isFeeMarketEIP1559Values(gasValues)).toBe(true); - expect(util.isGasPriceValue(gasValues)).toBe(false); - }); - }); - - describe('isGasPriceValue', () => { - it('should detect if isGasPriceValue', () => { - const gasValues: GasPriceValue = { - [GAS_PRICE]: PASS, - }; - expect(util.isGasPriceValue(gasValues)).toBe(true); - expect(util.isFeeMarketEIP1559Values(gasValues)).toBe(false); - }); - }); - - describe('validateMinimumIncrease', () => { - it('should throw if increase does not meet minimum requirement', () => { - expect(() => - util.validateMinimumIncrease('0x50fd51da', '0x5916a6d6'), - ).toThrow(Error); - - expect(() => - util.validateMinimumIncrease('0x50fd51da', '0x5916a6d6'), - ).toThrow( - 'The proposed value: 1358778842 should meet or exceed the minimum value: 1494656726', - ); - }); - - it('should not throw if increase meets minimum requirement', () => { - expect(() => - util.validateMinimumIncrease('0x5916a6d6', '0x5916a6d6'), - ).not.toThrow(Error); - }); - - it('should not throw if increase exceeds minimum requirement', () => { - expect(() => - util.validateMinimumIncrease('0x7162a5ca', '0x5916a6d6'), - ).not.toThrow(Error); - }); - }); - - describe('getFormattedIpfsUrl', () => { - it('should return a correctly formatted subdomained ipfs url when passed ipfsGateway without protocol prefix, no path and subdomainSupported argument set to true', () => { - expect( - util.getFormattedIpfsUrl( - IFPS_GATEWAY, - `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V1}`, - true, - ), - ).toStrictEqual(`https://${IPFS_CID_V1}.ipfs.${IFPS_GATEWAY}`); - }); - - it('should return a correctly formatted subdomained ipfs url when passed ipfsGateway with protocol prefix, a cidv0 and no path and subdomainSupported argument set to true', () => { - expect( - util.getFormattedIpfsUrl( - `https://${IFPS_GATEWAY}`, - `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V0}`, - true, - ), - ).toStrictEqual(`https://${IPFS_CID_V1}.ipfs.${IFPS_GATEWAY}`); - }); - - it('should return a correctly formatted subdomained ipfs url when passed ipfsGateway with protocol prefix, a path at the end of the url, and subdomainSupported argument set to true', () => { - expect( - util.getFormattedIpfsUrl( - `https://${IFPS_GATEWAY}`, - `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V1}/test`, - true, - ), - ).toStrictEqual(`https://${IPFS_CID_V1}.ipfs.${IFPS_GATEWAY}/test`); - }); - - it('should return a correctly formatted non-subdomained ipfs url when passed ipfsGateway with no "/ipfs/" appended, a path at the end of the url, and subdomainSupported argument set to false', () => { - expect( - util.getFormattedIpfsUrl( - `https://${IFPS_GATEWAY}`, - `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V1}/test`, - false, - ), - ).toStrictEqual(`https://${IFPS_GATEWAY}/ipfs/${IPFS_CID_V1}/test`); - }); - - it('should return a correctly formatted non-subdomained ipfs url when passed an ipfsGateway with "/ipfs/" appended, a path at the end of the url, subdomainSupported argument set to false', () => { - expect( - util.getFormattedIpfsUrl( - `https://${IFPS_GATEWAY}/ipfs/`, - `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V1}/test`, - false, - ), - ).toStrictEqual(`https://${IFPS_GATEWAY}/ipfs/${IPFS_CID_V1}/test`); - }); - }); - - describe('removeIpfsProtocolPrefix', () => { - it('should return content identifier and path combined string from default ipfs url format', () => { - expect( - util.removeIpfsProtocolPrefix( - `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V0}/test`, - ), - ).toStrictEqual(`${IPFS_CID_V0}/test`); - }); - - it('should return content identifier string from default ipfs url format if no path preset', () => { - expect( - util.removeIpfsProtocolPrefix( - `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V0}`, - ), - ).toStrictEqual(IPFS_CID_V0); - }); - - it('should return content identifier string from alternate ipfs url format', () => { - expect( - util.removeIpfsProtocolPrefix( - `${ALTERNATIVE_IPFS_URL_FORMAT}${IPFS_CID_V0}`, - ), - ).toStrictEqual(IPFS_CID_V0); - }); - - it('should throw error if passed a non ipfs url', () => { - expect(() => util.removeIpfsProtocolPrefix(SOME_API)).toThrow( - 'this method should not be used with non ipfs urls', - ); - }); - }); - - describe('addUrlProtocolPrefix', () => { - it('should return a URL with https:// prepended if input URL does not already have it', () => { - expect(util.addUrlProtocolPrefix(IFPS_GATEWAY)).toStrictEqual( - `https://${IFPS_GATEWAY}`, - ); - }); - - it('should return a URL as is if https:// is already prepended', () => { - expect(util.addUrlProtocolPrefix(SOME_API)).toStrictEqual(SOME_API); - }); - }); - - describe('getIpfsCIDv1AndPath', () => { - it('should return content identifier from default ipfs url format', () => { - expect( - util.getIpfsCIDv1AndPath(`${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V0}`), - ).toStrictEqual({ cid: IPFS_CID_V1, path: undefined }); - }); - - it('should return content identifier from alternative ipfs url format', () => { - expect( - util.getIpfsCIDv1AndPath( - `${ALTERNATIVE_IPFS_URL_FORMAT}${IPFS_CID_V0}`, - ), - ).toStrictEqual({ cid: IPFS_CID_V1, path: undefined }); - }); - - it('should return unchanged content identifier if already v1', () => { - expect( - util.getIpfsCIDv1AndPath(`${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V1}`), - ).toStrictEqual({ cid: IPFS_CID_V1, path: undefined }); - }); - - it('should return a path when url contains one', () => { - expect( - util.getIpfsCIDv1AndPath( - `${DEFAULT_IPFS_URL_FORMAT}${IPFS_CID_V1}/test/test/test`, - ), - ).toStrictEqual({ cid: IPFS_CID_V1, path: '/test/test/test' }); - }); - }); -}); - -describe('isPlainObject', () => { - it('returns false for null values', () => { - expect(util.isPlainObject(null)).toBe(false); - expect(util.isPlainObject(undefined)).toBe(false); - }); - - it('returns false for non objects', () => { - expect(util.isPlainObject(5)).toBe(false); - expect(util.isPlainObject('foo')).toBe(false); - }); - - it('returns false for arrays', () => { - expect(util.isPlainObject(['foo'])).toBe(false); - expect(util.isPlainObject([{}])).toBe(false); - }); - - it('returns true for objects', () => { - expect(util.isPlainObject({ foo: 'bar' })).toBe(true); - expect(util.isPlainObject({ foo: 'bar', test: { num: 5 } })).toBe(true); - }); -}); - -describe('hasProperty', () => { - it('returns false for non existing properties', () => { - expect(util.hasProperty({ foo: 'bar' }, 'property')).toBe(false); - }); - - it('returns true for existing properties', () => { - expect(util.hasProperty({ foo: 'bar' }, 'foo')).toBe(true); - }); -}); - -describe('isNonEmptyArray', () => { - it('returns false non arrays', () => { - // @ts-expect-error Invalid type for testing purposes - expect(util.isNonEmptyArray(null)).toBe(false); - // @ts-expect-error Invalid type for testing purposes - expect(util.isNonEmptyArray(undefined)).toBe(false); - }); - - it('returns false for empty array', () => { - expect(util.isNonEmptyArray([])).toBe(false); - }); - - it('returns true arrays with at least one item', () => { - expect(util.isNonEmptyArray([1])).toBe(true); - expect(util.isNonEmptyArray([1, 2, 3, 4])).toBe(true); - }); -}); - -describe('isValidJson', () => { - it('returns false for class instances', () => { - expect(util.isValidJson(new Map())).toBe(false); - }); - - it('returns true for valid JSON', () => { - expect(util.isValidJson({ foo: 'bar', test: { num: 5 } })).toBe(true); - }); -}); - -describe('isTokenDetectionSupportedForNetwork', () => { - it('returns true for Mainnet', () => { - expect( - util.isTokenDetectionSupportedForNetwork( - util.SupportedTokenDetectionNetworks.mainnet, - ), - ).toBe(true); - }); - - it('returns true for custom network such as BSC', () => { - expect( - util.isTokenDetectionSupportedForNetwork( - util.SupportedTokenDetectionNetworks.bsc, - ), - ).toBe(true); - }); - - it('returns false for testnets such as Ropsten', () => { - expect( - util.isTokenDetectionSupportedForNetwork(NetworksChainId.ropsten), - ).toBe(false); - }); -}); - -describe('isTokenListSupportedForNetwork', () => { - it('returns true for Mainnet when chainId is passed as a decimal string', () => { - expect( - util.isTokenListSupportedForNetwork( - util.SupportedTokenDetectionNetworks.mainnet, - ), - ).toBe(true); - }); - - it('returns true for Mainnet when chainId is passed as a hexadecimal string', () => { - expect(util.isTokenListSupportedForNetwork('0x1')).toBe(true); - }); - - it('returns true for ganache local network', () => { - expect(util.isTokenListSupportedForNetwork(GANACHE_CHAIN_ID)).toBe(true); - }); - - it('returns true for custom network such as Polygon', () => { - expect( - util.isTokenListSupportedForNetwork( - util.SupportedTokenDetectionNetworks.polygon, - ), - ).toBe(true); - }); - - it('returns false for testnets such as Ropsten', () => { - expect(util.isTokenListSupportedForNetwork(NetworksChainId.ropsten)).toBe( - false, - ); - }); -}); diff --git a/src/util.ts b/src/util.ts deleted file mode 100644 index 2064657591..0000000000 --- a/src/util.ts +++ /dev/null @@ -1,1053 +0,0 @@ -import { - addHexPrefix, - isValidAddress, - isHexString, - bufferToHex, - BN, - toChecksumAddress, - stripHexPrefix, -} from 'ethereumjs-util'; -import { fromWei, toWei } from 'ethjs-unit'; -import { ethErrors } from 'eth-rpc-errors'; -import ensNamehash from 'eth-ens-namehash'; -import { TYPED_MESSAGE_SCHEMA, typedSignatureHash } from 'eth-sig-util'; -import { validate } from 'jsonschema'; -import { CID } from 'multiformats/cid'; -import deepEqual from 'fast-deep-equal'; -import { - Transaction, - FetchAllOptions, - GasPriceValue, - FeeMarketEIP1559Values, -} from './transaction/TransactionController'; -import { MessageParams } from './message-manager/MessageManager'; -import { PersonalMessageParams } from './message-manager/PersonalMessageManager'; -import { TypedMessageParams } from './message-manager/TypedMessageManager'; -import { Token } from './assets/TokenRatesController'; -import { MAINNET, GANACHE_CHAIN_ID } from './constants'; -import { Json } from './BaseControllerV2'; - -const TIMEOUT_ERROR = new Error('timeout'); - -const hexRe = /^[0-9A-Fa-f]+$/gu; - -const NORMALIZERS: { [param in keyof Transaction]: any } = { - data: (data: string) => addHexPrefix(data), - from: (from: string) => addHexPrefix(from).toLowerCase(), - gas: (gas: string) => addHexPrefix(gas), - gasPrice: (gasPrice: string) => addHexPrefix(gasPrice), - nonce: (nonce: string) => addHexPrefix(nonce), - to: (to: string) => addHexPrefix(to).toLowerCase(), - value: (value: string) => addHexPrefix(value), - maxFeePerGas: (maxFeePerGas: string) => addHexPrefix(maxFeePerGas), - maxPriorityFeePerGas: (maxPriorityFeePerGas: string) => - addHexPrefix(maxPriorityFeePerGas), - estimatedBaseFee: (maxPriorityFeePerGas: string) => - addHexPrefix(maxPriorityFeePerGas), -}; - -/** - * Converts a BN object to a hex string with a '0x' prefix. - * - * @param inputBn - BN instance to convert to a hex string. - * @returns A '0x'-prefixed hex string. - */ -export function BNToHex(inputBn: any) { - return addHexPrefix(inputBn.toString(16)); -} - -/** - * Used to multiply a BN by a fraction. - * - * @param targetBN - Number to multiply by a fraction. - * @param numerator - Numerator of the fraction multiplier. - * @param denominator - Denominator of the fraction multiplier. - * @returns Product of the multiplication. - */ -export function fractionBN( - targetBN: any, - numerator: number | string, - denominator: number | string, -) { - const numBN = new BN(numerator); - const denomBN = new BN(denominator); - return targetBN.mul(numBN).div(denomBN); -} - -/** - * Used to convert a base-10 number from GWEI to WEI. Can handle numbers with decimal parts. - * - * @param n - The base 10 number to convert to WEI. - * @returns The number in WEI, as a BN. - */ -export function gweiDecToWEIBN(n: number | string) { - if (Number.isNaN(n)) { - return new BN(0); - } - - const parts = n.toString().split('.'); - const wholePart = parts[0] || '0'; - let decimalPart = parts[1] || ''; - - if (!decimalPart) { - return toWei(wholePart, 'gwei'); - } - - if (decimalPart.length <= 9) { - return toWei(`${wholePart}.${decimalPart}`, 'gwei'); - } - - const decimalPartToRemove = decimalPart.slice(9); - const decimalRoundingDigit = decimalPartToRemove[0]; - - decimalPart = decimalPart.slice(0, 9); - let wei = toWei(`${wholePart}.${decimalPart}`, 'gwei'); - - if (Number(decimalRoundingDigit) >= 5) { - wei = wei.add(new BN(1)); - } - - return wei; -} - -/** - * Used to convert values from wei hex format to dec gwei format. - * - * @param hex - The value in hex wei. - * @returns The value in dec gwei as string. - */ -export function weiHexToGweiDec(hex: string) { - const hexWei = new BN(stripHexPrefix(hex), 16); - return fromWei(hexWei, 'gwei').toString(10); -} - -/** - * Return a URL that can be used to obtain ETH for a given network. - * - * @param networkCode - Network code of desired network. - * @param address - Address to deposit obtained ETH. - * @param amount - How much ETH is desired. - * @returns URL to buy ETH based on network. - */ -export function getBuyURL( - networkCode = '1', - address?: string, - amount = 5, -): string | undefined { - switch (networkCode) { - case '1': - return `https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=${amount}&address=${address}&crypto_currency=ETH`; - case '3': - return 'https://faucet.metamask.io/'; - case '4': - return 'https://www.rinkeby.io/'; - case '5': - return 'https://goerli-faucet.slock.it/'; - case '42': - return 'https://github.com/kovan-testnet/faucet'; - default: - return undefined; - } -} - -/** - * Return a URL that can be used to fetch ETH transactions. - * - * @param networkType - Network type of desired network. - * @param urlParams - The parameters used to construct the URL. - * @returns URL to fetch the access the endpoint. - */ -export function getEtherscanApiUrl( - networkType: string, - urlParams: any, -): string { - let etherscanSubdomain = 'api'; - if (networkType !== MAINNET) { - etherscanSubdomain = `api-${networkType}`; - } - const apiUrl = `https://${etherscanSubdomain}.etherscan.io`; - let url = `${apiUrl}/api?`; - - for (const paramKey in urlParams) { - if (urlParams[paramKey]) { - url += `${paramKey}=${urlParams[paramKey]}&`; - } - } - url += 'tag=latest&page=1'; - return url; -} - -/** - * Handles the fetch of incoming transactions. - * - * @param networkType - Network type of desired network. - * @param address - Address to get the transactions from. - * @param txHistoryLimit - The maximum number of transactions to fetch. - * @param opt - Object that can contain fromBlock and Etherscan service API key. - * @returns Responses for both ETH and ERC20 token transactions. - */ -export async function handleTransactionFetch( - networkType: string, - address: string, - txHistoryLimit: number, - opt?: FetchAllOptions, -): Promise<[{ [result: string]: [] }, { [result: string]: [] }]> { - // transactions - const urlParams = { - module: 'account', - address, - startBlock: opt?.fromBlock, - apikey: opt?.etherscanApiKey, - offset: txHistoryLimit.toString(), - order: 'desc', - }; - const etherscanTxUrl = getEtherscanApiUrl(networkType, { - ...urlParams, - action: 'txlist', - }); - const etherscanTxResponsePromise = handleFetch(etherscanTxUrl); - - // tokens - const etherscanTokenUrl = getEtherscanApiUrl(networkType, { - ...urlParams, - action: 'tokentx', - }); - const etherscanTokenResponsePromise = handleFetch(etherscanTokenUrl); - - let [etherscanTxResponse, etherscanTokenResponse] = await Promise.all([ - etherscanTxResponsePromise, - etherscanTokenResponsePromise, - ]); - - if ( - etherscanTxResponse.status === '0' || - etherscanTxResponse.result.length <= 0 - ) { - etherscanTxResponse = { status: etherscanTxResponse.status, result: [] }; - } - - if ( - etherscanTokenResponse.status === '0' || - etherscanTokenResponse.result.length <= 0 - ) { - etherscanTokenResponse = { - status: etherscanTokenResponse.status, - result: [], - }; - } - - return [etherscanTxResponse, etherscanTokenResponse]; -} - -/** - * Converts a hex string to a BN object. - * - * @param inputHex - Number represented as a hex string. - * @returns A BN instance. - */ -export function hexToBN(inputHex: string) { - return new BN(stripHexPrefix(inputHex), 16); -} - -/** - * A helper function that converts hex data to human readable string. - * - * @param hex - The hex string to convert to string. - * @returns A human readable string conversion. - */ -export function hexToText(hex: string) { - try { - const stripped = stripHexPrefix(hex); - const buff = Buffer.from(stripped, 'hex'); - return buff.toString('utf8'); - } catch (e) { - /* istanbul ignore next */ - return hex; - } -} - -/** - * Parses a hex string and converts it into a number that can be operated on in a bignum-safe, - * base-10 way. - * - * @param value - A base-16 number encoded as a string. - * @returns The number as a BN object in base-16 mode. - */ -export function fromHex(value: string | BN): BN { - if (BN.isBN(value)) { - return value; - } - return new BN(hexToBN(value).toString(10)); -} - -/** - * Converts an integer to a hexadecimal representation. - * - * @param value - An integer, an integer encoded as a base-10 string, or a BN. - * @returns The integer encoded as a hex string. - */ -export function toHex(value: number | string | BN): string { - if (typeof value === 'string' && isHexString(value)) { - return value; - } - const hexString = BN.isBN(value) - ? value.toString(16) - : new BN(value.toString(), 10).toString(16); - return `0x${hexString}`; -} - -/** - * Normalizes properties on a Transaction object. - * - * @param transaction - Transaction object to normalize. - * @returns Normalized Transaction object. - */ -export function normalizeTransaction(transaction: Transaction) { - const normalizedTransaction: Transaction = { from: '' }; - let key: keyof Transaction; - for (key in NORMALIZERS) { - if (transaction[key as keyof Transaction]) { - normalizedTransaction[key] = NORMALIZERS[key](transaction[key]) as never; - } - } - return normalizedTransaction; -} - -/** - * Execute and return an asynchronous operation without throwing errors. - * - * @param operation - Function returning a Promise. - * @param logError - Determines if the error should be logged. - * @returns Promise resolving to the result of the async operation. - */ -export async function safelyExecute( - operation: () => Promise, - logError = false, -) { - try { - return await operation(); - } catch (error: any) { - /* istanbul ignore next */ - if (logError) { - console.error(error); - } - return undefined; - } -} - -/** - * Execute and return an asynchronous operation with a timeout. - * - * @param operation - Function returning a Promise. - * @param logError - Determines if the error should be logged. - * @param timeout - Timeout to fail the operation. - * @returns Promise resolving to the result of the async operation. - */ -export async function safelyExecuteWithTimeout( - operation: () => Promise, - logError = false, - timeout = 500, -) { - try { - return await Promise.race([ - operation(), - new Promise((_, reject) => - setTimeout(() => { - reject(TIMEOUT_ERROR); - }, timeout), - ), - ]); - } catch (error) { - /* istanbul ignore next */ - if (logError) { - console.error(error); - } - return undefined; - } -} - -/** - * Convert an address to a checksummed hexidecimal address. - * - * @param address - The address to convert. - * @returns A 0x-prefixed hexidecimal checksummed address. - */ -export function toChecksumHexAddress(address: string) { - const hexPrefixed = addHexPrefix(address); - if (!isHexString(hexPrefixed)) { - // Version 5.1 of ethereumjs-utils would have returned '0xY' for input 'y' - // but we shouldn't waste effort trying to change case on a clearly invalid - // string. Instead just return the hex prefixed original string which most - // closely mimics the original behavior. - return hexPrefixed; - } - return toChecksumAddress(hexPrefixed); -} - -/** - * Validates that the input is a hex address. This utility method is a thin - * wrapper around ethereumjs-util.isValidAddress, with the exception that it - * does not throw an error when provided values that are not hex strings. In - * addition, and by default, this method will return true for hex strings that - * meet the length requirement of a hex address, but are not prefixed with `0x` - * Finally, if the mixedCaseUseChecksum flag is true and a mixed case string is - * provided this method will validate it has the proper checksum formatting. - * - * @param possibleAddress - Input parameter to check against. - * @param options - The validation options. - * @param options.allowNonPrefixed - If true will first ensure '0x' is prepended to the string. - * @returns Whether or not the input is a valid hex address. - */ -export function isValidHexAddress( - possibleAddress: string, - { allowNonPrefixed = true } = {}, -) { - const addressToCheck = allowNonPrefixed - ? addHexPrefix(possibleAddress) - : possibleAddress; - if (!isHexString(addressToCheck)) { - return false; - } - - return isValidAddress(addressToCheck); -} - -/** - * Validates a Transaction object for required properties and throws in - * the event of any validation error. - * - * @param transaction - Transaction object to validate. - */ -export function validateTransaction(transaction: Transaction) { - if ( - !transaction.from || - typeof transaction.from !== 'string' || - !isValidHexAddress(transaction.from) - ) { - throw new Error( - `Invalid "from" address: ${transaction.from} must be a valid string.`, - ); - } - - if (transaction.to === '0x' || transaction.to === undefined) { - if (transaction.data) { - delete transaction.to; - } else { - throw new Error( - `Invalid "to" address: ${transaction.to} must be a valid string.`, - ); - } - } else if ( - transaction.to !== undefined && - !isValidHexAddress(transaction.to) - ) { - throw new Error( - `Invalid "to" address: ${transaction.to} must be a valid string.`, - ); - } - - if (transaction.value !== undefined) { - const value = transaction.value.toString(); - if (value.includes('-')) { - throw new Error(`Invalid "value": ${value} is not a positive number.`); - } - - if (value.includes('.')) { - throw new Error( - `Invalid "value": ${value} number must be denominated in wei.`, - ); - } - const intValue = parseInt(transaction.value, 10); - const isValid = - Number.isFinite(intValue) && - !Number.isNaN(intValue) && - !isNaN(Number(value)) && - Number.isSafeInteger(intValue); - if (!isValid) { - throw new Error( - `Invalid "value": ${value} number must be a valid number.`, - ); - } - } -} - -/** - * A helper function that converts rawmessageData buffer data to a hex, or just returns the data if - * it is already formatted as a hex. - * - * @param data - The buffer data to convert to a hex. - * @returns A hex string conversion of the buffer data. - */ -export function normalizeMessageData(data: string) { - try { - const stripped = stripHexPrefix(data); - if (stripped.match(hexRe)) { - return addHexPrefix(stripped); - } - } catch (e) { - /* istanbul ignore next */ - } - return bufferToHex(Buffer.from(data, 'utf8')); -} - -/** - * Validates a PersonalMessageParams and MessageParams objects for required properties and throws in - * the event of any validation error. - * - * @param messageData - PersonalMessageParams object to validate. - */ -export function validateSignMessageData( - messageData: PersonalMessageParams | MessageParams, -) { - const { from, data } = messageData; - if (!from || typeof from !== 'string' || !isValidHexAddress(from)) { - throw new Error(`Invalid "from" address: ${from} must be a valid string.`); - } - - if (!data || typeof data !== 'string') { - throw new Error(`Invalid message "data": ${data} must be a valid string.`); - } -} - -/** - * Validates a TypedMessageParams object for required properties and throws in - * the event of any validation error for eth_signTypedMessage_V1. - * - * @param messageData - TypedMessageParams object to validate. - */ -export function validateTypedSignMessageDataV1( - messageData: TypedMessageParams, -) { - if ( - !messageData.from || - typeof messageData.from !== 'string' || - !isValidHexAddress(messageData.from) - ) { - throw new Error( - `Invalid "from" address: ${messageData.from} must be a valid string.`, - ); - } - - if (!messageData.data || !Array.isArray(messageData.data)) { - throw new Error( - `Invalid message "data": ${messageData.data} must be a valid array.`, - ); - } - - try { - // typedSignatureHash will throw if the data is invalid. - typedSignatureHash(messageData.data as any); - } catch (e) { - throw new Error(`Expected EIP712 typed data.`); - } -} - -/** - * Validates a TypedMessageParams object for required properties and throws in - * the event of any validation error for eth_signTypedMessage_V3. - * - * @param messageData - TypedMessageParams object to validate. - */ -export function validateTypedSignMessageDataV3( - messageData: TypedMessageParams, -) { - if ( - !messageData.from || - typeof messageData.from !== 'string' || - !isValidHexAddress(messageData.from) - ) { - throw new Error( - `Invalid "from" address: ${messageData.from} must be a valid string.`, - ); - } - - if (!messageData.data || typeof messageData.data !== 'string') { - throw new Error( - `Invalid message "data": ${messageData.data} must be a valid array.`, - ); - } - let data; - try { - data = JSON.parse(messageData.data); - } catch (e) { - throw new Error('Data must be passed as a valid JSON string.'); - } - const validation = validate(data, TYPED_MESSAGE_SCHEMA); - if (validation.errors.length > 0) { - throw new Error( - 'Data must conform to EIP-712 schema. See https://git.io/fNtcx.', - ); - } -} - -/** - * Validates a ERC20 token to be added with EIP747. - * - * @param token - Token object to validate. - */ -export function validateTokenToWatch(token: Token) { - const { address, symbol, decimals } = token; - if (!address || !symbol || typeof decimals === 'undefined') { - throw ethErrors.rpc.invalidParams( - `Must specify address, symbol, and decimals.`, - ); - } - - if (typeof symbol !== 'string') { - throw ethErrors.rpc.invalidParams(`Invalid symbol: not a string.`); - } - - if (symbol.length > 11) { - throw ethErrors.rpc.invalidParams( - `Invalid symbol "${symbol}": longer than 11 characters.`, - ); - } - const numDecimals = parseInt(decimals as unknown as string, 10); - if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) { - throw ethErrors.rpc.invalidParams( - `Invalid decimals "${decimals}": must be 0 <= 36.`, - ); - } - - if (!isValidHexAddress(address)) { - throw ethErrors.rpc.invalidParams(`Invalid address "${address}".`); - } -} - -/** - * Returns whether the given code corresponds to a smart contract. - * - * @param code - The potential smart contract code. - * @returns Whether the code was smart contract code or not. - */ -export function isSmartContractCode(code: string) { - /* istanbul ignore if */ - if (!code) { - return false; - } - // Geth will return '0x', and ganache-core v2.2.1 will return '0x0' - const smartContractCode = code !== '0x' && code !== '0x0'; - return smartContractCode; -} - -/** - * Execute fetch and verify that the response was successful. - * - * @param request - Request information. - * @param options - Fetch options. - * @returns The fetch response. - */ -export async function successfulFetch(request: string, options?: RequestInit) { - const response = await fetch(request, options); - if (!response.ok) { - throw new Error( - `Fetch failed with status '${response.status}' for request '${request}'`, - ); - } - return response; -} - -/** - * Execute fetch and return object response. - * - * @param request - The request information. - * @param options - The fetch options. - * @returns The fetch response JSON data. - */ -export async function handleFetch(request: string, options?: RequestInit) { - const response = await successfulFetch(request, options); - const object = await response.json(); - return object; -} - -/** - * Execute fetch and return object response, log if known error thrown, otherwise rethrow error. - * - * @param request - the request options object - * @param request.url - The request url to query. - * @param request.options - The fetch options. - * @param request.timeout - Timeout to fail request - * @param request.errorCodesToCatch - array of error codes for errors we want to catch in a particular context - * @returns The fetch response JSON data or undefined (if error occurs). - */ -export async function fetchWithErrorHandling({ - url, - options, - timeout, - errorCodesToCatch, -}: { - url: string; - options?: RequestInit; - timeout?: number; - errorCodesToCatch?: number[]; -}) { - let result; - try { - if (timeout) { - result = Promise.race([ - await handleFetch(url, options), - new Promise((_, reject) => - setTimeout(() => { - reject(TIMEOUT_ERROR); - }, timeout), - ), - ]); - } else { - result = await handleFetch(url, options); - } - } catch (e) { - logOrRethrowError(e, errorCodesToCatch); - } - return result; -} - -/** - * Fetch that fails after timeout. - * - * @param url - Url to fetch. - * @param options - Options to send with the request. - * @param timeout - Timeout to fail request. - * @returns Promise resolving the request. - */ -export async function timeoutFetch( - url: string, - options?: RequestInit, - timeout = 500, -): Promise { - return Promise.race([ - successfulFetch(url, options), - new Promise((_, reject) => - setTimeout(() => { - reject(TIMEOUT_ERROR); - }, timeout), - ), - ]); -} - -/** - * Normalizes the given ENS name. - * - * @param ensName - The ENS name. - * @returns The normalized ENS name string. - */ -export function normalizeEnsName(ensName: string): string | null { - if (ensName && typeof ensName === 'string') { - try { - const normalized = ensNamehash.normalize(ensName.trim()); - // this regex is only sufficient with the above call to ensNamehash.normalize - // TODO: change 7 in regex to 3 when shorter ENS domains are live - if (normalized.match(/^(([\w\d-]+)\.)*[\w\d-]{7,}\.(eth|test)$/u)) { - return normalized; - } - } catch (_) { - // do nothing - } - } - return null; -} - -/** - * Wrapper method to handle EthQuery requests. - * - * @param ethQuery - EthQuery object initialized with a provider. - * @param method - Method to request. - * @param args - Arguments to send. - * @returns Promise resolving the request. - */ -export function query( - ethQuery: any, - method: string, - args: any[] = [], -): Promise { - return new Promise((resolve, reject) => { - const cb = (error: Error, result: any) => { - if (error) { - reject(error); - return; - } - resolve(result); - }; - - if (typeof ethQuery[method] === 'function') { - ethQuery[method](...args, cb); - } else { - ethQuery.sendAsync({ method, params: args }, cb); - } - }); -} - -/** - * Checks if a transaction is EIP-1559 by checking for the existence of - * maxFeePerGas and maxPriorityFeePerGas within its parameters. - * - * @param transaction - Transaction object to add. - * @returns Boolean that is true if the transaction is EIP-1559 (has maxFeePerGas and maxPriorityFeePerGas), otherwise returns false. - */ -export const isEIP1559Transaction = (transaction: Transaction): boolean => { - const hasOwnProp = (obj: Transaction, key: string) => - Object.prototype.hasOwnProperty.call(obj, key); - return ( - hasOwnProp(transaction, 'maxFeePerGas') && - hasOwnProp(transaction, 'maxPriorityFeePerGas') - ); -}; - -/** - * Converts valid hex strings to decimal numbers, and handles unexpected arg types. - * - * @param value - a string that is either a hexadecimal with `0x` prefix or a decimal string. - * @returns a decimal number. - */ -export const convertHexToDecimal = ( - value: string | undefined = '0x0', -): number => { - if (isHexString(value)) { - return parseInt(value, 16); - } - - return Number(value) ? Number(value) : 0; -}; - -export const getIncreasedPriceHex = (value: number, rate: number): string => - addHexPrefix(`${parseInt(`${value * rate}`, 10).toString(16)}`); - -export const getIncreasedPriceFromExisting = ( - value: string | undefined, - rate: number, -): string => { - return getIncreasedPriceHex(convertHexToDecimal(value), rate); -}; - -export const validateGasValues = ( - gasValues: GasPriceValue | FeeMarketEIP1559Values, -) => { - Object.keys(gasValues).forEach((key) => { - const value = (gasValues as any)[key]; - if (typeof value !== 'string' || !isHexString(value)) { - throw new TypeError( - `expected hex string for ${key} but received: ${value}`, - ); - } - }); -}; - -export const isFeeMarketEIP1559Values = ( - gasValues?: GasPriceValue | FeeMarketEIP1559Values, -): gasValues is FeeMarketEIP1559Values => - (gasValues as FeeMarketEIP1559Values)?.maxFeePerGas !== undefined || - (gasValues as FeeMarketEIP1559Values)?.maxPriorityFeePerGas !== undefined; - -export const isGasPriceValue = ( - gasValues?: GasPriceValue | FeeMarketEIP1559Values, -): gasValues is GasPriceValue => - (gasValues as GasPriceValue)?.gasPrice !== undefined; - -/** - * Validates that the proposed value is greater than or equal to the minimum value. - * - * @param proposed - The proposed value. - * @param min - The minimum value. - * @returns The proposed value. - * @throws Will throw if the proposed value is too low. - */ -export function validateMinimumIncrease(proposed: string, min: string) { - const proposedDecimal = convertHexToDecimal(proposed); - const minDecimal = convertHexToDecimal(min); - if (proposedDecimal >= minDecimal) { - return proposed; - } - const errorMsg = `The proposed value: ${proposedDecimal} should meet or exceed the minimum value: ${minDecimal}`; - throw new Error(errorMsg); -} - -/** - * Removes IPFS protocol prefix from input string. - * - * @param ipfsUrl - An IPFS url (e.g. ipfs://{content id}) - * @returns IPFS content identifier and (possibly) path in a string - * @throws Will throw if the url passed is not IPFS. - */ -export function removeIpfsProtocolPrefix(ipfsUrl: string) { - if (ipfsUrl.startsWith('ipfs://ipfs/')) { - return ipfsUrl.replace('ipfs://ipfs/', ''); - } else if (ipfsUrl.startsWith('ipfs://')) { - return ipfsUrl.replace('ipfs://', ''); - } - // this method should not be used with non-ipfs urls (i.e. startsWith('ipfs://') === true) - throw new Error('this method should not be used with non ipfs urls'); -} - -/** - * Extracts content identifier and path from an input string. - * - * @param ipfsUrl - An IPFS URL minus the IPFS protocol prefix - * @returns IFPS content identifier (cid) and sub path as string. - * @throws Will throw if the url passed is not ipfs. - */ -export function getIpfsCIDv1AndPath(ipfsUrl: string): { - cid: string; - path?: string; -} { - const url = removeIpfsProtocolPrefix(ipfsUrl); - - // check if there is a path - // (CID is everything preceding first forward slash, path is everything after) - const index = url.indexOf('/'); - const cid = index !== -1 ? url.substring(0, index) : url; - const path = index !== -1 ? url.substring(index) : undefined; - - // We want to ensure that the CID is v1 (https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) - // because most cid v0s appear to be incompatible with IPFS subdomains - return { - cid: CID.parse(cid).toV1().toString(), - path, - }; -} - -/** - * Adds URL protocol prefix to input URL string if missing. - * - * @param urlString - An IPFS URL. - * @returns A URL with a https:// prepended. - */ -export function addUrlProtocolPrefix(urlString: string): string { - if (!urlString.match(/(^http:\/\/)|(^https:\/\/)/u)) { - return `https://${urlString}`; - } - return urlString; -} - -/** - * Formats URL correctly for use retrieving assets hosted on IPFS. - * - * @param ipfsGateway - The users preferred IPFS gateway (full URL or just host). - * @param ipfsUrl - The IFPS URL pointed at the asset. - * @param subdomainSupported - Boolean indicating whether the URL should be formatted with subdomains or not. - * @returns A formatted URL, with the user's preferred IPFS gateway and format (subdomain or not), pointing to an asset hosted on IPFS. - */ -export function getFormattedIpfsUrl( - ipfsGateway: string, - ipfsUrl: string, - subdomainSupported: boolean, -): string { - const { host, protocol, origin } = new URL(addUrlProtocolPrefix(ipfsGateway)); - if (subdomainSupported) { - const { cid, path } = getIpfsCIDv1AndPath(ipfsUrl); - return `${protocol}//${cid}.ipfs.${host}${path ?? ''}`; - } - const cidAndPath = removeIpfsProtocolPrefix(ipfsUrl); - return `${origin}/ipfs/${cidAndPath}`; -} - -type PlainObject = Record; - -/** - * Determines whether a value is a "plain" object. - * - * @param value - A value to check - * @returns True if the passed value is a plain object - */ -export function isPlainObject(value: unknown): value is PlainObject { - return Boolean(value) && typeof value === 'object' && !Array.isArray(value); -} - -export const hasProperty = ( - object: PlainObject, - key: string | number | symbol, -) => Reflect.hasOwnProperty.call(object, key); - -/** - * Like {@link Array}, but always non-empty. - * - * @template T - The non-empty array member type. - */ -export type NonEmptyArray = [T, ...T[]]; - -/** - * Type guard for {@link NonEmptyArray}. - * - * @template T - The non-empty array member type. - * @param value - The value to check. - * @returns Whether the value is a non-empty array. - */ -export function isNonEmptyArray(value: T[]): value is NonEmptyArray { - return Array.isArray(value) && value.length > 0; -} - -/** - * Type guard for {@link Json}. - * - * @param value - The value to check. - * @returns Whether the value is valid JSON. - */ -export function isValidJson(value: unknown): value is Json { - try { - return deepEqual(value, JSON.parse(JSON.stringify(value))); - } catch (_) { - return false; - } -} - -/** - * Networks where token detection is supported - Values are in decimal format - */ -export enum SupportedTokenDetectionNetworks { - mainnet = '1', - bsc = '56', - polygon = '137', - avax = '43114', -} - -/** - * Check if token detection is enabled for certain networks. - * - * @param chainId - ChainID of network - * @returns Whether the current network supports token detection - */ -export function isTokenDetectionSupportedForNetwork(chainId: string): boolean { - return Object.values(SupportedTokenDetectionNetworks).includes( - chainId, - ); -} - -/** - * Check if token list polling is enabled for a given network. - * Currently this method is used to support e2e testing for consumers of this package. - * - * @param chainId - ChainID of network - * @returns Whether the current network supports tokenlists - */ -export function isTokenListSupportedForNetwork(chainId: string): boolean { - const chainIdDecimal = convertHexToDecimal(chainId).toString(); - return ( - isTokenDetectionSupportedForNetwork(chainIdDecimal) || - chainIdDecimal === GANACHE_CHAIN_ID - ); -} - -/** - * Utility method to log if error is a common fetch error and otherwise rethrow it. - * - * @param error - Caught error that we should either rethrow or log to console - * @param codesToCatch - array of error codes for errors we want to catch and log in a particular context - */ -function logOrRethrowError(error: any, codesToCatch: number[] = []) { - if (!error) { - return; - } - - const includesErrorCodeToCatch = codesToCatch.some((code) => - error.message?.includes(`Fetch failed with status '${code}'`), - ); - - if ( - error instanceof Error && - (includesErrorCodeToCatch || - error.message?.includes('Failed to fetch') || - error === TIMEOUT_ERROR) - ) { - console.error(error); - } else { - throw error; - } -} diff --git a/tests/setupTests.ts b/tests/setup.ts similarity index 100% rename from tests/setupTests.ts rename to tests/setup.ts diff --git a/tsconfig.build.json b/tsconfig.build.json index e2c5987c4b..e96861ff25 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,10 +1,60 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "declaration": true, - "declarationDir": "./dist", - "outDir": "./dist", - "rootDir": "./src" - }, - "exclude": ["**/*.test.ts"] + "references": [ + { + "path": "./packages/address-book-controller/tsconfig.build.json" + }, + { + "path": "./packages/announcement-controller/tsconfig.build.json" + }, + { + "path": "./packages/approval-controller/tsconfig.build.json" + }, + { + "path": "./packages/assets-controllers/tsconfig.build.json" + }, + { + "path": "./packages/base-controller/tsconfig.build.json" + }, + { + "path": "./packages/composable-controller/tsconfig.build.json" + }, + { + "path": "./packages/controller-utils/tsconfig.build.json" + }, + { + "path": "./packages/ens-controller/tsconfig.build.json" + }, + { + "path": "./packages/gas-fee-controller/tsconfig.build.json" + }, + { + "path": "./packages/keyring-controller/tsconfig.build.json" + }, + { + "path": "./packages/message-manager/tsconfig.build.json" + }, + { + "path": "./packages/network-controller/tsconfig.build.json" + }, + { + "path": "./packages/notification-controller/tsconfig.build.json" + }, + { + "path": "./packages/permission-controller/tsconfig.build.json" + }, + { + "path": "./packages/phishing-controller/tsconfig.build.json" + }, + { + "path": "./packages/preferences-controller/tsconfig.build.json" + }, + { + "path": "./packages/rate-limit-controller/tsconfig.build.json" + }, + { + "path": "./packages/subject-metadata-controller/tsconfig.build.json" + } + ], + "files": [], + "include": [] } diff --git a/tsconfig.json b/tsconfig.json index de3572e5f7..1fcd6fd18d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,64 @@ { "compilerOptions": { - "baseUrl": ".", "esModuleInterop": true, - "inlineSources": true, - "module": "commonjs", - "moduleResolution": "node", - "sourceMap": true, - "strict": true, - "target": "es6" + "noEmit": true }, - "include": ["./types/**/*.d.ts", "./src/**/*.ts"] + "references": [ + { + "path": "./packages/address-book-controller" + }, + { + "path": "./packages/announcement-controller" + }, + { + "path": "./packages/approval-controller" + }, + { + "path": "./packages/assets-controllers" + }, + { + "path": "./packages/base-controller" + }, + { + "path": "./packages/composable-controller" + }, + { + "path": "./packages/controller-utils" + }, + { + "path": "./packages/ens-controller" + }, + { + "path": "./packages/gas-fee-controller" + }, + { + "path": "./packages/keyring-controller" + }, + { + "path": "./packages/message-manager" + }, + { + "path": "./packages/network-controller" + }, + { + "path": "./packages/notification-controller" + }, + { + "path": "./packages/permission-controller" + }, + { + "path": "./packages/phishing-controller" + }, + { + "path": "./packages/preferences-controller" + }, + { + "path": "./packages/rate-limit-controller" + }, + { + "path": "./packages/subject-metadata-controller" + } + ], + "files": [], + "include": [] } diff --git a/tsconfig.packages.build.json b/tsconfig.packages.build.json new file mode 100644 index 0000000000..7ff3998a9f --- /dev/null +++ b/tsconfig.packages.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.packages.json", + "compilerOptions": { + "declaration": true, + "inlineSources": true, + "sourceMap": true + }, + "exclude": ["./jest.config.packages.ts", "**/*.test.ts", "**/jest.config.ts"] +} diff --git a/tsconfig.packages.json b/tsconfig.packages.json new file mode 100644 index 0000000000..cb223deaae --- /dev/null +++ b/tsconfig.packages.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "composite": true, + "esModuleInterop": true, + "lib": ["ES2017", "DOM"], + "module": "CommonJS", + "moduleResolution": "node", + /** + * Here we ensure that TypeScript resolves `@metamask/*` imports to the + * uncompiled source code for packages that live in this repo. + * + * NOTE: This must be synchronized with the `moduleNameMapper` option in + * `jest.config.packages.js`. + */ + "paths": { + "@metamask/*": ["../*/src"] + }, + "strict": true, + "target": "es6" + } +} diff --git a/yarn.lock b/yarn.lock index 19bcad1f20..3167c4c8dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1031,17 +1031,6 @@ __metadata: languageName: node linkType: hard -"@jest/environment@npm:^25.5.0": - version: 25.5.0 - resolution: "@jest/environment@npm:25.5.0" - dependencies: - "@jest/fake-timers": ^25.5.0 - "@jest/types": ^25.5.0 - jest-mock: ^25.5.0 - checksum: 93a9ddbcfafef26c21bb880ea947493f4b248e5d929ed165290079ac28559fa0d6983641ad57abe30d9ae13d3ecf73034964e2adc3b7bb207f1888818e6a3432 - languageName: node - linkType: hard - "@jest/environment@npm:^26.3.0": version: 26.3.0 resolution: "@jest/environment@npm:26.3.0" @@ -1054,19 +1043,6 @@ __metadata: languageName: node linkType: hard -"@jest/fake-timers@npm:^25.5.0": - version: 25.5.0 - resolution: "@jest/fake-timers@npm:25.5.0" - dependencies: - "@jest/types": ^25.5.0 - jest-message-util: ^25.5.0 - jest-mock: ^25.5.0 - jest-util: ^25.5.0 - lolex: ^5.0.0 - checksum: e34dc713a2e26e936aa15d0d6f479ad9ffbea13d50436f873631fd8077fd746d23e2ce1f0bd2ac32fe99f0dac3eae35960a59fdd98830c0134819e5c9b7e822e - languageName: node - linkType: hard - "@jest/fake-timers@npm:^26.3.0": version: 26.3.0 resolution: "@jest/fake-timers@npm:26.3.0" @@ -1323,9 +1299,114 @@ __metadata: languageName: node linkType: hard -"@metamask/auto-changelog@npm:^2.6.0": - version: 2.6.0 - resolution: "@metamask/auto-changelog@npm:2.6.0" +"@metamask/action-utils@npm:^0.0.2": + version: 0.0.2 + resolution: "@metamask/action-utils@npm:0.0.2" + dependencies: + "@types/semver": ^7.3.6 + glob: ^7.1.7 + semver: ^7.3.5 + checksum: 4d3552d77a329791e2b1da0ca5023e9e04c1920c61f06ef070e8b4f4f072dd1f632124003964d47cdebf314a621a12d7209fbdd6871db37cfa1330a6ed679a11 + languageName: node + linkType: hard + +"@metamask/address-book-controller@workspace:packages/address-book-controller, @metamask/address-book-controller@workspace:~": + version: 0.0.0-use.local + resolution: "@metamask/address-book-controller@workspace:packages/address-book-controller" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/controller-utils": "workspace:~" + "@types/jest": ^26.0.22 + deepmerge: ^4.2.2 + jest: ^26.4.2 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + +"@metamask/announcement-controller@workspace:packages/announcement-controller": + version: 0.0.0-use.local + resolution: "@metamask/announcement-controller@workspace:packages/announcement-controller" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@types/jest": ^26.0.22 + deepmerge: ^4.2.2 + jest: ^26.4.2 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + +"@metamask/approval-controller@workspace:packages/approval-controller, @metamask/approval-controller@workspace:~": + version: 0.0.0-use.local + resolution: "@metamask/approval-controller@workspace:packages/approval-controller" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@types/jest": ^26.0.22 + deepmerge: ^4.2.2 + eth-rpc-errors: ^4.0.0 + immer: ^9.0.6 + jest: ^26.4.2 + nanoid: ^3.1.31 + sinon: ^9.2.4 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + +"@metamask/assets-controllers@workspace:packages/assets-controllers, @metamask/assets-controllers@workspace:~": + version: 0.0.0-use.local + resolution: "@metamask/assets-controllers@workspace:packages/assets-controllers" + dependencies: + "@ethersproject/abi": ^5.7.0 + "@ethersproject/contracts": ^5.7.0 + "@ethersproject/providers": ^5.7.0 + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/contract-metadata": ^1.35.0 + "@metamask/controller-utils": "workspace:~" + "@metamask/metamask-eth-abis": 3.0.0 + "@metamask/network-controller": "workspace:~" + "@metamask/preferences-controller": "workspace:~" + "@types/jest": ^26.0.22 + "@types/node": ^14.14.31 + "@types/uuid": ^8.3.0 + "@types/web3": ^1.0.6 + abort-controller: ^3.0.0 + async-mutex: ^0.2.6 + babel-runtime: ^6.26.0 + deepmerge: ^4.2.2 + eth-query: ^2.1.2 + eth-rpc-errors: ^4.0.0 + ethereumjs-util: ^7.0.10 + ethjs-provider-http: ^0.1.6 + immer: ^9.0.6 + jest: ^26.4.2 + multiformats: ^9.5.2 + nock: ^13.0.7 + single-call-balance-checker-abi: ^1.0.0 + sinon: ^9.2.4 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + uuid: ^8.3.2 + web3: ^0.20.7 + languageName: unknown + linkType: soft + +"@metamask/auto-changelog@npm:^3.0.0": + version: 3.0.0 + resolution: "@metamask/auto-changelog@npm:3.0.0" dependencies: diff: ^5.0.0 execa: ^5.1.1 @@ -1333,10 +1414,28 @@ __metadata: yargs: ^17.0.1 bin: auto-changelog: dist/cli.js - checksum: f01ae6a1aef85c277b155a0b0bbfb1d8f4d17a3e93888e7c60780e1fd5f7b2a1730b3ba3247487a91fb4beb9ddfb0fe9d3ba61f4b84391dc5bde2d4607c575c8 + checksum: ee6f41b466e8f0deb8bc454936513602c4767b7be94f704da3579e3100154c92779dcfde542076158138d5fcfb64ce491ab7fc30248ae097c7d903be0cad9fb4 languageName: node linkType: hard +"@metamask/base-controller@workspace:packages/base-controller, @metamask/base-controller@workspace:~": + version: 0.0.0-use.local + resolution: "@metamask/base-controller@workspace:packages/base-controller" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@types/jest": ^26.0.22 + "@types/sinon": ^9.0.10 + deepmerge: ^4.2.2 + immer: ^9.0.6 + jest: ^26.4.2 + sinon: ^9.2.4 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + "@metamask/bip39@npm:^4.0.0": version: 4.0.0 resolution: "@metamask/bip39@npm:4.0.0" @@ -1349,6 +1448,30 @@ __metadata: languageName: node linkType: hard +"@metamask/composable-controller@workspace:packages/composable-controller": + version: 0.0.0-use.local + resolution: "@metamask/composable-controller@workspace:packages/composable-controller" + dependencies: + "@metamask/address-book-controller": "workspace:~" + "@metamask/assets-controllers": "workspace:~" + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/controller-utils": "workspace:~" + "@metamask/ens-controller": "workspace:~" + "@metamask/network-controller": "workspace:~" + "@metamask/preferences-controller": "workspace:~" + "@types/jest": ^26.0.22 + deepmerge: ^4.2.2 + immer: ^9.0.6 + jest: ^26.4.2 + sinon: ^9.2.4 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + "@metamask/contract-metadata@npm:^1.35.0": version: 1.35.0 resolution: "@metamask/contract-metadata@npm:1.35.0" @@ -1356,40 +1479,42 @@ __metadata: languageName: node linkType: hard -"@metamask/controllers@workspace:.": +"@metamask/controller-utils@workspace:packages/controller-utils, @metamask/controller-utils@workspace:~": version: 0.0.0-use.local - resolution: "@metamask/controllers@workspace:." + resolution: "@metamask/controller-utils@workspace:packages/controller-utils" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@types/jest": ^26.0.22 + abort-controller: ^3.0.0 + deepmerge: ^4.2.2 + eth-ens-namehash: ^2.0.8 + eth-rpc-errors: ^4.0.0 + ethereumjs-util: ^7.0.10 + ethjs-unit: ^0.1.6 + fast-deep-equal: ^3.1.3 + isomorphic-fetch: ^3.0.0 + jest: ^26.4.2 + nock: ^13.0.7 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + +"@metamask/controllers-monorepo@workspace:.": + version: 0.0.0-use.local + resolution: "@metamask/controllers-monorepo@workspace:." dependencies: - "@ethereumjs/common": ^2.3.1 - "@ethereumjs/tx": ^3.2.1 - "@ethersproject/abi": ^5.7.0 - "@ethersproject/contracts": ^5.7.0 - "@ethersproject/providers": ^5.7.0 - "@keystonehq/bc-ur-registry-eth": ^0.9.0 - "@keystonehq/metamask-airgapped-keyring": ^0.6.1 "@lavamoat/allow-scripts": ^2.0.2 - "@metamask/auto-changelog": ^2.6.0 - "@metamask/contract-metadata": ^1.35.0 + "@metamask/create-release-branch": ^1.0.0 "@metamask/eslint-config": ^9.0.0 "@metamask/eslint-config-jest": ^9.0.0 "@metamask/eslint-config-nodejs": ^9.0.0 "@metamask/eslint-config-typescript": ^9.0.1 - "@metamask/metamask-eth-abis": 3.0.0 - "@metamask/types": ^1.1.0 - "@types/deep-freeze-strict": ^1.1.0 - "@types/jest": ^26.0.22 - "@types/jest-when": ^2.7.3 - "@types/node": ^14.14.31 - "@types/punycode": ^2.1.0 - "@types/sinon": ^9.0.10 - "@types/uuid": ^8.3.0 - "@types/web3": ^1.0.6 "@typescript-eslint/eslint-plugin": ^4.33.0 "@typescript-eslint/parser": ^4.33.0 - abort-controller: ^3.0.0 - async-mutex: ^0.2.6 - babel-runtime: ^6.26.0 - deep-freeze-strict: ^1.1.1 eslint: ^7.24.0 eslint-config-prettier: ^8.5.0 eslint-import-resolver-typescript: ^2.4.0 @@ -1398,43 +1523,49 @@ __metadata: eslint-plugin-jsdoc: ^36.1.0 eslint-plugin-node: ^11.1.0 eslint-plugin-prettier: ^3.4.1 - eth-ens-namehash: ^2.0.8 - eth-json-rpc-infura: ^5.1.0 - eth-keyring-controller: ^7.0.2 - eth-method-registry: 1.1.0 - eth-phishing-detect: ^1.2.0 - eth-query: ^2.1.2 - eth-rpc-errors: ^4.0.0 - eth-sig-util: ^3.0.0 - ethereumjs-util: ^7.0.10 - ethereumjs-wallet: ^1.0.1 - ethjs-provider-http: ^0.1.6 - ethjs-unit: ^0.1.6 - fast-deep-equal: ^3.1.3 - immer: ^9.0.6 isomorphic-fetch: ^3.0.0 - jest: ^26.4.2 - jest-environment-jsdom: ^25.0.0 - jest-when: ^3.4.2 - json-rpc-engine: ^6.1.0 - jsonschema: ^1.2.4 - multiformats: ^9.5.2 - nanoid: ^3.1.31 - nock: ^13.0.7 prettier: ^2.6.2 prettier-plugin-packagejson: ^2.2.17 - punycode: ^2.1.1 rimraf: ^3.0.2 simple-git-hooks: ^2.8.0 - single-call-balance-checker-abi: ^1.0.0 - sinon: ^9.2.4 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + +"@metamask/create-release-branch@npm:^1.0.0": + version: 1.0.0 + resolution: "@metamask/create-release-branch@npm:1.0.0" + dependencies: + "@metamask/action-utils": ^0.0.2 + "@metamask/utils": ^3.0.3 + debug: ^4.3.4 + execa: ^5.0.0 + glob: ^8.0.3 + pony-cause: ^2.1.0 + semver: ^7.3.7 + which: ^2.0.2 + yaml: ^2.1.1 + yargs: ^17.5.1 + bin: + create-release-branch: bin/create-release-branch.js + checksum: e720f0cc2d0ba13246368891f667b55992093b10c751548af344943cb5146775261d1ca1c2c5789bd01a34bd76adbeb4717cdad6173ded6ca24b5d4167343f13 + languageName: node + linkType: hard + +"@metamask/ens-controller@workspace:packages/ens-controller, @metamask/ens-controller@workspace:~": + version: 0.0.0-use.local + resolution: "@metamask/ens-controller@workspace:packages/ens-controller" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/controller-utils": "workspace:~" + "@types/jest": ^26.0.22 + deepmerge: ^4.2.2 + jest: ^26.4.2 ts-jest: ^26.5.2 typedoc: ^0.22.15 typedoc-plugin-missing-exports: ^0.22.6 typescript: ~4.6.3 - uuid: ^8.3.2 - web3: ^0.20.7 - web3-provider-engine: ^16.0.3 languageName: unknown linkType: soft @@ -1513,6 +1644,87 @@ __metadata: languageName: node linkType: hard +"@metamask/gas-fee-controller@workspace:packages/gas-fee-controller": + version: 0.0.0-use.local + resolution: "@metamask/gas-fee-controller@workspace:packages/gas-fee-controller" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/controller-utils": "workspace:~" + "@metamask/network-controller": "workspace:~" + "@types/jest": ^26.0.22 + "@types/jest-when": ^2.7.3 + "@types/uuid": ^8.3.0 + babel-runtime: ^6.26.0 + deepmerge: ^4.2.2 + eth-query: ^2.1.2 + ethereumjs-util: ^7.0.10 + ethjs-unit: ^0.1.6 + immer: ^9.0.6 + jest: ^26.4.2 + jest-when: ^3.4.2 + nock: ^13.0.7 + sinon: ^9.2.4 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + uuid: ^8.3.2 + languageName: unknown + linkType: soft + +"@metamask/keyring-controller@workspace:packages/keyring-controller": + version: 0.0.0-use.local + resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" + dependencies: + "@ethereumjs/common": ^2.3.1 + "@ethereumjs/tx": ^3.2.1 + "@keystonehq/bc-ur-registry-eth": ^0.9.0 + "@keystonehq/metamask-airgapped-keyring": ^0.6.1 + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/controller-utils": "workspace:~" + "@metamask/message-manager": "workspace:~" + "@metamask/preferences-controller": "workspace:~" + "@types/jest": ^26.0.22 + async-mutex: ^0.2.6 + deepmerge: ^4.2.2 + eth-keyring-controller: ^7.0.2 + eth-sig-util: ^3.0.0 + ethereumjs-util: ^7.0.10 + ethereumjs-wallet: ^1.0.1 + jest: ^26.4.2 + sinon: ^9.2.4 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + uuid: ^8.3.2 + languageName: unknown + linkType: soft + +"@metamask/message-manager@workspace:packages/message-manager, @metamask/message-manager@workspace:~": + version: 0.0.0-use.local + resolution: "@metamask/message-manager@workspace:packages/message-manager" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/controller-utils": "workspace:~" + "@types/jest": ^26.0.22 + "@types/uuid": ^8.3.0 + deepmerge: ^4.2.2 + eth-sig-util: ^3.0.0 + ethereumjs-util: ^7.0.10 + jest: ^26.4.2 + jsonschema: ^1.2.4 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + uuid: ^8.3.2 + languageName: unknown + linkType: soft + "@metamask/metamask-eth-abis@npm:3.0.0": version: 3.0.0 resolution: "@metamask/metamask-eth-abis@npm:3.0.0" @@ -1520,15 +1732,141 @@ __metadata: languageName: node linkType: hard +"@metamask/network-controller@workspace:packages/network-controller, @metamask/network-controller@workspace:~": + version: 0.0.0-use.local + resolution: "@metamask/network-controller@workspace:packages/network-controller" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/controller-utils": "workspace:~" + "@types/jest": ^26.0.22 + async-mutex: ^0.2.6 + babel-runtime: ^6.26.0 + deepmerge: ^4.2.2 + eth-json-rpc-infura: ^5.1.0 + eth-query: ^2.1.2 + immer: ^9.0.6 + jest: ^26.4.2 + sinon: ^9.2.4 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + web3-provider-engine: ^16.0.3 + languageName: unknown + linkType: soft + +"@metamask/notification-controller@workspace:packages/notification-controller": + version: 0.0.0-use.local + resolution: "@metamask/notification-controller@workspace:packages/notification-controller" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/controller-utils": "workspace:~" + "@types/jest": ^26.0.22 + deepmerge: ^4.2.2 + immer: ^9.0.6 + jest: ^26.4.2 + nanoid: ^3.1.31 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + "@metamask/obs-store@npm:^7.0.0": version: 7.0.0 resolution: "@metamask/obs-store@npm:7.0.0" dependencies: - "@metamask/safe-event-emitter": ^2.0.0 - through2: ^2.0.3 - checksum: e1497140384de0ac689adbe7286df43e843c5d73fd8ba7080af2faab3de73e823b46b8214be1c839d9e9e5f86fb40df50a26e93bae936329daeaedae5e523323 - languageName: node - linkType: hard + "@metamask/safe-event-emitter": ^2.0.0 + through2: ^2.0.3 + checksum: e1497140384de0ac689adbe7286df43e843c5d73fd8ba7080af2faab3de73e823b46b8214be1c839d9e9e5f86fb40df50a26e93bae936329daeaedae5e523323 + languageName: node + linkType: hard + +"@metamask/permission-controller@workspace:packages/permission-controller, @metamask/permission-controller@workspace:~": + version: 0.0.0-use.local + resolution: "@metamask/permission-controller@workspace:packages/permission-controller" + dependencies: + "@metamask/approval-controller": "workspace:~" + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/controller-utils": "workspace:~" + "@metamask/types": ^1.1.0 + "@types/deep-freeze-strict": ^1.1.0 + "@types/jest": ^26.0.22 + deep-freeze-strict: ^1.1.1 + deepmerge: ^4.2.2 + eth-rpc-errors: ^4.0.0 + immer: ^9.0.6 + jest: ^26.4.2 + json-rpc-engine: ^6.1.0 + nanoid: ^3.1.31 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + +"@metamask/phishing-controller@workspace:packages/phishing-controller": + version: 0.0.0-use.local + resolution: "@metamask/phishing-controller@workspace:packages/phishing-controller" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/controller-utils": "workspace:~" + "@types/jest": ^26.0.22 + "@types/punycode": ^2.1.0 + deepmerge: ^4.2.2 + eth-phishing-detect: ^1.2.0 + isomorphic-fetch: ^3.0.0 + jest: ^26.4.2 + nock: ^13.0.7 + punycode: ^2.1.1 + sinon: ^9.2.4 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + +"@metamask/preferences-controller@workspace:packages/preferences-controller, @metamask/preferences-controller@workspace:~": + version: 0.0.0-use.local + resolution: "@metamask/preferences-controller@workspace:packages/preferences-controller" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/controller-utils": "workspace:~" + "@types/jest": ^26.0.22 + deepmerge: ^4.2.2 + jest: ^26.4.2 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + +"@metamask/rate-limit-controller@workspace:packages/rate-limit-controller": + version: 0.0.0-use.local + resolution: "@metamask/rate-limit-controller@workspace:packages/rate-limit-controller" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@types/jest": ^26.0.22 + deepmerge: ^4.2.2 + eth-rpc-errors: ^4.0.0 + immer: ^9.0.6 + jest: ^26.4.2 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + languageName: unknown + linkType: soft "@metamask/safe-event-emitter@npm:^2.0.0": version: 2.0.0 @@ -1537,6 +1875,56 @@ __metadata: languageName: node linkType: hard +"@metamask/subject-metadata-controller@workspace:packages/subject-metadata-controller": + version: 0.0.0-use.local + resolution: "@metamask/subject-metadata-controller@workspace:packages/subject-metadata-controller" + dependencies: + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/permission-controller": "workspace:~" + "@metamask/types": ^1.1.0 + "@types/jest": ^26.0.22 + deepmerge: ^4.2.2 + immer: ^9.0.6 + jest: ^26.4.2 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + +"@metamask/transaction-controller@workspace:packages/transaction-controller": + version: 0.0.0-use.local + resolution: "@metamask/transaction-controller@workspace:packages/transaction-controller" + dependencies: + "@ethereumjs/common": ^2.3.1 + "@ethereumjs/tx": ^3.2.1 + "@metamask/auto-changelog": ^3.0.0 + "@metamask/base-controller": "workspace:~" + "@metamask/controller-utils": "workspace:~" + "@metamask/network-controller": "workspace:~" + "@types/jest": ^26.0.22 + "@types/node": ^14.14.31 + async-mutex: ^0.2.6 + babel-runtime: ^6.26.0 + deepmerge: ^4.2.2 + eth-method-registry: 1.1.0 + eth-query: ^2.1.2 + eth-rpc-errors: ^4.0.0 + ethereumjs-util: ^7.0.10 + ethjs-provider-http: ^0.1.6 + isomorphic-fetch: ^3.0.0 + jest: ^26.4.2 + sinon: ^9.2.4 + ts-jest: ^26.5.2 + typedoc: ^0.22.15 + typedoc-plugin-missing-exports: ^0.22.6 + typescript: ~4.6.3 + uuid: ^8.3.2 + languageName: unknown + linkType: soft + "@metamask/types@npm:^1.1.0": version: 1.1.0 resolution: "@metamask/types@npm:1.1.0" @@ -1544,6 +1932,18 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^3.0.3": + version: 3.2.0 + resolution: "@metamask/utils@npm:3.2.0" + dependencies: + "@types/debug": ^4.1.7 + debug: ^4.3.4 + fast-deep-equal: ^3.1.3 + superstruct: ^0.16.5 + checksum: 99adcbd273c69075628913259f8c3fb843291898eba813f4f5fe0bfc060ae5955e2c69e70e15b04156793f8d84edd077d1fac3f8c3927e067d0f311eef9d4469 + languageName: node + linkType: hard + "@ngraveio/bc-ur@npm:^1.1.5": version: 1.1.6 resolution: "@ngraveio/bc-ur@npm:1.1.6" @@ -1804,6 +2204,15 @@ __metadata: languageName: node linkType: hard +"@types/debug@npm:^4.1.7": + version: 4.1.7 + resolution: "@types/debug@npm:4.1.7" + dependencies: + "@types/ms": "*" + checksum: 0a7b89d8ed72526858f0b61c6fd81f477853e8c4415bb97f48b1b5545248d2ae389931680b94b393b993a7cfe893537a200647d93defe6d87159b96812305adc + languageName: node + linkType: hard + "@types/deep-freeze-strict@npm:^1.1.0": version: 1.1.0 resolution: "@types/deep-freeze-strict@npm:1.1.0" @@ -1932,6 +2341,13 @@ __metadata: languageName: node linkType: hard +"@types/ms@npm:*": + version: 0.7.31 + resolution: "@types/ms@npm:0.7.31" + checksum: daadd354aedde024cce6f5aa873fefe7b71b22cd0e28632a69e8b677aeb48ae8caa1c60e5919bb781df040d116b01cb4316335167a3fc0ef6a63fa3614c0f6da + languageName: node + linkType: hard + "@types/node@npm:*": version: 10.14.15 resolution: "@types/node@npm:10.14.15" @@ -1992,6 +2408,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7.3.6": + version: 7.3.12 + resolution: "@types/semver@npm:7.3.12" + checksum: 35536b2fc5602904f21cae681f6c9498e177dab3f54ae37c92f9a1b7e43c35f18bcd81e1c98c1cf0d33ee046bb06c771e9928c1c00a401d56a03f56549252a15 + languageName: node + linkType: hard + "@types/sinon@npm:^9.0.10": version: 9.0.10 resolution: "@types/sinon@npm:9.0.10" @@ -2225,13 +2648,6 @@ __metadata: languageName: node linkType: hard -"abab@npm:^2.0.0": - version: 2.0.5 - resolution: "abab@npm:2.0.5" - checksum: 0ec951b46d5418c2c2f923021ec193eaebdb4e802ffd5506286781b454be722a13a8430f98085cd3e204918401d9130ec6cc8f5ae19be315b3a0e857d83196e1 - languageName: node - linkType: hard - "abab@npm:^2.0.3": version: 2.0.4 resolution: "abab@npm:2.0.4" @@ -2273,16 +2689,6 @@ __metadata: languageName: node linkType: hard -"acorn-globals@npm:^4.3.2": - version: 4.3.4 - resolution: "acorn-globals@npm:4.3.4" - dependencies: - acorn: ^6.0.1 - acorn-walk: ^6.0.1 - checksum: c31bfde102d8a104835e9591c31dd037ec771449f9c86a6b1d2ac3c7c336694f828cfabba7687525b094f896a854affbf1afe6e1b12c0d998be6bab5d49c9663 - languageName: node - linkType: hard - "acorn-globals@npm:^6.0.0": version: 6.0.0 resolution: "acorn-globals@npm:6.0.0" @@ -2302,13 +2708,6 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^6.0.1": - version: 6.2.0 - resolution: "acorn-walk@npm:6.2.0" - checksum: ea241a5d96338f1e8030aafae72a91ff0ec4360e2775e44a2fdb2eb618b07fc309e000a5126056631ac7f00fe8bd9bbd23fcb6d018eee4ba11086eb36c1b2e61 - languageName: node - linkType: hard - "acorn-walk@npm:^7.1.1": version: 7.2.0 resolution: "acorn-walk@npm:7.2.0" @@ -2316,16 +2715,16 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^6.0.1": - version: 6.4.1 - resolution: "acorn@npm:6.4.1" +"acorn@npm:^7.1.1": + version: 7.4.0 + resolution: "acorn@npm:7.4.0" bin: acorn: bin/acorn - checksum: 5ea4faa1fd30712b1d725da65e9612a93e566f40b0125df955c34669c33b81531e053a3c1501966e11217ca6026a0165f970e73c4eb8d3be7a957e4bef4ab67c + checksum: 1cbf7cae01f8fdc9ee2c65294b7f0a741a67760b22fee4ea3bbbffd0102fc76b07cd7437494221df7f7e51e75fdff3dae4bf11763d29e310e779fc61d3378ad5 languageName: node linkType: hard -"acorn@npm:^7.1.0, acorn@npm:^7.4.0": +"acorn@npm:^7.4.0": version: 7.4.1 resolution: "acorn@npm:7.4.1" bin: @@ -2334,15 +2733,6 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^7.1.1": - version: 7.4.0 - resolution: "acorn@npm:7.4.0" - bin: - acorn: bin/acorn - checksum: 1cbf7cae01f8fdc9ee2c65294b7f0a741a67760b22fee4ea3bbbffd0102fc76b07cd7437494221df7f7e51e75fdff3dae4bf11763d29e310e779fc61d3378ad5 - languageName: node - linkType: hard - "aes-js@npm:^3.1.1": version: 3.1.2 resolution: "aes-js@npm:3.1.2" @@ -2558,13 +2948,6 @@ __metadata: languageName: node linkType: hard -"array-equal@npm:^1.0.0": - version: 1.0.0 - resolution: "array-equal@npm:1.0.0" - checksum: 3f68045806357db9b2fa1ad583e42a659de030633118a0cd35ee4975cb20db3b9a3d36bbec9b5afe70011cf989eefd215c12fe0ce08c498f770859ca6e70688a - languageName: node - linkType: hard - "array-includes@npm:^3.1.1": version: 3.1.1 resolution: "array-includes@npm:3.1.1" @@ -3365,6 +3748,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: ^4.2.0 + strip-ansi: ^6.0.1 + wrap-ansi: ^7.0.0 + checksum: 79648b3b0045f2e285b76fb2e24e207c6db44323581e421c3acbd0e86454cba1b37aea976ab50195a49e7384b871e6dfb2247ad7dec53c02454ac6497394cb56 + languageName: node + linkType: hard + "clone@npm:^2.0.0, clone@npm:^2.1.1": version: 2.1.2 resolution: "clone@npm:2.1.2" @@ -3623,7 +4017,7 @@ __metadata: languageName: node linkType: hard -"cssom@npm:^0.4.1, cssom@npm:^0.4.4": +"cssom@npm:^0.4.4": version: 0.4.4 resolution: "cssom@npm:0.4.4" checksum: e3bc1076e7ee4213d4fef05e7ae03bfa83dc05f32611d8edc341f4ecc3d9647b89c8245474c7dd2cdcdb797a27c462e99da7ad00a34399694559f763478ff53f @@ -3637,7 +4031,7 @@ __metadata: languageName: node linkType: hard -"cssstyle@npm:^2.0.0, cssstyle@npm:^2.2.0": +"cssstyle@npm:^2.2.0": version: 2.3.0 resolution: "cssstyle@npm:2.3.0" dependencies: @@ -3655,17 +4049,6 @@ __metadata: languageName: node linkType: hard -"data-urls@npm:^1.1.0": - version: 1.1.0 - resolution: "data-urls@npm:1.1.0" - dependencies: - abab: ^2.0.0 - whatwg-mimetype: ^2.2.0 - whatwg-url: ^7.0.0 - checksum: dc4bd9621df0dff336d7c4c0517c792488ef3cf11cd37e72ab80f3a7f0a0aa14bad677ac97cf22c87c6eb9518e58b98590e1c8c756b56240940f0e470c81612e - languageName: node - linkType: hard - "data-urls@npm:^2.0.0": version: 2.0.0 resolution: "data-urls@npm:2.0.0" @@ -3677,7 +4060,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.3.1, debug@npm:^4.3.3": +"debug@npm:4, debug@npm:^4.3.1, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -3919,15 +4302,6 @@ __metadata: languageName: node linkType: hard -"domexception@npm:^1.0.1": - version: 1.0.1 - resolution: "domexception@npm:1.0.1" - dependencies: - webidl-conversions: ^4.0.2 - checksum: f564a9c0915dcb83ceefea49df14aaed106b1468fbe505119e8bcb0b77e242534f3aba861978537c0fc9dc6f35b176d0ffc77b3e342820fb27a8f215e7ae4d52 - languageName: node - linkType: hard - "domexception@npm:^2.0.1": version: 2.0.1 resolution: "domexception@npm:2.0.1" @@ -4159,7 +4533,7 @@ __metadata: languageName: node linkType: hard -"escodegen@npm:^1.11.1, escodegen@npm:^1.14.1": +"escodegen@npm:^1.14.1": version: 1.14.3 resolution: "escodegen@npm:1.14.3" dependencies: @@ -5162,7 +5536,7 @@ __metadata: languageName: node linkType: hard -"execa@npm:^5.1.1": +"execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" dependencies: @@ -5683,7 +6057,21 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.1": +"glob@npm:^7.1.7": + version: 7.2.3 + resolution: "glob@npm:7.2.3" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^3.1.1 + once: ^1.3.0 + path-is-absolute: ^1.0.0 + checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 + languageName: node + linkType: hard + +"glob@npm:^8.0.1, glob@npm:^8.0.3": version: 8.0.3 resolution: "glob@npm:8.0.3" dependencies: @@ -5975,15 +6363,6 @@ __metadata: languageName: node linkType: hard -"html-encoding-sniffer@npm:^1.0.2": - version: 1.0.2 - resolution: "html-encoding-sniffer@npm:1.0.2" - dependencies: - whatwg-encoding: ^1.0.1 - checksum: b874df6750451b7642fbe8e998c6bdd2911b0f42ad2927814b717bf1f4b082b0904b6178a1bfbc40117bf5799777993b0825e7713ca0fca49844e5aec03aa0e2 - languageName: node - linkType: hard - "html-encoding-sniffer@npm:^2.0.1": version: 2.0.1 resolution: "html-encoding-sniffer@npm:2.0.1" @@ -6942,20 +7321,6 @@ __metadata: languageName: node linkType: hard -"jest-environment-jsdom@npm:^25.0.0": - version: 25.5.0 - resolution: "jest-environment-jsdom@npm:25.5.0" - dependencies: - "@jest/environment": ^25.5.0 - "@jest/fake-timers": ^25.5.0 - "@jest/types": ^25.5.0 - jest-mock: ^25.5.0 - jest-util: ^25.5.0 - jsdom: ^15.2.1 - checksum: 3f8b54a0a49492ba82aedcf0b0015dbb106a8eb6adca4525424072abadf1b654383ea6f42de76eeb3deb5aac17728583df2b538bf481ca85a3e61f07e7e6ec3e - languageName: node - linkType: hard - "jest-environment-jsdom@npm:^26.3.0": version: 26.3.0 resolution: "jest-environment-jsdom@npm:26.3.0" @@ -7079,22 +7444,6 @@ __metadata: languageName: node linkType: hard -"jest-message-util@npm:^25.5.0": - version: 25.5.0 - resolution: "jest-message-util@npm:25.5.0" - dependencies: - "@babel/code-frame": ^7.0.0 - "@jest/types": ^25.5.0 - "@types/stack-utils": ^1.0.1 - chalk: ^3.0.0 - graceful-fs: ^4.2.4 - micromatch: ^4.0.2 - slash: ^3.0.0 - stack-utils: ^1.0.1 - checksum: 16ab8999802649069504a6eb1b2ee645d048cfe8dd2a8ac2a552d5f7f67bf657f02e1974c8e18313dbe9b4e9d83f80510757c1e6b4e5392db7d5da68d4eeebba - languageName: node - linkType: hard - "jest-message-util@npm:^26.3.0": version: 26.3.0 resolution: "jest-message-util@npm:26.3.0" @@ -7111,15 +7460,6 @@ __metadata: languageName: node linkType: hard -"jest-mock@npm:^25.5.0": - version: 25.5.0 - resolution: "jest-mock@npm:25.5.0" - dependencies: - "@jest/types": ^25.5.0 - checksum: b0e3cc2ccb05b45fc1ec52476d07740cab980d7ed41bf621c9000b9c5e4dafb05bc3f8ca6f7907a865d89522001a14f582863c6481af9e972a8f1765f0fe852e - languageName: node - linkType: hard - "jest-mock@npm:^26.3.0": version: 26.3.0 resolution: "jest-mock@npm:26.3.0" @@ -7273,19 +7613,6 @@ __metadata: languageName: node linkType: hard -"jest-util@npm:^25.5.0": - version: 25.5.0 - resolution: "jest-util@npm:25.5.0" - dependencies: - "@jest/types": ^25.5.0 - chalk: ^3.0.0 - graceful-fs: ^4.2.4 - is-ci: ^2.0.0 - make-dir: ^3.0.0 - checksum: 4c982e37968914d9e8b8330d2838533a4e8566b80b38cbb0916a19660a805357913aae1382fef35aeb4e348ba5dad77eb7413a16d533cdba7317941e01236352 - languageName: node - linkType: hard - "jest-util@npm:^26.1.0": version: 26.6.2 resolution: "jest-util@npm:26.6.2" @@ -7444,45 +7771,6 @@ __metadata: languageName: node linkType: hard -"jsdom@npm:^15.2.1": - version: 15.2.1 - resolution: "jsdom@npm:15.2.1" - dependencies: - abab: ^2.0.0 - acorn: ^7.1.0 - acorn-globals: ^4.3.2 - array-equal: ^1.0.0 - cssom: ^0.4.1 - cssstyle: ^2.0.0 - data-urls: ^1.1.0 - domexception: ^1.0.1 - escodegen: ^1.11.1 - html-encoding-sniffer: ^1.0.2 - nwsapi: ^2.2.0 - parse5: 5.1.0 - pn: ^1.1.0 - request: ^2.88.0 - request-promise-native: ^1.0.7 - saxes: ^3.1.9 - symbol-tree: ^3.2.2 - tough-cookie: ^3.0.1 - w3c-hr-time: ^1.0.1 - w3c-xmlserializer: ^1.1.2 - webidl-conversions: ^4.0.2 - whatwg-encoding: ^1.0.5 - whatwg-mimetype: ^2.3.0 - whatwg-url: ^7.0.0 - ws: ^7.0.0 - xml-name-validator: ^3.0.0 - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true - checksum: eff437b977330b1e63cd3ee2c2fe7c799c876799cae35525e1e6864d939dd41631ebd65f847adaeb83c2160c828d027d0f1d0dbe88366d1da22c875a5165a78c - languageName: node - linkType: hard - "jsdom@npm:^16.2.2": version: 16.4.0 resolution: "jsdom@npm:16.4.0" @@ -7890,15 +8178,6 @@ __metadata: languageName: node linkType: hard -"lolex@npm:^5.0.0": - version: 5.1.2 - resolution: "lolex@npm:5.1.2" - dependencies: - "@sinonjs/commons": ^1.7.0 - checksum: 7eb468d4ef4746c024d23cb2b75f679f79449a9d5cbe11abadf2f3b147c1d7ffe28816438bedfb8a75c58357a625c2f9ba197b050c226d2b3f0c4a956cf556fb - languageName: node - linkType: hard - "lru-cache@npm:^6.0.0": version: 6.0.0 resolution: "lru-cache@npm:6.0.0" @@ -8154,6 +8433,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^3.1.1": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: ^1.1.7 + checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a + languageName: node + linkType: hard + "minimatch@npm:^5.0.1": version: 5.0.1 resolution: "minimatch@npm:5.0.1" @@ -8899,13 +9187,6 @@ __metadata: languageName: node linkType: hard -"parse5@npm:5.1.0": - version: 5.1.0 - resolution: "parse5@npm:5.1.0" - checksum: 13c44c6d47035a3cc75303655ae5630dc264f9b9ab8344feb3f79ca195d8b57a2a246af902abef1d780ad1eee92eb9b88cd03098a7ee7dd111f032152ebaf0a6 - languageName: node - linkType: hard - "parse5@npm:5.1.1": version: 5.1.1 resolution: "parse5@npm:5.1.1" @@ -9089,10 +9370,10 @@ __metadata: languageName: node linkType: hard -"pn@npm:^1.1.0": - version: 1.1.0 - resolution: "pn@npm:1.1.0" - checksum: e4654186dc92a187c8c7fe4ccda902f4d39dd9c10f98d1c5a08ce5fad5507ef1e33ddb091240c3950bee81bd201b4c55098604c433a33b5e8bdd97f38b732fa0 +"pony-cause@npm:^2.1.0": + version: 2.1.4 + resolution: "pony-cause@npm:2.1.4" + checksum: 390b182b89421f642de2b60224c136db8aa494a9067c5c66f05308d2088c1fc51883e7d5033adafbc712db99dcdc1d297f144d7a660f334d399a4e4e5724b180 languageName: node linkType: hard @@ -9546,7 +9827,7 @@ __metadata: languageName: node linkType: hard -"request-promise-native@npm:^1.0.7, request-promise-native@npm:^1.0.8": +"request-promise-native@npm:^1.0.8": version: 1.0.9 resolution: "request-promise-native@npm:1.0.9" dependencies: @@ -9587,7 +9868,7 @@ __metadata: languageName: node linkType: hard -"request@npm:^2.88.0, request@npm:^2.88.2": +"request@npm:^2.88.2": version: 2.88.2 resolution: "request@npm:2.88.2" dependencies: @@ -9913,15 +10194,6 @@ __metadata: languageName: node linkType: hard -"saxes@npm:^3.1.9": - version: 3.1.11 - resolution: "saxes@npm:3.1.11" - dependencies: - xmlchars: ^2.1.1 - checksum: 3b69918c013fffae51c561f629a0f620c02dba70f762dab38f3cd92676dfe5edf1f0a523ca567882838f1a80e26e4671a8c2c689afa05c68f45a78261445aba0 - languageName: node - linkType: hard - "saxes@npm:^5.0.0": version: 5.0.1 resolution: "saxes@npm:5.0.1" @@ -10015,6 +10287,17 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.3.7": + version: 7.3.8 + resolution: "semver@npm:7.3.8" + dependencies: + lru-cache: ^6.0.0 + bin: + semver: bin/semver.js + checksum: ba9c7cbbf2b7884696523450a61fee1a09930d888b7a8d7579025ad93d459b2d1949ee5bbfeb188b2be5f4ac163544c5e98491ad6152df34154feebc2cc337c1 + languageName: node + linkType: hard + "semver@npm:~5.4.1": version: 5.4.1 resolution: "semver@npm:5.4.1" @@ -10434,13 +10717,6 @@ __metadata: languageName: node linkType: hard -"stack-utils@npm:^1.0.1": - version: 1.0.2 - resolution: "stack-utils@npm:1.0.2" - checksum: a8353a26f26b036d5b33d7c67ec7b0075e854c738e7d40dc1e27ca026b037381fc0cec9be2f6438e8963dcd17097180921d3029676add21ae6687235348e8bb3 - languageName: node - linkType: hard - "stack-utils@npm:^2.0.2": version: 2.0.2 resolution: "stack-utils@npm:2.0.2" @@ -10676,6 +10952,13 @@ __metadata: languageName: node linkType: hard +"superstruct@npm:^0.16.5": + version: 0.16.5 + resolution: "superstruct@npm:0.16.5" + checksum: 9f843c38695b584a605ae9b028629de18a85bd0dca0e9449b4ab98bb7b9ac3d82599870acbab9fbd2ee454c6b187af7e61562e252dfadabd974191ab4ab2e3ce + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -10704,7 +10987,7 @@ __metadata: languageName: node linkType: hard -"symbol-tree@npm:^3.2.2, symbol-tree@npm:^3.2.4": +"symbol-tree@npm:^3.2.4": version: 3.2.4 resolution: "symbol-tree@npm:3.2.4" checksum: 6e8fc7e1486b8b54bea91199d9535bb72f10842e40c79e882fc94fb7b14b89866adf2fd79efa5ebb5b658bc07fb459ccce5ac0e99ef3d72f474e74aaf284029d @@ -10867,15 +11150,6 @@ __metadata: languageName: node linkType: hard -"tr46@npm:^1.0.1": - version: 1.0.1 - resolution: "tr46@npm:1.0.1" - dependencies: - punycode: ^2.1.0 - checksum: 96d4ed46bc161db75dbf9247a236ea0bfcaf5758baae6749e92afab0bc5a09cb59af21788ede7e55080f2bf02dce3e4a8f2a484cc45164e29f4b5e68f7cbcc1a - languageName: node - linkType: hard - "tr46@npm:^2.0.2": version: 2.0.2 resolution: "tr46@npm:2.0.2" @@ -11311,7 +11585,7 @@ __metadata: languageName: node linkType: hard -"w3c-hr-time@npm:^1.0.1, w3c-hr-time@npm:^1.0.2": +"w3c-hr-time@npm:^1.0.2": version: 1.0.2 resolution: "w3c-hr-time@npm:1.0.2" dependencies: @@ -11320,17 +11594,6 @@ __metadata: languageName: node linkType: hard -"w3c-xmlserializer@npm:^1.1.2": - version: 1.1.2 - resolution: "w3c-xmlserializer@npm:1.1.2" - dependencies: - domexception: ^1.0.1 - webidl-conversions: ^4.0.2 - xml-name-validator: ^3.0.0 - checksum: 1683e083d0dfc1529988f8956510a3a26e90738b41c4df0c7eb95283bfbeabeb492308117dcd32afef2a141e2a959ddf10ce562983d91b9f474a530b9dcdd337 - languageName: node - linkType: hard - "w3c-xmlserializer@npm:^2.0.0": version: 2.0.0 resolution: "w3c-xmlserializer@npm:2.0.0" @@ -11399,13 +11662,6 @@ __metadata: languageName: node linkType: hard -"webidl-conversions@npm:^4.0.2": - version: 4.0.2 - resolution: "webidl-conversions@npm:4.0.2" - checksum: c93d8dfe908a0140a4ae9c0ebc87a33805b416a33ee638a605b551523eec94a9632165e54632f6d57a39c5f948c4bab10e0e066525e9a4b87a79f0d04fbca374 - languageName: node - linkType: hard - "webidl-conversions@npm:^5.0.0": version: 5.0.0 resolution: "webidl-conversions@npm:5.0.0" @@ -11420,7 +11676,7 @@ __metadata: languageName: node linkType: hard -"whatwg-encoding@npm:^1.0.1, whatwg-encoding@npm:^1.0.5": +"whatwg-encoding@npm:^1.0.5": version: 1.0.5 resolution: "whatwg-encoding@npm:1.0.5" dependencies: @@ -11443,7 +11699,7 @@ __metadata: languageName: node linkType: hard -"whatwg-mimetype@npm:^2.2.0, whatwg-mimetype@npm:^2.3.0": +"whatwg-mimetype@npm:^2.3.0": version: 2.3.0 resolution: "whatwg-mimetype@npm:2.3.0" checksum: 23eb885940bcbcca4ff841c40a78e9cbb893ec42743993a42bf7aed16085b048b44b06f3402018931687153550f9a32d259dfa524e4f03577ab898b6965e5383 @@ -11460,17 +11716,6 @@ __metadata: languageName: node linkType: hard -"whatwg-url@npm:^7.0.0": - version: 7.1.0 - resolution: "whatwg-url@npm:7.1.0" - dependencies: - lodash.sortby: ^4.7.0 - tr46: ^1.0.1 - webidl-conversions: ^4.0.2 - checksum: fecb07c87290b47d2ec2fb6d6ca26daad3c9e211e0e531dd7566e7ff95b5b3525a57d4f32640ad4adf057717e0c215731db842ad761e61d947e81010e05cf5fd - languageName: node - linkType: hard - "whatwg-url@npm:^8.0.0": version: 8.2.2 resolution: "whatwg-url@npm:8.2.2" @@ -11635,7 +11880,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.0.0, ws@npm:^7.2.3": +"ws@npm:^7.2.3": version: 7.3.1 resolution: "ws@npm:7.3.1" peerDependencies: @@ -11685,7 +11930,7 @@ __metadata: languageName: node linkType: hard -"xmlchars@npm:^2.1.1, xmlchars@npm:^2.2.0": +"xmlchars@npm:^2.2.0": version: 2.2.0 resolution: "xmlchars@npm:2.2.0" checksum: 8c70ac94070ccca03f47a81fcce3b271bd1f37a591bf5424e787ae313fcb9c212f5f6786e1fa82076a2c632c0141552babcd85698c437506dfa6ae2d58723062 @@ -11736,6 +11981,13 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.1.1": + version: 2.1.3 + resolution: "yaml@npm:2.1.3" + checksum: 91316062324a93f9cb547469092392e7d004ff8f70c40fecb420f042a4870b2181557350da56c92f07bd44b8f7a252b0be26e6ade1f548e1f4351bdd01c9d3c7 + languageName: node + linkType: hard + "yargs-parser@npm:20.x": version: 20.2.6 resolution: "yargs-parser@npm:20.2.6" @@ -11760,6 +12012,13 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^21.0.0": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c + languageName: node + linkType: hard + "yargs@npm:^15.3.1": version: 15.4.1 resolution: "yargs@npm:15.4.1" @@ -11808,3 +12067,18 @@ __metadata: checksum: 4ffffa5a82647e5d07840b64bed88c365b901d3d4a4c51745dddb10d177902d85014026d7224aae18c42df9ca3f75a41c5aff556e5342e2f8ffc5177d149cd17 languageName: node linkType: hard + +"yargs@npm:^17.5.1": + version: 17.6.0 + resolution: "yargs@npm:17.6.0" + dependencies: + cliui: ^8.0.1 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 + require-directory: ^2.1.1 + string-width: ^4.2.3 + y18n: ^5.0.5 + yargs-parser: ^21.0.0 + checksum: 604bdb4a6395a870540d2f3fea083c8e28441f12da8fd05b172b1e68480f00ed73d76be4a05fac19de9bf55ec7729b41e81cf555cccaed700aa192e4fff64872 + languageName: node + linkType: hard