diff --git a/.codebuild/build.sh b/.codebuild/build.sh new file mode 100644 index 00000000..4675f4d1 --- /dev/null +++ b/.codebuild/build.sh @@ -0,0 +1,102 @@ +set -e; + +send_slack_message() { + MESSAGE=$1; + + curl -H "Content-type: application/json" \ + --data "{\"channel\":\"${SLACK_DEPLOYS_CHANNEL_ID}\",\"blocks\":[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"*Hathor Wallet Service*\n${MESSAGE}\"}}]}" \ + -H "Authorization: Bearer ${SLACK_OAUTH_TOKEN}" \ + -X POST https://slack.com/api/chat.postMessage; +} + +echo "Building git ref ${GIT_REF_TO_DEPLOY}..." + +exit=false; + +# Checks whether there is a file called "rollback_mainnet_production", which is used by our other CodeBuild to indicate that this is a mainnet-production rollback +if [ -f "rollback_mainnet_production" ]; then + # Gets all env vars with `mainnet_` prefix and re-exports them without the prefix + for var in "${!mainnet_@}"; do + export ${var#mainnet_}="${!var}" + done + make deploy-lambdas-mainnet; + send_slack_message "Rollback performed on mainnet-production to: ${GIT_REF_TO_DEPLOY}"; + exit=true; +fi; + +# Checks whether there is a file called "rollback_testnet_production", which is used by our other CodeBuild to indicate that this is a testnet-production rollback +if [ -f "rollback_testnet_production" ]; then + # Gets all env vars with `testnet_` prefix and re-exports them without the prefix + for var in "${!testnet_@}"; do + export ${var#testnet_}="${!var}" + done + make deploy-lambdas-testnet; + send_slack_message "Rollback performed on testnet-production to: ${GIT_REF_TO_DEPLOY}"; + exit=true; +fi; + +if [ "$exit" = true ]; then + echo "Rollbacks performed successfully. Exiting now."; + exit 0; +fi + +if expr "${GIT_REF_TO_DEPLOY}" : "master" >/dev/null; then + # Gets all env vars with `dev_` prefix and re-exports them without the prefix + for var in "${!dev_@}"; do + export ${var#dev_}="${!var}" + done + + make migrate; + make build-daemon; + make deploy-lambdas-dev-testnet; + # The idea here is that if the lambdas deploy fail, the built image won't be pushed: + make push-daemon; + +elif expr "${GIT_REF_TO_DEPLOY}" : "v[0-9]\+\.[0-9]\+\.[0-9]\+-rc\.[0-9]\+" >/dev/null; then + # Gets all env vars with `mainnet_staging_` prefix and re-exports them without the prefix + for var in "${!mainnet_staging_@}"; do + export ${var#mainnet_staging_}="${!var}" + done + + echo $GIT_REF_TO_DEPLOY > /tmp/docker_image_tag + make migrate; + make build-daemon; + make deploy-lambdas-mainnet-staging; + make push-daemon; + send_slack_message "New version deployed to mainnet-staging: ${GIT_REF_TO_DEPLOY}" +elif expr "${GIT_REF_TO_DEPLOY}" : "v.*" >/dev/null; then + # Gets all env vars with `testnet_` prefix and re-exports them without the prefix + for var in "${!testnet_@}"; do + export ${var#testnet_}="${!var}" + done + + echo $GIT_REF_TO_DEPLOY > /tmp/docker_image_tag + make migrate; + make build-daemon; + make deploy-lambdas-testnet; + make push-daemon; + + # Unsets all the testnet env vars so we make sure they don't leak to the mainnet deploy below + for var in "${!testnet_@}"; do + unset ${var#testnet_} + done + + # Gets all env vars with `mainnet_` prefix and re-exports them without the prefix + for var in "${!mainnet_@}"; do + export ${var#mainnet_}="${!var}" + done + make migrate; + make build-daemon; + make deploy-lambdas-mainnet; + make push-daemon; + send_slack_message "New version deployed to testnet-production and mainnet-production: ${GIT_REF_TO_DEPLOY}" +else + # Gets all env vars with `dev_` prefix and re-exports them without the prefix + for var in "${!dev_@}"; do + export ${var#dev_}="${!var}" + done + make migrate; + make build-daemon; + make deploy-lambdas-dev-testnet; + make push-daemon; +fi; diff --git a/.codebuild/buildspec.yml b/.codebuild/buildspec.yml new file mode 100644 index 00000000..68eb00de --- /dev/null +++ b/.codebuild/buildspec.yml @@ -0,0 +1,184 @@ +version: 0.2 + +# The envs are organized in a way that some of them will have prefixes, indicating the environment corresponding to them. +# In the build section we check which environment is being deployed and choose the envs accordingly. +# The ones without prefixes are used in all environments. +env: + shell: bash + git-credential-helper: yes + variables: + NODE_ENV: "production" + MAX_ADDRESS_GAP: 20 + WALLET_CONN_LIMIT: 10 + BLOCK_REWARD_LOCK: 300 + CONFIRM_FIRST_ADDRESS: true + VOIDED_TX_OFFSET: 20 + TX_HISTORY_MAX_COUNT: 50 + dev_DEFAULT_SERVER: "https://wallet-service.private-nodes.testnet.hathor.network/v1a/" + dev_WS_DOMAIN: "ws.dev.wallet-service.testnet.hathor.network" + dev_NETWORK: "testnet" + dev_LOG_LEVEL: "debug" + dev_NFT_AUTO_REVIEW_ENABLED: "true" + dev_EXPLORER_STAGE: "dev" + dev_EXPLORER_SERVICE_LAMBDA_ENDPOINT: "https://lambda.eu-central-1.amazonaws.com" + dev_WALLET_SERVICE_LAMBDA_ENDPOINT: "https://lambda.eu-central-1.amazonaws.com" + dev_PUSH_NOTIFICATION_ENABLED: "true" + dev_PUSH_ALLOWED_PROVIDERS: "android,ios" + dev_APPLICATION_NAME: "wallet-service-dev" + testnet_DEFAULT_SERVER: "https://wallet-service.private-nodes.testnet.hathor.network/v1a/" + testnet_WS_DOMAIN: "ws.wallet-service.testnet.hathor.network" + testnet_NETWORK: "testnet" + testnet_LOG_LEVEL: "debug" + testnet_NFT_AUTO_REVIEW_ENABLED: "true" + testnet_EXPLORER_STAGE: "testnet" + testnet_EXPLORER_SERVICE_LAMBDA_ENDPOINT: "https://lambda.eu-central-1.amazonaws.com" + testnet_WALLET_SERVICE_LAMBDA_ENDPOINT: "https://lambda.eu-central-1.amazonaws.com" + testnet_PUSH_NOTIFICATION_ENABLED: "true" + testnet_PUSH_ALLOWED_PROVIDERS: "android,ios" + testnet_APPLICATION_NAME: "wallet-service-testnet" + mainnet_staging_DEFAULT_SERVER: "https://wallet-service.private-nodes.hathor.network/v1a/" + mainnet_staging_WS_DOMAIN: "ws.staging.wallet-service.hathor.network" + mainnet_staging_NETWORK: "mainnet" + mainnet_staging_LOG_LEVEL: "debug" + mainnet_staging_NFT_AUTO_REVIEW_ENABLED: "false" + mainnet_staging_EXPLORER_STAGE: "mainnet-staging" # This stage doesn't exist in explorer-service and we don't need it because we have disabled the integration. + mainnet_staging_EXPLORER_SERVICE_LAMBDA_ENDPOINT: "https://lambda.eu-central-1.amazonaws.com" + mainnet_staging_WALLET_SERVICE_LAMBDA_ENDPOINT: "https://lambda.eu-central-1.amazonaws.com" + mainnet_staging_PUSH_NOTIFICATION_ENABLED: "true" + mainnet_staging_PUSH_ALLOWED_PROVIDERS: "android,ios" + mainnet_staging_APPLICATION_NAME: "wallet-service-mainnet-staging" + mainnet_DEFAULT_SERVER: "https://wallet-service.private-nodes.hathor.network/v1a/" + mainnet_WS_DOMAIN: "ws.wallet-service.hathor.network" + mainnet_NETWORK: "mainnet" + mainnet_LOG_LEVEL: "debug" + mainnet_NFT_AUTO_REVIEW_ENABLED: "true" + mainnet_EXPLORER_STAGE: "mainnet" + mainnet_EXPLORER_SERVICE_LAMBDA_ENDPOINT: "https://lambda.eu-central-1.amazonaws.com" + mainnet_WALLET_SERVICE_LAMBDA_ENDPOINT: "https://lambda.eu-central-1.amazonaws.com" + mainnet_PUSH_NOTIFICATION_ENABLED: "true" + mainnet_PUSH_ALLOWED_PROVIDERS: "android,ios" + mainnet_APPLICATION_NAME: "wallet-service-mainnet" + # https://eu-central-1.console.aws.amazon.com/secretsmanager/home?region=eu-central-1#!/listSecrets + secrets-manager: + # CI secrets + SLACK_OAUTH_TOKEN: "WalletService/ci:slack_oauth_token" + SLACK_DEPLOYS_CHANNEL_ID: "WalletService/ci:slack_deploys_channel_id" + # Dev secrets + dev_ACCOUNT_ID: "WalletService/dev:account_id" + dev_AUTH_SECRET: "WalletService/dev:auth_secret" + dev_AWS_VPC_DEFAULT_SG_ID: "WalletService/dev:aws_vpc_default_sg_id" + dev_AWS_SUBNET_ID_1: "WalletService/dev:aws_subnet_id_1" + dev_AWS_SUBNET_ID_2: "WalletService/dev:aws_subnet_id_2" + dev_AWS_SUBNET_ID_3: "WalletService/dev:aws_subnet_id_3" + dev_DB_NAME: "WalletService/rds/dev:dbname" + dev_DB_USER: "WalletService/rds/dev:username" + dev_DB_PASS: "WalletService/rds/dev:password" + dev_DB_ENDPOINT: "WalletService/rds/dev:host" + dev_DB_PORT: "WalletService/rds/dev:port" + dev_REDIS_URL: "WalletService/redis/dev:url" + dev_REDIS_PASSWORD: "WalletService/redis/dev:password" + dev_FIREBASE_PROJECT_ID: "WalletService/dev:FIREBASE_PROJECT_ID" + dev_FIREBASE_PRIVATE_KEY_ID: "WalletService/dev:FIREBASE_PRIVATE_KEY_ID" + dev_FIREBASE_PRIVATE_KEY: "WalletService/dev:FIREBASE_PRIVATE_KEY" + dev_FIREBASE_CLIENT_EMAIL: "WalletService/dev:FIREBASE_CLIENT_EMAIL" + dev_FIREBASE_CLIENT_ID: "WalletService/dev:FIREBASE_CLIENT_ID" + dev_FIREBASE_AUTH_URI: "WalletService/dev:FIREBASE_AUTH_URI" + dev_FIREBASE_TOKEN_URI: "WalletService/dev:FIREBASE_TOKEN_URI" + dev_FIREBASE_AUTH_PROVIDER_X509_CERT_URL: "WalletService/dev:FIREBASE_AUTH_PROVIDER_X509_CERT_URL" + dev_FIREBASE_CLIENT_X509_CERT_URL: "WalletService/dev:FIREBASE_CLIENT_X509_CERT_URL" + dev_ALERT_MANAGER_REGION: "WalletService/dev:ALERT_MANAGER_REGION" + dev_ALERT_MANAGER_TOPIC: "WalletService/dev:ALERT_MANAGER_TOPIC" + # Testnet secrets + testnet_ACCOUNT_ID: "WalletService/testnet:account_id" + testnet_AUTH_SECRET: "WalletService/testnet:auth_secret" + testnet_AWS_VPC_DEFAULT_SG_ID: "WalletService/testnet:aws_vpc_default_sg_id" + testnet_AWS_SUBNET_ID_1: "WalletService/testnet:aws_subnet_id_1" + testnet_AWS_SUBNET_ID_2: "WalletService/testnet:aws_subnet_id_2" + testnet_AWS_SUBNET_ID_3: "WalletService/testnet:aws_subnet_id_3" + testnet_DB_NAME: "WalletService/rds/testnet:dbname" + testnet_DB_USER: "WalletService/rds/testnet:username" + testnet_DB_PASS: "WalletService/rds/testnet:password" + testnet_DB_ENDPOINT: "WalletService/rds/testnet:host" + testnet_DB_PORT: "WalletService/rds/testnet:port" + testnet_REDIS_URL: "WalletService/redis/testnet:url" + testnet_REDIS_PASSWORD: "WalletService/redis/testnet:password" + testnet_FIREBASE_PROJECT_ID: "WalletService/testnet:FIREBASE_PROJECT_ID" + testnet_FIREBASE_PRIVATE_KEY_ID: "WalletService/testnet:FIREBASE_PRIVATE_KEY_ID" + testnet_FIREBASE_PRIVATE_KEY: "WalletService/testnet:FIREBASE_PRIVATE_KEY" + testnet_FIREBASE_CLIENT_EMAIL: "WalletService/testnet:FIREBASE_CLIENT_EMAIL" + testnet_FIREBASE_CLIENT_ID: "WalletService/testnet:FIREBASE_CLIENT_ID" + testnet_FIREBASE_AUTH_URI: "WalletService/testnet:FIREBASE_AUTH_URI" + testnet_FIREBASE_TOKEN_URI: "WalletService/testnet:FIREBASE_TOKEN_URI" + testnet_FIREBASE_AUTH_PROVIDER_X509_CERT_URL: "WalletService/testnet:FIREBASE_AUTH_PROVIDER_X509_CERT_URL" + testnet_FIREBASE_CLIENT_X509_CERT_URL: "WalletService/testnet:FIREBASE_CLIENT_X509_CERT_URL" + testnet_ALERT_MANAGER_REGION: "WalletService/testnet:ALERT_MANAGER_REGION" + testnet_ALERT_MANAGER_TOPIC: "WalletService/testnet:ALERT_MANAGER_TOPIC" + # Mainnet Staging secrets + mainnet_staging_ACCOUNT_ID: "WalletService/mainnet_staging:account_id" + mainnet_staging_AUTH_SECRET: "WalletService/mainnet_staging:auth_secret" + mainnet_staging_AWS_VPC_DEFAULT_SG_ID: "WalletService/mainnet_staging:aws_vpc_default_sg_id" + mainnet_staging_AWS_SUBNET_ID_1: "WalletService/mainnet_staging:aws_subnet_id_1" + mainnet_staging_AWS_SUBNET_ID_2: "WalletService/mainnet_staging:aws_subnet_id_2" + mainnet_staging_AWS_SUBNET_ID_3: "WalletService/mainnet_staging:aws_subnet_id_3" + mainnet_staging_DB_NAME: "WalletService/rds/mainnet_staging:dbname" + mainnet_staging_DB_USER: "WalletService/rds/mainnet_staging:username" + mainnet_staging_DB_PASS: "WalletService/rds/mainnet_staging:password" + mainnet_staging_DB_ENDPOINT: "WalletService/rds/mainnet_staging:host" + mainnet_staging_DB_PORT: "WalletService/rds/mainnet_staging:port" + mainnet_staging_REDIS_URL: "WalletService/redis/mainnet_staging:url" + mainnet_staging_REDIS_PASSWORD: "WalletService/redis/mainnet_staging:password" + mainnet_staging_FIREBASE_PROJECT_ID: "WalletService/mainnet_staging:FIREBASE_PROJECT_ID" + mainnet_staging_FIREBASE_PRIVATE_KEY_ID: "WalletService/mainnet_staging:FIREBASE_PRIVATE_KEY_ID" + mainnet_staging_FIREBASE_PRIVATE_KEY: "WalletService/mainnet_staging:FIREBASE_PRIVATE_KEY" + mainnet_staging_FIREBASE_CLIENT_EMAIL: "WalletService/mainnet_staging:FIREBASE_CLIENT_EMAIL" + mainnet_staging_FIREBASE_CLIENT_ID: "WalletService/mainnet_staging:FIREBASE_CLIENT_ID" + mainnet_staging_FIREBASE_AUTH_URI: "WalletService/mainnet_staging:FIREBASE_AUTH_URI" + mainnet_staging_FIREBASE_TOKEN_URI: "WalletService/mainnet_staging:FIREBASE_TOKEN_URI" + mainnet_staging_FIREBASE_AUTH_PROVIDER_X509_CERT_URL: "WalletService/mainnet_staging:FIREBASE_AUTH_PROVIDER_X509_CERT_URL" + mainnet_staging_FIREBASE_CLIENT_X509_CERT_URL: "WalletService/mainnet_staging:FIREBASE_CLIENT_X509_CERT_URL" + mainnet_staging_ALERT_MANAGER_REGION: "WalletService/mainnet_staging:ALERT_MANAGER_REGION" + mainnet_staging_ALERT_MANAGER_TOPIC: "WalletService/mainnet_staging:ALERT_MANAGER_TOPIC" + # Mainnet secrets + mainnet_ACCOUNT_ID: "WalletService/mainnet:account_id" + mainnet_AUTH_SECRET: "WalletService/mainnet:auth_secret" + mainnet_AWS_VPC_DEFAULT_SG_ID: "WalletService/mainnet:aws_vpc_default_sg_id" + mainnet_AWS_SUBNET_ID_1: "WalletService/mainnet:aws_subnet_id_1" + mainnet_AWS_SUBNET_ID_2: "WalletService/mainnet:aws_subnet_id_2" + mainnet_AWS_SUBNET_ID_3: "WalletService/mainnet:aws_subnet_id_3" + mainnet_DB_NAME: "WalletService/rds/mainnet:dbname" + mainnet_DB_USER: "WalletService/rds/mainnet:username" + mainnet_DB_PASS: "WalletService/rds/mainnet:password" + mainnet_DB_ENDPOINT: "WalletService/rds/mainnet:host" + mainnet_DB_PORT: "WalletService/rds/mainnet:port" + mainnet_REDIS_URL: "WalletService/redis/mainnet:url" + mainnet_REDIS_PASSWORD: "WalletService/redis/mainnet:password" + mainnet_FIREBASE_PROJECT_ID: "WalletService/mainnet:FIREBASE_PROJECT_ID" + mainnet_FIREBASE_PRIVATE_KEY_ID: "WalletService/mainnet:FIREBASE_PRIVATE_KEY_ID" + mainnet_FIREBASE_PRIVATE_KEY: "WalletService/mainnet:FIREBASE_PRIVATE_KEY" + mainnet_FIREBASE_CLIENT_EMAIL: "WalletService/mainnet:FIREBASE_CLIENT_EMAIL" + mainnet_FIREBASE_CLIENT_ID: "WalletService/mainnet:FIREBASE_CLIENT_ID" + mainnet_FIREBASE_AUTH_URI: "WalletService/mainnet:FIREBASE_AUTH_URI" + mainnet_FIREBASE_TOKEN_URI: "WalletService/mainnet:FIREBASE_TOKEN_URI" + mainnet_FIREBASE_AUTH_PROVIDER_X509_CERT_URL: "WalletService/mainnet:FIREBASE_AUTH_PROVIDER_X509_CERT_URL" + mainnet_FIREBASE_CLIENT_X509_CERT_URL: "WalletService/mainnet:FIREBASE_CLIENT_X509_CERT_URL" + mainnet_ALERT_MANAGER_REGION: "WalletService/mainnet:ALERT_MANAGER_REGION" + mainnet_ALERT_MANAGER_TOPIC: "WalletService/mainnet:ALERT_MANAGER_TOPIC" +phases: + install: + #If you use the Ubuntu standard image 2.0 or later, you must specify runtime-versions. + #If you specify runtime-versions and use an image other than Ubuntu standard image 2.0, the build fails. + runtime-versions: + nodejs: 20 + # name: version + commands: + - npm install -g yarn + - corepack enable + - yarn set version stable + - yarn install + pre_build: + commands: + # This file is created in another CodeBuild specification that runs before this and is not committed to this repo. + - export GIT_REF_TO_DEPLOY=$(cat git_ref_to_deploy) + build: + commands: + - bash .codebuild/build.sh diff --git a/.dockerignore b/.dockerignore index 849ddff3..43c5234c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,11 @@ dist/ +node_modules/ +__tests__/ +.git/ +.github/ +.direnv/ +flake.* +node_modules/ +packages/daemon/dist/ +packages/daemon/node_modules/ +packages/wallet-service diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..7a65628c --- /dev/null +++ b/.envrc @@ -0,0 +1,7 @@ +if [[ $(type -t use_flake) != function ]]; then + echo "ERROR: use_flake function missing." + echo "Please update direnv to v2.30.0 or later." + exit 1 +fi + +use flake diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..ca7cc821 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +dist/ +db/ diff --git a/.eslintrc.yml b/.eslintrc.yml index 98520a97..49a583f5 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,84 +1,30 @@ -parser: "@typescript-eslint/parser" +root: true +parser: '@typescript-eslint/parser' +parserOptions: + ecmaVersion: 2022 + sourceType: 'module' + project: './tsconfig.json' env: - browser: false node: true es6: true -extends: -- airbnb-base -- plugin:jest/all -- plugin:import/errors -- plugin:import/warnings -- plugin:import/typescript -- plugin:@typescript-eslint/recommended plugins: -- jest -- "@typescript-eslint" -root: true -globals: {} + - '@typescript-eslint' +extends: + - 'eslint:recommended' + - 'plugin:@typescript-eslint/recommended' rules: - import/no-unresolved: - - 2 - - commonjs: true - amd: true - max-len: - - error - - code: 150 - ignoreComments: true - ignoreTrailingComments: true - ignoreUrls: true - ignoreStrings: true - prefer-destructuring: 'off' - no-await-in-loop: 'off' - no-plusplus: 'off' - no-continue: 'off' - no-restricted-syntax: - - error - - ForInStatement - - LabeledStatement - - WithStatement - no-use-before-define: - - error - - functions: false - variables: false - no-underscore-dangle: 'off' - object-curly-newline: - - error - - consistent: true - import/prefer-default-export: 'off' - no-multi-spaces: - - error - - ignoreEOLComments: true - jest/require-top-level-describe: 'off' - jest/no-hooks: 'off' - jest/no-if: 'off' - jest/no-conditional-expect: 'off' - jest/no-expect-resolves: 'off' - jest/lowercase-name: 'off' - "@typescript-eslint/naming-convention": - - error - - selector: variableLike - format: - - camelCase - leadingUnderscore: allow - - selector: variable - format: - - camelCase - - UPPER_CASE - leadingUnderscore: allow - "@typescript-eslint/no-unused-vars": - - warn - - argsIgnorePattern: "^_" -overrides: [] -settings: - import/resolver: - alias: - map: - - - "@src" - - "./src" - - - "@tests" - - "./tests" - - - "@events" - - "./events" - extensions: - - ".ts" - - ".js" + '@typescript-eslint/ban-ts-comment': off + '@typescript-eslint/no-explicit-any': off + '@typescript-eslint/no-unused-vars': off +overrides: + - files: + - 'src/**/*.ts' + excludedFiles: + - 'dist/*' + - 'node_modules/*' + - files: + - "*.js" + parser: "espree" + parserOptions: + ecmaVersion: 2022 + sourceType: 'module' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d1be67b4..a167360e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,5 @@ -### Acceptance Criteria -- Include here all things that this PR should solve +Please go the the `Preview` tab and select the appropriate Pull Request template: - -### Security Checklist -- [ ] Make sure you do not include new dependencies in the project unless strictly necessary and do not include dev-dependencies as production ones. More dependencies increase the possibility of one of them being hijacked and affecting us. +* [Feature Branch](?expand=1&template=feature_branch_pr_template.md) - Use this PR template when you are merging a feature branch into `master` +* [Release Candidate](?expand=1&template=release_candidate_pr_template.md) - Use this PR template when you are merging `master` into `release-candidate` +* [Release](?expand=1&template=release_pr_template.md) - Use this PR template when you are merging `release-candidate` into `release` diff --git a/.github/PULL_REQUEST_TEMPLATE/feature_branch_pr_template.md b/.github/PULL_REQUEST_TEMPLATE/feature_branch_pr_template.md new file mode 100644 index 00000000..43c4d247 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/feature_branch_pr_template.md @@ -0,0 +1,12 @@ +### Motivation + +What was the motivation for the changes in this PR? + +### Acceptance Criteria + +- Include here all things that this PR should solve + +### Checklist +- [ ] If you are requesting a merge into `master`, confirm this code is production-ready and can be included in future releases as soon as it gets merged +- [ ] Make sure either the unit tests and/or the QA tests are capable of testing the new features +- [ ] Make sure you do not include new dependencies in the project unless strictly necessary and do not include dev-dependencies as production ones. More dependencies increase the possibility of one of them being hijacked and affecting us. diff --git a/.github/PULL_REQUEST_TEMPLATE/release_candidate_pr_template.md b/.github/PULL_REQUEST_TEMPLATE/release_candidate_pr_template.md new file mode 100644 index 00000000..e5f18b0c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/release_candidate_pr_template.md @@ -0,0 +1,8 @@ +# Changes + +Link here all the PRs that are included in this release candidate + +# Checklist + +- [ ] I've read and followed the release candidate process described in https://github.com/HathorNetwork/ops-tools/blob/master/docs/release-guides/wallet-service.md#release-candidate +- [ ] I confirm this release candidate only includes production-ready changes diff --git a/.github/PULL_REQUEST_TEMPLATE/release_pr_template.md b/.github/PULL_REQUEST_TEMPLATE/release_pr_template.md new file mode 100644 index 00000000..defb3957 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/release_pr_template.md @@ -0,0 +1,12 @@ +# Changes + +Link here all the PRs that are included in this release + +# Release-candidates + +Link here the release-candidates that were deployed as part of this release + +# Checklist + +- [ ] I've read and followed the release process described in https://github.com/HathorNetwork/ops-tools/blob/master/docs/release-guides/wallet-service.md#stable-release +- [ ] The QA process was run successfully during the tests of the corresponding release-candidate(s) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index e6cf4627..00000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: deploy - -on: - push: - branches: [dev, master, ci/deploy] - tags: ['v*.*.*'] - -env: - AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: 'eu-central-1' - -jobs: - deploy: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Push Dev Image - if: github.ref == 'refs/heads/dev' - run: | - make build-and-push-docker - - echo "deployed_environment=dev-testnet" >> $GITHUB_ENV - - name: Push Testnet Image - if: github.ref == 'refs/heads/master' - run: | - commit=`git rev-parse HEAD`; - timestamp=`date +%s`; - export DOCKER_IMAGE_TAG="testnet-$commit-$timestamp"; - - make build-and-push-docker - - echo "deployed_environment=testnet" >> $GITHUB_ENV - - name: Push Mainnet Image - if: startsWith(github.ref, 'refs/tags/v') - run: | - export DOCKER_IMAGE_TAG=${GITHUB_REF#refs/*/} - make build-and-push-docker - - echo "deployed_environment=mainnet" >> $GITHUB_ENV - - name: Slack Notification - if: env.deployed_environment - uses: rtCamp/action-slack-notify@28e8b353eabda5998a2e1203aed33c5999944779 - env: - SLACK_CHANNEL: deploys - SLACK_COLOR: ${{ job.status }} # or a specific color like 'good' or '#ff00ff' - SLACK_MESSAGE: 'Make sure the image is correctly deployed by checking if a new commit by fluxcdbot was made in: https://github.com/HathorNetwork/ops-tools/commits/master' - SLACK_TITLE: 'WalletServiceDaemon - new ${{ env.deployed_environment }} Docker image pushed :rocket:' - SLACK_USERNAME: HathorSlack - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - SLACK_FOOTER: '' - MSG_MINIMAL: actions url - - name: Clean - run: | - rm /home/runner/.docker/config.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2bde8348..2ccc6000 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,41 +1,117 @@ name: CI on: [push] -env: - NODE_OPTIONS: --max_old_space_size=4096 jobs: - build: - name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} - - runs-on: ${{ matrix.os }} - strategy: - matrix: - node: ['10.x', '12.x', '14.x'] - os: [ubuntu-latest, windows-latest, macOS-latest] - + test: + runs-on: ubuntu-latest + services: + mysql: + # We are using this image because the official one didn't + # support settings default-authentication-plugin using env var + # About the --default-authentication-plugin: https://stackoverflow.com/questions/50093144/mysql-8-0-client-does-not-support-authentication-protocol-requested-by-server/56509065#56509065 + image: centos/mysql-80-centos7 + env: + MYSQL_DATABASE: wallet_service_ci + MYSQL_USER: wallet_service_user + MYSQL_PASSWORD: password + MYSQL_DEFAULT_AUTHENTICATION_PLUGIN: mysql_native_password + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval 10s + --health-timeout 5s + --health-retries 3 + redis: + image: redis:6.2 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node }} + - name: Checkout code + uses: actions/checkout@v3 - - name: Install deps and build (with cache) - uses: bahmutov/npm-install@c67aaab58a864ea2873950cde9c1c9379f9f711a + - name: Install Nix + uses: cachix/install-nix-action@v20 + with: + nix_path: nixpkgs=channel:nixos-unstable + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Cache Nix + uses: DeterminateSystems/magic-nix-cache-action@v2 - - name: Lint - run: yarn lint + - name: Install dependencies + run: | + nix develop . -c yarn install - - name: Test - run: yarn test --ci --coverage --maxWorkers=2 + - name: Initialize DB + run: | + nix develop . -c yarn sequelize db:migrate + env: + NODE_ENV: test + CI_DB_NAME: wallet_service_ci + CI_DB_USERNAME: wallet_service_user + CI_DB_PASSWORD: password + CI_DB_HOST: 127.0.0.1 + CI_DB_PORT: 3306 - - name: Build - run: yarn build + - name: Run tests on the daemon + run: | + nix develop . -c yarn workspace sync-daemon run test + env: + DB_ENDPOINT: 127.0.0.1 + DB_NAME: wallet_service_ci + DB_USER: wallet_service_user + DB_PASS: password + DB_PORT: 3306 + STREAM_ID: f7d9157c-9906-4bd2-bc84-cfb9f5b607d1 + FULLNODE_PEER_ID: bdf4fa876f5cdba84be0cab53b21fc9eb45fe4b3d6ede99f493119d37df4e560 - - name: Upload coverage - uses: codecov/codecov-action@v3 - if: ${{ matrix.node-version }} == 14.x - with: - verbose: true + - name: Run tests on the wallet-service + run: | + nix develop . -c yarn workspace wallet-service jest + env: + NODE_ENV: test + STAGE: local + MAX_ADDRESS_GAP: 10 + NETWORK: mainnet + BLOCK_REWARD_LOCK: 300 + DEV_DB: mysql + DB_ENDPOINT: 127.0.0.1 + DB_NAME: wallet_service_ci + DB_USER: wallet_service_user + DB_PASS: password + DB_PORT: 3306 + CI_DB_USERNAME: wallet_service_user + CI_DB_PASSWORD: password + CI_DB_NAME: wallet_service_ci + CONFIRM_FIRST_ADDRESS: true + SERVICE_NAME: hathor-wallet-service + DEFAULT_SERVER: https://node1.mainnet.hathor.network/v1a/ + VOIDED_TX_OFFSET: 5 + WS_DOMAIN: ws.wallet-service.hathor.network + AUTH_SECRET: "" + WALLET_SERVICE_LAMBDA_ENDPOINT: "" + FIREBASE_PROJECT_ID: "" + FIREBASE_PRIVATE_KEY_ID: "" + FIREBASE_PRIVATE_KEY: "" + FIREBASE_CLIENT_EMAIL: "" + FIREBASE_CLIENT_ID: "" + FIREBASE_AUTH_URI: "" + FIREBASE_TOKEN_URI: "" + FIREBASE_AUTH_PROVIDER_X509_CERT_URL: "" + FIREBASE_CLIENT_X509_CERT_URL: "" + APPLICATION_NAME: "hathor-wallet-service" + ACCOUNT_ID: 1234 + ALERT_MANAGER_REGION: us-east-1 + ALERT_MANAGER_TOPIC: alert-topic + PUSH_ALLOWED_PROVIDERS: "" + - name: Run integration tests on the daemon + run: | + export NODE_ENV=test + nix develop . -c yarn workspace sync-daemon run test_integration diff --git a/.gitignore b/.gitignore index 29f34121..c30fb637 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,12 @@ node_modules dist .env +.direnv/ +coverage/ +packages/daemon/dist +packages/daemon/node_modules +packages/wallet-service/node_modules +packages/wallet-service/.serverless +packages/wallet-service/.webpack +packages/wallet-service/.env* +.yarn/ diff --git a/.sequelizerc b/.sequelizerc new file mode 100644 index 00000000..2a2b3162 --- /dev/null +++ b/.sequelizerc @@ -0,0 +1,8 @@ +const path = require('path'); + +module.exports = { + 'config': path.resolve('db', 'config.js'), + 'models-path': path.resolve('db', 'models'), + 'seeders-path': path.resolve('db', 'seeders'), + 'migrations-path': path.resolve('db', 'migrations'), +}; diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 00000000..d88b7277 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,7 @@ +compressionLevel: mixed + +enableGlobalCache: false + +nmHoistingLimits: dependencies + +nodeLinker: node-modules diff --git a/Dockerfile b/Dockerfile index 842133fd..fb630757 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,35 @@ -# Copyright 2020 Hathor Labs +# Copyright 2024 Hathor Labs # This software is provided ‘as-is’, without any express or implied # warranty. In no event will the authors be held liable for any damages # arising from the use of this software. # This software cannot be redistributed unless explicitly agreed in writing with the authors. -FROM node:14 AS builder -COPY package.json /app/ +# Build phase +FROM node:20-alpine AS builder -RUN cd /app && npm install +WORKDIR /app -COPY . /app/ +RUN apk update && apk add python3 g++ make -RUN cd /app && npm run build +COPY . . -FROM node:14-alpine3.13 +# Use the last stable berry version: +RUN yarn set version stable -COPY --from=builder /app/dist/ /app/ -COPY --from=builder /app/package.json /app/ +# This will install dependencies for the sync-daemon, devDependencies included: +RUN yarn workspaces focus sync-daemon -RUN cd /app && npm install --production +RUN yarn workspace sync-daemon build -CMD node /app/index.js +# This will remove all dependencies and install production deps only: +RUN yarn workspaces focus sync-daemon --production + +FROM node:20-alpine + +WORKDIR /app + +COPY --from=builder /app/packages/daemon/dist . +COPY --from=builder /app/packages/daemon/node_modules ./node_modules + +CMD ["node", "index.js"] diff --git a/Makefile b/Makefile index deac029d..1695bed3 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,40 @@ -.PHONY: build-and-push-docker -build-and-push-docker: - bash scripts/build-and-push-docker.sh +.PHONY: build-and-push-daemon +build-and-push-daemon: + bash scripts/build-and-push-daemon.sh + +.PHONY: build-daemon +build-daemon: + bash scripts/build-daemon.sh + +.PHONY: push-daemon +push-daemon: + bash scripts/push-daemon.sh + +.PHONY: deploy-lambdas-dev-testnet +deploy-lambdas-dev-testnet: + AWS_SDK_LOAD_CONFIG=1 yarn workspace wallet-service run serverless deploy --stage dev-testnet --region eu-central-1 + +.PHONY: deploy-lambdas-testnet +deploy-lambdas-testnet: + AWS_SDK_LOAD_CONFIG=1 yarn workspace wallet-service run serverless deploy --stage testnet --region eu-central-1 + +.PHONY: deploy-lambdas-mainnet-staging +deploy-lambdas-mainnet-staging: + AWS_SDK_LOAD_CONFIG=1 yarn workspace wallet-service run serverless deploy --stage mainnet-stg --region eu-central-1 + +.PHONY: deploy-lambdas-mainnet +deploy-lambdas-mainnet: + AWS_SDK_LOAD_CONFIG=1 yarn workspace wallet-service run serverless deploy --stage mainnet --region eu-central-1 + +.PHONY invoke-local: +invoke-local: + AWS_SDK_LOAD_CONFIG=1 yarn workspace wallet-service run serverless invoke local --function $(FUNCTION) --stage dev-testnet --region eu-central-1 + +.PHONY: migrate +migrate: + @echo "Migrating..." + npx sequelize-cli db:migrate + +.PHONY: new-migration +new-migration: + npx sequelize migration:generate --name "$(NAME)" diff --git a/README.md b/README.md index 52c11a89..2cc44002 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +Refer to https://github.com/HathorNetwork/rfcs/blob/master/projects/wallet-service-reliable-integration/0001-design.md + # Hathor Wallet Service -- Sync Daemon ## Running @@ -6,11 +8,13 @@ #### System dependencies -You need nodejs installed on your enviroment, we are using the latest Active LTS version (v14.16.1) on the dev environment. You can read more about installing nodejs on https://nodejs.org/en/download/package-manager/ +You need nodejs installed on your enviroment, we suggest the latest Active LTS version (v18.x.x). #### Clone the project and install dependencies -`git clone https://github.com/HathorNetwork/hathor-wallet-service-sync_daemon.git && npm install` +`git clone https://github.com/HathorNetwork/hathor-wallet-service-sync_daemon.git` + +`npm install` #### Add env variables or an .env file to the repository: @@ -18,17 +22,26 @@ Example: ``` NETWORK=testnet -MAX_ADDRESS_GAP=20 -WALLET_SERVICE_NAME=hathor-wallet-service -WALLET_SERVICE_STAGE=local -DEFAULT_SERVER=http://fullnode_url/v1a/ +DB_ENDPOINT=localhost +DB_PORT=3306 +DB_USER=hathor +DB_PASS=hathor +WS_URL=ws://localhost:3003 +FULLNODE_PEER_ID=74f75f4df6b19856a75dea6ed894441fbee7768bc561806c7a2fe6368ce4db18 +FULLNODE_STREAM_ID=f10ed6b9-8d77-430d-b85f-ae20257af465 +ALERT_QUEUE_URL=... +TX_CACHE_SIZE=10000 ``` `NETWORK` - The current hathor network we want to connect to -`MAX_ADDRESS_GAP` - The full-node configured GAP between addresses -`WALLET_SERVICE_NAME` - The Wallet-Service's service name as it was registered on AWS -`WALLET_SERVICE_STAGE` - Wallet-Service's deployment stage, e.g. `local`, `production`, `staging` -`DEFAULT_SERVER` - The full-node API url +`DB_ENDPOINT` - The MySQL database endpoint we want to connect to +`DB_PORT` - The MySQL database port number +`DB_USER` - The MySQL database username to use +`DB_PASS` - The MySQL database password to use +`WS_URL` - The fullnode event websocket feed +`FULLNODE_PEER_ID` - The fullnode peer id +`FULLNODE_STREAM_ID` - The fullnode stream id +`ALERT_QUEUE_URL` - The alert queue to publish alerts to If the wallet-service is not running locally, you also need to specify the AWS-SDK env variables: @@ -39,70 +52,4 @@ AWS_ACCESS_KEY_ID="..." AWS_SECRET_ACCESS_KEY="..." ``` -#### Run: - -`npm start` - - -### Deploy - -The recommended way to deploy this service is to use docker. - -#### Building the image: - -`docker build -t hathor/sync-daemon .` - -#### Running: - -``` -docker run -d -e WALLET_SERVICE_STAGE="production" \ - -e NODE_ENV="production" \ - -e AWS_REGION="us-east-1" \ - -e AWS_DEFAULT_REGION="us-east-1" \ - -e AWS_ACCESS_KEY_ID="..." \ - -e AWS_SECRET_ACCESS_KEY="..." \ - -e NETWORK="testnet" \ - -e MAX_ADDRESS_GAP=20 \ - -e NETWORK="testnet" \ - -e WALLET_SERVICE_NAME="hathor-wallet-service" \ - -e DEFAULT_SERVER="http://fullnode:8082/v1a/" \ - -ti localhost/hathor/sync-daemon -``` - -In this example, we are passing the env variables to the container and running as a daemon (`-d`). We are also expecting a fullnode to be running on fullnode:8082. - -## State Machine - -The state machine diagram can be visualized at https://xstate.js.org/viz/?gist=19dd8bc6d62533add23e124ef31adb78 - -## States: - -### Idle - -The machine starts at the idle state, it will stay there until a `NEW_BLOCK` action is received. - -Every time the state of the machine is transitioned to `idle`, the machine will check if `hasMoreBlocks` is set on the state context. If it is, the machine will transition to `syncing`. - -#### Actions: - `NEW_BLOCK`: When a `NEW_BLOCK` action is received, the machine will transition to the `syncing` state. - -### Syncing - -Everytime the state of the machine is transitioned to `syncing`, the machine will invoke the `syncHandler` service that will start syncing new blocks. - -#### Actions: - `NEW_BLOCK`: When a `NEW_BLOCK` action is received, the machine will assign `true` to the `hasMoreBlocks` context on the state, so the next time we transition to `IDLE`, the machine will know that there are more blocks to be downloaded. - `DONE`: When a `DONE` action is received, the machine will transition to `idle` to await for new blocks - `ERROR`: When a `ERROR` action is received, the machine will transition to the `failure` state - `REORG`: When a `REORG` action is received, the machine will transition to the `reorg` state - `STOP`: When a `STOP` action is received, the machine will transition to the `idle` state - -### Failure - -This is a `final` state, meaning that the machine will ignore all actions and wait for a manual restart. - -This state can trigger actions to try to automatically solve issues or notify us about it. - -### Reorg - -This is temporarily a `final` state, this will be changed on a new PR with the reorg code. +These are used for communicating with the alert SQS diff --git a/db/config.js b/db/config.js new file mode 100644 index 00000000..7d201df9 --- /dev/null +++ b/db/config.js @@ -0,0 +1,37 @@ +require('dotenv').config() + +module.exports = { + development: { + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: '127.0.0.1', + port: process.env.DB_PORT || 3306, + dialect: 'mysql', + dialectOptions: { + bigNumberStrings: true, + }, + }, + test: { + username: process.env.CI_DB_USERNAME, + password: process.env.CI_DB_PASSWORD, + database: process.env.CI_DB_NAME, + host: process.env.CI_DB_HOST || '127.0.0.1', + port: process.env.CI_DB_PORT || 3306, + dialect: 'mysql', + dialectOptions: { + bigNumberStrings: true, + }, + }, + production: { + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: process.env.DB_ENDPOINT, + port: process.env.DB_PORT, + dialect: 'mysql', + dialectOptions: { + bigNumberStrings: true, + }, + }, +}; diff --git a/db/migrations/20210706163010-create-address.js b/db/migrations/20210706163010-create-address.js new file mode 100644 index 00000000..27adc999 --- /dev/null +++ b/db/migrations/20210706163010-create-address.js @@ -0,0 +1,29 @@ +'use strict'; // eslint-disable-line +module.exports = { + up: async (queryInterface, Sequelize) => { // eslint-disable-line + await queryInterface.createTable('address', { + address: { + primaryKey: true, + allowNull: false, + type: Sequelize.STRING(34), + }, + index: { + allowNull: true, + defaultValue: null, + type: Sequelize.INTEGER.UNSIGNED, + }, + wallet_id: { + allowNull: true, + defaultValue: null, + type: Sequelize.STRING(64), + }, + transactions: { + allowNull: false, + type: Sequelize.INTEGER.UNSIGNED, + }, + }); + }, + down: async (queryInterface, Sequelize) => { // eslint-disable-line + await queryInterface.dropTable('address'); + }, +}; diff --git a/db/migrations/20210706164553-create-address-balance.js b/db/migrations/20210706164553-create-address-balance.js new file mode 100644 index 00000000..420e04fc --- /dev/null +++ b/db/migrations/20210706164553-create-address-balance.js @@ -0,0 +1,46 @@ +'use strict'; // eslint-disable-line +module.exports = { + up: async (queryInterface, Sequelize) => { // eslint-disable-line + await queryInterface.createTable('address_balance', { + address: { + type: Sequelize.STRING(34), + allowNull: false, + primaryKey: true, + }, + token_id: { + type: Sequelize.STRING(64), + allowNull: false, + primaryKey: true, + }, + unlocked_balance: { + type: Sequelize.BIGINT.UNSIGNED, + allowNull: false, + }, + locked_balance: { + type: Sequelize.BIGINT.UNSIGNED, + allowNull: false, + }, + unlocked_authorities: { + type: Sequelize.TINYINT.UNSIGNED, + allowNull: false, + defaultValue: 0, + }, + locked_authorities: { + type: Sequelize.TINYINT.UNSIGNED, + allowNull: false, + defaultValue: 0, + }, + timelock_expires: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: true, + }, + transactions: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + }); + }, + down: async (queryInterface, Sequelize) => { // eslint-disable-line + await queryInterface.dropTable('address_balance'); + }, +}; diff --git a/db/migrations/20210706172327-create-address-tx-history.js b/db/migrations/20210706172327-create-address-tx-history.js new file mode 100644 index 00000000..403a04ee --- /dev/null +++ b/db/migrations/20210706172327-create-address-tx-history.js @@ -0,0 +1,38 @@ +'use strict'; // eslint-disable-line +module.exports = { + up: async (queryInterface, Sequelize) => { // eslint-disable-line + await queryInterface.createTable('address_tx_history', { + address: { + type: Sequelize.STRING(34), + allowNull: false, + primaryKey: true, + }, + tx_id: { + type: Sequelize.STRING(64), + allowNull: false, + primaryKey: true, + }, + token_id: { + type: Sequelize.STRING(64), + allowNull: false, + primaryKey: true, + }, + balance: { + type: Sequelize.BIGINT, + allowNull: false, + }, + timestamp: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + voided: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + }); + }, + down: async (queryInterface, Sequelize) => { // eslint-disable-line + await queryInterface.dropTable('address_tx_history'); + }, +}; diff --git a/db/migrations/20210706175820-create-version-data.js b/db/migrations/20210706175820-create-version-data.js new file mode 100644 index 00000000..72f9910e --- /dev/null +++ b/db/migrations/20210706175820-create-version-data.js @@ -0,0 +1,60 @@ +'use strict'; // eslint-disable-line +module.exports = { + up: async (queryInterface, Sequelize) => { // eslint-disable-line + await queryInterface.createTable('version_data', { + id: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + primaryKey: true, + defaultValue: 1, + }, + timestamp: { + type: Sequelize.BIGINT.UNSIGNED, + allowNull: false, + }, + version: { + type: Sequelize.STRING(11), + allowNull: false, + }, + network: { + type: Sequelize.STRING(8), + allowNull: false, + }, + min_weight: { + type: Sequelize.FLOAT.UNSIGNED, + allowNull: false, + }, + min_tx_weight: { + type: Sequelize.FLOAT.UNSIGNED, + allowNull: false, + }, + min_tx_weight_coefficient: { + type: Sequelize.FLOAT.UNSIGNED, + allowNull: false, + }, + min_tx_weight_k: { + type: Sequelize.FLOAT.UNSIGNED, + allowNull: false, + }, + token_deposit_percentage: { + type: Sequelize.FLOAT.UNSIGNED, + allowNull: false, + }, + reward_spend_min_blocks: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + max_number_inputs: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + max_number_outputs: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + }); + }, + down: async (queryInterface, Sequelize) => { // eslint-disable-line + await queryInterface.dropTable('version_data'); + }, +}; diff --git a/db/migrations/20210707174009-create-token.js b/db/migrations/20210707174009-create-token.js new file mode 100644 index 00000000..a6548f2c --- /dev/null +++ b/db/migrations/20210707174009-create-token.js @@ -0,0 +1,23 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('token', { + id: { + type: Sequelize.STRING(64), + allowNull: false, + primaryKey: true, + }, + name: { + type: Sequelize.STRING(30), + allowNull: false, + }, + symbol: { + type: Sequelize.STRING(5), + allowNull: false, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('token'); + }, +}; diff --git a/db/migrations/20210707174416-create-tx-proposal.js b/db/migrations/20210707174416-create-tx-proposal.js new file mode 100644 index 00000000..3f344461 --- /dev/null +++ b/db/migrations/20210707174416-create-tx-proposal.js @@ -0,0 +1,32 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('tx_proposal', { + id: { + type: Sequelize.STRING(36), + allowNull: false, + primaryKey: true, + }, + wallet_id: { + type: Sequelize.STRING(64), + allowNull: false, + }, + status: { + type: Sequelize.ENUM(['open', 'sent', 'send_error', 'cancelled']), + allowNull: false, + }, + created_at: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + updated_at: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: true, + defaultValue: null, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('tx_proposal'); + }, +}; diff --git a/db/migrations/20210707183801-create-tx-proposal-outputs.js b/db/migrations/20210707183801-create-tx-proposal-outputs.js new file mode 100644 index 00000000..e5e571b7 --- /dev/null +++ b/db/migrations/20210707183801-create-tx-proposal-outputs.js @@ -0,0 +1,38 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('tx_proposal_outputs', { + tx_proposal_id: { + type: Sequelize.STRING(36), + primaryKey: true, + allowNull: false, + }, + index: { + type: Sequelize.TINYINT.UNSIGNED, + primaryKey: true, + allowNull: false, + }, + address: { + type: Sequelize.STRING(34), + allowNull: false, + }, + token_id: { + type: Sequelize.STRING(64), + allowNull: false, + }, + value: { + type: Sequelize.BIGINT, + allowNull: true, + defaultValue: null, + }, + timelock: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: true, + defaultValue: null, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('tx_proposal_outputs'); + } +}; diff --git a/db/migrations/20210707184609-create-wallet.js b/db/migrations/20210707184609-create-wallet.js new file mode 100644 index 00000000..1fa03323 --- /dev/null +++ b/db/migrations/20210707184609-create-wallet.js @@ -0,0 +1,38 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('wallet', { + id: { + type: Sequelize.STRING(64), + primaryKey: true, + allowNull: false, + }, + xpubkey: { + type: Sequelize.STRING(120), + allowNull: false, + }, + status: { + type: Sequelize.ENUM(['creating', 'ready', 'error']), + allowNull: false, + defaultValue: 'creating', + }, + max_gap: { + type: Sequelize.SMALLINT.UNSIGNED, + allowNull: false, + defaultValue: 20, + }, + created_at: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + ready_at: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: true, + defaultValue: null, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('wallet'); + } +}; diff --git a/db/migrations/20210707185314-create-wallet-tx-history.js b/db/migrations/20210707185314-create-wallet-tx-history.js new file mode 100644 index 00000000..e9862171 --- /dev/null +++ b/db/migrations/20210707185314-create-wallet-tx-history.js @@ -0,0 +1,38 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('wallet_tx_history', { + wallet_id: { + type: Sequelize.STRING(64), + primaryKey: true, + allowNull: false, + }, + token_id: { + type: Sequelize.STRING(64), + primaryKey: true, + allowNull: false, + }, + tx_id: { + type: Sequelize.STRING(64), + primaryKey: true, + allowNull: false, + }, + balance: { + type: Sequelize.BIGINT, + allowNull: false, + }, + timestamp: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + voided: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('wallet_tx_history'); + } +}; diff --git a/db/migrations/20210707190612-create-transaction.js b/db/migrations/20210707190612-create-transaction.js new file mode 100644 index 00000000..4a3284a7 --- /dev/null +++ b/db/migrations/20210707190612-create-transaction.js @@ -0,0 +1,41 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('transaction', { + tx_id: { + type: Sequelize.STRING(64), + allowNull: false, + primaryKey: true, + }, + timestamp: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + version: { + type: Sequelize.TINYINT.UNSIGNED, + allowNull: false, + }, + voided: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + height: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: true, + defaultValue: null, + }, + }).then(() => queryInterface.addIndex( + 'transaction', + ['version'], + { + name: 'transaction_version_idx', + fields: ['version'], + using: 'HASH', + }, + )); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('transaction'); + }, +}; diff --git a/db/migrations/20210707191309-create-wallet-balance.js b/db/migrations/20210707191309-create-wallet-balance.js new file mode 100644 index 00000000..1d0a67d1 --- /dev/null +++ b/db/migrations/20210707191309-create-wallet-balance.js @@ -0,0 +1,46 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('wallet_balance', { + wallet_id: { + type: Sequelize.STRING(64), + allowNull: false, + primaryKey: true, + }, + token_id: { + type: Sequelize.STRING(64), + allowNull: false, + primaryKey: true, + }, + unlocked_balance: { + type: Sequelize.BIGINT.UNSIGNED, + allowNull: false, + }, + locked_balance: { + type: Sequelize.BIGINT.UNSIGNED, + allowNull: false, + }, + unlocked_authorities: { + type: Sequelize.TINYINT.UNSIGNED, + allowNull: false, + defaultValue: 0, + }, + locked_authorities: { + type: Sequelize.TINYINT.UNSIGNED, + allowNull: false, + defaultValue: 0, + }, + timelock_expires: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: true, + }, + transactions: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('wallet_balance'); + }, +}; diff --git a/db/migrations/20210707191936-create-tx-output.js b/db/migrations/20210707191936-create-tx-output.js new file mode 100644 index 00000000..d804ff07 --- /dev/null +++ b/db/migrations/20210707191936-create-tx-output.js @@ -0,0 +1,72 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('tx_output', { + tx_id: { + type: Sequelize.STRING(64), + allowNull: false, + primaryKey: true, + }, + index: { + type: Sequelize.TINYINT.UNSIGNED, + allowNull: false, + primaryKey: true, + }, + token_id: { + type: Sequelize.STRING(64), + allowNull: false, + }, + address: { + type: Sequelize.STRING(34), + allowNull: false, + }, + value: { + type: Sequelize.BIGINT.UNSIGNED, + allowNull: false, + }, + authorities: { + type: Sequelize.TINYINT.UNSIGNED, + allowNull: true, + defaultValue: null, + }, + timelock: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: true, + defaultValue: null, + }, + heightlock: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: true, + defaultValue: null, + }, + locked: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + tx_proposal: { + type: Sequelize.STRING(36), + allowNull: true, + defaultValue: null, + }, + tx_proposal_index: { + type: Sequelize.TINYINT.UNSIGNED, + allowNull: true, + defaultValue: null, + }, + spent_by: { + type: Sequelize.STRING(64), + allowNull: true, + defaultValue: null, + }, + voided: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('tx_output'); + } +}; diff --git a/db/migrations/20210714204028-tx_output_heightlock_index.js b/db/migrations/20210714204028-tx_output_heightlock_index.js new file mode 100644 index 00000000..92a6ee78 --- /dev/null +++ b/db/migrations/20210714204028-tx_output_heightlock_index.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + return queryInterface.addIndex( + 'tx_output', + ['heightlock'], + { + name: 'tx_output_heightlock_idx', + fields: 'heightlock', + } + ) + }, + down: async (queryInterface, Sequelize) => { + return queryInterface.removeIndex('tx_output', 'tx_output_heightlock_idx'); + }, +}; diff --git a/db/migrations/20210714204037-tx_output_timelock_index.js b/db/migrations/20210714204037-tx_output_timelock_index.js new file mode 100644 index 00000000..7299b6d9 --- /dev/null +++ b/db/migrations/20210714204037-tx_output_timelock_index.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + return queryInterface.addIndex( + 'tx_output', + ['timelock'], + { + name: 'tx_output_timelock_idx', + fields: 'timelock', + } + ) + }, + down: async (queryInterface, Sequelize) => { + return queryInterface.removeIndex('tx_output', 'tx_output_timelock_idx'); + }, +}; diff --git a/db/migrations/20210714204059-transaction_height_index.js b/db/migrations/20210714204059-transaction_height_index.js new file mode 100644 index 00000000..ae4a8968 --- /dev/null +++ b/db/migrations/20210714204059-transaction_height_index.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + return queryInterface.addIndex( + 'transaction', + ['height'], + { + name: 'transaction_height_idx', + fields: 'height', + } + ) + }, + down: async (queryInterface, Sequelize) => { + return queryInterface.removeIndex('transaction', 'transaction_height_idx'); + }, +}; diff --git a/db/migrations/20210714205430-tx_output_address_index.js b/db/migrations/20210714205430-tx_output_address_index.js new file mode 100644 index 00000000..d0b7a9de --- /dev/null +++ b/db/migrations/20210714205430-tx_output_address_index.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + return queryInterface.addIndex( + 'tx_output', + ['address'], + { + name: 'tx_output_address_idx', + fields: 'address', + } + ) + }, + down: async (queryInterface, Sequelize) => { + return queryInterface.removeIndex('tx_output', 'tx_output_address_idx'); + }, +}; diff --git a/db/migrations/20210714205436-tx_output_token_id_index.js b/db/migrations/20210714205436-tx_output_token_id_index.js new file mode 100644 index 00000000..369c4a69 --- /dev/null +++ b/db/migrations/20210714205436-tx_output_token_id_index.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + return queryInterface.addIndex( + 'tx_output', + ['token_id'], + { + name: 'tx_output_token_id_idx', + fields: 'token_id', + } + ) + }, + down: async (queryInterface, Sequelize) => { + return queryInterface.removeIndex('tx_output', 'tx_output_token_id_idx'); + }, +}; diff --git a/db/migrations/20210718180425-remove_tx_proposal_outputs.js b/db/migrations/20210718180425-remove_tx_proposal_outputs.js new file mode 100644 index 00000000..84a939fa --- /dev/null +++ b/db/migrations/20210718180425-remove_tx_proposal_outputs.js @@ -0,0 +1,40 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('tx_proposal_outputs'); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.createTable('tx_proposal_outputs', { + tx_proposal_id: { + type: Sequelize.STRING(36), + primaryKey: true, + allowNull: false, + }, + index: { + type: Sequelize.TINYINT.UNSIGNED, + primaryKey: true, + allowNull: false, + }, + address: { + type: Sequelize.STRING(34), + allowNull: false, + }, + token_id: { + type: Sequelize.STRING(64), + allowNull: false, + }, + value: { + type: Sequelize.BIGINT, + allowNull: true, + defaultValue: null, + }, + timelock: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: true, + defaultValue: null, + }, + }); + } +}; diff --git a/db/migrations/20210728212035-wallet-status-add-retry-count.js b/db/migrations/20210728212035-wallet-status-add-retry-count.js new file mode 100644 index 00000000..ab566975 --- /dev/null +++ b/db/migrations/20210728212035-wallet-status-add-retry-count.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('wallet', 'retry_count', { + type: Sequelize.SMALLINT.UNSIGNED, + allowNull: false, + defaultValue: 0, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('wallet', 'retry_count') + } +}; diff --git a/db/migrations/20210902175955-tx_output_txproposal_index.js b/db/migrations/20210902175955-tx_output_txproposal_index.js new file mode 100644 index 00000000..cc5ed23f --- /dev/null +++ b/db/migrations/20210902175955-tx_output_txproposal_index.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + return queryInterface.addIndex( + 'tx_output', + ['tx_proposal'], + { + name: 'tx_output_txproposal_idx', + fields: 'tx_proposal', + } + ) + }, + down: async (queryInterface, Sequelize) => { + return queryInterface.removeIndex('tx_output', 'tx_output_txproposal_idx'); + }, +}; diff --git a/db/migrations/20211017211022-address_tx_history_tx_id_index.js b/db/migrations/20211017211022-address_tx_history_tx_id_index.js new file mode 100644 index 00000000..3a0e615f --- /dev/null +++ b/db/migrations/20211017211022-address_tx_history_tx_id_index.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + return queryInterface.addIndex( + 'address_tx_history', + ['tx_id'], + { + name: 'address_tx_history_txid_idx', + fields: 'tx_id', + } + ) + }, + down: async (queryInterface, Sequelize) => { + return queryInterface.removeIndex('address_tx_history', 'address_tx_history_txid_idx'); + }, +}; diff --git a/db/migrations/20211103023137-create-miner.js b/db/migrations/20211103023137-create-miner.js new file mode 100644 index 00000000..37991c17 --- /dev/null +++ b/db/migrations/20211103023137-create-miner.js @@ -0,0 +1,30 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('miner', { + address: { + type: Sequelize.STRING(34), + allowNull: false, + primaryKey: true, + }, + first_block: { + type: Sequelize.STRING(64), + allowNull: false, + primaryKey: false, + }, + last_block: { + type: Sequelize.STRING(64), + allowNull: false, + primaryKey: false, + }, + count: { + type: Sequelize.BIGINT.UNSIGNED, + allowNull: false, + primaryKey: false, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('miner'); + }, +}; diff --git a/db/migrations/20220120171115-add-auth-xpubkey.js b/db/migrations/20220120171115-add-auth-xpubkey.js new file mode 100644 index 00000000..87f131ab --- /dev/null +++ b/db/migrations/20220120171115-add-auth-xpubkey.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('wallet', 'auth_xpubkey', { + type: Sequelize.STRING(120), + allowNull: false, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('wallet', 'auth_xpubkey') + } +}; diff --git a/db/migrations/20220315192400-add-timestamp-fields-token.js b/db/migrations/20220315192400-add-timestamp-fields-token.js new file mode 100644 index 00000000..16be3e41 --- /dev/null +++ b/db/migrations/20220315192400-add-timestamp-fields-token.js @@ -0,0 +1,22 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('token', 'created_at', { + type: 'TIMESTAMP', + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }); + + await queryInterface.addColumn('token', 'updated_at', { + type: 'TIMESTAMP', + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + }); + }, + + down: async (queryInterface) => { + await queryInterface.removeColumn('token', 'created_at'); + await queryInterface.removeColumn('token', 'updated_at'); + }, +}; diff --git a/db/migrations/20220414214125-address_tx_history_tokenid_index.js b/db/migrations/20220414214125-address_tx_history_tokenid_index.js new file mode 100644 index 00000000..0349517f --- /dev/null +++ b/db/migrations/20220414214125-address_tx_history_tokenid_index.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + return queryInterface.addIndex( + 'address_tx_history', + ['token_id'], + { + name: 'address_tx_history_tokenid_idx', + fields: 'token_id', + } + ) + }, + down: async (queryInterface, Sequelize) => { + return queryInterface.removeIndex('address_tx_history', 'address_tx_history_tokenid_idx'); + }, +}; diff --git a/db/migrations/20220517191541-add-weight-to-tx.js b/db/migrations/20220517191541-add-weight-to-tx.js new file mode 100644 index 00000000..2ae5d498 --- /dev/null +++ b/db/migrations/20220517191541-add-weight-to-tx.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('transaction', 'weight', { + type: Sequelize.FLOAT.UNSIGNED, + // We will temporarily support null values for weight until the first data migration is done. + // After that, we must remove support for null values on weight column. + allowNull: true, + defaultValue: null, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('transaction', 'weight'); + } +}; diff --git a/db/migrations/20220523151819-address_balance-add-timestamp-fields.js b/db/migrations/20220523151819-address_balance-add-timestamp-fields.js new file mode 100644 index 00000000..ccbfe534 --- /dev/null +++ b/db/migrations/20220523151819-address_balance-add-timestamp-fields.js @@ -0,0 +1,22 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('address_balance', 'created_at', { + type: 'TIMESTAMP', + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }); + + await queryInterface.addColumn('address_balance', 'updated_at', { + type: 'TIMESTAMP', + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + }); + }, + + down: async (queryInterface) => { + await queryInterface.removeColumn('address_balance', 'created_at'); + await queryInterface.removeColumn('address_balance', 'updated_at'); + }, +}; diff --git a/db/migrations/20220531191235-add-timestamp-fields-tx.js b/db/migrations/20220531191235-add-timestamp-fields-tx.js new file mode 100644 index 00000000..34ee8d3d --- /dev/null +++ b/db/migrations/20220531191235-add-timestamp-fields-tx.js @@ -0,0 +1,49 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn('transaction', 'created_at', { + type: 'TIMESTAMP', + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, { transaction }); + + await queryInterface.addColumn('transaction', 'updated_at', { + type: 'TIMESTAMP', + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + }, { transaction }); + + await queryInterface.addIndex( + 'transaction', + ['updated_at'], + { + name: 'transaction_updated_at_idx', + fields: 'updated_at', + transaction, + } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + down: async (queryInterface) => { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('transaction', 'created_at', { transaction }); + await queryInterface.removeColumn('transaction', 'updated_at', { transaction }); + await queryInterface.removeIndex('transaction', 'transaction_updated_at_idx', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/db/migrations/20220721003340-wallet_highest_used_index.js b/db/migrations/20220721003340-wallet_highest_used_index.js new file mode 100644 index 00000000..fc842ddc --- /dev/null +++ b/db/migrations/20220721003340-wallet_highest_used_index.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + up: async (queryInterface) => { + await queryInterface.addColumn('wallet', 'last_used_address_index', { + type: 'INTEGER', + allowNull: false, + defaultValue: -1, + }); + }, + + down: async (queryInterface) => { + await queryInterface.removeColumn('wallet', 'last_used_address_index'); + }, +}; diff --git a/db/migrations/20220721154812-add_total_received.js b/db/migrations/20220721154812-add_total_received.js new file mode 100644 index 00000000..e81bdce6 --- /dev/null +++ b/db/migrations/20220721154812-add_total_received.js @@ -0,0 +1,35 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addIndex( + 'address_tx_history', + ['timestamp'], + { + name: 'address_tx_history_timestamp_idx', + fields: 'timestamp', + }, + ); + + await queryInterface.addIndex( + 'address_balance', + ['updated_at'], + { + name: 'address_balance_updated_at_idx', + fields: 'updated_at', + }, + ); + + await queryInterface.addColumn('address_balance', 'total_received', { + type: Sequelize.BIGINT.UNSIGNED, + allowNull: false, + defaultValue: 0, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeIndex('address_tx_history', 'address_tx_history_timestamp_idx'); + await queryInterface.removeIndex('address_balance', 'address_balance_updated_at_idx'); + await queryInterface.removeColumn('address_balance', 'total_received'); + }, +}; diff --git a/db/migrations/20220811192312-add-transaction-count-to-token.js b/db/migrations/20220811192312-add-transaction-count-to-token.js new file mode 100644 index 00000000..7ffe54b9 --- /dev/null +++ b/db/migrations/20220811192312-add-transaction-count-to-token.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('token', 'transactions', { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + defaultValue: 0, + }); + }, + + down: async (queryInterface) => { + await queryInterface.removeColumn('token', 'transactions'); + }, +}; diff --git a/db/migrations/20220811212756-add-hathor-to-token-table.js b/db/migrations/20220811212756-add-hathor-to-token-table.js new file mode 100644 index 00000000..9a97ae17 --- /dev/null +++ b/db/migrations/20220811212756-add-hathor-to-token-table.js @@ -0,0 +1,20 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('token', [{ + id: '00', + name: 'Hathor', + symbol: 'HTR', + transactions: 0, + }]); + }, + + down: async (queryInterface) => { + await queryInterface.bulkDelete('token', [{ + id: '00', + name: 'Hathor', + symbol: 'HTR', + }]); + }, +}; diff --git a/db/migrations/20220811222729-add-voided-index-to-address-tx-history.js b/db/migrations/20220811222729-add-voided-index-to-address-tx-history.js new file mode 100644 index 00000000..fdaa03aa --- /dev/null +++ b/db/migrations/20220811222729-add-voided-index-to-address-tx-history.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + up: async (queryInterface) => queryInterface.addIndex( + 'address_tx_history', + ['voided'], { + name: 'address_tx_history_voided_idx', + fields: 'voided', + }, + ), + down: async (queryInterface) => queryInterface.removeIndex( + 'address_tx_history', + 'address_tx_history_voided_idx', + ), +}; diff --git a/db/migrations/20220816175011-add-total-received-on-wallet-balance.js b/db/migrations/20220816175011-add-total-received-on-wallet-balance.js new file mode 100644 index 00000000..85f6d5e7 --- /dev/null +++ b/db/migrations/20220816175011-add-total-received-on-wallet-balance.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('wallet_balance', 'total_received', { + type: Sequelize.BIGINT.UNSIGNED, + allowNull: false, + defaultValue: 0, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('wallet_balance', 'total_received'); + } +}; diff --git a/db/migrations/20221108235926-create-pushdevices.js.js b/db/migrations/20221108235926-create-pushdevices.js.js new file mode 100644 index 00000000..ecc3a122 --- /dev/null +++ b/db/migrations/20221108235926-create-pushdevices.js.js @@ -0,0 +1,49 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('push_devices', { + device_id: { + type: Sequelize.STRING(256), + allowNull: false, + primaryKey: true, + }, + push_provider: { + type: Sequelize.ENUM(['ios', 'android']), + allowNull: false, + }, + wallet_id: { + type: Sequelize.STRING(64), + allowNull: false, + references: { + model: 'wallet', + key: 'id', + }, + }, + enable_push: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + enable_show_amounts: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + enable_only_new_tx: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + updated_at: { + type: 'TIMESTAMP', + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + }, + }); + }, + async down(queryInterface) { + await queryInterface.dropTable('push_devices'); + }, +}; diff --git a/db/migrations/20221114185330-remove-enable-only-new-tx-conlumn.js b/db/migrations/20221114185330-remove-enable-only-new-tx-conlumn.js new file mode 100644 index 00000000..68459cac --- /dev/null +++ b/db/migrations/20221114185330-remove-enable-only-new-tx-conlumn.js @@ -0,0 +1,16 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface) { + queryInterface.removeColumn('push_devices', 'enable_only_new_tx'); + }, + + async down(queryInterface, Sequelize) { + queryInterface.addColumn('push_devices', 'enable_only_new_tx', { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }); + }, +}; diff --git a/db/migrations/20230420153727-tx-output-add-spent_by-idx.js b/db/migrations/20230420153727-tx-output-add-spent_by-idx.js new file mode 100644 index 00000000..525a901a --- /dev/null +++ b/db/migrations/20230420153727-tx-output-add-spent_by-idx.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface) => { + await queryInterface.addIndex( + 'tx_output', + ['spent_by'], { + name: 'tx_output_spent_by_idx', + fields: 'spent_by', + }, + ); + }, + + down: async (queryInterface) => { + await queryInterface.removeIndex('tx_output', 'tx_output_spent_by_idx'); + }, +}; diff --git a/db/migrations/20230420153753-tx-output-add-voided-idx.js b/db/migrations/20230420153753-tx-output-add-voided-idx.js new file mode 100644 index 00000000..52a06e94 --- /dev/null +++ b/db/migrations/20230420153753-tx-output-add-voided-idx.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface) => { + await queryInterface.addIndex( + 'tx_output', + ['voided'], { + name: 'tx_output_voided_idx', + fields: 'voided', + }, + ); + }, + + down: async (queryInterface) => { + await queryInterface.removeIndex('tx_output', 'tx_output_voided_idx'); + }, +}; diff --git a/db/migrations/20230420153759-tx-output-add-locked-idx.js b/db/migrations/20230420153759-tx-output-add-locked-idx.js new file mode 100644 index 00000000..e3515e22 --- /dev/null +++ b/db/migrations/20230420153759-tx-output-add-locked-idx.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface) => { + await queryInterface.addIndex( + 'tx_output', + ['locked'], { + name: 'tx_output_locked_idx', + fields: 'locked', + }, + ); + }, + + down: async (queryInterface) => { + await queryInterface.removeIndex('tx_output', 'tx_output_locked_idx'); + }, +}; diff --git a/db/migrations/20230601151507-address_index_idx.js b/db/migrations/20230601151507-address_index_idx.js new file mode 100644 index 00000000..b1105f6f --- /dev/null +++ b/db/migrations/20230601151507-address_index_idx.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + up: async (queryInterface) => { + await queryInterface.addIndex( + 'address', + ['index'], { + name: 'address_index_idx', + fields: 'index', + }, + ); + }, + + down: async (queryInterface) => { + await queryInterface.removeIndex('address', 'address_index_idx'); + }, +}; diff --git a/db/migrations/20230929112709-add_sync_metadata_table.js b/db/migrations/20230929112709-add_sync_metadata_table.js new file mode 100644 index 00000000..1ad92437 --- /dev/null +++ b/db/migrations/20230929112709-add_sync_metadata_table.js @@ -0,0 +1,23 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('sync_metadata', { + id: { + type: Sequelize.INTEGER(), + primaryKey: true, + allowNull: false, + }, + last_event_id: { + type: Sequelize.INTEGER(), + allowNull: false, + }, + updated_at: { + type: Sequelize.STRING(64), + defaultValue: 0, + }, + }); + }, + down: async (queryInterface) => { + await queryInterface.dropTable('wallet_tx_history'); + } +}; diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..2a01bd58 --- /dev/null +++ b/flake.lock @@ -0,0 +1,108 @@ +{ + "nodes": { + "devshell": { + "inputs": { + "nixpkgs": "nixpkgs", + "systems": "systems" + }, + "locked": { + "lastModified": 1695973661, + "narHash": "sha256-BP2H4c42GThPIhERtTpV1yCtwQHYHEKdRu7pjrmQAwo=", + "owner": "numtide", + "repo": "devshell", + "rev": "cd4e2fda3150dd2f689caeac07b7f47df5197c31", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1649676176, + "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1677383253, + "narHash": "sha256-UfpzWfSxkfXHnb4boXZNaKsAcUrZT9Hw+tao1oZxd08=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9952d6bc395f5841262b006fbace8dd7e143b634", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1708247094, + "narHash": "sha256-H2VS7VwesetGDtIaaz4AMsRkPoSLEVzL/Ika8gnbUnE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "045b51a3ae66f673ed44b5bbd1f4a341d96703bf", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "devshell": "devshell", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs_2", + "unstableNixPkgs": "unstableNixPkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "unstableNixPkgs": { + "locked": { + "lastModified": 1708118438, + "narHash": "sha256-kk9/0nuVgA220FcqH/D2xaN6uGyHp/zoxPNUmPCMmEE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5863c27340ba4de8f83e7e3c023b9599c3cb3c80", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..7d012645 --- /dev/null +++ b/flake.nix @@ -0,0 +1,42 @@ +{ + description = "virtual environments"; + + inputs = { + devshell.url = "github:numtide/devshell"; + flake-utils.url = "github:numtide/flake-utils"; + unstableNixPkgs.url = "nixpkgs/nixos-unstable"; + }; + + outputs = { self, flake-utils, devshell, nixpkgs, unstableNixPkgs, ... }@inputs: + let + overlays.default = final: prev: + let + packages = self.packages.${final.system}; + inherit (packages) node-packages; + in + { + nodejs = final.nodejs_20; + nodePackages = prev.nodePackages; + yarn = (import unstableNixPkgs { system = final.system; }).yarn-berry; + }; + in + flake-utils.lib.eachDefaultSystem (system: { + devShell = + let pkgs = import nixpkgs { + inherit system; + + overlays = [ + devshell.overlays.default + overlays.default + ]; + }; + in + pkgs.devshell.mkShell { + packages = with pkgs; [ + nixpkgs-fmt + nodejs_20 + yarn + ]; + }; + }); +} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index f4ad2e85..00000000 --- a/package-lock.json +++ /dev/null @@ -1,14507 +0,0 @@ -{ - "name": "hathor-wallet-service-sync_daemon", - "version": "1.4.2-beta", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.12.13" - } - }, - "@babel/compat-data": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.12.tgz", - "integrity": "sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ==", - "dev": true - }, - "@babel/core": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.14.tgz", - "integrity": "sha512-wZso/vyF4ki0l0znlgM4inxbdrUvCb+cVz8grxDq+6C9k6qbqoIJteQOKicaKjCipU3ISV+XedCqpL2RJJVehA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.13.9", - "@babel/helper-compilation-targets": "^7.13.13", - "@babel/helper-module-transforms": "^7.13.14", - "@babel/helpers": "^7.13.10", - "@babel/parser": "^7.13.13", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.13", - "@babel/types": "^7.13.14", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.13.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", - "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", - "dev": true, - "requires": { - "@babel/types": "^7.13.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz", - "integrity": "sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.12.13.tgz", - "integrity": "sha512-CZOv9tGphhDRlVjVkAgm8Nhklm9RzSmWpX2my+t7Ua/KT616pEzXsQCjinzvkRvHWJ9itO4f296efroX23XCMA==", - "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.12.13", - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz", - "integrity": "sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.13.12", - "@babel/helper-validator-option": "^7.12.17", - "browserslist": "^4.14.5", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.13.11", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz", - "integrity": "sha512-ays0I7XYq9xbjCSvT+EvysLgfc3tOkwCULHjrnscGT3A9qD4sk3wXnJ3of0MAWsWGjdinFvajHU2smYuqXKMrw==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-member-expression-to-functions": "^7.13.0", - "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/helper-replace-supers": "^7.13.0", - "@babel/helper-split-export-declaration": "^7.12.13" - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.12.17", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.17.tgz", - "integrity": "sha512-p2VGmBu9oefLZ2nQpgnEnG0ZlRPvL8gAGvPUMQwUdaE8k49rOMuZpOwdQoy5qJf6K8jL3bcAMhVUlHAjIgJHUg==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.12.13", - "regexpu-core": "^4.7.1" - } - }, - "@babel/helper-define-polyfill-provider": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.1.5.tgz", - "integrity": "sha512-nXuzCSwlJ/WKr8qxzW816gwyT6VZgiJG17zR40fou70yfAcqjoNyTLl/DQ+FExw5Hx5KNqshmN8Ldl/r2N7cTg==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.13.0", - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/traverse": "^7.13.0", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.13.0.tgz", - "integrity": "sha512-qS0peLTDP8kOisG1blKbaoBg/o9OSa1qoumMjTK5pM+KDTtpxpsiubnCGP34vK8BXGcb2M9eigwgvoJryrzwWA==", - "dev": true, - "requires": { - "@babel/types": "^7.13.0" - } - }, - "@babel/helper-function-name": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", - "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", - "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.0.tgz", - "integrity": "sha512-0kBzvXiIKfsCA0y6cFEIJf4OdzfpRuNk4+YTeHZpGGc666SATFKTz6sRncwFnQk7/ugJ4dSrCj6iJuvW4Qwr2g==", - "dev": true, - "requires": { - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.0" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", - "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", - "dev": true, - "requires": { - "@babel/types": "^7.13.12" - } - }, - "@babel/helper-module-imports": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", - "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", - "dev": true, - "requires": { - "@babel/types": "^7.13.12" - } - }, - "@babel/helper-module-transforms": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz", - "integrity": "sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.13.12", - "@babel/helper-replace-supers": "^7.13.12", - "@babel/helper-simple-access": "^7.13.12", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/helper-validator-identifier": "^7.12.11", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.13", - "@babel/types": "^7.13.14" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", - "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", - "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", - "dev": true - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.13.0.tgz", - "integrity": "sha512-pUQpFBE9JvC9lrQbpX0TmeNIy5s7GnZjna2lhhcHC7DzgBs6fWn722Y5cfwgrtrqc7NAJwMvOa0mKhq6XaE4jg==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.12.13", - "@babel/helper-wrap-function": "^7.13.0", - "@babel/types": "^7.13.0" - } - }, - "@babel/helper-replace-supers": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz", - "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.13.12", - "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.12" - } - }, - "@babel/helper-simple-access": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", - "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", - "dev": true, - "requires": { - "@babel/types": "^7.13.12" - } - }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", - "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", - "dev": true, - "requires": { - "@babel/types": "^7.12.1" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", - "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", - "dev": true, - "requires": { - "@babel/types": "^7.12.13" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.12.17", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", - "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==", - "dev": true - }, - "@babel/helper-wrap-function": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.13.0.tgz", - "integrity": "sha512-1UX9F7K3BS42fI6qd2A4BjKzgGjToscyZTdp1DjknHLCIvpgne6918io+aL5LXFcER/8QWiwpoY902pVEqgTXA==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.0" - } - }, - "@babel/helpers": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.13.10.tgz", - "integrity": "sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==", - "dev": true, - "requires": { - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.0" - } - }, - "@babel/highlight": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", - "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.13.tgz", - "integrity": "sha512-OhsyMrqygfk5v8HmWwOzlYjJrtLaFhF34MrfG/Z73DgYCI6ojNUTUp2TYbtnjo8PegeJp12eamsNettCQjKjVw==", - "dev": true - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.13.12.tgz", - "integrity": "sha512-d0u3zWKcoZf379fOeJdr1a5WPDny4aOFZ6hlfKivgK0LY7ZxNfoaHL2fWwdGtHyVvra38FC+HVYkO+byfSA8AQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", - "@babel/plugin-proposal-optional-chaining": "^7.13.12" - } - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.8.tgz", - "integrity": "sha512-rPBnhj+WgoSmgq+4gQUtXx/vOcU+UYtjy1AA/aeD61Hwj410fwYyqfUcRP3lR8ucgliVJL/G7sXcNUecC75IXA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/helper-remap-async-to-generator": "^7.13.0", - "@babel/plugin-syntax-async-generators": "^7.8.4" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.13.0.tgz", - "integrity": "sha512-KnTDjFNC1g+45ka0myZNvSBFLhNCLN+GeGYLDEA8Oq7MZ6yMgfLoIRh86GRT0FjtJhZw8JyUskP9uvj5pHM9Zg==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.13.0", - "@babel/helper-plugin-utils": "^7.13.0" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.13.8.tgz", - "integrity": "sha512-ONWKj0H6+wIRCkZi9zSbZtE/r73uOhMVHh256ys0UzfM7I3d4n+spZNWjOnJv2gzopumP2Wxi186vI8N0Y2JyQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - } - }, - "@babel/plugin-proposal-export-namespace-from": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.13.tgz", - "integrity": "sha512-INAgtFo4OnLN3Y/j0VwAgw3HDXcDtX+C/erMvWzuV9v71r7urb6iyMXu7eM9IgLr1ElLlOkaHjJ0SbCmdOQ3Iw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.13.8.tgz", - "integrity": "sha512-w4zOPKUFPX1mgvTmL/fcEqy34hrQ1CRcGxdphBc6snDnnqJ47EZDIyop6IwXzAC8G916hsIuXB2ZMBCExC5k7Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/plugin-syntax-json-strings": "^7.8.3" - } - }, - "@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.13.8.tgz", - "integrity": "sha512-aul6znYB4N4HGweImqKn59Su9RS8lbUIqxtXTOcAGtNIDczoEFv+l1EhmX8rUBp3G1jMjKJm8m0jXVp63ZpS4A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.8.tgz", - "integrity": "sha512-iePlDPBn//UhxExyS9KyeYU7RM9WScAG+D3Hhno0PLJebAEpDZMocbDe64eqynhNAnwz/vZoL/q/QB2T1OH39A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - } - }, - "@babel/plugin-proposal-numeric-separator": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.13.tgz", - "integrity": "sha512-O1jFia9R8BUCl3ZGB7eitaAPu62TXJRHn7rh+ojNERCFyqRwJMTmhz+tJ+k0CwI6CLjX/ee4qW74FSqlq9I35w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.8.tgz", - "integrity": "sha512-DhB2EuB1Ih7S3/IRX5AFVgZ16k3EzfRbq97CxAVI1KSYcW+lexV8VZb7G7L8zuPVSdQMRn0kiBpf/Yzu9ZKH0g==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.13.8", - "@babel/helper-compilation-targets": "^7.13.8", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.13.0" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.13.8.tgz", - "integrity": "sha512-0wS/4DUF1CuTmGo+NiaHfHcVSeSLj5S3e6RivPTg/2k3wOv3jO35tZ6/ZWsQhQMvdgI7CwphjQa/ccarLymHVA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, - "@babel/plugin-proposal-optional-chaining": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.12.tgz", - "integrity": "sha512-fcEdKOkIB7Tf4IxrgEVeFC4zeJSTr78no9wTdBuZZbqF64kzllU0ybo2zrzm7gUQfxGhBgq4E39oRs8Zx/RMYQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "@babel/plugin-proposal-private-methods": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.13.0.tgz", - "integrity": "sha512-MXyyKQd9inhx1kDYPkFRVOBXQ20ES8Pto3T7UZ92xj2mY0EVD8oAVzeyYuVfy/mxAdTSIayOvg+aVzcHV2bn6Q==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.13.0", - "@babel/helper-plugin-utils": "^7.13.0" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.13.tgz", - "integrity": "sha512-XyJmZidNfofEkqFV5VC/bLabGmO5QzenPO/YOfGuEbgU+2sSwMmio3YLb4WtBgcmmdwZHyVyv8on77IUjQ5Gvg==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.12.13", - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.13.tgz", - "integrity": "sha512-A81F9pDwyS7yM//KwbCSDqy3Uj4NMIurtplxphWxoYtNPov7cJsDkAFNNyVlIZ3jwGycVsurZ+LtOA8gZ376iQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz", - "integrity": "sha512-96lgJagobeVmazXFaDrbmCLQxBysKu7U6Do3mLsx27gf5Dk85ezysrs2BZUpXD703U/Su1xTBDxxar2oa4jAGg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.13.0.tgz", - "integrity": "sha512-3j6E004Dx0K3eGmhxVJxwwI89CTJrce7lg3UrtFuDAVQ/2+SJ/h/aSFOeE6/n0WB1GsOffsJp6MnPQNQ8nmwhg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/helper-remap-async-to-generator": "^7.13.0" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.13.tgz", - "integrity": "sha512-zNyFqbc3kI/fVpqwfqkg6RvBgFpC4J18aKKMmv7KdQ/1GgREapSJAykLMVNwfRGO3BtHj3YQZl8kxCXPcVMVeg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.13.tgz", - "integrity": "sha512-Pxwe0iqWJX4fOOM2kEZeUuAxHMWb9nK+9oh5d11bsLoB0xMg+mkDpt0eYuDZB7ETrY9bbcVlKUGTOGWy7BHsMQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.13.0.tgz", - "integrity": "sha512-9BtHCPUARyVH1oXGcSJD3YpsqRLROJx5ZNP6tN5vnk17N0SVf9WCtf8Nuh1CFmgByKKAIMstitKduoCmsaDK5g==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.12.13", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/helper-replace-supers": "^7.13.0", - "@babel/helper-split-export-declaration": "^7.12.13", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.13.0.tgz", - "integrity": "sha512-RRqTYTeZkZAz8WbieLTvKUEUxZlUTdmL5KGMyZj7FnMfLNKV4+r5549aORG/mgojRmFlQMJDUupwAMiF2Q7OUg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.13.0.tgz", - "integrity": "sha512-zym5em7tePoNT9s964c0/KU3JPPnuq7VhIxPRefJ4/s82cD+q1mgKfuGRDMCPL0HTyKz4dISuQlCusfgCJ86HA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.13.tgz", - "integrity": "sha512-foDrozE65ZFdUC2OfgeOCrEPTxdB3yjqxpXh8CH+ipd9CHd4s/iq81kcUpyH8ACGNEPdFqbtzfgzbT/ZGlbDeQ==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.12.13", - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.13.tgz", - "integrity": "sha512-NfADJiiHdhLBW3pulJlJI2NB0t4cci4WTZ8FtdIuNc2+8pslXdPtRRAEWqUY+m9kNOk2eRYbTAOipAxlrOcwwQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.13.tgz", - "integrity": "sha512-fbUelkM1apvqez/yYx1/oICVnGo2KM5s63mhGylrmXUxK/IAXSIf87QIxVfZldWf4QsOafY6vV3bX8aMHSvNrA==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.12.13", - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.13.0.tgz", - "integrity": "sha512-IHKT00mwUVYE0zzbkDgNRP6SRzvfGCYsOxIRz8KsiaaHCcT9BWIkO+H9QRJseHBLOGBZkHUdHiqj6r0POsdytg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.13.tgz", - "integrity": "sha512-6K7gZycG0cmIwwF7uMK/ZqeCikCGVBdyP2J5SKNCXO5EOHcqi+z7Jwf8AmyDNcBgxET8DrEtCt/mPKPyAzXyqQ==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.13.tgz", - "integrity": "sha512-FW+WPjSR7hiUxMcKqyNjP05tQ2kmBCdpEpZHY1ARm96tGQCCBvXKnpjILtDplUnJ/eHZ0lALLM+d2lMFSpYJrQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.13.tgz", - "integrity": "sha512-kxLkOsg8yir4YeEPHLuO2tXP9R/gTjpuTOjshqSpELUN3ZAg2jfDnKUvzzJxObun38sw3wm4Uu69sX/zA7iRvg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.13.0.tgz", - "integrity": "sha512-EKy/E2NHhY/6Vw5d1k3rgoobftcNUmp9fGjb9XZwQLtTctsRBOTRO7RHHxfIky1ogMN5BxN7p9uMA3SzPfotMQ==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.13.0", - "@babel/helper-plugin-utils": "^7.13.0", - "babel-plugin-dynamic-import-node": "^2.3.3" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.13.8.tgz", - "integrity": "sha512-9QiOx4MEGglfYZ4XOnU79OHr6vIWUakIj9b4mioN8eQIoEh+pf5p/zEB36JpDFWA12nNMiRf7bfoRvl9Rn79Bw==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.13.0", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/helper-simple-access": "^7.12.13", - "babel-plugin-dynamic-import-node": "^2.3.3" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz", - "integrity": "sha512-hwqctPYjhM6cWvVIlOIe27jCIBgHCsdH2xCJVAYQm7V5yTMoilbVMi9f6wKg0rpQAOn6ZG4AOyvCqFF/hUh6+A==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.13.0", - "@babel/helper-module-transforms": "^7.13.0", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/helper-validator-identifier": "^7.12.11", - "babel-plugin-dynamic-import-node": "^2.3.3" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.13.0.tgz", - "integrity": "sha512-D/ILzAh6uyvkWjKKyFE/W0FzWwasv6vPTSqPcjxFqn6QpX3u8DjRVliq4F2BamO2Wee/om06Vyy+vPkNrd4wxw==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.13.0", - "@babel/helper-plugin-utils": "^7.13.0" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.13.tgz", - "integrity": "sha512-Xsm8P2hr5hAxyYblrfACXpQKdQbx4m2df9/ZZSQ8MAhsadw06+jW7s9zsSw6he+mJZXRlVMyEnVktJo4zjk1WA==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.12.13" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.13.tgz", - "integrity": "sha512-/KY2hbLxrG5GTQ9zzZSc3xWiOy379pIETEhbtzwZcw9rvuaVV4Fqy7BYGYOWZnaoXIQYbbJ0ziXLa/sKcGCYEQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.13.tgz", - "integrity": "sha512-JzYIcj3XtYspZDV8j9ulnoMPZZnF/Cj0LUxPOjR89BdBVx+zYJI9MdMIlUZjbXDX+6YVeS6I3e8op+qQ3BYBoQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13", - "@babel/helper-replace-supers": "^7.12.13" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.13.0.tgz", - "integrity": "sha512-Jt8k/h/mIwE2JFEOb3lURoY5C85ETcYPnbuAJ96zRBzh1XHtQZfs62ChZ6EP22QlC8c7Xqr9q+e1SU5qttwwjw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.13.tgz", - "integrity": "sha512-nqVigwVan+lR+g8Fj8Exl0UQX2kymtjcWfMOYM1vTYEKujeyv2SkMgazf2qNcK7l4SDiKyTA/nHCPqL4e2zo1A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.13.tgz", - "integrity": "sha512-lxb2ZAvSLyJ2PEe47hoGWPmW22v7CtSl9jW8mingV4H2sEX/JOcrAj2nPuGWi56ERUm2bUpjKzONAuT6HCn2EA==", - "dev": true, - "requires": { - "regenerator-transform": "^0.14.2" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.13.tgz", - "integrity": "sha512-xhUPzDXxZN1QfiOy/I5tyye+TRz6lA7z6xaT4CLOjPRMVg1ldRf0LHw0TDBpYL4vG78556WuHdyO9oi5UmzZBg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.13.tgz", - "integrity": "sha512-xpL49pqPnLtf0tVluuqvzWIgLEhuPpZzvs2yabUHSKRNlN7ScYU7aMlmavOeyXJZKgZKQRBlh8rHbKiJDraTSw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.13.0.tgz", - "integrity": "sha512-V6vkiXijjzYeFmQTr3dBxPtZYLPcUfY34DebOU27jIl2M/Y8Egm52Hw82CSjjPqd54GTlJs5x+CR7HeNr24ckg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.13.tgz", - "integrity": "sha512-Jc3JSaaWT8+fr7GRvQP02fKDsYk4K/lYwWq38r/UGfaxo89ajud321NH28KRQ7xy1Ybc0VUE5Pz8psjNNDUglg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.13.0.tgz", - "integrity": "sha512-d67umW6nlfmr1iehCcBv69eSUSySk1EsIS8aTDX4Xo9qajAh6mYtcl4kJrBkGXuxZPEgVr7RVfAvNW6YQkd4Mw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.13.tgz", - "integrity": "sha512-eKv/LmUJpMnu4npgfvs3LiHhJua5fo/CysENxa45YCQXZwKnGCQKAg87bvoqSW1fFT+HA32l03Qxsm8ouTY3ZQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.13.tgz", - "integrity": "sha512-0bHEkdwJ/sN/ikBHfSmOXPypN/beiGqjo+o4/5K+vxEFNPRPdImhviPakMKG4x96l85emoa0Z6cDflsdBusZbw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.13.tgz", - "integrity": "sha512-mDRzSNY7/zopwisPZ5kM9XKCfhchqIYwAKRERtEnhYscZB79VRekuRSoYbN0+KVe3y8+q1h6A4svXtP7N+UoCA==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.12.13", - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/preset-env": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.13.12.tgz", - "integrity": "sha512-JzElc6jk3Ko6zuZgBtjOd01pf9yYDEIH8BcqVuYIuOkzOwDesoa/Nz4gIo4lBG6K861KTV9TvIgmFuT6ytOaAA==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.13.12", - "@babel/helper-compilation-targets": "^7.13.10", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/helper-validator-option": "^7.12.17", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.13.12", - "@babel/plugin-proposal-async-generator-functions": "^7.13.8", - "@babel/plugin-proposal-class-properties": "^7.13.0", - "@babel/plugin-proposal-dynamic-import": "^7.13.8", - "@babel/plugin-proposal-export-namespace-from": "^7.12.13", - "@babel/plugin-proposal-json-strings": "^7.13.8", - "@babel/plugin-proposal-logical-assignment-operators": "^7.13.8", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", - "@babel/plugin-proposal-numeric-separator": "^7.12.13", - "@babel/plugin-proposal-object-rest-spread": "^7.13.8", - "@babel/plugin-proposal-optional-catch-binding": "^7.13.8", - "@babel/plugin-proposal-optional-chaining": "^7.13.12", - "@babel/plugin-proposal-private-methods": "^7.13.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.12.13", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.12.13", - "@babel/plugin-transform-arrow-functions": "^7.13.0", - "@babel/plugin-transform-async-to-generator": "^7.13.0", - "@babel/plugin-transform-block-scoped-functions": "^7.12.13", - "@babel/plugin-transform-block-scoping": "^7.12.13", - "@babel/plugin-transform-classes": "^7.13.0", - "@babel/plugin-transform-computed-properties": "^7.13.0", - "@babel/plugin-transform-destructuring": "^7.13.0", - "@babel/plugin-transform-dotall-regex": "^7.12.13", - "@babel/plugin-transform-duplicate-keys": "^7.12.13", - "@babel/plugin-transform-exponentiation-operator": "^7.12.13", - "@babel/plugin-transform-for-of": "^7.13.0", - "@babel/plugin-transform-function-name": "^7.12.13", - "@babel/plugin-transform-literals": "^7.12.13", - "@babel/plugin-transform-member-expression-literals": "^7.12.13", - "@babel/plugin-transform-modules-amd": "^7.13.0", - "@babel/plugin-transform-modules-commonjs": "^7.13.8", - "@babel/plugin-transform-modules-systemjs": "^7.13.8", - "@babel/plugin-transform-modules-umd": "^7.13.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.13", - "@babel/plugin-transform-new-target": "^7.12.13", - "@babel/plugin-transform-object-super": "^7.12.13", - "@babel/plugin-transform-parameters": "^7.13.0", - "@babel/plugin-transform-property-literals": "^7.12.13", - "@babel/plugin-transform-regenerator": "^7.12.13", - "@babel/plugin-transform-reserved-words": "^7.12.13", - "@babel/plugin-transform-shorthand-properties": "^7.12.13", - "@babel/plugin-transform-spread": "^7.13.0", - "@babel/plugin-transform-sticky-regex": "^7.12.13", - "@babel/plugin-transform-template-literals": "^7.13.0", - "@babel/plugin-transform-typeof-symbol": "^7.12.13", - "@babel/plugin-transform-unicode-escapes": "^7.12.13", - "@babel/plugin-transform-unicode-regex": "^7.12.13", - "@babel/preset-modules": "^0.1.4", - "@babel/types": "^7.13.12", - "babel-plugin-polyfill-corejs2": "^0.1.4", - "babel-plugin-polyfill-corejs3": "^0.1.3", - "babel-plugin-polyfill-regenerator": "^0.1.2", - "core-js-compat": "^3.9.0", - "semver": "^6.3.0" - }, - "dependencies": { - "babel-plugin-polyfill-regenerator": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.1.6.tgz", - "integrity": "sha512-OUrYG9iKPKz8NxswXbRAdSwF0GhRdIEMTloQATJi4bDuFqrXaXcCUT/VGNrr8pBcjMh1RxZ7Xt9cytVJTJfvMg==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.1.5" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/preset-modules": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", - "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - } - }, - "@babel/runtime": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", - "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/runtime-corejs3": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.13.10.tgz", - "integrity": "sha512-x/XYVQ1h684pp1mJwOV4CyvqZXqbc8CMsMGUnAbuc82ZCdv1U63w5RSUzgDSXQHG5Rps/kiksH6g2D5BuaKyXg==", - "dev": true, - "requires": { - "core-js-pure": "^3.0.0", - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" - } - }, - "@babel/traverse": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.13.tgz", - "integrity": "sha512-CblEcwmXKR6eP43oQGG++0QMTtCjAsa3frUuzHoiIJWpaIIi8dwMyEFUJoXRLxagGqCK+jALRwIO+o3R9p/uUg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.13.9", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.13.13", - "@babel/types": "^7.13.13", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", - "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "@cnakazawa/watch": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", - "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", - "dev": true, - "requires": { - "exec-sh": "^0.3.2", - "minimist": "^1.2.0" - } - }, - "@dabh/diagnostics": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", - "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==", - "requires": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "@hathor/wallet-lib": { - "version": "0.20.3", - "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-0.20.3.tgz", - "integrity": "sha512-V6ikQfu5Hu8Zg5aeEBs92s/T1eo/pc23aZE7WTL+emSVNb+Q0bzOR/4ey4gMY+hLw3Gm0KUBmu+kg3oxUN8taw==", - "requires": { - "axios": "^0.18.0", - "bitcore-lib": "^8.25.10", - "bitcore-mnemonic": "^8.25.10", - "crypto-js": "^3.1.9-1", - "isomorphic-ws": "^4.0.1", - "lodash": "^4.17.11", - "long": "^4.0.0", - "ws": "^7.2.1" - }, - "dependencies": { - "axios": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", - "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", - "requires": { - "follow-redirects": "1.5.10", - "is-buffer": "^2.0.2" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - } - }, - "is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" - } - } - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jest/console": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.5.0.tgz", - "integrity": "sha512-T48kZa6MK1Y6k4b89sexwmSF4YLeZS/Udqg3Jj3jG/cHH+N/sLFCEoXEDMOKugJQ9FxPN1osxIknvKkxt6MKyw==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "jest-message-util": "^25.5.0", - "jest-util": "^25.5.0", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/core": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.5.4.tgz", - "integrity": "sha512-3uSo7laYxF00Dg/DMgbn4xMJKmDdWvZnf89n8Xj/5/AeQ2dOQmn6b6Hkj/MleyzZWXpwv+WSdYWl4cLsy2JsoA==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/reporters": "^25.5.1", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-changed-files": "^25.5.0", - "jest-config": "^25.5.4", - "jest-haste-map": "^25.5.1", - "jest-message-util": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-resolve-dependencies": "^25.5.4", - "jest-runner": "^25.5.4", - "jest-runtime": "^25.5.4", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "jest-watcher": "^25.5.0", - "micromatch": "^4.0.2", - "p-each-series": "^2.1.0", - "realpath-native": "^2.0.0", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "@jest/environment": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.5.0.tgz", - "integrity": "sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA==", - "dev": true, - "requires": { - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0" - } - }, - "@jest/fake-timers": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz", - "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==", - "dev": true, - "requires": { - "@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" - } - }, - "@jest/globals": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-25.5.2.tgz", - "integrity": "sha512-AgAS/Ny7Q2RCIj5kZ+0MuKM1wbF0WMLxbCVl/GOMoCNbODRdJ541IxJ98xnZdVSZXivKpJlNPIWa3QmY0l4CXA==", - "dev": true, - "requires": { - "@jest/environment": "^25.5.0", - "@jest/types": "^25.5.0", - "expect": "^25.5.0" - } - }, - "@jest/reporters": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.5.1.tgz", - "integrity": "sha512-3jbd8pPDTuhYJ7vqiHXbSwTJQNavczPs+f1kRprRDxETeE3u6srJ+f0NPuwvOmk+lmunZzPkYWIFZDLHQPkviw==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.4", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "jest-haste-map": "^25.5.1", - "jest-resolve": "^25.5.1", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "node-notifier": "^6.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^3.1.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^4.1.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/source-map": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.5.0.tgz", - "integrity": "sha512-eIGx0xN12yVpMcPaVpjXPnn3N30QGJCJQSkEDUt9x1fI1Gdvb07Ml6K5iN2hG7NmMP6FDmtPEssE3z6doOYUwQ==", - "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", - "source-map": "^0.6.0" - }, - "dependencies": { - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - } - } - }, - "@jest/test-result": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.5.0.tgz", - "integrity": "sha512-oV+hPJgXN7IQf/fHWkcS99y0smKLU2czLBJ9WA0jHITLst58HpQMtzSYxzaBvYc6U5U6jfoMthqsUlUlbRXs0A==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/types": "^25.5.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.5.4.tgz", - "integrity": "sha512-pTJGEkSeg1EkCO2YWq6hbFvKNXk8ejqlxiOg1jBNLnWrgXOkdY6UmqZpwGFXNnRt9B8nO1uWMzLLZ4eCmhkPNA==", - "dev": true, - "requires": { - "@jest/test-result": "^25.5.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^25.5.1", - "jest-runner": "^25.5.4", - "jest-runtime": "^25.5.4" - } - }, - "@jest/transform": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.5.1.tgz", - "integrity": "sha512-Y8CEoVwXb4QwA6Y/9uDkn0Xfz0finGkieuV0xkdF9UtZGJeLukD5nLkaVrVsODB1ojRWlaoD0AJZpVHCSnJEvg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^25.5.0", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^3.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^25.5.1", - "jest-regex-util": "^25.2.6", - "jest-util": "^25.5.0", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "@jest/types": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", - "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", - "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.4", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", - "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", - "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.4", - "fastq": "^1.6.0" - } - }, - "@polka/url": { - "version": "1.0.0-next.12", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz", - "integrity": "sha512-6RglhutqrGFMO1MNUXp95RBuYIuc8wTnMAV5MUhLmjTOy78ncwOw7RgeQ/HeymkKXRhZd0s2DNrM1rL7unk3MQ==", - "dev": true - }, - "@rollup/plugin-babel": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz", - "integrity": "sha512-9uIC8HZOnVLrLHxayq/PTzw+uS25E14KPUBh5ktF+18Mjo5yK0ToMMx6epY0uEgkjwJw0aBW4x2horYXh8juWw==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.10.4", - "@rollup/pluginutils": "^3.1.0" - } - }, - "@rollup/plugin-commonjs": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.1.0.tgz", - "integrity": "sha512-Ycr12N3ZPN96Fw2STurD21jMqzKwL9QuFhms3SD7KKRK7oaXUsBU9Zt0jL/rOPHiPYisI21/rXGO3jr9BnLHUA==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^3.0.8", - "commondir": "^1.0.1", - "estree-walker": "^1.0.1", - "glob": "^7.1.2", - "is-reference": "^1.1.2", - "magic-string": "^0.25.2", - "resolve": "^1.11.0" - } - }, - "@rollup/plugin-json": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz", - "integrity": "sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^3.0.8" - } - }, - "@rollup/plugin-node-resolve": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-9.0.0.tgz", - "integrity": "sha512-gPz+utFHLRrd41WMP13Jq5mqqzHL3OXrfj3/MkSyB6UBIcuNt9j60GCbarzMzdf1VHFpOxfQh/ez7wyadLMqkg==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "builtin-modules": "^3.1.0", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.17.0" - } - }, - "@rollup/plugin-replace": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", - "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^3.1.0", - "magic-string": "^0.25.7" - } - }, - "@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "dev": true, - "requires": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" - } - }, - "@sinonjs/commons": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz", - "integrity": "sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@size-limit/file": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/@size-limit/file/-/file-4.10.2.tgz", - "integrity": "sha512-IrmEzZitNMTyGcbvIN5bMN6u8A5x8M1YVjfJnEiO3mukMtszGK2yOqVYltyyvB0Qm0Wvqcm4qXAxxRASXtDwVg==", - "dev": true, - "requires": { - "semver": "7.3.5" - } - }, - "@size-limit/preset-small-lib": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/@size-limit/preset-small-lib/-/preset-small-lib-4.10.2.tgz", - "integrity": "sha512-TjnxyhwLbazXXMUPYqfta+l0lFKhdhg3GJ92rdxioiO1syS8dMbrvi8VBb9b7CJkfjnAf3gI4kmQxALwfhbCiA==", - "dev": true, - "requires": { - "@size-limit/file": "4.10.2", - "@size-limit/webpack": "4.10.2" - } - }, - "@size-limit/webpack": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/@size-limit/webpack/-/webpack-4.10.2.tgz", - "integrity": "sha512-ZWGQk4RO8XGOQmYVWiOj5tTsltb7O4f2FEr5iULURbaOuziMItDk6fR1Bs8mXFawrb4s1lKSIGzBxi4uf+TjTQ==", - "dev": true, - "requires": { - "css-loader": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "file-loader": "^6.2.0", - "mkdirp": "^1.0.4", - "nanoid": "^3.1.22", - "optimize-css-assets-webpack-plugin": "^5.0.4", - "pnp-webpack-plugin": "^1.6.4", - "rimraf": "^3.0.2", - "style-loader": "^2.0.0", - "webpack": "^4.44.1", - "webpack-bundle-analyzer": "^4.4.0" - } - }, - "@types/babel__core": { - "version": "7.1.14", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz", - "integrity": "sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", - "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", - "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.1.tgz", - "integrity": "sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw==", - "dev": true, - "requires": { - "@babel/types": "^7.3.0" - } - }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, - "@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true - }, - "@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", - "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*", - "@types/istanbul-lib-report": "*" - } - }, - "@types/jest": { - "version": "25.2.3", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.2.3.tgz", - "integrity": "sha512-JXc1nK/tXHiDhV55dvfzqtmP4S3sy3T3ouV2tkViZgxY/zeUkcpQcQPGRlgF4KmWzWW5oiWYSZwtCB+2RsE4Fw==", - "dev": true, - "requires": { - "jest-diff": "^25.2.1", - "pretty-format": "^25.2.1" - } - }, - "@types/json-schema": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", - "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", - "dev": true - }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, - "@types/lodash": { - "version": "4.14.172", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", - "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==", - "dev": true - }, - "@types/node": { - "version": "17.0.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.21.tgz", - "integrity": "sha1-hkuYfAxo0HtDRYRcPmO3Xt0UNkQ=", - "dev": true - }, - "@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", - "dev": true - }, - "@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, - "@types/prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", - "dev": true - }, - "@types/q": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", - "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", - "dev": true - }, - "@types/resolve": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", - "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", - "dev": true - }, - "@types/yargs": { - "version": "15.0.13", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", - "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "20.2.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", - "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", - "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "2.34.0", - "functional-red-black-tree": "^1.0.1", - "regexpp": "^3.0.0", - "tsutils": "^3.17.1" - } - }, - "@typescript-eslint/experimental-utils": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", - "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.34.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - } - } - }, - "@typescript-eslint/parser": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", - "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", - "dev": true, - "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.34.0", - "@typescript-eslint/typescript-estree": "2.34.0", - "eslint-visitor-keys": "^1.1.0" - } - }, - "@typescript-eslint/typescript-estree": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", - "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@webassemblyjs/ast": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", - "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", - "dev": true, - "requires": { - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", - "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", - "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", - "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", - "dev": true - }, - "@webassemblyjs/helper-code-frame": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", - "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", - "dev": true, - "requires": { - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "@webassemblyjs/helper-fsm": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", - "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", - "dev": true - }, - "@webassemblyjs/helper-module-context": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", - "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", - "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", - "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", - "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", - "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", - "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/helper-wasm-section": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-opt": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", - "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", - "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", - "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "@webassemblyjs/wast-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", - "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/floating-point-hex-parser": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-code-frame": "1.9.0", - "@webassemblyjs/helper-fsm": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", - "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", - "dev": true - }, - "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "dev": true - }, - "acorn-globals": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", - "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", - "dev": true, - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn-walk": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", - "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", - "dev": true - } - } - }, - "acorn-jsx": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true - }, - "acorn-walk": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.0.2.tgz", - "integrity": "sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==", - "dev": true - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true - }, - "alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", - "dev": true - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - }, - "dependencies": { - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - } - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.10.2", - "@babel/runtime-corejs3": "^7.10.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-includes": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz", - "integrity": "sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.5" - } - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "array.prototype.flat": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz", - "integrity": "sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - } - }, - "array.prototype.flatmap": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz", - "integrity": "sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1", - "function-bind": "^1.1.1" - } - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "asyncro": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/asyncro/-/asyncro-3.0.0.tgz", - "integrity": "sha512-nEnWYfrBmA3taTiuiOoZYmgJ/CNrSoQLeLs29SeLcPu60yaw/mHDBHV0iOZ051fTvsTHxpCY+gXibqT9wbQYfg==", - "dev": true - }, - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sdk": { - "version": "2.878.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.878.0.tgz", - "integrity": "sha512-Mi6ASjd6UugdIskkXqTBgn6tC5I61BlEPwKXYtC6FFrNyvxMF+USH7FHD0O3ZBwqDCSI+BR9a296ReRNzQ//tw==", - "requires": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.15.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "uuid": "3.3.2", - "xml2js": "0.4.19" - }, - "dependencies": { - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - }, - "sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" - }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - } - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "dev": true - }, - "axe-core": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.3.tgz", - "integrity": "sha512-vwPpH4Aj4122EW38mxO/fxhGKtwWTMLDIJfZ1He0Edbtjcfna/R3YB67yVhezUMzqc3Jr3+Ii50KRntlENL4xQ==", - "dev": true - }, - "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", - "requires": { - "follow-redirects": "^1.10.0" - } - }, - "axobject-query": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", - "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", - "dev": true - }, - "babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - } - }, - "babel-jest": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.5.1.tgz", - "integrity": "sha512-9dA9+GmMjIzgPnYtkhBg73gOo/RHqPmLruP3BaGL4KEX3Dwz6pI8auSN8G8+iuEG90+GSswyKvslN+JYSaacaQ==", - "dev": true, - "requires": { - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "@types/babel__core": "^7.1.7", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "babel-plugin-annotate-pure-calls": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/babel-plugin-annotate-pure-calls/-/babel-plugin-annotate-pure-calls-0.4.0.tgz", - "integrity": "sha512-oi4M/PWUJOU9ZyRGoPTfPMqdyMp06jbJAomd3RcyYuzUtBOddv98BqLm96Lucpi2QFoQHkdGQt0ACvw7VzVEQA==", - "dev": true - }, - "babel-plugin-dev-expression": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/babel-plugin-dev-expression/-/babel-plugin-dev-expression-0.2.2.tgz", - "integrity": "sha512-y32lfBif+c2FIh5dwGfcc/IfX5aw/Bru7Du7W2n17sJE/GJGAsmIk5DPW/8JOoeKpXW5evJfJOvRq5xkiS6vng==", - "dev": true - }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.5.0.tgz", - "integrity": "sha512-u+/W+WAjMlvoocYGTwthAiQSxDcJAyHpQ6oWlHdFZaaN+Rlk8Q7iiwDPg2lN/FyJtAYnKjFxbn7xus4HCFkg5g==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-plugin-macros": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", - "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.7.2", - "cosmiconfig": "^6.0.0", - "resolve": "^1.12.0" - }, - "dependencies": { - "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - } - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - } - } - }, - "babel-plugin-polyfill-corejs2": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.1.10.tgz", - "integrity": "sha512-DO95wD4g0A8KRaHKi0D51NdGXzvpqVLnLu5BTvDlpqUEpTmeEtypgC1xqesORaWmiUOQI14UHKlzNd9iZ2G3ZA==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.13.0", - "@babel/helper-define-polyfill-provider": "^0.1.5", - "semver": "^6.1.1" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.1.7.tgz", - "integrity": "sha512-u+gbS9bbPhZWEeyy1oR/YaaSpod/KDT07arZHb80aTpl8H5ZBq+uN1nN9/xtX7jQyfLdPfoqI4Rue/MQSWJquw==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.1.5", - "core-js-compat": "^3.8.1" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.0.4.tgz", - "integrity": "sha512-+/uCzO9JTYVZVGCpZpVAQkgPGt2zkR0VYiZvJ4aVoCe4ccgpKvNQqcjzAgQzSsjK64Jhc5hvrCR3l0087BevkA==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.0.3" - }, - "dependencies": { - "@babel/helper-define-polyfill-provider": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.0.3.tgz", - "integrity": "sha512-dULDd/APiP4JowYDAMosecKOi/1v+UId99qhBGiO3myM29KtAVKS/R3x3OJJNBR0FeYB1BcYb2dCwkhqvxWXXQ==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.10.4", - "@babel/helper-module-imports": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/traverse": "^7.11.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "babel-plugin-transform-rename-import": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-rename-import/-/babel-plugin-transform-rename-import-2.3.0.tgz", - "integrity": "sha512-dPgJoT57XC0PqSnLgl2FwNvxFrWlspatX2dkk7yjKQj5HHGw071vAcOf+hqW8ClqcBDMvEbm6mevn5yHAD8mlQ==", - "dev": true - }, - "babel-preset-current-node-syntax": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.4.tgz", - "integrity": "sha512-5/INNCYhUGqw7VbVjT/hb3ucjgkVHKXY7lX3ZjlN4gm565VyFmJUrJ/h+h16ECVB38R/9SF6aACydpKMLZ/c9w==", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.5.0.tgz", - "integrity": "sha512-8ZczygctQkBU+63DtSOKGh7tFL0CeCuz+1ieud9lJ1WPQ9O6A1a/r+LGn6Y705PA6whHQ3T1XuB/PmpfNYf8Fw==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^25.5.0", - "babel-preset-current-node-syntax": "^0.1.2" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base-x": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz", - "integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "bech32": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.3.tgz", - "integrity": "sha512-yuVFUvrNcoJi0sv5phmqc6P+Fl1HjRDRNOOkHY2X/3LBy2bIGNSFx4fZ95HMaXHupuS7cZR15AsvtmCIF4UEyg==" - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bitcore-lib": { - "version": "8.25.10", - "resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.25.10.tgz", - "integrity": "sha512-MyHpSg7aFRHe359RA/gdkaQAal3NswYZTLEuu0tGX1RGWXAYN9i/24fsjPqVKj+z0ua+gzAT7aQs0KiKXWCgKA==", - "requires": { - "bech32": "=1.1.3", - "bn.js": "=4.11.8", - "bs58": "^4.0.1", - "buffer-compare": "=1.1.1", - "elliptic": "^6.5.3", - "inherits": "=2.0.1", - "lodash": "^4.17.20" - }, - "dependencies": { - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" - }, - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" - } - } - }, - "bitcore-mnemonic": { - "version": "8.25.10", - "resolved": "https://registry.npmjs.org/bitcore-mnemonic/-/bitcore-mnemonic-8.25.10.tgz", - "integrity": "sha512-FeXxO37BLV5JRvxPmVFB91zRHalavV8H4TdQGt1/hz0AkoPymIV68OkuB+TptpjeYgatcgKPoPvPhglJkTzFQQ==", - "requires": { - "bitcore-lib": "^8.25.10", - "unorm": "^1.4.1" - } - }, - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "dev": true, - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, - "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "requires": { - "pako": "~1.0.5" - } - }, - "browserslist": { - "version": "4.16.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", - "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001219", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.723", - "escalade": "^3.1.1", - "node-releases": "^1.1.71" - } - }, - "bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "requires": { - "fast-json-stable-stringify": "2.x" - } - }, - "bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", - "requires": { - "base-x": "^3.0.2" - } - }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "buffer-compare": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-compare/-/buffer-compare-1.1.1.tgz", - "integrity": "sha1-W+e+hTr4kZjR9N3AkNHWakiu9ZY=" - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "bufferutil": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz", - "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==", - "requires": { - "node-gyp-build": "^4.2.0" - } - }, - "builtin-modules": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", - "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", - "dev": true - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "bytes-iec": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/bytes-iec/-/bytes-iec-3.1.1.tgz", - "integrity": "sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA==", - "dev": true - }, - "cacache": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", - "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, - "requires": { - "callsites": "^2.0.0" - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - }, - "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", - "dev": true - }, - "caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001319", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001319.tgz", - "integrity": "sha512-xjlIAFHucBRSMUo1kb5D4LYgcN1M45qdKP++lhqowDpwJwGkpIRTt5qQqnhxjj1vHcI7nrJxWhCC1ATrCEBTcw==", - "dev": true - }, - "capture-exit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", - "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", - "dev": true, - "requires": { - "rsvp": "^4.8.4" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "chokidar": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", - "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.3.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "ci-job-number": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/ci-job-number/-/ci-job-number-1.2.2.tgz", - "integrity": "sha512-CLOGsVDrVamzv8sXJGaILUVI6dsuAkouJP/n6t+OxLPeeA4DDby7zn9SB6EUpa1H7oIKoE+rMmkW80zYsFfUjA==", - "dev": true - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-spinners": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz", - "integrity": "sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q==", - "dev": true - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dev": true, - "requires": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - } - }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", - "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", - "dev": true, - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.4" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "color-string": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", - "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "colorette": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", - "dev": true - }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" - }, - "colorspace": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", - "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", - "requires": { - "color": "3.0.x", - "text-hex": "1.0.x" - }, - "dependencies": { - "color": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", - "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - } - } - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "confusing-browser-globals": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz", - "integrity": "sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA==", - "dev": true - }, - "console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", - "dev": true - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-js-compat": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.10.0.tgz", - "integrity": "sha512-9yVewub2MXNYyGvuLnMHcN1k9RkvB7/ofktpeKTIaASyB88YYqGzUnu0ywMMhJrDHOMiTjSHWGzR+i7Wb9Z1kQ==", - "dev": true, - "requires": { - "browserslist": "^4.16.3", - "semver": "7.0.0" - }, - "dependencies": { - "semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true - } - } - }, - "core-js-pure": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.10.0.tgz", - "integrity": "sha512-CC582enhrFZStO4F8lGI7QL3SYx7/AIRc+IdSi3btrQGrVsTawo5K/crmKbRrQ+MOMhNX4v+PATn0k2NN6wI7A==", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - } - }, - "create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "crypto-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", - "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" - }, - "css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", - "dev": true - }, - "css-declaration-sorter": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", - "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", - "dev": true, - "requires": { - "postcss": "^7.0.1", - "timsort": "^0.3.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "css-loader": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.0.tgz", - "integrity": "sha512-MfRo2MjEeLXMlUkeUwN71Vx5oc6EJnx5UQ4Yi9iUtYQvrPtwLUucYptz0hc6n++kdNcyF5olYBS4vPjJDAcLkw==", - "dev": true, - "requires": { - "camelcase": "^6.2.0", - "cssesc": "^3.0.0", - "icss-utils": "^5.1.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.8", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^3.0.0", - "semver": "^7.3.4" - } - }, - "css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true - }, - "css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", - "dev": true, - "requires": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" - } - }, - "css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "dev": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "cssnano": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", - "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", - "dev": true, - "requires": { - "cosmiconfig": "^5.0.0", - "cssnano-preset-default": "^4.0.7", - "is-resolvable": "^1.0.0", - "postcss": "^7.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "cssnano-preset-default": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", - "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", - "dev": true, - "requires": { - "css-declaration-sorter": "^4.0.1", - "cssnano-util-raw-cache": "^4.0.1", - "postcss": "^7.0.0", - "postcss-calc": "^7.0.1", - "postcss-colormin": "^4.0.3", - "postcss-convert-values": "^4.0.1", - "postcss-discard-comments": "^4.0.2", - "postcss-discard-duplicates": "^4.0.2", - "postcss-discard-empty": "^4.0.1", - "postcss-discard-overridden": "^4.0.1", - "postcss-merge-longhand": "^4.0.11", - "postcss-merge-rules": "^4.0.3", - "postcss-minify-font-values": "^4.0.2", - "postcss-minify-gradients": "^4.0.2", - "postcss-minify-params": "^4.0.2", - "postcss-minify-selectors": "^4.0.2", - "postcss-normalize-charset": "^4.0.1", - "postcss-normalize-display-values": "^4.0.2", - "postcss-normalize-positions": "^4.0.2", - "postcss-normalize-repeat-style": "^4.0.2", - "postcss-normalize-string": "^4.0.2", - "postcss-normalize-timing-functions": "^4.0.2", - "postcss-normalize-unicode": "^4.0.1", - "postcss-normalize-url": "^4.0.1", - "postcss-normalize-whitespace": "^4.0.2", - "postcss-ordered-values": "^4.1.2", - "postcss-reduce-initial": "^4.0.3", - "postcss-reduce-transforms": "^4.0.2", - "postcss-svgo": "^4.0.2", - "postcss-unique-selectors": "^4.0.1" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "cssnano-util-get-arguments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", - "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", - "dev": true - }, - "cssnano-util-get-match": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", - "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", - "dev": true - }, - "cssnano-util-raw-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", - "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "cssnano-util-same-parent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", - "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", - "dev": true - }, - "csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "requires": { - "css-tree": "^1.1.2" - }, - "dependencies": { - "css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "requires": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - } - }, - "mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - } - } - }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, - "cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", - "dev": true - }, - "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "damerau-levenshtein": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", - "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, - "defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "dev": true, - "requires": { - "clone": "^1.0.2" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - }, - "diff-sequences": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", - "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", - "dev": true - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true - } - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true - }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" - } - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "dotenv": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" - }, - "duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "electron-to-chromium": { - "version": "1.3.782", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.782.tgz", - "integrity": "sha512-6AI2se1NqWA1SBf/tlD6tQD/6ZOt+yAhqmrTlh4XZw4/g0Mt3p6JhTQPZxRPxPZiOg0o7ss1EBP/CpYejfnoIA==", - "dev": true - }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true - }, - "enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "enhanced-resolve": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", - "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - }, - "dependencies": { - "memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - } - } - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, - "entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true - }, - "errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", - "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.2", - "is-string": "^1.0.5", - "object-inspect": "^1.9.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.0" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "eslint-config-prettier": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", - "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", - "dev": true, - "requires": { - "get-stdin": "^6.0.0" - } - }, - "eslint-config-react-app": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-5.2.1.tgz", - "integrity": "sha512-pGIZ8t0mFLcV+6ZirRgYK6RVqUIKRIi9MmgzUEmrIknsn3AdO0I32asO86dJgloHq+9ZPl8UIg8mYrvgP5u2wQ==", - "dev": true, - "requires": { - "confusing-browser-globals": "^1.0.9" - } - }, - "eslint-import-resolver-node": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", - "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", - "dev": true, - "requires": { - "debug": "^2.6.9", - "resolve": "^1.13.1" - } - }, - "eslint-module-utils": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", - "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", - "dev": true, - "requires": { - "debug": "^2.6.9", - "pkg-dir": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - } - } - }, - "eslint-plugin-flowtype": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-3.13.0.tgz", - "integrity": "sha512-bhewp36P+t7cEV0b6OdmoRWJCBYRiHFlqPZAG1oS3SF+Y0LQkeDvFSM4oxoxvczD1OdONCXMlJfQFiWLcV9urw==", - "dev": true, - "requires": { - "lodash": "^4.17.15" - } - }, - "eslint-plugin-import": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", - "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", - "dev": true, - "requires": { - "array-includes": "^3.1.1", - "array.prototype.flat": "^1.2.3", - "contains-path": "^0.1.0", - "debug": "^2.6.9", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.4", - "eslint-module-utils": "^2.6.0", - "has": "^1.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.1", - "read-pkg-up": "^2.0.0", - "resolve": "^1.17.0", - "tsconfig-paths": "^3.9.0" - }, - "dependencies": { - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "dev": true, - "requires": { - "pify": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "dev": true, - "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - } - } - } - }, - "eslint-plugin-jsx-a11y": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz", - "integrity": "sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.11.2", - "aria-query": "^4.2.2", - "array-includes": "^3.1.1", - "ast-types-flow": "^0.0.7", - "axe-core": "^4.0.2", - "axobject-query": "^2.2.0", - "damerau-levenshtein": "^1.0.6", - "emoji-regex": "^9.0.0", - "has": "^1.0.3", - "jsx-ast-utils": "^3.1.0", - "language-tags": "^1.0.5" - }, - "dependencies": { - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - } - } - }, - "eslint-plugin-prettier": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz", - "integrity": "sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-plugin-react": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.23.1.tgz", - "integrity": "sha512-MvFGhZjI8Z4HusajmSw0ougGrq3Gs4vT/0WgwksZgf5RrLrRa2oYAw56okU4tZJl8+j7IYNuTM+2RnFEuTSdRQ==", - "dev": true, - "requires": { - "array-includes": "^3.1.3", - "array.prototype.flatmap": "^1.2.4", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.0.4", - "object.entries": "^1.1.3", - "object.fromentries": "^2.0.4", - "object.values": "^1.1.3", - "prop-types": "^15.7.2", - "resolve": "^2.0.0-next.3", - "string.prototype.matchall": "^4.0.4" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "resolve": { - "version": "2.0.0-next.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", - "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - } - } - }, - "eslint-plugin-react-hooks": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.1.tgz", - "integrity": "sha512-Y2c4b55R+6ZzwtTppKwSmK/Kar8AdLiC2f9NADCuxbcTgPPg41Gyqa6b9GppgXSvCtkRw43ZE86CT5sejKC6/g==", - "dev": true - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - }, - "espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", - "dev": true, - "requires": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "exec-sh": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", - "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==", - "dev": true - }, - "execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expect": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz", - "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "ansi-styles": "^4.0.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-regex-util": "^25.2.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, - "ext": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", - "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", - "requires": { - "type": "^2.0.0" - }, - "dependencies": { - "type": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", - "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==" - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "fast-glob": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", - "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fast-safe-stringify": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", - "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" - }, - "fastq": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", - "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, - "fecha": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz", - "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==" - }, - "figgy-pudding": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", - "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", - "dev": true - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - }, - "dependencies": { - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - } - } - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "requires": { - "flat-cache": "^2.0.1" - } - }, - "file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "dev": true, - "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - } - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" - }, - "follow-redirects": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", - "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "dev": true - }, - "globby": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", - "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true - }, - "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", - "dev": true - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true, - "optional": true - }, - "gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "requires": { - "duplexer": "^0.1.2" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "dev": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "hex-color-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", - "dev": true - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "hsl-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", - "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", - "dev": true - }, - "hsla-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", - "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", - "dev": true - }, - "html-comment-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", - "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", - "dev": true - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true - }, - "humanize-duration": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.25.1.tgz", - "integrity": "sha512-P+dRo48gpLgc2R9tMRgiDRNULPKCmqFYgguwqOO2C0fjO35TgdURDQDANSR1Nt92iHlbHGMxOTnsB8H8xnMa2Q==", - "dev": true - }, - "husky": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/husky/-/husky-6.0.0.tgz", - "integrity": "sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ==", - "dev": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", - "dev": true - }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - } - }, - "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - } - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", - "dev": true - }, - "infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "dev": true - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true - }, - "is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-bigint": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz", - "integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==", - "dev": true - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-boolean-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz", - "integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==", - "dev": true, - "requires": { - "call-bind": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", - "dev": true - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-color-stop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", - "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", - "dev": true, - "requires": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" - } - }, - "is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-docker": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", - "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", - "dev": true, - "optional": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true - }, - "is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", - "dev": true - }, - "is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-number-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", - "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", - "dev": true - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "requires": { - "@types/estree": "*" - } - }, - "is-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", - "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-symbols": "^1.0.1" - } - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" - }, - "is-string": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", - "dev": true - }, - "is-svg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", - "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", - "dev": true, - "requires": { - "html-comment-regex": "^1.1.0" - } - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isomorphic-ws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", - "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jest": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest/-/jest-25.5.4.tgz", - "integrity": "sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ==", - "dev": true, - "requires": { - "@jest/core": "^25.5.4", - "import-local": "^3.0.2", - "jest-cli": "^25.5.4" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "jest-cli": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.5.4.tgz", - "integrity": "sha512-rG8uJkIiOUpnREh1768/N3n27Cm+xPFkSNFO91tgg+8o2rXeVLStz+vkXkGr4UtzH6t1SNbjwoiswd7p4AhHTw==", - "dev": true, - "requires": { - "@jest/core": "^25.5.4", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "import-local": "^3.0.2", - "is-ci": "^2.0.0", - "jest-config": "^25.5.4", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "prompts": "^2.0.1", - "realpath-native": "^2.0.0", - "yargs": "^15.3.1" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-changed-files": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.5.0.tgz", - "integrity": "sha512-EOw9QEqapsDT7mKF162m8HFzRPbmP8qJQny6ldVOdOVBz3ACgPm/1nAn5fPQ/NDaYhX/AHkrGwwkCncpAVSXcw==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "execa": "^3.2.0", - "throat": "^5.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "execa": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", - "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "p-finally": "^2.0.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "jest-config": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.5.4.tgz", - "integrity": "sha512-SZwR91SwcdK6bz7Gco8qL7YY2sx8tFJYzvg216DLihTWf+LKY/DoJXpM9nTzYakSyfblbqeU48p/p7Jzy05Atg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^25.5.4", - "@jest/types": "^25.5.0", - "babel-jest": "^25.5.1", - "chalk": "^3.0.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.4", - "jest-environment-jsdom": "^25.5.0", - "jest-environment-node": "^25.5.0", - "jest-get-type": "^25.2.6", - "jest-jasmine2": "^25.5.4", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "micromatch": "^4.0.2", - "pretty-format": "^25.5.0", - "realpath-native": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "jest-diff": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", - "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-docblock": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.3.0.tgz", - "integrity": "sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.5.0.tgz", - "integrity": "sha512-QBogUxna3D8vtiItvn54xXde7+vuzqRrEeaw8r1s+1TG9eZLVJE5ZkKoSUlqFwRjnlaA4hyKGiu9OlkFIuKnjA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "jest-util": "^25.5.0", - "pretty-format": "^25.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-environment-jsdom": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.5.0.tgz", - "integrity": "sha512-7Jr02ydaq4jaWMZLY+Skn8wL5nVIYpWvmeatOHL3tOcV3Zw8sjnPpx+ZdeBfc457p8jCR9J6YCc+Lga0oIy62A==", - "dev": true, - "requires": { - "@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" - } - }, - "jest-environment-node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.5.0.tgz", - "integrity": "sha512-iuxK6rQR2En9EID+2k+IBs5fCFd919gVVK5BeND82fYeLWPqvRcFNPKu9+gxTwfB5XwBGBvZ0HFQa+cHtIoslA==", - "dev": true, - "requires": { - "@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", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "jest-get-type": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", - "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", - "dev": true - }, - "jest-haste-map": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.5.1.tgz", - "integrity": "sha512-dddgh9UZjV7SCDQUrQ+5t9yy8iEgKc1AKqZR9YDww8xsVOtzPQSMVLDChc21+g29oTRexb9/B0bIlZL+sWmvAQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "@types/graceful-fs": "^4.1.2", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.4", - "jest-serializer": "^25.5.0", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7", - "which": "^2.0.2" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "jest-jasmine2": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.5.4.tgz", - "integrity": "sha512-9acbWEfbmS8UpdcfqnDO+uBUgKa/9hcRh983IHdM+pKmJPL77G0sWAAK0V0kr5LK3a8cSBfkFSoncXwQlRZfkQ==", - "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^25.5.0", - "@jest/source-map": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "co": "^4.6.0", - "expect": "^25.5.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^25.5.0", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-runtime": "^25.5.4", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "pretty-format": "^25.5.0", - "throat": "^5.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-leak-detector": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.5.0.tgz", - "integrity": "sha512-rV7JdLsanS8OkdDpZtgBf61L5xZ4NnYLBq72r6ldxahJWWczZjXawRsoHyXzibM5ed7C2QRjpp6ypgwGdKyoVA==", - "dev": true, - "requires": { - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-matcher-utils": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", - "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-message-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", - "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", - "dev": true, - "requires": { - "@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" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "jest-mock": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz", - "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0" - } - }, - "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true - }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, - "jest-resolve": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.5.1.tgz", - "integrity": "sha512-Hc09hYch5aWdtejsUZhA+vSzcotf7fajSlPA6EZPE1RmPBAD39XtJhvHWFStid58iit4IPDLI/Da4cwdDmAHiQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "browser-resolve": "^1.11.3", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "jest-pnp-resolver": "^1.2.1", - "read-pkg-up": "^7.0.1", - "realpath-native": "^2.0.0", - "resolve": "^1.17.0", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-resolve-dependencies": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.5.4.tgz", - "integrity": "sha512-yFmbPd+DAQjJQg88HveObcGBA32nqNZ02fjYmtL16t1xw9bAttSn5UGRRhzMHIQbsep7znWvAvnD4kDqOFM0Uw==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-snapshot": "^25.5.1" - } - }, - "jest-runner": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.5.4.tgz", - "integrity": "sha512-V/2R7fKZo6blP8E9BL9vJ8aTU4TH2beuqGNxHbxi6t14XzTb+x90B3FRgdvuHm41GY8ch4xxvf0ATH4hdpjTqg==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/environment": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-config": "^25.5.4", - "jest-docblock": "^25.3.0", - "jest-haste-map": "^25.5.1", - "jest-jasmine2": "^25.5.4", - "jest-leak-detector": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-resolve": "^25.5.1", - "jest-runtime": "^25.5.4", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "source-map-support": "^0.5.6", - "throat": "^5.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-runtime": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.5.4.tgz", - "integrity": "sha512-RWTt8LeWh3GvjYtASH2eezkc8AehVoWKK20udV6n3/gC87wlTbE1kIA+opCvNWyyPeBs6ptYsc6nyHUb1GlUVQ==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/environment": "^25.5.0", - "@jest/globals": "^25.5.2", - "@jest/source-map": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.4", - "jest-config": "^25.5.4", - "jest-haste-map": "^25.5.1", - "jest-message-util": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0", - "yargs": "^15.3.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-serializer": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.5.0.tgz", - "integrity": "sha512-LxD8fY1lByomEPflwur9o4e2a5twSQ7TaVNLlFUuToIdoJuBt8tzHfCsZ42Ok6LkKXWzFWf3AGmheuLAA7LcCA==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4" - } - }, - "jest-snapshot": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.5.1.tgz", - "integrity": "sha512-C02JE1TUe64p2v1auUJ2ze5vcuv32tkv9PyhEb318e8XOKF7MOyXdJ7kdjbvrp3ChPLU2usI7Rjxs97Dj5P0uQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0", - "@jest/types": "^25.5.0", - "@types/prettier": "^1.19.0", - "chalk": "^3.0.0", - "expect": "^25.5.0", - "graceful-fs": "^4.2.4", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-resolve": "^25.5.1", - "make-dir": "^3.0.0", - "natural-compare": "^1.4.0", - "pretty-format": "^25.5.0", - "semver": "^6.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz", - "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "make-dir": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-validate": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.5.0.tgz", - "integrity": "sha512-okUFKqhZIpo3jDdtUXUZ2LxGUZJIlfdYBvZb1aczzxrlyMlqdnnws9MOxezoLGhSaFc2XYaHNReNQfj5zPIWyQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "leven": "^3.1.0", - "pretty-format": "^25.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-watch-typeahead": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-0.5.0.tgz", - "integrity": "sha512-4r36w9vU8+rdg48hj0Z7TvcSqVP6Ao8dk04grlHQNgduyCB0SqrI0xWIl85ZhXrzYvxQ0N5H+rRLAejkQzEHeQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "jest-regex-util": "^25.2.1", - "jest-watcher": "^25.2.4", - "slash": "^3.0.0", - "string-length": "^3.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-watcher": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.5.0.tgz", - "integrity": "sha512-XrSfJnVASEl+5+bb51V0Q7WQx65dTSk7NL4yDdVjPnRNpM0hG+ncFmDYJo9O8jaSRcAitVbuVawyXCRoxGrT5Q==", - "dev": true, - "requires": { - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "jest-util": "^25.5.0", - "string-length": "^3.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-worker": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", - "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", - "dev": true, - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jmespath": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", - "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" - }, - "jpjs": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/jpjs/-/jpjs-1.2.1.tgz", - "integrity": "sha512-GxJWybWU4NV0RNKi6EIqk6IRPOTqd/h+U7sbtyuD7yUISUzV78LdHnq2xkevJsTlz/EImux4sWj+wfMiwKLkiw==", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsdom": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", - "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", - "dev": true, - "requires": { - "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" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "jsx-ast-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", - "integrity": "sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==", - "dev": true, - "requires": { - "array-includes": "^3.1.2", - "object.assign": "^4.1.2" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - }, - "kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" - }, - "language-subtag-registry": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", - "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==", - "dev": true - }, - "language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", - "dev": true, - "requires": { - "language-subtag-registry": "~0.3.2" - } - }, - "last-call-webpack-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", - "integrity": "sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==", - "dev": true, - "requires": { - "lodash": "^4.17.5", - "webpack-sources": "^1.1.0" - } - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lilconfig": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.2.tgz", - "integrity": "sha512-4zUThttj8TQ4N7Pps92Z79jPf1OMcll4m61pivQSVk5MT78hVhNa2LrKTuNYD0AGLpmpf7zeIKOxSt6hHBfypw==", - "dev": true - }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", - "dev": true - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "log-update": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", - "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "cli-cursor": "^2.0.0", - "wrap-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "wrap-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", - "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" - } - } - } - }, - "logform": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", - "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", - "requires": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "triple-beam": "^1.3.0" - }, - "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "magic-string": { - "version": "0.25.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", - "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", - "dev": true, - "requires": { - "sourcemap-codec": "^1.4.4" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.x" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", - "dev": true - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", - "dev": true - }, - "mime-db": { - "version": "1.46.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", - "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==", - "dev": true - }, - "mime-types": { - "version": "2.1.29", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", - "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", - "dev": true, - "requires": { - "mime-db": "1.46.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "mri": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz", - "integrity": "sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", - "dev": true, - "optional": true - }, - "nanoid": { - "version": "3.1.22", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", - "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==", - "dev": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "requires": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node-gyp-build": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", - "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==" - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", - "dev": true, - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true - }, - "node-notifier": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz", - "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==", - "dev": true, - "optional": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^2.1.1", - "semver": "^6.3.0", - "shellwords": "^0.1.1", - "which": "^1.3.1" - }, - "dependencies": { - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "optional": true, - "requires": { - "is-docker": "^2.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "optional": true - } - } - }, - "node-releases": { - "version": "1.1.73", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz", - "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - }, - "dependencies": { - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - } - } - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - }, - "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", - "dev": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - }, - "object.entries": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.3.tgz", - "integrity": "sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1", - "has": "^1.0.3" - } - }, - "object.fromentries": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.4.tgz", - "integrity": "sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", - "has": "^1.0.3" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz", - "integrity": "sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "object.values": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.3.tgz", - "integrity": "sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", - "has": "^1.0.3" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "requires": { - "fn.name": "1.x.x" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "dev": true - }, - "optimize-css-assets-webpack-plugin": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.4.tgz", - "integrity": "sha512-wqd6FdI2a5/FdoiCNNkEvLeA//lHHfG24Ln2Xm2qqdIk4aOlsR18jwpyOihqQ8849W3qu2DX8fOYxpvTMj+93A==", - "dev": true, - "requires": { - "cssnano": "^4.1.10", - "last-call-webpack-plugin": "^3.0.0" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "ora": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.0.tgz", - "integrity": "sha512-1StwyXQGoU6gdjYkyVcqOLnVlbKj+6yPNNOxJVgpt9t4eksKjiriiHuxktLYkgllwk+D6MbC4ihH84L1udRXPg==", - "dev": true, - "requires": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-each-series": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", - "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, - "parallel-transform": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", - "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", - "dev": true, - "requires": { - "cyclist": "^1.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - }, - "dependencies": { - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - } - } - }, - "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", - "dev": true - }, - "pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true, - "optional": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "pbkdf2": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", - "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "dev": true, - "requires": { - "node-modules-regexp": "^1.0.0" - } - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, - "pnp-webpack-plugin": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", - "integrity": "sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==", - "dev": true, - "requires": { - "ts-pnp": "^1.1.6" - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "postcss": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.5.tgz", - "integrity": "sha512-NxTuJocUhYGsMiMFHDUkmjSKT3EdH4/WbGF6GCi1NDGk+vbcUTun4fpbOqaPtD8IIsztA2ilZm2DhYCuyN58gA==", - "dev": true, - "requires": { - "colorette": "^1.2.2", - "nanoid": "^3.1.23", - "source-map-js": "^0.6.2" - }, - "dependencies": { - "nanoid": { - "version": "3.1.23", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", - "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", - "dev": true - } - } - }, - "postcss-calc": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", - "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", - "dev": true, - "requires": { - "postcss": "^7.0.27", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.2" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-colormin": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", - "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "color": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-convert-values": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", - "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-discard-comments": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", - "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-discard-duplicates": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", - "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-discard-empty": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", - "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-discard-overridden": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", - "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-merge-longhand": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", - "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", - "dev": true, - "requires": { - "css-color-names": "0.0.4", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "stylehacks": "^4.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-merge-rules": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", - "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "cssnano-util-same-parent": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0", - "vendors": "^1.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-minify-font-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", - "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-minify-gradients": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", - "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "is-color-stop": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-minify-params": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", - "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "browserslist": "^4.0.0", - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "uniqs": "^2.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-minify-selectors": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", - "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true - }, - "postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "dev": true, - "requires": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - } - }, - "postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.4" - } - }, - "postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "requires": { - "icss-utils": "^5.0.0" - } - }, - "postcss-normalize-charset": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", - "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-normalize-display-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", - "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-positions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", - "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-repeat-style": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", - "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-string": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", - "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", - "dev": true, - "requires": { - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-timing-functions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", - "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-unicode": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", - "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", - "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", - "dev": true, - "requires": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-whitespace": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", - "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-ordered-values": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", - "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-reduce-initial": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", - "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-reduce-transforms": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", - "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-selector-parser": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", - "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1", - "util-deprecate": "^1.0.2" - } - }, - "postcss-svgo": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", - "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", - "dev": true, - "requires": { - "is-svg": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "svgo": "^1.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-unique-selectors": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", - "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "postcss": "^7.0.0", - "uniqs": "^2.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "pretty-format": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", - "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^16.12.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "progress-estimator": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/progress-estimator/-/progress-estimator-0.2.2.tgz", - "integrity": "sha512-GF76Ac02MTJD6o2nMNtmtOFjwWCnHcvXyn5HOWPQnEMO8OTLw7LAvNmrwe8LmdsB+eZhwUu9fX/c9iQnBxWaFA==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "cli-spinners": "^1.3.1", - "humanize-duration": "^3.15.3", - "log-update": "^2.3.0" - }, - "dependencies": { - "cli-spinners": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz", - "integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==", - "dev": true - } - } - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", - "dev": true - }, - "prompts": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz", - "integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } - }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "requires": { - "resolve": "^1.1.6" - } - }, - "regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", - "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", - "dev": true, - "requires": { - "regenerate": "^1.4.0" - } - }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", - "dev": true - }, - "regenerator-transform": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", - "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexp.prototype.flags": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", - "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", - "dev": true - }, - "regexpu-core": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", - "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", - "dev": true, - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.2.0", - "regjsgen": "^0.5.1", - "regjsparser": "^0.6.4", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.2.0" - } - }, - "regjsgen": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", - "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", - "dev": true - }, - "regjsparser": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.9.tgz", - "integrity": "sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } - } - }, - "request-promise-core": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", - "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", - "dev": true, - "requires": { - "lodash": "^4.17.19" - } - }, - "request-promise-native": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", - "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", - "dev": true, - "requires": { - "request-promise-core": "1.1.4", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - }, - "dependencies": { - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rgb-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", - "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", - "dev": true - }, - "rgba-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", - "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "rollup": { - "version": "1.32.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz", - "integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==", - "dev": true, - "requires": { - "@types/estree": "*", - "@types/node": "*", - "acorn": "^7.1.0" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } - } - }, - "rollup-plugin-sourcemaps": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz", - "integrity": "sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^3.0.9", - "source-map-resolve": "^0.6.0" - }, - "dependencies": { - "source-map-resolve": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", - "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0" - } - } - } - }, - "rollup-plugin-terser": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.3.1.tgz", - "integrity": "sha512-1pkwkervMJQGFYvM9nscrUoncPwiKR/K+bHdjv6PFgRo3cgPHoRT83y2Aa3GvINj4539S15t/tpFPb775TDs6w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.5.5", - "jest-worker": "^24.9.0", - "rollup-pluginutils": "^2.8.2", - "serialize-javascript": "^4.0.0", - "terser": "^4.6.2" - }, - "dependencies": { - "jest-worker": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", - "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", - "dev": true, - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^6.1.0" - } - } - } - }, - "rollup-plugin-typescript2": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.27.3.tgz", - "integrity": "sha512-gmYPIFmALj9D3Ga1ZbTZAKTXq1JKlTQBtj299DXhqYz9cL3g/AQfUvbb2UhH+Nf++cCq941W2Mv7UcrcgLzJJg==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^3.1.0", - "find-cache-dir": "^3.3.1", - "fs-extra": "8.1.0", - "resolve": "1.17.0", - "tslib": "2.0.1" - }, - "dependencies": { - "find-cache-dir": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", - "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "tslib": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", - "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", - "dev": true - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - } - } - }, - "rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "requires": { - "estree-walker": "^0.6.1" - }, - "dependencies": { - "estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true - } - } - }, - "rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", - "dev": true - }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", - "dev": true, - "requires": { - "aproba": "^1.1.1" - } - }, - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, - "sade": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz", - "integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==", - "dev": true, - "requires": { - "mri": "^1.1.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sane": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", - "dev": true, - "requires": { - "@cnakazawa/watch": "^1.0.3", - "anymatch": "^2.0.0", - "capture-exit": "^2.0.0", - "exec-sh": "^0.3.2", - "execa": "^1.0.0", - "fb-watchman": "^2.0.0", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - } - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "saxes": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", - "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", - "dev": true, - "requires": { - "xmlchars": "^2.1.1" - } - }, - "schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.6", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shelljs": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", - "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", - "dev": true, - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true, - "optional": true - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - } - } - }, - "sirv": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.11.tgz", - "integrity": "sha512-SR36i3/LSWja7AJNRBz4fF/Xjpn7lQFI30tZ434dIy+bitLYSP+ZEenHg36i23V2SGEz+kqjksg0uOGZ5LPiqg==", - "dev": true, - "requires": { - "@polka/url": "^1.0.0-next.9", - "mime": "^2.3.1", - "totalist": "^1.0.0" - } - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "size-limit": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/size-limit/-/size-limit-4.10.2.tgz", - "integrity": "sha512-FvRqs/F3SfmDPI9UX7tBcQM7PPEgtSFjOao+awOjn73GYY9LUy4SDMyE0BEZgvYJbG5ditfqZffaTvPsde0cag==", - "dev": true, - "requires": { - "bytes-iec": "^3.1.1", - "chokidar": "^3.5.1", - "ci-job-number": "^1.2.2", - "colorette": "^1.2.2", - "globby": "^11.0.3", - "lilconfig": "^2.0.2", - "ora": "^5.4.0", - "read-pkg-up": "^7.0.1" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - } - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", - "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "dev": true - }, - "sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", - "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", - "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" - }, - "stack-utils": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.4.tgz", - "integrity": "sha512-IPDJfugEGbfizBwBZRZ3xpccMdRyP5lqsBWXGQWimVjua/ccLCeMOAVjlc1R7LxFjo5sEDhyNIXd8mo/AiDS9w==", - "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - } - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", - "dev": true - }, - "string-length": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", - "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", - "dev": true, - "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^5.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "string.prototype.matchall": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.4.tgz", - "integrity": "sha512-pknFIWVachNcyqRfaQSeu/FUfpvJTe4uskUSZ9Wc1RijsPuzbZ8TyYT8WCNnntCjUEqQ3vUHMAfVj2+wLAisPQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", - "has-symbols": "^1.0.1", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.3.1", - "side-channel": "^1.0.4" - } - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "style-loader": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz", - "integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==", - "dev": true, - "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - } - }, - "stylehacks": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", - "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-hyperlinks": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", - "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", - "dev": true, - "requires": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "svgo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - } - } - }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true - }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - } - }, - "terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", - "dev": true, - "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - } - }, - "terser-webpack-plugin": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", - "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", - "dev": true, - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "throat": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "dev": true, - "requires": { - "setimmediate": "^1.0.4" - } - }, - "timsort": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", - "dev": true - }, - "tiny-glob": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.8.tgz", - "integrity": "sha512-vkQP7qOslq63XRX9kMswlby99kyO5OvKptw7AMwBVMjXEI7Tb61eoI5DydyEMOseyGS5anDN1VPoVxEvH01q8w==", - "dev": true, - "requires": { - "globalyzer": "0.1.0", - "globrex": "^0.1.2" - } - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "totalist": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", - "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", - "dev": true - }, - "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", - "dev": true, - "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" - }, - "ts-jest": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.5.1.tgz", - "integrity": "sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw==", - "dev": true, - "requires": { - "bs-logger": "0.x", - "buffer-from": "1.x", - "fast-json-stable-stringify": "2.x", - "json5": "2.x", - "lodash.memoize": "4.x", - "make-error": "1.x", - "micromatch": "4.x", - "mkdirp": "0.x", - "semver": "6.x", - "yargs-parser": "18.x" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "ts-pnp": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", - "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==", - "dev": true - }, - "tsconfig-paths": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", - "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", - "dev": true, - "requires": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - } - } - }, - "tsdx": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/tsdx/-/tsdx-0.14.1.tgz", - "integrity": "sha512-keHmFdCL2kx5nYFlBdbE3639HQ2v9iGedAFAajobrUTH2wfX0nLPdDhbHv+GHLQZqf0c5ur1XteE8ek/+Eyj5w==", - "dev": true, - "requires": { - "@babel/core": "^7.4.4", - "@babel/helper-module-imports": "^7.0.0", - "@babel/parser": "^7.11.5", - "@babel/plugin-proposal-class-properties": "^7.4.4", - "@babel/preset-env": "^7.11.0", - "@babel/traverse": "^7.11.5", - "@rollup/plugin-babel": "^5.1.0", - "@rollup/plugin-commonjs": "^11.0.0", - "@rollup/plugin-json": "^4.0.0", - "@rollup/plugin-node-resolve": "^9.0.0", - "@rollup/plugin-replace": "^2.2.1", - "@types/jest": "^25.2.1", - "@typescript-eslint/eslint-plugin": "^2.12.0", - "@typescript-eslint/parser": "^2.12.0", - "ansi-escapes": "^4.2.1", - "asyncro": "^3.0.0", - "babel-eslint": "^10.0.3", - "babel-plugin-annotate-pure-calls": "^0.4.0", - "babel-plugin-dev-expression": "^0.2.1", - "babel-plugin-macros": "^2.6.1", - "babel-plugin-polyfill-regenerator": "^0.0.4", - "babel-plugin-transform-rename-import": "^2.3.0", - "camelcase": "^6.0.0", - "chalk": "^4.0.0", - "enquirer": "^2.3.4", - "eslint": "^6.1.0", - "eslint-config-prettier": "^6.0.0", - "eslint-config-react-app": "^5.2.1", - "eslint-plugin-flowtype": "^3.13.0", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-prettier": "^3.1.0", - "eslint-plugin-react": "^7.14.3", - "eslint-plugin-react-hooks": "^2.2.0", - "execa": "^4.0.3", - "fs-extra": "^9.0.0", - "jest": "^25.3.0", - "jest-watch-typeahead": "^0.5.0", - "jpjs": "^1.2.1", - "lodash.merge": "^4.6.2", - "ora": "^4.0.3", - "pascal-case": "^3.1.1", - "prettier": "^1.19.1", - "progress-estimator": "^0.2.2", - "regenerator-runtime": "^0.13.7", - "rollup": "^1.32.1", - "rollup-plugin-sourcemaps": "^0.6.2", - "rollup-plugin-terser": "^5.1.2", - "rollup-plugin-typescript2": "^0.27.3", - "sade": "^1.4.2", - "semver": "^7.1.1", - "shelljs": "^0.8.3", - "tiny-glob": "^0.2.6", - "ts-jest": "^25.3.1", - "tslib": "^1.9.3", - "typescript": "^3.7.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", - "dev": true, - "requires": { - "chalk": "^2.4.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "ora": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-4.1.1.tgz", - "integrity": "sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.2.0", - "is-interactive": "^1.0.0", - "log-symbols": "^3.0.0", - "mute-stream": "0.0.8", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "dependencies": { - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - } - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "typescript": { - "version": "3.9.9", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", - "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", - "dev": true - } - } - }, - "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "typescript": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", - "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", - "dev": true - }, - "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", - "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^1.0.4", - "unicode-property-aliases-ecmascript": "^1.0.4" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", - "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", - "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", - "dev": true - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", - "dev": true - }, - "uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", - "dev": true - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, - "unorm": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", - "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==" - }, - "unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true, - "optional": true - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "utf-8-validate": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.4.tgz", - "integrity": "sha512-MEF05cPSq3AwJ2C7B7sHAA6i53vONoZbMGX8My5auEVm6W+dJ2Jd/TZPyGJ5CH42V2XtbI5FD28HeHeqlPzZ3Q==", - "requires": { - "node-gyp-build": "^4.2.0" - } - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "dev": true, - "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "util.promisify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" - } - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "v8-to-istanbul": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz", - "integrity": "sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "vendors": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", - "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true - }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, - "w3c-xmlserializer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", - "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", - "dev": true, - "requires": { - "domexception": "^1.0.1", - "webidl-conversions": "^4.0.2", - "xml-name-validator": "^3.0.0" - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "requires": { - "makeerror": "1.0.x" - } - }, - "watchpack": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", - "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", - "dev": true, - "requires": { - "chokidar": "^3.4.1", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0", - "watchpack-chokidar2": "^2.0.1" - } - }, - "watchpack-chokidar2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", - "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", - "dev": true, - "optional": true, - "requires": { - "chokidar": "^2.1.8" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "optional": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "optional": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true, - "optional": true - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "optional": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "optional": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "optional": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "optional": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "optional": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - } - } - }, - "wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "dev": true, - "requires": { - "defaults": "^1.0.3" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "webpack": { - "version": "4.46.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", - "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/wasm-edit": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "acorn": "^6.4.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.5.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.3", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.7.4", - "webpack-sources": "^1.4.1" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "webpack-bundle-analyzer": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.0.tgz", - "integrity": "sha512-9DhNa+aXpqdHk8LkLPTBU/dMfl84Y+WE2+KnfI6rSpNRNVKa0VGLjPd2pjFubDeqnWmulFggxmWBxhfJXZnR0g==", - "dev": true, - "requires": { - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "chalk": "^4.1.0", - "commander": "^6.2.0", - "gzip-size": "^6.0.0", - "lodash": "^4.17.20", - "opener": "^1.5.2", - "sirv": "^1.0.7", - "ws": "^7.3.1" - }, - "dependencies": { - "acorn": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.1.0.tgz", - "integrity": "sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "websocket": { - "version": "1.0.33", - "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.33.tgz", - "integrity": "sha512-XwNqM2rN5eh3G2CUQE3OHZj+0xfdH42+OFK6LdC2yqiC0YU8e5UK0nYre220T0IyyN031V/XOvtHvXozvJYFWA==", - "requires": { - "bufferutil": "^4.0.1", - "debug": "^2.2.0", - "es5-ext": "^0.10.50", - "typedarray-to-buffer": "^3.1.5", - "utf-8-validate": "^5.0.2", - "yaeti": "^0.0.6" - } - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "winston": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", - "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", - "requires": { - "@dabh/diagnostics": "^2.0.2", - "async": "^3.1.0", - "is-stream": "^2.0.0", - "logform": "^2.2.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.4.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "winston-transport": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", - "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", - "requires": { - "readable-stream": "^2.3.7", - "triple-beam": "^1.2.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "worker-farm": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", - "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", - "dev": true, - "requires": { - "errno": "~0.1.7" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - } - } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "ws": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", - "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==" - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" - }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "xstate": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.17.1.tgz", - "integrity": "sha512-3q7so9qAKFnz9/t7BNQXQtV+9fwDATCOkC+0tAvVqczboEbu6gz2dvPPVCCkj55Hyzgro9aSOntGSPGLei82BA==" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, - "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", - "dev": true - }, - "yaeti": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } - } - } - } -} diff --git a/package.json b/package.json index c469ff65..8955aed6 100644 --- a/package.json +++ b/package.json @@ -1,67 +1,24 @@ { - "version": "1.4.2-beta", - "license": "MIT", - "main": "dist/index.js", - "typings": "dist/index.d.ts", - "files": [ - "dist", - "src" + "name": "hathor-wallet-service", + "version": "1.4.6", + "workspaces": [ + "packages/daemon", + "packages/wallet-service" ], "engines": { - "node": ">=10" + "node": ">=18" }, - "scripts": { - "start": "tsdx watch", - "build": "tsdx build --format cjs", - "test": "tsdx test", - "lint": "tsdx lint", - "prepare": "tsdx build", - "size": "size-limit", - "analyze": "size-limit --why" - }, - "peerDependencies": {}, - "husky": { - "hooks": { - "pre-commit": "tsdx lint" - } - }, - "prettier": { - "printWidth": 80, - "semi": true, - "singleQuote": true, - "trailingComma": "es5" - }, - "name": "hathor-wallet-service-sync_daemon", - "author": "André Abadesso", - "module": "dist/hathor-wallet-service-sync_daemon.esm.js", - "size-limit": [ - { - "path": "dist/hathor-wallet-service-sync_daemon.cjs.production.min.js", - "limit": "10 KB" - }, - { - "path": "dist/hathor-wallet-service-sync_daemon.esm.js", - "limit": "10 KB" - } + "nohoist": [ + "**" ], + "repository": "git@github.com:HathorNetwork/hathor-wallet-service-sync_daemon.git", + "author": "André Abadesso ", + "private": true, "devDependencies": { - "@size-limit/preset-small-lib": "^4.10.2", - "@types/lodash": "^4.14.172", - "@types/node": "^17.0.21", - "husky": "^6.0.0", - "size-limit": "^4.10.2", - "tsdx": "^0.14.1", - "tslib": "^2.1.0", - "typescript": "^4.2.3" + "dotenv": "^16.3.1", + "mysql2": "^3.6.1", + "sequelize": "^6.33.0", + "sequelize-cli": "^6.6.1" }, - "dependencies": { - "@hathor/wallet-lib": "^0.20.3", - "aws-sdk": "^2.878.0", - "axios": "^0.21.1", - "dotenv": "^8.2.0", - "lodash": "^4.17.21", - "websocket": "^1.0.33", - "winston": "^3.3.3", - "xstate": "^4.17.1" - } + "packageManager": "yarn@4.1.0" } diff --git a/packages/daemon/__tests__/__fixtures__/events.ts b/packages/daemon/__tests__/__fixtures__/events.ts new file mode 100644 index 00000000..cbc766e3 --- /dev/null +++ b/packages/daemon/__tests__/__fixtures__/events.ts @@ -0,0 +1,127 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export default { + VERTEX_METADATA_CHANGED: { + type: 'EVENT', + event: { + stream_id: 'f7d9157c-9906-4bd2-bc84-cfb9f5b607d1', + network: 'mainnet', + peer_id: 'bdf4fa876f5cdba84be0cab53b21fc9eb45fe4b3d6ede99f493119d37df4e560', + id: 37, + timestamp: 1572653409.0, + type: 'VERTEX_METADATA_CHANGED', + data: { + hash: 'f42fbcd1549389632236f85a80ad2dd8cac2f150501fb40b11210bad03718f79', + nonce: 2, + timestamp: 1572653369, + version: 1, + weight: 18.664694903964126, + inputs: [{ + tx_id: 'd8d221392cda50bdb2c4bef1f11f826ddcad85ddab395d062d05fc4a592195c2', + index: 0, + data: 'SDBGAiEAwRECSYXApimxuQ9cD88w9U0N+SdAtJZfi0x1e3VgGmYCIQDsIsEC2nZzWgIa1U+eh/pIzhMg0rKvH3u8BaRLCpz4ICEC6Y5mbQB/qe5dH40iULOaEGoGq9CKeQMumnT8+yyMIHM=', + }], + outputs: [{ + value: 1431, + script: 'dqkU91U6sMdzgT3zxOtdIVGbqobP0FmIrA==', + token_data: 0 + }, { + value: 4969, + script: 'dqkUm3CeNv0dX1HsZAvl2H0Cr6NZ40CIrA==', + token_data: 0 + }], + parents: ['16ba3dbe424c443e571b00840ca54b9ff4cff467e10b6a15536e718e2008f952', '33e14cb555a96967841dcbe0f95e9eab5810481d01de8f4f73afb8cce365e869'], + tokens: [], + token_name: null, + token_symbol: null, + metadata: { + hash: 'f42fbcd1549389632236f85a80ad2dd8cac2f150501fb40b11210bad03718f79', + spent_outputs: [{ + index: 0, + tx_ids: ['58fba3126e91f546fc11792637d0c4112e2de12920628f98ca1abe4fa97cc74f'] + }, { + index: 1, + tx_ids: ['58fba3126e91f546fc11792637d0c4112e2de12920628f98ca1abe4fa97cc74f'] + }], + conflict_with: [], + voided_by: [], + received_by: [], + children: ['58fba3126e91f546fc11792637d0c4112e2de12920628f98ca1abe4fa97cc74f', '01375179ce0f6a6d6501fec0ee14dba8e134372a8fe6519aa952ced7b0577aaa'], + twins: [], + accumulated_weight: 18.664694903964126, + score: 0.0, + first_block: '01375179ce0f6a6d6501fec0ee14dba8e134372a8fe6519aa952ced7b0577aaa', + height: 0, + validation: 'full' + }, + aux_pow: null + }, + group_id: null + }, + latest_event_id: 38 + }, + NEW_VERTEX_ACCEPTED: { + type: 'EVENT', + event: { + peer_id: '9083fc84b47a475862b97534296b9713bb05e6dcd6640b804be4c20c3639d3f5', + id: 49, + timestamp: 1691028449.1147473, + type: 'NEW_VERTEX_ACCEPTED', + data: { + hash: '00000000171cb374cb433745b4080bcc7a44f42c4f563af1a624eea588f3f146', + nonce: 297718091, + timestamp: 1578077286, + version: 0, + weight: 34.879398065365535, + inputs: [], + outputs: [{ + value: 6400, + script: 'dqkUym0SWcUWwA1Du+i9fiZl4MbEfwWIrA==', + token_data: 0, + }], + parents: ['00000000008fe9c79211df3d1e2236202839534e1dab2fce587d7c4360d8b0b4', '0002d4d2a15def7604688e1878ab681142a7b155cbe52a6b4e031250ae96db0a', '0002ad8d1519daaddc8e1a37b14aac0b045129c01832281fb1c02d873c7abbf9'], + tokens: [], + token_name: null, + token_symbol: null, + metadata: { + hash: '00000000171cb374cb433745b4080bcc7a44f42c4f563af1a624eea588f3f146', + spent_outputs: [], + conflict_with: [], + voided_by: [], + received_by: [], + children: ['0000000009c21644558a29eb5e89061a993b8241b2580d26071b7d3efd7a9e03'], + twins: [], + accumulated_weight: 34.879398065365535, + score: 42.309318350260796, + first_block: null, + height: 46, + validation: 'full', + }, + aux_pow: null, + }, + group_id: null, + }, + latest_event_id: 5089156 + }, + REORG_STARTED: { + type: 'EVENT', + event: { + peer_id: '34370eed38ad67ef3d95fe005acdf182de6e0d50ebbea6d8234d9ee07e46ed1b', + id: 5524457, + timestamp: 1696040277.2181926, + type: 'REORG_STARTED', + data: { + reorg_size: 1, + previous_best_block: '000000000000000063345d97b451acc930eb7c1e15473bcfeb30797b8c417621', + new_best_block: '000000000000000135496ab5bd8a8d3ecf249fc19f6ee41afdf2230722900a60', + common_block: '000000000000000406a302805d80634675b1e9d2bab6e26d5b326abb3303e8ba' + }, + group_id: 1500, + }, + } +}; diff --git a/packages/daemon/__tests__/actors/HealthCheckActor.test.ts b/packages/daemon/__tests__/actors/HealthCheckActor.test.ts new file mode 100644 index 00000000..04b50257 --- /dev/null +++ b/packages/daemon/__tests__/actors/HealthCheckActor.test.ts @@ -0,0 +1,195 @@ +import HealthCheckActor from '../../src/actors/HealthCheckActor'; +import axios from 'axios'; +import logger from '../../src/logger'; +import { EventTypes } from '../../src/types/event'; +import getConfig from '../../src/config'; +import { get } from 'lodash'; + +jest.useFakeTimers(); +jest.spyOn(global, 'setInterval'); +jest.spyOn(global, 'clearInterval'); + +describe('HealthCheckActor', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should not start pinging on initialization', () => { + const config = getConfig(); + + config['HEALTHCHECK_ENABLED'] = true; + + // Mock axios and logger + const mockAxios = jest.spyOn(axios, 'post'); + const mockLogger = jest.spyOn(logger, 'info'); + + // Mock the callback and receive functions + const mockCallback = jest.fn(); + + let receiveCallback: any; + + const mockReceive = jest.fn().mockImplementation((callback) => { + receiveCallback = callback; + }); + + // Call the HealthCheckActor function + HealthCheckActor(mockCallback, mockReceive, config); + + expect(setInterval).not.toHaveBeenCalled(); + }); + + it('should start pinging when receiving a START event and stop when receiving a STOP event', () => { + const config = getConfig(); + + config['HEALTHCHECK_ENABLED'] = true; + config['HEALTHCHECK_SERVER_URL'] = 'http://localhost:3000'; + + // Mock axios and logger + const mockAxios = jest.spyOn(axios, 'post'); + const mockLogger = jest.spyOn(logger, 'info'); + + // Mock the callback and receive functions + const mockCallback = jest.fn(); + + let receiveCallback: any; + + const mockReceive = jest.fn().mockImplementation((callback) => { + receiveCallback = callback; + }); + + // Call the HealthCheckActor function + HealthCheckActor(mockCallback, mockReceive, config); + + // Call the receive callback with a START event + receiveCallback({ + type: EventTypes.HEALTHCHECK_EVENT, + event: { + type: 'START', + }, + }); + + expect(setInterval).toHaveBeenCalledTimes(1); + + // Call the receive callback with a STOP event + receiveCallback({ + type: EventTypes.HEALTHCHECK_EVENT, + event: { + type: 'STOP', + }, + }); + + expect(clearInterval).toHaveBeenCalledTimes(1); + }); + + it('should stop pinging when the actor is stopped', () => { + const config = getConfig(); + config['HEALTHCHECK_ENABLED'] = true; + config['HEALTHCHECK_SERVER_URL'] = 'http://localhost:3000'; + + // Mock axios and logger + const mockAxios = jest.spyOn(axios, 'post'); + const mockLogger = jest.spyOn(logger, 'info'); + + // Mock the callback and receive functions + const mockCallback = jest.fn(); + + let receiveCallback: any; + + const mockReceive = jest.fn().mockImplementation((callback) => { + receiveCallback = callback; + }); + + // Call the HealthCheckActor function + const stopHealthCheckActor = HealthCheckActor(mockCallback, mockReceive, config); + + // Call the receive callback with a START event + receiveCallback({ + type: EventTypes.HEALTHCHECK_EVENT, + event: { + type: 'START', + }, + }); + + expect(setInterval).toHaveBeenCalledTimes(1); + + // Call the stop function + stopHealthCheckActor(); + + expect(clearInterval).toHaveBeenCalledTimes(1); + }); + + it('should not start pinging when HEALTHCHECK_ENABLED is false', () => { + const config = getConfig(); + config['HEALTHCHECK_ENABLED'] = false; + + // Mock axios and logger + const mockAxios = jest.spyOn(axios, 'post'); + const mockLogger = jest.spyOn(logger, 'info'); + + // Mock the callback and receive functions + const mockCallback = jest.fn(); + + let receiveCallback: any; + + const mockReceive = jest.fn().mockImplementation((callback) => { + receiveCallback = callback; + }); + + // Call the HealthCheckActor function + HealthCheckActor(mockCallback, mockReceive, config); + + expect(mockReceive).not.toHaveBeenCalled(); + }); + + it('should send ping after the configured interval', () => { + const config = getConfig(); + config['HEALTHCHECK_ENABLED'] = true; + config['HEALTHCHECK_SERVER_URL'] = 'http://localhost:3000'; + config['HEALTHCHECK_SERVER_API_KEY'] = 'test-api-key'; + + // Mock axios and logger + const mockAxios = jest.spyOn(axios, 'post').mockResolvedValue({ status: 200 }); + const mockLogger = jest.spyOn(logger, 'info'); + + // Mock the callback and receive functions + const mockCallback = jest.fn(); + + let receiveCallback: any; + + const mockReceive = jest.fn().mockImplementation((callback) => { + receiveCallback = callback; + }); + + // Call the HealthCheckActor function + HealthCheckActor(mockCallback, mockReceive, config); + + // Call the receive callback with a START event + receiveCallback({ + type: EventTypes.HEALTHCHECK_EVENT, + event: { + type: 'START', + }, + }); + + expect(setInterval).toHaveBeenCalledTimes(1); + + // Fast-forward until all timers have been executed + jest.runOnlyPendingTimers(); + + expect(mockAxios).toHaveBeenCalledTimes(1); + expect(mockAxios).toHaveBeenCalledWith( + 'http://localhost:3000', + {}, + { + headers: { + 'Content-Type': 'application/json', + 'X-Api-Key': 'test-api-key', + }, + }, + ); + }); +}); diff --git a/packages/daemon/__tests__/db/index.test.ts b/packages/daemon/__tests__/db/index.test.ts new file mode 100644 index 00000000..bc3157d8 --- /dev/null +++ b/packages/daemon/__tests__/db/index.test.ts @@ -0,0 +1,1165 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + addMiner, + addNewAddresses, + addOrUpdateTx, + addUtxos, + fetchAddressBalance, + fetchAddressTxHistorySum, + generateAddresses, + getAddressWalletInfo, + getBestBlockHeight, + getDbConnection, + getExpiredTimelocksUtxos, + getLastSyncedEvent, + getLockedUtxoFromInputs, + getMinersList, + getTokenInformation, + getTokenSymbols, + getTransactionById, + getTxOutput, + getTxOutputs, + getTxOutputsAtHeight, + getTxOutputsBySpent, + getTxOutputsFromTx, + getUtxosLockedAtHeight, + incrementTokensTxCount, + markUtxosAsVoided, + storeTokenInformation, + unlockUtxos, + unspendUtxos, + updateAddressLockedBalance, + updateAddressTablesWithTx, + updateLastSyncedEvent, + updateTxOutputSpentBy, + updateWalletLockedBalance, + updateWalletTablesWithTx +} from '../../src/db'; +import { Connection } from 'mysql2/promise'; +import { + ADDRESSES, + addToAddressBalanceTable, + addToAddressTable, + addToAddressTxHistoryTable, + addToTokenTable, + addToUtxoTable, + addToWalletBalanceTable, + addToWalletTable, + checkAddressBalanceTable, + checkAddressTable, + checkAddressTxHistoryTable, + checkTokenTable, + checkUtxoTable, + checkWalletBalanceTable, + checkWalletTxHistoryTable, + cleanDatabase, + countTxOutputTable, + createEventTxInput, + createInput, + createOutput, + XPUBKEY, +} from '../utils'; +import { isAuthority } from '../../src/utils'; +import { Authorities, DbTxOutput, StringMap, TokenBalanceMap, TokenInfo, WalletStatus } from '../../src/types'; + +// Use a single mysql connection for all tests +let mysql: Connection; +beforeAll(async () => { + try { + mysql = await getDbConnection(); + } catch(e) { + console.error('Failed to establish db connection', e); + throw e; + } +}); + +afterAll(() => { + mysql.destroy(); +}); + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + +describe('transaction methods', () => { + test('should insert a new tx to the database', async () => { + expect.hasAssertions(); + + await addOrUpdateTx(mysql, 'txId1', null, 1, 1, 65.4321); + const tx = await getTransactionById(mysql, 'txId1'); + + expect(tx?.weight).toStrictEqual(65.4321); + }); + + test('db which is not on our database should return null', async () => { + expect.hasAssertions(); + + await expect(getTransactionById(mysql, 'txId1')).resolves.toBeNull(); + }); + + test('should update the height on a already existing transaction', async () => { + expect.hasAssertions(); + + await addOrUpdateTx(mysql, 'txId1', null, 1, 1, 65.4321); + await addOrUpdateTx(mysql, 'txId1', 1, 1, 1, 65.4321); + + const tx = await getTransactionById(mysql, 'txId1'); + expect(tx?.height).toStrictEqual(1); + }); + + test('should be able to get the best block height', async () => { + expect.hasAssertions(); + + await addOrUpdateTx(mysql, 'txId1', 0, 1, 1, 65.4321); + await addOrUpdateTx(mysql, 'txId2', 2, 1, 1, 65.4321); + await addOrUpdateTx(mysql, 'txId3', 3, 1, 1, 65.4321); + await addOrUpdateTx(mysql, 'txId4', 4, 1, 1, 65.4321); + + const bestBlock = await getBestBlockHeight(mysql); + expect(bestBlock).toStrictEqual(4); + }); +}); + +describe('tx output methods', () => { + test('addUtxos, unlockUtxos, updateTxOutputSpentBy, unspendUtxos, getTxOutput, getTxOutputsBySpent and markUtxosAsVoided', async () => { + expect.hasAssertions(); + + const txId = 'txId'; + const utxos = [ + { value: 5, address: 'address1', tokenId: 'token1', locked: false }, + { value: 15, address: 'address1', tokenId: 'token1', locked: false }, + { value: 25, address: 'address2', tokenId: 'token2', timelock: 500, locked: true }, + { value: 35, address: 'address2', tokenId: 'token1', locked: false }, + // authority utxo + { value: 0b11, address: 'address1', tokenId: 'token1', locked: false, tokenData: 129 }, + ]; + + // empty list should be fine + await addUtxos(mysql, txId, []); + + // add to utxo table + const outputs = utxos.map((utxo, index) => createOutput( + index, + utxo.value, + utxo.address, + utxo.tokenId, + utxo.timelock || null, + utxo.locked, + utxo.tokenData || 0, + )); + await addUtxos(mysql, txId, outputs); + + for (const [_, output] of outputs.entries()) { + let { value } = output; + const { token, decoded } = output; + let authorities = 0; + if (isAuthority(output.token_data)) { + authorities = value; + value = 0; + } + await expect( + checkUtxoTable(mysql, utxos.length, txId, output.index, token, decoded?.address, value, authorities, decoded?.timelock, null, output.locked), + ).resolves.toBe(true); + } + + + // get an unspent tx output + expect(await getTxOutput(mysql, txId, 0, true)).toStrictEqual({ + txId: 'txId', + index: 0, + tokenId: utxos[0].tokenId, + address: utxos[0].address, + value: utxos[0].value, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + txProposalId: null, + txProposalIndex: null, + }); + + // empty list should be fine + await unlockUtxos(mysql, []); + + const inputs = utxos.map((utxo, index) => createInput(utxo.value, utxo.address, txId, index, utxo.tokenId, utxo.timelock)); + + // set tx_outputs as spent + await updateTxOutputSpentBy(mysql, inputs, txId); + + // get a spent tx output + expect(await getTxOutput(mysql, txId, 0, false)).toStrictEqual({ + txId: 'txId', + index: 0, + tokenId: utxos[0].tokenId, + address: utxos[0].address, + value: utxos[0].value, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: txId, + txProposalId: null, + txProposalIndex: null, + }); + + // if the tx output is not found, it should return null + expect(await getTxOutput(mysql, 'unknown-tx-id', 0, false)).toBeNull(); + + await expect(checkUtxoTable(mysql, 0)).resolves.toBe(true); + + const spentTxOutputs = await getTxOutputsBySpent(mysql, [txId]); + expect(spentTxOutputs).toHaveLength(5); + + const txOutputs = utxos.map((utxo, index) => ({ + ...utxo, + txId, + authorities: 0, + heightlock: null, + timelock: null, + index, + })); + + await unspendUtxos(mysql, txOutputs); + + for (const [index, output] of outputs.entries()) { + let { value } = output; + const { token, decoded } = output; + let authorities = 0; + if (isAuthority(output.token_data)) { + authorities = value; + value = 0; + } + await expect( + checkUtxoTable(mysql, utxos.length, txId, index, token, decoded?.address, value, authorities, decoded?.timelock, null, output.locked), + ).resolves.toBe(true); + } + + // unlock the locked one + const first = { + tx_id: txId, + index: 2, + token: 'token2', + token_data: 0, + decoded: { + type: 'P2PKH', + address: 'address2', + timelock: null, + }, + script: '', + value: 25, + authorities: 0, + timelock: 500, + heightlock: null, + locked: true, + }; + await unlockUtxos(mysql, [first]); + await expect(checkUtxoTable( + mysql, + utxos.length, + first.tx_id, + first.index, + first.token, + first.decoded.address, + first.value, + 0, + first.timelock, + first.heightlock, + false, + )).resolves.toBe(true); + + const countBeforeDelete = await countTxOutputTable(mysql); + expect(countBeforeDelete).toStrictEqual(5); + + await markUtxosAsVoided(mysql, txOutputs); + + const countAfterDelete = await countTxOutputTable(mysql); + expect(countAfterDelete).toStrictEqual(0); + }); + + test('getTxOutputsFromTx, getTxOutputs, getTxOutput', async () => { + expect.hasAssertions(); + + const txId = 'txId'; + const utxos: DbTxOutput[] = [ + { txId, index: 0, tokenId: 'token1', address: 'address1', value: 5, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null }, + { txId, index: 1, tokenId: 'token1', address: 'address1', value: 15, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null}, + { txId, index: 2, tokenId: 'token1', address: 'address1', value: 25, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null }, + { txId, index: 3, tokenId: 'token1', address: 'address1', value: 1, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null }, + { txId, index: 4, tokenId: 'token1', address: 'address1', value: 3, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null }, + ]; + + // add to utxo table + const outputs = utxos.map((utxo, index) => createOutput( + index, + utxo.value, + utxo.address, + utxo.tokenId, + utxo.timelock, + utxo.locked, + 0, + )); + + await addUtxos(mysql, txId, outputs); + + expect(await getTxOutputsFromTx(mysql, txId)).toStrictEqual(utxos); + expect(await getTxOutputs(mysql, utxos.map((utxo) => ({txId: utxo.txId, index: utxo.index})))).toStrictEqual(utxos); + expect(await getTxOutput(mysql, utxos[0].txId, utxos[0].index, false )).toStrictEqual(utxos[0]); + }); + + test('getTxOutputsAtHeight', async () => { + expect.hasAssertions(); + + const txId = 'txId'; + await addOrUpdateTx(mysql, txId, 0, 1, 1, 65); + + const utxos: DbTxOutput[] = [ + { txId, index: 0, tokenId: 'token1', address: 'address1', value: 5, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null }, + { txId, index: 1, tokenId: 'token1', address: 'address1', value: 15, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null}, + ]; + + // add to utxo table + const outputs = utxos.map((utxo, index) => createOutput( + index, + utxo.value, + utxo.address, + utxo.tokenId, + utxo.timelock, + utxo.locked, + 0, + )); + + await addUtxos(mysql, txId, outputs); + + expect(await getTxOutputsAtHeight(mysql, 0)).toStrictEqual(utxos); + }); + + test('getUtxosLockedAtHeight', async () => { + expect.hasAssertions(); + + const txId = 'txId'; + const txId2 = 'txId2'; + const utxos = [ + // no locks + { value: 5, address: 'address1', token: 'token1', locked: false }, + // only timelock + { value: 25, address: 'address2', token: 'token2', timelock: 50, locked: false }, + + ]; + const utxos2 = [ + // only heightlock + { value: 35, address: 'address2', token: 'token1', timelock: null, locked: true }, + // timelock and heightlock + { value: 45, address: 'address2', token: 'token1', timelock: 100, locked: true }, + { value: 55, address: 'address2', token: 'token1', timelock: 1000, locked: true }, + ]; + + // add to utxo table + const outputs = utxos.map((utxo, index) => createOutput(index, utxo.value, utxo.address, utxo.token, utxo.timelock, utxo.locked)); + await addUtxos(mysql, txId, outputs, null); + const outputs2 = utxos2.map((utxo, index) => createOutput(index, utxo.value, utxo.address, utxo.token, utxo.timelock, utxo.locked)); + await addUtxos(mysql, txId2, outputs2, 10); + + // fetch on timestamp=99 and heightlock=10. Should return: + // { value: 35, address: 'address2', token: 'token1', timelock: null}, + let results = await getUtxosLockedAtHeight(mysql, 99, 10); + expect(results).toHaveLength(1); + expect(results[0].value).toBe(35); + + // fetch on timestamp=100 and heightlock=10. Should return: + // { value: 35, address: 'address2', token: 'token1', timelock: null}, + // { value: 45, address: 'address2', token: 'token1', timelock: 100}, + results = await getUtxosLockedAtHeight(mysql, 100, 10); + expect(results).toHaveLength(2); + expect([35, 45]).toContain(results[0].value); + expect([35, 45]).toContain(results[1].value); + + // fetch on timestamp=100 and heightlock=9. Should return empty + results = await getUtxosLockedAtHeight(mysql, 1000, 9); + expect(results).toStrictEqual([]); + + // unlockedHeight < 0. This means the block is still very early after genesis and no blocks have been unlocked + results = await getUtxosLockedAtHeight(mysql, 1000, -2); + expect(results).toStrictEqual([]); + }); + + test('getExpiredTimelocksUtxos', async () => { + expect.hasAssertions(); + + const txId = 'txId'; + const utxos = [ + { value: 5, address: 'address1', tokenId: 'token1', locked: true }, + { value: 15, address: 'address1', tokenId: 'token1', locked: true }, + { value: 25, address: 'address2', tokenId: 'token2', timelock: 100, locked: true }, + { value: 35, address: 'address2', tokenId: 'token1', timelock: 200, locked: true }, + // authority utxo + { value: 0b11, address: 'address1', tokenId: 'token1', timelock: 300, locked: true, tokenData: 129 }, + ]; + + // empty list should be fine + await addUtxos(mysql, txId, []); + + // add to utxo table + const outputs = utxos.map((utxo, index) => createOutput( + index, + utxo.value, + utxo.address, + utxo.tokenId, + utxo.timelock || null, + utxo.locked, + utxo.tokenData || 0, + )); + + await addUtxos(mysql, txId, outputs); + + const unlockedUtxos0: DbTxOutput[] = await getExpiredTimelocksUtxos(mysql, 100); + const unlockedUtxos1: DbTxOutput[] = await getExpiredTimelocksUtxos(mysql, 101); + const unlockedUtxos2: DbTxOutput[] = await getExpiredTimelocksUtxos(mysql, 201); + const unlockedUtxos3: DbTxOutput[] = await getExpiredTimelocksUtxos(mysql, 301); + + expect(unlockedUtxos0).toHaveLength(0); + expect(unlockedUtxos1).toHaveLength(1); + expect(unlockedUtxos1[0].value).toStrictEqual(outputs[2].value); + expect(unlockedUtxos2).toHaveLength(2); + expect(unlockedUtxos2[1].value).toStrictEqual(outputs[3].value); + expect(unlockedUtxos3).toHaveLength(3); + // last one is an authority utxo + expect(unlockedUtxos3[2].authorities).toStrictEqual(outputs[4].value); + }); + + test('getLockedUtxoFromInputs', async () => { + expect.hasAssertions(); + const txId = 'txId'; + const utxos = [ + { value: 5, address: 'address1', token: 'token1', locked: false }, + { value: 25, address: 'address2', token: 'token2', timelock: 500, locked: true }, + { value: 35, address: 'address2', token: 'token1', locked: false }, + ]; + + // add to utxo table + const outputs = utxos.map((utxo, index) => createOutput(index, utxo.value, utxo.address, utxo.token, utxo.timelock || null, utxo.locked)); + await addUtxos(mysql, txId, outputs); + for (const [index, output] of outputs.entries()) { + const { token, decoded, value } = output; + await expect(checkUtxoTable(mysql, 3, txId, index, token, decoded?.address, value, 0, decoded?.timelock, null, output.locked)).resolves.toBe(true); + } + + const inputs = utxos.map((utxo, index) => createEventTxInput(utxo.value, utxo.address, txId, index, utxo.timelock)); + const results = await getLockedUtxoFromInputs(mysql, inputs); + expect(results).toHaveLength(1); + expect(results[0].value).toBe(25); + }); +}); + +describe('address and wallet related tests', () => { + test('updateAddressTablesWithTx', async () => { + expect.hasAssertions(); + const address1 = 'address1'; + const address2 = 'address2'; + const token1 = 'token1'; + const token2 = 'token2'; + const token3 = 'token3'; + // we'll add address1 to the address table already, as if it had already received another transaction + await addToAddressTable(mysql, [ + { address: address1, index: null, walletId: null, transactions: 1 }, + ]); + + const txId1 = 'txId1'; + const timestamp1 = 10; + const addrMap1 = { + address1: TokenBalanceMap.fromStringMap({ + token1: { unlocked: 10, locked: 0 }, + token2: { unlocked: 7, locked: 0 }, + token3: { unlocked: 2, locked: 0, unlockedAuthorities: new Authorities(0b01) }, + }), + address2: TokenBalanceMap.fromStringMap({ token1: { unlocked: 8, locked: 0, unlockedAuthorities: new Authorities(0b01) } }), + }; + + await updateAddressTablesWithTx(mysql, txId1, timestamp1, addrMap1); + await expect(checkAddressTable(mysql, 2, address1, null, null, 2)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 2, address2, null, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address1, token1, 10, 0, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address1, token2, 7, 0, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address1, token3, 2, 0, null, 1, 0b01, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address2, token1, 8, 0, null, 1, 0b01, 0)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 4, address1, txId1, token1, 10, timestamp1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 4, address1, txId1, token2, 7, timestamp1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 4, address1, txId1, token3, 2, timestamp1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 4, address2, txId1, token1, 8, timestamp1)).resolves.toBe(true); + + // this tx removes an authority for address1,token3 + const txId2 = 'txId2'; + const timestamp2 = 15; + const addrMap2 = { + address1: TokenBalanceMap.fromStringMap({ token1: { unlocked: -5, locked: 0 }, + token3: { unlocked: 6, locked: 0, unlockedAuthorities: new Authorities([-1]) } }), + address2: TokenBalanceMap.fromStringMap({ token1: { unlocked: 8, locked: 0, unlockedAuthorities: new Authorities(0b10) }, + token2: { unlocked: 3, locked: 0 } }), + }; + + await updateAddressTablesWithTx(mysql, txId2, timestamp2, addrMap2); + await expect(checkAddressTable(mysql, 2, address1, null, null, 3)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 2, address2, null, null, 2)).resolves.toBe(true); + // final balance for each (address,token) + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5, 0, null, 2)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token2', 7, 0, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token3', 8, 0, null, 2, 0, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address2, 'token1', 16, 0, null, 2, 0b11, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address2, 'token2', 3, 0, null, 1)).resolves.toBe(true); + // tx history + await expect(checkAddressTxHistoryTable(mysql, 8, address1, txId2, token1, -5, timestamp2)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, address1, txId2, token3, 6, timestamp2)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, address2, txId2, token1, 8, timestamp2)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, address2, txId2, token2, 3, timestamp2)).resolves.toBe(true); + // make sure entries in address_tx_history from txId1 haven't been changed + await expect(checkAddressTxHistoryTable(mysql, 8, address1, txId1, token1, 10, timestamp1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, address1, txId1, token2, 7, timestamp1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, address1, txId1, token3, 2, timestamp1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, address2, txId1, token1, 8, timestamp1)).resolves.toBe(true); + + // a tx with timelock + const txId3 = 'txId3'; + const timestamp3 = 20; + const lockExpires = 5000; + const addrMap3 = { + address1: TokenBalanceMap.fromStringMap({ token1: { unlocked: 0, locked: 3, lockExpires } }), + }; + await updateAddressTablesWithTx(mysql, txId3, timestamp3, addrMap3); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5, 3, lockExpires, 3)).resolves.toBe(true); + + // another tx, with higher timelock + const txId4 = 'txId4'; + const timestamp4 = 25; + const addrMap4 = { + address1: TokenBalanceMap.fromStringMap({ token1: { unlocked: 0, locked: 2, lockExpires: lockExpires + 1 } }), + }; + await updateAddressTablesWithTx(mysql, txId4, timestamp4, addrMap4); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5, 5, lockExpires, 4)).resolves.toBe(true); + + // another tx, with lower timelock + const txId5 = 'txId5'; + const timestamp5 = 25; + const addrMap5 = { + address1: TokenBalanceMap.fromStringMap({ token1: { unlocked: 0, locked: 2, lockExpires: lockExpires - 1 } }), + }; + await updateAddressTablesWithTx(mysql, txId5, timestamp5, addrMap5); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5, 7, lockExpires - 1, 5)).resolves.toBe(true); + }); + + test('updateAddressLockedBalance', async () => { + expect.hasAssertions(); + + const addr1 = 'address1'; + const addr2 = 'address2'; + const tokenId = 'tokenId'; + const otherToken = 'otherToken'; + const entries = [ + [addr1, tokenId, 50, 20, null, 3, 0, 0b01, 70], + [addr2, tokenId, 0, 5, null, 1, 0, 0, 10], + [addr1, otherToken, 5, 5, null, 1, 0, 0, 10], + ]; + await addToAddressBalanceTable(mysql, entries); + + const addr1Map = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 10, locked: 0, unlockedAuthorities: new Authorities(0b01) } }); + const addr2Map = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 5, locked: 0 } }); + await updateAddressLockedBalance(mysql, { [addr1]: addr1Map, [addr2]: addr2Map }); + await expect(checkAddressBalanceTable(mysql, 3, addr1, tokenId, 60, 10, null, 3, 0b01, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 3, addr2, tokenId, 5, 0, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 3, addr1, otherToken, 5, 5, null, 1)).resolves.toBe(true); + + // now pretend there's another locked authority, so final balance of locked authorities should be updated accordingly + await addToUtxoTable(mysql, [{ + txId: 'txId', + index: 0, + tokenId, + address: addr1, + value: 0, + authorities: 0b01, + timelock: 10000, + heightlock: null, + locked: true, + spentBy: null, + }]); + const newMap = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 0, locked: 0, unlockedAuthorities: new Authorities(0b10) } }); + await updateAddressLockedBalance(mysql, { [addr1]: newMap }); + await expect(checkAddressBalanceTable(mysql, 3, addr1, tokenId, 60, 10, null, 3, 0b11, 0b01)).resolves.toBe(true); + }); + + test('updateWalletLockedBalance', async () => { + expect.hasAssertions(); + + const wallet1 = 'wallet1'; + const wallet2 = 'wallet2'; + const tokenId = 'tokenId'; + const otherToken = 'otherToken'; + const now = 1000; + + const entries = [{ + walletId: wallet1, + tokenId, + unlockedBalance: 10, + lockedBalance: 20, + unlockedAuthorities: 0b01, + lockedAuthorities: 0, + timelockExpires: now, + transactions: 5, + }, { + walletId: wallet2, + tokenId, + unlockedBalance: 0, + lockedBalance: 100, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: now, + transactions: 4, + }, { + walletId: wallet1, + tokenId: otherToken, + unlockedBalance: 1, + lockedBalance: 2, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]; + await addToWalletBalanceTable(mysql, entries); + + const wallet1Map = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 15, locked: 0, unlockedAuthorities: new Authorities(0b11) } }); + const wallet2Map = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 50, locked: 0 } }); + await updateWalletLockedBalance(mysql, { [wallet1]: wallet1Map, [wallet2]: wallet2Map }); + await expect(checkWalletBalanceTable(mysql, 3, wallet1, tokenId, 25, 5, now, 5, 0b11, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, wallet2, tokenId, 50, 50, now, 4)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, wallet1, otherToken, 1, 2, null, 1)).resolves.toBe(true); + + // now pretend there's another locked authority, so final balance of locked authorities should be updated accordingly + await addToAddressTable(mysql, [{ + address: 'address1', + index: 0, + walletId: wallet1, + transactions: 1, + }]); + await addToAddressBalanceTable(mysql, [['address1', tokenId, 0, 0, null, 1, 0, 0b01, 0]]); + const newMap = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 0, locked: 0, unlockedAuthorities: new Authorities(0b10) } }); + await updateWalletLockedBalance(mysql, { [wallet1]: newMap }); + await expect(checkWalletBalanceTable(mysql, 3, wallet1, tokenId, 25, 5, now, 5, 0b11, 0b01)).resolves.toBe(true); + }); + + test('getAddressWalletInfo', async () => { + expect.hasAssertions(); + const wallet1 = { walletId: 'wallet1', xpubkey: 'xpubkey1', authXpubkey: 'authXpubkey', maxGap: 5 }; + const wallet2 = { walletId: 'wallet2', xpubkey: 'xpubkey2', authXpubkey: 'authXpubkey2', maxGap: 5 }; + const finalMap = { + addr1: wallet1, + addr2: wallet1, + addr3: wallet2, + }; + + // populate address table + for (const [address, wallet] of Object.entries(finalMap)) { + await addToAddressTable(mysql, [{ + address, + index: 0, + walletId: wallet.walletId, + transactions: 0, + }]); + } + // add address that won't be requested on walletAddressMap + await addToAddressTable(mysql, [{ + address: 'addr4', + index: 0, + walletId: 'wallet3', + transactions: 0, + }]); + + // populate wallet table + for (const wallet of Object.values(finalMap)) { + const entry = { + id: wallet.walletId, + xpubkey: wallet.xpubkey, + auth_xpubkey: wallet.authXpubkey, + status: WalletStatus.READY, + max_gap: wallet.maxGap, + created_at: 0, + ready_at: 0, + }; + await mysql.query('INSERT INTO `wallet` SET ? ON DUPLICATE KEY UPDATE id=id', [entry]); + } + // add wallet that should not be on the results + await addToWalletTable(mysql, [{ + id: 'wallet3', + xpubkey: 'xpubkey3', + authXpubkey: 'authxpubkey3', + status: WalletStatus.READY, + maxGap: 5, + createdAt: 0, + readyAt: 0, + }]); + + const addressWalletMap = await getAddressWalletInfo(mysql, Object.keys(finalMap)); + expect(addressWalletMap).toStrictEqual(finalMap); + }); + + test('updateWalletLockedBalance', async () => { + expect.hasAssertions(); + + const wallet1 = 'wallet1'; + const wallet2 = 'wallet2'; + const tokenId = 'tokenId'; + const otherToken = 'otherToken'; + const now = 1000; + + const entries = [{ + walletId: wallet1, + tokenId, + unlockedBalance: 10, + lockedBalance: 20, + unlockedAuthorities: 0b01, + lockedAuthorities: 0, + timelockExpires: now, + transactions: 5, + }, { + walletId: wallet2, + tokenId, + unlockedBalance: 0, + lockedBalance: 100, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: now, + transactions: 4, + }, { + walletId: wallet1, + tokenId: otherToken, + unlockedBalance: 1, + lockedBalance: 2, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]; + await addToWalletBalanceTable(mysql, entries); + + const wallet1Map = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 15, locked: 0, unlockedAuthorities: new Authorities(0b11) } }); + const wallet2Map = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 50, locked: 0 } }); + await updateWalletLockedBalance(mysql, { [wallet1]: wallet1Map, [wallet2]: wallet2Map }); + await expect(checkWalletBalanceTable(mysql, 3, wallet1, tokenId, 25, 5, now, 5, 0b11, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, wallet2, tokenId, 50, 50, now, 4)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, wallet1, otherToken, 1, 2, null, 1)).resolves.toBe(true); + + // now pretend there's another locked authority, so final balance of locked authorities should be updated accordingly + await addToAddressTable(mysql, [{ + address: 'address1', + index: 0, + walletId: wallet1, + transactions: 1, + }]); + await addToAddressBalanceTable(mysql, [['address1', tokenId, 0, 0, null, 1, 0, 0b01, 0]]); + const newMap = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 0, locked: 0, unlockedAuthorities: new Authorities(0b10) } }); + await updateWalletLockedBalance(mysql, { [wallet1]: newMap }); + await expect(checkWalletBalanceTable(mysql, 3, wallet1, tokenId, 25, 5, now, 5, 0b11, 0b01)).resolves.toBe(true); + }); + + test('generateAddresses', async () => { + expect.hasAssertions(); + const maxGap = 5; + const address0 = ADDRESSES[0]; + + // check first with no addresses on database, so it should return only maxGap addresses + let addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); + + expect(addressesInfo.addresses).toHaveLength(maxGap); + expect(addressesInfo.existingAddresses).toStrictEqual({}); + expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(maxGap); + expect(addressesInfo.addresses[0]).toBe(address0); + + // add first address with no transactions. As it's not used, we should still only generate maxGap addresses + await addToAddressTable(mysql, [{ + address: address0, + index: 0, + walletId: null, + transactions: 0, + }]); + addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); + expect(addressesInfo.addresses).toHaveLength(maxGap); + expect(addressesInfo.existingAddresses).toStrictEqual({ [address0]: 0 }); + expect(addressesInfo.lastUsedAddressIndex).toStrictEqual(-1); + + let totalLength = Object.keys(addressesInfo.addresses).length; + let existingLength = Object.keys(addressesInfo.existingAddresses).length; + expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(totalLength - existingLength); + expect(addressesInfo.addresses[0]).toBe(address0); + + // mark address as used and check again + let usedIndex = 0; + await mysql.query('UPDATE `address` SET `transactions` = ? WHERE `address` = ?', [1, address0]); + addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); + expect(addressesInfo.addresses).toHaveLength(maxGap + usedIndex + 1); + expect(addressesInfo.existingAddresses).toStrictEqual({ [address0]: 0 }); + expect(addressesInfo.lastUsedAddressIndex).toStrictEqual(0); + + totalLength = Object.keys(addressesInfo.addresses).length; + existingLength = Object.keys(addressesInfo.existingAddresses).length; + expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(totalLength - existingLength); + + // add address with index 1 as used + usedIndex = 1; + const address1 = ADDRESSES[1]; + await addToAddressTable(mysql, [{ + address: address1, + index: usedIndex, + walletId: null, + transactions: 1, + }]); + addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); + expect(addressesInfo.addresses).toHaveLength(maxGap + usedIndex + 1); + expect(addressesInfo.existingAddresses).toStrictEqual({ [address0]: 0, [address1]: 1 }); + expect(addressesInfo.lastUsedAddressIndex).toStrictEqual(1); + totalLength = Object.keys(addressesInfo.addresses).length; + existingLength = Object.keys(addressesInfo.existingAddresses).length; + expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(totalLength - existingLength); + + // add address with index 4 as used + usedIndex = 4; + const address4 = ADDRESSES[4]; + await addToAddressTable(mysql, [{ + address: address4, + index: usedIndex, + walletId: null, + transactions: 1, + }]); + addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); + expect(addressesInfo.addresses).toHaveLength(maxGap + usedIndex + 1); + expect(addressesInfo.existingAddresses).toStrictEqual({ [address0]: 0, [address1]: 1, [address4]: 4 }); + expect(addressesInfo.lastUsedAddressIndex).toStrictEqual(4); + totalLength = Object.keys(addressesInfo.addresses).length; + existingLength = Object.keys(addressesInfo.existingAddresses).length; + expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(totalLength - existingLength); + + // make sure no address was skipped from being generated + for (const [index, address] of addressesInfo.addresses.entries()) { + expect(ADDRESSES[index]).toBe(address); + } + }, 15000); + + test('addNewAddresses', async () => { + expect.hasAssertions(); + const walletId = 'walletId'; + + const addrMap: StringMap = {}; + for (const [index, address] of ADDRESSES.entries()) { + addrMap[address] = index; + } + + // test adding empty dict + await addNewAddresses(mysql, walletId, {}, -1); + await expect(checkAddressTable(mysql, 0)).resolves.toBe(true); + + // add some addresses + await addNewAddresses(mysql, walletId, addrMap, -1); + for (const [index, address] of ADDRESSES.entries()) { + await expect(checkAddressTable(mysql, ADDRESSES.length, address, index, walletId, 0)).resolves.toBe(true); + } + }); + + test('updateWalletTablesWithTx', async () => { + expect.hasAssertions(); + const walletId = 'walletId'; + const walletId2 = 'walletId2'; + const token1 = 'token1'; + const token2 = 'token2'; + const tx1 = 'txId1'; + const tx2 = 'txId2'; + const tx3 = 'txId3'; + const ts1 = 10; + const ts2 = 20; + const ts3 = 30; + + await addToAddressTable(mysql, [ + { address: 'addr1', index: 0, walletId, transactions: 1 }, + { address: 'addr2', index: 1, walletId, transactions: 1 }, + { address: 'addr3', index: 2, walletId, transactions: 1 }, + { address: 'addr4', index: 0, walletId: walletId2, transactions: 1 }, + ]); + + // add tx1 + const walletBalanceMap1 = { + walletId: TokenBalanceMap.fromStringMap({ token1: { unlocked: 5, locked: 0, unlockedAuthorities: new Authorities(0b01) } }), + }; + await updateWalletTablesWithTx(mysql, tx1, ts1, walletBalanceMap1); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token1, 5, 0, null, 1, 0b01, 0)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 1, walletId, token1, tx1, 5, ts1)).resolves.toBe(true); + + // add tx2 + const walletBalanceMap2 = { + walletId: TokenBalanceMap.fromStringMap( + { + token1: { unlocked: -2, locked: 1, lockExpires: 500, unlockedAuthorities: new Authorities(0b11) }, + token2: { unlocked: 7, locked: 0 }, + }, + ), + }; + await updateWalletTablesWithTx(mysql, tx2, ts2, walletBalanceMap2); + await expect(checkWalletBalanceTable(mysql, 2, walletId, token1, 3, 1, 500, 2, 0b11, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 2, walletId, token2, 7, 0, null, 1)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 3, walletId, token1, tx1, 5, ts1)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 3, walletId, token1, tx2, -1, ts2)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 3, walletId, token2, tx2, 7, ts2)).resolves.toBe(true); + + // add tx3 + const walletBalanceMap3 = { + walletId: TokenBalanceMap.fromStringMap({ token1: { unlocked: 1, locked: 2, lockExpires: 200, unlockedAuthorities: new Authorities([-1, -1]) } }), + walletId2: TokenBalanceMap.fromStringMap({ token2: { unlocked: 10, locked: 0 } }), + }; + // the tx above removes an authority, which will trigger a "refresh" on the available authorities. + // Let's pretend there's another utxo with some authorities as well + await addToAddressTable(mysql, [{ + address: 'address1', + index: 0, + walletId, + transactions: 1, + }]); + await addToAddressBalanceTable(mysql, [['address1', token1, 0, 0, null, 1, 0b10, 0, 0]]); + + await updateWalletTablesWithTx(mysql, tx3, ts3, walletBalanceMap3); + await expect(checkWalletBalanceTable(mysql, 3, walletId, token1, 4, 3, 200, 3, 0b10, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, walletId, token2, 7, 0, null, 1)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, walletId2, token2, 10, 0, null, 1)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token1, tx1, 5, ts1)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token1, tx2, -1, ts2)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token2, tx2, 7, ts2)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token1, tx3, 3, ts3)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId2, token2, tx3, 10, ts3)).resolves.toBe(true); + }); + + test('fetchAddressBalance', async () => { + expect.hasAssertions(); + + const addr1 = 'addr1'; + const addr2 = 'addr2'; + const addr3 = 'addr3'; + const token1 = 'token1'; + const token2 = 'token2'; + const timelock = 500; + + const addressEntries = [ + // address, tokenId, unlocked, locked, lockExpires, transactions + [addr1, token1, 2, 0, null, 2, 0, 0, 4], + [addr1, token2, 1, 4, timelock, 1, 0, 0, 5], + [addr2, token1, 5, 2, null, 2, 0, 0, 10], + [addr2, token2, 0, 2, null, 1, 0, 0, 2], + [addr3, token1, 0, 1, null, 1, 0, 0, 1], + [addr3, token2, 10, 1, null, 1, 0, 0, 11], + ]; + + await addToAddressBalanceTable(mysql, addressEntries); + + const addressBalances = await fetchAddressBalance(mysql, [addr1, addr2, addr3]); + + expect(addressBalances[0].address).toStrictEqual('addr1'); + expect(addressBalances[0].tokenId).toStrictEqual('token1'); + expect(addressBalances[0].unlockedBalance).toStrictEqual(2); + expect(addressBalances[0].lockedBalance).toStrictEqual(0); + expect(addressBalances[1].address).toStrictEqual('addr1'); + expect(addressBalances[1].tokenId).toStrictEqual('token2'); + expect(addressBalances[1].unlockedBalance).toStrictEqual(1); + expect(addressBalances[1].lockedBalance).toStrictEqual(4); + + expect(addressBalances[2].address).toStrictEqual('addr2'); + expect(addressBalances[2].tokenId).toStrictEqual('token1'); + expect(addressBalances[2].unlockedBalance).toStrictEqual(5); + expect(addressBalances[2].lockedBalance).toStrictEqual(2); + expect(addressBalances[3].address).toStrictEqual('addr2'); + expect(addressBalances[3].tokenId).toStrictEqual('token2'); + expect(addressBalances[3].unlockedBalance).toStrictEqual(0); + expect(addressBalances[3].lockedBalance).toStrictEqual(2); + + expect(addressBalances[4].address).toStrictEqual('addr3'); + expect(addressBalances[4].tokenId).toStrictEqual('token1'); + expect(addressBalances[4].unlockedBalance).toStrictEqual(0); + expect(addressBalances[4].lockedBalance).toStrictEqual(1); + expect(addressBalances[5].address).toStrictEqual('addr3'); + expect(addressBalances[5].tokenId).toStrictEqual('token2'); + expect(addressBalances[5].unlockedBalance).toStrictEqual(10); + expect(addressBalances[5].lockedBalance).toStrictEqual(1); + }); + + test('fetchAddressTxHistorySum', async () => { + expect.hasAssertions(); + + const addr1 = 'addr1'; + const addr2 = 'addr2'; + const token1 = 'token1'; + const token2 = 'token2'; + const txId1 = 'txId1'; + const txId2 = 'txId2'; + const txId3 = 'txId3'; + const timestamp1 = 10; + const timestamp2 = 20; + const entries = [ + { address: addr1, txId: txId1, tokenId: token1, balance: 10, timestamp: timestamp1 }, + { address: addr1, txId: txId2, tokenId: token1, balance: 20, timestamp: timestamp2 }, + { address: addr1, txId: txId3, tokenId: token1, balance: 30, timestamp: timestamp2 }, + // total: 60 + { address: addr2, txId: txId1, tokenId: token2, balance: 20, timestamp: timestamp1 }, + { address: addr2, txId: txId2, tokenId: token2, balance: 20, timestamp: timestamp2 }, + { address: addr2, txId: txId3, tokenId: token2, balance: 10, timestamp: timestamp2 }, + // total: 50 + ]; + + await addToAddressTxHistoryTable(mysql, entries); + + const history = await fetchAddressTxHistorySum(mysql, [addr1, addr2]); + + expect(history[0].balance).toStrictEqual(60); + expect(history[1].balance).toStrictEqual(50); + }); +}); + +describe('miner list', () => { + test('getMinersList', async () => { + expect.hasAssertions(); + + await addMiner(mysql, 'address1', 'txId1'); + await addMiner(mysql, 'address2', 'txId2'); + await addMiner(mysql, 'address3', 'txId3'); + + let results = await getMinersList(mysql); + + expect(results).toHaveLength(3); + expect(new Set(results)).toStrictEqual(new Set([ + { address: 'address1', firstBlock: 'txId1', lastBlock: 'txId1', count: 1 }, + { address: 'address2', firstBlock: 'txId2', lastBlock: 'txId2', count: 1 }, + { address: 'address3', firstBlock: 'txId3', lastBlock: 'txId3', count: 1 }, + ])); + + await addMiner(mysql, 'address3', 'txId4'); + await addMiner(mysql, 'address3', 'txId5'); + + results = await getMinersList(mysql); + + expect(results).toHaveLength(3); + + expect(new Set(results)).toStrictEqual(new Set([ + { address: 'address1', firstBlock: 'txId1', lastBlock: 'txId1', count: 1 }, + { address: 'address2', firstBlock: 'txId2', lastBlock: 'txId2', count: 1 }, + { address: 'address3', firstBlock: 'txId3', lastBlock: 'txId5', count: 3 }, + ])); + }); +}); + +describe('token methods', () => { + test('storeTokenInformation and getTokenInformation', async () => { + expect.hasAssertions(); + + expect(await getTokenInformation(mysql, 'invalid')).toBeNull(); + + const info = new TokenInfo('tokenId', 'tokenName', 'TKNS'); + storeTokenInformation(mysql, info.id, info.name, info.symbol); + + expect(info).toStrictEqual(await getTokenInformation(mysql, info.id)); + }); + + test('incrementTokensTxCount', async () => { + expect.hasAssertions(); + + const htr = new TokenInfo('00', 'Hathor', 'HTR', 5); + const token1 = new TokenInfo('token1', 'MyToken1', 'MT1', 10); + const token2 = new TokenInfo('token2', 'MyToken2', 'MT2', 15); + + await addToTokenTable(mysql, [ + { id: htr.id, name: htr.name, symbol: htr.symbol, transactions: htr.transactions }, + { id: token1.id, name: token1.name, symbol: token1.symbol, transactions: token1.transactions }, + { id: token2.id, name: token2.name, symbol: token2.symbol, transactions: token2.transactions }, + ]); + + await incrementTokensTxCount(mysql, ['token1', '00', 'token2']); + + await expect(checkTokenTable(mysql, 3, [{ + tokenId: token1.id, + tokenSymbol: token1.symbol, + tokenName: token1.name, + transactions: token1.transactions + 1, + }, { + tokenId: token2.id, + tokenSymbol: token2.symbol, + tokenName: token2.name, + transactions: token2.transactions + 1, + }, { + tokenId: htr.id, + tokenSymbol: htr.symbol, + tokenName: htr.name, + transactions: htr.transactions + 1, + }])).resolves.toBe(true); + }); +}); + +describe('sync metadata', () => { + test('updateLastSyncedEvent, getLastSyncedEvent', async () => { + expect.hasAssertions(); + + await expect(updateLastSyncedEvent(mysql, 5)).resolves.not.toThrow(); + const lastSyncedEvent = await getLastSyncedEvent(mysql); + expect(lastSyncedEvent?.last_event_id).toStrictEqual(5); + }); +}); + +// TODO: This test is duplicated from the wallet-service package, we should +// have methods shared between the two projects +describe('getTokenSymbols', () => { + it('should return a map of token symbol by token id', async () => { + expect.hasAssertions(); + + const tokensToPersist = [ + new TokenInfo('token1', 'tokenName1', 'TKN1'), + new TokenInfo('token2', 'tokenName2', 'TKN2'), + new TokenInfo('token3', 'tokenName3', 'TKN3'), + new TokenInfo('token4', 'tokenName4', 'TKN4'), + new TokenInfo('token5', 'tokenName5', 'TKN5'), + ]; + + // persist tokens + for (const eachToken of tokensToPersist) { + await storeTokenInformation(mysql, eachToken.id, eachToken.name, eachToken.symbol); + } + + const tokenIdList = tokensToPersist.map((each: TokenInfo) => each.id); + const tokenSymbolMap = await getTokenSymbols(mysql, tokenIdList); + + expect(tokenSymbolMap).toStrictEqual({ + token1: 'TKN1', + token2: 'TKN2', + token3: 'TKN3', + token4: 'TKN4', + token5: 'TKN5', + }); + }); + + it('should return null when no token is found', async () => { + expect.hasAssertions(); + + const tokensToPersist = [ + new TokenInfo('token1', 'tokenName1', 'TKN1'), + new TokenInfo('token2', 'tokenName2', 'TKN2'), + new TokenInfo('token3', 'tokenName3', 'TKN3'), + new TokenInfo('token4', 'tokenName4', 'TKN4'), + new TokenInfo('token5', 'tokenName5', 'TKN5'), + ]; + + // no token persistence + + let tokenIdList = tokensToPersist.map((each: TokenInfo) => each.id); + let tokenSymbolMap = await getTokenSymbols(mysql, tokenIdList); + + expect(tokenSymbolMap).toBeNull(); + + tokenIdList = []; + tokenSymbolMap = await getTokenSymbols(mysql, tokenIdList); + + expect(tokenSymbolMap).toBeNull(); + }); +}); diff --git a/packages/daemon/__tests__/guards/guards.test.ts b/packages/daemon/__tests__/guards/guards.test.ts new file mode 100644 index 00000000..2726888c --- /dev/null +++ b/packages/daemon/__tests__/guards/guards.test.ts @@ -0,0 +1,235 @@ +import { Context, Event, FullNodeEventTypes } from '../../src/types'; +import { + metadataIgnore, + metadataVoided, + metadataNewTx, + metadataFirstBlock, + metadataChanged, + vertexAccepted, + invalidPeerId, + invalidStreamId, + websocketDisconnected, + voided, + unchanged, + invalidNetwork, +} from '../../src/guards'; +import { EventTypes } from '../../src/types'; + +jest.mock('../../src/utils', () => ({ + hashTxData: jest.fn(), +})); + +import { hashTxData } from '../../src/utils'; + +jest.mock('../../src/config', () => { + return { + __esModule: true, // This property is needed for mocking a default export + default: jest.fn(() => ({})), + }; +}); + +import getConfig from '../../src/config'; + +const TxCache = { + get: jest.fn(), + set: jest.fn(), +}; + +const mockContext: Context = { + socket: null, + retryAttempt: 0, + // @ts-ignore + event: {}, + initialEventId: null, + // @ts-ignore + txCache: TxCache, +}; + +const generateFullNodeEvent = (type: FullNodeEventTypes, data = {} as any): Event => ({ + type: EventTypes.FULLNODE_EVENT, + event: { + type: 'EVENT', + network: 'mainnet', + peer_id: '', + stream_id: '', + event: { + id: 0, + timestamp: 0, + type, + data, + }, + latest_event_id: 0, + }, +}); + +const generateMetadataDecidedEvent = (type: string): Event => ({ + type: EventTypes.METADATA_DECIDED, + event: { + type, + // @ts-ignore + originalEvent: {} as any, + }, +}); + +describe('metadata decided tests', () => { + test('metadataIgnore', async () => { + expect(metadataIgnore(mockContext, generateMetadataDecidedEvent('IGNORE'))).toBe(true); + expect(metadataIgnore(mockContext, generateMetadataDecidedEvent('TX_NEW'))).toBe(false); + expect(metadataIgnore(mockContext, generateMetadataDecidedEvent('TX_VOIDED'))).toBe(false); + expect(metadataIgnore(mockContext, generateMetadataDecidedEvent('TX_FIRST_BLOCK'))).toBe(false); + + // Any event other than METADATA_DECIDED should throw an error: + expect(() => metadataIgnore(mockContext, generateFullNodeEvent(FullNodeEventTypes.VERTEX_METADATA_CHANGED))).toThrow('Invalid event type on metadataIgnore guard: FULLNODE_EVENT'); + }); + + test('metadataVoided', () => { + expect(metadataVoided(mockContext, generateMetadataDecidedEvent('TX_VOIDED'))).toBe(true); + expect(metadataVoided(mockContext, generateMetadataDecidedEvent('IGNORE'))).toBe(false); + expect(metadataVoided(mockContext, generateMetadataDecidedEvent('TX_NEW'))).toBe(false); + expect(metadataVoided(mockContext, generateMetadataDecidedEvent('TX_FIRST_BLOCK'))).toBe(false); + + // Any event other than METADATA_DECIDED should return false: + expect(() => metadataIgnore(mockContext, generateFullNodeEvent(FullNodeEventTypes.VERTEX_METADATA_CHANGED))).toThrow('Invalid event type on metadataIgnore guard: FULLNODE_EVENT'); + }); + + test('metadataNewTx', () => { + expect(metadataNewTx(mockContext, generateMetadataDecidedEvent('TX_NEW'))).toBe(true); + expect(metadataNewTx(mockContext, generateMetadataDecidedEvent('TX_FIRST_BLOCK'))).toBe(false); + expect(metadataNewTx(mockContext, generateMetadataDecidedEvent('TX_VOIDED'))).toBe(false); + expect(metadataNewTx(mockContext, generateMetadataDecidedEvent('IGNORE'))).toBe(false); + + // Any event other than METADATA_DECIDED should return false: + expect(() => metadataIgnore(mockContext, generateFullNodeEvent(FullNodeEventTypes.VERTEX_METADATA_CHANGED))).toThrow('Invalid event type on metadataIgnore guard: FULLNODE_EVENT'); + }); + + test('metadataFirstBlock', () => { + expect(metadataFirstBlock(mockContext, generateMetadataDecidedEvent('TX_FIRST_BLOCK'))).toBe(true); + expect(metadataFirstBlock(mockContext, generateMetadataDecidedEvent('TX_VOIDED'))).toBe(false); + expect(metadataFirstBlock(mockContext, generateMetadataDecidedEvent('IGNORE'))).toBe(false); + expect(metadataFirstBlock(mockContext, generateMetadataDecidedEvent('TX_NEW'))).toBe(false); + + // Any event other than METADATA_DECIDED should return false: + expect(() => metadataIgnore(mockContext, generateFullNodeEvent(FullNodeEventTypes.VERTEX_METADATA_CHANGED))).toThrow('Invalid event type on metadataIgnore guard: FULLNODE_EVENT'); + }); +}); + +describe('fullnode event guards', () => { + test('vertexAccepted', () => { + expect(vertexAccepted(mockContext, generateFullNodeEvent(FullNodeEventTypes.NEW_VERTEX_ACCEPTED))).toBe(true); + expect(vertexAccepted(mockContext, generateFullNodeEvent(FullNodeEventTypes.VERTEX_METADATA_CHANGED))).toBe(false); + + // Any event other than FULLNODE_EVENT should return false + expect(() => vertexAccepted(mockContext, generateMetadataDecidedEvent('TX_NEW'))).toThrow('Invalid event type on vertexAccepted guard: METADATA_DECIDED'); + }); + + test('metadataChanged', () => { + expect(metadataChanged(mockContext, generateFullNodeEvent(FullNodeEventTypes.VERTEX_METADATA_CHANGED))).toBe(true); + expect(metadataChanged(mockContext, generateFullNodeEvent(FullNodeEventTypes.NEW_VERTEX_ACCEPTED))).toBe(false); + + // Any event other than FULLNODE_EVENT should return false + expect(() => metadataChanged(mockContext, generateMetadataDecidedEvent('IGNORE'))).toThrow('Invalid event type on metadataChanged guard: METADATA_DECIDED'); + }); + + test('voided', () => { + const fullNodeVoidedTxEvent = generateFullNodeEvent(FullNodeEventTypes.VERTEX_METADATA_CHANGED, { + hash: 'tx1', + metadata: { + voided_by: ['tx2'], + } + }); + const fullNodeNotVoidedEvent = generateFullNodeEvent(FullNodeEventTypes.VERTEX_METADATA_CHANGED, { + hash: 'tx1', + metadata: { + voided_by: [], + } + }); + + expect(voided(mockContext, fullNodeVoidedTxEvent)).toBe(true); + expect(voided(mockContext, fullNodeNotVoidedEvent)).toBe(false); + + // Any event other than FULLNODE_EVENT should return false + expect(() => voided(mockContext, generateMetadataDecidedEvent('TX_NEW'))).toThrow('Invalid event type on voided guard: METADATA_DECIDED'); + + // Any fullndode event other VERTEX_METADATA_CHANGED and NEW_VERTEX_ACCEPTED + // should return false + // @ts-ignore + expect(voided(mockContext, generateFullNodeEvent('SOMETHING_ELSE'))).toBe(false); + }); + + test('unchanged', () => { + const fullNodeEvent = generateFullNodeEvent(FullNodeEventTypes.VERTEX_METADATA_CHANGED); + + // @ts-ignore + TxCache.get.mockReturnValueOnce('mockedTxCache'); + // @ts-ignore + hashTxData.mockReturnValueOnce('mockedTxCache'); + + expect(unchanged(mockContext, fullNodeEvent)).toBe(true); + // Since I only mocked the return once, this should fail on next call: + expect(unchanged(mockContext, fullNodeEvent)).toBe(false); + + // Any event other than FULLNODE_EVENT should return false + expect(() => unchanged(mockContext, generateMetadataDecidedEvent('TX_NEW'))).toThrow('Invalid event type on unchanged guard: METADATA_DECIDED'); + }); +}); + +describe('fullnode validation guards', () => { + test('invalidStreamId', () => { + // @ts-ignore + getConfig.mockReturnValueOnce({ + STREAM_ID: 'mockStreamId', + }); + const fullNodeEvent = generateFullNodeEvent(FullNodeEventTypes.NEW_VERTEX_ACCEPTED); + // @ts-ignore + fullNodeEvent.event.stream_id = 'mockStreamId'; + expect(invalidStreamId(mockContext, fullNodeEvent)).toBe(false); + // @ts-ignore + fullNodeEvent.event.stream_id = 'invalidStreamId'; + expect(invalidStreamId(mockContext, fullNodeEvent)).toBe(true); + }); + + test('invalidNetwork', () => { + // @ts-ignore + getConfig.mockReturnValue({ + FULLNODE_NETWORK: 'mainnet', + }); + const fullNodeEvent = generateFullNodeEvent(FullNodeEventTypes.NEW_VERTEX_ACCEPTED); + // @ts-ignore + fullNodeEvent.event.network = 'mainnet'; + expect(invalidNetwork(mockContext, fullNodeEvent)).toBe(false); + // @ts-ignore + fullNodeEvent.event.network = 'testnet'; + expect(invalidNetwork(mockContext, fullNodeEvent)).toBe(true); + }); + + test('invalidPeerId', () => { + // @ts-ignore + getConfig.mockReturnValueOnce({ + FULLNODE_PEER_ID: 'mockPeerId', + }); + + const fullNodeEvent = generateFullNodeEvent(FullNodeEventTypes.NEW_VERTEX_ACCEPTED); + // @ts-ignore + fullNodeEvent.event.peer_id = 'mockPeerId'; + expect(invalidPeerId(mockContext, fullNodeEvent)).toBe(false); + // @ts-ignore + fullNodeEvent.event.peer_id = 'invalidPeerId'; + expect(invalidPeerId(mockContext, fullNodeEvent)).toBe(true); + }); +}); + +describe('websocket guards', () => { + test('websocketDisconnected', () => { + const mockDisconnectedEvent: Event = { + type: EventTypes.WEBSOCKET_EVENT, + event: { type: 'DISCONNECTED' } + }; + const mockConnectedEvent: Event = { + type: EventTypes.WEBSOCKET_EVENT, + event: { type: 'CONNECTED' } + }; + + expect(websocketDisconnected(mockContext, mockDisconnectedEvent)).toBe(true); + expect(websocketDisconnected(mockContext, mockConnectedEvent)).toBe(false); + }); +}); diff --git a/packages/daemon/__tests__/integration/balances.test.ts b/packages/daemon/__tests__/integration/balances.test.ts new file mode 100644 index 00000000..cc32fa70 --- /dev/null +++ b/packages/daemon/__tests__/integration/balances.test.ts @@ -0,0 +1,218 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import * as Services from '../../src/services'; +import { SyncMachine } from '../../src/machines'; +import { interpret } from 'xstate'; +import { getLastSyncedEvent, getDbConnection } from '../../src/db'; +import { Connection } from 'mysql2/promise'; +import { cleanDatabase, fetchAddressBalances, validateBalances } from './utils'; +import unvoidedScenarioBalances from './scenario_configs/unvoided_transactions.balances'; +import reorgScenarioBalances from './scenario_configs/reorg.balances'; +import singleChainBlocksAndTransactionsBalances from './scenario_configs/single_chain_blocks_and_transactions.balances'; +import { + DB_NAME, + DB_USER, + DB_PORT, + DB_PASS, + DB_ENDPOINT, + UNVOIDED_SCENARIO_PORT, + UNVOIDED_SCENARIO_LAST_EVENT, + REORG_SCENARIO_PORT, + REORG_SCENARIO_LAST_EVENT, + SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_PORT, + SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT, +} from './config'; + +jest.mock('../../src/config', () => { + return { + __esModule: true, // This property is needed for mocking a default export + default: jest.fn(() => ({})), + }; +}); + +import getConfig from '../../src/config'; + +// @ts-ignore +getConfig.mockReturnValue({ + SERVICE_NAME: 'daemon-test', + CONSOLE_LEVEL: 'debug', + TX_CACHE_SIZE: 100, + BLOCK_REWARD_LOCK: 300, + FULLNODE_PEER_ID: 'simulator_peer_id', + STREAM_ID: 'simulator_stream_id', + FULLNODE_NETWORK: 'simulator_network', + FULLNODE_HOST: `127.0.0.1:${UNVOIDED_SCENARIO_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, +}); + +// Use a single mysql connection for all tests +let mysql: Connection; +beforeAll(async () => { + try { + mysql = await getDbConnection(); + } catch(e) { + console.error('Failed to establish db connection', e); + throw e; + } +}); + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + +describe('unvoided transaction scenario', () => { + beforeAll(() => { + jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('should do a full sync and the balances should match', async () => { + // @ts-ignore + getConfig.mockReturnValue({ + SERVICE_NAME: 'daemon-test', + CONSOLE_LEVEL: 'debug', + TX_CACHE_SIZE: 100, + BLOCK_REWARD_LOCK: 300, + FULLNODE_PEER_ID: 'simulator_peer_id', + STREAM_ID: 'simulator_stream_id', + FULLNODE_NETWORK: 'simulator_network', + FULLNODE_HOST: `127.0.0.1:${UNVOIDED_SCENARIO_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + }); + + const machine = interpret(SyncMachine); + + await new Promise((resolve) => { + machine.onTransition(async (state) => { + if (state.matches('CONNECTED.idle')) { + // @ts-ignore + const lastSyncedEvent = await getLastSyncedEvent(mysql); + if (lastSyncedEvent?.last_event_id === UNVOIDED_SCENARIO_LAST_EVENT) { + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, unvoidedScenarioBalances)); + + machine.stop(); + + resolve(); + } + } + }); + + machine.start(); + }); + }); +}); + +describe('reorg scenario', () => { + beforeAll(() => { + jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + }); + + it('should do a full sync and the balances should match', async () => { + // @ts-ignore + getConfig.mockReturnValue({ + SERVICE_NAME: 'daemon-test', + CONSOLE_LEVEL: 'debug', + TX_CACHE_SIZE: 100, + BLOCK_REWARD_LOCK: 300, + FULLNODE_PEER_ID: 'simulator_peer_id', + STREAM_ID: 'simulator_stream_id', + FULLNODE_NETWORK: 'simulator_network', + FULLNODE_HOST: `127.0.0.1:${REORG_SCENARIO_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + }); + + const machine = interpret(SyncMachine); + + await new Promise((resolve) => { + machine.onTransition(async (state) => { + if (state.matches('CONNECTED.idle')) { + // @ts-ignore + const lastSyncedEvent = await getLastSyncedEvent(mysql); + if (lastSyncedEvent?.last_event_id === REORG_SCENARIO_LAST_EVENT) { + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, reorgScenarioBalances)); + + machine.stop(); + + resolve(); + } + } + }); + + machine.start(); + }); + }); +}); + +describe('single chain blocks and transactions scenario', () => { + beforeAll(() => { + jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + }); + + it('should do a full sync and the balances should match', async () => { + // @ts-ignore + getConfig.mockReturnValue({ + SERVICE_NAME: 'daemon-test', + CONSOLE_LEVEL: 'debug', + TX_CACHE_SIZE: 100, + BLOCK_REWARD_LOCK: 300, + FULLNODE_PEER_ID: 'simulator_peer_id', + STREAM_ID: 'simulator_stream_id', + FULLNODE_NETWORK: 'simulator_network', + FULLNODE_HOST: `127.0.0.1:${SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + }); + + const machine = interpret(SyncMachine); + + await new Promise((resolve) => { + machine.onTransition(async (state) => { + if (state.matches('CONNECTED.idle')) { + // @ts-ignore + const lastSyncedEvent = await getLastSyncedEvent(mysql); + if (lastSyncedEvent?.last_event_id === SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT) { + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, singleChainBlocksAndTransactionsBalances)); + + machine.stop(); + + resolve(); + } + } + }); + + machine.start(); + }); + }); +}); diff --git a/packages/daemon/__tests__/integration/config.ts b/packages/daemon/__tests__/integration/config.ts new file mode 100644 index 00000000..73f49b41 --- /dev/null +++ b/packages/daemon/__tests__/integration/config.ts @@ -0,0 +1,26 @@ +// Db information +export const DB_USER = 'root'; +export const DB_PASS = 'hathor'; +export const DB_NAME = 'hathor'; +export const DB_PORT = 3380; +export const DB_ENDPOINT = '127.0.0.1'; + +// unvoided +export const UNVOIDED_SCENARIO_PORT = 8081; + +// Last event is actually 39, but event 39 is ignored by the machine (because +// the transaction is already added), and when we ignore an event, we don't store +// it in the database. +export const UNVOIDED_SCENARIO_LAST_EVENT = 38; + +// reorg +export const REORG_SCENARIO_PORT = 8082; +// Same as the comment on the unvoided scenario last event +export const REORG_SCENARIO_LAST_EVENT = 19; + +// single chain blocks and transactions port +export const SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_PORT = 8083; +// Same as the comment on the unvoided scenario last event +export const SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT = 37; + +export const SCENARIOS = ['UNVOIDED_SCENARIO', 'REORG_SCENARIO', 'SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS']; diff --git a/packages/daemon/__tests__/integration/scenario_configs/reorg.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/reorg.balances.ts new file mode 100644 index 00000000..09be940b --- /dev/null +++ b/packages/daemon/__tests__/integration/scenario_configs/reorg.balances.ts @@ -0,0 +1,6 @@ +export default { + "HFyF1jYJP9FXfiC3LRqf3q4768TBL1rxbn": 6400, + "HMbS5P3NTLQ5oR5TfLNvAkeQ7L8MPn9VM3": 6400, + "HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs": 0, + "HVayMofEDh4XGsaQJeRJKhutYxYodYNop6": 100000000000, +} diff --git a/packages/daemon/__tests__/integration/scenario_configs/single_chain_blocks_and_transactions.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/single_chain_blocks_and_transactions.balances.ts new file mode 100644 index 00000000..74184a92 --- /dev/null +++ b/packages/daemon/__tests__/integration/scenario_configs/single_chain_blocks_and_transactions.balances.ts @@ -0,0 +1,16 @@ +export default { + "HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh": 6400, + "HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns": 6400, + "HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ": 6400, + "HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu": 6400, + "HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW": 6400, + "HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26": 6400, + "HQijr325t63VJFdc4vYkaTyd87oeBLpSed": 6400, + "H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR": 6400, + "HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ": 6400, + "HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8": 6400, + "HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs": 3000, + "HRH8Wbmr1A3BrLswSBhvVE4hhsv4jUdyVA": 3400, + "HSd6PqXesUmHHv6MoN24aUiMuw7Pdcxrwk": 6400, + "HNqTfEASfdx7H4vMUGzfD2HyD3GeKuxjTJ": 0, +} diff --git a/packages/daemon/__tests__/integration/scenario_configs/unvoided_transactions.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/unvoided_transactions.balances.ts new file mode 100644 index 00000000..58e6059c --- /dev/null +++ b/packages/daemon/__tests__/integration/scenario_configs/unvoided_transactions.balances.ts @@ -0,0 +1,16 @@ +export default { + "HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh": 6400, + "HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns": 6400, + "HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ": 6400, + "HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu": 6400, + "HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW": 6400, + "HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26": 6400, + "HQijr325t63VJFdc4vYkaTyd87oeBLpSed": 6400, + "H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR": 6400, + "HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ": 6400, + "HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8": 6400, + "H9hHteu9QdAS5p6X743Mpfue6G19rV9GeY": 6400, + "HNqTfEASfdx7H4vMUGzfD2HyD3GeKuxjTJ": 5400, + "HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs": 1000, + "HVayMofEDh4XGsaQJeRJKhutYxYodYNop6": 100000000000 +} diff --git a/packages/daemon/__tests__/integration/scripts/docker-compose.yml b/packages/daemon/__tests__/integration/scripts/docker-compose.yml new file mode 100644 index 00000000..9262e1bb --- /dev/null +++ b/packages/daemon/__tests__/integration/scripts/docker-compose.yml @@ -0,0 +1,41 @@ +version: "3.9" + +services: + mysql: + image: mysql + networks: + - database + environment: + MYSQL_ROOT_PASSWORD: hathor + ports: + - "3380:3306" + unvoided_transaction: + image: hathornetwork/hathor-core:stable + command: [ + "events_simulator", + "--scenario", "UNVOIDED_TRANSACTION", + "--seed", "1" + ] + ports: + - "8081:8080" + reorg: + image: hathornetwork/hathor-core:stable + command: [ + "events_simulator", + "--scenario", "REORG", + "--seed", "1" + ] + ports: + - "8082:8080" + single_chain_blocks_and_transactions: + image: hathornetwork/hathor-core:stable + command: [ + "events_simulator", + "--scenario", "SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS", + "--seed", "1" + ] + ports: + - "8083:8080" + +networks: + database: diff --git a/packages/daemon/__tests__/integration/scripts/sequelize-config.js b/packages/daemon/__tests__/integration/scripts/sequelize-config.js new file mode 100644 index 00000000..89d382ba --- /dev/null +++ b/packages/daemon/__tests__/integration/scripts/sequelize-config.js @@ -0,0 +1,4 @@ +module.exports = { + 'config': './sequelize-db-config.js', + 'migrations-path': '../../../../db/migrations' +}; diff --git a/packages/daemon/__tests__/integration/scripts/sequelize-db-config.js b/packages/daemon/__tests__/integration/scripts/sequelize-db-config.js new file mode 100644 index 00000000..ee52475a --- /dev/null +++ b/packages/daemon/__tests__/integration/scripts/sequelize-db-config.js @@ -0,0 +1,13 @@ +module.exports = { + test: { + username: 'root', + password: 'hathor', + database: 'hathor', + host: '127.0.0.1', + port: 3380, + dialect: 'mysql', + dialectOptions: { + bigNumberStrings: true, + }, + }, +}; diff --git a/packages/daemon/__tests__/integration/scripts/setup-database.ts b/packages/daemon/__tests__/integration/scripts/setup-database.ts new file mode 100644 index 00000000..667ceec9 --- /dev/null +++ b/packages/daemon/__tests__/integration/scripts/setup-database.ts @@ -0,0 +1,23 @@ +import mysql from 'mysql2/promise'; +import { + DB_USER, + DB_PASS, + DB_PORT, + DB_NAME, + DB_ENDPOINT, +} from '../config'; + +const main = async () => { + const conn = await mysql.createConnection({ + host: DB_ENDPOINT, + user: DB_USER, + password: DB_PASS, + port: DB_PORT, + }) + + await conn.query(`CREATE DATABASE IF NOT EXISTS ${DB_NAME};`); + console.log('Database created successfully'); + process.exit(0); +}; + +main(); diff --git a/packages/daemon/__tests__/integration/scripts/wait-for-db-up.ts b/packages/daemon/__tests__/integration/scripts/wait-for-db-up.ts new file mode 100644 index 00000000..5af9d4d9 --- /dev/null +++ b/packages/daemon/__tests__/integration/scripts/wait-for-db-up.ts @@ -0,0 +1,43 @@ +import mysql from 'mysql2/promise'; +import { + DB_USER, + DB_PASS, + DB_PORT, + DB_ENDPOINT, +} from '../config'; + +const attemptConnection = async (maxAttempts: number, interval: number): Promise => { + let attempts = 0; + + while (attempts < maxAttempts) { + try { + const conn = await mysql.createConnection({ + host: DB_ENDPOINT, + user: DB_USER, + password: DB_PASS, + port: DB_PORT, + }); + await conn.query('SELECT 1'); + + console.log('Successfully connected to the database!'); + await conn.end(); + return; + } catch (err: any) { + console.error('Failed to connect to the database:', err.message); + attempts++; + if (attempts < maxAttempts) { + console.log(`Retrying connection... Attempt ${attempts} of ${maxAttempts}`); + await new Promise(resolve => setTimeout(resolve, interval)); + } + } + } + + throw new Error('Maximum connection attempts reached. Exiting.');; +}; + +// Attempt to connect +attemptConnection(10, 5000) // 10 attempts, 5 seconds interval + .catch((err) => { + console.log(err); + process.exit(1); + }); diff --git a/packages/daemon/__tests__/integration/scripts/wait-for-ws-up.ts b/packages/daemon/__tests__/integration/scripts/wait-for-ws-up.ts new file mode 100644 index 00000000..437722f6 --- /dev/null +++ b/packages/daemon/__tests__/integration/scripts/wait-for-ws-up.ts @@ -0,0 +1,66 @@ +import { WebSocket } from 'ws'; +import { SCENARIOS } from '../config'; +import * as config from '../config'; + +const attemptConnection = async (port: number, maxAttempts: number, interval: number): Promise => { + let attempts = 0; + while (attempts < maxAttempts) { + try { + await new Promise((resolve, reject) => { + // Create a new WebSocket connection + const client = new WebSocket(`ws://127.0.0.1:${port}/v1a/event_ws`); + + client.on('open', function open() { + // Start the stream + client.send(JSON.stringify({ + type: 'START_STREAM', + window_size: 1, + })); + }); + + client.on('message', (data) => { + const message = JSON.parse(data.toString()); + + if (message.event.type === 'LOAD_STARTED') { + client.close(); + resolve(null); + return; + } + + throw new Error('Unexpected response from websocket'); + }); + + // Event listener for handling errors + client.on('error', (err) => reject(err)); + }); + return; + } catch (err: any) { + console.error('Failed to connect to websocket:', err.message); + attempts++; + if (attempts < maxAttempts) { + console.log(`Retrying connection... Attempt ${attempts} of ${maxAttempts}`); + await new Promise(resolve => setTimeout(resolve, interval)); + } else { + console.error('Maximum connection attempts reached. Exiting.'); + throw err; + } + } + } +}; + +const main = async () => { + // We should test all scenarios + for (let i = 0; i < SCENARIOS.length; i++) { + try { + // @ts-ignore + const port = config[`${SCENARIOS[i]}_PORT`]; + // Attempt to connect + await attemptConnection(port, 30, 10000); + } catch (err) { + console.log(err); + process.exit(1); + } + } +} + +main(); diff --git a/packages/daemon/__tests__/integration/utils/index.ts b/packages/daemon/__tests__/integration/utils/index.ts new file mode 100644 index 00000000..836aa063 --- /dev/null +++ b/packages/daemon/__tests__/integration/utils/index.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import { Connection } from 'mysql2/promise'; +import { AddressBalance, AddressBalanceRow } from '../../../src/types'; + +export const cleanDatabase = async (mysql: Connection): Promise => { + const TABLES = [ + 'address', + 'address_balance', + 'address_tx_history', + 'miner', + 'sync_metadata', + 'token', + 'transaction', + 'tx_output', + 'tx_proposal', + 'version_data', + 'wallet', + 'wallet_balance', + 'wallet_tx_history', + 'push_devices', + ]; + await mysql.query('SET FOREIGN_KEY_CHECKS = 0'); + + for (const table of TABLES) { + await mysql.query(`DELETE FROM ${table}`); + } + + await mysql.query('SET FOREIGN_KEY_CHECKS = 1'); +}; + +export const fetchAddressBalances = async ( + mysql: Connection +): Promise => { + const [results] = await mysql.query( + `SELECT * + FROM \`address_balance\` + ORDER BY \`address\`, \`token_id\``, + ); + + return results.map((result): AddressBalance => ({ + address: result.address as string, + tokenId: result.token_id as string, + unlockedBalance: result.unlocked_balance as number, + lockedBalance: result.locked_balance as number, + lockedAuthorities: result.locked_authorities as number, + unlockedAuthorities: result.unlocked_authorities as number, + timelockExpires: result.timelock_expires as number, + transactions: result.transactions as number, + })); +}; + +export const validateBalances = async ( + balancesA: AddressBalance[], + balancesB: { string: number }, +): Promise => { + const length = Math.max(balancesA.length, Object.keys(balancesB).length); + + for (let i = 0; i < length; i++) { + const balanceA = balancesA[i]; + const address = balanceA.address; + // @ts-ignore + const balanceB = balancesB[address]; + const totalBalanceA = balanceA.lockedBalance + balanceA.unlockedBalance; + + if (totalBalanceA !== balanceB) { + console.log(totalBalanceA); + console.log(balanceB); + throw new Error(`Balances are not equal for address: ${address}`); + } + } +}; diff --git a/packages/daemon/__tests__/machines/SyncMachine.test.ts b/packages/daemon/__tests__/machines/SyncMachine.test.ts new file mode 100644 index 00000000..1fed2945 --- /dev/null +++ b/packages/daemon/__tests__/machines/SyncMachine.test.ts @@ -0,0 +1,541 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * @jest-environment node + */ + +import { + CONNECTED_STATES, + SyncMachine, + SYNC_MACHINE_STATES, +} from '../../src/machines'; +import { + invalidPeerId, + invalidStreamId, + unchanged, + voided, +} from '../../src/guards'; +import { LRU } from '../../src/utils'; +import EventFixtures from '../__fixtures__/events'; +import { FullNodeEvent, Event, Context, EventTypes } from '../../src/types'; +import { hashTxData } from '../../src/utils'; +import getConfig from '../../src/config'; + +const { TX_CACHE_SIZE, FULLNODE_PEER_ID, STREAM_ID } = getConfig(); +const { VERTEX_METADATA_CHANGED, NEW_VERTEX_ACCEPTED, REORG_STARTED } = EventFixtures; + + +const TxCache = new LRU(TX_CACHE_SIZE); + +beforeAll(async () => { + jest.clearAllMocks(); +}); + +afterAll(async () => { + TxCache.clear(); +}); + +// @ts-ignore +const untilIdle = (machine: Machine) => { + let currentState = machine.initialState; + + expect(currentState.matches(SYNC_MACHINE_STATES.INITIALIZING)).toBeTruthy(); + + currentState = machine.transition(currentState, { + // @ts-ignore + type: `done.invoke.SyncMachine.${SYNC_MACHINE_STATES.INITIALIZING}:invocation[0]`, + // @ts-ignore + data: { lastEventId: 999 }, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTING}`)).toBeTruthy(); + expect(currentState.context.initialEventId).toStrictEqual(999); + + currentState = machine.transition(currentState, { + type: EventTypes.WEBSOCKET_EVENT, + event: { type: 'CONNECTED' }, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.idle}`)).toBeTruthy(); + + return currentState; +}; + +describe('machine initialization', () => { + it('should fetch initial state, connect to websocket and validate network before transitioning to idle', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + actions: { + startStream: () => {}, + }, + }); + + let currentState = MockedFetchMachine.initialState; + + expect(currentState.matches(SYNC_MACHINE_STATES.INITIALIZING)).toBeTruthy(); + + currentState = MockedFetchMachine.transition(currentState, { + // @ts-ignore + type: `done.invoke.SyncMachine.${SYNC_MACHINE_STATES.INITIALIZING}:invocation[0]`, + // @ts-ignore + data: { lastEventId: 999 }, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTING}`)).toBeTruthy(); + expect(currentState.context.initialEventId).toStrictEqual(999); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.WEBSOCKET_EVENT, + event: { type: 'CONNECTED' }, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.idle}`)).toBeTruthy(); + }); + + it('should transition to RECONNECTING if the websocket fails to initialize', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + delays: { + RETRY_BACKOFF_INCREASE: 100, + }, + }); + + let currentState = MockedFetchMachine.initialState; + + expect(currentState.matches(SYNC_MACHINE_STATES.INITIALIZING)).toBeTruthy(); + + currentState = MockedFetchMachine.transition(currentState, { + // @ts-ignore + type: `done.invoke.SyncMachine.${SYNC_MACHINE_STATES.INITIALIZING}:invocation[0]`, + // @ts-ignore + data: { lastEventId: 999 }, + }); + + expect(currentState.matches(SYNC_MACHINE_STATES.CONNECTING)).toBeTruthy(); + expect(currentState.context.initialEventId).toStrictEqual(999); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.WEBSOCKET_EVENT, + event: { type: 'DISCONNECTED', + }}); + + expect(currentState.matches(SYNC_MACHINE_STATES.RECONNECTING)).toBeTruthy(); + }); + + it('should transition to CONNECTING to reconnect after a failure in the initial connection', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + delays: { + RETRY_BACKOFF_INCREASE: 100, + }, + }); + + let currentState = MockedFetchMachine.initialState; + + expect(currentState.matches(SYNC_MACHINE_STATES.INITIALIZING)).toBeTruthy(); + + currentState = MockedFetchMachine.transition(currentState, { + // @ts-ignore + type: `done.invoke.SyncMachine.${SYNC_MACHINE_STATES.INITIALIZING}:invocation[0]`, + // @ts-ignore + data: { lastEventId: 999 }, + }); + + expect(currentState.matches(SYNC_MACHINE_STATES.CONNECTING)).toBeTruthy(); + expect(currentState.context.initialEventId).toStrictEqual(999); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.WEBSOCKET_EVENT, + event: { type: 'DISCONNECTED', } + }); + + expect(currentState.matches(SYNC_MACHINE_STATES.RECONNECTING)).toBeTruthy(); + + currentState = MockedFetchMachine.transition(currentState, { + // @ts-ignore + type: `xstate.after(BACKOFF_DELAYED_RECONNECT)#SyncMachine.${SYNC_MACHINE_STATES.RECONNECTING}`, + }); + + expect(currentState.matches('CONNECTING')).toBeTruthy(); + }); + + it('should transition to RECONNECTING to reconnect after a failure', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + actions: { + startStream: () => {}, + }, + }); + + let currentState = MockedFetchMachine.initialState; + + expect(currentState.matches(SYNC_MACHINE_STATES.INITIALIZING)).toBeTruthy(); + + currentState = MockedFetchMachine.transition(currentState, { + // @ts-ignore + type: `done.invoke.SyncMachine.${SYNC_MACHINE_STATES.INITIALIZING}:invocation[0]`, + // @ts-ignore + data: { lastEventId: 999 }, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTING}`)).toBeTruthy(); + expect(currentState.context.initialEventId).toStrictEqual(999); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.WEBSOCKET_EVENT, + event: { type: 'CONNECTED' }, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.idle}`)).toBeTruthy(); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.WEBSOCKET_EVENT, + event: { + type: 'DISCONNECTED', + }, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.RECONNECTING}`)).toBeTruthy(); + }); +}); + +describe('Event handling', () => { + let originalFullNodePeerId: string | undefined; + let originalStreamId: string | undefined; + + beforeAll(() => { + originalFullNodePeerId = FULLNODE_PEER_ID; + originalStreamId = STREAM_ID; + }); + + afterEach(() => { + // Restore the original values after each test + process.env.FULLNODE_PEER_ID = originalFullNodePeerId; + process.env.STREAM_ID = originalStreamId; + }); + + it('should validate the peerid on every message', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + guards: { + invalidPeerId, + invalidStreamId: () => { + return false; + } + }, + }); + + let currentState = untilIdle(MockedFetchMachine); + + process.env.FULLNODE_PEER_ID = 'invalidPeerId'; + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + }); + + expect(currentState.matches(SYNC_MACHINE_STATES.ERROR)).toBeTruthy(); + }); + + it('should validate the stream id on every message', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + guards: { + invalidStreamId, + }, + }); + + let currentState = untilIdle(MockedFetchMachine); + + process.env.STREAM_ID = 'invalidStreamId'; + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + }); + + expect(currentState.matches(SYNC_MACHINE_STATES.ERROR)).toBeTruthy(); + }); + + it('should ignore already processed transactions', () => { + const unchangedMock = jest.fn(); + const sendAckMock = jest.fn(); + const MockedFetchMachine = SyncMachine.withConfig({ + actions: { + sendAck: sendAckMock, + }, + guards: { + invalidPeerId: () => false, + invalidStreamId: () => false, + invalidNetwork: () => false, + unchanged: unchangedMock, + }, + }).withContext({ + event: null, + socket: null, + healthcheck: null, + retryAttempt: 0, + initialEventId: 0, + txCache: TxCache, + }); + + unchangedMock.mockImplementation(unchanged); + + let currentState = untilIdle(MockedFetchMachine); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.idle}`)).toBeTruthy(); + + const hashedTx = hashTxData(VERTEX_METADATA_CHANGED.event.data.metadata); + TxCache.set(VERTEX_METADATA_CHANGED.event.data.hash, hashedTx); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + }); + + // Should still be in the idle state: + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.idle}`)).toBeTruthy(); + + // Should have called the unchanged guard + expect(unchangedMock).toHaveBeenCalledTimes(1); + expect(unchangedMock).toHaveReturnedWith(true); + + // @ts-ignore: last event id should be the event we sent + expect(currentState.context.event.event.id).toStrictEqual(VERTEX_METADATA_CHANGED.event.id); + + TxCache.clear(); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingMetadataChanged}`)).toBeTruthy(); + expect(unchangedMock).toHaveBeenCalledTimes(2); + expect(unchangedMock).toHaveReturnedWith(false); + // @ts-ignore + expect(currentState.context.event.event.id).toStrictEqual(VERTEX_METADATA_CHANGED.event.id); + }); + + it('should transition to handlingVoidedTx if TX_VOIDED action is received from diff detector', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + guards: { + invalidPeerId: () => false, + invalidStreamId: () => false, + invalidNetwork: () => false, + }, + }); + + let currentState = untilIdle(MockedFetchMachine); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingMetadataChanged}.detectingDiff`)).toBeTruthy(); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.METADATA_DECIDED, + event: { + type: 'TX_VOIDED', + originalEvent: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + }, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingVoidedTx}`)).toBeTruthy(); + }); + + it('should transition to handlingUnvoidedTx if TX_UNVOIDED action is received from diff detector', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + guards: { + invalidPeerId: () => false, + invalidStreamId: () => false, + invalidNetwork: () => false, + }, + }); + + let currentState = untilIdle(MockedFetchMachine); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingMetadataChanged}.detectingDiff`)).toBeTruthy(); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.METADATA_DECIDED, + event: { + type: 'TX_UNVOIDED', + originalEvent: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + }, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingUnvoidedTx}`)).toBeTruthy(); + }); + + it('should transition to handlingVertexAccepted if TX_NEW action is received from diff detector', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + guards: { + invalidPeerId: () => false, + invalidStreamId: () => false, + invalidNetwork: () => false, + }, + }); + + let currentState = untilIdle(MockedFetchMachine); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingMetadataChanged}.detectingDiff`)).toBeTruthy(); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.METADATA_DECIDED, + event: { + type: 'TX_NEW', + originalEvent: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + } + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingVertexAccepted}`)).toBeTruthy(); + }); + + it('should transition to handlingFirstBlock if TX_FIRST_BLOCK action is received from diff detector', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + guards: { + invalidPeerId: () => false, + invalidStreamId: () => false, + invalidNetwork: () => false, + }, + }); + + let currentState = untilIdle(MockedFetchMachine); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingMetadataChanged}.detectingDiff`)).toBeTruthy(); + + currentState = MockedFetchMachine.transition(currentState, { + // @ts-ignore + type: EventTypes.METADATA_DECIDED, + event: { + type: 'TX_FIRST_BLOCK', + originalEvent: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + } + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingFirstBlock}`)).toBeTruthy(); + }); + + it('should transition to handlingUnhandledEvent if IGNORE action is received from diff detector', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + guards: { + invalidPeerId: () => false, + invalidStreamId: () => false, + invalidNetwork: () => false, + }, + }); + + let currentState = untilIdle(MockedFetchMachine); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingMetadataChanged}.detectingDiff`)).toBeTruthy(); + + currentState = MockedFetchMachine.transition(currentState, { + // @ts-ignore + type: EventTypes.METADATA_DECIDED, + event: { + type: 'IGNORE', + originalEvent: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + } + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingUnhandledEvent}`)).toBeTruthy(); + }); + + it('should ignore NEW_VERTEX_ACCEPTED events if the transaction is already voided', () => { + const voidedGuardMock = jest.fn(); + const MockedFetchMachine = SyncMachine.withConfig({ + guards: { + voided: voidedGuardMock, + invalidPeerId: () => false, + invalidStreamId: () => false, + invalidNetwork: () => false, + }, + }); + + voidedGuardMock.mockImplementation(voided); + + let currentState = untilIdle(MockedFetchMachine); + + const VOIDED_NEW_VERTEX_ACCEPTED = { ...NEW_VERTEX_ACCEPTED }; + // @ts-ignore + VOIDED_NEW_VERTEX_ACCEPTED.event.data.metadata.voided_by = ['tx1']; + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: VOIDED_NEW_VERTEX_ACCEPTED as unknown as FullNodeEvent, + }); + + expect(voidedGuardMock).toHaveBeenCalledTimes(1); + expect(voidedGuardMock).toHaveReturnedWith(true); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.idle}`)).toBeTruthy(); + }); + + it('should ignore unhandled events but still send ack', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + guards: { + invalidPeerId: () => false, + invalidStreamId: () => false, + invalidNetwork: () => false, + }, + }); + + let currentState = untilIdle(MockedFetchMachine); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: { + type: 'EVENT', + event: { + peer_id: '123', + id: 38, + timestamp: 1, + type: 'FULLNODE_EXPLODED', + }, + } as unknown as FullNodeEvent, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingUnhandledEvent}`)).toBeTruthy(); + }); + + it('should ignore REORG_STARTED event but still send ack', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + guards: { + invalidPeerId: () => false, + invalidStreamId: () => false, + invalidNetwork: () => false, + }, + }); + + let currentState = untilIdle(MockedFetchMachine); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: REORG_STARTED as unknown as FullNodeEvent, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingUnhandledEvent}`)).toBeTruthy(); + }); +}); diff --git a/packages/daemon/__tests__/services/services.test.ts b/packages/daemon/__tests__/services/services.test.ts new file mode 100644 index 00000000..db4692cb --- /dev/null +++ b/packages/daemon/__tests__/services/services.test.ts @@ -0,0 +1,639 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * @jest-environment node + */ +import axios from 'axios'; +import { + getDbConnection, + getLastSyncedEvent, + updateLastSyncedEvent as dbUpdateLastSyncedEvent, + getTxOutputsFromTx, + voidTransaction, + getTransactionById, + getUtxosLockedAtHeight, + addOrUpdateTx, + getAddressWalletInfo, + generateAddresses, +} from '../../src/db'; +import { + fetchInitialState, + updateLastSyncedEvent, + handleTxFirstBlock, + handleVoidedTx, + handleVertexAccepted, + metadataDiff, + fetchMinRewardBlocks, +} from '../../src/services'; +import logger from '../../src/logger'; +import { + getAddressBalanceMap, + prepareInputs, + prepareOutputs, + hashTxData, + getFullnodeHttpUrl, +} from '../../src/utils'; + +jest.mock('@hathor/wallet-lib'); +jest.mock('../../src/logger', () => ({ + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), +})); + +jest.mock('axios', () => ({ + get: jest.fn(), +})); + +jest.mock('../../src/db', () => ({ + getDbConnection: jest.fn(), + getLastSyncedEvent: jest.fn(), + updateLastSyncedEvent: jest.fn(), + addOrUpdateTx: jest.fn(), + getTxOutputsFromTx: jest.fn(), + voidTransaction: jest.fn(), + markUtxosAsVoided: jest.fn(), + dbUpdateLastSyncedEvent: jest.fn(), + getTransactionById: jest.fn(), + getUtxosLockedAtHeight: jest.fn(), + unlockUtxos: jest.fn(), + addMiner: jest.fn(), + storeTokenInformation: jest.fn(), + getLockedUtxoFromInputs: jest.fn(), + addUtxos: jest.fn(), + updateTxOutputSpentBy: jest.fn(), + incrementTokensTxCount: jest.fn(), + updateAddressTablesWithTx: jest.fn(), + getAddressWalletInfo: jest.fn(), + generateAddresses: jest.fn(), + addNewAddresses: jest.fn(), + updateWalletTablesWithTx: jest.fn(), +})); + +jest.mock('../../src/utils', () => ({ + prepareOutputs: jest.fn(), + prepareInputs: jest.fn(), + getAddressBalanceMap: jest.fn(), + validateAddressBalances: jest.fn(), + LRU: jest.fn(), + unlockTimelockedUtxos: jest.fn(), + markLockedOutputs: jest.fn(), + getWalletBalanceMap: jest.fn(), + hashTxData: jest.fn(), + getTokenListFromInputsAndOutputs: jest.fn(), + getUnixTimestamp: jest.fn(), + unlockUtxos: jest.fn(), + getFullnodeHttpUrl: jest.fn(), +})); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('fetchInitialState', () => { + beforeAll(() => { + const mockUrl = 'http://mock-host:8080/v1a/'; + (getFullnodeHttpUrl as jest.Mock).mockReturnValue(mockUrl); + + // @ts-ignore + axios.get.mockResolvedValue({ + status: 200, + data: { + version: '0.58.0-rc.1', + network: 'mainnet', + min_weight: 14, + min_tx_weight: 14, + min_tx_weight_coefficient: 1.6, + min_tx_weight_k: 100, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 255 + } + }); + }); + + it('should return the last event id', async () => { + // Mock the return values of the dependencies + const mockDb = { destroy: jest.fn() }; + + // @ts-ignore + getDbConnection.mockReturnValue(mockDb); + // @ts-ignore + getLastSyncedEvent.mockResolvedValue({ + id: 0, + last_event_id: 123, + updated_at: Date.now(), + }); + + const result = await fetchInitialState(); + + expect(result).toEqual({ + lastEventId: 123, + rewardMinBlocks: expect.any(Number), + }); + expect(mockDb.destroy).toHaveBeenCalled(); + }); + + it('should return the fullnode\'s reward spend min blocks', async () => { + // Mock the return values of the dependencies + const mockDb = { destroy: jest.fn() }; + + // @ts-ignore + getDbConnection.mockReturnValue(mockDb); + // @ts-ignore + getLastSyncedEvent.mockResolvedValue({ + id: 0, + last_event_id: 123, + updated_at: Date.now(), + }); + + const result = await fetchInitialState(); + + expect(result).toEqual({ + lastEventId: expect.any(Number), + rewardMinBlocks: 300, + }); + + expect(mockDb.destroy).toHaveBeenCalled(); + }); + + it('should return undefined if no last event is found', async () => { + const mockDb = { destroy: jest.fn() }; + // @ts-ignore + getDbConnection.mockResolvedValue(mockDb); + // @ts-ignore + getLastSyncedEvent.mockResolvedValue(null); + + const result = await fetchInitialState(); + + expect(result).toEqual({ + lastEventId: undefined, + rewardMinBlocks: expect.any(Number), + }); + expect(mockDb.destroy).toHaveBeenCalled(); + }); +}); + +describe('updateLastSyncedEvent', () => { + const mockDb = { destroy: jest.fn() }; + + beforeEach(() => { + jest.clearAllMocks(); + (getDbConnection as jest.Mock).mockResolvedValue(mockDb); + }); + + it('should update when the lastEventId is greater', async () => { + (getLastSyncedEvent as jest.Mock).mockResolvedValue({ last_event_id: 100 }); + + // @ts-ignore + await updateLastSyncedEvent({ event: { event: { id: 101 } } }); + + expect(dbUpdateLastSyncedEvent).toHaveBeenCalledWith(mockDb, 101); + expect(mockDb.destroy).toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); + }); + + it('should log error and throw when the lastEventId is less than or equal', async () => { + (getLastSyncedEvent as jest.Mock).mockResolvedValue({ last_event_id: 102 }); + + // @ts-ignore + await expect(updateLastSyncedEvent({ event: { event: { id: 100 } } })).rejects.toThrow('Event lower than stored one.'); + + expect(dbUpdateLastSyncedEvent).not.toHaveBeenCalled(); + expect(mockDb.destroy).toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith('Tried to store an event lower than the one on the database', { + lastEventId: 100, + lastDbSyncedEvent: JSON.stringify({ last_event_id: 102 }), + }); + }); +}); + +describe('handleTxFirstBlock', () => { + const mockDb = { + beginTransaction: jest.fn(), + commit: jest.fn(), + rollback: jest.fn(), + destroy: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + (getDbConnection as jest.Mock).mockResolvedValue(mockDb); + }); + + it('should handle the tx first block successfully', async () => { + const context = { + event: { + event: { + data: { + hash: 'hashValue', + metadata: { + height: 123, + first_block: ['hash2'], + }, + timestamp: 'timestampValue', + version: 'versionValue', + weight: 'weightValue', + }, + id: 'idValue', + }, + }, + }; + + await handleTxFirstBlock(context as any); + + expect(addOrUpdateTx).toHaveBeenCalledWith(mockDb, 'hashValue', 123, 'timestampValue', 'versionValue', 'weightValue'); + expect(dbUpdateLastSyncedEvent).toHaveBeenCalledWith(mockDb, 'idValue'); + expect(logger.debug).toHaveBeenCalledWith('Confirmed tx hashValue: idValue'); + expect(mockDb.commit).toHaveBeenCalled(); + expect(mockDb.destroy).toHaveBeenCalled(); + }); + + it('should rollback on error and rethrow', async () => { + (addOrUpdateTx as jest.Mock).mockRejectedValue(new Error('Test error')); + + const context = { + event: { + event: { + data: { + hash: 'hashValue', + metadata: { + height: 123, + first_block: ['hash2'], + }, + timestamp: 'timestampValue', + version: 'versionValue', + weight: 'weightValue', + }, + id: 'idValue', + }, + }, + }; + + await expect(handleTxFirstBlock(context as any)).rejects.toThrow('Test error'); + expect(logger.error).toHaveBeenCalledWith('E: ', expect.any(Error)); + expect(mockDb.rollback).toHaveBeenCalled(); + expect(mockDb.destroy).toHaveBeenCalled(); + }); +}); + +describe('handleVoidedTx', () => { + const mockDb = { + beginTransaction: jest.fn(), + commit: jest.fn(), + rollback: jest.fn(), + destroy: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + (getDbConnection as jest.Mock).mockResolvedValue(mockDb); + }); + + it('should handle the voided tx', async () => { + const context = { + event: { + event: { + data: { + hash: 'hashValue', + outputs: 'outputsValue', + inputs: 'inputsValue', + tokens: 'tokensValue', + }, + id: 'idValue', + }, + }, + }; + + (prepareOutputs as jest.Mock).mockReturnValue([]); + (prepareInputs as jest.Mock).mockReturnValue([]); + (getAddressBalanceMap as jest.Mock).mockReturnValue({}); + (getTxOutputsFromTx as jest.Mock).mockResolvedValue([]); + + await handleVoidedTx(context as any); + + expect(voidTransaction).toHaveBeenCalledWith(expect.any(Object), 'hashValue', {}); + expect(logger.debug).toHaveBeenCalledWith('Will handle voided tx for hashValue'); + expect(logger.debug).toHaveBeenCalledWith('Voided tx hashValue'); + expect(mockDb.beginTransaction).toHaveBeenCalled(); + expect(mockDb.commit).toHaveBeenCalled(); + expect(mockDb.destroy).toHaveBeenCalled(); + }); + + it('should throw an error if transaction output is different from database output', async () => { + const context = { + event: { + event: { + data: { + hash: 'hashValue', + outputs: 'outputsValue', + inputs: 'inputsValue', + tokens: 'tokensValue', + }, + id: 'idValue', + }, + }, + }; + + // Mock the return values + const mockTxOutputs = [ + { index: 1, value: 5 }, + { index: 2, value: 10 }, + ]; + const mockDbTxOutputs = [ + { index: 1, value: 5, locked: false }, + // Omitting index 2 to create a mismatch + ]; + + (prepareOutputs as jest.Mock).mockReturnValue(mockTxOutputs); + (getTxOutputsFromTx as jest.Mock).mockResolvedValue(mockDbTxOutputs); + + // Now, when handleVoidedTx is called, it should throw the error because of the mismatch + await expect(handleVoidedTx(context as any)).rejects.toThrow('Transaction output different from database output!'); + expect(mockDb.rollback).toHaveBeenCalled(); + expect(mockDb.destroy).toHaveBeenCalled(); + }); + + it('should rollback on error and rethrow', async () => { + (getTxOutputsFromTx as jest.Mock).mockRejectedValue(new Error('Test error')); + + const context = { + event: { + event: { + data: { + hash: 'hashValue', + outputs: 'outputsValue', + inputs: 'inputsValue', + tokens: 'tokensValue', + }, + id: 'idValue', + }, + }, + }; + + await expect(handleVoidedTx(context as any)).rejects.toThrow('Test error'); + expect(logger.debug).toHaveBeenCalledWith(expect.any(Error)); + expect(mockDb.beginTransaction).toHaveBeenCalled(); + expect(mockDb.rollback).toHaveBeenCalled(); + expect(mockDb.destroy).toHaveBeenCalled(); + }); +}); + +describe('handleVertexAccepted', () => { + const mockDb = { + beginTransaction: jest.fn(), + commit: jest.fn(), + rollback: jest.fn(), + destroy: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + + (getDbConnection as jest.Mock).mockResolvedValue(mockDb); + (getAddressWalletInfo as jest.Mock).mockResolvedValue({}); + (generateAddresses as jest.Mock).mockResolvedValue({ + newAddresses: ['mockAddress1', 'mockAddress2'], + lastUsedAddressIndex: 1 + }); + }); + + it('should handle vertex accepted successfully', async () => { + const context = { + event: { + event: { + data: { + hash: 'hashValue', + metadata: { + height: 123, + first_block: true, + voided_by: [], + }, + timestamp: 'timestampValue', + version: 'versionValue', + weight: 'weightValue', + outputs: 'outputsValue', + inputs: 'inputsValue', + tokens: 'tokensValue', + token_name: 'tokenName', + token_symbol: 'tokenSymbol', + }, + id: 'idValue', + }, + }, + rewardMinBlocks: 300, + txCache: { + get: jest.fn(), + set: jest.fn(), + }, + }; + + (addOrUpdateTx as jest.Mock).mockReturnValue(Promise.resolve()); + (getTransactionById as jest.Mock).mockResolvedValue(null); // Transaction is not in the database + (prepareOutputs as jest.Mock).mockReturnValue([]); + (prepareInputs as jest.Mock).mockReturnValue([]); + (getAddressBalanceMap as jest.Mock).mockReturnValue({}); + (getUtxosLockedAtHeight as jest.Mock).mockResolvedValue([]); + (hashTxData as jest.Mock).mockReturnValue('hashedData'); + (getAddressWalletInfo as jest.Mock).mockResolvedValue({ + 'address1': { + walletId: 'wallet1', + xpubkey: 'xpubkey1', + maxGap: 10 + }, + }); + + await handleVertexAccepted(context as any, {} as any); + + expect(getDbConnection).toHaveBeenCalled(); + expect(mockDb.beginTransaction).toHaveBeenCalled(); + expect(getTransactionById).toHaveBeenCalledWith(mockDb, 'hashValue'); + expect(logger.debug).toHaveBeenCalledWith('Will add the tx with height', 123); + expect(mockDb.commit).toHaveBeenCalled(); + expect(mockDb.destroy).toHaveBeenCalled(); + }); +}); + +describe('metadataDiff', () => { + const mockDb = { + destroy: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + (getDbConnection as jest.Mock).mockResolvedValue(mockDb); + }); + + it('should ignore voided transactions not in database', async () => { + const event = { + event: { + event: { + data: { + hash: 'mockHash', + metadata: { voided_by: ['mockVoidedBy'], first_block: [] }, + }, + }, + }, + }; + + (getTransactionById as jest.Mock).mockResolvedValue(null); + + const result = await metadataDiff({} as any, event as any); + + expect(result.type).toBe('IGNORE'); + }); + + it('should handle new transactions', async () => { + const event = { + event: { + event: { + data: { + hash: 'mockHash', + metadata: { voided_by: [], first_block: [] }, + }, + }, + }, + }; + + (getTransactionById as jest.Mock).mockResolvedValue(null); + + const result = await metadataDiff({} as any, event as any); + expect(result.type).toBe('TX_NEW'); + }); + + it('should handle transaction voided but not voided in database', async () => { + const event = { + event: { + event: { + data: { + hash: 'mockHash', + metadata: { voided_by: ['mockVoidedBy'], first_block: [] }, + }, + }, + }, + }; + const mockDbTransaction = { voided: false }; + (getTransactionById as jest.Mock).mockResolvedValue(mockDbTransaction); + + const result = await metadataDiff({} as any, event as any); + expect(result.type).toBe('TX_VOIDED'); + }); + + it('should ignore transaction voided and also voided in database', async () => { + const event = { + event: { + event: { + data: { + hash: 'mockHash', + metadata: { voided_by: ['mockVoidedBy'], first_block: [] }, + }, + }, + }, + }; + const mockDbTransaction = { voided: true }; + (getTransactionById as jest.Mock).mockResolvedValue(mockDbTransaction); + + const result = await metadataDiff({} as any, event as any); + expect(result.type).toBe('IGNORE'); + }); + + it('should handle transaction with first_block but no height in database', async () => { + const event = { + event: { + event: { + data: { + hash: 'mockHash', + metadata: { voided_by: [], first_block: ['mockFirstBlock'] }, + }, + }, + }, + }; + const mockDbTransaction = { height: null }; + (getTransactionById as jest.Mock).mockResolvedValue(mockDbTransaction); + + const result = await metadataDiff({} as any, event as any); + expect(result.type).toBe('TX_FIRST_BLOCK'); + }); + + it('should ignore transaction with first_block and height in database', async () => { + const event = { + event: { + event: { + data: { + hash: 'mockHash', + metadata: { voided_by: [], first_block: ['mockFirstBlock'] }, + }, + }, + }, + }; + const mockDbTransaction = { height: 1 }; + (getTransactionById as jest.Mock).mockResolvedValue(mockDbTransaction); + + const result = await metadataDiff({} as any, event as any); + expect(result.type).toBe('IGNORE'); + }); + + it('should return IGNORE for other scenarios', async () => { + const event = { + event: { + event: { + data: { + hash: 'mockHash', + metadata: { voided_by: [], first_block: [] }, + }, + }, + }, + }; + const mockDbTransaction = { height: 1, voided: false }; + (getTransactionById as jest.Mock).mockResolvedValue(mockDbTransaction); + + const result = await metadataDiff({} as any, event as any); + expect(result.type).toBe('IGNORE'); + }); + + it('should handle errors and destroy the database connection', async () => { + const event = { + event: { + event: { + data: { + hash: 'mockHash', + metadata: { voided_by: [], first_block: [] }, + }, + }, + }, + }; + (getTransactionById as jest.Mock).mockRejectedValue(new Error('Mock Error')); + + await expect(metadataDiff({} as any, event as any)).rejects.toThrow('Mock Error'); + expect(mockDb.destroy).toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith('e', new Error('Mock Error')); + }); + + it('should handle transaction transactions that are not voided anymore', async () => { + const event = { + event: { + event: { + data: { + hash: 'mockHash', + metadata: { voided_by: [], first_block: [] }, + }, + }, + }, + }; + const mockDbTransaction = { voided: true }; // Indicate that the transaction was voided in the database. + (getTransactionById as jest.Mock).mockResolvedValue(mockDbTransaction); + + const result = await metadataDiff({} as any, event as any); + expect(result.type).toBe('TX_UNVOIDED'); + }); +}); diff --git a/packages/daemon/__tests__/types.ts b/packages/daemon/__tests__/types.ts new file mode 100644 index 00000000..b78c7e32 --- /dev/null +++ b/packages/daemon/__tests__/types.ts @@ -0,0 +1,51 @@ +export interface AddressTableEntry { + address: string; + index?: number | null; + walletId?: string | null; + transactions: number; +} + +export interface WalletBalanceEntry { + walletId: string; + tokenId: string; + unlockedBalance: number; + lockedBalance: number; + unlockedAuthorities: number; + lockedAuthorities: number; + timelockExpires?: number | null; + transactions: number; +} + +export interface WalletTableEntry { + id: string; + xpubkey: string; + authXpubkey: string; + status: string; + maxGap: number; + highestUsedIndex?: number; + createdAt: number; + readyAt: number; +} + +export interface TokenTableEntry { + id: string; + name: string; + symbol: string; + transactions: number; +} + +export type Token = { + tokenId: string; + tokenSymbol: string; + tokenName: string; + transactions: number; +} + +export interface AddressTxHistoryTableEntry { + address: string; + txId: string; + tokenId: string; + balance: number; + timestamp: number; + voided?: boolean; +} diff --git a/packages/daemon/__tests__/utils.ts b/packages/daemon/__tests__/utils.ts new file mode 100644 index 00000000..74a9af53 --- /dev/null +++ b/packages/daemon/__tests__/utils.ts @@ -0,0 +1,708 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Connection as MysqlConnection, RowDataPacket } from 'mysql2/promise'; +import { DbTxOutput, EventTxInput, TxInput, TxOutputWithIndex } from "../src/types"; +import { + AddressBalanceRow, + AddressTableRow, + AddressTxHistoryRow, + TokenInformationRow, + TxOutputRow, + WalletBalanceRow, + WalletTxHistoryRow, +} from '../src/types'; +import { + Token, + AddressTableEntry, + TokenTableEntry, + WalletBalanceEntry, + WalletTableEntry, + AddressTxHistoryTableEntry +} from './types'; +import { isEqual } from 'lodash'; + +export const XPUBKEY = 'xpub6CsZPtBWMkwxVxyBTKT8AWZcYqzwZ5K2qMkqjFpibMbBZ72JAvLMz7LquJNs4svfTiNYy6GbLo8gqECWsC6hTRt7imnphUFNEMz6VuRSjww'; +export const ADDRESSES = [ + 'HBCQgVR8Xsyv1BLDjf9NJPK1Hwg4rKUh62', + 'HPDWdurEygcubNMUUnTDUAzngrSXFaqGQc', + 'HEYCNNZZYrimD97AtoRcgcNFzyxtkgtt9Q', + 'HPTtSRrDd4ekU4ZQ2jnSLYayL8hiToE5D4', + 'HTYymKpjyXnz4ssEAnywtwnXnfneZH1Dbh', + 'HUp754aDZ7yKndw2JchXEiMvgzKuXasUmF', + 'HLfGaQoxssGbZ4h9wbLyiCafdE8kPm6Fo4', + 'HV3ox5B1Dai6Jp5EhV8DvUiucc1z3WJHjL', + 'HNWxs2bxgYtzfCpU6cJMGLgmqv7eGupTHr', + 'H9Ef7qteC4vAoVUYx5mvP9jCfmZgU9rSvL', + 'H7hxR75zsPzwfPWbrdkkFbKN2SiL2Lvyuw', + 'HVCa4QJbHB6pkqvNkmQZD2vpmwTYMNdzVo', + 'HBchgf1JLxwJzUg6epckK3YJn6Bq8XJMPV', + 'HVWf61fwoj9Dx15NvWicqXQgGMYVYedSx4', + 'H7PfxBmaqjoBisFRzpizoB9JcYSvoo8D2j', + 'HC1NXVzGcVAd84QMfFngHiKyK2K8SUiTaL', + 'HCqsSDrbs1cfqnF6QMUQkdGYXjEMyt9N3Y', +]; + +export const createOutput = ( + index: number, + value: number, + address: string, + token = '00', + timelock: number | null = null, + locked = false, + tokenData = 0, + spentBy = null, +): TxOutputWithIndex => ( + { + value, + token, + locked, + index, + decoded: { + type: 'P2PKH', + address, + timelock, + }, + token_data: tokenData, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + spent_by: spentBy, + } +); + +export const createEventTxInput = ( + value: number, + address: string, + txId: string, + index: number, + timelock: number | null | undefined = null, + tokenData = 0, +): EventTxInput => ( + { + tx_id: txId, + index, + spent_output: { + value, + token_data: tokenData, + script: 'dqkUCEboPJo9txn548FA/NLLaMLsfsSIrA==', + locked: false, + decoded: { + type: 'P2PKH', + address, + timelock, + }, + } + } +); + +export const createInput = ( + value: number, + address: string, + txId: string, + index: number, + token = '00', + timelock: number | null | undefined = null, + tokenData = 0, +): TxInput => ( + { + value, + token_data: tokenData, + script: 'dqkUCEboPJo9txn548FA/NLLaMLsfsSIrA==', + decoded: { + type: 'P2PKH', + address, + timelock, + }, + token, + tx_id: txId, + index, + } +); + +export const checkUtxoTable = async ( + mysql: MysqlConnection, + totalResults: number, + txId?: string, + index?: number, + tokenId?: string, + address?: string, + value?: number, + authorities?: number, + timelock?: number | null, + heightlock?: number | null, + locked?: boolean, + spentBy?: string | null, + voided = false, +): Promise> => { + // first check the total number of rows in the table + let [results] = await mysql.query('SELECT * FROM `tx_output` WHERE spent_by IS NULL'); + if (results.length !== totalResults) { + return { + error: 'checkUtxoTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + if (totalResults === 0) return true; + + // now fetch the exact entry + const baseQuery = ` + SELECT * + FROM \`tx_output\` + WHERE \`tx_id\` = ? + AND \`index\` = ? + AND \`token_id\` = ? + AND \`address\` = ? + AND \`value\` = ? + AND \`authorities\` = ? + AND \`locked\` = ? + AND \`voided\` = ? + AND \`timelock\``; + + [results] = await mysql.query( + `${baseQuery} ${timelock ? '= ?' : 'IS ?'} + AND \`heightlock\` ${heightlock ? '= ?' : 'IS ?'} + AND \`spent_by\` ${spentBy ? '= ?' : 'IS ?'} + `, + [txId, index, tokenId, address, value, authorities, locked, voided, timelock, heightlock, spentBy], + ); + + if (results.length !== 1) { + return { + error: 'checkUtxoTable query', + params: { txId, index, tokenId, address, value, authorities, timelock, heightlock, locked, spentBy, voided }, + results, + }; + } + return true; +}; + +export const cleanDatabase = async (mysql: MysqlConnection): Promise => { + const TABLES = [ + 'address', + 'address_balance', + 'address_tx_history', + 'token', + 'tx_proposal', + 'transaction', + 'tx_output', + 'version_data', + 'wallet', + 'wallet_balance', + 'wallet_tx_history', + 'miner', + 'push_devices', + 'sync_metadata', + ]; + await mysql.query('SET FOREIGN_KEY_CHECKS = 0'); + for (const table of TABLES) { + await mysql.query(`DELETE FROM ${table}`); + } + await mysql.query('SET FOREIGN_KEY_CHECKS = 1'); +}; + +interface CountRow extends RowDataPacket { + count: number; +} + +export const countTxOutputTable = async ( + mysql: MysqlConnection, +): Promise => { + const [results] = await mysql.query( + `SELECT COUNT(*) AS count + FROM \`tx_output\` + WHERE \`voided\` = FALSE`, + ); + + if (results.length > 0) { + return results[0].count as number; + } + + return 0; +}; + +export const addToAddressTable = async ( + mysql: MysqlConnection, + entries: AddressTableEntry[], +): Promise => { + const payload = entries.map((entry) => ([ + entry.address, + entry.index, + entry.walletId, + entry.transactions, + ])); + + await mysql.query(` + INSERT INTO \`address\`(\`address\`, \`index\`, + \`wallet_id\`, \`transactions\`) + VALUES ?`, + [payload]); +}; + +export const checkAddressTable = async ( + mysql: MysqlConnection, + totalResults: number, + address?: string, + index?: number | null, + walletId?: string | null, + transactions?: number, +): Promise> => { + // first check the total number of rows in the table + let [results] = await mysql.query('SELECT * FROM `address`'); + if (results.length !== totalResults) { + return { + error: 'checkAddressTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + if (totalResults === 0) return true; + + // now fetch the exact entry + const baseQuery = ` + SELECT * + FROM \`address\` + WHERE \`address\` = ? + AND \`transactions\` = ? + AND \`index\` + `; + const query = `${baseQuery} ${index !== null ? '= ?' : 'IS ?'} AND wallet_id ${walletId ? '= ?' : 'IS ?'}`; + [results] = await mysql.query( + query, + [address, transactions, index, walletId], + ); + if (results.length !== 1) { + return { + error: 'checkAddressTable query', + params: { address, transactions, index, walletId }, + results, + }; + } + return true; +}; + +export const checkAddressBalanceTable = async ( + mysql: MysqlConnection, + totalResults: number, + address: string, + tokenId: string, + unlocked: number, + locked: number, + lockExpires: number | null, + transactions: number, + unlockedAuthorities = 0, + lockedAuthorities = 0, +): Promise> => { + // first check the total number of rows in the table + let [results] = await mysql.query(` + SELECT * + FROM \`address_balance\` + `); + if (results.length !== totalResults) { + return { + error: 'checkAddressBalanceTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + // now fetch the exact entry + const baseQuery = ` + SELECT * + FROM \`address_balance\` + WHERE \`address\` = ? + AND \`token_id\` = ? + AND \`unlocked_balance\` = ? + AND \`locked_balance\` = ? + AND \`transactions\` = ? + AND \`unlocked_authorities\` = ? + AND \`locked_authorities\` = ?`; + + [results] = await mysql.query( + `${baseQuery} AND timelock_expires ${lockExpires === null ? 'IS' : '='} ?`, [ + address, + tokenId, + unlocked, + locked, + transactions, + unlockedAuthorities, + lockedAuthorities, + lockExpires, + ], + ); + + if (results.length !== 1) { + return { + error: 'checkAddressBalanceTable query', + params: { address, tokenId, unlocked, locked, lockExpires, transactions, unlockedAuthorities, lockedAuthorities }, + results, + }; + } + return true; +}; + +export const checkAddressTxHistoryTable = async ( + mysql: MysqlConnection, + totalResults: number, + address: string, + txId: string, + tokenId: string, + balance: number, + timestamp: number, +): Promise> => { + // first check the total number of rows in the table + let [results] = await mysql.query('SELECT * FROM `address_tx_history`'); + expect(results).toHaveLength(totalResults); + if (results.length !== totalResults) { + return { + error: 'checkAddressTxHistoryTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + // If we expect the table to be empty, we can return now. + if (totalResults === 0) { + return true; + } + + // now fetch the exact entry + [results] = await mysql.query( + `SELECT * + FROM \`address_tx_history\` + WHERE \`address\` = ? + AND \`tx_id\` = ? + AND \`token_id\` = ? + AND \`balance\` = ? + AND \`timestamp\` = ?`, + [ + address, + txId, + tokenId, + balance, + timestamp, + ], + ); + if (results.length !== 1) { + return { + error: 'checkAddressTxHistoryTable query', + params: { address, txId, tokenId, balance, timestamp }, + results, + }; + } + return true; +}; + +export const addToAddressBalanceTable = async ( + mysql: MysqlConnection, + entries: unknown[][], +): Promise => { + await mysql.query(` + INSERT INTO \`address_balance\`(\`address\`, \`token_id\`, + \`unlocked_balance\`, \`locked_balance\`, + \`timelock_expires\`, \`transactions\`, + \`unlocked_authorities\`, \`locked_authorities\`, + \`total_received\`) + VALUES ?`, + [entries]); +}; + +export const addToWalletBalanceTable = async ( + mysql: MysqlConnection, + entries: WalletBalanceEntry[], +): Promise => { + const payload = entries.map((entry) => ([ + entry.walletId, + entry.tokenId, + entry.unlockedBalance, + entry.lockedBalance, + entry.unlockedAuthorities, + entry.lockedAuthorities, + entry.timelockExpires, + entry.transactions, + ])); + + await mysql.query(` + INSERT INTO \`wallet_balance\`(\`wallet_id\`, \`token_id\`, + \`unlocked_balance\`, \`locked_balance\`, + \`unlocked_authorities\`, \`locked_authorities\`, + \`timelock_expires\`, \`transactions\`) + VALUES ?`, + [payload]); +}; + +export const addToUtxoTable = async ( + mysql: MysqlConnection, + entries: DbTxOutput[], +): Promise => { + const payload = entries.map((entry: DbTxOutput) => ([ + entry.txId, + entry.index, + entry.tokenId, + entry.address, + entry.value, + entry.authorities, + entry.timelock || null, + entry.heightlock || null, + entry.locked, + entry.spentBy || null, + entry.txProposalId || null, + entry.txProposalIndex, + entry.voided || false, + ])); + await mysql.query( + `INSERT INTO \`tx_output\`( + \`tx_id\` + , \`index\` + , \`token_id\` + , \`address\` + , \`value\` + , \`authorities\` + , \`timelock\` + , \`heightlock\` + , \`locked\` + , \`spent_by\` + , \`tx_proposal\` + , \`tx_proposal_index\` + , \`voided\`) + VALUES ?`, + [payload], + ); +}; + +export const checkWalletBalanceTable = async ( + mysql: MysqlConnection, + totalResults: number, + walletId?: string, + tokenId?: string, + unlocked?: number, + locked?: number, + lockExpires?: number | null, + transactions?: number, + unlockedAuthorities = 0, + lockedAuthorities = 0, +): Promise> => { + // first check the total number of rows in the table + let [results] = await mysql.query(` + SELECT * + FROM \`wallet_balance\` + `); + expect(results).toHaveLength(totalResults); + if (results.length !== totalResults) { + return { + error: 'checkWalletBalanceTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + if (totalResults === 0) return true; + + // now fetch the exact entry + const baseQuery = ` + SELECT * + FROM \`wallet_balance\` + WHERE \`wallet_id\` = ? + AND \`token_id\` = ? + AND \`unlocked_balance\` = ? + AND \`locked_balance\` = ? + AND \`transactions\` = ? + AND \`unlocked_authorities\` = ? + AND \`locked_authorities\` = ? + `; + [results] = await mysql.query( + `${baseQuery} AND timelock_expires ${lockExpires === null ? 'IS' : '='} ?`, + [walletId, tokenId, unlocked, locked, transactions, unlockedAuthorities, lockedAuthorities, lockExpires], + ); + if (results.length !== 1) { + return { + error: 'checkWalletBalanceTable query', + params: { walletId, tokenId, unlocked, locked, lockExpires, transactions, unlockedAuthorities, lockedAuthorities }, + results, + }; + } + return true; +}; + +export const addToWalletTable = async ( + mysql: MysqlConnection, + entries: WalletTableEntry[], +): Promise => { + const payload = entries.map((entry) => [ + entry.id, + entry.xpubkey, + entry.highestUsedIndex || -1, + entry.authXpubkey, + entry.status, + entry.maxGap, + entry.createdAt, + entry.readyAt, + ]); + await mysql.query(` + INSERT INTO \`wallet\`(\`id\`, \`xpubkey\`, + \`last_used_address_index\`, + \`auth_xpubkey\`, + \`status\`, \`max_gap\`, + \`created_at\`, \`ready_at\`) + VALUES ?`, + [payload]); +}; + +export const addToTokenTable = async ( + mysql: MysqlConnection, + entries: TokenTableEntry[], +): Promise => { + const payload = entries.map((entry) => ([ + entry.id, + entry.name, + entry.symbol, + entry.transactions, + ])); + + await mysql.query( + 'INSERT INTO `token`(`id`, `name`, `symbol`, `transactions`) VALUES ?', + [payload], + ); +}; + +export const checkTokenTable = async ( + mysql: MysqlConnection, + totalResults: number, + entries: Token[], +): Promise> => { + // first check the total number of rows in the table + let [results] = await mysql.query('SELECT * FROM `token`'); + if (results.length !== totalResults) { + return { + error: 'checkTokenTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + if (totalResults === 0) return true; + + // Fetch the exact entries + const query = ` + SELECT * + FROM \`token\` + WHERE \`id\` IN (?) + `; + [results] = await mysql.query( + query, + [entries.map((token) => token.tokenId)], + ); + + const resultTokens: Token[] = results.map((result: TokenInformationRow) => ({ + tokenId: result.id, + tokenSymbol: result.symbol, + tokenName: result.name, + transactions: result.transactions, + })); + const invalidResults = resultTokens.filter((token: Token) => { + const entry = entries.find(({ tokenId }) => tokenId === token.tokenId); + + if (!entry) { + return true; + } + + // token is a RowDataPacket, so just cast it to an object so we can + // compare it + if (!isEqual({ ...token }, entry)) { + return true; + } + + return false; + }); + + if (invalidResults.length > 0) { + return { + error: 'checkTokenTable query', + params: entries, + invalidResults, + }; + } + return true; +}; + +export const checkWalletTxHistoryTable = async ( + mysql: MysqlConnection, + totalResults: number, + walletId?: string, + tokenId?: string, + txId?: string, + balance?: number, + timestamp?: number): Promise> => { + // first check the total number of rows in the table + let [results] = await mysql.query('SELECT * FROM `wallet_tx_history`'); + expect(results).toHaveLength(totalResults); + if (results.length !== totalResults) { + return { + error: 'checkWalletTxHistoryTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + if (totalResults === 0) return true; + + // now fetch the exact entry + [results] = await mysql.query( + `SELECT * + FROM \`wallet_tx_history\` + WHERE \`wallet_id\` = ? + AND \`token_id\` = ? + AND \`tx_id\` = ? + AND \`balance\` = ? + AND \`timestamp\` = ?`, + [ + walletId, + tokenId, + txId, + balance, + timestamp, + ], + ); + + if (results.length !== 1) { + return { + error: 'checkWalletTxHistoryTable query', + params: { walletId, tokenId, txId, balance, timestamp }, + results, + }; + } + return true; +}; + +export const addToAddressTxHistoryTable = async ( + mysql: MysqlConnection, + entries: AddressTxHistoryTableEntry[], +): Promise => { + const payload = entries.map((entry) => ([ + entry.address, + entry.txId, + entry.tokenId, + entry.balance, + entry.timestamp, + entry.voided || false, + ])); + + await mysql.query(` + INSERT INTO \`address_tx_history\`(\`address\`, \`tx_id\`, + \`token_id\`, \`balance\`, + \`timestamp\`, \`voided\`) + VALUES ?`, + [payload]); +}; diff --git a/packages/daemon/jest.config.js b/packages/daemon/jest.config.js new file mode 100644 index 00000000..d23f73ed --- /dev/null +++ b/packages/daemon/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + roots: ["/__tests__"], + testRegex: ".*\\.test\\.ts$", + transform: { + "^.+\\.ts$": ["ts-jest", { + tsconfig: "./tsconfig.json", + babelConfig: { + sourceMaps: true, + } + }] + }, + testPathIgnorePatterns: ['/__tests__/integration/'], + moduleFileExtensions: ["ts", "js", "json", "node"] +}; diff --git a/packages/daemon/jest_integration.config.js b/packages/daemon/jest_integration.config.js new file mode 100644 index 00000000..2e30f587 --- /dev/null +++ b/packages/daemon/jest_integration.config.js @@ -0,0 +1,19 @@ +// Minor helper for test development. Allows for specific file testing. +// (Taken from the wallet-headless repository) +const mainTestMatch = process.env.SPECIFIC_INTEGRATION_TEST_FILE + ? `/__tests__/integration/**/${process.env.SPECIFIC_INTEGRATION_TEST_FILE}.test.ts` + : '/__tests__/integration/**/*.test.ts'; + +module.exports = { + roots: ["/__tests__"], + transform: { + "^.+\\.ts$": ["ts-jest", { + tsconfig: "./tsconfig.json", + babelConfig: { + sourceMaps: true, + } + }] + }, + testMatch: [mainTestMatch], + moduleFileExtensions: ["ts", "js", "json", "node"] +}; diff --git a/packages/daemon/package.json b/packages/daemon/package.json new file mode 100644 index 00000000..8b4efd23 --- /dev/null +++ b/packages/daemon/package.json @@ -0,0 +1,61 @@ +{ + "license": "MIT", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "lint": "eslint .", + "build": "tsc", + "watch": "tsc -w", + "test_images_up": "docker-compose -f ./__tests__/integration/scripts/docker-compose.yml up -d", + "test_images_down": "docker-compose -f ./__tests__/integration/scripts/docker-compose.yml down", + "test_images_integration": "jest --config ./jest_integration.config.js --runInBand --forceExit", + "test_images_migrate": "DB_NAME=hathor DB_PORT=3380 DB_PASS=hathor DB_USER=hathor yarn run sequelize-cli --migrations-path ../../db/migrations --config ./__tests__/integration/scripts/sequelize-db-config.js db:migrate", + "test_images_wait_for_db": "yarn dlx ts-node ./__tests__/integration/scripts/wait-for-db-up.ts", + "test_images_wait_for_ws": "yarn dlx ts-node ./__tests__/integration/scripts/wait-for-ws-up.ts", + "test_images_setup_database": "yarn dlx ts-node ./__tests__/integration/scripts/setup-database.ts", + "test": "jest --coverage", + "test_integration": "yarn run test_images_up && yarn run test_images_wait_for_db && yarn run test_images_wait_for_ws && yarn run test_images_setup_database && yarn run test_images_migrate && yarn run test_images_integration && yarn run test_images_down" + }, + "name": "sync-daemon", + "author": "André Abadesso", + "module": "dist/index.js", + "devDependencies": { + "@types/jest": "^29.5.4", + "@types/lodash": "^4.14.199", + "@types/mysql": "^2.15.21", + "@types/node": "^17.0.45", + "@types/ws": "^8.5.5", + "@typescript-eslint/eslint-plugin": "^6.7.3", + "@typescript-eslint/parser": "^6.7.3", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-plugin-jest": "^27.4.0", + "jest": "^29.6.4", + "sequelize-cli": "^6.6.1", + "ts-jest": "^29.1.1", + "tslib": "^2.1.0", + "typescript": "^4.9.5" + }, + "dependencies": { + "@aws-sdk/client-lambda": "^3.474.0", + "@aws-sdk/client-sqs": "^3.474.0", + "@hathor/wallet-lib": "^0.39.0", + "assert": "^2.1.0", + "aws-sdk": "^2.1454.0", + "axios": "^1.6.2", + "dotenv": "^8.2.0", + "lodash": "^4.17.21", + "mysql2": "^3.5.2", + "sequelize": "^6.33.0", + "websocket": "^1.0.33", + "winston": "^3.3.3", + "ws": "^8.13.0", + "xstate": "^4.38.2" + } +} diff --git a/packages/daemon/src/actions/index.ts b/packages/daemon/src/actions/index.ts new file mode 100644 index 00000000..a46b9170 --- /dev/null +++ b/packages/daemon/src/actions/index.ts @@ -0,0 +1,196 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { assign, AssignAction, raise, sendTo } from 'xstate'; +import { Context, Event, EventTypes } from '../types'; +import { get } from 'lodash'; +import logger from '../logger'; +import { hashTxData } from '../utils'; +import { createStartStreamMessage, createSendAckMessage } from '../actors'; + +/* + * This action is used to store the initial event id on the context + */ +export const storeInitialState = assign({ + initialEventId: (_context: Context, event: Event) => { + // @ts-ignore + return event.data.lastEventId; + }, + rewardMinBlocks: (_context: Context, event: Event) => { + // @ts-ignore + return event.data.rewardMinBlocks; + }, +}); + +/* + * This action is used to set the context event to the event that comes on the + * event. + * + * This is used after the metadataDiff service detects what is the type of the + * event, so the state is transitioned to the right place and the event is set + * to the original event (that initiated the metadata diff check) + */ +export const unwrapEvent = assign({ + event: (_context: Context, event: Event) => { + if (event.type !== 'METADATA_DECIDED') { + throw new Error(`Received unhandled ${event.type} on unwrapEvent action`); + } + + return event.event.originalEvent.event; + }, +}); + +/* + * This action is used to increase the retry count on the context + */ +export const increaseRetry = assign({ + retryAttempt: (context: Context) => context.retryAttempt + 1, +}); + +/* + * This is a helper to get the socket ref from the context and throw if it's not + * found. + */ +export const getSocketRefFromContext = (context: Context) => { + if (!context.socket) { + throw new Error('No socket in context'); + } + + return context.socket; +}; + +/* + * This is a helper to get the healthcheck ref from the context and throw if it's not + * found. + */ +export const getHealthcheckRefFromContext = (context: Context) => { + if (!context.healthcheck) { + throw new Error('No healthcheck in context'); + } + + return context.healthcheck; +}; + +/* + * This action sends an event to the socket actor + */ +export const startStream = sendTo( + getSocketRefFromContext, + (context: Context, _event: Event) => { + const lastAckEventId = get(context, 'event.event.id', context.initialEventId); + + return { + type: 'WEBSOCKET_SEND_EVENT', + // @ts-ignore + event: createStartStreamMessage(lastAckEventId), + }; + }); + +/* + * This action clears the socket ref from context + */ +export const clearSocket = assign({ + socket: null, +}); + +/* + * This action stores the event on the machine's context. It also asserts that + * the event being saved is higher than the last one and fails if it's not. + */ +export const storeEvent: AssignAction = assign({ + event: (context: Context, event: Event) => { + if (event.type !== 'FULLNODE_EVENT') { + return context.event; + } + + const eventId = get(event, 'event.event.id', -1); + const contextEventId = get(context, 'event.id', -1); + + if (eventId === -1) { + return; + } + + if (context.event && contextEventId > -1) { + + if (eventId < contextEventId) { + throw new Error('Event lower than last event on storeEvent action'); + } + + if (!context.initialEventId) { + // This should never happen + throw new Error('No initialEventId on context'); + } + + if (event.event.event.id < context.initialEventId) { + throw new Error('Event lower than initial event on storeEvent action'); + } + } + + return event.event; + }, +}); + +/* + * This action is used to send an ACK event to the socket actor + */ +export const sendAck = sendTo(getSocketRefFromContext, + (context: Context, _event) => { + if (!context.event) { + throw new Error('No event in context, can\'t send ack'); + } + + return { + type: EventTypes.WEBSOCKET_SEND_EVENT, + event: createSendAckMessage(context.event.event.id), + } + }); + +/* + * This action is used to raise the metadataDecided event on the machine. + * This is currently used to indicate that the metadataDiff service finished and + * yielded a result + */ +export const metadataDecided = raise((_context: Context, event: Event) => ({ + type: EventTypes.METADATA_DECIDED, + // @ts-ignore + event: event.data, +})); + +/* + * Updates the cache with the last processed event (from the context) + */ +export const updateCache = (context: Context) => { + const fullNodeEvent = context.event; + if (!fullNodeEvent) { + return; + } + const { metadata, hash } = fullNodeEvent.event.data; + const hashedTxData = hashTxData(metadata); + + context.txCache.set(hash, hashedTxData); +}; + +/* + * Starts the ping timer in the healthcheck actor +*/ +export const startHealthcheckPing = sendTo( + getHealthcheckRefFromContext, + { type: EventTypes.HEALTHCHECK_EVENT, event: { type: 'START' } }, +); + +/* + * Stops the ping timer in the healthcheck actor +*/ +export const stopHealthcheckPing = sendTo( + getHealthcheckRefFromContext, + { type: EventTypes.HEALTHCHECK_EVENT, event: { type: 'STOP' } }, +); + +/* + * Logs the event as an error log + */ +export const logEventError = (_context: Context, event: Event) => logger.error(JSON.stringify(event)); diff --git a/packages/daemon/src/actors/HealthCheckActor.ts b/packages/daemon/src/actors/HealthCheckActor.ts new file mode 100644 index 00000000..b287c60b --- /dev/null +++ b/packages/daemon/src/actors/HealthCheckActor.ts @@ -0,0 +1,109 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import axios from 'axios'; +import logger from '../logger'; +import getConfig from '../config'; +import { Event, EventTypes } from '../types'; + +/** + * Send a ping to the health-monitor server +**/ +const sendPing = async (config = getConfig()) => { + if (!config.HEALTHCHECK_SERVER_URL) { + logger.warn('Health-monitor server URL not set. Skipping ping'); + return; + } + + try { + const headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': config.HEALTHCHECK_SERVER_API_KEY + }; + const response = await axios.post( + config.HEALTHCHECK_SERVER_URL, + {}, + { headers } + ); + + if (response.status > 399) { + logger.warn(`Health-monitor returned status ${response.status}`); + } + } catch (err) { + logger.warn(`Error sending ping to health-monitor: ${err}`); + } +} + +/** + * HealthCheckActor + * + * This actor is responsible for controlling the healthcheck ping to the health-monitor server + * It will send a ping every HEALTHCHECK_PING_INTERVAL, if the feature is enabled. + * + * In case an event of type HEALTHCHECK_EVENT is received, it will start or stop the ping, + * depending on the event content. + * + * The events are received from the SyncMachine. When the SyncMachine connects to the + * full node, it will send a HEALTHCHECK_EVENT with type START, and when it disconnects or errors, it will + * send a HEALTHCHECK_EVENT with type STOP. + * + * This description could get outdated, so please check the machine code for the latest implementation. + * + **/ +export default (callback: any, receive: any, config = getConfig()) => { + if (!config.HEALTHCHECK_ENABLED) { + logger.info('Healthcheck feature is disabled. Not starting healthcheck actor'); + + return () => {}; + } + + logger.info('Starting healthcheck actor'); + + let pingTimer: NodeJS.Timer | null = null; + + const createPingTimer = () => { + if (pingTimer) { + clearPingTimer(); + } + + pingTimer = setInterval(async () => { + logger.info('Sending ping to health-monitor server'); + await sendPing(config); + }, config.HEALTHCHECK_PING_INTERVAL); + }; + + const clearPingTimer = () => { + if (pingTimer) { + clearInterval(pingTimer); + pingTimer = null; + } + }; + + receive((event: Event) => { + if (event.type !== EventTypes.HEALTHCHECK_EVENT) { + logger.warn('Event of a different type than HEALTHCHECK_EVENT reached the healthcheck actor'); + + return; + } + + if (event.event.type === 'STOP') { + logger.info('Stopping healthcheck ping'); + clearPingTimer(); + } + + if (event.event.type === 'START') { + logger.info('Starting healthcheck ping'); + createPingTimer(); + } + }); + + // Clear the interval when the actor is stopped just to be sure + return () => { + logger.info('Stopping healthcheck actor'); + clearPingTimer(); + }; +}; diff --git a/packages/daemon/src/actors/WebSocketActor.ts b/packages/daemon/src/actors/WebSocketActor.ts new file mode 100644 index 00000000..8f15b7fd --- /dev/null +++ b/packages/daemon/src/actors/WebSocketActor.ts @@ -0,0 +1,109 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { WebSocket } from 'ws'; +import { Event } from '../types'; +import { get } from 'lodash'; +import logger from '../logger'; +import { getFullnodeWsUrl } from '../utils'; + +const PING_TIMEOUT = 30000; // 30s timeout +const PING_INTERVAL = 5000; // Will ping every 5s + +export default (callback: any, receive: any) => { + const createPingTimeout = (): NodeJS.Timeout => setTimeout(() => { + socket.terminate(); + }, PING_TIMEOUT); + const createPingTimer = (): NodeJS.Timer => setInterval(() => { + logger.debug('Sending ping to server'); + socket.ping(); + }, PING_INTERVAL); + + // @ts-ignore: We already check for missing envs in startup + const socket: WebSocket = new WebSocket(getFullnodeWsUrl()); + let pingTimeout: NodeJS.Timeout = createPingTimeout(); + let pingTimer: NodeJS.Timer; + + const heartbeat = () => { + logger.debug('Pong received from server'); + clearTimeout(pingTimeout); + pingTimeout = createPingTimeout(); + }; + + receive((event: Event) => { + if (event.type !== 'WEBSOCKET_SEND_EVENT') { + logger.warn('Message that is not websocket_send_event reached the websocket actor'); + + return; + } + + if (!socket) { + logger.error('Received event but no socket yet'); + + return; + } + + const payload = JSON.stringify(event.event); + + logger.debug('Sending:') + logger.debug(payload); + socket.send(payload); + }); + + socket.on('pong', heartbeat); + + socket.onopen = () => { + // Start pinging + pingTimer = createPingTimer(); + callback({ + type: 'WEBSOCKET_EVENT', + event: { + type: 'CONNECTED', + }, + }); + }; + + socket.onmessage = (socketEvent) => { + const event = JSON.parse(socketEvent.data.toString()); + const type = get(event, 'event.type'); + + logger.debug(`Received ${type}: ${get(event, 'event.id')} from socket.`, event); + + if (!type) { + logger.error(JSON.stringify(event)); + throw new Error('Received an event with no defined type'); + } + + callback({ + type: 'FULLNODE_EVENT', + event, + }); + }; + + socket.onerror = (e) => { + logger.error('Socket erroed'); + logger.error(e); + }; + + socket.onclose = () => { + clearTimeout(pingTimeout); + clearInterval(pingTimer); + callback({ + type: 'WEBSOCKET_EVENT', + event: { + type: 'DISCONNECTED', + }, + }); + }; + + // Delete websocket connection here: + return () => { + if (socket) { + socket.close(); + } + }; +}; diff --git a/packages/daemon/src/actors/helpers.ts b/packages/daemon/src/actors/helpers.ts new file mode 100644 index 00000000..86bbe251 --- /dev/null +++ b/packages/daemon/src/actors/helpers.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const DEFAULT_WINDOW_SIZE = 1; + +export const createStartStreamMessage = (lastEventId: number) => ({ + type: 'START_STREAM', + window_size: DEFAULT_WINDOW_SIZE, + last_ack_event_id: lastEventId, +}); + +export const createSendAckMessage = (eventId: number) => ({ + type: 'ACK', + window_size: DEFAULT_WINDOW_SIZE, + ack_event_id: eventId, +}); diff --git a/packages/daemon/src/actors/index.ts b/packages/daemon/src/actors/index.ts new file mode 100644 index 00000000..e2907d54 --- /dev/null +++ b/packages/daemon/src/actors/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export { default as WebSocketActor } from './WebSocketActor'; +export { default as HealthCheckActor } from './HealthCheckActor'; +export * from './helpers'; diff --git a/packages/daemon/src/config.ts b/packages/daemon/src/config.ts new file mode 100644 index 00000000..6b157b8f --- /dev/null +++ b/packages/daemon/src/config.ts @@ -0,0 +1,115 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const requiredEnvs = [ + 'DB_ENDPOINT', + 'DB_NAME', + 'DB_USER', + 'DB_PORT', + 'DB_PASS', + 'FULLNODE_PEER_ID', + 'FULLNODE_HOST', + 'USE_SSL', + 'STREAM_ID', + 'NETWORK', + 'FULLNODE_NETWORK', + 'NEW_TX_SQS', + 'PUSH_NOTIFICATION_ENABLED', + 'WALLET_SERVICE_LAMBDA_ENDPOINT', + 'STAGE', + 'ACCOUNT_ID', + 'ALERT_MANAGER_TOPIC', + 'ALERT_MANAGER_REGION', +]; + + +export const checkEnvVariables = () => { + const missingEnv = requiredEnvs.filter(envVar => process.env[envVar] === undefined); + + if (missingEnv.length > 0) { + throw new Error(`Missing required environment variables: ${missingEnv.join(', ')}`); + } +}; + +// The service name to go with the logs +export const SERVICE_NAME = process.env.SERVICE_NAME ?? 'wallet-service-daemon'; +// The default log level +export const CONSOLE_LEVEL = process.env.CONSOLE_LEVEL ?? 'debug'; +// Number of transactions to cache in the LRU in-memory cache +export const TX_CACHE_SIZE = parseInt(process.env.TX_CACHE_SIZE ?? '10000', 10); +// Number of blocks before unlocking a block utxo +export const BLOCK_REWARD_LOCK = parseInt(process.env.BLOCK_REWARD_LOCK ?? '10', 10); +export const STAGE = process.env.STAGE; + +// Fullnode information, used to make sure we're connected to the same fullnode +export const FULLNODE_PEER_ID = process.env.FULLNODE_PEER_ID; +export const FULLNODE_HOST = process.env.FULLNODE_HOST; +export const STREAM_ID = process.env.STREAM_ID; +export const NETWORK = process.env.NETWORK; +/* The network name that comes from the fullnode events might be different from + * the network we should use to derive addresses, e.g. testnet-golf instead of + * testnet + */ +export const FULLNODE_NETWORK = process.env.FULLNODE_NETWORK; + +// Database info +export const DB_ENDPOINT = process.env.DB_ENDPOINT; +export const DB_NAME = process.env.DB_NAME; +export const DB_USER = process.env.DB_USER; +export const DB_PASS = process.env.DB_PASS; +export const DB_PORT = parseInt(process.env.DB_PORT ?? '3306', 10); + +// Lambdas info +export const NEW_TX_SQS = process.env.NEW_TX_SQS; +export const PUSH_NOTIFICATION_ENABLED = process.env.PUSH_NOTIFICATION_ENABLED === 'true'; +export const WALLET_SERVICE_LAMBDA_ENDPOINT = process.env.WALLET_SERVICE_LAMBDA_ENDPOINT; +export const ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME = process.env.ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME; +export const PUSH_NOTIFICATION_LAMBDA_REGION = process.env.PUSH_NOTIFICATION_LAMBDA_REGION; + +// AWS information +export const ACCOUNT_ID = process.env.ACCOUNT_ID; +export const ALERT_MANAGER_REGION = process.env.ALERT_MANAGER_REGION; +export const ALERT_MANAGER_TOPIC = process.env.ALERT_MANAGER_TOPIC; + +// Healthcheck configuration +export const HEALTHCHECK_ENABLED = process.env.HEALTHCHECK_ENABLED === 'true'; +export const HEALTHCHECK_SERVER_URL = process.env.HEALTHCHECK_SERVER_URL; +export const HEALTHCHECK_SERVER_API_KEY = process.env.HEALTHCHECK_SERVER_API_KEY; +export const HEALTHCHECK_PING_INTERVAL = parseInt(process.env.HEALTHCHECK_PING_INTERVAL ?? '10000', 10); // 10 seconds + +// Other +export const USE_SSL = process.env.USE_SSL; + +export default () => ({ + SERVICE_NAME, + CONSOLE_LEVEL, + TX_CACHE_SIZE, + FULLNODE_PEER_ID, + FULLNODE_HOST, + USE_SSL, + STREAM_ID, + NETWORK, + FULLNODE_NETWORK, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + NEW_TX_SQS, + PUSH_NOTIFICATION_ENABLED, + WALLET_SERVICE_LAMBDA_ENDPOINT, + STAGE, + ACCOUNT_ID, + ALERT_MANAGER_REGION, + ALERT_MANAGER_TOPIC, + ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME, + PUSH_NOTIFICATION_LAMBDA_REGION, + HEALTHCHECK_ENABLED, + HEALTHCHECK_SERVER_URL, + HEALTHCHECK_SERVER_API_KEY, + HEALTHCHECK_PING_INTERVAL, +}); diff --git a/packages/daemon/src/db/index.ts b/packages/daemon/src/db/index.ts new file mode 100644 index 00000000..43bd30a1 --- /dev/null +++ b/packages/daemon/src/db/index.ts @@ -0,0 +1,1564 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import mysql, { Connection as MysqlConnection, Pool } from 'mysql2/promise'; +import { + TokenBalanceMap, + DbTxOutput, + StringMap, + Wallet, + TxInput, + TxOutputWithIndex, + EventTxInput, + GenerateAddresses, + AddressIndexMap, + LastSyncedEvent, + AddressBalance, + AddressTotalBalance, + DbTransaction, + TokenInfo, + Miner, + TokenSymbolsRow, +} from '../types'; +import { isAuthority } from '../utils'; +import { + AddressBalanceRow, + AddressTxHistorySumRow, + BestBlockRow, + LastSyncedEventRow, + MinerRow, + TokenInformationRow, + TransactionRow, + TxOutputRow, +} from '../types'; +// @ts-ignore +import { walletUtils } from '@hathor/wallet-lib'; +import getConfig from '../config'; + +let pool: Pool; + +/** + * Get a database connection. + * + * @returns The database connection + */ +export const getDbConnection = async (): Promise => { + if (!pool) { + const { + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PORT, + DB_PASS, + } = getConfig(); + const newPool: Pool = mysql.createPool({ + host: DB_ENDPOINT, + database: DB_NAME, + user: DB_USER, + port: DB_PORT, + password: DB_PASS, + }); + + pool = newPool; + } + + return pool.getConnection(); +}; + +/** + * Add a tx to the transaction table. + * + * @remarks + * This method adds a transaction to the transaction table + * + * @param mysql - Database connection + * @param txId - Transaction id + * @param timestamp - The transaction timestamp + * @param version - The transaction version + * @param weight - the transaction weight + */ +export const addOrUpdateTx = async ( + mysql: any, + txId: string, + height: number | null, + timestamp: number, + version: number, + weight: number, +): Promise => { + const entries = [[txId, height, timestamp, version, weight]]; + + await mysql.query( + `INSERT INTO \`transaction\` (tx_id, height, timestamp, version, weight) + VALUES ? + ON DUPLICATE KEY UPDATE height = ?`, + [entries, height], + ); +}; + +/** + * Add a tx outputs to the utxo table. + * + * @remarks + * This function receives a list of outputs and supposes they're all from the same block + * or transaction. So if heighlock is set, it'll be set to all outputs. + * + * @param mysql - Database connection + * @param txId - Transaction id + * @param outputs - The transaction outputs + * @param heightlock - Block heightlock + */ +export const addUtxos = async ( + mysql: any, + txId: string, + outputs: TxOutputWithIndex[], + heightlock: number | null = null, +): Promise => { + // outputs might be empty if we're destroying authorities + if (outputs.length === 0) return; + + const entries = outputs.map( + (output) => { + let authorities = 0; + let value = output.value; + + if (isAuthority(output.token_data)) { + authorities = value; + value = 0; + } + + return [ + txId, + output.index, + output.token, + value, + authorities, + output.decoded?.address, + output.decoded?.timelock, + heightlock, + output.locked, + ]; + }, + ); + + // we are safe to ignore duplicates because our transaction might have already been in the mempool + await mysql.query( + `INSERT INTO \`tx_output\` (\`tx_id\`, \`index\`, \`token_id\`, + \`value\`, \`authorities\`, \`address\`, + \`timelock\`, \`heightlock\`, \`locked\`) + VALUES ? + ON DUPLICATE KEY UPDATE tx_id=tx_id`, + [entries], + ); +}; + +/** + * Remove a tx inputs from the utxo table. + * + * @param mysql - Database connection + * @param inputs - The transaction inputs + * @param txId - The transaction that spent these utxos + */ +export const updateTxOutputSpentBy = async (mysql: any, inputs: TxInput[], txId: string): Promise => { + const entries = inputs.map((input) => [input.tx_id, input.index]); + // entries might be empty if there are no inputs + if (entries.length) { + // get the rows before deleting + + /* We are forcing this query to use the PRIMARY index because MySQL is not using the index when there is + * more than 185 elements in the IN query. I couldn't find a reason for that. Here is the EXPLAIN with exactly 185 + * elements: + * +----+-------------+-----------+------------+-------+---------------+---------+---------+-------------+------+ + * | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | + * +----+-------------+-----------+------------+-------+---------------+---------+---------+-------------+------+ + * | 1 | UPDATE | tx_output | NULL | range | PRIMARY | PRIMARY | 259 | const,const | 250 | + * +----+-------------+-----------+------------+-------+---------------+---------+---------+-------------+------+ + * + * And here is the EXPLAIN query with exactly 186 elements: + * +----+-------------+-----------+------------+-------+---------------+---------+---------+------+---------+ + * | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | + * +----+-------------+-----------+------------+-------+---------------+---------+---------+------+---------+ + * | 1 | UPDATE | tx_output | NULL | index | NULL | PRIMARY | 259 | NULL | 1933979 | + * +----+-------------+-----------+------------+-------+---------------+---------+---------+------+---------+ + */ + await mysql.query( + `UPDATE \`tx_output\` USE INDEX (PRIMARY) + SET \`spent_by\` = ? + WHERE (\`tx_id\` ,\`index\`) + IN (?)`, + [txId, entries], + ); + } +}; + +/** + * Get a list of tx outputs from a transaction + * + * @param mysql - Database connection + * @param transaction - The hash of the transaction + + * @returns A list of tx outputs + */ +export const getTxOutputsFromTx = async ( + mysql: any, + txId: string, +): Promise => { + const [results] = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`tx_id\` = ?`, + [txId], + ); + + const utxos = []; + for (const result of results) { + const utxo: DbTxOutput = { + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + locked: result.locked > 0, + txProposalId: result.tx_proposal as string, + txProposalIndex: result.tx_proposal_index as number, + spentBy: result.spent_by ? result.spent_by as string : null, + }; + utxos.push(utxo); + } + + return utxos; +}; +/** + * Get a list of tx outputs from a list of txId and indexes + * + * @param mysql - Database connection + * @param transactions - The list of transactions + + * @returns A list of tx outputs + */ +export const getTxOutputs = async ( + mysql: any, + inputs: {txId: string, index: number}[], +): Promise => { + if (inputs.length <= 0) return []; + const txIdIndexPair = inputs.map((utxo) => [utxo.txId, utxo.index]); + const [results] = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE (\`tx_id\`, \`index\`) IN (?)`, + [txIdIndexPair], + ); + + const utxos = []; + for (const result of results) { + const utxo: DbTxOutput = { + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + locked: result.locked > 0, + txProposalId: result.tx_proposal as string, + txProposalIndex: result.tx_proposal_index as number, + spentBy: result.spent_by ? result.spent_by as string : null, + }; + utxos.push(utxo); + } + + return utxos; +}; + +/** + * Get the requested tx output. + * + * @param mysql - Database connection + * @param txId - The tx id to search + * @param index - The index to search + * @param skipSpent - Skip spent tx_output (if we want only utxos) + * @returns The requested tx_output or null if it is not found + */ +export const getTxOutput = async ( + mysql: MysqlConnection, + txId: string, + index: number, + skipSpent: boolean, +): Promise => { + const [results] = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`tx_id\` = ? + AND \`index\` = ? + ${skipSpent ? 'AND `spent_by` IS NULL' : ''} + AND \`voided\` = FALSE`, + [txId, index], + ); + + if (!results.length || results.length === 0) { + return null; + } + + const result = results[0]; + + const txOutput: DbTxOutput = mapDbResultToDbTxOutput(result); + + return txOutput; +}; + +/** + * Get tx outputs at a given height + * + * @param mysql - Database connection + * @param height - The height to search for + * + * @returns The requested tx_outputs + */ +export const getTxOutputsAtHeight = async ( + mysql: MysqlConnection, + height: number, +): Promise => { + const [results] = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`tx_id\` IN ( + SELECT tx_id + FROM transaction + WHERE height = ? + ) + AND \`voided\` = FALSE`, + [height], + ); + const rows = results; + + const utxos = []; + for (const result of rows) { + const utxo: DbTxOutput = { + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + // @ts-ignore + locked: result.locked > 0, + spentBy: result.spent_by as string, + txProposalId: result.tx_proposal as string, + txProposalIndex: result.tx_proposal_index as number, + }; + utxos.push(utxo); + } + + return utxos; +}; + +export const voidTransaction = async ( + mysql: any, + txId: string, + addressBalanceMap: StringMap, +): Promise => { + const addressEntries = Object.keys(addressBalanceMap).map((address) => [address, 0]); + await mysql.query( + `INSERT INTO \`address\`(\`address\`, \`transactions\`) + VALUES ? + ON DUPLICATE KEY UPDATE transactions = transactions - 1`, + [addressEntries], + ); + + const entries = []; + for (const [address, tokenMap] of Object.entries(addressBalanceMap)) { + for (const [token, tokenBalance] of tokenMap.iterator()) { + // update address_balance table or update balance and transactions if there's an entry already + const entry = { + address, + token_id: token, + // totalAmountSent is the sum of the value of all outputs of this token on the tx being sent to this address + // which means it is the "total_received" for this address + total_received: tokenBalance.totalAmountSent, + // if it's < 0, there must be an entry already, so it will execute "ON DUPLICATE KEY UPDATE" instead of setting it to 0 + unlocked_balance: (tokenBalance.unlockedAmount < 0 ? 0 : tokenBalance.unlockedAmount), + // this is never less than 0, as locked balance only changes when a tx is unlocked + locked_balance: tokenBalance.lockedAmount, + unlocked_authorities: tokenBalance.unlockedAuthorities.toUnsignedInteger(), + locked_authorities: tokenBalance.lockedAuthorities.toUnsignedInteger(), + timelock_expires: tokenBalance.lockExpires, + transactions: 1, + }; + + // save the smaller value of timelock_expires, when not null + await mysql.query( + `INSERT INTO address_balance + SET ? + ON DUPLICATE KEY + UPDATE total_received = total_received - ?, + unlocked_balance = unlocked_balance - ?, + locked_balance = locked_balance - ?, + transactions = transactions - 1, + timelock_expires = CASE + WHEN timelock_expires IS NULL THEN VALUES(timelock_expires) + WHEN VALUES(timelock_expires) IS NULL THEN timelock_expires + ELSE LEAST(timelock_expires, VALUES(timelock_expires)) + END, + unlocked_authorities = (unlocked_authorities | VALUES(unlocked_authorities)), + locked_authorities = locked_authorities | VALUES(locked_authorities)`, + [entry, tokenBalance.totalAmountSent, tokenBalance.unlockedAmount, tokenBalance.lockedAmount, address, token], + ); + + // if we're removing any of the authorities, we need to refresh the authority columns. Unlike the values, + // we cannot only sum/subtract, as authorities are binary: you have it or you don't. We might be spending + // an authority output in this tx without creating a new one, but it doesn't mean this address does not + // have this authority anymore, as it might have other authority outputs + if (tokenBalance.unlockedAuthorities.hasNegativeValue()) { + await mysql.query( + `UPDATE \`address_balance\` + SET \`unlocked_authorities\` = ( + SELECT BIT_OR(\`authorities\`) + FROM \`tx_output\` + WHERE \`address\` = ? + AND \`token_id\` = ? + AND \`locked\` = FALSE + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE + ) + WHERE \`address\` = ? + AND \`token_id\` = ?`, + [address, token, address, token], + ); + } + // for locked authorities, it doesn't make sense to perform the same operation. The authority needs to be + // unlocked before it can be spent. In case we're just adding new locked authorities, this will be taken + // care by the first sql query. + + // update address_tx_history with one entry for each pair (address, token) + entries.push(txId); + } + } + + await mysql.query( + `DELETE FROM \`address_tx_history\` + WHERE \`tx_id\` + IN (?)`, + [entries], + ); + + await mysql.query( + `UPDATE \`transaction\` + SET \`voided\` = TRUE + WHERE \`tx_id\` + IN (?)`, + [entries], + ); +}; + +/** + * Update addresses tables with a new transaction. + * + * @remarks + * When a new transaction arrives, it will change the balance and tx history for addresses. This function + * updates the address, address_balance and address_tx_history tables with information from this transaction. + * + * @param mysql - Database connection + * @param txId - Transaction id + * @param timestamp - Transaction timestamp + * @param addressBalanceMap - Map with the transaction's balance for each address + */ +export const updateAddressTablesWithTx = async ( + mysql: any, + txId: string, + timestamp: number, + addressBalanceMap: StringMap, +): Promise => { + /* + * update address table + * + * If an address is not yet present, add entry with index = null, walletId = null and transactions = 1. + * Later, when the corresponding wallet is started, index and walletId will be updated. + * + * If address is already present, just increment the transactions counter. + */ + const addressEntries = Object.keys(addressBalanceMap).map((address) => [address, 1]); + await mysql.query( + `INSERT INTO \`address\`(\`address\`, \`transactions\`) + VALUES ? + ON DUPLICATE KEY UPDATE transactions = transactions + 1`, + [addressEntries], + ); + + const entries = []; + for (const [address, tokenMap] of Object.entries(addressBalanceMap)) { + for (const [token, tokenBalance] of tokenMap.iterator()) { + // update address_balance table or update balance and transactions if there's an entry already + const entry = { + address, + token_id: token, + // totalAmountSent is the sum of the value of all outputs of this token on the tx being sent to this address + // which means it is the "total_received" for this address + total_received: tokenBalance.totalAmountSent, + // if it's < 0, there must be an entry already, so it will execute "ON DUPLICATE KEY UPDATE" instead of setting it to 0 + unlocked_balance: (tokenBalance.unlockedAmount < 0 ? 0 : tokenBalance.unlockedAmount), + // this is never less than 0, as locked balance only changes when a tx is unlocked + locked_balance: tokenBalance.lockedAmount, + unlocked_authorities: tokenBalance.unlockedAuthorities.toUnsignedInteger(), + locked_authorities: tokenBalance.lockedAuthorities.toUnsignedInteger(), + timelock_expires: tokenBalance.lockExpires, + transactions: 1, + }; + // save the smaller value of timelock_expires, when not null + await mysql.query( + `INSERT INTO address_balance + SET ? + ON DUPLICATE KEY + UPDATE total_received = total_received + ?, + unlocked_balance = unlocked_balance + ?, + locked_balance = locked_balance + ?, + transactions = transactions + 1, + timelock_expires = CASE + WHEN timelock_expires IS NULL THEN VALUES(timelock_expires) + WHEN VALUES(timelock_expires) IS NULL THEN timelock_expires + ELSE LEAST(timelock_expires, VALUES(timelock_expires)) + END, + unlocked_authorities = (unlocked_authorities | VALUES(unlocked_authorities)), + locked_authorities = locked_authorities | VALUES(locked_authorities)`, + [entry, tokenBalance.totalAmountSent, tokenBalance.unlockedAmount, tokenBalance.lockedAmount, address, token], + ); + + // if we're removing any of the authorities, we need to refresh the authority columns. Unlike the values, + // we cannot only sum/subtract, as authorities are binary: you have it or you don't. We might be spending + // an authority output in this tx without creating a new one, but it doesn't mean this address does not + // have this authority anymore, as it might have other authority outputs + if (tokenBalance.unlockedAuthorities.hasNegativeValue()) { + await mysql.query( + `UPDATE \`address_balance\` + SET \`unlocked_authorities\` = ( + SELECT BIT_OR(\`authorities\`) + FROM \`tx_output\` + WHERE \`address\` = ? + AND \`token_id\` = ? + AND \`locked\` = FALSE + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE + ) + WHERE \`address\` = ? + AND \`token_id\` = ?`, + [address, token, address, token], + ); + } + // for locked authorities, it doesn't make sense to perform the same operation. The authority needs to be + // unlocked before it can be spent. In case we're just adding new locked authorities, this will be taken + // care by the first sql query. + + // update address_tx_history with one entry for each pair (address, token) + entries.push([address, txId, token, tokenBalance.total(), timestamp]); + } + } + + await mysql.query( + `INSERT INTO \`address_tx_history\`(\`address\`, \`tx_id\`, + \`token_id\`, \`balance\`, + \`timestamp\`) + VALUES ?`, + [entries], + ); +}; + +/** + * Get a transaction by its ID. + * + * @param mysql - Database connection + * @param txId - A transaction ID + * @returns The requested transaction + */ +export const getTransactionById = async ( + mysql: MysqlConnection, + txId: string, +): Promise => { + const [result] = await mysql.query(` + SELECT * + FROM transaction + WHERE tx_id = ? + `, [txId]); + + if (result.length <= 0) { + return null; + } + + return result[0] as DbTransaction; +}; + +/** + * Get the utxos that are locked at a certain height. + * + * @remarks + * UTXOs from blocks are locked by height. This function returns the ones that are locked at the given height. + * + * Also, these UTXOs might have a timelock. Even though this is not common, it is also considered. + * + * @param mysql - Database connection + * @param now - Current timestamp + * @param height - The block height queried + * @returns A list of UTXOs locked at the given height + */ +export const getUtxosLockedAtHeight = async ( + mysql: MysqlConnection, + now: number, + height: number, +): Promise => { + const utxos = []; + if (height >= 0) { + const [results] = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`heightlock\` = ? + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE + AND (\`timelock\` <= ? + OR \`timelock\` is NULL) + AND \`locked\` = 1`, + [height, now], + ); + + const rows = results; + + for (const result of rows) { + const utxo: DbTxOutput = { + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + // @ts-ignore + locked: result.locked > 0, + }; + utxos.push(utxo); + } + } + return utxos; +}; + +/** + * Mark UTXOs as unlocked. + * + * @param mysql - Database connection + * @param utxos - List of UTXOs to unlock + */ +export const unlockUtxos = async (mysql: MysqlConnection, utxos: TxInput[]): Promise => { + if (utxos.length === 0) return; + const entries = utxos.map((utxo) => [utxo.tx_id, utxo.index]); + await mysql.query( + `UPDATE \`tx_output\` + SET \`locked\` = FALSE + WHERE (\`tx_id\` ,\`index\`) + IN (?)`, + [entries], + ); +}; + +/** + * Update the unlocked and locked balances for addresses. + * + * @remarks + * The balance of an address might change as a locked amount becomes unlocked. This function updates + * the address_balance table, subtracting from the locked column and adding to the unlocked column. + * + * @param mysql - Database connection + * @param addressBalanceMap - A map of addresses and the unlocked balances + * @param updateTimelock - If this update is triggered by a timelock expiring, update the next expire timestamp + */ +export const updateAddressLockedBalance = async ( + mysql: MysqlConnection, + addressBalanceMap: StringMap, + updateTimelocks = false, +): Promise => { + for (const [address, tokenBalanceMap] of Object.entries(addressBalanceMap)) { + for (const [token, tokenBalance] of tokenBalanceMap.iterator()) { + await mysql.query( + `UPDATE \`address_balance\` + SET \`unlocked_balance\` = \`unlocked_balance\` + ?, + \`locked_balance\` = \`locked_balance\` - ?, + \`unlocked_authorities\` = (unlocked_authorities | ?) + WHERE \`address\` = ? + AND \`token_id\` = ?`, [ + tokenBalance.unlockedAmount, + tokenBalance.unlockedAmount, + tokenBalance.unlockedAuthorities.toInteger(), + address, + token, + ], + ); + + // if any authority has been unlocked, we have to refresh the locked authorities + if (tokenBalance.unlockedAuthorities.toInteger() > 0) { + await mysql.query( + `UPDATE \`address_balance\` + SET \`locked_authorities\` = ( + SELECT BIT_OR(\`authorities\`) + FROM \`tx_output\` + WHERE \`address\` = ? + AND \`token_id\` = ? + AND \`locked\` = TRUE + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE) + WHERE \`address\` = ? + AND \`token_id\` = ?`, + [address, token, address, token], + ); + } + + // if this is being unlocked due to a timelock, also update the timelock_expires column + if (updateTimelocks) { + await mysql.query(` + UPDATE \`address_balance\` + SET \`timelock_expires\` = ( + SELECT MIN(\`timelock\`) + FROM \`tx_output\` + WHERE \`address\` = ? + AND \`token_id\` = ? + AND \`locked\` = TRUE + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE + ) + WHERE \`address\` = ? + AND \`token_id\` = ?`, + [address, token, address, token]); + } + } + } +}; + +/** + * Get wallet information for the given addresses. + * + * @remarks + * For each address in the list, check if it's from a started wallet and return its information. If + * address is not from a started wallet, it won't be on the final map. + * + * @param mysql - Database connection + * @param addresses - Addresses to fetch wallet information + * @returns A map of address and corresponding wallet information + */ +export const getAddressWalletInfo = async (mysql: MysqlConnection, addresses: string[]): Promise> => { + const addressWalletMap: StringMap = {}; + const [results] = await mysql.query( + `SELECT DISTINCT a.\`address\`, + a.\`wallet_id\`, + w.\`auth_xpubkey\`, + w.\`xpubkey\`, + w.\`max_gap\` + FROM \`address\` a + INNER JOIN \`wallet\` w + ON a.wallet_id = w.id + WHERE a.\`address\` + IN (?)`, + [addresses], + ); + + // @ts-ignore + for (const entry of results) { + const walletInfo: Wallet = { + walletId: entry.wallet_id as string, + authXpubkey: entry.auth_xpubkey as string, + xpubkey: entry.xpubkey as string, + maxGap: entry.max_gap as number, + }; + addressWalletMap[entry.address as string] = walletInfo; + } + return addressWalletMap; +}; + +/** + * Update the unlocked and locked balances for wallets. + * + * @remarks + * The balance of a wallet might change as a locked amount becomes unlocked. This function updates + * the wallet_balance table, subtracting from the locked column and adding to the unlocked column. + * + * @param mysql - Database connection + * @param walletBalanceMap - A map of walletId and the unlocked balances + * @param updateTimelocks - If this update is triggered by a timelock expiring, update the next lock expiration + */ +export const updateWalletLockedBalance = async ( + mysql: MysqlConnection, + walletBalanceMap: StringMap, + updateTimelocks = false, +): Promise => { + for (const [walletId, tokenBalanceMap] of Object.entries(walletBalanceMap)) { + for (const [token, tokenBalance] of tokenBalanceMap.iterator()) { + await mysql.query( + `UPDATE \`wallet_balance\` + SET \`unlocked_balance\` = \`unlocked_balance\` + ?, + \`locked_balance\` = \`locked_balance\` - ?, + \`unlocked_authorities\` = (\`unlocked_authorities\` | ?) + WHERE \`wallet_id\` = ? + AND \`token_id\` = ?`, + [tokenBalance.unlockedAmount, tokenBalance.unlockedAmount, + tokenBalance.unlockedAuthorities.toInteger(), walletId, token], + ); + + // if any authority has been unlocked, we have to refresh the locked authorities + if (tokenBalance.unlockedAuthorities.toInteger() > 0) { + await mysql.query( + `UPDATE \`wallet_balance\` + SET \`locked_authorities\` = ( + SELECT BIT_OR(\`locked_authorities\`) + FROM \`address_balance\` + WHERE \`address\` IN ( + SELECT \`address\` + FROM \`address\` + WHERE \`wallet_id\` = ?) + AND \`token_id\` = ?) + WHERE \`wallet_id\` = ? + AND \`token_id\` = ?`, + [walletId, token, walletId, token], + ); + } + + // if this is being unlocked due to a timelock, also update the timelock_expires column + if (updateTimelocks) { + await mysql.query( + `UPDATE \`wallet_balance\` + SET \`timelock_expires\` = ( + SELECT MIN(\`timelock_expires\`) + FROM \`address_balance\` + WHERE \`address\` + IN ( + SELECT \`address\` + FROM \`address\` + WHERE \`wallet_id\` = ?) + AND \`token_id\` = ?) + WHERE \`wallet_id\` = ? AND \`token_id\` = ?`, + [walletId, token, walletId, token], + ); + } + } + } +}; + +/** + * Add a miner to the database + * + * @param mysql - Database connection + */ +export const addMiner = async ( + mysql: MysqlConnection, + address: string, + txId: string, +): Promise => { + await mysql.query( + `INSERT INTO \`miner\` (address, first_block, last_block, count) + VALUES (?, ?, ?, 1) + ON DUPLICATE KEY UPDATE last_block = ?, count = count + 1`, + [address, txId, txId, txId], + ); +}; + +/** + * Get the list of miners on database + * + * @param mysql - Database connection + + * @returns A list of strings with miners addresses + */ +export const getMinersList = async ( + mysql: MysqlConnection, +): Promise => { + const [results] = await mysql.query(` + SELECT address, first_block, last_block, count + FROM miner; + `); + + const minerList: Miner[] = []; + + for (const result of results) { + minerList.push({ + address: result.address as string, + firstBlock: result.first_block as string, + lastBlock: result.last_block as string, + count: result.count as number, + }); + } + + return minerList; +}; + +/** + * Get from database utxos that must be unlocked because their timelocks expired + * + * @param mysql - Database connection + * @param now - Current timestamp + + * @returns A list of timelocked utxos + */ +export const getExpiredTimelocksUtxos = async ( + mysql: MysqlConnection, + now: number, +): Promise => { + const [results] = await mysql.query(` + SELECT * + FROM tx_output + WHERE locked = TRUE + AND timelock IS NOT NULL + AND timelock < ? + `, [now]); + + const lockedUtxos: DbTxOutput[] = results.map(mapDbResultToDbTxOutput); + + return lockedUtxos; +}; + +/** + * Maps the result from the database to DbTxOutput + * + * @param results - The tx_output results from the database + * @returns A list of tx_outputs mapped to the DbTxOutput type + */ +export const mapDbResultToDbTxOutput = (result: TxOutputRow): DbTxOutput => ({ + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + locked: result.locked ? Boolean(result.locked) : false, + txProposalId: result.tx_proposal as string, + txProposalIndex: result.tx_proposal_index as number, + spentBy: result.spent_by as string, +}); + +/** + * Store the token information. + * + * @param mysql - Database connection + * @param tokenId - The token's id + * @param tokenName - The token's name + * @param tokenSymbol - The token's symbol + */ +export const storeTokenInformation = async ( + mysql: MysqlConnection, + tokenId: string, + tokenName: string, + tokenSymbol: string, +): Promise => { + const entry = { id: tokenId, name: tokenName, symbol: tokenSymbol }; + await mysql.query( + 'INSERT INTO `token` SET ?', + [entry], + ); +}; + +/** + * Get tx inputs that are still marked as locked. + * + * @remarks + * At first, it doesn't make sense to talk about locked inputs. Any UTXO can only be spent after + * it's unlocked. However, in this service, we have a "lazy" unlock policy, only unlocking the UTXOs + * when the wallet owner requests its balance. Therefore, we might receive a transaction with a UTXO + * that is sill marked as locked in our database. That might happen if the user sends his transaction + * using a service other than this one. Otherwise the locked amount would have been updated before + * sending. + * + * @param mysql - Database connection + * @param inputs - The transaction inputs + * @returns The locked UTXOs + */ +export const getLockedUtxoFromInputs = async (mysql: MysqlConnection, inputs: EventTxInput[]): Promise => { + const entries = inputs.map((input) => [input.tx_id, input.index]); + // entries might be empty if there are no inputs + if (entries.length) { + // get the rows before deleting + const [results] = await mysql.query( + `SELECT * + FROM \`tx_output\` USE INDEX (PRIMARY) + WHERE (\`tx_id\` ,\`index\`) + IN (?) + AND \`locked\` = TRUE + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE`, + [entries], + ); + + return results.map((utxo) => ({ + txId: utxo.tx_id as string, + index: utxo.index as number, + tokenId: utxo.token_id as string, + address: utxo.address as string, + value: utxo.value as number, + authorities: utxo.authorities as number, + timelock: utxo.timelock as number, + heightlock: utxo.heightlock as number, + locked: utxo.locked ? Boolean(utxo.locked) : false, + })); + } + + return []; +}; + +/** + * Increment a list of tokens transactions count + * + * @param mysql - Database connection + * @param tokenList - The list of tokens to increment + */ +export const incrementTokensTxCount = async ( + mysql: MysqlConnection, + tokenList: string[], +): Promise => { + await mysql.query(` + UPDATE \`token\` + SET \`transactions\` = \`transactions\` + 1 + WHERE \`id\` IN (?) + `, [tokenList]); +}; + +/** + * Given an xpubkey, generate its addresses. + * + * @remarks + * Also, check which addresses are used, taking into account the maximum gap of unused addresses (maxGap). + * This function doesn't update anything on the database, just reads data from it. + * + * @param mysql - Database connection + * @param xpubkey - The xpubkey + * @param maxGap - Number of addresses that should have no transactions before we consider all addresses loaded + * @returns Object with all addresses for the given xpubkey and corresponding index + */ +export const generateAddresses = async (mysql: MysqlConnection, xpubkey: string, maxGap: number): Promise => { + const existingAddresses: AddressIndexMap = {}; + const newAddresses: AddressIndexMap = {}; + const allAddresses: string[] = []; + + // We currently generate only addresses in change derivation path 0 + // (more details in https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#Change) + // so we derive our xpub to this path and use it to get the addresses + const derivedXpub = walletUtils.xpubDeriveChild(xpubkey, 0); + + let highestCheckedIndex = -1; + let lastUsedAddressIndex = -1; + do { + const { NETWORK } = getConfig(); + const addrMap = walletUtils.getAddresses(derivedXpub, highestCheckedIndex + 1, maxGap, NETWORK); + allAddresses.push(...Object.keys(addrMap)); + + const [results] = await mysql.query( + `SELECT \`address\`, + \`index\`, + \`transactions\` + FROM \`address\` + WHERE \`address\` + IN (?)`, + [Object.keys(addrMap)], + ); + + for (const entry of results) { + const address = entry.address as string; + // get index from addrMap as the one from entry might be null + const index = addrMap[address]; + // add to existingAddresses + existingAddresses[address] = index; + + // if address is used, check if its index is higher than the current highest used index + if (entry.transactions > 0 && index > lastUsedAddressIndex) { + lastUsedAddressIndex = index; + } + + delete addrMap[address]; + } + + highestCheckedIndex += maxGap; + Object.assign(newAddresses, addrMap); + } while (lastUsedAddressIndex + maxGap > highestCheckedIndex); + + // we probably generated more addresses than needed, as we always generate + // addresses in maxGap blocks + const totalAddresses = lastUsedAddressIndex + maxGap + 1; + for (const [address, index] of Object.entries(newAddresses)) { + if (index > lastUsedAddressIndex + maxGap) { + delete newAddresses[address]; + } + } + + return { + addresses: allAddresses.slice(0, totalAddresses), + newAddresses, + existingAddresses, + lastUsedAddressIndex, + }; +}; + +/** + * Add addresses to address table. + * + * @remarks + * The addresses are added with the given walletId and 0 transactions. + * + * @param mysql - Database connection + * @param walletId - The wallet id + * @param addresses - A map of addresses and corresponding indexes + */ +export const addNewAddresses = async ( + mysql: MysqlConnection, + walletId: string, + addresses: AddressIndexMap, + lastUsedAddressIndex: number, +): Promise => { + if (Object.keys(addresses).length === 0) return; + const entries = []; + for (const [address, index] of Object.entries(addresses)) { + entries.push([address, index, walletId, 0]); + } + await mysql.query( + `INSERT INTO \`address\`(\`address\`, \`index\`, + \`wallet_id\`, \`transactions\`) + VALUES ?`, + [entries], + ); + + // Store on the wallet table the highest used index + await mysql.query( + `UPDATE \`wallet\` + SET \`last_used_address_index\` = ? + WHERE \`id\` = ?`, + [lastUsedAddressIndex, walletId], + ); +}; + +/** + * Update a wallet's balance and tx history with a new transaction. + * + * @remarks + * When a new transaction arrives, it can change the balance and tx history for the wallets. This function + * updates the wallet_balance and wallet_tx_history tables with information from this transaction. + * + * @param mysql - Database connection + * @param txId - Transaction id + * @param timestamp - Transaction timestamp + * @param walletBalanceMap - Map with the transaction's balance for each wallet (by walletId) + */ +export const updateWalletTablesWithTx = async ( + mysql: MysqlConnection, + txId: string, + timestamp: number, + walletBalanceMap: StringMap, +): Promise => { + const entries = []; + for (const [walletId, tokenBalanceMap] of Object.entries(walletBalanceMap)) { + for (const [token, tokenBalance] of tokenBalanceMap.iterator()) { + // on wallet_balance table, balance cannot be negative (it's unsigned). That's why we use balance + // as (tokenBalance < 0 ? 0 : tokenBalance). In case the wallet's balance in this tx is negative, + // there must necessarily be an entry already and we'll fall on the ON DUPLICATE KEY case, so the + // entry value won't be used. We'll just update balance = balance + tokenBalance + const entry = { + wallet_id: walletId, + token_id: token, + // totalAmountSent is the sum of the value of all outputs of this token on the tx being sent to this address + // which means it is the "total_received" for this wallet + total_received: tokenBalance.totalAmountSent, + unlocked_balance: (tokenBalance.unlockedAmount < 0 ? 0 : tokenBalance.unlockedAmount), + locked_balance: tokenBalance.lockedAmount, + unlocked_authorities: tokenBalance.unlockedAuthorities.toUnsignedInteger(), + locked_authorities: tokenBalance.lockedAuthorities.toUnsignedInteger(), + timelock_expires: tokenBalance.lockExpires, + transactions: 1, + }; + + // save the smaller value of timelock_expires, when not null + await mysql.query( + `INSERT INTO wallet_balance + SET ? + ON DUPLICATE KEY + UPDATE total_received = total_received + ?, + unlocked_balance = unlocked_balance + ?, + locked_balance = locked_balance + ?, + transactions = transactions + 1, + timelock_expires = CASE WHEN timelock_expires IS NULL THEN VALUES(timelock_expires) + WHEN VALUES(timelock_expires) IS NULL THEN timelock_expires + ELSE LEAST(timelock_expires, VALUES(timelock_expires)) + END, + unlocked_authorities = (unlocked_authorities | VALUES(unlocked_authorities)), + locked_authorities = locked_authorities | VALUES(locked_authorities)`, + [entry, tokenBalance.totalAmountSent, tokenBalance.unlockedAmount, tokenBalance.lockedAmount, walletId, token], + ); + + // same logic here as in the updateAddressTablesWithTx function + if (tokenBalance.unlockedAuthorities.hasNegativeValue()) { + // If we got here, it means that we spent an authority, so we need to update the table to refresh the current + // value. + // To do that, we get all unlocked_authorities from all addresses (querying by wallet and token_id) and + // bitwise OR them with each other. + await mysql.query( + `UPDATE \`wallet_balance\` + SET \`unlocked_authorities\` = ( + SELECT BIT_OR(\`unlocked_authorities\`) + FROM \`address_balance\` + WHERE \`address\` IN ( + SELECT \`address\` + FROM \`address\` + WHERE \`wallet_id\` = ?) + AND \`token_id\` = ?) + WHERE \`wallet_id\` = ? + AND \`token_id\` = ?`, + [walletId, token, walletId, token], + ); + } + + entries.push([walletId, token, txId, tokenBalance.total(), timestamp]); + } + } + + if (entries.length > 0) { + await mysql.query( + `INSERT INTO \`wallet_tx_history\` (\`wallet_id\`, \`token_id\`, + \`tx_id\`, \`balance\`, + \`timestamp\`) + VALUES ?`, + [entries], + ); + } +}; + +/** + * Alias for addOrUpdateTx + * + * @remarks + * This method is simply an alias for addOrUpdateTx in the current implementation. + * + * @param mysql - Database connection + * @param txId - Transaction id + * @param timestamp - The transaction timestamp + * @param version - The transaction version + * @param weight - The transaction weight + */ +export const updateTx = async ( + mysql: MysqlConnection, + txId: string, + height: number, + timestamp: number, + version: number, + weight: number, +): Promise => addOrUpdateTx(mysql, txId, height, timestamp, version, weight); + +/** + * Get a list of tx outputs from their spent_by txId + * + * @param mysql - Database connection + * @param txIds - The list of transactions that spent the tx_outputs we are querying + + * @returns A list of tx_outputs + */ +export const getTxOutputsBySpent = async ( + mysql: MysqlConnection, + txIds: string[], +): Promise => { + const [results] = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`spent_by\` IN (?)`, + [txIds], + ); + + const utxos = []; + for (const result of results) { + const utxo: DbTxOutput = { + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + locked: result.locked ? Boolean(result.locked) : false, + txProposalId: result.tx_proposal as string, + txProposalIndex: result.tx_proposal_index as number, + spentBy: result.spent_by ? result.spent_by as string : null, + }; + + utxos.push(utxo); + } + + return utxos; +}; + +/** + * Set a list of tx_outputs as unspent + * + * @param mysql - Database connection + * @param txOutputs - The list of tx_outputs to unspend + */ +export const unspendUtxos = async ( + mysql: MysqlConnection, + txOutputs: DbTxOutput[], +): Promise => { + const txIdIndexList = txOutputs.map((txOutput) => [txOutput.txId, txOutput.index]); + + await mysql.query( + `UPDATE \`tx_output\` + SET \`spent_by\` = NULL + WHERE (\`tx_id\`, \`index\`) IN (?)`, + [txIdIndexList], + ); +}; + +/** + * Deletes utxos from the tx_outputs table + * + * @param mysql - Database connection + * @param utxos - The list of utxos to delete from the database + */ +export const markUtxosAsVoided = async ( + mysql: MysqlConnection, + utxos: DbTxOutput[], +): Promise => { + const txIds = utxos.map((tx) => tx.txId); + + await mysql.query(` + UPDATE \`tx_output\` + SET \`voided\` = TRUE + WHERE \`tx_id\` IN (?)`, + [txIds]); +}; + +export const updateLastSyncedEvent = async ( + mysql: MysqlConnection, + lastEventId: number, +): Promise => { + await mysql.query(` + INSERT INTO \`sync_metadata\` (\`id\`, \`last_event_id\`) + VALUES (0, ?) +ON DUPLICATE KEY + UPDATE last_event_id = ?`, + [lastEventId, lastEventId]); +}; + +export const getLastSyncedEvent = async ( + mysql: MysqlConnection, +): Promise => { + const [results] = await mysql.query( + `SELECT * FROM \`sync_metadata\` LIMIT 1`, + [], + ); + + if (!results.length) { + return null; + } + + const lastSyncedEvent: LastSyncedEvent = { + id: results[0].id, + last_event_id: results[0].last_event_id, + updated_at: results[0].updated_at, + }; + + return lastSyncedEvent; +}; + +export const getBestBlockHeight = async ( + mysql: MysqlConnection, +): Promise => { + const [results] = await mysql.query( + `SELECT MAX(height) AS height + FROM \`transaction\` + LIMIT 1`, + [], + ); + + const maxHeight = results[0].height; + + return maxHeight; +}; + +/** + * Retrieves a list of `AddressBalance`s from a list of addresses + * + * @param mysql - Database connection + * @param addresses - The addresses to query + */ +export const fetchAddressBalance = async ( + mysql: MysqlConnection, + addresses: string[], +): Promise => { + const [results] = await mysql.query( + `SELECT * + FROM \`address_balance\` + WHERE \`address\` IN (?) + ORDER BY \`address\`, \`token_id\``, + [addresses], + ); + + return results.map((result): AddressBalance => ({ + address: result.address as string, + tokenId: result.token_id as string, + unlockedBalance: result.unlocked_balance as number, + lockedBalance: result.locked_balance as number, + lockedAuthorities: result.locked_authorities as number, + unlockedAuthorities: result.unlocked_authorities as number, + timelockExpires: result.timelock_expires as number, + transactions: result.transactions as number, + })); +}; + +/** + * Retrieves a list of `AddressTotalBalance`s from a list of addresses + * + * @param mysql - Database connection + * @param addresses - The addresses to query + */ +export const fetchAddressTxHistorySum = async ( + mysql: MysqlConnection, + addresses: string[], +): Promise => { + const [results] = await mysql.query( + `SELECT address, + token_id, + SUM(\`balance\`) AS balance, + COUNT(\`tx_id\`) AS transactions + FROM \`address_tx_history\` + WHERE \`address\` IN (?) + AND \`voided\` = FALSE + GROUP BY address, token_id + ORDER BY address, token_id`, + [addresses], + ); + + return results.map((result): AddressTotalBalance => ({ + address: result.address as string, + tokenId: result.token_id as string, + balance: parseInt(result.balance), + transactions: parseInt(result.transactions), + })); +}; + +export const getTxOutputsHeightUnlockedAtHeight = async ( + mysql: MysqlConnection, + height: number, +): Promise => { + const [results] = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`heightlock\` = ? + AND \`voided\` = FALSE`, + [height], + ); + + const utxos = []; + for (const result of results) { + const utxo: DbTxOutput = { + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + locked: result.locked ? Boolean(result.locked) : false, + txProposalId: result.tx_proposal as string, + txProposalIndex: result.tx_proposal_index as number, + spentBy: result.spent_by ? result.spent_by as string : null, + }; + utxos.push(utxo); + } + + return utxos; +}; + +/** + * Get the token information. + * + * @param mysql - Database connection + * @param tokenId - The token's id + * @returns The token information (or null if id is not found) + */ +export const getTokenInformation = async ( + mysql: MysqlConnection, + tokenId: string, +): Promise => { + const [results] = await mysql.query( + 'SELECT * FROM `token` WHERE `id` = ?', + [tokenId], + ); + + if (results.length === 0) return null; + + return new TokenInfo(tokenId, results[0].name as string, results[0].symbol as string); +}; + +/** + * Cleanup all records from a transaction that was voided in the past + * + * @remarks + * This does not re-calculates balances, so it's only supposed to be used to clear + * the tx_output, address_tx_history and wallet_tx_history tables + * + * @param mysql - Database connection + * @param txId - The transaction to clear from database + */ +export const cleanupVoidedTx = async (mysql: MysqlConnection, txId: string): Promise => { + await mysql.query( + `DELETE FROM \`transaction\` + WHERE tx_id = ? + AND voided = true`, + [txId], + ); + + await mysql.query( + `DELETE FROM \`tx_output\` + WHERE tx_id = ? + AND voided = true`, + [txId], + ); + + await mysql.query( + `DELETE FROM \`address_tx_history\` + WHERE tx_id = ? + AND voided = true`, + [txId], + ); + + await mysql.query( + `DELETE FROM \`wallet_tx_history\` + WHERE tx_id = ? + AND voided = true`, + [txId], + ); +}; + +/** + * Get token symbol map, correlating token id to its symbol. + * + * @param mysql - Database connection + * @param tokenIdList - A list of token ids + * @returns The token information (or null if id is not found) + * + * @todo This method is duplicated from the wallet-service lambdas, + * we should have common methods for both packages + */ +export const getTokenSymbols = async ( + mysql: MysqlConnection, + tokenIdList: string[], +): Promise | null> => { + if (tokenIdList.length === 0) return null; + + const [results] = await mysql.query( + 'SELECT `id`, `symbol` FROM `token` WHERE `id` IN (?)', + [tokenIdList], + ); + + if (results.length === 0) return null; + return results.reduce((prev: Record, token: { id: string, symbol: string }) => { + // eslint-disable-next-line no-param-reassign + prev[token.id] = token.symbol; + return prev; + }, {}) as unknown as StringMap; +}; diff --git a/packages/daemon/src/delays/index.ts b/packages/daemon/src/delays/index.ts new file mode 100644 index 00000000..0ff5a4c9 --- /dev/null +++ b/packages/daemon/src/delays/index.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Context } from '../types'; + +const RETRY_BACKOFF_INCREASE = 1000; // 1s increase in the backoff strategy +const MAX_BACKOFF_RETRIES = 10; // The retry backoff will top at 10s + +export const BACKOFF_DELAYED_RECONNECT = (context: Context) => { + if (context.retryAttempt > MAX_BACKOFF_RETRIES) { + return MAX_BACKOFF_RETRIES * RETRY_BACKOFF_INCREASE; + } + + return context.retryAttempt * RETRY_BACKOFF_INCREASE; +}; diff --git a/packages/daemon/src/guards/index.ts b/packages/daemon/src/guards/index.ts new file mode 100644 index 00000000..56bf8aaf --- /dev/null +++ b/packages/daemon/src/guards/index.ts @@ -0,0 +1,226 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Context, Event, EventTypes, FullNodeEventTypes } from '../types'; +import { hashTxData } from '../utils'; +import { METADATA_DIFF_EVENT_TYPES } from '../services'; +import getConfig from '../config'; +import logger from '../logger'; + +/* + * This guard is used during the `handlingMetadataChanged` to check if + * the result was an IGNORE event + */ +export const metadataIgnore = (_context: Context, event: Event) => { + if (event.type !== EventTypes.METADATA_DECIDED) { + throw new Error(`Invalid event type on metadataIgnore guard: ${event.type}`); + } + + return event.event.type === METADATA_DIFF_EVENT_TYPES.IGNORE; +}; + +/* + * This guard is used during the `handlingMetadataChanged` to check if + * the result was a TX_VOIDED event + */ +export const metadataVoided = (_context: Context, event: Event) => { + if (event.type !== EventTypes.METADATA_DECIDED) { + throw new Error(`Invalid event type on metadataVoided guard: ${event.type}`); + } + + return event.event.type === METADATA_DIFF_EVENT_TYPES.TX_VOIDED; +}; + +/* + * This guard is used during the `handlingMetadataChanged` to check if + * the result was a TX_UNVOIDED event, which means the tx was voided + * and then got unvoided + */ +export const metadataUnvoided = (_context: Context, event: Event) => { + if (event.type !== EventTypes.METADATA_DECIDED) { + throw new Error(`Invalid event type on metadataUnvoided guard: ${event.type}`); + } + + return event.event.type === METADATA_DIFF_EVENT_TYPES.TX_UNVOIDED; +}; + +/* + * This guard is used during the `handlingMetadataChanged` to check if + * the result was a TX_NEW event, which means that we should insert + * this transaction on the database + */ +export const metadataNewTx = (_context: Context, event: Event) => { + if (event.type !== EventTypes.METADATA_DECIDED) { + throw new Error(`Invalid event type on metadataNewTx guard: ${event.type}`); + } + + return event.event.type === METADATA_DIFF_EVENT_TYPES.TX_NEW; +}; + +/* + * This guard is used during the `handlingMetadataChanged` to check if + * the result was a TX_FIRST_BLOCK event, which means that we should insert + * the height of this transaction to the database + */ +export const metadataFirstBlock = (_context: Context, event: Event) => { + if (event.type !== EventTypes.METADATA_DECIDED) { + throw new Error(`Invalid event type on metadataFirstBlock guard: ${event.type}`); + } + + return event.event.type === METADATA_DIFF_EVENT_TYPES.TX_FIRST_BLOCK; +}; + +/* + * This guard is used on the `idle` state when an event is received + * from the fullnode to detect if this event is a VERTEX_METADATA_CHANGED + * event + */ +export const metadataChanged = (_context: Context, event: Event) => { + if (event.type !== EventTypes.FULLNODE_EVENT) { + throw new Error(`Invalid event type on metadataChanged guard: ${event.type}`); + } + + return event.event.event.type === FullNodeEventTypes.VERTEX_METADATA_CHANGED; +}; + +/* + * This guard is used on the `idle` state when an event is received + * from the fullnode to detect if this event is a NEW_VERTEX_ACCEPTED + * event + */ +export const vertexAccepted = (_context: Context, event: Event) => { + if (event.type !== EventTypes.FULLNODE_EVENT) { + throw new Error(`Invalid event type on vertexAccepted guard: ${event.type}`); + } + + return event.event.event.type === FullNodeEventTypes.NEW_VERTEX_ACCEPTED; +}; + +/* + * This guard is used on each event that is received from the fullnode to detect + * if the received peer_id is the same as we expect (from an env var) + */ +export const invalidPeerId = (_context: Context, event: Event) => { + if (event.type !== EventTypes.FULLNODE_EVENT) { + throw new Error(`Invalid event type on invalidPeerId guard: ${event.type}`); + } + const { FULLNODE_PEER_ID } = getConfig(); + + // @ts-ignore + const isInvalid = event.event.peer_id !== FULLNODE_PEER_ID; + + if (isInvalid) { + logger.error(`Invalid peer id. Expected ${FULLNODE_PEER_ID}, got ${event.event.peer_id}`); + } + + return isInvalid; +}; + +/* + * This guard is used on each event that is received from the fullnode to detect + * if the received network is the same as we expect (from an env var) + */ +export const invalidNetwork = (_context: Context, event: Event) => { + if (event.type !== EventTypes.FULLNODE_EVENT) { + throw new Error(`Invalid event type on invalidNetwork guard: ${event.type}`); + } + const { FULLNODE_NETWORK } = getConfig(); + + const isInvalid = event.event.network !== FULLNODE_NETWORK; + + if (isInvalid) { + logger.error(`Invalid network. Expected ${FULLNODE_NETWORK}, got ${event.event.network}`); + } + + return isInvalid; +}; + +/* + * This guard is used on each event that is received from the fullnode to detect + * if the received stream_id is the same as we expect (from an env var). + * This makes sure that the order of the events is the same. + */ +export const invalidStreamId = (_context: Context, event: Event) => { + if (event.type !== EventTypes.FULLNODE_EVENT) { + throw new Error(`Invalid event type on invalidStreamId guard: ${event.type}`); + } + const { STREAM_ID } = getConfig(); + + const isInvalid = event.event.stream_id !== STREAM_ID; + + if (isInvalid) { + logger.error(`Invalid stream id. Expected ${STREAM_ID}, got ${event.event.stream_id}`); + } + + return isInvalid; +} + +export const websocketDisconnected = (_context: Context, event: Event) => { + if (event.type !== EventTypes.WEBSOCKET_EVENT) { + throw new Error(`Invalid event type on websocketDisconnected guard: ${event.type}`); + } + + if (event.event.type === 'DISCONNECTED') { + return true; + } + + return false; +}; + +/* + * This guard is used in the `idle` state to detect if the transaction in the + * received event is voided, this can serve many functions, one of them is to + * ignore transactions that we don't have on our database but are already voided + */ +export const voided = (_context: Context, event: Event) => { + if (event.type !== EventTypes.FULLNODE_EVENT) { + throw new Error(`Invalid event type on voided guard: ${event.type}`); + } + + if (event.event.event.type !== FullNodeEventTypes.VERTEX_METADATA_CHANGED + && event.event.event.type !== FullNodeEventTypes.NEW_VERTEX_ACCEPTED) { + return false; + } + + const fullNodeEvent = event.event.event; + const { metadata: { voided_by } } = fullNodeEvent.data; + + return voided_by.length > 0; +}; + +/* + * This guard is used to check our transaction cache to see if any of the fields + * we monitor are changed. + * + * The idea is to ignore, without querying the database, events that don't change + * any of the fields we are interested on + */ +export const unchanged = (context: Context, event: Event) => { + if (event.type !== EventTypes.FULLNODE_EVENT) { + throw new Error(`Invalid event type on unchanged guard: ${event.type}`); + } + + if (event.event.event.type !== FullNodeEventTypes.VERTEX_METADATA_CHANGED + && event.event.event.type !== FullNodeEventTypes.NEW_VERTEX_ACCEPTED) { + + // Not unchanged + return false; + } + + const { data } = event.event.event; + + const txCache = context.txCache; + const txHashFromCache = txCache.get(data.hash); + // Not on the cache, it's not unchanged. + if (!txHashFromCache) { + return false; + } + + const txHashFromEvent = hashTxData(data.metadata); + + return txHashFromCache === txHashFromEvent; +}; diff --git a/packages/daemon/src/index.ts b/packages/daemon/src/index.ts new file mode 100644 index 00000000..49e3d308 --- /dev/null +++ b/packages/daemon/src/index.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { interpret } from 'xstate'; +import { SyncMachine } from './machines'; +import logger from './logger'; +import { checkEnvVariables } from './config'; + +const main = async () => { + checkEnvVariables(); + // Interpret the machine (start it and listen to its state changes) + const machine = interpret(SyncMachine); + + machine.onTransition((state) => { + logger.info(`Transitioned to ${JSON.stringify(state.value)}`); + }); + + machine.onEvent((event) => { + logger.info(`Processing event: ${JSON.stringify(event.type)}`); + }); + + machine.start(); +}; + +main(); diff --git a/packages/daemon/src/logger.ts b/packages/daemon/src/logger.ts new file mode 100644 index 00000000..41d53f9a --- /dev/null +++ b/packages/daemon/src/logger.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import { createLogger, format, transports } from 'winston'; +import getConfig from './config'; + +const { SERVICE_NAME, CONSOLE_LEVEL } = getConfig(); + +export default createLogger({ + level: CONSOLE_LEVEL, + format: format.combine( + format.colorize(), + format.timestamp(), + format.printf(({ timestamp, level, message }) => ( + `${timestamp} [${SERVICE_NAME}][${level}]: ${message}` + )), + ), + transports: [ + new transports.Console(), + ], +}); diff --git a/packages/daemon/src/machines/SyncMachine.ts b/packages/daemon/src/machines/SyncMachine.ts new file mode 100644 index 00000000..10c7982d --- /dev/null +++ b/packages/daemon/src/machines/SyncMachine.ts @@ -0,0 +1,308 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + Machine, + assign, + spawn, +} from 'xstate'; +import { LRU } from '../utils'; +import { WebSocketActor, HealthCheckActor } from '../actors'; +import { + Context, + Event, +} from '../types'; +import { + handleVertexAccepted, + metadataDiff, + handleVoidedTx, + handleTxFirstBlock, + updateLastSyncedEvent, + fetchInitialState, + handleUnvoidedTx, +} from '../services'; +import { + metadataIgnore, + metadataVoided, + metadataUnvoided, + metadataNewTx, + metadataFirstBlock, + metadataChanged, + vertexAccepted, + invalidPeerId, + invalidStreamId, + invalidNetwork, + websocketDisconnected, + voided, + unchanged, +} from '../guards'; +import { + storeInitialState, + unwrapEvent, + startStream, + clearSocket, + storeEvent, + sendAck, + metadataDecided, + increaseRetry, + logEventError, + updateCache, + startHealthcheckPing, + stopHealthcheckPing, +} from '../actions'; +import { BACKOFF_DELAYED_RECONNECT } from '../delays'; +import getConfig from '../config'; + +export const SYNC_MACHINE_STATES = { + INITIALIZING: 'INITIALIZING', + CONNECTING: 'CONNECTING', + CONNECTED: 'CONNECTED', + RECONNECTING: 'RECONNECTING', + ERROR: 'ERROR', +}; + +export const CONNECTED_STATES = { + idle: 'idle', + handlingUnhandledEvent: 'handlingUnhandledEvent', + handlingMetadataChanged: 'handlingMetadataChanged', + handlingVertexAccepted: 'handlingVertexAccepted', + handlingVoidedTx: 'handlingVoidedTx', + handlingUnvoidedTx: 'handlingUnvoidedTx', + handlingFirstBlock: 'handlingFirstBlock', +}; + +const { TX_CACHE_SIZE } = getConfig(); + +const SyncMachine = Machine({ + id: 'SyncMachine', + initial: SYNC_MACHINE_STATES.INITIALIZING, + context: { + socket: null, + healthcheck: null, + retryAttempt: 0, + event: null, + initialEventId: null, + txCache: new LRU(TX_CACHE_SIZE), + }, + states: { + [SYNC_MACHINE_STATES.INITIALIZING]: { + entry: assign({ + healthcheck: () => spawn(HealthCheckActor), + }), + invoke: { + src: 'fetchInitialState', + onDone: { + actions: ['storeInitialState'], + target: SYNC_MACHINE_STATES.CONNECTING, + }, + onError: { + target: `#${SYNC_MACHINE_STATES.ERROR}`, + }, + }, + }, + [SYNC_MACHINE_STATES.CONNECTING]: { + entry: assign({ + socket: () => spawn(WebSocketActor), + }), + on: { + WEBSOCKET_EVENT: [{ + cond: 'websocketDisconnected', + target: SYNC_MACHINE_STATES.RECONNECTING, + }, { + target: SYNC_MACHINE_STATES.CONNECTED, + }], + }, + }, + [SYNC_MACHINE_STATES.RECONNECTING]: { + onEntry: ['clearSocket', 'increaseRetry', 'stopHealthcheckPing'], + after: { + BACKOFF_DELAYED_RECONNECT: SYNC_MACHINE_STATES.CONNECTING, + }, + }, + [SYNC_MACHINE_STATES.CONNECTED]: { + id: SYNC_MACHINE_STATES.CONNECTED, + initial: CONNECTED_STATES.idle, + entry: ['startStream', 'startHealthcheckPing'], + states: { + [CONNECTED_STATES.idle]: { + id: CONNECTED_STATES.idle, + on: { + FULLNODE_EVENT: [{ + cond: 'invalidStreamId', + target: `#${SYNC_MACHINE_STATES.ERROR}`, + }, { + cond: 'invalidPeerId', + target: `#${SYNC_MACHINE_STATES.ERROR}`, + }, { + cond: 'invalidNetwork', + target: `#${SYNC_MACHINE_STATES.ERROR}`, + }, { + actions: ['storeEvent', 'sendAck'], + cond: 'unchanged', + target: CONNECTED_STATES.idle, + }, { + actions: ['storeEvent'], + cond: 'metadataChanged', + target: CONNECTED_STATES.handlingMetadataChanged, + }, { + actions: ['storeEvent', 'sendAck'], + /* If the transaction is already voided and is not + * VERTEX_METADATA_CHANGED, we should ignore it. + */ + cond: 'voided', + target: CONNECTED_STATES.idle, + }, { + actions: ['storeEvent'], + cond: 'vertexAccepted', + target: CONNECTED_STATES.handlingVertexAccepted, + }, { + actions: ['storeEvent'], + target: CONNECTED_STATES.handlingUnhandledEvent, + }], + }, + }, + [CONNECTED_STATES.handlingUnhandledEvent]: { + id: CONNECTED_STATES.handlingUnhandledEvent, + invoke: { + src: 'updateLastSyncedEvent', + onDone: { + actions: ['sendAck'], + target: 'idle', + }, + onError: `#${SYNC_MACHINE_STATES.ERROR}`, + }, + }, + [CONNECTED_STATES.handlingMetadataChanged]: { + id: 'handlingMetadataChanged', + initial: 'detectingDiff', + states: { + detectingDiff: { + invoke: { + src: 'metadataDiff', + onDone: { actions: ['metadataDecided'] }, + }, + on: { + METADATA_DECIDED: [ + { target: `#${CONNECTED_STATES.handlingVoidedTx}`, cond: 'metadataVoided', actions: ['unwrapEvent'] }, + { target: `#${CONNECTED_STATES.handlingUnvoidedTx}`, cond: 'metadataUnvoided', actions: ['unwrapEvent'] }, + { target: `#${CONNECTED_STATES.handlingVertexAccepted}`, cond: 'metadataNewTx', actions: ['unwrapEvent'] }, + { target: `#${CONNECTED_STATES.handlingFirstBlock}`, cond: 'metadataFirstBlock', actions: ['unwrapEvent'] }, + { target: `#${CONNECTED_STATES.handlingUnhandledEvent}`, cond: 'metadataIgnore' }, + ], + }, + }, + }, + }, + // We have the unchanged guard, so it's guaranteed that this is a new tx + [CONNECTED_STATES.handlingVertexAccepted]: { + id: CONNECTED_STATES.handlingVertexAccepted, + invoke: { + src: 'handleVertexAccepted', + data: (_context: Context, event: Event) => event, + onDone: { + target: 'idle', + actions: ['sendAck', 'storeEvent', 'updateCache'], + }, + onError: `#${SYNC_MACHINE_STATES.ERROR}`, + }, + }, + [CONNECTED_STATES.handlingVoidedTx]: { + id: CONNECTED_STATES.handlingVoidedTx, + invoke: { + src: 'handleVoidedTx', + data: (_context: Context, event: Event) => event, + onDone: { + target: 'idle', + actions: ['storeEvent', 'sendAck', 'updateCache'], + }, + onError: `#${SYNC_MACHINE_STATES.ERROR}`, + }, + }, + [CONNECTED_STATES.handlingUnvoidedTx]: { + id: CONNECTED_STATES.handlingUnvoidedTx, + invoke: { + src: 'handleUnvoidedTx', + data: (_context: Context, event: Event) => event, + onDone: { + // The handleUnvoidedTx will remove the tx from the database, we should + // re-add it: + target: `#${CONNECTED_STATES.handlingVertexAccepted}`, + // We shouldn't send ACK, as we'll send the ACK after handlingVertexAccepted + actions: ['storeEvent', 'updateCache'], + }, + onError: `#${SYNC_MACHINE_STATES.ERROR}`, + }, + }, + [CONNECTED_STATES.handlingFirstBlock]: { + id: CONNECTED_STATES.handlingFirstBlock, + invoke: { + src: 'handleTxFirstBlock', + data: (_context: Context, event: Event) => event, + onDone: { + target: 'idle', + actions: ['storeEvent', 'sendAck', 'updateCache'], + }, + onError: `#${SYNC_MACHINE_STATES.ERROR}`, + }, + }, + }, + on: { + WEBSOCKET_EVENT: [{ + cond: 'websocketDisconnected', + target: SYNC_MACHINE_STATES.RECONNECTING, + }], + }, + }, + [SYNC_MACHINE_STATES.ERROR]: { + id: SYNC_MACHINE_STATES.ERROR, + type: 'final', + onEntry: ['logEventError', 'stopHealthcheckPing'], + }, + }, +}, { + guards: { + invalidStreamId, + invalidPeerId, + invalidNetwork, + metadataIgnore, + metadataVoided, + metadataUnvoided, + metadataNewTx, + metadataFirstBlock, + metadataChanged, + vertexAccepted, + websocketDisconnected, + voided, + unchanged, + }, + delays: { BACKOFF_DELAYED_RECONNECT }, + actions: { + storeInitialState, + unwrapEvent, + startStream, + clearSocket, + storeEvent, + sendAck, + metadataDecided, + increaseRetry, + logEventError, + updateCache, + startHealthcheckPing, + stopHealthcheckPing, + }, + services: { + handleVoidedTx, + handleUnvoidedTx, + handleVertexAccepted, + handleTxFirstBlock, + metadataDiff, + updateLastSyncedEvent, + fetchInitialState, + }, +}); + +export default SyncMachine; diff --git a/packages/daemon/src/machines/index.ts b/packages/daemon/src/machines/index.ts new file mode 100644 index 00000000..4feba97e --- /dev/null +++ b/packages/daemon/src/machines/index.ts @@ -0,0 +1,2 @@ +export { default as SyncMachine } from './SyncMachine'; +export * from './SyncMachine'; diff --git a/packages/daemon/src/services/index.ts b/packages/daemon/src/services/index.ts new file mode 100644 index 00000000..98210a2e --- /dev/null +++ b/packages/daemon/src/services/index.ts @@ -0,0 +1,531 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// @ts-ignore +import hathorLib from '@hathor/wallet-lib'; +import axios from 'axios'; +import { get } from 'lodash'; +import { + TxOutputWithIndex, + StringMap, + TokenBalanceMap, + TxInput, + Wallet, + DbTxOutput, + DbTransaction, + LastSyncedEvent, + Event, + Context, + FullNodeEvent, + Transaction, +} from '../types'; +import { + prepareOutputs, + getAddressBalanceMap, + getUnixTimestamp, + unlockUtxos, + unlockTimelockedUtxos, + prepareInputs, + markLockedOutputs, + getTokenListFromInputsAndOutputs, + getWalletBalanceMap, + validateAddressBalances, + getWalletBalancesForTx, + getFullnodeHttpUrl, +} from '../utils'; +import { + getDbConnection, + addOrUpdateTx, + addUtxos, + updateTxOutputSpentBy, + updateAddressTablesWithTx, + getTransactionById, + getUtxosLockedAtHeight, + addMiner, + storeTokenInformation, + getLockedUtxoFromInputs, + incrementTokensTxCount, + getAddressWalletInfo, + generateAddresses, + addNewAddresses, + updateWalletTablesWithTx, + voidTransaction, + updateLastSyncedEvent as dbUpdateLastSyncedEvent, + getLastSyncedEvent, + getTxOutputsFromTx, + markUtxosAsVoided, + cleanupVoidedTx, +} from '../db'; +import getConfig from '../config'; +import logger from '../logger'; +import { invokeOnTxPushNotificationRequestedLambda, sendMessageSQS } from '../utils/aws'; + +export const METADATA_DIFF_EVENT_TYPES = { + IGNORE: 'IGNORE', + TX_VOIDED: 'TX_VOIDED', + TX_UNVOIDED: 'TX_UNVOIDED', + TX_NEW: 'TX_NEW', + TX_FIRST_BLOCK: 'TX_FIRST_BLOCK', +}; + +export const metadataDiff = async (_context: Context, event: Event) => { + const mysql = await getDbConnection(); + + try { + const fullNodeEvent = event.event as FullNodeEvent; + const { + hash, + metadata: { voided_by, first_block }, + } = fullNodeEvent.event.data; + const dbTx: DbTransaction | null = await getTransactionById(mysql, hash); + + if (!dbTx) { + if (voided_by.length > 0) { + // No need to add voided transactions + return { + type: METADATA_DIFF_EVENT_TYPES.IGNORE, + originalEvent: event, + }; + } + + return { + type: METADATA_DIFF_EVENT_TYPES.TX_NEW, + originalEvent: event, + }; + } + + // Tx is voided + if (voided_by.length > 0) { + // Was it voided on the database? + if (!dbTx.voided) { + return { + type: METADATA_DIFF_EVENT_TYPES.TX_VOIDED, + originalEvent: event, + }; + } + + return { + type: METADATA_DIFF_EVENT_TYPES.IGNORE, + originalEvent: event, + }; + } + + // Tx was voided in the database but is not anymore + if (dbTx.voided && voided_by.length <= 0) { + return { + type: METADATA_DIFF_EVENT_TYPES.TX_UNVOIDED, + originalEvent: event, + }; + } + + if (first_block + && first_block.length + && first_block.length > 0) { + if (!dbTx.height) { + return { + type: METADATA_DIFF_EVENT_TYPES.TX_FIRST_BLOCK, + originalEvent: event, + }; + } + + return { + type: METADATA_DIFF_EVENT_TYPES.IGNORE, + originalEvent: event, + }; + } + + return { + type: METADATA_DIFF_EVENT_TYPES.IGNORE, + originalEvent: event, + }; + } catch (e) { + logger.error('e', e); + return Promise.reject(e); + } finally { + mysql.destroy(); + } +}; + +export const isBlock = (version: number): boolean => version === hathorLib.constants.BLOCK_VERSION + || version === hathorLib.constants.MERGED_MINED_BLOCK_VERSION; + +export const handleVertexAccepted = async (context: Context, _event: Event) => { + const mysql = await getDbConnection(); + await mysql.beginTransaction(); + + try { + const fullNodeEvent = context.event as FullNodeEvent; + const now = getUnixTimestamp(); + const { NEW_TX_SQS, PUSH_NOTIFICATION_ENABLED } = getConfig(); + const blockRewardLock = context.rewardMinBlocks; + + if (!blockRewardLock) { + throw new Error('No block reward lock set'); + } + + const { + hash, + metadata, + timestamp, + version, + weight, + outputs, + inputs, + nonce, + tokens, + token_name, + token_symbol, + parents, + } = fullNodeEvent.event.data; + + const dbTx: DbTransaction | null = await getTransactionById(mysql, hash); + + if (dbTx) { + logger.error(`Transaction ${hash} already in the database, this should only happen if the service has been recently restarted`); + + // This might happen if the service has been recently restarted, + // so we should raise the alert and just ignore the tx + return; + } + + let height: number | null = metadata.height; + + if (!isBlock(version) && !metadata.first_block) { + height = null; + } + + const txOutputs: TxOutputWithIndex[] = prepareOutputs(outputs, tokens); + const txInputs: TxInput[] = prepareInputs(inputs, tokens); + + let heightlock = null; + if (isBlock(version)) { + if (typeof height !== 'number' && !height) { + throw new Error('Block with no height set in metadata.'); + } + + // unlock older blocks + const utxos = await getUtxosLockedAtHeight(mysql, now, height); + + if (utxos.length > 0) { + logger.debug(`Block transaction, unlocking ${utxos.length} locked utxos at height ${height}`); + await unlockUtxos(mysql, utxos, false); + } + + // set heightlock + heightlock = height + blockRewardLock; + + // get the first output address + const blockRewardOutput = outputs[0]; + + // add miner to the miners table + await addMiner(mysql, blockRewardOutput.decoded.address, hash); + + // here we check if we have any utxos on our database that is locked but + // has its timelock < now + // + // we've decided to do this here considering that it is acceptable to have + // a delay between the actual timelock expiration time and the next block + // (that will unlock it). This delay is only perceived on the wallet as the + // sync mechanism will unlock the timelocked utxos as soon as they are seen + // on a received transaction. + await unlockTimelockedUtxos(mysql, now); + } + + if (version === hathorLib.constants.CREATE_TOKEN_TX_VERSION) { + if (!token_name || !token_symbol) { + throw new Error('Processed a token creation event but it did not come with token name and symbol'); + } + await storeTokenInformation(mysql, hash, token_name, token_symbol); + } + + // check if any of the inputs are still marked as locked and update tables accordingly. + // See remarks on getLockedUtxoFromInputs for more explanation. It's important to perform this + // before updating the balances + const lockedInputs = await getLockedUtxoFromInputs(mysql, inputs); + await unlockUtxos(mysql, lockedInputs, true); + + // add transaction outputs to the tx_outputs table + markLockedOutputs(txOutputs, now, heightlock !== null); + + // Add the transaction + logger.debug('Will add the tx with height', height); + await addOrUpdateTx( + mysql, + hash, + height, + timestamp, + version, + weight, + ); + + // Add utxos + await addUtxos(mysql, hash, txOutputs, heightlock); + await updateTxOutputSpentBy(mysql, txInputs, hash); + + // Genesis tx has no inputs and outputs, so nothing to be updated, avoid it + if (inputs.length > 0 || outputs.length > 0) { + const tokenList: string[] = getTokenListFromInputsAndOutputs(txInputs, txOutputs); + + // Update transaction count with the new tx + await incrementTokensTxCount(mysql, tokenList); + + const addressBalanceMap: StringMap = getAddressBalanceMap(txInputs, txOutputs); + + // update address tables (address, address_balance, address_tx_history) + await updateAddressTablesWithTx(mysql, hash, timestamp, addressBalanceMap); + + // for the addresses present on the tx, check if there are any wallets associated + const addressWalletMap: StringMap = await getAddressWalletInfo(mysql, Object.keys(addressBalanceMap)); + + // for each already started wallet, update databases + const seenWallets = new Set(); + for (const wallet of Object.values(addressWalletMap)) { + const walletId = wallet.walletId; + + // this map might contain duplicate wallet values, as 2 different addresses might belong to the same wallet + if (seenWallets.has(walletId)) continue; + seenWallets.add(walletId); + const { newAddresses, lastUsedAddressIndex } = await generateAddresses(mysql, wallet.xpubkey, wallet.maxGap); + // might need to generate new addresses to keep maxGap + await addNewAddresses(mysql, walletId, newAddresses, lastUsedAddressIndex); + // update existing addresses' walletId and index + } + // update wallet_balance and wallet_tx_history tables + const walletBalanceMap: StringMap = getWalletBalanceMap(addressWalletMap, addressBalanceMap); + await updateWalletTablesWithTx(mysql, hash, timestamp, walletBalanceMap); + + const tx: Transaction = { + tx_id: hash, + nonce, + timestamp, + voided: metadata.voided_by.length > 0, + weight, + parents, + version, + inputs: txInputs, + outputs: txOutputs, + height: metadata.height, + token_name, + token_symbol, + }; + + try { + if (seenWallets.size > 0) { + const queueUrl = NEW_TX_SQS; + if (!queueUrl) { + throw new Error('Queue URL is invalid'); + } + + await sendMessageSQS(JSON.stringify({ + wallets: Array.from(seenWallets), + tx, + }), queueUrl); + } + } catch (e) { + logger.error('Failed to send transaction to SQS queue'); + logger.error(e); + } + + try { + if (PUSH_NOTIFICATION_ENABLED) { + const walletBalanceMap = await getWalletBalancesForTx(mysql, tx); + const { length: hasAffectWallets } = Object.keys(walletBalanceMap); + if (hasAffectWallets) { + invokeOnTxPushNotificationRequestedLambda(walletBalanceMap) + .catch((err: Error) => logger.error('Errored on invokeOnTxPushNotificationRequestedLambda invocation', err)); + } + } + } catch (e) { + logger.error('Failed to send push notification to wallet-service lambda'); + logger.error(e); + } + } + + // TODO: Send message on SQS for real-time update + await dbUpdateLastSyncedEvent(mysql, fullNodeEvent.event.id); + + await mysql.commit(); + } catch (e) { + await mysql.rollback(); + logger.error(e); + + throw e; + } finally { + mysql.destroy(); + } +}; + +export const handleVoidedTx = async (context: Context) => { + const mysql = await getDbConnection(); + await mysql.beginTransaction(); + + try { + const fullNodeEvent = context.event as FullNodeEvent; + + const { + hash, + outputs, + inputs, + tokens, + } = fullNodeEvent.event.data; + + logger.debug(`Will handle voided tx for ${hash}`); + + const dbTxOutputs: DbTxOutput[] = await getTxOutputsFromTx(mysql, hash); + const txOutputs: TxOutputWithIndex[] = prepareOutputs(outputs, tokens); + const txInputs: TxInput[] = prepareInputs(inputs, tokens); + + const txOutputsWithLocked = txOutputs.map((output) => { + const dbTxOutput = dbTxOutputs.find((_output) => _output.index === output.index); + + if (!dbTxOutput) { + throw new Error('Transaction output different from database output!'); + } + + return { + ...output, + locked: dbTxOutput.locked, + }; + }); + + const addressBalanceMap: StringMap = getAddressBalanceMap(txInputs, txOutputsWithLocked); + await voidTransaction(mysql, hash, addressBalanceMap); + await markUtxosAsVoided(mysql, dbTxOutputs); + + const addresses = Object.keys(addressBalanceMap); + await validateAddressBalances(mysql, addresses); + + await dbUpdateLastSyncedEvent(mysql, fullNodeEvent.event.id); + + logger.debug(`Voided tx ${hash}`); + + await mysql.commit(); + } catch (e) { + logger.debug(e); + await mysql.rollback(); + + throw e; + } finally { + mysql.destroy(); + } +}; + +export const handleUnvoidedTx = async (context: Context) => { + const mysql = await getDbConnection(); + await mysql.beginTransaction(); + + try { + const fullNodeEvent = context.event as FullNodeEvent; + + const { hash } = fullNodeEvent.event.data; + + logger.debug(`Tx ${hash} got unvoided, cleaning up the database.`); + + await cleanupVoidedTx(mysql, hash); + + logger.debug(`Unvoided tx ${hash}`); + + await mysql.commit(); + } catch (e) { + logger.debug(e); + await mysql.rollback(); + + throw e; + } finally { + mysql.destroy(); + } +}; + +export const handleTxFirstBlock = async (context: Context) => { + const mysql = await getDbConnection(); + await mysql.beginTransaction(); + + try { + const fullNodeEvent = context.event as FullNodeEvent; + + const { + hash, + metadata, + timestamp, + version, + weight, + } = fullNodeEvent.event.data; + + const height: number | null = metadata.height; + + if (!metadata.first_block) { + throw new Error('HandleTxFirstBlock called but no first block on metadata'); + } + + await addOrUpdateTx(mysql, hash, height, timestamp, version, weight); + await dbUpdateLastSyncedEvent(mysql, fullNodeEvent.event.id); + logger.debug(`Confirmed tx ${hash}: ${fullNodeEvent.event.id}`); + + await mysql.commit(); + } catch (e) { + logger.error('E: ', e); + await mysql.rollback(); + throw e; + } finally { + mysql.destroy(); + } +}; + +export const updateLastSyncedEvent = async (context: Context) => { + const mysql = await getDbConnection(); + + const lastDbSyncedEvent: LastSyncedEvent | null = await getLastSyncedEvent(mysql); + + if (!context.event) { + throw new Error('Tried to update last synced event but no event in context'); + } + + const lastEventId = context.event.event.id; + + if (lastDbSyncedEvent + && lastDbSyncedEvent.last_event_id > lastEventId) { + logger.error('Tried to store an event lower than the one on the database', { + lastEventId, + lastDbSyncedEvent: JSON.stringify(lastDbSyncedEvent), + }); + mysql.destroy(); + throw new Error('Event lower than stored one.'); + } + await dbUpdateLastSyncedEvent(mysql, lastEventId); + + mysql.destroy(); +}; + +export const fetchMinRewardBlocks = async () => { + const fullnodeUrl = getFullnodeHttpUrl(); + const response = await axios.get(`${fullnodeUrl}/version`); + + if (response.status !== 200) { + throw new Error('Request to version API failed'); + } + + const rewardSpendMinBlocks = get(response, 'data.reward_spend_min_blocks'); + + if (!rewardSpendMinBlocks) { + throw new Error('Failed to fetch reward spend min blocks'); + } + + return rewardSpendMinBlocks; +}; + +export const fetchInitialState = async () => { + const mysql = await getDbConnection(); + const lastEvent = await getLastSyncedEvent(mysql); + const rewardMinBlocks = await fetchMinRewardBlocks(); + + mysql.destroy(); + + return { + lastEventId: lastEvent?.last_event_id, + rewardMinBlocks, + }; +}; diff --git a/packages/daemon/src/types/address.ts b/packages/daemon/src/types/address.ts new file mode 100644 index 00000000..3b2eef88 --- /dev/null +++ b/packages/daemon/src/types/address.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + import { StringMap } from './utils'; + +export interface GenerateAddresses { + addresses: string[]; + existingAddresses: StringMap; + newAddresses: StringMap; + lastUsedAddressIndex: number; +} + +export interface AddressBalance { + address: string; + tokenId: string; + unlockedBalance: number; + lockedBalance: number; + unlockedAuthorities: number; + lockedAuthorities: number; + timelockExpires: number; + transactions: number; +} + +export interface AddressTotalBalance { + address: string; + tokenId: string; + balance: number; + transactions: number; +} + +export type AddressIndexMap = StringMap; + +export interface Miner { + address: string; + firstBlock: string; + lastBlock: string; + count: number; +} diff --git a/packages/daemon/src/types/alerting.ts b/packages/daemon/src/types/alerting.ts new file mode 100644 index 00000000..08eca9e5 --- /dev/null +++ b/packages/daemon/src/types/alerting.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Alerts should follow the on-call guide for alerting, see + * https://github.com/HathorNetwork/ops-tools/blob/master/docs/on-call/guide.md#alert-severitypriority + */ +export enum Severity { + CRITICAL = 'critical', + MAJOR = 'major', + MEDIUM = 'medium', + MINOR = 'minor', + WARNING = 'warning', + INFO = 'info', +} diff --git a/packages/daemon/src/types/db.ts b/packages/daemon/src/types/db.ts new file mode 100644 index 00000000..435ed076 --- /dev/null +++ b/packages/daemon/src/types/db.ts @@ -0,0 +1,134 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { RowDataPacket } from 'mysql2/promise'; + +export interface TxOutputRow extends RowDataPacket { + tx_id: string; + index: number; + token_id: string; + address: string; + value: number; + authorities: number; + timelock: number; + heightlock: number; + locked: boolean; + tx_proposal?: string; + tx_proposal_index?: number; + spent_by?: string; +} + +export interface LastSyncedEventRow extends RowDataPacket { + id: number; + last_event_id: number; + updated_at: number; +} + +export interface AddressBalanceRow extends RowDataPacket { + address: string; + token_id: string; + unlocked_balance: number; + locked_balance: number; + locked_authorities: number; + unlocked_authorities: number; + timelock_expires: number; + transactions: number; +} + +export interface AddressTxHistorySumRow extends RowDataPacket { + address: string; + token_id: string; + balance: string; + transactions: string; +} + +export interface AddressTableRow extends RowDataPacket { + address: string; + index: number; + wallet_id: string; + transactions: number; +} + +export interface AddressBalanceRow extends RowDataPacket { + address: string; + token_id: string; + unlocked_balance: number; + locked_balance: number; + locked_authorities: number; + unlocked_authorities: number; + timelock_expires: number; + transactions: number; + total_received: number; + created_at: number; + updated_at: number; +} + +export interface AddressTxHistoryRow extends RowDataPacket { + address: string; + tx_id: string; + token_id: string; + balance: number; + timestamp: number; + voided: boolean; +} + +export interface TransactionRow extends RowDataPacket { + tx_id: string; + timestamp: number; + version: number; + voided: boolean; + height?: number | null; + weight?: number | null; + created_at: number; + updated_at: number; +} + +export interface WalletBalanceRow extends RowDataPacket { + wallet_id: string; + token_id: string; + unlocked_balance: number; + locked_balance: number; + unlocked_authorities: number; + locked_authorities: number; + timelock_expires?: number | null; + transactions: number; + total_received: number; +} + +export interface MinerRow extends RowDataPacket { + address: string; + first_block: string; + last_block: string; + count: number; +} + +export interface TokenInformationRow extends RowDataPacket { + id: string; + name: string; + symbol: string; + transactions: number; + created_at: number; + updated_at: number; +} + +export interface WalletTxHistoryRow extends RowDataPacket { + wallet_id: string; + token_id: string; + tx_id: string; + balance: number; + timestamp: number; + voided: boolean; +} + +export interface BestBlockRow extends RowDataPacket { + height: number; +} + +export interface TokenSymbolsRow extends RowDataPacket { + id: string; + symbol: string; +} diff --git a/packages/daemon/src/types/event.ts b/packages/daemon/src/types/event.ts new file mode 100644 index 00000000..59079055 --- /dev/null +++ b/packages/daemon/src/types/event.ts @@ -0,0 +1,111 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export type WebSocketEvent = + | { type: 'CONNECTED' } + | { type: 'DISCONNECTED' }; + +export type MetadataDecidedEvent = { + type: 'TX_VOIDED' | 'TX_UNVOIDED' | 'TX_NEW' | 'TX_FIRST_BLOCK' | 'IGNORE'; + originalEvent: FullNodeEvent; +} + +export type WebSocketSendEvent = + | { + type: 'START_STREAM'; + window_size: number; + last_ack_event_id?: number; + } + | { + type: 'ACK'; + window_size: number; + ack_event_id?: number; + }; + +export type HealthCheckEvent = + | { type: 'START' } + | { type: 'STOP' }; + +export enum EventTypes { + WEBSOCKET_EVENT = 'WEBSOCKET_EVENT', + FULLNODE_EVENT = 'FULLNODE_EVENT', + METADATA_DECIDED = 'METADATA_DECIDED', + WEBSOCKET_SEND_EVENT = 'WEBSOCKET_SEND_EVENT', + HEALTHCHECK_EVENT = 'HEALTHCHECK_EVENT', +} + +export enum FullNodeEventTypes { + VERTEX_METADATA_CHANGED = 'VERTEX_METADATA_CHANGED', + NEW_VERTEX_ACCEPTED = 'NEW_VERTEX_ACCEPTED', + LOAD_STARTED = 'LOAD_STARTED', + LOAD_FINISHED = 'LOAD_FINISHED', + REORG_STARTED = 'REORG_FINISHED', +} + +export type Event = + | { type: EventTypes.WEBSOCKET_EVENT, event: WebSocketEvent } + | { type: EventTypes.FULLNODE_EVENT, event: FullNodeEvent } + | { type: EventTypes.METADATA_DECIDED, event: MetadataDecidedEvent } + | { type: EventTypes.WEBSOCKET_SEND_EVENT, event: WebSocketSendEvent } + | { type: EventTypes.HEALTHCHECK_EVENT, event: HealthCheckEvent}; + +export type FullNodeEvent = { + stream_id: string; + peer_id: string; + network: string; + type: string; + latest_event_id: number; + event: { + id: number; + timestamp: number; + type: FullNodeEventTypes; + data: { + hash: string; + timestamp: number; + version: number; + weight: number; + nonce: number; + inputs: EventTxInput[]; + outputs: EventTxOutput[]; + parents: string[]; + tokens: string[]; + token_name: null | string; + token_symbol: null | string; + metadata: { + hash: string; + voided_by: string[]; + first_block: null | string; + height: number; + }; + } + } +} + +export interface EventTxInput { + tx_id: string; + index: number; + spent_output: EventTxOutput; +} + +export interface EventTxOutput { + value: number; + token_data: number; + script: string; + locked?: boolean; + decoded: { + type: string; + address: string; + timelock: number | null; + }; +} + +export interface LastSyncedEvent { + id: number; + last_event_id: number; + updated_at: number; +} + diff --git a/packages/daemon/src/types/index.ts b/packages/daemon/src/types/index.ts new file mode 100644 index 00000000..cdca0655 --- /dev/null +++ b/packages/daemon/src/types/index.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + export * from './address'; + export * from './event'; + export * from './token'; + export * from './transaction'; + export * from './utils'; + export * from './wallet'; + export * from './db'; + export * from './machine'; + export * from './push_notification'; + export * from './alerting'; diff --git a/packages/daemon/src/types/machine.ts b/packages/daemon/src/types/machine.ts new file mode 100644 index 00000000..75265b69 --- /dev/null +++ b/packages/daemon/src/types/machine.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { ActorRef } from 'xstate'; +import { LRU } from '../utils'; +import { FullNodeEvent } from './event'; + +export interface Context { + socket: ActorRef | null; + healthcheck: ActorRef | null; + retryAttempt: number; + event?: FullNodeEvent | null; + initialEventId: null | number; + txCache: LRU; + rewardMinBlocks?: number | null; +} diff --git a/packages/daemon/src/types/push_notification.ts b/packages/daemon/src/types/push_notification.ts new file mode 100644 index 00000000..ccb56b51 --- /dev/null +++ b/packages/daemon/src/types/push_notification.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export interface SendNotificationToDevice { + deviceId: string, + /** + * A string map used to send data in the notification message. + * @see LocalizeMetadataNotification + * + * @example + * { + * "titleLocKey": "new_transaction_received_title", + * "bodyLocKey": "new_transaction_received_description_with_tokens", + * "bodyLocArgs": "['13 HTR', '8 TNT', '2']" + * } + */ + metadata: Record, +} diff --git a/packages/daemon/src/types/token.ts b/packages/daemon/src/types/token.ts new file mode 100644 index 00000000..e384707e --- /dev/null +++ b/packages/daemon/src/types/token.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// @ts-ignore +import hathorLib from '@hathor/wallet-lib'; + +export class TokenInfo { + id: string; + + name: string; + + symbol: string; + + transactions: number; + + constructor(id: string, name: string, symbol: string, transactions?: number) { + this.id = id; + this.name = name; + this.symbol = symbol; + this.transactions = transactions || 0; + + const hathorConfig = hathorLib.constants.HATHOR_TOKEN_CONFIG; + + if (this.id === hathorConfig.uid) { + this.name = hathorConfig.name; + this.symbol = hathorConfig.symbol; + } + } + + toJSON(): Record { + return { + id: this.id, + name: this.name, + symbol: this.symbol, + }; + } +} diff --git a/packages/daemon/src/types/transaction.ts b/packages/daemon/src/types/transaction.ts new file mode 100644 index 00000000..cbb04f3d --- /dev/null +++ b/packages/daemon/src/types/transaction.ts @@ -0,0 +1,448 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// @ts-ignore +import hathorLib from '@hathor/wallet-lib'; +import { isAuthority } from '../utils'; +import { StringMap } from './utils'; + + +export interface DecodedOutput { + type: string; + address: string; + timelock: number | null; +} + +export interface TxOutput { + value: number; + script: string; + token: string; + decoded: DecodedOutput | null; + // eslint-disable-next-line camelcase + spent_by?: string | null; + // eslint-disable-next-line camelcase + token_data: number; + locked?: boolean; +} + +export interface DbTxOutput { + txId: string; + index: number; + tokenId: string; + address: string; + value: number; + authorities: number; + timelock: number | null; + heightlock: number | null; + locked: boolean; + spentBy?: string | null; + txProposalId?: string | null; + txProposalIndex?: number | null; + voided?: boolean | null; +} + +export interface TxOutputWithIndex extends TxOutput { + index: number; +} + +export interface TxInput { + // eslint-disable-next-line camelcase + tx_id: string; + index: number; + value: number; + // eslint-disable-next-line camelcase + token_data: number; + script: string; + token: string; + decoded: DecodedOutput | null; +} + +export class Authorities { + /** + * Supporting up to 8 authorities (but we only have mint and melt at the moment) + */ + static LENGTH = 8; + + array: number[]; + + constructor(authorities?: number | number[]) { + let tmp: number[] = []; + if (authorities instanceof Array) { + tmp = authorities; + } else if (authorities != null) { + tmp = Authorities.intToArray(authorities); + } + + this.array = new Array(Authorities.LENGTH - tmp.length).fill(0).concat(tmp); + } + + /** + * Get the integer representation of this authority. + * + * @remarks + * Uses the array to calculate the final number. Examples: + * [0, 0, 0, 0, 1, 1, 0, 1] = 0b00001101 = 13 + * [0, 0, 1, 0, 0, 0, 0, 1] = 0b00100001 = 33 + * + * @returns The integer representation + */ + toInteger(): number { + let n = 0; + for (let i = 0; i < this.array.length; i++) { + if (this.array[i] === 0) continue; + + n += this.array[i] * (2 ** (this.array.length - i - 1)); + } + return n; + } + + toUnsignedInteger(): number { + return Math.abs(this.toInteger()); + } + + clone(): Authorities { + return new Authorities(this.array); + } + + /** + * Return a new object inverting each authority value sign. + * + * @remarks + * If value is set to 1, it becomes -1 and vice versa. Value 0 remains unchanged. + * + * @returns A new Authority object with the values inverted + */ + toNegative(): Authorities { + const finalAuthorities = this.array.map((value) => { + // This if is needed because Javascript uses the IEEE_754 standard and has negative and positive zeros, + // so (-1) * 0 would return -0. Apparently -0 === 0 is true on most cases, so there wouldn't be a problem, + // but we will leave this here to be safe. + // https://en.wikipedia.org/wiki/IEEE_754 + if (value === 0) return 0; + + return (-1) * value; + }); + return new Authorities(finalAuthorities); + } + + /** + * Return if any of the authorities has a negative value. + * + * @remarks + * Negative values for an authority only make sense when dealing with balances of a + * transaction. So if we consume an authority in the inputs but do not create the same + * one in the output, it will have value -1. + * + * @returns `true` if any authority is less than 0; `false` otherwise + */ + hasNegativeValue(): boolean { + return this.array.some((authority) => authority < 0); + } + + /** + * Transform an integer into an array, considering 1 array element per bit. + * + * @returns The array given an integer + */ + static intToArray(authorities: number): number[] { + const ret = []; + for (const c of authorities.toString(2)) { + ret.push(parseInt(c, 10)); + } + return ret; + } + + /** + * Merge two authorities. + * + * @remarks + * The process is done individualy for each authority value. Each a1[n] and a2[n] are compared. + * If both values are the same, the final value is the same. If one is 1 and the other -1, final + * value is 0. + * + * @returns A new object with the merged values + */ + static merge(a1: Authorities, a2: Authorities): Authorities { + return new Authorities(a1.array.map((value, index) => Math.sign(value + a2.array[index]))); + } + + toJSON(): Record { + const authorities = this.toInteger(); + return { + mint: (authorities & hathorLib.constants.TOKEN_MINT_MASK) > 0, // eslint-disable-line no-bitwise + melt: (authorities & hathorLib.constants.TOKEN_MELT_MASK) > 0, // eslint-disable-line no-bitwise + }; + } +} + +export class Balance { + totalAmountSent: number; + + lockedAmount: number; + + unlockedAmount: number; + + lockedAuthorities: Authorities; + + unlockedAuthorities: Authorities; + + lockExpires: number | null | undefined; + + constructor(totalAmountSent = 0, unlockedAmount = 0, lockedAmount = 0, lockExpires = null, unlockedAuthorities = null, lockedAuthorities = null) { + this.totalAmountSent = totalAmountSent; + this.unlockedAmount = unlockedAmount; + this.lockedAmount = lockedAmount; + this.lockExpires = lockExpires; + this.unlockedAuthorities = unlockedAuthorities || new Authorities(); + this.lockedAuthorities = lockedAuthorities || new Authorities(); + } + + /** + * Get the total balance, sum of unlocked and locked amounts. + * + * @returns The total balance + */ + total(): number { + return this.unlockedAmount + this.lockedAmount; + } + + /** + * Get all authorities, combination of unlocked and locked. + * + * @returns The combined authorities + */ + authorities(): Authorities { + return Authorities.merge(this.unlockedAuthorities, this.lockedAuthorities); + } + + /** + * Clone this Balance object. + * + * @returns A new Balance object with the same information + */ + clone(): Balance { + return new Balance( + this.totalAmountSent, + this.unlockedAmount, + this.lockedAmount, + // @ts-ignore + this.lockExpires, + this.unlockedAuthorities.clone(), + this.lockedAuthorities.clone(), + ); + } + + /** + * Merge two balances. + * + * @remarks + * In case lockExpires is set, it returns the lowest one. + * + * @param b1 - First balance + * @param b2 - Second balance + * @returns The sum of both balances and authorities + */ + static merge(b1: Balance, b2: Balance): Balance { + let lockExpires = null; + if (b1.lockExpires === null) { + lockExpires = b2.lockExpires; + } else if (b2.lockExpires === null) { + lockExpires = b1.lockExpires; + } else { + // @ts-ignore + lockExpires = Math.min(b1.lockExpires, b2.lockExpires); + } + return new Balance( + b1.totalAmountSent + b2.totalAmountSent, + b1.unlockedAmount + b2.unlockedAmount, + b1.lockedAmount + b2.lockedAmount, + // @ts-ignore + lockExpires, + Authorities.merge(b1.unlockedAuthorities, b2.unlockedAuthorities), + Authorities.merge(b1.lockedAuthorities, b2.lockedAuthorities), + ); + } +} + +export class TokenBalanceMap { + map: StringMap; + + constructor() { + this.map = {}; + } + + get(tokenId: string): Balance { + // if the token is not present, return 0 instead of undefined + return this.map[tokenId] || new Balance(0, 0, 0); + } + + set(tokenId: string, balance: Balance): void { + this.map[tokenId] = balance; + } + + getTokens(): string[] { + return Object.keys(this.map); + } + + iterator(): [string, Balance][] { + return Object.entries(this.map); + } + + clone(): TokenBalanceMap { + const cloned = new TokenBalanceMap(); + for (const [token, balance] of this.iterator()) { + cloned.set(token, balance.clone()); + } + return cloned; + } + + /** + * Return a TokenBalanceMap from js object. + * + * @remarks + * Js object is expected to have the format: + * ``` + * { + * token1: {unlocked: n, locked: m}, + * token2: {unlocked: a, locked: b, lockExpires: c}, + * token3: {unlocked: x, locked: y, unlockedAuthorities: z, lockedAuthorities: w}, + * } + * ``` + * + * @param tokenBalanceMap - The js object to convert to a TokenBalanceMap + * @returns - The new TokenBalanceMap object + */ + static fromStringMap(tokenBalanceMap: StringMap>): TokenBalanceMap { + const obj = new TokenBalanceMap(); + for (const [tokenId, balance] of Object.entries(tokenBalanceMap)) { + obj.set(tokenId, new Balance( + balance.totalSent as number, + balance.unlocked as number, + balance.locked as number, + // @ts-ignore + balance.lockExpires || null, + balance.unlockedAuthorities, + balance.lockedAuthorities, + )); + } + return obj; + } + + /** + * Merge two TokenBalanceMap objects, merging the balances for each token. + * + * @param balanceMap1 - First TokenBalanceMap + * @param balanceMap2 - Second TokenBalanceMap + * @returns The merged TokenBalanceMap + */ + static merge(balanceMap1: TokenBalanceMap, balanceMap2: TokenBalanceMap): TokenBalanceMap { + if (!balanceMap1) return balanceMap2.clone(); + if (!balanceMap2) return balanceMap1.clone(); + const mergedMap = balanceMap1.clone(); + for (const [token, balance] of balanceMap2.iterator()) { + const finalBalance = Balance.merge(mergedMap.get(token), balance); + mergedMap.set(token, finalBalance); + } + return mergedMap; + } + + /** + * Create a TokenBalanceMap from a TxOutput. + * + * @param output - The transaction output + * @returns The TokenBalanceMap object + */ + static fromTxOutput(output: TxOutput): TokenBalanceMap { + if (!output.decoded) { + throw new Error('Output has no decoded script'); + } + const token = output.token; + const value = output.value; + const obj = new TokenBalanceMap(); + + if (output.locked) { + if (isAuthority(output.token_data)) { + // @ts-ignore + obj.set(token, new Balance(0, 0, 0, output.decoded.timelock, 0, new Authorities(output.value))); + } else { + // @ts-ignore + obj.set(token, new Balance(value, 0, value, output.decoded.timelock, 0, 0)); + } + } else if (isAuthority(output.token_data)) { + // @ts-ignore + obj.set(token, new Balance(0, 0, 0, null, new Authorities(output.value), 0)); + } else { + obj.set(token, new Balance(value, value, 0, null)); + } + + return obj; + } + + /** + * Create a TokenBalanceMap from a TxInput. + * + * @remarks + * It will have only one token entry and balance will be negative. + * + * @param input - The transaction input + * @returns The TokenBalanceMap object + */ + static fromTxInput(input: TxInput): TokenBalanceMap { + const token = input.token; + const obj = new TokenBalanceMap(); + + if (isAuthority(input.token_data)) { + // for inputs, the authorities will have a value of -1 when set + const authorities = new Authorities(input.value); + obj.set( + token, + new Balance( + 0, + 0, + 0, + null, + // @ts-ignore + authorities.toNegative(), + new Authorities(0), + ), + ); + } else { + obj.set(token, new Balance(0, -input.value, 0, null)); + } + return obj; + } +} + +export interface Transaction { + // eslint-disable-next-line camelcase + tx_id: string; + nonce: number; + timestamp: number; + // eslint-disable-next-line camelcase + voided: boolean; + version: number; + weight: number; + parents: string[]; + inputs: TxInput[]; + outputs: TxOutput[]; + height?: number; + // eslint-disable-next-line camelcase + token_name?: string | null; + // eslint-disable-next-line camelcase + token_symbol?: string | null; +} + +export interface DbTransaction { + tx_id: string; + timestamp: number; + version: number; + voided: boolean; + height?: number | null; + weight?: number | null; + created_at: number; + updated_at: number; +} diff --git a/packages/daemon/src/types/utils.ts b/packages/daemon/src/types/utils.ts new file mode 100644 index 00000000..916e8177 --- /dev/null +++ b/packages/daemon/src/types/utils.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export interface StringMap { + [x: string]: T; +} diff --git a/packages/daemon/src/types/wallet.ts b/packages/daemon/src/types/wallet.ts new file mode 100644 index 00000000..bdbd9ee5 --- /dev/null +++ b/packages/daemon/src/types/wallet.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { TokenBalanceMap } from "./transaction"; + +export enum WalletStatus { + CREATING = 'creating', + READY = 'ready', + ERROR = 'error', +} + +export interface Wallet { + walletId: string; + xpubkey: string; + authXpubkey: string, + maxGap: number; + status?: WalletStatus; + retryCount?: number; + createdAt?: number; + readyAt?: number; +} + +export type TokenBalanceValue = { + tokenId: string, + tokenSymbol: string, + totalAmountSent: number; + lockedAmount: number; + unlockedAmount: number; + lockedAuthorities: Record; + unlockedAuthorities: Record; + lockExpires: number | null; + total: number; +} + +export interface WalletBalanceValue { + txId: string, + walletId: string, + addresses: string[], + walletBalanceForTx: TokenBalanceValue[], +} + +export interface WalletBalance { + txId: string, + walletId: string, + addresses: string[], + walletBalanceForTx: TokenBalanceMap, +} diff --git a/packages/daemon/src/utils/alerting.ts b/packages/daemon/src/utils/alerting.ts new file mode 100644 index 00000000..f74f0947 --- /dev/null +++ b/packages/daemon/src/utils/alerting.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Severity } from '../types'; +import getConfig from '../config'; +import logger from '../logger'; +import { sendMessageSQS } from './aws'; + +/** + * Adds a message to the SQS alerting queue + * + * @param title - The alert's title + * @param message - The alert's message + * @param severity - The alert's severity (critical, major, medium, minor, warning or info) + * @param metadata - Key value object being the key the title + */ +export const addAlert = async ( + title: string, + message: string, + severity: Severity = Severity.INFO, + metadata?: unknown, +): Promise => { + const { + NETWORK, + ACCOUNT_ID, + SERVICE_NAME, + ALERT_MANAGER_TOPIC, + ALERT_MANAGER_REGION, + } = getConfig(); + + const preparedMessage = { + title, + message, + severity, + metadata, + environment: NETWORK, + application: SERVICE_NAME, + }; + + try { + const QUEUE_URL = `https://sqs.${ALERT_MANAGER_REGION}.amazonaws.com/${ACCOUNT_ID}/${ALERT_MANAGER_TOPIC}`; + + await sendMessageSQS(QUEUE_URL, JSON.stringify(preparedMessage), { + None: { + DataType: 'String', + StringValue: '--', + }, + }); + } catch(err) { + logger.error('[ALERT] Erroed while sending message to the alert sqs queue'); + logger.error(err); + } +}; diff --git a/packages/daemon/src/utils/aws.ts b/packages/daemon/src/utils/aws.ts new file mode 100644 index 00000000..217800db --- /dev/null +++ b/packages/daemon/src/utils/aws.ts @@ -0,0 +1,73 @@ +import { Severity, WalletBalanceValue } from '../types'; +import { LambdaClient, InvokeCommand, InvokeCommandOutput } from '@aws-sdk/client-lambda'; +import { SendMessageCommand, SendMessageCommandOutput, SQSClient, MessageAttributeValue } from '@aws-sdk/client-sqs'; +import { StringMap } from '../types'; +import getConfig from '../config'; +import logger from '../logger'; +import { addAlert } from './alerting'; + +export function buildFunctionName(functionName: string): string { + const { STAGE } = getConfig(); + return `hathor-wallet-service-${STAGE}-${functionName}`; +} + +/** + * Invokes this application's own intermediary lambda `OnTxPushNotificationRequestedLambda`. + * @param walletBalanceValueMap - a map of walletId linked to its wallet balance data. + */ +export const invokeOnTxPushNotificationRequestedLambda = async (walletBalanceValueMap: StringMap): Promise => { + const { + PUSH_NOTIFICATION_ENABLED, + WALLET_SERVICE_LAMBDA_ENDPOINT, + ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME, + PUSH_NOTIFICATION_LAMBDA_REGION, + } = getConfig(); + + if (!PUSH_NOTIFICATION_ENABLED) { + logger.debug('Push notification is disabled. Skipping invocation of OnTxPushNotificationRequestedLambda lambda.'); + return; + } + + const client = new LambdaClient({ + endpoint: WALLET_SERVICE_LAMBDA_ENDPOINT, + region: PUSH_NOTIFICATION_LAMBDA_REGION, + }); + + const command = new InvokeCommand({ + FunctionName: ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME, + InvocationType: 'Event', + Payload: JSON.stringify(walletBalanceValueMap), + }); + + const response: InvokeCommandOutput = await client.send(command); + + if (response.StatusCode !== 202) { + // Event InvocationType returns 202 for a successful invokation + const walletIdList = Object.keys(walletBalanceValueMap); + + await addAlert( + 'Error on PushNotificationUtils', + `${ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME} lambda invoke failed for wallets`, + Severity.MINOR, + { Wallets: walletIdList }, + ); + throw new Error(`${ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME} lambda invoke failed for wallets: ${walletIdList}`); + } +} + +/** + * Sends a message to a specific SQS queue +* + * @param messageBody - A string with the message body + * @param queueUrl - The queue URL + */ +export const sendMessageSQS = async (messageBody: string, queueUrl: string, messageAttributes?: Record): Promise => { + const client = new SQSClient({}); + const command = new SendMessageCommand({ + QueueUrl: queueUrl, + MessageBody: messageBody, + MessageAttributes: messageAttributes, + }); + + return client.send(command); +}; diff --git a/packages/daemon/src/utils/cache.ts b/packages/daemon/src/utils/cache.ts new file mode 100644 index 00000000..8c813f5f --- /dev/null +++ b/packages/daemon/src/utils/cache.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Map remembers the insertion order, so we can use it as a FIFO queue +export class LRU { + max: number; + + cache: Map; + + constructor(max: number = 10) { + this.max = max; + this.cache = new Map(); + } + + get(txId: string): string { + const transaction = this.cache.get(txId); + + if (transaction) { + this.cache.delete(txId); + // Refresh it in the Map + this.cache.set(txId, transaction); + } + + return transaction; + } + + set(txId: string, transaction: string): void { + if (this.cache.has(txId)) { + // Refresh it in the map + this.cache.delete(txId); + } + + // Remove oldest + if (this.cache.size === this.max) { + this.cache.delete(this.first()); + } + + this.cache.set(txId, transaction); + } + + first(): string { + return this.cache.keys().next().value; + } + + clear(): void { + this.cache = new Map(); + } +} + diff --git a/packages/daemon/src/utils/date.ts b/packages/daemon/src/utils/date.ts new file mode 100644 index 00000000..7d6deef1 --- /dev/null +++ b/packages/daemon/src/utils/date.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Get the current Unix timestamp, in seconds. + * + * @returns The current Unix timestamp in seconds + */ +export const getUnixTimestamp = (): number => ( + Math.round((new Date()).getTime() / 1000) +); diff --git a/packages/daemon/src/utils/hash.ts b/packages/daemon/src/utils/hash.ts new file mode 100644 index 00000000..7e985308 --- /dev/null +++ b/packages/daemon/src/utils/hash.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as crypto from 'crypto'; + +/** + * Generates an MD5 hash of the provided string data. + * + * @param data - The string data to hash. + * @returns - The MD5 hash of the data in hexadecimal format. + */ +export const md5Hash = (data: string): string => { + const hash = crypto.createHash('md5'); + hash.update(data); + return hash.digest('hex'); +}; + +/** + * Serializes select transaction metadata attributes into a string format. + * + * @param meta - The transaction metadata to serialize. + * @returns - A serialized string representing specific fields of the metadata. + */ +export const serializeTxData = (meta: unknown): string => + // @ts-ignore + `${meta.hash}|${meta.voided_by.length > 0}|${meta.first_block}|${meta.height}`; + +/** + * Hashes transaction metadata using MD5. + * + * Serializes the relevant fields of transaction metadata and then computes its MD5 hash. + * + * @param meta - The transaction metadata to hash. + * @returns - The MD5 hash of the serialized metadata. + */ +export const hashTxData = (meta: unknown): string => + md5Hash(serializeTxData(meta)) +; diff --git a/packages/daemon/src/utils/helpers.ts b/packages/daemon/src/utils/helpers.ts new file mode 100644 index 00000000..d24e2ac0 --- /dev/null +++ b/packages/daemon/src/utils/helpers.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import getConfig from '../config'; +import { StringMap } from '../types'; + +export function stringMapIterator(stringMap: StringMap): [string, T][] { + return Object.entries(stringMap); +} + +export const getFullnodeHttpUrl = () => { + const { USE_SSL, FULLNODE_HOST } = getConfig(); + const protocol = USE_SSL ? 'https://' : 'http://'; + + const fullNodeUrl = new URL(`${protocol}${FULLNODE_HOST}`); + fullNodeUrl.pathname = '/v1a'; + + return fullNodeUrl.toString(); +}; + +export const getFullnodeWsUrl = () => { + const { USE_SSL, FULLNODE_HOST } = getConfig(); + const protocol = USE_SSL ? 'wss://' : 'ws://'; + + const fullNodeUrl = new URL(`${protocol}${FULLNODE_HOST}`); + fullNodeUrl.pathname = '/v1a/event_ws'; + + return fullNodeUrl.toString(); +}; diff --git a/packages/daemon/src/utils/index.ts b/packages/daemon/src/utils/index.ts new file mode 100644 index 00000000..f037423a --- /dev/null +++ b/packages/daemon/src/utils/index.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export * from './hash'; +export * from './cache'; +export * from './wallet'; +export * from './date'; +export * from './helpers'; diff --git a/packages/daemon/src/utils/wallet.ts b/packages/daemon/src/utils/wallet.ts new file mode 100644 index 00000000..18c1c8a6 --- /dev/null +++ b/packages/daemon/src/utils/wallet.ts @@ -0,0 +1,512 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// @ts-ignore +import hathorLib, { constants, Output } from '@hathor/wallet-lib'; +import { Connection as MysqlConnection } from 'mysql2/promise'; +import { strict as assert } from 'assert'; +import { + AddressBalance, + AddressTotalBalance, + DbTxOutput, + DecodedOutput, + EventTxInput, + EventTxOutput, + StringMap, + TokenBalanceMap, + TokenBalanceValue, + Transaction, + TxInput, + TxOutput, + TxOutputWithIndex, + Wallet, + WalletBalance, + WalletBalanceValue, +} from '../types'; +import { + fetchAddressBalance, + fetchAddressTxHistorySum, + getAddressWalletInfo, + getExpiredTimelocksUtxos, + getTokenSymbols, + unlockUtxos as dbUnlockUtxos, + updateAddressLockedBalance, + updateWalletLockedBalance, +} from '../db'; +import logger from '../logger'; +import { stringMapIterator } from './helpers'; + +/** + * Checks if a given tokenData has any authority bit set + * + * tokenData merges two fields: first bit is the authority flag, while remaining + * bits represent the token index. If the first bit is 0, this is a regular + * output, if it's 1, it's an authority output + */ +export const isAuthority = (tokenData: number): boolean => ( + (tokenData & constants.TOKEN_AUTHORITY_MASK) > 0 +); + +/** + * Prepares transaction outputs with additional metadata and indexing. + * + * This function expects a list of EventTxOutput objects as inputs and an array + * of tokens to produce an array of TxOutputWithIndex objects. Each output is + * enhanced with additional data like the token it represents, its index in the + * transaction, and its decoded information. + * + * @param outputs - An array of transaction outputs, each containing data like value, + * script, and token data. + * @param tokens - An array of token identifiers corresponding to different tokens involved + * in the transaction. + * @returns - An array of outputs, each augmented with index and additional + * metadata. + */ +export const prepareOutputs = (outputs: EventTxOutput[], tokens: string[]): TxOutputWithIndex[] => { + const preparedOutputs: [number, TxOutputWithIndex[]] = outputs.reduce( + ([currIndex, newOutputs]: [number, TxOutputWithIndex[]], _output: EventTxOutput): [number, TxOutputWithIndex[]] => { + const output = new Output(_output.value, Buffer.from(_output.script, 'base64'), { + tokenData: _output.token_data, + }); + + let token = '00'; + if (!output.isTokenHTR()) { + token = tokens[output.getTokenIndex()]; + } + // @ts-ignore + output.token = token; + + if (!_output.decoded + || _output.decoded.type === null + || _output.decoded.type === undefined) { + console.log('Decode failed, skipping..'); + return [currIndex + 1, newOutputs]; + } + + // @ts-ignore + output.locked = false; + + const finalOutput = { + ...output, + index: currIndex, + decoded: _output.decoded, + token_data: output.tokenData, + }; + + // @ts-ignore + return [ currIndex + 1, [ ...newOutputs, finalOutput, ], ]; + }, + [0, []], + ); + + return preparedOutputs[1]; +}; + +/** + * Get the map of token balances for each address in the transaction inputs and outputs. + * + * @example + * Return map has this format: + * ``` + * { + * address1: {token1: balance1, token2: balance2}, + * address2: {token1: balance3} + * } + * ``` + * + * @param inputs - The transaction inputs + * @param outputs - The transaction outputs + * @returns A map of addresses and its token balances + */ +export const getAddressBalanceMap = ( + inputs: TxInput[], + outputs: TxOutput[], +): StringMap => { + const addressBalanceMap = {}; + + for (const input of inputs) { + if (!input.decoded) { + throw new Error('Input has no decoded script'); + } + + const address = input.decoded?.address; + + // get the TokenBalanceMap from this input + const tokenBalanceMap = TokenBalanceMap.fromTxInput(input); + // merge it with existing TokenBalanceMap for the address + // @ts-ignore + addressBalanceMap[address] = TokenBalanceMap.merge(addressBalanceMap[address], tokenBalanceMap); + } + + for (const output of outputs) { + if (!output.decoded) { + throw new Error('Output has no decoded script'); + } + + if (!output.decoded.address) { + throw new Error('Decoded output data has no address'); + } + const address = output.decoded.address; + + // get the TokenBalanceMap from this output + const tokenBalanceMap = TokenBalanceMap.fromTxOutput(output); + + // merge it with existing TokenBalanceMap for the address + // @ts-ignore + addressBalanceMap[address] = TokenBalanceMap.merge(addressBalanceMap[address], tokenBalanceMap); + } + + return addressBalanceMap; +}; + +/** + * Update the unlocked/locked balances for addresses and wallets connected to the given UTXOs. + * + * @param mysql - Database connection + * @param utxos - List of UTXOs that are unlocked by height + * @param updateTimelocks - If this update is triggered by a timelock expiring, update the next lock expiration + */ +export const unlockUtxos = async (mysql: MysqlConnection, utxos: DbTxOutput[], updateTimelocks: boolean): Promise => { + if (utxos.length === 0) return; + + const outputs: TxOutput[] = utxos.map((utxo) => { + const decoded: DecodedOutput = { + type: 'P2PKH', + address: utxo.address, + timelock: utxo.timelock, + }; + + return { + value: utxo.authorities > 0 ? utxo.authorities : utxo.value, + token: utxo.tokenId, + decoded, + locked: false, + // set authority bit if necessary + token_data: utxo.authorities > 0 ? hathorLib.constants.TOKEN_AUTHORITY_MASK : 0, + // we don't care about spent_by and script + spent_by: null, + script: '', + }; + }); + + // mark as unlocked in database (this just changes the 'locked' flag) + await dbUnlockUtxos(mysql, utxos.map((utxo: DbTxOutput): TxInput => ({ + tx_id: utxo.txId, + index: utxo.index, + value: utxo.value, + token_data: 0, + script: '', + token: utxo.tokenId, + decoded: null, + }))); + + const addressBalanceMap: StringMap = getAddressBalanceMap([], outputs); + // update address_balance table + await updateAddressLockedBalance(mysql, addressBalanceMap, updateTimelocks); + + // check if addresses belong to any started wallet + const addressWalletMap: StringMap = await getAddressWalletInfo(mysql, Object.keys(addressBalanceMap)); + + // update wallet_balance table + const walletBalanceMap: StringMap = getWalletBalanceMap(addressWalletMap, addressBalanceMap); + await updateWalletLockedBalance(mysql, walletBalanceMap, updateTimelocks); +}; + +/** + * Get the map of token balances for each wallet. + * + * @remarks + * Different addresses can belong to the same wallet, so this function merges their + * token balances. + * + * @example + * Return map has this format: + * ``` + * { + * wallet1: {token1: balance1, token2: balance2}, + * wallet2: {token1: balance3} + * } + * ``` + * + * @param addressWalletMap - Map of addresses and corresponding wallets + * @param addressBalanceMap - Map of addresses and corresponding token balances + * @returns A map of wallet ids and its token balances + */ +export const getWalletBalanceMap = ( + addressWalletMap: StringMap, + addressBalanceMap: StringMap, +): StringMap => { + const walletBalanceMap = {}; + for (const [address, balanceMap] of Object.entries(addressBalanceMap)) { + const wallet = addressWalletMap[address]; + const walletId = wallet && wallet.walletId; + + // if this address is not from a started wallet, ignore + if (!walletId) continue; + + // @ts-ignore + walletBalanceMap[walletId] = TokenBalanceMap.merge(walletBalanceMap[walletId], balanceMap); + } + return walletBalanceMap; +}; + +/** + * Update the unlocked/locked balances for addresses and wallets connected to the UTXOs that were unlocked + * because of their timelocks expiring + * + * @param mysql - Database connection + * @param now - Current timestamp + */ +export const unlockTimelockedUtxos = async (mysql: MysqlConnection, now: number): Promise => { + const utxos: DbTxOutput[] = await getExpiredTimelocksUtxos(mysql, now); + + await unlockUtxos(mysql, utxos, true); +}; + +/** + * Prepares transaction input data for processing or display. + * + * This function takes an array of EventTxInput objects and an array of token identifiers + * to prepare an array of TxInput objects. Each input is processed to include additional information + * such as the token involved and the decoded output data. + * + * @param inputs - An array of transaction inputs, each containing data like + * transaction hash, index, and spent output information. + * @param tokens - An array of token identifiers corresponding to different tokens involved + * in the transaction. + * @returns - An array of prepared inputs, each enriched with additional data. + */ +export const prepareInputs = (inputs: EventTxInput[], tokens: string[]): TxInput[] => { + const preparedInputs: TxInput[] = inputs.reduce((newInputs: TxInput[], _input: EventTxInput): TxInput[] => { + const output = _input.spent_output; + const utxo: Output = new Output(output.value, Buffer.from(output.script, 'base64'), { + tokenData: output.token_data, + }); + let token = '00'; + if (!utxo.isTokenHTR()) { + token = tokens[utxo.getTokenIndex()]; + } + + const input: TxInput = { + tx_id: _input.tx_id, + index: _input.index, + value: utxo.value, + token_data: utxo.tokenData, + // @ts-ignore + script: utxo.script, + token, + decoded: { + type: output.decoded.type, + address: output.decoded.address, + timelock: output.decoded.timelock, + }, + }; + + return [...newInputs, input]; + }, []); + + return preparedInputs; +}; + +/** + * Mark a transaction's outputs that are locked. Modifies the outputs in place. + * + * @remarks + * The timestamp is used to determine if each output is locked by time. On the other hand, `hasHeightLock` + * applies to all outputs. + * + * The idea is that `hasHeightLock = true` should be used for blocks, whose outputs are locked by + * height. Timelocks are handled by the `now` parameter. + * + * @param outputs - The transaction outputs + * @param now - Current timestamp + * @param hasHeightLock - Flag that tells if outputs are locked by height + */ +export const markLockedOutputs = (outputs: TxOutput[], now: number, hasHeightLock = false): void => { + for (const output of outputs) { + output.locked = false; + if (hasHeightLock || (output.decoded?.timelock ? output.decoded?.timelock : 0) > now) { + output.locked = true; + } + } +}; + +/** + * Gets a list of tokens from a list of inputs and outputs + * + * @param inputs - The transaction inputs + * @param outputs - The transaction outputs + * @returns A list of tokens present in the inputs and outputs + */ +export const getTokenListFromInputsAndOutputs = (inputs: TxInput[], outputs: TxOutputWithIndex[]): string[] => { + const tokenIds = new Set([]); + + for (const input of inputs) { + tokenIds.add(input.token); + } + + for (const output of outputs) { + tokenIds.add(output.token); + } + + return [...tokenIds]; +}; + +/** + * Validates the consistency of address balances. + * + * This method is designed to validate that the sum of unlocked and locked balances + * for each address in a given set matches the corresponding total balance from the address's + * transaction history. + * + * If any of these conditions are not met, the function will throw an assertion error, indicating a mismatch. + * + * @param mysql - The MySQL connection object to perform database operations. + * @param addresses - An array of addresses whose balances need to be validated. + * @returns - The function returns a promise that resolves to void. It does not return + * any value but serves the purpose of validation. + */ +export const validateAddressBalances = async (mysql: MysqlConnection, addresses: string[]): Promise => { + const addressBalances: AddressBalance[] = await fetchAddressBalance(mysql, addresses); + const addressTxHistorySums: AddressTotalBalance[] = await fetchAddressTxHistorySum(mysql, addresses); + + logger.debug(`Validating address balances for ${JSON.stringify(addresses)}`); + + /* We need to filter out zero transactions address balances as we won't have + * any records in the address_tx_history table and the assertion ahead will + * fail. + * + * This might happen after a re-org for an address that only had one transaction + * as this transaction will be removed from the address_tx_history table (or + * marked as voided) and the address_balance table will be updated, removing + * one from the transactions column. + */ + const filteredAddressBalances = addressBalances.filter( + (addressBalance: AddressBalance) => addressBalance.transactions > 0 + ); + + for (let i = 0; i < addressTxHistorySums.length; i++) { + const addressBalance: AddressBalance = filteredAddressBalances[i]; + const addressTxHistorySum: AddressTotalBalance = addressTxHistorySums[i]; + + assert.strictEqual(addressBalance.tokenId, addressTxHistorySum.tokenId); + + // balances must match + assert.strictEqual(Number(addressBalance.unlockedBalance + addressBalance.lockedBalance), Number(addressTxHistorySum.balance)); + } +}; + +/** + * Get a list of wallet balance per token by informed transaction. + * + * @param mysql + * @param tx - The transaction to get related wallets and their token balances + * @returns + */ +export const getWalletBalancesForTx = async (mysql: MysqlConnection, tx: Transaction): Promise> => { + const addressBalanceMap: StringMap = getAddressBalanceMap(tx.inputs, tx.outputs); + // return only wallets that were started + const addressWalletMap: StringMap = await getAddressWalletInfo(mysql, Object.keys(addressBalanceMap)); + + // Create a new map focused on the walletId and storing its balance variation from this tx + const walletsMap: StringMap = {}; + + // Accumulation of tokenId to be used to extract its symbols. + const tokenIdAccumulation = []; + + // Iterates all the addresses to populate the map's data + const addressWalletEntries = stringMapIterator(addressWalletMap); + for (const [address, wallet] of addressWalletEntries) { + // Create a new walletId entry if it does not exist + if (!walletsMap[wallet.walletId]) { + walletsMap[wallet.walletId] = { + txId: tx.tx_id, + walletId: wallet.walletId, + addresses: [], + walletBalanceForTx: new TokenBalanceMap(), + }; + } + const walletData = walletsMap[wallet.walletId]; + + // Add this address to the wallet's affected addresses list + walletData.addresses.push(address); + + // Merge the balance of this address with the total balance of the wallet + const mergedBalance = TokenBalanceMap.merge(walletData.walletBalanceForTx, addressBalanceMap[address]); + walletData.walletBalanceForTx = mergedBalance; + + const tokenIdList = Object.keys(mergedBalance.map); + tokenIdAccumulation.push(tokenIdList); + } + + const tokenIdSet = new Set(tokenIdAccumulation.reduce((prev, eachGroup) => [...prev, ...eachGroup], [])); + const tokenSymbolsMap = await getTokenSymbols(mysql, Array.from(tokenIdSet.values())); + + // @ts-ignore + return WalletBalanceMapConverter.toValue(walletsMap, tokenSymbolsMap); +}; + +export class FromTokenBalanceMapToBalanceValueList { + /** + * Convert the map of token balance instance into a map of token balance value. + * It also hydrate each token balance value with token symbol. + * + * @param tokenBalanceMap - Map of token balance instance + * @param tokenSymbolsMap - Map token's id to its symbol + * @returns a map of token balance value + */ + static convert(tokenBalanceMap: TokenBalanceMap, tokenSymbolsMap: StringMap): TokenBalanceValue[] { + const entryBalances = Object.entries(tokenBalanceMap.map); + const balances = entryBalances.map(([tokenId, balance]) => ({ + tokenId, + tokenSymbol: tokenSymbolsMap[tokenId], + lockedAmount: balance.lockedAmount, + lockedAuthorities: balance.lockedAuthorities.toJSON(), + lockExpires: balance.lockExpires, + unlockedAmount: balance.unlockedAmount, + unlockedAuthorities: balance.unlockedAuthorities.toJSON(), + totalAmountSent: balance.totalAmountSent, + total: balance.total(), + } as TokenBalanceValue)); + return balances; + } +} + +export const sortBalanceValueByAbsTotal = (balanceA: TokenBalanceValue, balanceB: TokenBalanceValue): number => { + if (Math.abs(balanceA.total) - Math.abs(balanceB.total) >= 0) return -1; + return 0; +}; + +export class WalletBalanceMapConverter { + /** + * Convert the map of wallet balance instance into a map of wallet balance value. + * + * @param walletBalanceMap - Map wallet's id to its balance + * @param tokenSymbolsMap - Map token's id to its symbol + * @returns a map of wallet id to its balance value + */ + static toValue(walletBalanceMap: StringMap, tokenSymbolsMap: StringMap): StringMap { + const walletBalanceEntries = Object.entries(walletBalanceMap); + + const walletBalanceValueMap: StringMap = {}; + for (const [walletId, walletBalance] of walletBalanceEntries) { + const sortedTokenBalanceList = FromTokenBalanceMapToBalanceValueList + // hydrate token balance value with token symbol while convert to value + .convert(walletBalance.walletBalanceForTx, tokenSymbolsMap) + .sort(sortBalanceValueByAbsTotal); + + walletBalanceValueMap[walletId] = { + addresses: walletBalance.addresses, + txId: walletBalance.txId, + walletId: walletBalance.walletId, + walletBalanceForTx: sortedTokenBalanceList, + }; + } + + return walletBalanceValueMap; + } +} diff --git a/packages/daemon/tsconfig.json b/packages/daemon/tsconfig.json new file mode 100644 index 00000000..3c872075 --- /dev/null +++ b/packages/daemon/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "types": ["node", "jest"] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/packages/wallet-service/AUTHENTICATION.md b/packages/wallet-service/AUTHENTICATION.md new file mode 100644 index 00000000..84704e2c --- /dev/null +++ b/packages/wallet-service/AUTHENTICATION.md @@ -0,0 +1,66 @@ +# Authentication + +### Protected functions + +- wallet + - get +- addresses + - get +- balances + - get +- txhistory + - get +- txproposals + - post + - put + - delete + +### JWT Authentication + +To request a JWT token you need to provide some information to the `auth/token` view. +The method below creates the object with the information needed (must provide xprivkey). + +```typescript +import hathorLib from '@hathor/wallet-lib'; +import bitcore from 'bitcore-lib'; + +const createSignatureData = ( + xprivkey: string +): string => { + const hdprivkey = new bitcore.HDPrivateKey(xprivkey); + + // derive hdprivkey to desired path + // skip this step if xprivkey was already a derived key + const derivedPrivKey = hdprivkey.deriveChild("m/44'/280'/0'/0"); + + const timestamp = Math.floor(Date.now() / 1000); + // remember to use hathor's wallet-lib network, not bitcore default nertwork + const address = derivedPrivKey.publicKey.toAddress(hathorLib.network.getNetwork()).toString(); + // walletId == sha256sha256 of xpubkey as hex + const walletId = getWalletId(derivedPrivKey.xpubkey); + + // message is a concatenation of known data: timestamp+walletId+address + const message = new bitcore.Message(String(timestamp).concat(walletId).concat(address)); + + return { + 'ts': timestamp, + 'xpub': derivedPrivKey.xpubkey, + 'sign': message.sign(derivedPrivKey.privateKey), + }; +}; +``` + +The endpoint will return a JSON response with: + +```ts +{ + "success": true, + "token": "..." +} +``` + +The token in this response shoud be used to authenticate the caller on any calls listed on [#Protected functions]() + +### Authentication Header + +For http(s) triggers, the caller should include the token on the `Authorization` header using the bearer scheme. (i.e. `Bearer abc123token`) diff --git a/packages/wallet-service/DATABASE.md b/packages/wallet-service/DATABASE.md new file mode 100644 index 00000000..96f4a492 --- /dev/null +++ b/packages/wallet-service/DATABASE.md @@ -0,0 +1,193 @@ +# Database + +The service requires the following databases to work. + +``` +// TODO most `varchar` fields can be converted to `binary` +// TODO create db indexes + +CREATE TABLE `address` ( + `address` varchar(34) NOT NULL, + `index` int unsigned DEFAULT NULL, + `wallet_id` varchar(64) DEFAULT NULL, + `transactions` int unsigned NOT NULL, + PRIMARY KEY (`address`) +); + +-- Unlocked authorities represents: +-- null or 0b00 - Has no authority +-- 0b01 - Mint authority +-- 0b11 - Mint and Melt authority +-- 0b10 - Melt authority + +-- This is always up to date with the authorities in every +-- UTXO for this address. + +CREATE TABLE `address_balance` ( + `address` varchar(34) NOT NULL, + `token_id` varchar(64) NOT NULL, + `unlocked_balance` bigint unsigned NOT NULL, + `locked_balance` bigint unsigned NOT NULL, + `unlocked_authorities` tinyint unsigned NOT NULL DEFAULT '0', + `locked_authorities` tinyint unsigned NOT NULL DEFAULT '0', + `timelock_expires` int unsigned, + `transactions` int unsigned NOT NULL, + PRIMARY KEY (`address`,`token_id`) +); + +CREATE TABLE `address_tx_history` ( + `address` varchar(34) NOT NULL, + `tx_id` varchar(64) NOT NULL, + `token_id` varchar(64) NOT NULL, + `balance` bigint NOT NULL, + `timestamp` int unsigned NOT NULL, + `voided` tinyint unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`address`,`tx_id`,`token_id`) +); + +-- This should allow for only one row at a time +-- We do this by using the +CREATE TABLE `version_data` ( + `id` int unsigned NOT NULL DEFAULT 1, + `timestamp` bigint unsigned NOT NULL, + `version` varchar(11) NOT NULL, + `network` varchar(8) NOT NULL, + `min_weight` float unsigned NOT NULL, + `min_tx_weight` float unsigned NOT NULL, + `min_tx_weight_coefficient` float unsigned NOT NULL, + `min_tx_weight_k` float unsigned NOT NULL, + `token_deposit_percentage` float unsigned NOT NULL, + `reward_spend_min_blocks` int unsigned NOT NULL, + `max_number_inputs` int unsigned NOT NULL, + `max_number_outputs` int unsigned NOT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `token` ( + `id` varchar(64) NOT NULL, + `name` varchar(30) NOT NULL, + `symbol` varchar(5) NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`)); + +CREATE TABLE `tx_proposal` ( + `id` varchar(36) NOT NULL, + `wallet_id` varchar(64) NOT NULL, + `status` enum('open','sent','send_error','cancelled') NOT NULL, + `created_at` int unsigned NOT NULL, + `updated_at` int unsigned DEFAULT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `tx_output` ( + `tx_id` varchar(64) NOT NULL, -- tx_id might point to a block + `index` tinyint unsigned NOT NULL, + `token_id` varchar(64) NOT NULL, + `address` varchar(34) NOT NULL, + `value` bigint unsigned NOT NULL, + `authorities` tinyint unsigned DEFAULT NULL, + `timelock` int unsigned DEFAULT NULL, + `heightlock` int unsigned DEFAULT NULL, + `locked` tinyint unsigned NOT NULL DEFAULT '0', + `tx_proposal` varchar(36) DEFAULT NULL, + `tx_proposal_index` tinyint unsigned DEFAULT NULL, + `spent_by` varchar(64) DEFAULT NULL, + `voided` tinyint unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`tx_id`,`index`) +); + +CREATE TABLE `wallet` ( + `id` varchar(64) NOT NULL, + `xpubkey` varchar(120) NOT NULL, + `status` enum('creating','ready','error') NOT NULL DEFAULT 'creating', + `max_gap` smallint unsigned NOT NULL DEFAULT '20', + `created_at` int unsigned NOT NULL, + `ready_at` int unsigned DEFAULT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `wallet_balance` ( + `wallet_id` varchar(64) NOT NULL, + `token_id` varchar(64) NOT NULL, + `unlocked_balance` bigint unsigned NOT NULL, + `locked_balance` bigint unsigned NOT NULL, + `unlocked_authorities` tinyint unsigned NOT NULL DEFAULT '0', + `locked_authorities` tinyint unsigned NOT NULL DEFAULT '0', + `timelock_expires` int unsigned, + `transactions` int unsigned NOT NULL, + PRIMARY KEY (`wallet_id`,`token_id`) +); + +CREATE TABLE `wallet_tx_history` ( + `wallet_id` varchar(64) NOT NULL, + `token_id` varchar(64) NOT NULL, + `tx_id` varchar(64) NOT NULL, + `balance` bigint NOT NULL, + `timestamp` int unsigned NOT NULL, + `voided` tinyint unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`wallet_id`,`token_id`,`tx_id`) +); + +CREATE TABLE `transaction` ( + `tx_id` varchar(64) NOT NULL, + `timestamp` int unsigned NOT NULL, + `version` tinyint unsigned NOT NULL, + `voided` boolean NOT NULL DEFAULT false, + -- Height is the block's height if it's a block and the height of the `first_block` if it is a transaction. + `height` int unsigned DEFAULT NULL, + `weight` float unsigned NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`tx_id`) +); + +CREATE TABLE `push_devices` ( + `device_id` varchar(256) NOT NULL, + `push_provider` enum('ios','android') NOT NULL, + `wallet_id` varchar(64) NOT NULL, + `enable_push` tinyint(1) NOT NULL DEFAULT '0', + `enable_show_amounts` tinyint(1) NOT NULL DEFAULT '0', + `enable_only_new_tx` tinyint(1) NOT NULL DEFAULT '0', + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`device_id`), + KEY `wallet_id` (`wallet_id`), + CONSTRAINT `push_devices_ibfk_1` FOREIGN KEY (`wallet_id`) REFERENCES `wallet` (`id`) +); + +CREATE INDEX transaction_version_idx USING HASH ON `transaction`(`version`); +CREATE INDEX tx_output_heightlock_idx USING HASH ON `tx_output`(`heightlock`); +CREATE INDEX tx_output_timelock_idx USING HASH ON `tx_output`(`timelock`); +CREATE INDEX transaction_height_idx USING HASH ON `transaction`(`height`); +CREATE INDEX transaction_updated_at_idx USING HASH ON `transaction`(`updated_at`); + +``` + +# Genesis transactions + +We need to add the genesis transactions to the database as the service expects to already have them. + +## Mainnet +``` +INSERT INTO `transaction` (`tx_id`, `height`, `timestamp`, `version`, `voided`) VALUES ('000006cb93385b8b87a545a1cbb6197e6caff600c12cc12fc54250d39c8088fc', 0, 1578075305, 0, FALSE); +INSERT INTO `tx_output` (`tx_id`, `index`, `token_id`, `address`, `value`) + VALUES ('000006cb93385b8b87a545a1cbb6197e6caff600c12cc12fc54250d39c8088fc', + 0, + '00', + 'HJB2yxxsHtudGGy3jmVeadwMfRi2zNCKKD', + 100000000000 + ); +``` + +## Testnet + +``` +INSERT INTO `transaction` (`tx_id`, `height`, `timestamp`, `version`, `voided`) VALUES ('0000033139d08176d1051fb3a272c3610457f0c7f686afbe0afe3d37f966db85', 0, 1577836800, 0, FALSE); +INSERT INTO `tx_output` (`tx_id`, `index`, `token_id`, `address`, `value`) + VALUES ('0000033139d08176d1051fb3a272c3610457f0c7f686afbe0afe3d37f966db85', + 0, + '00', + 'WdmDUMp8KvzhWB7KLgguA2wBiKsh4Ha8eX', + 100000000000 + ); +``` diff --git a/packages/wallet-service/LICENSE b/packages/wallet-service/LICENSE new file mode 100644 index 00000000..7cb8bb9c --- /dev/null +++ b/packages/wallet-service/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Hathor Labs + +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 +SOFTWARE. diff --git a/packages/wallet-service/Makefile b/packages/wallet-service/Makefile new file mode 100644 index 00000000..1c8c1e0d --- /dev/null +++ b/packages/wallet-service/Makefile @@ -0,0 +1,36 @@ +.PHONY: deploy-lambdas-dev-testnet +deploy-lambdas-dev-testnet: + npx serverless deploy --stage dev-testnet --region eu-central-1 + +.PHONY: deploy-lambdas-testnet +deploy-lambdas-testnet: + npx serverless deploy --stage testnet --region eu-central-1 + +.PHONY: deploy-lambdas-mainnet-staging +deploy-lambdas-mainnet-staging: + npx serverless deploy --stage mainnet-stg --region eu-central-1 + +.PHONY: deploy-lambdas-mainnet +deploy-lambdas-mainnet: + npx serverless deploy --stage mainnet --region eu-central-1 + +.PHONY: migrate +migrate: + @echo "Migrating..." + npx sequelize-cli db:migrate + +.PHONY: new-migration +new-migration: + npx sequelize migration:generate --name "$(NAME)" + +.PHONY: seed_testnet +seed_testnet: + npx sequelize-cli db:seed --seed testnet + +.PHONY: seed_mainnet +seed_mainnet: + npx sequelize-cli db:seed --seed mainnet + +.PHONY: cleanup +cleanup: + rm db.sqlite3 diff --git a/packages/wallet-service/README.md b/packages/wallet-service/README.md new file mode 100644 index 00000000..1c4c59fa --- /dev/null +++ b/packages/wallet-service/README.md @@ -0,0 +1,222 @@ +# hathor-wallet-service + +The hathor-wallet-service is the backend for all official Hathor wallets. + +It's designed to run on AWS serverless environment and uses a MySQL database for persisting data. Upon receiving a new +transaction, a lambda handles updating the data. Later, when a wallet queries it, API lambdas only need to query the +database to get the information. + +``` ++─────────────+ +─────────────+ +──────────+ +| | new | | | | +| Sync Daemon | ───────────────────▶ | txProcessor | | Database | +| (k8s) | transactions | (Lambda) | ──────▶ | (RDS) | +| | | | | | ++─────────────+ +─────────────+ +──────────+ + ▲ ▲ + | | + ▼ ▼ ++────────────+ +────────────────────────+ +| | | APIs | +| Fullnode | | (API Gateway & Lambda) | +| | +────────────────────────+ ++────────────+ ▲ + | + wallet | requests + | +``` + + +## Test locally +The plugin `serverless-offline` is used to emulate AWS Lambda and API Gateway on a local machine. + +### Requirements +1. NodeJS v16 + +### Local database +To setup a local database, you will need: + +1. A MySQL database running and the env configured with its connection information +1. Run `npx sequelize-cli db:migrate` + +This should run all migrations from the `db/migrations` folder and get the database ready + +### .env file + +Create a `.env` file on the top project folder. It should have the following variables: +``` +STAGE=local +NETWORK=mainnet +SERVICE_NAME=hathor-wallet-service +MAX_ADDRESS_GAP=10 +VOIDED_TX_OFFSET=5 +BLOCK_REWARD_LOCK=300 +CONFIRM_FIRST_ADDRESS=true +WS_DOMAIN=ws.wallet-service.hathor.network +DEFAULT_SERVER=https://node1.mainnet.hathor.network/v1a/ +DB_ENDPOINT=localhost +DB_NAME=wallet_service +DB_USER=my_user +DB_PASS=password123 +REDIS_HOST=localhost +REDIS_PORT=6379 +AUTH_SECRET=foobar +EXPLORER_SERVICE_LAMBDA_ENDPOINT=http://localhost:3001 +WALLET_SERVICE_LAMBDA_ENDPOINT=http://localhost:3000 +PUSH_NOTIFICATION=true +PUSH_ALLOWED_PROVIDERS=android +``` + +Do not modify the `STAGE` variable. The other variables should be updated accordingly. + +### AWS cli credentials + +You need to have `awscli` [configured with your credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). +It is required even to locally invoke lambdas. + +### Start serverless-offline + +``` +npm run offline +``` +By default, it listens for API calls on `http://localhost:3000`. + +### Sync with the fullnode + +The sync with the fullnode is made using the [Sync Daemon](https://github.com/HathorNetwork/hathor-wallet-service-sync_daemon) + +### API calls + +After serverless-offline is running, you can make API calls. Here are some examples. + +#### Load a wallet +``` +curl --header "Content-Type: application/json" \ + --request POST \ + --data '{"xpubkey":"xpub6EcBoi2vDFcCW5sPAiQpXDYYtXd1mKhUJD64tUi8CPRG1VQFDkAbL8G5gqTmSZD6oq4Yhr5PZ8pKf3Xmb3W3pGcgqzUdFNaCRKL7TZa3res"}' \ + http://localhost:3000/wallet/ +``` + +#### Fetch wallet status +``` +curl --request GET http://localhost:3000/wallet/?id=23b44673413f093180ed37ce34b6577d7dedbdec9c1d909fe42be1b0bc341ec9 +``` + +#### Fetch wallet balance +``` +curl --request GET http://localhost:3000/balances/?id=23b44673413f093180ed37ce34b6577d7dedbdec9c1d909fe42be1b0bc341ec9 +``` + +#### Fetch wallet addresses +``` +curl --request GET http://localhost:3000/addresses/?id=23b44673413f093180ed37ce34b6577d7dedbdec9c1d909fe42be1b0bc341ec9 +``` + +#### Fetch tx history +``` +curl --request GET 'http://localhost:3000/txhistory/?id=23b44673413f093180ed37ce34b6577d7dedbdec9c1d909fe42be1b0bc341ec9&count=5' +``` + +#### Create tx proposal +You need to have some balance for this to succeed. + +``` +curl --header "Content-Type: application/json" \ + --request POST \ + --data '{ "id": "23b44673413f093180ed37ce34b6577d7dedbdec9c1d909fe42be1b0bc341ec9", "outputs": [{ "address": "H8F5neU87G8gs9XNbNY1XxN9DkAKQKhMoj", "value": 10, "token": "00", "timelock": null}] }' \ + http://localhost:3000/txproposals/ +``` + +#### Send tx proposal +Proposal must have been created before. Use the proposal id in the path and also update the parents, inputs signatures, weight and nonce. +``` +curl --header "Content-Type: application/json" \ + --request PUT \ + --data '{ "timestamp": 1599051796, "parents": ["0002ad8d1519daaddc8e1a37b14aac0b045129c01832281fb1c02d873c7abbf9", "0002d4d2a15def7604688e1878ab681142a7b155cbe52a6b4e031250ae96db0a"], "weight": 1, "nonce": 700, "inputsSignatures": ["aaaa"] }' \ + http://localhost:3000/txproposals/{txProposalId}/ +``` + +### WebSocket API + +It's designed to run on AWS serverless environment and uses Redis for ephemeral data (connection store). +API Gateway will manage connections while the lambda functions will handle incoming and outcoming messages. +If the lambda is responding to a client sent event it will have the information needed to respond to the client that initiated the call, +but if the event is not client initiated, the connection store should hold an updated list of connected clients and what information they are requesting +so the lambda can filter and send the message to the right clients. + + +``` + +───────────+ + +─────────────+ | | + | | +───────────+| +───────────────────+ + ◀──────────────────▶ | Api Gateway | | || | | + ws connections | | ◀─────▶ | Lambda fn |+ ◀──── | SQS | + +─────────────+ | | | (Real Time event) | + +───────────+ | | + | +───────────────────+ + | + ▼ + +────────────────────+ + | Redis | + | (Connection store) | + +────────────────────+ +``` + +#### WebSocket Action: PING +- Trigger: Client initiated +- body: `{"action":"ping"}` +- response: `{"message":"pong"}` + +This action is idempotent, the lambda just responds with a `PONG` message. + +#### WebSocket Action: Join Wallet +- Trigger: Client initiated +- body: `{"action":"join", "id":"my-wallet-id"}` + +This action will subscribe the client to any updates of the wallet identified by the id on the body. + +#### WebSocket Action: New TX +- Trigger: SQS Event +- When: A new tx is processed by the wallet-service +- To: All wallets affected by the tx +- body: `{"type": "new-tx", "data": "..."}` + +#### WebSocket Action: Update TX +- Trigger: SQS Event +- When: An update is made to a tx that was already processed +- To: All wallets affected by the tx +- body: `{"type": "update-tx", "data": "..."}` + +### Troubleshooting + +#### bitcore-lib + +> Error: More than one instance of bitcore-lib found + +This is probably only a bug when running locally. Used this hack to get it working: +https://github.com/bitpay/bitcore/issues/1454#issuecomment-306900782 + +#### jest using old files + +Sometimes, jest will use old cached js files, even after you modified the typescript code. Just run: + +```bash +./node_modules/.bin/jest --clearCache +``` + +## Standard Operating Procedures + +Check it in [docs/SOP.md](docs/SOP.md) + + +## Nix flakes + +## Using this project + +This project uses [Nix](https://nixos.org/) with [direnv](https://direnv.net/) to help with dependencies, including Node.js. To get started, you need to have Nix and direnv installed. + +1. Install [Nix](https://nixos.org/download.html) and [Direnv](https://direnv.net/docs/installation.html). +2. Enable flake support in Nix: `nix-env -iA nixpkgs.nixUnstable` +3. Allow direnv to work in your shell by running `direnv allow` + +Now, every time you enter the project directory, direnv will automatically activate the environment from flake.nix, including the specific version of Node.js specified there. When you leave the directory, it will deactivate. This ensures a consistent and isolated environment per project. diff --git a/codecov.yml b/packages/wallet-service/codecov.yml similarity index 91% rename from codecov.yml rename to packages/wallet-service/codecov.yml index 76e3c71f..94f12a10 100644 --- a/codecov.yml +++ b/packages/wallet-service/codecov.yml @@ -6,13 +6,13 @@ coverage: project: default: # minimum coverage ratio that the commit must meet to be considered a success - target: 45% + target: 88% if_ci_failed: error only_pulls: true patch: default: # minimum coverage ratio that the commit must meet to be considered a success - target: 80% + target: 88% if_ci_failed: error only_pulls: true diff --git a/packages/wallet-service/docker-compose.yml b/packages/wallet-service/docker-compose.yml new file mode 100644 index 00000000..e4a2ef4c --- /dev/null +++ b/packages/wallet-service/docker-compose.yml @@ -0,0 +1,27 @@ +version: "3.8" +services: + mysql: + image: centos/mysql-80-centos7 + environment: + MYSQL_DATABASE: wallet_service_ci + MYSQL_USER: wallet_service_user + MYSQL_PASSWORD: password + MYSQL_DEFAULT_AUTHENTICATION_PLUGIN: mysql_native_password + ports: + - 3306:3306 + healthcheck: + test: ["CMD", "mysqladmin", "ping"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 30s + redis: + image: redis:6.2 + ports: + - 6379:6379 + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s diff --git a/packages/wallet-service/docs/2021-07-29-infrastructure-design.md b/packages/wallet-service/docs/2021-07-29-infrastructure-design.md new file mode 100644 index 00000000..6558db79 --- /dev/null +++ b/packages/wallet-service/docs/2021-07-29-infrastructure-design.md @@ -0,0 +1,167 @@ +# Infrastructure Design + +Date: 2021-07-29 + +Issue: https://github.com/HathorNetwork/hathor-wallet-service/issues/80 + +## Summary + +- [Infrastructure Components](#infrastructure-components) +- [Continuous Integration](#continuous-integration) +- [Infra as Code](#infra-as-code) +- [Continuous Deployment](#continuous-deployment) +- [Monitoring](#monitoring) +- [Security](#security) + +## Infrastructure Components +- MySQL Database +- Redis Server +- Api Gateway + Lambdas +- SQS Queue +- Daemon (Kubernetes) +- FullNode (Kubernetes) + +We have a diagram of the interaction between them in https://github.com/HathorNetwork/ops-tools/blob/master/infra-diagram/img/hathor-wallet-service.png (private repo) + +## Continuous Integration + +- We will run tests on every PR, and spin up MySQL and Redis containers to be used in the tests +- The migrations and initial seed data will be run in this MySQL container before running the tests +- We will integrate with Codecov for test coverage reports + +## Infra as Code +We will try to keep everything commited as code. To achieve this, 3 mechanisms will be used: + +### Serverless +Will be used to describe and create the Lambdas, API Gateway and SQS Queue. The files will be commited in the same repo as the application. + +### Terraform +Will be used to describe and create any additional AWS resources that we need, mainly Redis with Elasticache and MySQL with RDS, but also everything else, like Security Groups, Route53 domains, CloudWatch Alarms, etc. + +Those files will be located in our infra private repo. + +### Kubernetes +Will be used to describe and create what will be run inside it, which currently are the Daemon and the FullNode. + +We will be using additional tools that are already installed in our cluster to support the application, like: +- NginxIngress to expose the FullNode internally in the VPC +- CertManager to generate SSL certificates for the FullNode's domain +- Flux as part of the CD pipeline. Check the [session below](#continuous-deployment) + +Everything will be commited as code in our infra private repo. + +## Continuous Deployment +We have two differente services to deploy, WalletService and Daemon. + +WalletService's deploy will consist of: + +- Run migrations in the database +- Deploy the new Lambdas + +Daemon's deploy will consist of: +- Build and push a new Docker image +- Flux detects the image and updates the container + +Those tasks will be orchestrated by AWS CodePipeline and CodeBuild, and will be automatically triggered by events in the Github repository. For deployments in the mainnet, the event will be the creation of a release tag. For testnet and dev environments, commits in master and dev branches. + +The reason for choosing Code Pipeline is that it's capable of accessing the database through our VPC to perform migrations in a safe manner. Check this issue for more details about this decision: https://github.com/HathorNetwork/ops-tools/issues/113 + +### How the process works +We use CodeBuild and CodePipeline in conjunction, in a series of steps. + +Those are the steps that happen when a commit or tag is created in Github: + +1. A Github Webhook in the repo sends a request to one of our CodeBuild projects, that we will call [github-trigger](https://eu-central-1.console.aws.amazon.com/codesuite/codebuild/769498303037/projects/hathor-wallet-service-github-trigger/history). +2. This project checks the branch and the author of the commit, to decide if it should run the build. If so, it runs some steps that are defined inline in the project, to write to a file which branch is being deployed, and uploads the project's code and this file to an S3 bucket: https://eu-central-1.console.aws.amazon.com/codesuite/codebuild/projects/hathor-wallet-service-github-trigger/edit/buildspec?region=eu-central-1 +3. A CodePipeline project is listening to changes in the bucket, and triggers when it detects them. +4. CodePipeline calls its next step, which is asking for Manual Approval. A message is sent to Slack channel `#deploys`. +5. After the approval is granted, CodePipeline procceeds to the next step, which is another CodeBuild project, which we will call `deploy`. The files deployed to the S3 bucket in step 2 are passed to this CodeBuild project. +6. The `deploy` CodeBuild runs the steps defined in `.codebuild/buildspec.yml` in this repo, to run migrations and deploy the Lambdas. + + +### Maintenance mode +A maintenance mode will be implemented in the WalletService, if we need to warn users before we run some potentially dangerous or slow migration that could cause downtimes. + +This maintenance mode will work by setting a flag in our Redis instance, that the service will use to know that this mode is enabled and warn the wallets. + +A Lambda function will be built that enables/disables the mode, and its execution will be triggered by the developers before deploying a new version. + +We will use CodePipeline's Manual Approval mechanism to ALWAYS stop the deployment pipeline and warn the user that he should think about whether the maintenance mode should be activated for this deployment or not. + +It will be the developer's responsibility to make this decision. + +If he/she thinks that it's safe to proceed, then the deployment can be approved. Otherwise, he/she will have to enable the maintenance mode manually before approving the deployment, and disabling it afterwards. + +### Avoiding downtimes during schema migrations + +The thing that inspired the maintenance mode above is the possibility we have of generating downtimes during database schema migrations. + +There are 2 possible causes of downtime during schema migrations: + +1. Downtime because of mismatch between DB schema and application code +2. Tables locked while migration runs + +The first case is only solvable by making sure we only do backwards-compatible changes in the DB schema. This article has some good examples: https://spring.io/blog/2016/05/31/zero-downtime-deployment-with-a-database + +The second case is more difficult to solve completely. It doesn't seem to be worth it to try, because it would introduce a lot of additional complexity to our setup. Probably something like a Blue-Green deployment would be needed, including replication of the database, and this creates too much complexity, like making sure the DBs are in-sync, which includes syncing them even when one has run the schema migrations while the other hasn't yet. + +So the best option seems to be simply minimize the effects of possible locks in the database. And that's where the maintenance mode comes into hand. + +#### How to know if a migration is downtime-safe + +1. Make sure all changes in the schema are backwards compatible. To know this, just think: If I run the migrations but do not update the application, will it still work? + +2. Make sure all operations run by the migrations do not lock whole tables, especially if they are big ones. + +MySQL includes features for online schema changes, and a lot of operations already allow running DML operations while a DDL operation is running. So schema migrations that just run those operations could be run without locking the table: https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html#online-ddl-column-operations + +In some cases it will work out of the box, but to be 100% sure, we should include the options `LOCK=NONE, ALGORITHM=INPLACE` in the schema migrations performed in big tables. + +### Alternatives +Other options were discussed in https://github.com/HathorNetwork/hathor-wallet-service/issues/80#issuecomment-879973859 + +## Monitoring + +We will employ different strategies to monitor the critical events we want to be alerted of. + +This is a table summarizing the main events and how they will be monitored. + +### Wallet Service + +| Event | Proposed Solution | +|-----------------------------------------------------------------------------|--------------------------------------------------------------------------- | +| Error on balance calculation, on MySQL connection or on FullNode connection | Log an error or exit error in the Lambda, then put CloudWatch alarms on then. | +| WalletService and FullNode out of sync | Expose the highest block height to Prometheus through API Gateway, and compare with the FullNode. | + + +### Daemon + +| Event | Proposed Solution | +|-----------------------------------------------------------------------------|------------------------------------------------------------------------ | +| A reorg is detected with more than 1000 blocks difference | Log this event with a marker, then create Alarms when the marker appears | +| More than X minutes/seconds without a new block from the connected fullnode | Already monitored in the full-nodes. | | | +| Websocket connection lost with the full-node after X retries | Log this event with a marker, then create Alarms when the marker appears | +| Daemon and FullNode out of sync | Expose the highest block height from the Daemon to Prometheus | + + +### Databases + +| Event | Proposed Solution | +|-----------------------------------------------------------------------------|------------------------------------------------------------------------- | +| AWS RDS metrics | CloudWatch alarms to warn about the most important ones | +| Locks in tables | Use MySQL Exporter to extract metrics to Prometheus and create alerts on this | +| Slow Queries | Use MySQL Exporter to extract metrics to Prometheus and create alerts on this | +| Slow Migrations | Measure the time they take to run in AWS CodePipeline | + +## Security + +Those are the security measures we will be taking: + +- The Database, Redis Server and FullNode will be exposed only inside our VPC +- Rate Limits will be configured in Api Gateway +- An authentication mechanism to assure only the owner can listen to a wallet in websocket is being designed in https://github.com/HathorNetwork/hathor-wallet-service/issues/84 + +## Other Aspects + +- The MySQL Database will have daily backups in AWS RDS +- To upgrade the full-node, one just has to do the same as in https://github.com/HathorNetwork/ops-tools/pull/71/files diff --git a/packages/wallet-service/docs/2022-04-18-feature-toggles.md b/packages/wallet-service/docs/2022-04-18-feature-toggles.md new file mode 100644 index 00000000..26e7f9cc --- /dev/null +++ b/packages/wallet-service/docs/2022-04-18-feature-toggles.md @@ -0,0 +1,100 @@ +# Feature Toggles + +Date: 2022-04-18 + +## Summary + +To control the rollout of the service to our production users on the mobile and and desktop wallets, we use a feature-flag service that will answer "Yes" or "No" for user requests, depending on a list of strategies that we define. Currently, the service we are using is [Unleash](https://www.getunleash.io/) + +This document describes the main features we are using and how to interact with it, when needed. + +## Feature toggles + +We have a list of [feature toggles](https://docs.getunleash.io/advanced/feature_toggle_types) that are queried by the wallets: + +* `wallet-service-mobile-android-mainnet.rollout` +* `wallet-service-mobile-android-testnet.rollout` +* `wallet-service-mobile-ios-mainnet.rollout` +* `wallet-service-mobile-ios-testnet.rollout` +* `wallet-service-wallet-desktop-mainnet.rollout` +* `wallet-service-wallet-desktop-testnet.rollout` + +Those feature toggles are `Release` toggles and represent each wallet and the network they are connected to, e.g. when the mobile wallet on iOS on the `mainnet` wants to know wether to use or not the wallet-service facade, it will request the `wallet-service-mobile-ios-mainnet.rollout` feature-flag that will answer "Yes" or "No" based on a list of strategies. + + +## Stategies + +> It is powerful to be able to turn a feature on and off instantaneously, without redeploying the application. The next level of control comes when you are able to enable a feature for specific users or enable it for a small subset of users. We achieve this level of control with the help of activation strategies. The most straightforward strategy is the standard strategy, which basically means that the feature should be enabled to everyone. + +> Unleash comes with a number of built-in strategies (described below) and also lets you add your own custom activation strategies if you need more control. However, while activation strategies are defined on the server, the server does not implement the strategies. Instead, activation strategy implementation is done client-side. This means that it is the client that decides whether a feature should be enabled or not. + +**From the unleash docs [here](https://docs.getunleash.io/user_guide/activation_strategy#userids)** + +We have a set of strategies configured for each of the feature toggles described above, they are: + + +1. [UserIDs](https://docs.getunleash.io/user_guide/activation_strategy#userids) + +Activates for users with a `userId` defined in the `userIds` list. We are currently using unique device identifiers on the mobile wallets (ios and android) and a random identifier on the desktop wallet (that is stored on the device's storage to persist between restarts). + +1. [Gradual rollout](https://docs.getunleash.io/user_guide/activation_strategy#gradual-rollout) + +This is a `percentage` based strategy, it will answer the feature toggles depending on the percentage of users that already received a positive or negative answer. + +For [stickness](https://docs.getunleash.io/advanced/stickiness), we are currently using `userId` on all the feature toggles, so if an user receives a positive response to the feature toggle request, it will continue receiving a positive response on consecutive requests + +### Adding a specific user to the UserID strategy + +The mechanism we have for making sure an user always receive a positive response for a feature toggle is by setting his unique identifier on the UserID strategy for the feature toggle his device requests + +These are the steps we need to take to add a new user to wallet-service: + +1. Ask for the user's unique identifier +2. Ask for the user's operational system +3. Find the correct feature toggle for his OS, device information and network + +![Image1](images/feature-toggle-img1.jpg) + +4. On the feature toggle details page, find the `UserIds` strategy on the `Activation strategies` section + +![Image2](images/feature-toggle-img2.jpg) + +5. Click on the pencil icon to edit it + +![Image3](images/feature-toggle-img3.jpg) + +6. Add the unique identifier using the "Add items" textbox and save +7. Ask for the user to close and re-open his app + + +### Disabling a feature toggle + +The best way to return false to a feature toggle for all our users is to disable the feature toggle entirely, this will ignore all configured strategies and return `false` to all feature toggle requests + +A feature toggle can be disabled either through the features list screen on the unleash frontend or on the feature toggle details screen + +![Feature toggles list screen](images/feature-toggle-img4.jpg) +***Feature toggles list screen*** + +![Feature toggles details screen](images/feature-toggle-img5.jpg) +***Feature toggles details screen*** + +Our wallets are constantly polling unleash for feature toggle updates, so on the next poll, if the wallet-service feature toggle changed to `false` and it was `true`, it will trigger a reload, sending the user to the old facade. + +### Setting a percentage of our user base to use a feature + +We have the `gradual rollout strategy` on all of our wallet-service feature toggles, so in order to change the percentage of our total userbase that will receive `true` for the wallet-service facade feature toggle, we need to do the following: + +1. Go on the `feature toggles list screen` and find the correct feature flag for the device OS we want to change + +![Feature Toggles List](images/feature-toggle-img1.jpg) + +2. On the feature toggle details screen, find the `Gradual rollout strategy` and click on the `pen` icon to edit + +![Gradual Rollout Strategy](images/feature-toggle-img7.jpg) + +3. Change the rollout percentage to the target percentage and save + +![Gradual Rollout Strategy](images/feature-toggle-img6.jpg) + +**Notice**: This will not trigger a refresh on the user's apps, they will have to close and open their apps again to load their wallets on the wallet-service facade. diff --git a/packages/wallet-service/docs/SOP.md b/packages/wallet-service/docs/SOP.md new file mode 100644 index 00000000..82d4d2a5 --- /dev/null +++ b/packages/wallet-service/docs/SOP.md @@ -0,0 +1,64 @@ +# Standard Operating Procedures + +## Deploying + +The deployment is partially automated with CodeBuild and CodePipeline in AWS. + +It's triggered when commits are made to `dev` or `master` branches, and when tags with names like `v*` are created. + +Each case deploys to a different environment: +- `dev` branch -> `dev-testnet` environment +- `master` branch -> `testnet` environment +- `v*` tags -> `mainnet` environment + +All of them require manual approval to proceed. You should keep an eye in the `#wallet-service-deploys` channel in Slack, the approval requests are sent there. + +If you need to know the exact steps that take place during deployment, check [this document](2021-07-29-infrastructure-design.md#how-the-process-works) + +### Avoiding downtime +We always want to make sure the migrations do not generate downtimes while running. + +Check [this document](2021-07-29-infrastructure-design.md#avoiding-downtimes-during-schema-migrations) for more info on how to build safe migrations. + +In case it's not possible to build a downtime-safe migration (this can happen), we have a maintenance mode in place that should be enabled before deploying, or before approving the deployment request in CodePipeline. Check below how to enable it. + +## Adding new environment variables + +If you need to add new environment variables, there are some steps that should be taken. + +Let's say we want to add the `ENV_VAR_1` env var. + +First step would be to add it to the [serverless.yml](https://github.com/HathorNetwork/hathor-wallet-service/blob/master/serverless.yml) file, under `provider.environment`. + +Then, you need to add it in [.codebuild/buildspec.yml](https://github.com/HathorNetwork/hathor-wallet-service/blob/master/.codebuild/buildspec.yml). If it's not a secret, just add it under `env.variables`. + +If it's a secret, you'll need to add it to `env.secrets-manager`, and one for each environment we have (`dev`, `testnet` and `mainnet`). You should use the same name for it as you did in the `serverless.yml` file, but adding a prefix indicating the name of the environment. The value should be the path to a key in AWS Secrets Manager. Ask some account admin for help on adding the secrets there and providing you with the key path. + +## Creating a new DB migration + +To create a new DB migration, run: + +```bash +make new-migration NAME=migration_name +``` + +It will create an empty migration file for you. You should include your migration logic there. + +To run your migration: + +```bash +make migrate +``` + +The migrations will run in the database specified in your local environment configuration. If you need to configure it for a local database, check [this](https://github.com/HathorNetwork/hathor-wallet-service/blob/dev/README.md#local-database). + +## Enabling debug logs + +The logger is set on the INFO level by default. + +To enable more verbose debug logs, we need to change the `LOG_LEVEL` environment variable. This can be done by either changing the default deploy variable on `$PROJECT_DIR/.codebuild/buildspec.yml` and triggering a new deploy by following the steps on the **Deploying** section or by manually setting it on the AWS Lambda configuration tab for the Lamdba you desire to change the log level, valid severity values are `error`, `warn`, `info`, `verbose`, `debug` and `silly` + +Changing the environment will cause the lambda to be restarted, so the next request will already be logged + +## Enabling Maintenance Mode +TODO - This is not implemented yet diff --git a/packages/wallet-service/docs/images/feature-toggle-img1.jpg b/packages/wallet-service/docs/images/feature-toggle-img1.jpg new file mode 100644 index 00000000..bdfc5394 Binary files /dev/null and b/packages/wallet-service/docs/images/feature-toggle-img1.jpg differ diff --git a/packages/wallet-service/docs/images/feature-toggle-img2.jpg b/packages/wallet-service/docs/images/feature-toggle-img2.jpg new file mode 100644 index 00000000..b90ce026 Binary files /dev/null and b/packages/wallet-service/docs/images/feature-toggle-img2.jpg differ diff --git a/packages/wallet-service/docs/images/feature-toggle-img3.jpg b/packages/wallet-service/docs/images/feature-toggle-img3.jpg new file mode 100644 index 00000000..1fddc746 Binary files /dev/null and b/packages/wallet-service/docs/images/feature-toggle-img3.jpg differ diff --git a/packages/wallet-service/docs/images/feature-toggle-img4.jpg b/packages/wallet-service/docs/images/feature-toggle-img4.jpg new file mode 100644 index 00000000..0c28fbf1 Binary files /dev/null and b/packages/wallet-service/docs/images/feature-toggle-img4.jpg differ diff --git a/packages/wallet-service/docs/images/feature-toggle-img5.jpg b/packages/wallet-service/docs/images/feature-toggle-img5.jpg new file mode 100644 index 00000000..c193cc91 Binary files /dev/null and b/packages/wallet-service/docs/images/feature-toggle-img5.jpg differ diff --git a/packages/wallet-service/docs/images/feature-toggle-img6.jpg b/packages/wallet-service/docs/images/feature-toggle-img6.jpg new file mode 100644 index 00000000..37a6c8bb Binary files /dev/null and b/packages/wallet-service/docs/images/feature-toggle-img6.jpg differ diff --git a/packages/wallet-service/docs/images/feature-toggle-img7.jpg b/packages/wallet-service/docs/images/feature-toggle-img7.jpg new file mode 100644 index 00000000..ebf0c98a Binary files /dev/null and b/packages/wallet-service/docs/images/feature-toggle-img7.jpg differ diff --git a/packages/wallet-service/events/eventTemplate.json b/packages/wallet-service/events/eventTemplate.json new file mode 100644 index 00000000..117735a1 --- /dev/null +++ b/packages/wallet-service/events/eventTemplate.json @@ -0,0 +1,52 @@ +{ + "Records": [ + { + "messageId": "cb663b7e-a937-4bae-94ec-a64d7b4b6630", + "receiptHandle": "AQEBMVp7ef/+ED+qWMAFpQbzKKPqTUq2bvoLwjBa/3Hm/RjfguJPHM10D9H7gIHNT8Pcv3F8H+dLEDu9Dvy8RpS9aKPq4hKb/rWZPEQtCeyc7Caosf1oCS+AkkDxI+kEqPoZ+KbyIGHumzSSgGMcSyeM++tKZHgXmo5gwktKQApr7l9fFjXIZS50puu8hXPwO5gYxG2zktFWtgSXScYoVIJ8u56wdipE3jMp6B7a5CfMIxuT1zK6ht9OkaLD3RWcO+Lnub762sItXExQNH33ZBzEfg==", + "body": { + "tx_id": null, + "nonce": "171169", + "timestamp": 1590503218, + "version": 0, + "weight": 21, + "is_voided": false, + "parents": [ + "000006cb93385b8b87a545a1cbb6197e6caff600c12cc12fc54250d39c8088fc", + "0002d4d2a15def7604688e1878ab681142a7b155cbe52a6b4e031250ae96db0a", + "0002ad8d1519daaddc8e1a37b14aac0b045129c01832281fb1c02d873c7abbf9" + ], + "inputs": [], + "outputs": [], + "height": 1 + }, + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1589930991158", + "SequenceNumber": "18853766407446000896", + "MessageGroupId": "message_group_id", + "SenderId": "AIDAJCXZLXEJBPGI37LX6", + "MessageDeduplicationId": "3fb3ff522533675f75ce8209c4c3be614a1db1673dd8554f230d4266aeeeeb0b", + "ApproximateFirstReceiveTimestamp": "1589930991158" + }, + "messageAttributes": { + "attr1_name": { + "stringValue": "attr1_value", + "stringListValues": [], + "binaryListValues": [], + "dataType": "String" + }, + "attr2_name": { + "stringValue": "attr2_value", + "stringListValues": [], + "binaryListValues": [], + "dataType": "String" + } + }, + "md5OfMessageAttributes": "8eed7b18becba5578f2b3f7ab2057c4c", + "md5OfBody": "ae2f9f1e1aeee5fa83f3dcbcf0b08843", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-2:769498303037:YanTest.fifo", + "awsRegion": "us-east-2" + } + ] +} diff --git a/packages/wallet-service/events/nftCreationTx.ts b/packages/wallet-service/events/nftCreationTx.ts new file mode 100644 index 00000000..91c34833 --- /dev/null +++ b/packages/wallet-service/events/nftCreationTx.ts @@ -0,0 +1,180 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* + * This file contains helper data and methods for the tests + */ + +/* eslint-disable @typescript-eslint/no-empty-function */ + +import { Context } from 'aws-lambda'; +import { Transaction } from '@src/types'; + +/** + * A sample transaction for a NFT creation, as obtained by a wallet's history methods + */ +export const nftCreationTx = { + tx_id: '0025a6488045d7466639ead179a7f6beb188320f41cdb6df3a971db2ee86dbc3', + signal_bits: 0, + version: 2, + weight: 8.000001, + timestamp: 1656543561, + is_voided: false, + inputs: [ + { + value: 100, + token_data: 0, + script: 'dqkUaf+xVJ8uAPML/AzwuSB+2W9/M7qIrA==', + decoded: { + type: 'P2PKH', + address: 'WYLW8ujPemSuLJwbeNvvH6y7nakaJ6cEwT', + timelock: null, + }, + token: '00', + tx_id: '00d749e2ca22edcb231696caaf9df77f489058bd20b6dd26237be24ec918153a', + index: 1, + }, + ], + outputs: [ + { + value: 1, + token_data: 0, + // Decoded script: 5ipfs://QmPCSXNDyPdhU9oQFpxFsNN3nTjg9ZoqESKY5n9Gp1XSJc + script: 'NWlwZnM6Ly9RbVBDU1hORHlQZGhVOW9RRnB4RnNOTjNuVGpnOVpvcUVTS1k1bjlHcDFYU0pjrA==', + decoded: {}, + token: '00', + spent_by: null, + selected_as_input: false, + }, + { + value: 98, + token_data: 0, + script: 'dqkUQcQx/3rV1s5VZXqZPc1dkQbPo6eIrA==', + decoded: { + type: 'P2PKH', + address: 'WUfmqHWQZWn7aodAFadwmSDfh2QaUUgCRJ', + timelock: null, + }, + token: '00', + spent_by: null, + }, + { + value: 1, + token_data: 1, + script: 'dqkUQcQx/3rV1s5VZXqZPc1dkQbPo6eIrA==', + decoded: { + type: 'P2PKH', + address: 'WUfmqHWQZWn7aodAFadwmSDfh2QaUUgCRJ', + timelock: null, + }, + token: '0025a6488045d7466639ead179a7f6beb188320f41cdb6df3a971db2ee86dbc3', + spent_by: null, + }, + { + value: 1, + token_data: 129, + script: 'dqkU1YP+t130UoYD+3ys9MYt1zkWeY6IrA==', + decoded: { + type: 'P2PKH', + address: 'Wi8zvxdXHjaUVAoCJf52t3WovTZYcU9aX6', + timelock: null, + }, + token: '0025a6488045d7466639ead179a7f6beb188320f41cdb6df3a971db2ee86dbc3', + spent_by: null, + }, + { + value: 2, + token_data: 129, + script: 'dqkULlcsARvA+pQS8qytBr6Ryjc/SLeIrA==', + decoded: { + type: 'P2PKH', + address: 'WSu4PZVu6cvi3aejtG8w7bomVmg77DtqYt', + timelock: null, + }, + token: '0025a6488045d7466639ead179a7f6beb188320f41cdb6df3a971db2ee86dbc3', + spent_by: null, + }, + ], + parents: [ + '00d749e2ca22edcb231696caaf9df77f489058bd20b6dd26237be24ec918153a', + '004829631be87e5835ff7ec3112f1ab28b59fd96b27c67395e3901555b26bd7e', + ], + token_name: 'New NFT', + token_symbol: 'NNFT', + tokens: [], + // Properties exclusive to the wallet-service, in comparison with the lib's sample tx + is_block: false, + type: 'network:new_tx_accepted', + throttled: false, +}; + +/** + * Gets a copy of the `nftCreationTx` in the Wallet Service's Transaction format. + */ +export function getTransaction(): Transaction { + const result = { + tx_id: nftCreationTx.tx_id, + nonce: 1, + timestamp: nftCreationTx.timestamp, + signal_bits: nftCreationTx.signal_bits, + version: nftCreationTx.version, + weight: nftCreationTx.weight, + parents: nftCreationTx.parents, + inputs: nftCreationTx.inputs.map((i) => ({ + tx_id: i.tx_id, + index: i.index, + value: i.value, + token_data: i.token_data, + script: i.script, + token: i.token, + decoded: { + type: i.decoded.type, + address: i.decoded.address, + timelock: i.decoded.timelock, + }, + })), + outputs: nftCreationTx.outputs.map((o) => ({ + value: o.value, + script: o.script, + token: o.token, + decoded: { + type: o.decoded.type, + address: o.decoded.address, + timelock: o.decoded.timelock, + }, + spent_by: o.spent_by, + token_data: o.token_data, + locked: false, + })), + height: 8, + token_name: nftCreationTx.token_name, + token_symbol: nftCreationTx.token_symbol, + }; + return result; +} + +/** + * Creates a Handler Context object, for use on tests invoking lambdas + */ +export function getHandlerContext(): Context { + return { + awsRequestId: '', + callbackWaitsForEmptyEventLoop: false, + functionName: '', + functionVersion: '', + invokedFunctionArn: '', + logGroupName: '', + logStreamName: '', + memoryLimitInMB: '', + done(): void {}, + fail(): void {}, + getRemainingTimeInMillis(): number { + return 0; + }, + succeed(): void {}, + }; +} diff --git a/packages/wallet-service/events/tokenCreationTx.json b/packages/wallet-service/events/tokenCreationTx.json new file mode 100644 index 00000000..17fa9952 --- /dev/null +++ b/packages/wallet-service/events/tokenCreationTx.json @@ -0,0 +1,82 @@ +{ + "tx_id": "000002cda72c0f95d11d9a270e3626a17e314c252cc270a39dae383027b7cee8", + "version": 2, + "weight": 21.27218479542122, + "timestamp": 1595939257, + "is_voided": false, + "inputs": [ + { + "value": 6400, + "token_data": 0, + "script": "dqkUCEboPJo9txn548FA/NLLaMLsfsSIrA==", + "decoded": { + "type": "P2PKH", + "address": "HLUjnbbgxzgDTLAU7TjsTHzuZpeYY2xezw", + "timelock": null + }, + "token": "00", + "tx_id": "00fe3013b576408034be71db6be57eaf2a7d39cf2c4844ff37dd7946df33f4ba", + "index": 0 + } + ], + "outputs": [ + { + "value": 5400, + "token_data": 0, + "script": "dqkUmR4QdNEC6K8uibwyUMVCsEW3DRiIrA==", + "decoded": { + "type": "P2PKH", + "address": "HLUjnbbgxzgDTLAU7TjsTHzuZpeYY2xezw", + "timelock": null + }, + "token": "00", + "spent_by": null + }, + { + "value": 100000, + "token_data": 1, + "script": "dqkUWWag8rS/Sh0mtMg1F3zs3O9OlwOIrA==", + "decoded": { + "type": "P2PKH", + "address": "HEfqUBf4Rd4A35uhdtv7fuUtthGtjptYQC", + "timelock": null + }, + "token": "000002cda72c0f95d11d9a270e3626a17e314c252cc270a39dae383027b7cee8", + "spent_by": null + }, + { + "value": 1, + "token_data": 129, + "script": "dqkUNxdW36waXXpOuO6W4930yfy4mQOIrA==", + "decoded": { + "type": "P2PKH", + "address": "HBYRWYMpDQzkBPCdAJMix4dGNVi81CC855", + "timelock": null + }, + "token": "000002cda72c0f95d11d9a270e3626a17e314c252cc270a39dae383027b7cee8", + "spent_by": null + }, + { + "value": 2, + "token_data": 129, + "script": "dqkUg2KkQGyXZXkFexuY+9ygPjDZ4EaIrA==", + "decoded": { + "type": "P2PKH", + "address": "HJVq5DKPTeJ73UpuivJURdhfWnTLG7WAjo", + "timelock": null + }, + "token": "000002cda72c0f95d11d9a270e3626a17e314c252cc270a39dae383027b7cee8", + "spent_by": null + } + ], + "parents": [ + "000000f4bf297f59f4f0d8aaf6b9ebc12f70194c1398091a0aa92b1a8d08c68b", + "000004cb6ae89be0997c63ec29ac1e03bf0fc25e78c3c9d8a52e18dbb6d9f494" + ], + "is_block": false, + "token_name": "MyCoin", + "token_symbol": "MYC", + "tokens": [], + "type": "network:new_tx_accepted", + "throttled": false +} diff --git a/packages/wallet-service/jest.config.js b/packages/wallet-service/jest.config.js new file mode 100644 index 00000000..2e863961 --- /dev/null +++ b/packages/wallet-service/jest.config.js @@ -0,0 +1,23 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + moduleNameMapper: { + '^@src/(.*)$': '/src/$1', + '^@tests/(.*)$': '/tests/$1', + '^@events/(.*)$': '/events/$1', + }, + setupFiles: ['./tests/jestSetup.ts'], + testPathIgnorePatterns: [ + '/tests/utils/pushnotification.utils.boundary.test.ts', + '/dist/', + ], + coveragePathIgnorePatterns: ['/node_modules/', '/tests/utils.ts'], + coverageThreshold: { + global: { + branches: 88, + functions: 91, + lines: 93, + statements: 93, + }, + }, +}; diff --git a/packages/wallet-service/package.json b/packages/wallet-service/package.json new file mode 100644 index 00000000..5a52cc95 --- /dev/null +++ b/packages/wallet-service/package.json @@ -0,0 +1,76 @@ +{ + "name": "wallet-service", + "description": "", + "scripts": { + "jest": "jest --runInBand --collectCoverage --detectOpenHandles --forceExit", + "lint": "eslint src/ tests/ --ext .js,.jsx,.ts,.tsx src tests", + "lint-fix": "eslint src/ tests/ --fix --ext .js,.jsx,.ts,.tsx src tests", + "check-types": "tsc --noemit --skipLibCheck", + "test": "jest" + }, + "author": "Hathor Labs", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-apigatewaymanagementapi": "^3.465.0", + "@aws-sdk/client-lambda": "^3.465.0", + "@aws-sdk/client-sqs": "^3.465.0", + "@hathor/healthcheck-lib": "^0.1.0", + "@hathor/wallet-lib": "^0.39.0", + "@middy/core": "^2.5.7", + "@middy/http-cors": "^2.5.7", + "@types/redis": "^2.8.28", + "aws-lambda": "^1.0.7", + "axios": "^0.21.1", + "bip32": "^3.0.1", + "bitcoinjs-lib": "^6.0.1", + "bitcoinjs-message": "^2.2.0", + "bitcore-mnemonic": "8.25.10", + "firebase-admin": "^11.3.0", + "joi": "^17.4.0", + "jsonwebtoken": "^8.5.1", + "lodash": "^4.17.21", + "mysql": "^2.18.1", + "mysql2": "^2.2.5", + "prom-client": "^13.2.0", + "redis": "^3.1.2", + "serverless-mysql": "^1.5.4", + "source-map-support": "^0.5.19", + "tiny-secp256k1": "^2.2.1", + "uuid": "^8.3.0", + "winston": "^3.7.2" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.95", + "@types/jest": "^27.0.24", + "@types/node": "^18.0.4", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^3.3.0", + "bitcore-lib": "8.25.10", + "dotenv": "^10.0.0", + "eslint": "^8.50.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-plugin-import": "^2.23.3", + "eslint-plugin-jest": "^23.13.2", + "eslint-plugin-module-resolver": "^0.16.0", + "fork-ts-checker-webpack-plugin": "^9.0.0", + "jest": "^29.7.0", + "npm-run-all": "^4.1.5", + "serverless": "^3.35.2", + "serverless-api-gateway-throttling": "^1.1.1", + "serverless-iam-roles-per-function": "^3.2.0", + "serverless-offline": "^13.1.2", + "serverless-plugin-aws-alerts": "^1.7.5", + "serverless-plugin-monorepo": "^0.11.0", + "serverless-plugin-warmup": "^8.2.1", + "serverless-prune-plugin": "^2.0.2", + "serverless-webpack": "^5.13.0", + "sqlite3": "^5.0.2", + "ts-jest": "^29.1.1", + "ts-loader": "^9.4.4", + "typescript": "^4.9.3", + "typescript-eslint": "0.0.1-alpha.0", + "webpack": "^5.88.2", + "webpack-node-externals": "^3.0.0" + } +} diff --git a/packages/wallet-service/serverless.yml b/packages/wallet-service/serverless.yml new file mode 100644 index 00000000..fdff80cf --- /dev/null +++ b/packages/wallet-service/serverless.yml @@ -0,0 +1,684 @@ +service: hathor-wallet-service +frameworkVersion: '3' + +useDotenv: true + +custom: + warmup: + walletWarmer: # Keeps the lambdas used by the wallets initialization warm + enabled: true + events: + - schedule: rate(5 minutes) + webpack: + webpackConfig: ./webpack.config.js + includeModules: true + prune: + automatic: true + number: 3 + authorizer: + walletBearer: + name: bearerAuthorizer + type: TOKEN + identitySource: method.request.header.Authorization + identityValidationExpression: Bearer (.*) + # Configures throttling settings for the API Gateway stage + # They apply to all http endpoints, unless specifically overridden + apiGatewayThrottling: + maxRequestsPerSecond: 500 + maxConcurrentRequests: 250 + stage: ${opt:stage, 'dev'} + explorerServiceStage: ${env:EXPLORER_STAGE, 'dev'} + alerts: + stages: # Select which stages to deploy alarms to + - mainnet + - mainnet-stg + - testnet + topics: # SNS Topics to send alerts to + major: + alarm: + topic: arn:aws:sns:eu-central-1:${self:provider.environment.ACCOUNT_ID}:opsgenie-cloudwatch-integration-production-major + minor: + alarm: + topic: arn:aws:sns:eu-central-1:${self:provider.environment.ACCOUNT_ID}:opsgenie-cloudwatch-integration-production-minor + definitions: # Definition of alarms + majorFunctionErrors: + description: "Too many errors in hathor-wallet-service. Runbook: https://github.com/HathorNetwork/ops-tools/blob/master/docs/runbooks/wallet-service/errors-in-logs.md" + namespace: 'AWS/Lambda' + metric: Errors + threshold: 5 + statistic: Sum + period: 60 + evaluationPeriods: 5 + comparisonOperator: GreaterThanOrEqualToThreshold + treatMissingData: notBreaching + alarmActions: + - major + minorFunctionErrors: + description: "Too many errors in hathor-wallet-service. Runbook: https://github.com/HathorNetwork/ops-tools/blob/master/docs/runbooks/wallet-service/errors-in-logs.md" + namespace: 'AWS/Lambda' + metric: Errors + threshold: 2 + statistic: Sum + period: 60 + evaluationPeriods: 1 + comparisonOperator: GreaterThanOrEqualToThreshold + treatMissingData: notBreaching + alarmActions: + - minor + wsLambdasExecutionDuration: + description: "WebSocket lambda took too long to execute" + namespace: 'AWS/Lambda' + metric: "Duration" + statistic: Average + threshold: 1000 # This is in milliseconds + period: 60 + evaluationPeriods: 1 + comparisonOperator: GreaterThanThreshold + alarmActions: + - minor + cleanTxProposalsUtxosDuration: + description: "Clean tx proposals utxos cronjob taking too long" + namespace: 'AWS/Lambda' + metric: "Duration" + statistic: Average + threshold: 30000 # 30s + period: 60 # seconds + evaluationPeriods: 1 + comparisonOperator: GreaterThanThreshold + alarmActions: + - minor + alarms: # Alarms that will be applied to all functions + - majorFunctionErrors + - minorFunctionErrors + +plugins: + - serverless-offline + - serverless-plugin-monorepo + - serverless-webpack + - serverless-prune-plugin + - serverless-api-gateway-throttling + - serverless-plugin-warmup + - serverless-iam-roles-per-function + - serverless-plugin-aws-alerts + +resources: + Resources: + # This is needed to add CORS headers when the authorizer rejects an authorization request + # as we don't have control over the response. + # Taken from: https://www.serverless.com/blog/cors-api-gateway-survival-guide/ + GatewayResponseDefault4XX: + Type: 'AWS::ApiGateway::GatewayResponse' + Properties: + ResponseParameters: + gatewayresponse.header.Access-Control-Allow-Origin: "'*'" + gatewayresponse.header.Access-Control-Allow-Headers: "'*'" + ResponseType: DEFAULT_4XX + RestApiId: + Ref: 'ApiGatewayRestApi' + WalletServiceNewTxQueue: + Type: "AWS::SQS::Queue" + Properties: + QueueName: + WalletServiceNewTxQueue_${self:custom.stage} + +provider: + name: aws + runtime: nodejs18.x + # In MB. This is the memory allocated for the Lambdas, they cannot use more than this + # and will break if they try. + memorySize: 256 + # This is the default timeout. Each function can specify a different value + timeout: 6 + websocketsApiName: wallet-realtime-ws-api-${self:custom.stage} + websocketsApiRouteSelectionExpression: $request.body.action + iam: + role: + statements: + - Effect: Allow + Action: + - sqs:* + Resource: + - Fn::GetAtt: [ WalletServiceNewTxQueue, Arn ] + - arn:aws:sqs:${self:provider.environment.ALERT_MANAGER_REGION}:${self:provider.environment.ACCOUNT_ID}:${self:provider.environment.ALERT_MANAGER_TOPIC} + vpc: + securityGroupIds: + - ${env:AWS_VPC_DEFAULT_SG_ID} + subnetIds: + - ${env:AWS_SUBNET_ID_1} + - ${env:AWS_SUBNET_ID_2} + - ${env:AWS_SUBNET_ID_3} + stackTags: + Application: "hathor-wallet-service" + Stage: "${self:custom.stage}" + apiGateway: + minimumCompressionSize: 1024 # Enable gzip compression for responses > 1 KB + apiKeys: + - ${self:custom.stage}-healthcheck-api-key + environment: + ACCOUNT_ID: ${env:ACCOUNT_ID} + AUTH_SECRET: ${env:AUTH_SECRET} + AWS_VPC_DEFAULT_SG_ID: ${env:AWS_VPC_DEFAULT_SG_ID} + AWS_SUBNET_ID_1: ${env:AWS_SUBNET_ID_1} + AWS_SUBNET_ID_2: ${env:AWS_SUBNET_ID_2} + AWS_SUBNET_ID_3: ${env:AWS_SUBNET_ID_3} + AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1 + APPLICATION_NAME: ${env:APPLICATION_NAME} + BLOCK_REWARD_LOCK: ${env:BLOCK_REWARD_LOCK} + CONFIRM_FIRST_ADDRESS: ${env:CONFIRM_FIRST_ADDRESS} + DB_ENDPOINT: ${env:DB_ENDPOINT} + DB_PORT: ${env:DB_PORT} + DB_NAME: ${env:DB_NAME} + DB_USER: ${env:DB_USER} + DB_PASS: ${env:DB_PASS} + MAX_ADDRESS_GAP: ${env:MAX_ADDRESS_GAP} + NETWORK: ${env:NETWORK} + NEW_TX_SQS: { Ref: WalletServiceNewTxQueue } + REDIS_URL: ${env:REDIS_URL} + REDIS_PASSWORD: ${env:REDIS_PASSWORD} + SERVICE_NAME: ${self:service} + STAGE: ${self:custom.stage} + EXPLORER_SERVICE_STAGE: ${self:custom.explorerServiceStage} + NFT_AUTO_REVIEW_ENABLED: ${env:NFT_AUTO_REVIEW_ENABLED} + VOIDED_TX_OFFSET: ${env:VOIDED_TX_OFFSET} + DEFAULT_SERVER: ${env:DEFAULT_SERVER} + WS_DOMAIN: ${env:WS_DOMAIN} + TX_HISTORY_MAX_COUNT: ${env:TX_HISTORY_MAX_COUNT} + WALLET_SERVICE_LAMBDA_ENDPOINT: ${env:WALLET_SERVICE_LAMBDA_ENDPOINT} + PUSH_NOTIFICATION_ENABLED: ${env:PUSH_NOTIFICATION_ENABLED} + PUSH_ALLOWED_PROVIDERS: ${env:PUSH_ALLOWED_PROVIDERS} + FIREBASE_PROJECT_ID: ${env:FIREBASE_PROJECT_ID} + FIREBASE_PRIVATE_KEY_ID: ${env:FIREBASE_PRIVATE_KEY_ID} + FIREBASE_PRIVATE_KEY: ${env:FIREBASE_PRIVATE_KEY} + FIREBASE_CLIENT_EMAIL: ${env:FIREBASE_CLIENT_EMAIL} + FIREBASE_CLIENT_ID: ${env:FIREBASE_CLIENT_ID} + FIREBASE_AUTH_URI: ${env:FIREBASE_AUTH_URI} + FIREBASE_TOKEN_URI: ${env:FIREBASE_TOKEN_URI} + FIREBASE_AUTH_PROVIDER_X509_CERT_URL: ${env:FIREBASE_AUTH_PROVIDER_X509_CERT_URL} + FIREBASE_CLIENT_X509_CERT_URL: ${env:FIREBASE_CLIENT_X509_CERT_URL} + LOG_LEVEL: ${env:LOG_LEVEL} + ALERT_MANAGER_REGION: ${env:ALERT_MANAGER_REGION} + ALERT_MANAGER_TOPIC: ${env:ALERT_MANAGER_TOPIC} + +functions: + getLatestBlock: + handler: src/height.getLatestBlock + warmup: + walletWarmer: + enabled: false + onMinersListRequest: + handler: src/api/miners.onMinersListRequest + warmup: + walletWarmer: + enabled: false + onTotalSupplyRequest: + handler: src/api/totalSupply.onTotalSupplyRequest + timeout: 120 # 2 minutes + warmup: + walletWarmer: + enabled: false + onHandleReorgRequest: + handler: src/txProcessor.onHandleReorgRequest + timeout: 300 # 5 minutes + warmup: + walletWarmer: + enabled: false + onNewTxEvent: + handler: src/txProcessor.onNewTxEvent + warmup: + walletWarmer: + enabled: false + onNewNftEvent: + handler: src/txProcessor.onNewNftEvent + warmup: + walletWarmer: + enabled: false + iamRoleStatementsInherit: true + iamRoleStatements: + - Effect: Allow + Action: + - lambda:InvokeFunction + - lambda:InvokeAsync + Resource: + arn:aws:lambda:eu-central-1:${self:provider.environment.ACCOUNT_ID}:function:hathor-explorer-service-${self:custom.explorerServiceStage}-create_or_update_dag_metadata + loadWalletAsync: + handler: src/api/wallet.loadWallet + warmup: + walletWarmer: + enabled: false + loadWalletApi: + role: arn:aws:iam::${self:provider.environment.ACCOUNT_ID}:role/WalletServiceLoadWalletLambda + handler: src/api/wallet.load + events: + - http: + path: wallet/init + method: post + cors: true + warmup: + walletWarmer: + enabled: true + changeWalletAuthXpubApi: + handler: src/api/wallet.changeAuthXpub + events: + - http: + path: wallet/auth + method: put + cors: true + warmup: + walletWarmer: + enabled: false + getWalletStatusApi: + handler: src/api/wallet.get + events: + - http: + path: wallet/status + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: true + checkAddressMineApi: + handler: src/api/addresses.checkMine + events: + - http: + path: wallet/addresses/check_mine + method: post + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: false + getAddressesApi: + handler: src/api/addresses.get + events: + - http: + path: wallet/addresses + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + request: + parameters: + paths: + index: false + warmup: + walletWarmer: + enabled: true + getNewAddresses: + handler: src/api/newAddresses.get + events: + - http: + path: wallet/addresses/new + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: true + getUtxos: + handler: src/api/txOutputs.getFilteredUtxos + events: + - http: + path: wallet/utxos + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: false + getTxOutputs: + handler: src/api/txOutputs.getFilteredTxOutputs + events: + - http: + path: wallet/tx_outputs + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: false + getBalanceApi: + handler: src/api/balances.get + events: + - http: + path: wallet/balances + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: true + getTokensApi: + handler: src/api/tokens.get + events: + - http: + path: wallet/tokens + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: true + getTokenDetails: + handler: src/api/tokens.getTokenDetails + events: + - http: + path: wallet/tokens/{token_id}/details + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + request: + parameters: + paths: + token_id: true + warmup: + walletWarmer: + enabled: false + getVersionData: + handler: src/api/version.get + events: + - http: + path: version + method: get + cors: true + warmup: + walletWarmer: + enabled: true + getTxHistoryApi: + handler: src/api/txhistory.get + events: + - http: + path: wallet/history + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: true + createTxProposalApi: + handler: src/api/txProposalCreate.create + events: + - http: + path: tx/proposal + method: post + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: false + sendTxProposalApi: + handler: src/api/txProposalSend.send + events: + - http: + path: tx/proposal/{txProposalId} + method: put + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + request: + parameters: + paths: + txProposalId: true + warmup: + walletWarmer: + enabled: false + deleteTxProposalApi: + handler: src/api/txProposalDestroy.destroy + events: + - http: + path: tx/proposal/{txProposalId} + method: delete + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + request: + parameters: + paths: + txProposalId: true + warmup: + walletWarmer: + enabled: false + wsConnect: + handler: src/ws/connection.connect + timeout: 2 + events: + - websocket: + route: $connect + - websocket: + route: $disconnect + - websocket: + route: ping + warmup: + walletWarmer: + enabled: false + alarms: # This gets merged with the global alarms + - wsLambdasExecutionDuration + wsJoin: + handler: src/ws/join.handler + timeout: 2 + events: + - websocket: + route: join + warmup: + walletWarmer: + enabled: false + alarms: + - wsLambdasExecutionDuration + wsTxNotifyNew: + handler: src/ws/txNotify.onNewTx + timeout: 2 + events: + - sqs: + arn: + Fn::GetAtt: + - WalletServiceNewTxQueue + - Arn + batchSize: 1 # Will send every tx to the lambda istead of batching it, this should be tuned when we have more + # users using the wallet-service facade + maximumBatchingWindow: 0 # This is the default value, will wait 0 seconds before calling the lambda + warmup: + walletWarmer: + enabled: false + alarms: + - wsLambdasExecutionDuration + wsTxNotifyUpdate: + handler: src/ws/txNotify.onUpdateTx + timeout: 2 + warmup: + walletWarmer: + enabled: false + alarms: + - wsLambdasExecutionDuration + wsAdminBroadcast: + handler: src/ws/admin.broadcast + timeout: 2 + warmup: + walletWarmer: + enabled: false + alarms: + - wsLambdasExecutionDuration + wsAdminDisconnect: + handler: src/ws/admin.disconnect + timeout: 2 + warmup: + walletWarmer: + enabled: false + alarms: + - wsLambdasExecutionDuration + wsAdminMulticast: + handler: src/ws/admin.multicast + timeout: 2 + warmup: + walletWarmer: + enabled: false + alarms: + - wsLambdasExecutionDuration + authTokenApi: + handler: src/api/auth.tokenHandler + timeout: 6 + events: + - http: + path: auth/token + method: post + cors: true + warmup: + walletWarmer: + enabled: false + bearerAuthorizer: + handler: src/api/auth.bearerAuthorizer + warmup: + walletWarmer: + enabled: false + metrics: + handler: src/metrics.getMetrics + events: + - http: + path: metrics + method: get + throttling: + maxRequestsPerSecond: 2 + maxConcurrentRequests: 2 + warmup: + walletWarmer: + enabled: false + pushRegister: + handler: src/api/pushRegister.register + events: + - http: + path: wallet/push/register + method: post + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: false + pushUpdate: + handler: src/api/pushUpdate.update + events: + - http: + path: wallet/push/update + method: put + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: false + pushUnregister: + handler: src/api/pushUnregister.unregister + events: + - http: + path: wallet/push/unregister/{deviceId} + method: delete + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + request: + parameters: + paths: + deviceId: true + warmup: + walletWarmer: + enabled: false + getTxById: + handler: src/api/txById.get + events: + - http: + path: wallet/transactions/{txId} + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + request: + parameters: + paths: + txId: true + warmup: + walletWarmer: + enabled: false + proxiedGetTxById: + handler: src/api/fullnodeProxy.getTransactionById + events: + - http: + path: wallet/proxy/transactions/{txId} + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + request: + parameters: + paths: + txId: true + throttling: + maxRequestsPerSecond: 50 + warmup: + walletWarmer: + enabled: false + proxiedGetConfirmationData: + handler: src/api/fullnodeProxy.getConfirmationData + events: + - http: + path: wallet/proxy/transactions/{txId}/confirmation_data + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + request: + parameters: + paths: + txId: true + throttling: + maxRequestsPerSecond: 10 + warmup: + walletWarmer: + enabled: false + proxiedGraphvizNeighborsQuery: + handler: src/api/fullnodeProxy.queryGraphvizNeighbours + events: + - http: + path: wallet/proxy/graphviz/neighbours + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + throttling: + maxRequestsPerSecond: 20 + warmup: + walletWarmer: + enabled: false + sendNotificationToDevice: + handler: src/api/pushSendNotificationToDevice.send + warmup: + walletWarmer: + enabled: false + txPushRequested: + handler: src/api/txPushNotificationRequested.handleRequest + warmup: + walletWarmer: + enabled: false + iamRoleStatementsInherit: true + iamRoleStatements: + - Effect: Allow + Action: + - lambda:InvokeFunction + - lambda:InvokeAsync + Resource: + Fn::GetAtt: [ SendNotificationToDeviceLambdaFunction , Arn ] + healthcheck: + handler: src/api/healthcheck.getHealthcheck + events: + - http: + private: true + path: health + method: get + throttling: + maxRequestsPerSecond: 1 + maxConcurrentRequests: 1 + deleteStalePushDevices: + handler: src/db/cronRoutines.cleanStalePushDevices + events: + - schedule: cron(17 3 */15 * ? *) # run every 15 days at 3:17 (GMT) + warmup: + walletWarmer: + enabled: false + cleanUnsentTxProposalsUtxos: + handler: src/db/cronRoutines.cleanUnsentTxProposalsUtxos + timeout: 60 # 1 minute + events: + - schedule: cron(*/5 * * * ? *) # run every 5 minutes + warmup: + walletWarmer: + enabled: false + alarms: + - cleanTxProposalsUtxosDuration diff --git a/packages/wallet-service/src/api-docs.json b/packages/wallet-service/src/api-docs.json new file mode 100644 index 00000000..1872fce6 --- /dev/null +++ b/packages/wallet-service/src/api-docs.json @@ -0,0 +1,38 @@ +{ + openapi: "3.0.0", + servers: [ + { url: "http://localhost:8000" } + ], + info: { + title: "Wallet Service API", + description: "This is a service to manage wallet operations.", + version: "0.0.1", + }, + produces: [ "application/json" ], + components: { + }, + security: [ + ], + paths: { + "/wallet/addresses/new": { + get: { + summary: "Get new addresses of the wallet. The addresses after the last used one.", + responses: { + 200: { + description: "Success", + content: { + "application/json": { + examples: { + success: { + summary: "Addresses returned with success.", + value: {"success": true, "addresses": [{"address": "WYDN3wbR5nT1kgs9ak6WU4euEH4w5rdhPy", "index": 10, "addressPath": "m/44'/280'/0'/0/10"}, {"address": "WUaHZ2bC3p1BxQWe29Hw5nNfDU2W8F3j4R", "index": 11, "addressPath": "m/44'/280'/0'/0/11"}]} + }, + }, + }, + }, + }, + }, + }, + }, + }, +} \ No newline at end of file diff --git a/packages/wallet-service/src/api/addresses.ts b/packages/wallet-service/src/api/addresses.ts new file mode 100644 index 00000000..2edc0de6 --- /dev/null +++ b/packages/wallet-service/src/api/addresses.ts @@ -0,0 +1,172 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import 'source-map-support/register'; + +import Joi, { ValidationError } from 'joi'; +import { APIGatewayProxyHandler } from 'aws-lambda'; +import { ApiError } from '@src/api/errors'; +import { closeDbAndGetError, warmupMiddleware } from '@src/api/utils'; +import { + getWallet, + getWalletAddresses, + getAddressAtIndex as dbGetAddressAtIndex, +} from '@src/db'; +import { AddressInfo, AddressAtIndexRequest } from '@src/types'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { walletIdProxyHandler } from '@src/commons'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; + +const mysql = getDbConnection(); + +const checkMineBodySchema = Joi.object({ + addresses: Joi.array() + // Validate that addresses are a base58 string and exactly 34 in length + .items(Joi.string().regex(/^[A-HJ-NP-Za-km-z1-9]*$/).min(34).max(34)) + .min(1) + .max(512) // max number of addresses in a tx (256 outputs and 256 inputs) + .required(), +}); + +class AddressAtIndexValidator { + static readonly bodySchema = Joi.object({ + index: Joi.number().min(0).optional(), + }); + + static validate(payload: unknown): { value: AddressAtIndexRequest, error: ValidationError} { + return AddressAtIndexValidator.bodySchema.validate(payload, { + abortEarly: false, // We want it to return all the errors not only the first + convert: true, // We need to convert as parameters are sent on the QueryString + }) as { value: AddressAtIndexRequest, error: ValidationError }; + } +} + +/* + * Check if a list of addresses belong to the caller wallet + * + * This lambda is called by API Gateway on POST /addresses/check_mine + */ +export const checkMine: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (walletId, event) => { + const status = await getWallet(mysql, walletId); + + // If the wallet is not started or ready, we can skip the query on the address table + if (!status) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_FOUND); + } + + if (!status.readyAt) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_READY); + } + + const eventBody = (function parseBody(body) { + try { + return JSON.parse(body); + } catch (e) { + return null; + } + }(event.body)); + + const { value, error } = checkMineBodySchema.validate(eventBody, { + abortEarly: false, + convert: false, + }); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const sentAddresses = value.addresses; + const dbWalletAddresses: AddressInfo[] = await getWalletAddresses(mysql, walletId, sentAddresses); + const walletAddresses: Set = dbWalletAddresses.reduce((acc, { address }) => acc.add(address), new Set([])); + + await closeDbConnection(mysql); + + const addressBelongMap = sentAddresses.reduce((acc: {string: boolean}, address: string) => { + acc[address] = walletAddresses.has(address); + + return acc; + }, {}); + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + addresses: addressBelongMap, + }), + }; +})).use(cors()); + +/* + * Get the addresses of a wallet, allowing an index filter + * Notice: If the index filter is passed, it will only find addresses + * that are already in our database, this will not derive new addresses + * + * This lambda is called by API Gateway on GET /addresses + */ +export const get: APIGatewayProxyHandler = middy( + walletIdProxyHandler(async (walletId, event) => { + const status = await getWallet(mysql, walletId); + + if (!status) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_FOUND); + } + + if (!status.readyAt) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_READY); + } + + const { value: body, error } = AddressAtIndexValidator.validate(event.pathParameters || {}); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + let response = null; + + if ('index' in body) { + const address: AddressInfo | null = await dbGetAddressAtIndex(mysql, walletId, body.index); + + if (!address) { + return closeDbAndGetError(mysql, ApiError.ADDRESS_NOT_FOUND); + } + + response = { + statusCode: 200, + body: JSON.stringify({ + success: true, + addresses: [address], + }), + }; + } else { + // Searching for multiple addresses + const addresses = await getWalletAddresses(mysql, walletId); + response = { + statusCode: 200, + body: JSON.stringify({ + success: true, + addresses, + }), + }; + } + + await closeDbConnection(mysql); + + return response; + }), +).use(cors()) + .use(warmupMiddleware()); diff --git a/packages/wallet-service/src/api/auth.ts b/packages/wallet-service/src/api/auth.ts new file mode 100644 index 00000000..22c4463d --- /dev/null +++ b/packages/wallet-service/src/api/auth.ts @@ -0,0 +1,235 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + APIGatewayProxyHandler, + APIGatewayTokenAuthorizerHandler, + CustomAuthorizerResult, + PolicyDocument, + Statement, +} from 'aws-lambda'; +import { v4 as uuid4 } from 'uuid'; +import Joi from 'joi'; +import jwt from 'jsonwebtoken'; +import { ApiError } from '@src/api/errors'; +import { Wallet } from '@src/types'; +import { getWallet } from '@src/db'; +import { + verifySignature, + getAddressFromXpub, + closeDbConnection, + getDbConnection, + validateAuthTimestamp, + AUTH_MAX_TIMESTAMP_SHIFT_IN_SECONDS, +} from '@src/utils'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; +import createDefaultLogger from '@src/logger'; +import { Logger } from 'winston'; + +const EXPIRATION_TIME_IN_SECONDS = 1800; + +const bodySchema = Joi.object({ + ts: Joi.number().positive().required(), + xpub: Joi.string().required(), + sign: Joi.string().required(), + walletId: Joi.string().required(), +}); + +function parseBody(body) { + try { + return JSON.parse(body); + } catch (e) { + return null; + } +} + +const mysql = getDbConnection(); + +export const tokenHandler: APIGatewayProxyHandler = middy(async (event) => { + const eventBody = parseBody(event.body); + + const { value, error } = bodySchema.validate(eventBody, { + abortEarly: false, + convert: false, + }); + + if (error) { + await closeDbConnection(mysql); + + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return { + statusCode: 400, + body: JSON.stringify({ + success: false, + error: ApiError.INVALID_PAYLOAD, + details, + }), + }; + } + + const signature = value.sign; + const timestamp = value.ts; + const authXpubStr = value.xpub; + const wallet: Wallet = await getWallet(mysql, value.walletId); + + const [validTimestamp, timestampShift] = validateAuthTimestamp(timestamp, Date.now() / 1000); + + if (!validTimestamp) { + const details = [{ + message: `The timestamp is shifted ${timestampShift}(s). Limit is ${AUTH_MAX_TIMESTAMP_SHIFT_IN_SECONDS}(s).`, + }]; + + return { + statusCode: 400, + body: JSON.stringify({ + success: false, + error: ApiError.AUTH_INVALID_SIGNATURE, + details, + }), + }; + } + + if (wallet.authXpubkey !== authXpubStr) { + const details = [{ + message: 'Provided auth_xpubkey does not match the stored auth_xpubkey', + }]; + + return { + statusCode: 400, + body: JSON.stringify({ + success: false, + error: ApiError.INVALID_PAYLOAD, + details, + }), + }; + } + + const address = getAddressFromXpub(authXpubStr); + const walletId = wallet.walletId; + + if (!verifySignature(signature, timestamp, address, walletId)) { + await closeDbConnection(mysql); + + const details = { + message: `The signature ${signature} does not match with the auth xpubkey ${authXpubStr} and the timestamp ${timestamp}`, + }; + + return { + statusCode: 400, + body: JSON.stringify({ + success: false, + error: ApiError.AUTH_INVALID_SIGNATURE, + details, + }), + }; + } + + // To understand the other options to the sign method: https://github.com/auth0/node-jsonwebtoken#readme + const token = jwt.sign( + { + sign: signature, + ts: timestamp, + addr: address.toString(), + wid: walletId, + }, + process.env.AUTH_SECRET, + { + expiresIn: EXPIRATION_TIME_IN_SECONDS, + jwtid: uuid4(), + }, + ); + + return { + statusCode: 200, + body: JSON.stringify({ success: true, token }), + }; +}).use(cors()); + +/** + * Generates a aws policy document to allow/deny access to the resource + */ +const _generatePolicy = (principalId: string, effect: string, resource: string, logger: Logger) => { + const resourcePrefix = `${resource.split('/').slice(0, 2).join('/')}/*`; + const policyDocument: PolicyDocument = { + Version: '2012-10-17', + Statement: [], + }; + + const statementOne: Statement = { + Action: 'execute-api:Invoke', + Effect: effect, + Resource: [ + `${resourcePrefix}/wallet/*`, + `${resourcePrefix}/tx/*`, + ], + }; + + policyDocument.Statement[0] = statementOne; + + const authResponse: CustomAuthorizerResult = { + policyDocument, + principalId, + }; + + const context = { walletId: principalId }; + authResponse.context = context; + + // XXX: to get the resulting policy on the logs, since we can't check the cached policy + logger.info('Generated policy:', authResponse); + return authResponse; +}; + +export const bearerAuthorizer: APIGatewayTokenAuthorizerHandler = middy(async (event) => { + const logger = createDefaultLogger(); + const { authorizationToken } = event; + if (!authorizationToken) { + throw new Error('Unauthorized'); // returns a 401 + } + const sanitizedToken = authorizationToken.replace(/Bearer /gi, ''); + let data; + + try { + data = jwt.verify( + sanitizedToken, + process.env.AUTH_SECRET, + ); + } catch (e) { + // XXX: find a way to return specific error to frontend or make all errors Unauthorized? + // + // Identify exception from jsonwebtoken by the name property + // https://github.com/auth0/node-jsonwebtoken/blob/master/lib/TokenExpiredError.js#L5 + if (e.name === 'JsonWebTokenError') { + throw new Error('Unauthorized'); + } else if (e.name === 'TokenExpiredError') { + throw new Error('Unauthorized'); + } else { + logger.warn('Error on bearerAuthorizer: ', e); + throw e; + } + } + + // signature data + const signature = data.sign; + const timestamp = data.ts; + const address = data.addr; + const walletId = data.wid; + + // header data + const expirationTs = data.exp; + const verified = verifySignature(signature, timestamp, address, walletId); + + if (verified && Math.floor(Date.now() / 1000) <= expirationTs) { + return _generatePolicy(walletId, 'Allow', event.methodArn, logger); + } + + return _generatePolicy(walletId, 'Deny', event.methodArn, logger); +}).use(cors()); diff --git a/packages/wallet-service/src/api/balances.ts b/packages/wallet-service/src/api/balances.ts new file mode 100644 index 00000000..1be3f92f --- /dev/null +++ b/packages/wallet-service/src/api/balances.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import 'source-map-support/register'; + +import { APIGatewayProxyHandler } from 'aws-lambda'; +import { ApiError } from '@src/api/errors'; +import { closeDbAndGetError, warmupMiddleware } from '@src/api/utils'; +import { getWalletBalances, walletIdProxyHandler } from '@src/commons'; +import { + getWallet, +} from '@src/db'; +import { + closeDbConnection, + getDbConnection, + getUnixTimestamp, +} from '@src/utils'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; +import Joi from 'joi'; + +const mysql = getDbConnection(); + +const paramsSchema = Joi.object({ + token_id: Joi.string() + .alphanum() + .optional(), +}); + +/* + * Get the balances of a wallet + * + * This lambda is called by API Gateway on GET /balances + * + * XXX: If token_id is not sent as a filter, we return all token balances + * Maybe we should limit the amount of tokens to query the balance to prevent an user + * with a lot of different tokens in his wallet from doing an expensive query + */ +export const get: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (walletId, event) => { + const params = event.queryStringParameters || {}; + + const { value, error } = paramsSchema.validate(params, { + abortEarly: false, + convert: false, + }); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const status = await getWallet(mysql, walletId); + + if (!status) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_FOUND); + } + if (!status.readyAt) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_READY); + } + + const tokenIds: string[] = []; + if (value.token_id) { + const tokenId = value.token_id; + tokenIds.push(tokenId); + } + + const balances = await getWalletBalances(mysql, getUnixTimestamp(), walletId, tokenIds); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ success: true, balances }), + }; +})).use(cors()) + .use(warmupMiddleware()); diff --git a/packages/wallet-service/src/api/errors.ts b/packages/wallet-service/src/api/errors.ts new file mode 100644 index 00000000..d1d3dc72 --- /dev/null +++ b/packages/wallet-service/src/api/errors.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export enum ApiError { + MISSING_PARAMETER = 'missing-parameter', + INVALID_BODY = 'invalid-body', + INVALID_TX_WEIGHT = 'invalid-tx-weight', + INVALID_SELECTION_ALGORITHM = 'invalid-selection-algorithm', + UNKNOWN_ERROR = 'unknown-error', + INPUTS_NOT_FOUND = 'inputs-not-found', + INPUTS_ALREADY_USED = 'inputs-already-used', + INPUTS_NOT_IN_WALLET = 'inputs-not-in-wallet', + INSUFFICIENT_FUNDS = 'insufficient-funds', + INSUFFICIENT_INPUTS = 'insufficient-inputs', + INVALID_PARAMETER = 'invalid-parameter', + AUTH_INVALID_SIGNATURE = 'invalid-auth-signature', + INVALID_PAYLOAD = 'invalid-payload', + TOO_MANY_INPUTS = 'too-many-inputs', + TOO_MANY_OUTPUTS = 'too-many-outputs', + TX_PROPOSAL_NOT_FOUND = 'tx-proposal-not-found', + TX_PROPOSAL_NOT_OPEN = 'tx-proposal-not-open', + TX_PROPOSAL_SEND_ERROR = 'tx-proposal-send-error', + TX_PROPOSAL_NO_MATCH = 'tx-proposal-no-match', + WALLET_NOT_FOUND = 'wallet-not-found', + WALLET_NOT_READY = 'wallet-not-ready', + WALLET_ALREADY_LOADED = 'wallet-already-loaded', + WALLET_MAX_RETRIES = 'wallet-max-retries', + ADDRESS_NOT_IN_WALLET = 'address-not-in-wallet', + ADDRESS_NOT_FOUND = 'address-not-found', + TX_OUTPUT_NOT_IN_WALLET = 'tx-output-not-in-wallet', + TOKEN_NOT_FOUND = 'token-not-found', + FORBIDDEN = 'forbidden', + UNAUTHORIZED = 'unauthorized', + DEVICE_NOT_FOUND = 'device-not-found', + TX_NOT_FOUND = 'tx-not-found', +} diff --git a/packages/wallet-service/src/api/fullnodeProxy.ts b/packages/wallet-service/src/api/fullnodeProxy.ts new file mode 100644 index 00000000..907b667f --- /dev/null +++ b/packages/wallet-service/src/api/fullnodeProxy.ts @@ -0,0 +1,137 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import 'source-map-support/register'; + +import { APIGatewayProxyHandler } from 'aws-lambda'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; +import Joi from 'joi'; + +import { ApiError } from '@src/api/errors'; +import { closeDbAndGetError, validateParams } from '@src/api/utils'; +import { walletIdProxyHandler } from '@src/commons'; +import fullnode from '@src/fullnode'; +import { + closeDbConnection, + getDbConnection, +} from '@src/utils'; +import { + GraphvizParams, + GetConfirmationDataParams, + GetTxByIdParams, + ParamValidationResult, +} from '@src/types'; + +const mysql = getDbConnection(); + +const txIdValidator = Joi.object({ + txId: Joi.string() + .alphanum() + .required(), +}); + +const graphvizValidator = Joi.object({ + txId: Joi.string() + .alphanum() + .required(), + graphType: Joi.string() + .alphanum() + .required(), + maxLevel: Joi.number() + .required(), +}); + +/* + * Get a transaction from the fullnode + * + * This lambda is called by API Gateway on GET /wallet/proxy/transactions/:id + */ +export const getTransactionById: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (_walletId: string, event) => { + const params = event.pathParameters || {}; + const validationResult: ParamValidationResult = validateParams(txIdValidator, params); + + if (validationResult.error) { + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { + details: validationResult.details, + }); + } + + const { txId } = validationResult.value; + const transaction = await fullnode.downloadTx(txId); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify(transaction), + }; +})).use(cors()); + +/* + * Get confirmation data for a tx from the fullnode + * + * This lambda is called by API Gateway on GET /wallet/proxy/transactions/:id/confirmation_data + */ +export const getConfirmationData: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (_walletId: string, event) => { + const params = event.pathParameters || {}; + const validationResult: ParamValidationResult = validateParams(txIdValidator, params); + + if (validationResult.error) { + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { + details: validationResult.details, + }); + } + + const { txId } = validationResult.value; + const confirmationData = await fullnode.getConfirmationData(txId); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify(confirmationData), + }; +})).use(cors()); + +/* + * Makes graphviz queries on the fullnode + * + * This lambda is called by API Gateway on GET /wallet/proxy/graphviz/neighbours + */ +export const queryGraphvizNeighbours: APIGatewayProxyHandler = middy( + walletIdProxyHandler(async (_walletId: string, event) => { + const params = event.queryStringParameters || {}; + const validationResult: ParamValidationResult = validateParams(graphvizValidator, params, { + abortEarly: false, + // Since we receive params as queryString, + // we want Joi to convert maxLevel from string to number + convert: true, + }); + + if (validationResult.error) { + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { + details: validationResult.details, + }); + } + + const { + txId, + graphType, + maxLevel, + } = validationResult.value; + + const graphVizData = await fullnode.queryGraphvizNeighbours(txId, graphType, maxLevel); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify(graphVizData), + }; + }), +).use(cors()); diff --git a/packages/wallet-service/src/api/healthcheck.ts b/packages/wallet-service/src/api/healthcheck.ts new file mode 100644 index 00000000..ead57fad --- /dev/null +++ b/packages/wallet-service/src/api/healthcheck.ts @@ -0,0 +1,145 @@ +import middy from '@middy/core'; +import { + Healthcheck, + HealthcheckInternalComponent, + HealthcheckDatastoreComponent, + HealthcheckHTTPComponent, + HealthcheckCallbackResponse, + HealthcheckStatus, +} from '@hathor/healthcheck-lib'; +import { getLatestHeight } from '@src/db'; +import fullnode from '@src/fullnode'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { APIGatewayProxyHandler } from 'aws-lambda'; +import { getRedisClient, ping } from '@src/redis'; + +const mysql = getDbConnection(); + +const HEALTHCHECK_MAXIMUM_HEIGHT_DIFFERENCE = Number(process.env.HEALTHCHECK_MAXIMUM_HEIGHT_DIFFERENCE ?? 5) + +const checkDatabaseHeight: HealthcheckCallbackResponse = async () => { + try { + const [currentHeight, fullnodeStatus] = await Promise.all([ + getLatestHeight(mysql), + fullnode.getStatus() + ]); + + const currentFullnodeHeight = fullnodeStatus['dag']['best_block']['height']; + + if (currentFullnodeHeight - currentHeight < HEALTHCHECK_MAXIMUM_HEIGHT_DIFFERENCE) { + return new HealthcheckCallbackResponse({ + status: HealthcheckStatus.PASS, + output: `Database and fullnode heights are within ${HEALTHCHECK_MAXIMUM_HEIGHT_DIFFERENCE} blocks difference`, + }); + } else { + return new HealthcheckCallbackResponse({ + status: HealthcheckStatus.FAIL, + output: `Database height is ${currentHeight} but fullnode height is ${currentFullnodeHeight}`, + }); + } + } catch (e) { + console.error(e); + + return new HealthcheckCallbackResponse({ + status: HealthcheckStatus.FAIL, + output: `Error checking database and fullnode height: ${e.message}`, + }); + } +}; + +const checkRedisConnection: HealthcheckCallbackResponse = async () => { + const client = getRedisClient(); + try { + const pingResult = await ping(client); + + if (pingResult === 'PONG') { + return new HealthcheckCallbackResponse({ + status: HealthcheckStatus.PASS, + output: `Redis connection is up`, + }); + } else { + return new HealthcheckCallbackResponse({ + status: HealthcheckStatus.FAIL, + output: `Redis responded ping with invalid response: ${pingResult}`, + }); + } + } catch (e) { + console.error(e); + + return new HealthcheckCallbackResponse({ + status: HealthcheckStatus.FAIL, + output: `Error checking redis connection: ${e.message}`, + }); + } +}; + +const checkFullnodeHealth: HealthcheckCallbackResponse = async () => { + try { + const health = await fullnode.getHealth(); + + if (health['status'] === HealthcheckStatus.PASS) { + return new HealthcheckCallbackResponse({ + status: HealthcheckStatus.PASS, + output: `Fullnode is healthy`, + }); + } else if (health['status'] === HealthcheckStatus.WARN) { + return new HealthcheckCallbackResponse({ + status: HealthcheckStatus.WARN, + output: `Fullnode has health warnings: ${health}`, + }); + } else { + return new HealthcheckCallbackResponse({ + status: HealthcheckStatus.FAIL, + output: `Fullnode is unhealthy: ${JSON.stringify(health)}`, + }); + } + } catch (e) { + console.error(e); + + return new HealthcheckCallbackResponse({ + status: HealthcheckStatus.FAIL, + output: `Error checking fullnode health: ${e.message}`, + }); + } +}; + +const setupHealthcheck: Healthcheck = () => { + const healthcheck = new Healthcheck({ name: 'hathor-wallet-service', warnIsUnhealthy: true }); + + // Height healthcheck component + const heightHealthcheck = new HealthcheckInternalComponent({ + name: 'mysql:block_height', + }); + heightHealthcheck.add_healthcheck(checkDatabaseHeight); + + // Redis healthcheck component + const redisHealthcheck = new HealthcheckDatastoreComponent({ + name: 'redis:connection', + }); + redisHealthcheck.add_healthcheck(checkRedisConnection); + + // Fullnode healthcheck component + const fullnodeHealthcheck = new HealthcheckHTTPComponent({ + name: 'fullnode:health', + }); + fullnodeHealthcheck.add_healthcheck(checkFullnodeHealth); + + // Register components + healthcheck.add_component(heightHealthcheck); + healthcheck.add_component(redisHealthcheck); + healthcheck.add_component(fullnodeHealthcheck); + + return healthcheck; +}; + +export const getHealthcheck: APIGatewayProxyHandler = middy(async (event) => { + const healthcheck = setupHealthcheck(); + const response = await healthcheck.run(); + + await closeDbConnection(mysql); + + return { + statusCode: response.getHttpStatusCode(), + body: response.toJson(), + }; +}); \ No newline at end of file diff --git a/packages/wallet-service/src/api/miners.ts b/packages/wallet-service/src/api/miners.ts new file mode 100644 index 00000000..c80d4499 --- /dev/null +++ b/packages/wallet-service/src/api/miners.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import 'source-map-support/register'; + +import { APIGatewayProxyHandler } from 'aws-lambda'; +import { + getMinersList, +} from '@src/db'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { Miner } from '@src/types'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; + +const mysql = getDbConnection(); + +/* + * Gets a list of all miners on the database. We consider a miner an address + * that has received at least one mining transaction + * + * @remarks + * This is a lambda function that should be invoked using the aws-sdk. + */ +export const onMinersListRequest: APIGatewayProxyHandler = middy(async () => { + const minersList: Miner[] = await getMinersList(mysql); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + miners: minersList, + }), + }; +}).use(cors()); diff --git a/packages/wallet-service/src/api/newAddresses.ts b/packages/wallet-service/src/api/newAddresses.ts new file mode 100644 index 00000000..5514812f --- /dev/null +++ b/packages/wallet-service/src/api/newAddresses.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import 'source-map-support/register'; + +import { APIGatewayProxyHandler } from 'aws-lambda'; +import { ApiError } from '@src/api/errors'; +import { closeDbAndGetError, warmupMiddleware } from '@src/api/utils'; +import { + getWallet, + getNewAddresses, +} from '@src/db'; +import { closeDbConnection, getDbConnection } from '@src/utils'; + +import { walletIdProxyHandler } from '@src/commons'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; + +const mysql = getDbConnection(); + +/* + * Get the addresses of a wallet to be used in new transactions + * It returns the empty addresses after the last used one + * + * This lambda is called by API Gateway on GET /addresses/new + */ +export const get: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (walletId) => { + const status = await getWallet(mysql, walletId); + + if (!status) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_FOUND); + } + if (!status.readyAt) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_READY); + } + + const addresses = await getNewAddresses(mysql, walletId); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ success: true, addresses }), + }; +})).use(cors()) + .use(warmupMiddleware()); diff --git a/packages/wallet-service/src/api/pushRegister.ts b/packages/wallet-service/src/api/pushRegister.ts new file mode 100644 index 00000000..1e6bed77 --- /dev/null +++ b/packages/wallet-service/src/api/pushRegister.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { APIGatewayProxyHandler } from 'aws-lambda'; +import { ApiError } from '@src/api/errors'; +import { closeDbAndGetError, warmupMiddleware, pushProviderRegexPattern } from '@src/api/utils'; +import { removeAllPushDevicesByDeviceId, registerPushDevice, existsWallet } from '@src/db'; +import { getDbConnection } from '@src/utils'; +import { walletIdProxyHandler } from '@src/commons'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; +import Joi, { ValidationError } from 'joi'; +import { PushRegister } from '@src/types'; + +const mysql = getDbConnection(); + +class PushRegisterInputValidator { + static readonly bodySchema = Joi.object({ + pushProvider: Joi.string().pattern(pushProviderRegexPattern()).required(), + deviceId: Joi.string().max(256).required(), + enablePush: Joi.boolean().default(false).optional(), + enableShowAmounts: Joi.boolean().default(false).optional(), + }); + + static validate(payload: unknown): { value: PushRegister, error: ValidationError } { + return PushRegisterInputValidator.bodySchema.validate(payload, { + abortEarly: false, // We want it to return all the errors not only the first + convert: true, // We need to convert as parameters are sent on the QueryString + }) as { value: PushRegister, error: ValidationError }; + } +} + +/* + * Register a device to receive push notification. + * + * This lambda is called by API Gateway on POST /push/register + */ +export const register: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (walletId, event) => { + const eventBody = (function parseBody(body) { + try { + return JSON.parse(body); + } catch (e) { + return null; + } + }(event.body)); + + const { value: body, error } = PushRegisterInputValidator.validate(eventBody); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const walletExists = await existsWallet(mysql, walletId); + if (!walletExists) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_FOUND); + } + + await removeAllPushDevicesByDeviceId(mysql, body.deviceId); + + await registerPushDevice(mysql, { + walletId, + deviceId: body.deviceId, + pushProvider: body.pushProvider, + enablePush: body.enablePush, + enableShowAmounts: body.enableShowAmounts, + }); + + return { + statusCode: 200, + body: JSON.stringify({ success: true }), + }; +})) + .use(cors()) + .use(warmupMiddleware()); diff --git a/packages/wallet-service/src/api/pushSendNotificationToDevice.ts b/packages/wallet-service/src/api/pushSendNotificationToDevice.ts new file mode 100644 index 00000000..353f71e9 --- /dev/null +++ b/packages/wallet-service/src/api/pushSendNotificationToDevice.ts @@ -0,0 +1,103 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Handler } from 'aws-lambda'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import Joi, { ValidationError } from 'joi'; +import { Severity, SendNotificationToDevice } from '@src/types'; +import { getPushDevice, unregisterPushDevice } from '@src/db'; +import createDefaultLogger from '@src/logger'; +import { isPushProviderAllowed, PushNotificationUtils, PushNotificationError } from '@src/utils/pushnotification.utils'; +import { addAlert } from '@src/utils/alerting.utils'; + +const mysql = getDbConnection(); + +class PushSendNotificationToDeviceInputValidator { + static readonly bodySchema = Joi.object({ + deviceId: Joi.string().max(256).required(), + metadata: Joi.object({ + txId: Joi.string().required(), + titleLocKey: Joi.string().required(), + bodyLocKey: Joi.string().required(), + bodyLocArgs: Joi.string().optional(), + }).required(), + }).required(); + + static validate(payload: unknown): { value: SendNotificationToDevice, error: ValidationError } { + return PushSendNotificationToDeviceInputValidator.bodySchema.validate(payload, { + abortEarly: false, // We want it to return all the errors not only the first + convert: true, // We need to convert as parameters are sent on the QueryString + }) as { value: SendNotificationToDevice, error: ValidationError }; + } +} + +/* + * Send a notification to the registered device given a wallet. + * + * This lambda is called by API Gateway on POST /push/register + */ +export const send: Handler = async (event, context) => { + const logger = createDefaultLogger(); + // Logs the request id on every line, so we can see all logs from a request + logger.defaultMeta = { + requestId: context.awsRequestId, + }; + + const { value: body, error } = PushSendNotificationToDeviceInputValidator.validate(event); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + closeDbConnection(mysql); + logger.error('Invalid payload.', { details }); + return { success: false, message: 'Failed due to invalid payload, see details.', details }; + } + + const pushDevice = await getPushDevice(mysql, body.deviceId); + + if (!pushDevice) { + closeDbConnection(mysql); + await addAlert( + 'Device not found while trying to send notification', + '-', + Severity.MINOR, + { deviceId: body.deviceId }, + ); + logger.error('Device not found.', { + deviceId: body.deviceId, + }); + return { success: false, message: 'Failed due to device not found.' }; + } + + if (!isPushProviderAllowed(pushDevice.pushProvider)) { + closeDbConnection(mysql); + await addAlert( + 'Invalid provider error while sending push notification', + '-', + Severity.MINOR, + { deviceId: body.deviceId, pushProvider: pushDevice.pushProvider }, + ); + logger.error('Provider invalid.', { + deviceId: body.deviceId, + pushProvider: pushDevice.pushProvider, + }); + return { success: false, message: 'Failed due to invalid provider.' }; + } + + const result = await PushNotificationUtils.sendToFcm(body); + if (result.errorMessage === PushNotificationError.INVALID_DEVICE_ID) { + await unregisterPushDevice(mysql, body.deviceId); + return { success: false, message: 'Failed due to invalid device id.' }; + } + + return { + success: true, + }; +}; diff --git a/packages/wallet-service/src/api/pushUnregister.ts b/packages/wallet-service/src/api/pushUnregister.ts new file mode 100644 index 00000000..267bbd87 --- /dev/null +++ b/packages/wallet-service/src/api/pushUnregister.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { APIGatewayProxyHandler } from 'aws-lambda'; +import { ApiError } from '@src/api/errors'; +import { closeDbAndGetError } from '@src/api/utils'; +import { unregisterPushDevice } from '@src/db'; +import { getDbConnection } from '@src/utils'; +import { walletIdProxyHandler } from '@src/commons'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; +import Joi, { ValidationError } from 'joi'; +import { PushDelete } from '@src/types'; + +const mysql = getDbConnection(); + +class PushUpdateUnregisterValidator { + static readonly bodySchema = Joi.object({ + deviceId: Joi.string().max(256).required(), + }); + + static validate(payload: unknown): { value: PushDelete, error: ValidationError } { + return PushUpdateUnregisterValidator.bodySchema.validate(payload, { + abortEarly: false, // We want it to return all the errors not only the first + convert: true, // We need to convert as parameters are sent on the QueryString + }) as { value: PushDelete, error: ValidationError }; + } +} + +/* + * Unregister a device to receive push notification. + * + * This lambda is called by API Gateway on DELETE /push/unregister + */ +export const unregister: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (walletId, event) => { + const { value: body, error } = PushUpdateUnregisterValidator.validate(event.pathParameters); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + await unregisterPushDevice(mysql, body.deviceId, walletId); + + return { + statusCode: 200, + body: JSON.stringify({ success: true }), + }; +})) + .use(cors()); diff --git a/packages/wallet-service/src/api/pushUpdate.ts b/packages/wallet-service/src/api/pushUpdate.ts new file mode 100644 index 00000000..d37d165c --- /dev/null +++ b/packages/wallet-service/src/api/pushUpdate.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { APIGatewayProxyHandler } from 'aws-lambda'; +import { ApiError } from '@src/api/errors'; +import { closeDbAndGetError } from '@src/api/utils'; +import { existsPushDevice, updatePushDevice } from '@src/db'; +import { getDbConnection } from '@src/utils'; +import { walletIdProxyHandler } from '@src/commons'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; +import Joi, { ValidationError } from 'joi'; +import { PushUpdate } from '@src/types'; + +const mysql = getDbConnection(); + +class PushUpdateInputValidator { + static readonly bodySchema = Joi.object({ + deviceId: Joi.string().max(256).required(), + enablePush: Joi.boolean().default(false).optional(), + enableShowAmounts: Joi.boolean().default(false).optional(), + }); + + static validate(payload: unknown): { value: PushUpdate, error: ValidationError } { + return PushUpdateInputValidator.bodySchema.validate(payload, { + abortEarly: false, // We want it to return all the errors not only the first + convert: true, // We need to convert as parameters are sent on the QueryString + }) as { value: PushUpdate, error: ValidationError }; + } +} + +/* + * Update a device to receive push notification. + * + * This lambda is called by API Gateway on POST /push/register + */ +export const update: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (walletId, event) => { + const eventBody = (function parseBody(body) { + try { + return JSON.parse(body); + } catch (e) { + return null; + } + }(event.body)); + + const { value: body, error } = PushUpdateInputValidator.validate(eventBody); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const deviceExists = await existsPushDevice(mysql, body.deviceId, walletId); + if (!deviceExists) { + return closeDbAndGetError(mysql, ApiError.DEVICE_NOT_FOUND); + } + + await updatePushDevice(mysql, { + walletId, + deviceId: body.deviceId, + enablePush: body.enablePush, + enableShowAmounts: body.enableShowAmounts, + }); + + return { + statusCode: 200, + body: JSON.stringify({ success: true }), + }; +})) + .use(cors()); diff --git a/packages/wallet-service/src/api/tokens.ts b/packages/wallet-service/src/api/tokens.ts new file mode 100644 index 00000000..fa58e565 --- /dev/null +++ b/packages/wallet-service/src/api/tokens.ts @@ -0,0 +1,115 @@ +import 'source-map-support/register'; + +import { walletIdProxyHandler } from '@src/commons'; +import { + getWalletTokens, + getTotalSupply, + getTotalTransactions, + getTokenInformation, + getAuthorityUtxo, +} from '@src/db'; +import { + TokenInfo, +} from '@src/types'; +import { getDbConnection } from '@src/utils'; +import { ApiError } from '@src/api/errors'; +import { closeDbAndGetError, warmupMiddleware, txIdJoiValidator } from '@src/api/utils'; +import Joi from 'joi'; +import { constants } from '@hathor/wallet-lib'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; + +const mysql = getDbConnection(); + +/* + * List wallet tokens + * + * This lambda is called by API Gateway on GET /wallet/tokens + */ +export const get = middy(walletIdProxyHandler(async (walletId) => { + const walletTokens: string[] = await getWalletTokens(mysql, walletId); + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + tokens: walletTokens, + }), + }; +})).use(cors()) + .use(warmupMiddleware()); + +const getTokenDetailsParamsSchema = Joi.object({ + token_id: txIdJoiValidator.required(), +}); + +/* + * Get token details + * + * This lambda is called by API Gateway on GET /wallet/tokens/:token_id/details + */ +export const getTokenDetails = middy(walletIdProxyHandler(async (walletId, event) => { + const params = event.pathParameters || {}; + + const { value, error } = getTokenDetailsParamsSchema.validate(params, { + abortEarly: false, + convert: true, + }); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const tokenId = value.token_id; + const tokenInfo: TokenInfo = await getTokenInformation(mysql, tokenId); + + if (tokenId === constants.HATHOR_TOKEN_CONFIG.uid) { + const details = [{ + message: 'Invalid tokenId', + }]; + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + if (!tokenInfo) { + const details = [{ + message: 'Token not found', + }]; + + return closeDbAndGetError(mysql, ApiError.TOKEN_NOT_FOUND, { details }); + } + + const [ + totalSupply, + totalTransactions, + meltAuthority, + mintAuthority, + ] = await Promise.all([ + getTotalSupply(mysql, tokenId), + getTotalTransactions(mysql, tokenId), + getAuthorityUtxo(mysql, tokenId, constants.TOKEN_MELT_MASK), + getAuthorityUtxo(mysql, tokenId, constants.TOKEN_MINT_MASK), + ]); + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + details: { + tokenInfo, + totalSupply, + totalTransactions, + authorities: { + mint: mintAuthority !== null, + melt: meltAuthority !== null, + }, + }, + }), + }; +})).use(cors()) + .use(warmupMiddleware()); diff --git a/packages/wallet-service/src/api/totalSupply.ts b/packages/wallet-service/src/api/totalSupply.ts new file mode 100644 index 00000000..283aed53 --- /dev/null +++ b/packages/wallet-service/src/api/totalSupply.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import 'source-map-support/register'; + +import { APIGatewayProxyHandler } from 'aws-lambda'; +import { ApiError } from '@src/api/errors'; +import { + getTotalSupply, +} from '@src/db'; +import { closeDbAndGetError } from '@src/api/utils'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; +import hathorLib from '@hathor/wallet-lib'; +import Joi from 'joi'; + +const htrToken = hathorLib.constants.HATHOR_TOKEN_CONFIG.uid; +const mysql = getDbConnection(); +const paramsSchema = Joi.object({ + tokenId: Joi.string() + .alphanum() + .default(htrToken) + .optional(), +}); + +/* + * Gets the calculated sum of utxos on the database, excluding the burned ones + * + * @remarks + * This is a lambda function that should be invoked using the aws-sdk. + */ +export const onTotalSupplyRequest: APIGatewayProxyHandler = middy(async (event) => { + const { value, error } = paramsSchema.validate(event.body, { + abortEarly: false, + convert: true, + }); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const tokenId = value.tokenId; + const totalSupply: number = await getTotalSupply(mysql, tokenId); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + totalSupply, + }), + }; +}).use(cors()); diff --git a/packages/wallet-service/src/api/txById.ts b/packages/wallet-service/src/api/txById.ts new file mode 100644 index 00000000..7e6f25af --- /dev/null +++ b/packages/wallet-service/src/api/txById.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { APIGatewayProxyHandler } from 'aws-lambda'; +import { ApiError } from '@src/api/errors'; +import { closeDbAndGetError, warmupMiddleware } from '@src/api/utils'; +import { getTransactionById } from '@src/db'; +import { getDbConnection } from '@src/utils'; +import { walletIdProxyHandler } from '@src/commons'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; +import Joi, { ValidationError } from 'joi'; +import { TxByIdRequest } from '@src/types'; + +const mysql = getDbConnection(); + +class TxByIdValidator { + static readonly bodySchema = Joi.object({ + txId: Joi.string().min(64).max(64).required(), + }); + + static validate(payload): { value: TxByIdRequest, error: ValidationError } { + return TxByIdValidator.bodySchema.validate(payload, { + abortEarly: false, // We want it to return all the errors not only the first + convert: true, // We need to convert as parameters are sent on the QueryString + }) as { value: TxByIdRequest, error: ValidationError }; + } +} + +/* + * Get a transaction by its ID. + * + * This lambda is called by API Gateway on GET /wallet/transactions/:txId + */ +export const get: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (walletId, event) => { + const { value: body, error } = TxByIdValidator.validate(event.pathParameters); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const txTokens = await getTransactionById(mysql, body.txId, walletId); + if (!txTokens.length) { + return closeDbAndGetError(mysql, ApiError.TX_NOT_FOUND); + } + + return { + statusCode: 200, + body: JSON.stringify({ success: true, txTokens }), + }; +})) + .use(cors()) + .use(warmupMiddleware()); diff --git a/packages/wallet-service/src/api/txOutputs.ts b/packages/wallet-service/src/api/txOutputs.ts new file mode 100644 index 00000000..50af388e --- /dev/null +++ b/packages/wallet-service/src/api/txOutputs.ts @@ -0,0 +1,226 @@ +import 'source-map-support/register'; +import Joi from 'joi'; + +import { walletIdProxyHandler } from '@src/commons'; +import { ApiError } from '@src/api/errors'; +import { + filterTxOutputs, + getWalletAddresses, + getTxOutput, +} from '@src/db'; +import { + DbTxOutput, + DbTxOutputWithPath, + IFilterTxOutput, + AddressInfo, +} from '@src/types'; +import { closeDbAndGetError } from '@src/api/utils'; +import { getDbConnection } from '@src/utils'; +import { constants } from '@hathor/wallet-lib'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; + +const mysql = getDbConnection(); + +const bodySchema = Joi.object({ + id: Joi.string().optional(), + addresses: Joi.array() + .items(Joi.string().alphanum()) + .min(1) + .optional(), + tokenId: Joi.string().default('00'), + authority: Joi.number().default(0).integer().positive(), + ignoreLocked: Joi.boolean().optional(), + biggerThan: Joi.number().integer().positive().default(-1), + smallerThan: Joi.number().integer().positive().default(constants.MAX_OUTPUT_VALUE + 1), + maxOutputs: Joi.number().integer().positive().default(constants.MAX_OUTPUTS), + skipSpent: Joi.boolean().optional().default(true), + txId: Joi.string().optional(), + index: Joi.number().optional().min(0), +}).and('txId', 'index'); + +/* + * Filter utxos + * + * This lambda is called by API Gateway on GET /wallet/utxos + * + * NOTICE: This method will be deprecated in the future, we are only keeping it because our deployed mobile wallet + * uses it. As soon as it is updated and we are sure that no users are using that old version, we should remove this + * API + */ +export const getFilteredUtxos = middy(walletIdProxyHandler(async (walletId, event) => { + const multiQueryString = event.multiValueQueryStringParameters || {}; + const queryString = event.queryStringParameters || {}; + + const eventBody = { + id: queryString.id, + addresses: multiQueryString.addresses, + tokenId: queryString.tokenId, + authority: queryString.authority, + ignoreLocked: queryString.ignoreLocked, + biggerThan: queryString.biggerThan, + smallerThan: queryString.smallerThan, + skipSpent: true, // utxo is always unspent + txId: queryString.txId, + index: queryString.index, + }; + + const { value, error } = bodySchema.validate(eventBody, { + abortEarly: false, // We want it to return all the errors not only the first + convert: true, // We need to convert as parameters are sent on the QueryString + }); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const response = await _getFilteredTxOutputs(walletId, value); + + // The /wallet/utxos API expects `utxos` on the response body, we should transform the + // response accordingly + if (response.statusCode === 200) { + const body = JSON.parse(response.body); + body.utxos = body.txOutputs; + delete body.txOutputs; + + response.body = JSON.stringify(body); + } + + return response; +})).use(cors()); + +/* + * Filter tx_outputs + * + * This lambda is called by API Gateway on GET /wallet/tx_outputs + */ +export const getFilteredTxOutputs = middy(walletIdProxyHandler(async (walletId, event) => { + const multiQueryString = event.multiValueQueryStringParameters || {}; + const queryString = event.queryStringParameters || {}; + + const eventBody = { + id: queryString.id, + addresses: multiQueryString.addresses, + tokenId: queryString.tokenId, + authority: queryString.authority, + ignoreLocked: queryString.ignoreLocked, + biggerThan: queryString.biggerThan, + smallerThan: queryString.smallerThan, + skipSpent: queryString.skipSpent, + txId: queryString.txId, + index: queryString.index, + }; + + const { value, error } = bodySchema.validate(eventBody, { + abortEarly: false, // We want it to return all the errors not only the first + convert: true, // We need to convert as parameters are sent on the QueryString + }); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + return _getFilteredTxOutputs(walletId, value); +})).use(cors()); + +const _getFilteredTxOutputs = async (walletId: string, filters: IFilterTxOutput) => { + const walletAddresses = await getWalletAddresses(mysql, walletId); + + // txId will only be on the body when the user is searching for specific tx outputs + if (filters.txId !== undefined) { + let txOutputList: DbTxOutputWithPath[] = []; + const txOutput: DbTxOutput = await getTxOutput(mysql, filters.txId, filters.index, filters.skipSpent); + + if (txOutput) { + // check if the utxo is a member of the user's wallet + const denied = validateAddresses(walletAddresses, [txOutput.address]); + + if (denied.length > 0) { + // the requested utxo does not belong to the user's wallet. + return closeDbAndGetError(mysql, ApiError.TX_OUTPUT_NOT_IN_WALLET); + } + + txOutputList = mapTxOutputsWithPath(walletAddresses, [txOutput]); + } + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + txOutputs: txOutputList, + }), + }; + } + + const newFilters = { + ...filters, + }; + + if (newFilters.addresses) { + const denied = validateAddresses(walletAddresses, newFilters.addresses); + + if (denied.length > 0) { + return closeDbAndGetError(mysql, ApiError.ADDRESS_NOT_IN_WALLET, { missing: denied }); + } + } else { + newFilters.addresses = walletAddresses.map((addressInfo) => addressInfo.address); + } + + const txOutputs: DbTxOutput[] = await filterTxOutputs(mysql, newFilters); + const txOutputsWithPath: DbTxOutputWithPath[] = mapTxOutputsWithPath(walletAddresses, txOutputs); + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + txOutputs: txOutputsWithPath, + }), + }; +}; + +/** + * Returns a new list of utxos with the addressPaths for each tx_output + * + * @param walletAddress - A list of addresses for the user's wallet + * @param txOutputs - A list of txOutputs to map + * @returns A list with the mapped tx_outputs + */ +export const mapTxOutputsWithPath = (walletAddresses: AddressInfo[], txOutputs: DbTxOutput[]): DbTxOutputWithPath[] => txOutputs.map((txOutput) => { + const addressDetail: AddressInfo = walletAddresses.find((address) => address.address === txOutput.address); + if (!addressDetail) { + // this should never happen, so we will throw here + throw new Error('Tx output address not in user\'s wallet'); + } + const addressPath = `m/44'/${constants.HATHOR_BIP44_CODE}'/0'/0/${addressDetail.index}`; + return { ...txOutput, addressPath }; +}); + +/** + * Confirm that the requested addresses belongs to the user's wallet + * + * @param walletAddresses - The user wallet id + * @param addresses - List of addresses to validate + * @returns A list with the denied addresses, if any + */ +export const validateAddresses = (walletAddresses: AddressInfo[], addresses: string[]): string[] => { + const flatAddresses = walletAddresses.map((walletAddress) => walletAddress.address); + const denied: string[] = []; + + for (let i = 0; i < addresses.length; i++) { + if (!flatAddresses.includes(addresses[i])) { + denied.push(addresses[i]); + } + } + + return denied; +}; diff --git a/packages/wallet-service/src/api/txProposalCreate.ts b/packages/wallet-service/src/api/txProposalCreate.ts new file mode 100644 index 00000000..9bf67930 --- /dev/null +++ b/packages/wallet-service/src/api/txProposalCreate.ts @@ -0,0 +1,204 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import 'source-map-support/register'; +import { v4 as uuidv4 } from 'uuid'; +import Joi from 'joi'; + +import { ApiError } from '@src/api/errors'; +import { maybeRefreshWalletConstants, walletIdProxyHandler } from '@src/commons'; +import { + createTxProposal, + getUtxos, + getWallet, + getWalletAddresses, + getWalletAddressDetail, + markUtxosWithProposalId, +} from '@src/db'; +import { + AddressInfo, + IWalletInput, + DbTxOutput, +} from '@src/types'; +import { closeDbAndGetError } from '@src/api/utils'; +import { closeDbConnection, getDbConnection, getUnixTimestamp } from '@src/utils'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; +import hathorLib from '@hathor/wallet-lib'; + +const mysql = getDbConnection(); + +const bodySchema = Joi.object({ + txHex: Joi.string().alphanum(), +}); + +/* + * Create a tx-proposal. + * + * This lambda is called by API Gateway on POST /txproposals + */ +export const create = middy(walletIdProxyHandler(async (walletId, event) => { + await maybeRefreshWalletConstants(mysql); + + const eventBody = (function parseBody(body) { + try { + return JSON.parse(body); + } catch (e) { + return null; + } + }(event.body)); + + const { value, error } = bodySchema.validate(eventBody, { + abortEarly: false, // We want it to return all the errors not only the first + convert: false, // We want it to be strict with the parameters and not parse a string as integer + }); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const body = value; + const tx: hathorLib.Transaction = hathorLib.helpersUtils.createTxFromHex(body.txHex, new hathorLib.Network(process.env.NETWORK)); + + if (tx.outputs.length > hathorLib.transaction.getMaxOutputsConstant()) { + return closeDbAndGetError(mysql, ApiError.TOO_MANY_OUTPUTS, { outputs: tx.outputs.length }); + } + + const inputs: IWalletInput[] = tx.inputs.map((input) => ({ + txId: input.hash, + index: input.index, + })); + + const status = await getWallet(mysql, walletId); + + if (!status) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_FOUND); + } + + if (!status.readyAt) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_READY); + } + + const now = getUnixTimestamp(); + + // fetch the utxos that will be used + const inputUtxos: DbTxOutput[] = await getUtxos(mysql, inputs); + const missing = checkMissingUtxos(inputs, inputUtxos); + + if (missing.length > 0) { + return closeDbAndGetError(mysql, ApiError.INPUTS_NOT_FOUND, { missing }); + } + + // check if the inputs sent by the user belong to his wallet + const denied = await validateUtxoAddresses(walletId, inputUtxos); + + if (denied.length > 0) { + return closeDbAndGetError(mysql, ApiError.INPUTS_NOT_IN_WALLET, { missing }); + } + + // check if inputs sent by user are not part of another tx proposal + if (checkUsedUtxos(inputUtxos)) { + return closeDbAndGetError(mysql, ApiError.INPUTS_ALREADY_USED); + } + + if (inputUtxos.length > hathorLib.transaction.getMaxInputsConstant()) { + return closeDbAndGetError(mysql, ApiError.TOO_MANY_INPUTS, { inputs: inputUtxos.length }); + } + + // mark utxos with tx-proposal id + const txProposalId = uuidv4(); + markUtxosWithProposalId(mysql, txProposalId, inputUtxos); + + await createTxProposal(mysql, txProposalId, walletId, now); + + await closeDbConnection(mysql); + + const inputPromises = inputUtxos.map(async (utxo) => { + const addressDetail: AddressInfo = await getWalletAddressDetail(mysql, walletId, utxo.address); + // XXX We should store in address table the path of the address, not the index + // For now we return the hardcoded path with only the address index as variable + // The client will be prepared to receive any path when we add this in the service in the future + const addressPath = `m/44'/${hathorLib.constants.HATHOR_BIP44_CODE}'/0'/0/${addressDetail.index}`; + return { txId: utxo.txId, index: utxo.index, addressPath }; + }); + + const retInputs = await Promise.all(inputPromises); + + return { + statusCode: 201, + body: JSON.stringify({ + success: true, + txProposalId, + inputs: retInputs, + }), + }; +})).use(cors()); + +/** + * Confirm that all inputs requested by the user have been fetched. + * + * @param inputs - List of inputs sent by the user + * @param utxos - List of UTXOs retrieved from database + * @returns A list with the missing UTXOs, if any + */ +export const checkMissingUtxos = (inputs: IWalletInput[], utxos: DbTxOutput[]): IWalletInput[] => { + if (inputs.length === utxos.length) return []; + + const remaining = new Set(inputs.map((input) => `${input.txId}_${input.index}`)); + for (const utxo of utxos) { + remaining.delete(`${utxo.txId}_${utxo.index}`); + } + + const missing = []; + for (const utxo of remaining) { + missing.push({ txId: utxo[0], index: utxo[1] }); + } + return missing; +}; + +/** + * Confirm that the inputs requested by the user are not already being used on another TxProposal + * + * @param utxos - List of UTXOs retrieved from database + * @returns A list with the missing UTXOs, if any + */ +export const checkUsedUtxos = (utxos: DbTxOutput[]): boolean => { + for (let x = 0; x < utxos.length; x++) { + if (utxos[x].txProposalId) { + return true; + } + } + + return false; +}; + +/** + * Confirm that the requested utxos belongs to the user's wallet + * + * @param walletId - The user wallet id + * @param utxos - List of UTXOs to validate + * @returns A list with the denied UTXOs, if any + */ +export const validateUtxoAddresses = async (walletId: string, utxos: DbTxOutput[]): Promise => { + // fetch all addresses that belong to this wallet + const walletAddresses = await getWalletAddresses(mysql, walletId); + const flatAddresses = walletAddresses.map((walletAddress) => walletAddress.address); + const denied: DbTxOutput[] = []; + + for (let i = 0; i < utxos.length; i++) { + if (!flatAddresses.includes(utxos[i].address)) { + denied.push(utxos[i]); + } + } + + return denied; +}; diff --git a/packages/wallet-service/src/api/txProposalDestroy.ts b/packages/wallet-service/src/api/txProposalDestroy.ts new file mode 100644 index 00000000..6ef42edd --- /dev/null +++ b/packages/wallet-service/src/api/txProposalDestroy.ts @@ -0,0 +1,70 @@ +import { APIGatewayProxyHandler } from 'aws-lambda'; + +import 'source-map-support/register'; + +import { ApiError } from '@src/api/errors'; +import { + getTxProposal, + updateTxProposal, + releaseTxProposalUtxos, +} from '@src/db'; +import { walletIdProxyHandler } from '@src/commons'; +import { TxProposalStatus } from '@src/types'; +import { closeDbConnection, getDbConnection, getUnixTimestamp } from '@src/utils'; +import { closeDbAndGetError } from '@src/api/utils'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; + +const mysql = getDbConnection(); + +/* + * Destroy a txProposal. + * + * This lambda is called by API Gateway on DELETE /txproposals/{proposalId} + */ +export const destroy: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (walletId, event) => { + const params = event.pathParameters; + let txProposalId: string; + + if (params && params.txProposalId) { + txProposalId = params.txProposalId; + } else { + return closeDbAndGetError(mysql, ApiError.MISSING_PARAMETER, { parameter: 'txProposalId' }); + } + + const txProposal = await getTxProposal(mysql, txProposalId); + + if (txProposal === null) { + return closeDbAndGetError(mysql, ApiError.TX_PROPOSAL_NOT_FOUND); + } + + if (txProposal.walletId !== walletId) { + return closeDbAndGetError(mysql, ApiError.FORBIDDEN); + } + + if (txProposal.status !== TxProposalStatus.OPEN && txProposal.status !== TxProposalStatus.SEND_ERROR) { + return closeDbAndGetError(mysql, ApiError.TX_PROPOSAL_NOT_OPEN); + } + + const now = getUnixTimestamp(); + + await updateTxProposal( + mysql, + [txProposalId], + now, + TxProposalStatus.CANCELLED, + ); + + // Remove tx_proposal_id and tx_proposal_index from utxo table + await releaseTxProposalUtxos(mysql, [txProposalId]); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + txProposalId, + }), + }; +})).use(cors()); diff --git a/packages/wallet-service/src/api/txProposalSend.ts b/packages/wallet-service/src/api/txProposalSend.ts new file mode 100644 index 00000000..fcd42f38 --- /dev/null +++ b/packages/wallet-service/src/api/txProposalSend.ts @@ -0,0 +1,147 @@ +import { APIGatewayProxyHandler } from 'aws-lambda'; +import hathorLib from '@hathor/wallet-lib'; + +import Joi from 'joi'; + +import 'source-map-support/register'; + +import { ApiError } from '@src/api/errors'; +import { + getTxProposal, + getTxProposalInputs, + updateTxProposal, +} from '@src/db'; +import { + TxProposalStatus, + ApiResponse, +} from '@src/types'; +import { + closeDbConnection, + getDbConnection, + getUnixTimestamp, +} from '@src/utils'; + +import { + walletIdProxyHandler, +} from '@src/commons'; + +import { closeDbAndGetError } from '@src/api/utils'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; + +const mysql = getDbConnection(); + +const paramsSchema = Joi.object({ + txProposalId: Joi.string() + .guid({ + version: [ + 'uuidv4', + 'uuidv5', + ], + }) + .required(), +}); + +const bodySchema = Joi.object({ + txHex: Joi.string().alphanum(), +}); + +/* + * Send a transaction. + * + * This lambda is called by API Gateway on PUT /txproposals/{proposalId} + */ +export const send: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (walletId, event) => { + if (!event.pathParameters) { + return closeDbAndGetError(mysql, ApiError.MISSING_PARAMETER, { parameter: 'txProposalId' }); + } + + const { value, error } = paramsSchema.validate(event.pathParameters); + + if (error) { + // There is only one parameter on this API (txProposalId) and it is on path 0 + const parameter = error.details[0].path[0]; + + return closeDbAndGetError(mysql, ApiError.INVALID_PARAMETER, { parameter }); + } + + const { txProposalId } = value; + + const bodyValidation = bodySchema.validate(JSON.parse(event.body)); + + if (bodyValidation.error) { + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD); + } + + const { txHex } = bodyValidation.value; + const txProposal = await getTxProposal(mysql, txProposalId); + + if (txProposal === null) { + return closeDbAndGetError(mysql, ApiError.TX_PROPOSAL_NOT_FOUND); + } + + if (txProposal.walletId !== walletId) { + return closeDbAndGetError(mysql, ApiError.FORBIDDEN); + } + + // we can only send if it's still open or there was an error sending before + if (txProposal.status !== TxProposalStatus.OPEN && txProposal.status !== TxProposalStatus.SEND_ERROR) { + return closeDbAndGetError(mysql, ApiError.TX_PROPOSAL_NOT_OPEN, { status: txProposal.status }); + } + + const now = getUnixTimestamp(); + const txProposalInputs = await getTxProposalInputs(mysql, txProposalId); + const tx = hathorLib.helpersUtils.createTxFromHex(txHex, new hathorLib.Network(process.env.NETWORK)); + + if (tx.inputs.length !== txProposalInputs.length) { + return closeDbAndGetError(mysql, ApiError.TX_PROPOSAL_NO_MATCH); + } + + const txHexInputHashes = tx.inputs.map((input) => input.hash); + + for (let i = 0; i < txProposalInputs.length; i++) { + // Validate that the inputs on the txHex are the same as those sent on txProposalCreate + if (txHexInputHashes.indexOf(txProposalInputs[i].txId) < 0) { + return closeDbAndGetError(mysql, ApiError.TX_PROPOSAL_NO_MATCH); + } + } + + try { + const response: ApiResponse = await new Promise((resolve) => { + hathorLib.txApi.pushTx(txHex, false, resolve); + }); + + if (!response.success) throw new Error(response.message); + + await updateTxProposal( + mysql, + [txProposalId], + now, + TxProposalStatus.SENT, + ); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + txProposalId, + txHex, + }), + }; + } catch (e) { + await updateTxProposal( + mysql, + [txProposalId], + now, + TxProposalStatus.SEND_ERROR, + ); + + return closeDbAndGetError(mysql, ApiError.TX_PROPOSAL_SEND_ERROR, { + message: e.message, + txProposalId, + txHex, + }); + } +})).use(cors()); diff --git a/packages/wallet-service/src/api/txPushNotificationRequested.ts b/packages/wallet-service/src/api/txPushNotificationRequested.ts new file mode 100644 index 00000000..c4f9a03e --- /dev/null +++ b/packages/wallet-service/src/api/txPushNotificationRequested.ts @@ -0,0 +1,213 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Handler } from 'aws-lambda'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import Joi, { ValidationError } from 'joi'; +import { + Severity, + TokenBalanceValue, + LocalizeMetadataNotification, + SendNotificationToDevice, + StringMap, + WalletBalanceValue, +} from '@src/types'; +import { getPushDeviceSettingsList } from '@src/db'; +import createDefaultLogger from '@src/logger'; +import { PushNotificationUtils } from '@src/utils/pushnotification.utils'; +import { Logger } from 'winston'; +import { addAlert } from '@src/utils/alerting.utils'; + +const mysql = getDbConnection(); + +export const pushNotificationMessage = { + newTransaction: { + titleKey: 'new_transaction_received_title', + withoutTokens: { + descriptionKey: 'new_transaction_received_description_without_tokens', + }, + withTokens: { + descriptionKey: 'new_transaction_received_description_with_tokens', + }, + }, + invalidPayload: 'Failed due to invalid payload error. See details.', + deviceSettingsNotFound: 'Failed due to device settings not found.', +}; + +class TxPushNotificationRequestValidator { + static readonly authoritiesSchema = Joi.object({ + melt: Joi.boolean().required(), + mint: Joi.boolean().required(), + }); + + static readonly walletBalanceSchema = Joi.array().items( + Joi.object({ + tokenId: Joi.string().required(), + tokenSymbol: Joi.string().required(), + totalAmountSent: Joi.number().required(), + lockedAmount: Joi.number().required(), + unlockedAmount: Joi.number().required(), + lockedAuthorities: TxPushNotificationRequestValidator.authoritiesSchema, + unlockedAuthorities: TxPushNotificationRequestValidator.authoritiesSchema, + lockExpires: Joi.number().integer().min(0).valid(null), + total: Joi.number().required(), + }), + ).required(); + + static readonly bodySchema = Joi.object().pattern(Joi.string().required(), Joi.object({ + txId: Joi.string().max(256).required(), + walletId: Joi.string().required(), + addresses: Joi.array().items(Joi.string().required()).required(), + walletBalanceForTx: TxPushNotificationRequestValidator.walletBalanceSchema, + }).required()).required().min(1); + + static validate(payload: unknown): { value: StringMap, error: ValidationError } { + return TxPushNotificationRequestValidator.bodySchema.validate(payload, { + abortEarly: false, // We want it to return all the errors not only the first + convert: true, // We need to convert as parameters are sent on the QueryString + }) as { value: StringMap, error: ValidationError }; + } +} + +/* + * Handles a push notification request from post processing transaction. + * + * This lambda is called internally by an invoker. + */ +// eslint-disable-next-line max-len +export const handleRequest: Handler, { success: boolean, message?: string, details?: unknown }> = async (event, context) => { + const logger = createDefaultLogger(); + // Logs the request id on every line, so we can see all logs from a request + logger.defaultMeta = { + module: __filename, + scope: handleRequest.name, + requestId: context.awsRequestId, + }; + + const { value: body, error } = TxPushNotificationRequestValidator.validate(event); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + closeDbConnection(mysql); + await addAlert( + 'Invalid payload while handling push notification request.', + '-', + Severity.MINOR, + { details }, + ); + logger.error('Invalid payload while handling push notification request.', { details }); + return { success: false, message: pushNotificationMessage.invalidPayload, details }; + } + + const walletIdList = Object.keys(body); + const deviceSettings = await getPushDeviceSettingsList(mysql, walletIdList); + + const noDeviceSettingsFound = deviceSettings?.length === 0; + if (noDeviceSettingsFound) { + closeDbConnection(mysql); + return { success: false, message: pushNotificationMessage.deviceSettingsNotFound }; + } + + const devicesEnabledToPush = deviceSettings + .filter((eachSettings) => eachSettings.enablePush) + // filter wallets in which the token balance > 0 + .filter((eachSettings) => { + const wallet = body[eachSettings.walletId]; + // verify by the first token balance + return wallet.walletBalanceForTx[0].total > 0; + }); + + const genericMessages = devicesEnabledToPush + .filter((eachSettings) => !eachSettings.enableShowAmounts) + .map((eachSettings) => { + const wallet = body[eachSettings.walletId]; + return _assembleGenericMessage(eachSettings.deviceId, wallet.txId); + }); + + for (const eachNotification of genericMessages) { + await _sendNotification(eachNotification, logger); + } + + const specificMessages = devicesEnabledToPush + .filter((eachSettings) => eachSettings.enableShowAmounts) + .map((eachSettings) => { + const wallet = body[eachSettings.walletId]; + return _assembleSpecificMessage(eachSettings.deviceId, wallet.txId, wallet.walletBalanceForTx); + }); + + for (const eachNotification of specificMessages) { + await _sendNotification(eachNotification, logger); + } + + return { + success: true, + }; +}; + +const _assembleGenericMessage = (deviceId, txId): SendNotificationToDevice => { + const localize = { + titleLocKey: pushNotificationMessage.newTransaction.titleKey, + bodyLocKey: pushNotificationMessage.newTransaction.withoutTokens.descriptionKey, + } as LocalizeMetadataNotification; + + return { + deviceId, + metadata: { + txId, + ...localize, + }, + } as SendNotificationToDevice; +}; + +const _assembleSpecificMessage = (deviceId: string, txId: string, tokenBalanceList: TokenBalanceValue[]): SendNotificationToDevice => { + const upperLimit = 2; + const isTokensOverLimit = tokenBalanceList.length > upperLimit; + + const tokens = []; + for (const eachBalance of tokenBalanceList.slice(0, upperLimit)) { + const amount = eachBalance.total; + const tokenSymbol = eachBalance.tokenSymbol; + tokens.push(`${amount} ${tokenSymbol}`); + } + + if (isTokensOverLimit) { + const remainingTokens = tokenBalanceList.length - upperLimit; + tokens.push(remainingTokens.toString()); + } + + const localize = { + titleLocKey: pushNotificationMessage.newTransaction.titleKey, + bodyLocKey: pushNotificationMessage.newTransaction.withTokens.descriptionKey, + bodyLocArgs: JSON.stringify(tokens), + } as LocalizeMetadataNotification; + + return { + deviceId, + metadata: { + txId, + ...localize, + }, + } as SendNotificationToDevice; +}; + +const _sendNotification = async (notification: SendNotificationToDevice, logger: Logger): Promise => { + try { + await PushNotificationUtils.invokeSendNotificationHandlerLambda(notification); + } catch (error) { + await addAlert( + 'Unexpected failure while calling invokeSendNotificationHandlerLambda', + '-', + Severity.MINOR, + { ...notification }, + ); + logger.error('Unexpected failure while calling invokeSendNotificationHandlerLambda.', { ...notification, error }); + } +}; diff --git a/packages/wallet-service/src/api/txhistory.ts b/packages/wallet-service/src/api/txhistory.ts new file mode 100644 index 00000000..5540b051 --- /dev/null +++ b/packages/wallet-service/src/api/txhistory.ts @@ -0,0 +1,89 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import 'source-map-support/register'; +import hathorLib from '@hathor/wallet-lib'; + +import { ApiError } from '@src/api/errors'; +import { closeDbAndGetError, warmupMiddleware } from '@src/api/utils'; +import { + getWallet, + getWalletTxHistory, +} from '@src/db'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { walletIdProxyHandler } from '@src/commons'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; +import Joi from 'joi'; + +const MAX_COUNT = parseInt(process.env.TX_HISTORY_MAX_COUNT || '50', 10); +const htrToken = hathorLib.constants.HATHOR_TOKEN_CONFIG.uid; + +const paramsSchema = Joi.object({ + token_id: Joi.string() + .alphanum() + .default(htrToken) + .optional(), + skip: Joi.number() + .integer() + .min(0) + .default(0) + .optional(), + count: Joi.number() + .integer() + .positive() + .default(MAX_COUNT) + .max(MAX_COUNT) + .optional(), +}); + +const mysql = getDbConnection(); + +/* + * Get the tx-history of a wallet + * + * This lambda is called by API Gateway on GET /txhistory + */ +export const get = middy(walletIdProxyHandler(async (walletId, event) => { + const params = event.queryStringParameters || {}; + + const { value, error } = paramsSchema.validate(params, { + abortEarly: false, + convert: true, // Skip and count will come as query params as strings + }); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const tokenId = value.token_id; + const skip = value.skip; + const count = Math.min(MAX_COUNT, value.count); + const status = await getWallet(mysql, walletId); + + if (!status) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_FOUND); + } + if (!status.readyAt) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_READY); + } + + const history = await getWalletTxHistory(mysql, walletId, tokenId, skip, count); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ success: true, history, skip, count }), + }; +})).use(cors()) + .use(warmupMiddleware()); diff --git a/packages/wallet-service/src/api/utils.ts b/packages/wallet-service/src/api/utils.ts new file mode 100644 index 00000000..76e812a6 --- /dev/null +++ b/packages/wallet-service/src/api/utils.ts @@ -0,0 +1,139 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { APIGatewayProxyResult, APIGatewayProxyEvent } from 'aws-lambda'; +import { ServerlessMysql } from 'serverless-mysql'; +import middy from '@middy/core'; +import Joi, { + Schema, + ValidationOptions, + ValidationResult, +} from 'joi'; + +import { ApiError } from '@src/api/errors'; +import { + PushProvider, + StringMap, + ParamValidationResult, +} from '@src/types'; +import { closeDbConnection } from '@src/utils'; + +export const STATUS_CODE_TABLE = { + [ApiError.MISSING_PARAMETER]: 400, + [ApiError.INVALID_BODY]: 400, + [ApiError.INVALID_TX_WEIGHT]: 400, + [ApiError.INVALID_SELECTION_ALGORITHM]: 400, + [ApiError.UNKNOWN_ERROR]: 500, + [ApiError.INPUTS_NOT_FOUND]: 400, + [ApiError.INPUTS_ALREADY_USED]: 400, + [ApiError.INSUFFICIENT_FUNDS]: 400, + [ApiError.INSUFFICIENT_INPUTS]: 400, + [ApiError.INVALID_PARAMETER]: 400, + [ApiError.AUTH_INVALID_SIGNATURE]: 400, + [ApiError.INVALID_PAYLOAD]: 400, + [ApiError.TOO_MANY_INPUTS]: 400, + [ApiError.TOO_MANY_OUTPUTS]: 400, + [ApiError.TX_PROPOSAL_NOT_FOUND]: 404, + [ApiError.TX_PROPOSAL_NOT_OPEN]: 400, + [ApiError.TX_PROPOSAL_SEND_ERROR]: 400, + [ApiError.TX_PROPOSAL_NO_MATCH]: 400, + [ApiError.WALLET_NOT_FOUND]: 404, + [ApiError.WALLET_NOT_READY]: 400, + [ApiError.WALLET_ALREADY_LOADED]: 400, + [ApiError.FORBIDDEN]: 403, + [ApiError.UNAUTHORIZED]: 401, + [ApiError.INPUTS_NOT_IN_WALLET]: 400, + [ApiError.TX_OUTPUT_NOT_IN_WALLET]: 403, + [ApiError.ADDRESS_NOT_IN_WALLET]: 400, + [ApiError.WALLET_MAX_RETRIES]: 400, + [ApiError.TOKEN_NOT_FOUND]: 404, + [ApiError.DEVICE_NOT_FOUND]: 404, + [ApiError.TX_NOT_FOUND]: 404, + [ApiError.ADDRESS_NOT_FOUND]: 404, +}; + +/** + * Close database connection and get error object. + * + * @param mysql - The database connection + * @param error - ApiError return code + * @param extra - Extra data to be sent on the body of the error object + * @returns The error object + */ +export const closeDbAndGetError = async ( + mysql: ServerlessMysql, + error: ApiError, + extra?: StringMap, +): Promise => { + await closeDbConnection(mysql); + const body = { success: false, error, ...extra }; + return { + statusCode: STATUS_CODE_TABLE[error], + body: JSON.stringify(body), + }; +}; + +/** + * Will return early if the request is a wake-up call from serverless-plugin-warmup + */ +export const warmupMiddleware = (): middy.MiddlewareObj => { + const warmupBefore = (request: middy.Request): APIGatewayProxyResult | undefined => { + if (request.event.source === 'serverless-plugin-warmup') { + return { + statusCode: 200, + body: 'OK', + }; + } + + return undefined; + }; + + return { + before: warmupBefore, + }; +}; + +export const pushProviderRegexPattern = (): RegExp => { + const entries = Object.values(PushProvider); + const options = entries.join('|'); + return new RegExp(`^(?:${options})$`); +}; + +export const validateParams = ( + validator: Schema, + params: unknown, + validatorOptions: ValidationOptions = { + abortEarly: false, + convert: false, + }, +): ParamValidationResult => { + const result: ValidationResult = validator.validate(params, validatorOptions); + + const { error, value } = result; + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return { + error: true, + details, + }; + } + + return { + error: false, + value, + }; +}; + +/** + * This should be used inside a Joi validator object + */ +export const txIdJoiValidator = Joi.string().alphanum().min(64).max(64); diff --git a/packages/wallet-service/src/api/version.ts b/packages/wallet-service/src/api/version.ts new file mode 100644 index 00000000..64fc00f7 --- /dev/null +++ b/packages/wallet-service/src/api/version.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { APIGatewayProxyHandler } from 'aws-lambda'; +import 'source-map-support/register'; + +import { + getVersionData, +} from '@src/db'; +import { + FullNodeVersionData, +} from '@src/types'; +import { + closeDbConnection, + getDbConnection, +} from '@src/utils'; +import { warmupMiddleware } from '@src/api/utils'; +import { maybeRefreshWalletConstants } from '@src/commons'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; + +const mysql = getDbConnection(); + +/* + * Get version data from the stored data from the connected fullnode + * + * This lambda is called by API Gateway on GET /version + */ +export const get: APIGatewayProxyHandler = middy(async () => { + await maybeRefreshWalletConstants(mysql); + + const versionData: FullNodeVersionData = await getVersionData(mysql); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + data: versionData, + }), + }; +}).use(cors()) + .use(warmupMiddleware()); diff --git a/packages/wallet-service/src/api/wallet.ts b/packages/wallet-service/src/api/wallet.ts new file mode 100644 index 00000000..4a252f95 --- /dev/null +++ b/packages/wallet-service/src/api/wallet.ts @@ -0,0 +1,444 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { APIGatewayProxyHandler, Handler } from 'aws-lambda'; +import { LambdaClient, InvokeCommand, InvokeCommandOutput } from '@aws-sdk/client-lambda'; +import 'source-map-support/register'; + +import { ApiError } from '@src/api/errors'; +import { + addNewAddresses, + createWallet, + generateAddresses, + getWallet, + initWalletBalance, + initWalletTxHistory, + updateExistingAddresses, + updateWalletStatus, + updateWalletAuthXpub, +} from '@src/db'; +import { WalletStatus } from '@src/types'; +import { + closeDbConnection, + getDbConnection, + getWalletId, + verifySignature, + getAddressFromXpub, + confirmFirstAddress, + validateAuthTimestamp, + AUTH_MAX_TIMESTAMP_SHIFT_IN_SECONDS, +} from '@src/utils'; +import { closeDbAndGetError, warmupMiddleware } from '@src/api/utils'; +import { walletIdProxyHandler } from '@src/commons'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; +import Joi from 'joi'; +import createDefaultLogger from '@src/logger'; + +const mysql = getDbConnection(); + +const MAX_LOAD_WALLET_RETRIES: number = parseInt(process.env.MAX_LOAD_WALLET_RETRIES || '5', 10); + +/* + * Get the status of a wallet + * + * This lambda is called by API Gateway on GET /wallet + */ +export const get: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (walletId) => { + const status = await getWallet(mysql, walletId); + if (!status) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_FOUND); + } + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ success: true, status }), + }; +})).use(cors()) + .use(warmupMiddleware()); + +// If the env requires to validate the first address +// then we must set the firstAddress field as required +const shouldConfirmFirstAddress = process.env.CONFIRM_FIRST_ADDRESS === 'true'; +const firstAddressJoi = shouldConfirmFirstAddress ? Joi.string().required() : Joi.string(); + +const loadBodySchema = Joi.object({ + xpubkey: Joi.string() + .required(), + authXpubkey: Joi.string() + .required(), + xpubkeySignature: Joi.string() + .required(), + authXpubkeySignature: Joi.string() + .required(), + timestamp: Joi.number().positive().required(), + firstAddress: firstAddressJoi, +}); + +/** + * Invoke the loadWalletAsync function + * + * @param xpubkey - The xpubkey to load + * @param maxGap - The max gap + */ +/* istanbul ignore next */ +export const invokeLoadWalletAsync = async (xpubkey: string, maxGap: number): Promise => { + const client = new LambdaClient({ + endpoint: process.env.STAGE === 'dev' + ? 'http://localhost:3002' + : `https://lambda.${process.env.AWS_REGION}.amazonaws.com`, + region: process.env.AWS_REGION, + }); + const command = new InvokeCommand({ + // FunctionName is composed of: service name - stage - function name + FunctionName: `${process.env.SERVICE_NAME}-${process.env.STAGE}-loadWalletAsync`, + InvocationType: 'Event', + Payload: JSON.stringify({ xpubkey, maxGap }), + }); + + const response: InvokeCommandOutput = await client.send(command); + + // Event InvocationType returns 202 for a successful invokation + if (response.StatusCode !== 202) { + throw new Error('Lambda invoke failed'); + } +}; + +/** + * Calls verifySignature for both the wallet's xpub signature and + * the auth_xpub signature. + * + * @param walletId - The wallet id + * @param timestamp - The timestamp the message has been signed with + * @param xpubkeyStr - A string with the wallet's xpubkey + * @param xpubkeySignature - A string with the signature that proves the user owns the xpub + * @param authXpubkeyStr - A string with the auth xpubkey + * @param authXpubkeySignature- A string with the signature that proves the user owns the xpub + */ +export const validateSignatures = ( + walletId: string, + timestamp: number, + xpubkeyStr: string, + xpubkeySignature: string, + authXpubkeyStr: string, + authXpubkeySignature: string, +): boolean => { + // verify that the user owns the xpubkey + const xpubAddress = getAddressFromXpub(xpubkeyStr); + const xpubValid = verifySignature(xpubkeySignature, timestamp, xpubAddress, walletId.toString()); + + // verify that the user owns the auth_xpubkey + const authXpubAddress = getAddressFromXpub(authXpubkeyStr); + const authXpubValid = verifySignature(authXpubkeySignature, timestamp, authXpubAddress, walletId.toString()); + + return xpubValid && authXpubValid; +}; + +/* + * Changes the auth_xpubkey of a wallet after validating the user owns both the xpub and the auth_xpub + * + * This lambda is called by API Gateway on PUT /wallet/auth + */ +export const changeAuthXpub: APIGatewayProxyHandler = middy(async (event) => { + const eventBody = (function parseBody(body) { + try { + return JSON.parse(body); + } catch (e) { + return null; + } + }(event.body)); + + // body should have the same schema as load + const { value, error } = loadBodySchema.validate(eventBody, { + abortEarly: false, + convert: false, + }); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const xpubkeyStr = value.xpubkey; + const authXpubkeyStr = value.authXpubkey; + + const timestamp = value.timestamp; + const xpubkeySignature = value.xpubkeySignature; + const authXpubkeySignature = value.authXpubkeySignature; + + const [validTimestamp, timestampShift] = validateAuthTimestamp(timestamp, Date.now() / 1000); + + if (!validTimestamp) { + const details = [{ + message: `The timestamp is shifted ${timestampShift}(s). Limit is ${AUTH_MAX_TIMESTAMP_SHIFT_IN_SECONDS}(s).`, + }]; + + return { + statusCode: 400, + body: JSON.stringify({ + success: false, + error: ApiError.INVALID_PAYLOAD, + details, + }), + }; + } + + // is wallet already loaded/loading? + const walletId = getWalletId(xpubkeyStr); + const wallet = await getWallet(mysql, walletId); + + if (!wallet) { + return closeDbAndGetError(mysql, ApiError.WALLET_NOT_FOUND); + } + + if (shouldConfirmFirstAddress) { + const expectedFirstAddress = value.firstAddress; + const [firstAddressEqual, firstAddress] = confirmFirstAddress(expectedFirstAddress, xpubkeyStr); + + if (!firstAddressEqual) { + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { + message: `Expected first address to be ${expectedFirstAddress} but it is ${firstAddress}`, + }); + } + } + + const signaturesValid = validateSignatures(walletId, timestamp, xpubkeyStr, xpubkeySignature, authXpubkeyStr, authXpubkeySignature); + + if (!signaturesValid) { + await closeDbConnection(mysql); + + const details = [{ + message: 'Signatures are not valid', + }]; + + return { + statusCode: 403, + body: JSON.stringify({ success: false, details }), + }; + } + + await updateWalletAuthXpub(mysql, walletId, authXpubkeyStr); + + const updatedWallet = await getWallet(mysql, walletId); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + status: updatedWallet, + }), + }; +}).use(cors()); + +/* + * Load a wallet. First checks if the wallet doesn't exist already and then call another + * lamdba to asynchronously add new wallet info to database + * + * This lambda is called by API Gateway on POST /wallet + */ +export const load: APIGatewayProxyHandler = middy(async (event) => { + const logger = createDefaultLogger(); + const eventBody = (function parseBody(body) { + try { + return JSON.parse(body); + } catch (e) { + return null; + } + }(event.body)); + + const { value, error } = loadBodySchema.validate(eventBody, { + abortEarly: false, + convert: false, + }); + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const xpubkeyStr = value.xpubkey; + const authXpubkeyStr = value.authXpubkey; + const maxGap = parseInt(process.env.MAX_ADDRESS_GAP, 10); + + const timestamp = value.timestamp; + const xpubkeySignature = value.xpubkeySignature; + const authXpubkeySignature = value.authXpubkeySignature; + + const [validTimestamp, timestampShift] = validateAuthTimestamp(timestamp, Date.now() / 1000); + + if (!validTimestamp) { + const details = [{ + message: `The timestamp is shifted ${timestampShift}(s). Limit is ${AUTH_MAX_TIMESTAMP_SHIFT_IN_SECONDS}(s).`, + }]; + + return { + statusCode: 400, + body: JSON.stringify({ + success: false, + error: ApiError.INVALID_PAYLOAD, + details, + }), + }; + } + + // is wallet already loaded/loading? + const walletId = getWalletId(xpubkeyStr); + let wallet = await getWallet(mysql, walletId); + + // check if wallet is already loaded so we can fail early + if (wallet) { + if (wallet.status === WalletStatus.READY + || wallet.status === WalletStatus.CREATING) { + return closeDbAndGetError(mysql, ApiError.WALLET_ALREADY_LOADED, { status: wallet }); + } + + if (wallet.status === WalletStatus.ERROR + && wallet.retryCount >= MAX_LOAD_WALLET_RETRIES) { + return closeDbAndGetError(mysql, ApiError.WALLET_MAX_RETRIES, { status: wallet }); + } + } + + if (shouldConfirmFirstAddress) { + const expectedFirstAddress = value.firstAddress; + const [firstAddressEqual, firstAddress] = confirmFirstAddress(expectedFirstAddress, xpubkeyStr); + + if (!firstAddressEqual) { + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { + message: `Expected first address to be ${expectedFirstAddress} but it is ${firstAddress}`, + }); + } + } + + if (!validateSignatures(walletId, timestamp, xpubkeyStr, xpubkeySignature, authXpubkeyStr, authXpubkeySignature)) { + await closeDbConnection(mysql); + + const details = [{ + message: 'Signatures are not valid', + }]; + + return { + statusCode: 403, + body: JSON.stringify({ success: false, details }), + }; + } + + // if wallet does not exist at this point, we should add it to the wallet table with 'creating' status + if (!wallet) { + wallet = await createWallet(mysql, walletId, xpubkeyStr, authXpubkeyStr, maxGap); + } + + try { + /* This calls the lambda function as a "Event", so we don't care here for the response, + * we only care if the invokation failed or not + */ + await invokeLoadWalletAsync(xpubkeyStr, maxGap); + } catch (e) { + logger.error('Error on lambda wallet invoke', e); + + const newRetryCount = wallet.retryCount ? wallet.retryCount + 1 : 1; + // update wallet status to 'error' + await updateWalletStatus(mysql, walletId, WalletStatus.ERROR, newRetryCount); + + // refresh the variable with latest status, so we can return it properly + wallet = await getWallet(mysql, walletId); + } + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ success: true, status: wallet }), + }; +}).use(cors()) + .use(warmupMiddleware()); + +interface LoadEvent { + source?: string; + xpubkey: string; + maxGap: number; +} + +interface LoadResult { + success: boolean; + walletId: string; + xpubkey: string; +} + +/* + * This does the "heavy" work when loading a new wallet, updating the database tables accordingly. It + * expects a wallet entry already on the database + * + * This lambda is called async by another lambda, the one reponsible for the load wallet API + */ +export const loadWallet: Handler = async (event) => { + const logger = createDefaultLogger(); + // Can't use a middleware on this event, so we should just check the source (added by the warmup plugin) as + // our default middleware does + if (event.source === 'serverless-plugin-warmup') { + return { + success: true, + walletId: '', + xpubkey: '', + }; + } + + const xpubkey = event.xpubkey; + const maxGap = event.maxGap; + const walletId = getWalletId(xpubkey); + + try { + const { addresses, existingAddresses, newAddresses, lastUsedAddressIndex } = await generateAddresses(mysql, xpubkey, maxGap); + + // update address table with new addresses + await addNewAddresses(mysql, walletId, newAddresses, lastUsedAddressIndex); + + // update existing addresses' walletId and index + await updateExistingAddresses(mysql, walletId, existingAddresses); + + // from address_tx_history, update wallet_tx_history + await initWalletTxHistory(mysql, walletId, addresses); + + // from address_balance table, update balance table + await initWalletBalance(mysql, walletId, addresses); + + // update wallet status to 'ready' + await updateWalletStatus(mysql, walletId, WalletStatus.READY); + + await closeDbConnection(mysql); + + return { + success: true, + walletId, + xpubkey, + }; + } catch (e) { + logger.error('Erroed on loadWalletAsync: ', e); + + const wallet = await getWallet(mysql, walletId); + const newRetryCount = wallet.retryCount ? wallet.retryCount + 1 : 1; + + await updateWalletStatus(mysql, walletId, WalletStatus.ERROR, newRetryCount); + + return { + success: false, + walletId, + xpubkey, + }; + } +}; diff --git a/packages/wallet-service/src/commons.ts b/packages/wallet-service/src/commons.ts new file mode 100644 index 00000000..35fcc51c --- /dev/null +++ b/packages/wallet-service/src/commons.ts @@ -0,0 +1,684 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { APIGatewayProxyHandler } from 'aws-lambda'; + +import { ServerlessMysql } from 'serverless-mysql'; +import { strict as assert } from 'assert'; + +import { + getAddressWalletInfo, + getLatestHeight, + getWalletBalances as dbGetWalletBalances, + getWalletUnlockedUtxos, + getExpiredTimelocksUtxos, + unlockUtxos as dbUnlockUtxos, + updateAddressLockedBalance, + updateWalletLockedBalance, + getVersionData, + updateVersionData, + getBlockByHeight, + getTxsAfterHeight, + markTxsAsVoided, + getTxOutputs, + getTxOutputsBySpent, + removeTxsHeight, + unspendUtxos, + markUtxosAsVoided, + getTransactionsById, + deleteBlocksAfterHeight, + markWalletTxHistoryAsVoided, + markAddressTxHistoryAsVoided, + rebuildAddressBalancesFromUtxos, + fetchAddressBalance, + fetchAddressTxHistorySum, + getTokenSymbols, +} from '@src/db'; +import { + DecodedOutput, + StringMap, + TokenBalanceMap, + TxInput, + TxOutput, + TxOutputWithIndex, + DbTxOutput, + Tx, + Wallet, + Block, + WalletTokenBalance, + FullNodeVersionData, + AddressBalance, + AddressTotalBalance, + WalletProxyHandler, + WalletBalance, + Transaction, + WalletBalanceValue, + Severity, +} from '@src/types'; +import { Logger } from 'winston'; +import { addAlert } from '@src/utils/alerting.utils'; + +import { + getUnixTimestamp, + isTxVoided, +} from '@src/utils'; + +import hathorLib from '@hathor/wallet-lib'; +import { stringMapIterator, WalletBalanceMapConverter } from '@src/db/utils'; + +const VERSION_CHECK_MAX_DIFF = 60 * 60 * 1000; // 1 hour +const WARN_MAX_REORG_SIZE = parseInt(process.env.WARN_MAX_REORG_SIZE || '100', 10); + +/** + * Update the unlocked/locked balances for addresses and wallets connected to the given UTXOs. + * + * @param mysql - Database connection + * @param utxos - List of UTXOs that are unlocked by height + * @param updateTimelocks - If this update is triggered by a timelock expiring, update the next lock expiration + */ +export const unlockUtxos = async (mysql: ServerlessMysql, utxos: DbTxOutput[], updateTimelocks: boolean): Promise => { + if (utxos.length === 0) return; + + const outputs: TxOutput[] = utxos.map((utxo) => { + const decoded: DecodedOutput = { + type: 'P2PKH', + address: utxo.address, + timelock: utxo.timelock, + }; + + return { + value: utxo.authorities > 0 ? utxo.authorities : utxo.value, + token: utxo.tokenId, + decoded, + locked: false, + // set authority bit if necessary + token_data: utxo.authorities > 0 ? hathorLib.constants.TOKEN_AUTHORITY_MASK : 0, + // we don't care about spent_by and script + spent_by: null, + script: '', + }; + }); + + // mark as unlocked in database (this just changes the 'locked' flag) + await dbUnlockUtxos(mysql, utxos); + + const addressBalanceMap: StringMap = getAddressBalanceMap([], outputs); + // update address_balance table + await updateAddressLockedBalance(mysql, addressBalanceMap, updateTimelocks); + + // check if addresses belong to any started wallet + const addressWalletMap: StringMap = await getAddressWalletInfo(mysql, Object.keys(addressBalanceMap)); + + // update wallet_balance table + const walletBalanceMap: StringMap = getWalletBalanceMap(addressWalletMap, addressBalanceMap); + await updateWalletLockedBalance(mysql, walletBalanceMap, updateTimelocks); +}; + +/** + * Update the unlocked/locked balances for addresses and wallets connected to the UTXOs that were unlocked + * because of their timelocks expiring + * + * @param mysql - Database connection + * @param now - Current timestamp + */ +export const unlockTimelockedUtxos = async (mysql: ServerlessMysql, now: number): Promise => { + const utxos: DbTxOutput[] = await getExpiredTimelocksUtxos(mysql, now); + + await unlockUtxos(mysql, utxos, true); +}; + +/** + * Mark a transaction's outputs that are locked. Modifies the outputs in place. + * + * @remarks + * The timestamp is used to determine if each output is locked by time. On the other hand, `hasHeightLock` + * applies to all outputs. + * + * The idea is that `hasHeightLock = true` should be used for blocks, whose outputs are locked by + * height. Timelocks are handled by the `now` parameter. + * + * @param outputs - The transaction outputs + * @param now - Current timestamp + * @param hasHeightLock - Flag that tells if outputs are locked by height + */ +export const markLockedOutputs = (outputs: TxOutputWithIndex[], now: number, hasHeightLock = false): void => { + for (const output of outputs) { + output.locked = false; + if (hasHeightLock || output.decoded.timelock > now) { + output.locked = true; + } + } +}; + +/** + * Get the map of token balances for each address in the transaction inputs and outputs. + * + * @example + * Return map has this format: + * ``` + * { + * address1: {token1: balance1, token2: balance2}, + * address2: {token1: balance3} + * } + * ``` + * + * @param inputs - The transaction inputs + * @param outputs - The transaction outputs + * @returns A map of addresses and its token balances + */ +export const getAddressBalanceMap = ( + inputs: TxInput[], + outputs: TxOutput[], +): StringMap => { + const addressBalanceMap = {}; + + for (const input of inputs) { + const address = input.decoded.address; + + // get the TokenBalanceMap from this input + const tokenBalanceMap = TokenBalanceMap.fromTxInput(input); + // merge it with existing TokenBalanceMap for the address + addressBalanceMap[address] = TokenBalanceMap.merge(addressBalanceMap[address], tokenBalanceMap); + } + + for (const output of outputs) { + const address = output.decoded.address; + + // get the TokenBalanceMap from this output + const tokenBalanceMap = TokenBalanceMap.fromTxOutput(output); + + // merge it with existing TokenBalanceMap for the address + addressBalanceMap[address] = TokenBalanceMap.merge(addressBalanceMap[address], tokenBalanceMap); + } + + return addressBalanceMap; +}; + +/** + * Gets a list of tokens from a list of inputs and outputs + * + * @param inputs - The transaction inputs + * @param outputs - The transaction outputs + * @returns A list of tokens present in the inputs and outputs + */ +export const getTokenListFromInputsAndOutputs = (inputs: TxInput[], outputs: TxOutputWithIndex[]): string[] => { + const tokenIds = new Set([]); + + for (const input of inputs) { + tokenIds.add(input.token); + } + + for (const output of outputs) { + tokenIds.add(output.token); + } + + return [...tokenIds]; +}; + +/** + * Get the map of token balances for each wallet. + * + * @remarks + * Different addresses can belong to the same wallet, so this function merges their + * token balances. + * + * @example + * Return map has this format: + * ``` + * { + * wallet1: {token1: balance1, token2: balance2}, + * wallet2: {token1: balance3} + * } + * ``` + * + * @param addressWalletMap - Map of addresses and corresponding wallets + * @param addressBalanceMap - Map of addresses and corresponding token balances + * @returns A map of wallet ids and its token balances + */ +export const getWalletBalanceMap = ( + addressWalletMap: StringMap, + addressBalanceMap: StringMap, +): StringMap => { + const walletBalanceMap = {}; + for (const [address, balanceMap] of Object.entries(addressBalanceMap)) { + const wallet = addressWalletMap[address]; + const walletId = wallet && wallet.walletId; + + // if this address is not from a started wallet, ignore + if (!walletId) continue; + + walletBalanceMap[walletId] = TokenBalanceMap.merge(walletBalanceMap[walletId], balanceMap); + } + return walletBalanceMap; +}; + +/** + * Get a wallet's balance, taking into account any existing timelocks. + * + * @remarks + * If any timelock has expired, database tables will be refreshed before returning the balances. + * + * @param mysql - Database connection + * @param now - Current timestamp + * @param walletId - The wallet id + * @param tokenIds - A list of token ids + */ +export const getWalletBalances = async ( + mysql: ServerlessMysql, + now: number, + walletId: string, + tokenIds: string[] = [], +): Promise => { + let balances = await dbGetWalletBalances(mysql, walletId, tokenIds); + + // if any of the balances' timelock has expired, update the tables before returning + const refreshBalances = balances.some((tb) => { + if (tb.balance.lockExpires && tb.balance.lockExpires <= now) { + return true; + } + return false; + }); + + if (refreshBalances) { + const currentHeight = await getLatestHeight(mysql); + const utxos = await getWalletUnlockedUtxos(mysql, walletId, now, currentHeight); + await unlockUtxos(mysql, utxos, true); + balances = await dbGetWalletBalances(mysql, walletId, tokenIds); + } + return balances; +}; + +/** + * Updates the wallet-lib constants if needed. + * + * @returns {Promise} A promise that resolves when the wallet-lib constants have been set. + */ +export const maybeRefreshWalletConstants = async (mysql: ServerlessMysql): Promise => { + const lastVersionData: FullNodeVersionData = await getVersionData(mysql); + const now = getUnixTimestamp(); + + if (!lastVersionData || now - lastVersionData.timestamp > VERSION_CHECK_MAX_DIFF) { + // Query and update versions + const apiResponse = await hathorLib.version.checkApiVersion(); + const versionData: FullNodeVersionData = { + timestamp: now, + version: apiResponse.version, + network: apiResponse.network, + minWeight: apiResponse.min_weight, + minTxWeight: apiResponse.min_tx_weight, + minTxWeightCoefficient: apiResponse.min_tx_weight_coefficient, + minTxWeightK: apiResponse.min_tx_weight_k, + tokenDepositPercentage: apiResponse.token_deposit_percentage, + rewardSpendMinBlocks: apiResponse.reward_spend_min_blocks, + maxNumberInputs: apiResponse.max_number_inputs, + maxNumberOutputs: apiResponse.max_number_outputs, + }; + + await updateVersionData(mysql, versionData); + } else { + hathorLib.transaction.updateTransactionWeightConstants( + lastVersionData.minTxWeight, + lastVersionData.minTxWeightCoefficient, + lastVersionData.minTxWeightK, + ); + hathorLib.tokens.updateDepositPercentage(lastVersionData.tokenDepositPercentage); + hathorLib.transaction.updateMaxInputsConstant(lastVersionData.maxNumberInputs); + hathorLib.transaction.updateMaxOutputsConstant(lastVersionData.maxNumberOutputs); + hathorLib.wallet.updateRewardLockConstant(lastVersionData.rewardSpendMinBlocks); + } +}; + +/** + * Searches our blocks database for the last block that is not voided. + * + * @param mysql - Database connection + * + * @returns A Block instance with the last block that is not voided. + */ +export const searchForLatestValidBlock = async (mysql: ServerlessMysql): Promise => { + // Get our current best block. + const latestHeight: number = await getLatestHeight(mysql); + const bestBlock: Block = await getBlockByHeight(mysql, latestHeight); + + let start = 0; + let end = bestBlock.height; + let latestValidBlock = bestBlock; + + while (start <= end) { + const midHeight = Math.floor((start + end) / 2); + + // Check if the block at middle position is voided + const middleBlock: Block = await getBlockByHeight(mysql, midHeight); + const [isVoided] = await isTxVoided(middleBlock.txId); + + if (!isVoided) { + // Not voided, discard left half as all blocks to the left should + // be valid, the reorg happened after this height. + latestValidBlock = middleBlock; + start = midHeight + 1; + } else { + end = midHeight - 1; + } + } + + return latestValidBlock; +}; + +/* + * Receives a list of transactions that are being voided on a reorg and returns a list of transactions that spend them. + * + * Also marks wallet and addresses history that use this transaction as voided + * + * @param mysql - Database connection + * @param txs - List of voided transactions to handle + * + * @returns A new list of voided transactions that are linked to the received list and a list of tx_outputs affected + * by this iteration. + */ +export const handleVoidedTxList = async (mysql: ServerlessMysql, logger: Logger, txs: Tx[]): Promise<[Tx[], DbTxOutput[]]> => { + logger.debug(`Setting ${txs.length} transactions as voided.`, { + transactions: txs, + }); + await markTxsAsVoided(mysql, txs); + logger.debug(`Setting WalletTxHistory as voided from ${txs.length} transactions.`); + await markWalletTxHistoryAsVoided(mysql, txs); + logger.debug(`Setting AddressTxHistory as voided from ${txs.length} transactions.`); + await markAddressTxHistoryAsVoided(mysql, txs); + + // tx outputs are the list of all outputs in the transaction list + const txOutputs: DbTxOutput[] = await getTxOutputs(mysql, txs); + + logger.debug(`Fetched ${txOutputs.length} utxos from the voided transaction list`, { + txOutputs, + }); + + // get outputs that were spent in txOutputs + const txOutputsTxIds: Set = txOutputs.reduce( + (acc: Set, txOutput: DbTxOutput) => acc.add(txOutput.txId), + new Set(), + ); + + // spent outputs are the list of outputs that were spent by those tx_outputs + const spentOutputs: DbTxOutput[] = await getTxOutputsBySpent(mysql, [...txOutputsTxIds]); + + // unspend them as the tx_outputs that spent them are now voided + if (spentOutputs.length > 0) { + logger.debug(`Unspending ${spentOutputs.length} tx_outputs.`, { + txOutputs: spentOutputs, + }); + await unspendUtxos(mysql, [...spentOutputs]); + } + + const affectedUtxoList = [...txOutputs, ...spentOutputs]; + + // mark the tx_outputs from the received tx list as voided + logger.debug(`Setting ${txOutputs.length} tx_outputs as voided.`, { + txOutputs, + }); + await markUtxosAsVoided(mysql, txOutputs); + + // get the list of tx ids that spend the tx_outputs list from the received tx list + const txIds = txOutputs.reduce((acc: Set, utxo: DbTxOutput) => { + if (utxo.spentBy) { + acc.add(utxo.spentBy); + } + + return acc; + }, new Set()); + + // fetch all transactions that spend those voided txs outputs: + const newTxs = await getTransactionsById(mysql, [...txIds]); + + logger.debug(`Fetched ${newTxs.length} transactions that spend the voided tx outputs list`, { + transactions: newTxs, + }); + + return [newTxs, affectedUtxoList]; +}; + +/** + * Handles a voided transaction by re-calculating the balances for all affected addresses after + * removing the tx + * + * @param mysql - Database connection + * @param tx - The voided transaction to remove + */ +export const handleVoided = async (mysql: ServerlessMysql, logger: Logger, tx: Tx): Promise => { + let txs: Tx[] = [tx]; + let affectedUtxoList: DbTxOutput[] = []; + + while (txs.length > 0) { + const [newTxs, newAffectedUtxoList] = await handleVoidedTxList(mysql, logger, txs); + txs = newTxs; + affectedUtxoList = [...affectedUtxoList, ...newAffectedUtxoList]; + } + + // fetch all addresses and transactions affected by the voided transaction + const [affectedAddresses, affectedTxIds] = affectedUtxoList.reduce( + (acc: [Set, Set], utxo: DbTxOutput) => { + acc[0].add(utxo.address); + acc[1].add(utxo.txId); + + return acc; + }, + [new Set(), new Set()], + ); + + if (affectedAddresses.size > 0) { + const addresses = [...affectedAddresses]; + + logger.debug(`Rebuilding balances for ${addresses.length} addresses.`, { + addresses, + }); + logger.debug(`Rebuilding tx count from ${affectedTxIds.size} transactions`, { + affectedTxIds, + }); + await rebuildAddressBalancesFromUtxos(mysql, addresses, [...affectedTxIds]); + await validateAddressBalances(mysql, addresses); + } + + logger.debug('Handle voided tx is done.'); +}; + +export const validateAddressBalances = async (mysql: ServerlessMysql, addresses: string[]): Promise => { + const addressBalances: AddressBalance[] = await fetchAddressBalance(mysql, addresses); + const addressTxHistorySums: AddressTotalBalance[] = await fetchAddressTxHistorySum(mysql, addresses); + + for (let i = 0; i < addressTxHistorySums.length; i++) { + const addressBalance: AddressBalance = addressBalances[i]; + const addressTxHistorySum: AddressTotalBalance = addressTxHistorySums[i]; + + assert.strictEqual(addressBalance.tokenId, addressTxHistorySum.tokenId); + + // balances must match + assert.strictEqual(addressBalance.unlockedBalance + addressBalance.lockedBalance, addressTxHistorySum.balance); + } +}; + +/** + * Handles a reorg by finding the last valid block on the service's database and + * removing transactions and tx_outputs before re-calculating the address balances. + * + * @param mysql - Database connection + * + * @returns The new best block height + */ +export const handleReorg = async (mysql: ServerlessMysql, logger: Logger): Promise => { + const { height } = await searchForLatestValidBlock(mysql); + const currentHeight = await getLatestHeight(mysql); + + logger.debug(`Handling reorg. Our latest valid block is ${height} and our highest block height is ${currentHeight}`, { + height, + currentHeight, + }); + + if ((currentHeight - height) > WARN_MAX_REORG_SIZE) { + logger.error(`A reorg with ${currentHeight - height} blocks has been detected`); + await addAlert( + 'Big reorg detected', + `A reorg with ${currentHeight - height} blocks has been detected`, + Severity.MINOR, + { walletServiceHeight: currentHeight, fullNodeHeight: height }, + ); + } + + // fetch all block transactions where height > latestValidBlock + const allTxsAfterHeight = await getTxsAfterHeight(mysql, height); + let txs: Tx[] = allTxsAfterHeight.filter((tx) => [ + hathorLib.constants.BLOCK_VERSION, + hathorLib.constants.MERGED_MINED_BLOCK_VERSION, + ].indexOf(tx.version) > -1); + + // remove blocks where height > latestValidBlock as we already have them on memory + await deleteBlocksAfterHeight(mysql, height); + + logger.debug('Removing transactions', txs.map((tx) => tx.txId)); + + let affectedUtxoList: DbTxOutput[] = []; + + /* + * Here we need to traverse the DAG of "funds", starting from the voided tx_outputs from the blocks + * and stopping when there are no more linked tx_outputs voided. + * + * We do that by using a BFS, that mutates the DAG on every iteration (by setting the transactions + * as voided on the database). + */ + while (txs.length > 0) { + const [newTxs, newAffectedUtxoList] = await handleVoidedTxList(mysql, logger, txs); + + txs = newTxs; + affectedUtxoList = [...affectedUtxoList, ...newAffectedUtxoList]; + } + + // get all remaining txs and set height = null (mempool) + const remainingTxs: Tx[] = await getTxsAfterHeight(mysql, height); + if (remainingTxs.length > 0) { + logger.debug(`Setting ${remainingTxs.length} unconfirmed transactions to the mempool (height = NULL).`, { + remainingTxs, + }); + await removeTxsHeight(mysql, remainingTxs); + } + + // fetch all addresses and transactions affected by the reorg + const [affectedAddresses, affectedTxIds] = affectedUtxoList.reduce( + (acc: [Set, Set], utxo: DbTxOutput) => { + acc[0].add(utxo.address); + acc[1].add(utxo.txId); + + return acc; + }, + [new Set(), new Set()], + ); + + if (affectedAddresses.size > 0) { + const addresses = [...affectedAddresses]; + logger.debug(`Rebuilding balances for ${addresses.length} addresses.`, { + addresses, + }); + logger.debug(`Rebuilding tx count from ${affectedTxIds.size} transactions`, { + affectedTxIds, + }); + await rebuildAddressBalancesFromUtxos(mysql, addresses, [...affectedTxIds]); + await validateAddressBalances(mysql, addresses); + } + + logger.debug('Reorg is done.'); + + return height; +}; + +export const walletIdProxyHandler = (handler: WalletProxyHandler): APIGatewayProxyHandler => ( + async (event, context) => { + let walletId: string; + try { + walletId = event.requestContext.authorizer.principalId; + // validate walletId? + } catch (e) { + return { + statusCode: 401, + body: 'Unauthorized', + }; + } + return handler(walletId, event, context); + } +); + +export const prepareOutputs = (outputs: TxOutput[], txId: string, logger: Logger): TxOutputWithIndex[] => { + const preparedOutputs: [number, TxOutputWithIndex[]] = outputs.reduce( + ([currIndex, newOutputs]: [number, TxOutputWithIndex[]], output: TxOutput): [number, TxOutputWithIndex[]] => { + if (!output.decoded + || output.decoded.type === null + || output.decoded.type === undefined) { + logger.warn(`Ignoring tx output with index ${currIndex} from tx ${txId} as script couldn't be decoded.`); + return [currIndex + 1, newOutputs]; + } + + return [ + currIndex + 1, + [ + ...newOutputs, + { + ...output, + index: currIndex, + }, + ], + ]; + }, + [0, []], + ); + + return preparedOutputs[1]; +}; + +/** + * Get a list of wallet balance per token by informed transaction. + * + * @param mysql + * @param tx - The transaction to get related wallets and their token balances + * @returns + */ +export const getWalletBalancesForTx = async (mysql: ServerlessMysql, tx: Transaction): Promise> => { + const addressBalanceMap: StringMap = getAddressBalanceMap(tx.inputs, tx.outputs); + // return only wallets that were started + const addressWalletMap: StringMap = await getAddressWalletInfo(mysql, Object.keys(addressBalanceMap)); + + // Create a new map focused on the walletId and storing its balance variation from this tx + const walletsMap: StringMap = {}; + + // Accumulation of tokenId to be used to extract its symbols. + const tokenIdAccumulation = []; + + // Iterates all the addresses to populate the map's data + const addressWalletEntries = stringMapIterator(addressWalletMap) as [string, Wallet][]; + for (const [address, wallet] of addressWalletEntries) { + // Create a new walletId entry if it does not exist + if (!walletsMap[wallet.walletId]) { + walletsMap[wallet.walletId] = { + txId: tx.tx_id, + walletId: wallet.walletId, + addresses: [], + walletBalanceForTx: new TokenBalanceMap(), + }; + } + const walletData = walletsMap[wallet.walletId]; + + // Add this address to the wallet's affected addresses list + walletData.addresses.push(address); + + // Merge the balance of this address with the total balance of the wallet + const mergedBalance = TokenBalanceMap.merge(walletData.walletBalanceForTx, addressBalanceMap[address]); + walletData.walletBalanceForTx = mergedBalance; + + const tokenIdList = Object.keys(mergedBalance.map); + tokenIdAccumulation.push(tokenIdList); + } + + const tokenIdSet = new Set(tokenIdAccumulation.reduce((prev, eachGroup) => [...prev, ...eachGroup], [])); + const tokenSymbolsMap = await getTokenSymbols(mysql, Array.from(tokenIdSet.values())); + + return WalletBalanceMapConverter.toValue(walletsMap, tokenSymbolsMap); +}; diff --git a/packages/wallet-service/src/db/cronRoutines.ts b/packages/wallet-service/src/db/cronRoutines.ts new file mode 100644 index 00000000..e56f581e --- /dev/null +++ b/packages/wallet-service/src/db/cronRoutines.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import 'source-map-support/register'; +import { + getUnsentTxProposals, + releaseTxProposalUtxos, + countStalePushDevices, + deleteStalePushDevices, + updateTxProposal, +} from '@src/db'; +import { + closeDbConnection, + getDbConnection, + getUnixTimestamp, +} from '@src/utils'; +import createDefaultLogger from '@src/logger'; +import { TxProposalStatus } from '@src/types'; + +const STALE_TX_PROPOSAL_INTERVAL = 5 * 60; + +const mysql = getDbConnection(); + +/** + * Function called to clean stale push devices. + * + * @remarks + * This is a lambda function that should be triggered by an scheduled event. This will run by default with + * frequency of 15 days (configurable on serverless.yml) and will query for devices not updated more than 1 month. + */ +export const cleanStalePushDevices = async (): Promise => { + const logger = createDefaultLogger(); + + const staleDevices: number = await countStalePushDevices(mysql); + logger.debug(`Found ${staleDevices} stale devices to be cleaned up.`, { + staleDevices, + }); + + await deleteStalePushDevices(mysql); + + await closeDbConnection(mysql); +}; + +/** + * Function called to cleanup old unsent tx proposal utxos + * + * @remarks + * This is a lambda function that should be triggered by an scheduled event. This will run by default with + * frequency of 5 minutes (configurable on serverless.yml) and will query for devices not updated more than 5 minutes + */ +export const cleanUnsentTxProposalsUtxos = async (): Promise => { + const logger = createDefaultLogger(); + + const now = getUnixTimestamp(); + const txProposalsBefore = now - STALE_TX_PROPOSAL_INTERVAL; + const unsentTxProposals: string[] = await getUnsentTxProposals(mysql, txProposalsBefore); + + if (unsentTxProposals.length > 0) { + logger.debug(`Will clean utxos from ${unsentTxProposals.length} txproposals`); + + try { + await releaseTxProposalUtxos(mysql, unsentTxProposals); + await updateTxProposal(mysql, unsentTxProposals, now, TxProposalStatus.CANCELLED); + } catch (e) { + logger.error('Failed to release unspent tx proposals: ', unsentTxProposals, e); + } + } else { + logger.debug('No txproposals utxos to clean.'); + } + + await closeDbConnection(mysql); +}; diff --git a/packages/wallet-service/src/db/index.ts b/packages/wallet-service/src/db/index.ts new file mode 100644 index 00000000..248a2ca6 --- /dev/null +++ b/packages/wallet-service/src/db/index.ts @@ -0,0 +1,3199 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import { strict as assert } from 'assert'; +import { ServerlessMysql } from 'serverless-mysql'; +import { get } from 'lodash'; +import { OkPacket } from 'mysql'; +import { constants } from '@hathor/wallet-lib'; +import { + AddressIndexMap, + AddressInfo, + Authorities, + Balance, + DbSelectResult, + GenerateAddresses, + IWalletInput, + ShortAddressInfo, + StringMap, + TokenBalanceMap, + TokenInfo, + TxInput, + TxOutputWithIndex, + TxProposal, + TxProposalStatus, + TxTokenBalance, + DbTxOutput, + Wallet, + WalletStatus, + WalletTokenBalance, + FullNodeVersionData, + Block, + Tx, + AddressBalance, + AddressTotalBalance, + IFilterTxOutput, + Miner, + PushDevice, + TxByIdToken, + PushDeviceSettings, + Severity, +} from '@src/types'; +import { + getUnixTimestamp, + isAuthority, + getAddressPath, + xpubDeriveChild, + getAddresses, +} from '@src/utils'; +import { + getWalletFromDbEntry, + getTxsFromDBResult, +} from '@src/db/utils'; +import { addAlert } from '@src/utils/alerting.utils'; + +const BLOCK_VERSION = [ + constants.BLOCK_VERSION, + constants.MERGED_MINED_BLOCK_VERSION, +]; +const BURN_ADDRESS = 'HDeadDeadDeadDeadDeadDeadDeagTPgmn'; + +/** + * Checks if a transaction was on the database in the past and got voided. + * + * @remarks + * Since we delete transactions from the transactions table when it's voided, + * we can use the address_tx_history table (which stores voided txs) to check + * if it's there. + * + * @param mysql - Database connection + * @param txId - The transaction id to search for + * @returns True or False + */ +export const checkTxWasVoided = async (mysql: ServerlessMysql, txId: string): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT * FROM \`address_tx_history\` + WHERE tx_id = ? + LIMIT 1`, + [txId], + ); + + if (!results.length) { + return false; + } + + const addressTxHistory = results[0]; + + return Boolean(addressTxHistory.voided); +}; + +/** + * Cleanup all records from a transaction that was voided in the past + * + * @remarks + * This does not re-calculates balances, so it's only supposed to be used to clear + * the tx_output, address_tx_history and wallet_tx_history tables after the + * handleReorg method voided this transaction + * + * @param mysql - Database connection + * @param txId - The transaction to clear from database + */ +export const cleanupVoidedTx = async (mysql: ServerlessMysql, txId: string): Promise => { + await mysql.query( + `DELETE FROM \`transaction\` + WHERE tx_id = ? + AND voided = true`, + [txId], + ); + + await mysql.query( + `DELETE FROM \`tx_output\` + WHERE tx_id = ? + AND voided = true`, + [txId], + ); + + await mysql.query( + `DELETE FROM \`address_tx_history\` + WHERE tx_id = ? + AND voided = true`, + [txId], + ); + + await mysql.query( + `DELETE FROM \`wallet_tx_history\` + WHERE tx_id = ? + AND voided = true`, + [txId], + ); +}; + +/** + * Given an xpubkey, generate its addresses. + * + * @remarks + * Also, check which addresses are used, taking into account the maximum gap of unused addresses (maxGap). + * This function doesn't update anything on the database, just reads data from it. + * + * @param mysql - Database connection + * @param xpubkey - The xpubkey + * @param maxGap - Number of addresses that should have no transactions before we consider all addresses loaded + * @returns Object with all addresses for the given xpubkey and corresponding index + */ +export const generateAddresses = async (mysql: ServerlessMysql, xpubkey: string, maxGap: number): Promise => { + const existingAddresses: AddressIndexMap = {}; + const newAddresses: AddressIndexMap = {}; + const allAddresses: string[] = []; + + // We currently generate only addresses in change derivation path 0 + // (more details in https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#Change) + // so we derive our xpub to this path and use it to get the addresses + const derivedXpub = xpubDeriveChild(xpubkey, 0); + + let highestCheckedIndex = -1; + let lastUsedAddressIndex = -1; + do { + const addrMap = getAddresses(derivedXpub, highestCheckedIndex + 1, maxGap); + allAddresses.push(...Object.keys(addrMap)); + + const results: DbSelectResult = await mysql.query( + `SELECT \`address\`, + \`index\`, + \`transactions\` + FROM \`address\` + WHERE \`address\` + IN (?)`, + [Object.keys(addrMap)], + ); + + for (const entry of results) { + const address = entry.address as string; + // get index from addrMap as the one from entry might be null + const index = addrMap[address]; + // add to existingAddresses + existingAddresses[address] = index; + + // if address is used, check if its index is higher than the current highest used index + if (entry.transactions > 0 && index > lastUsedAddressIndex) { + lastUsedAddressIndex = index; + } + + delete addrMap[address]; + } + + highestCheckedIndex += maxGap; + Object.assign(newAddresses, addrMap); + } while (lastUsedAddressIndex + maxGap > highestCheckedIndex); + + // we probably generated more addresses than needed, as we always generate + // addresses in maxGap blocks + const totalAddresses = lastUsedAddressIndex + maxGap + 1; + for (const [address, index] of Object.entries(newAddresses)) { + if (index > lastUsedAddressIndex + maxGap) { + delete newAddresses[address]; + } + } + + return { + addresses: allAddresses.slice(0, totalAddresses), + newAddresses, + existingAddresses, + lastUsedAddressIndex, + }; +}; + +/** + * Get wallet information for the given addresses. + * + * @remarks + * For each address in the list, check if it's from a started wallet and return its information. If + * address is not from a started wallet, it won't be on the final map. + * + * @param mysql - Database connection + * @param addresses - Addresses to fetch wallet information + * @returns A map of address and corresponding wallet information + */ +export const getAddressWalletInfo = async (mysql: ServerlessMysql, addresses: string[]): Promise> => { + const addressWalletMap: StringMap = {}; + const results: DbSelectResult = await mysql.query( + `SELECT DISTINCT a.\`address\`, + a.\`wallet_id\`, + w.\`auth_xpubkey\`, + w.\`xpubkey\`, + w.\`max_gap\` + FROM \`address\` a + INNER JOIN \`wallet\` w + ON a.wallet_id = w.id + WHERE a.\`address\` + IN (?)`, + [addresses], + ); + for (const entry of results) { + const walletInfo: Wallet = { + walletId: entry.wallet_id as string, + authXpubkey: entry.auth_xpubkey as string, + xpubkey: entry.xpubkey as string, + maxGap: entry.max_gap as number, + }; + addressWalletMap[entry.address as string] = walletInfo; + } + return addressWalletMap; +}; + +/** + * Get the wallet information. + * + * @param mysql - Database connection + * @param walletId - The wallet id + * @returns The wallet information or null if it was not found + */ +export const getWallet = async (mysql: ServerlessMysql, walletId: string): Promise => { + const results: DbSelectResult = await mysql.query('SELECT * FROM `wallet` WHERE `id` = ?', walletId); + if (results.length) { + const result = results[0]; + return getWalletFromDbEntry(result); + } + return null; +}; + +/** + * Create a wallet on database. + * + * @param mysql - Database connection + * @param walletId - The wallet id + * @param xpubkey - The wallet's xpubkey + * @param maxGap - Maximum gap of addresses for this wallet + * @returns The wallet information + */ +export const createWallet = async ( + mysql: ServerlessMysql, + walletId: string, + xpubkey: string, + authXpubkey: string, + maxGap: number, +): Promise => { + const ts = getUnixTimestamp(); + const entry = { + id: walletId, + xpubkey, + auth_xpubkey: authXpubkey, + status: WalletStatus.CREATING, + created_at: ts, + max_gap: maxGap, + }; + await mysql.query( + `INSERT INTO \`wallet\` + SET ?`, + [entry], + ); + return { + walletId, + xpubkey, + authXpubkey, + maxGap, + retryCount: 0, + status: WalletStatus.CREATING, + createdAt: ts, + readyAt: null, + }; +}; + +/** + * Update an existing wallet's status. + * + * @param mysql - Database connection + * @param walletId - The wallet id + * @param status - The new wallet status + */ +export const updateWalletStatus = async ( + mysql: ServerlessMysql, + walletId: string, + status: WalletStatus, + retryCount = 0, +): Promise => { + const ts = getUnixTimestamp(); + await mysql.query( + `UPDATE \`wallet\` + SET \`status\` = ?, + \`ready_at\` = ?, + \`retry_count\` = ? + WHERE \`id\` = ?`, + [status, ts, retryCount, walletId], + ); +}; + +/** + * Update an existing wallet's auth_xpubkey + * + * @param mysql - Database connection + * @param walletId - The wallet id + * @param authXpubkey - The new wallet auth_xpubkey + */ +export const updateWalletAuthXpub = async ( + mysql: ServerlessMysql, + walletId: string, + authXpubkey: string, +): Promise => { + await mysql.query( + `UPDATE \`wallet\` + SET \`auth_xpubkey\` = ? + WHERE \`id\` = ?`, + [authXpubkey, walletId], + ); +}; + +/** + * Add addresses to address table. + * + * @remarks + * The addresses are added with the given walletId and 0 transactions. + * + * @param mysql - Database connection + * @param walletId - The wallet id + * @param addresses - A map of addresses and corresponding indexes + */ +export const addNewAddresses = async ( + mysql: ServerlessMysql, + walletId: string, + addresses: AddressIndexMap, + lastUsedAddressIndex: number, +): Promise => { + if (Object.keys(addresses).length === 0) return; + const entries = []; + for (const [address, index] of Object.entries(addresses)) { + entries.push([address, index, walletId, 0]); + } + await mysql.query( + `INSERT INTO \`address\`(\`address\`, \`index\`, + \`wallet_id\`, \`transactions\`) + VALUES ?`, + [entries], + ); + + // Store on the wallet table the highest used index + await mysql.query( + `UPDATE \`wallet\` + SET \`last_used_address_index\` = ? + WHERE \`id\` = ?`, + [lastUsedAddressIndex, walletId], + ); +}; + +/** + * Update addresses on the address table. + * + * @remarks + * It updates both the walletId and index of given addresses. + * + * @param mysql - Database connection + * @param walletId - The wallet id + * @param addresses - A map of addresses and corresponding indexes + */ +export const updateExistingAddresses = async (mysql: ServerlessMysql, walletId: string, addresses: AddressIndexMap): Promise => { + if (Object.keys(addresses).length === 0) return; + + for (const [address, index] of Object.entries(addresses)) { + await mysql.query( + `UPDATE \`address\` + SET \`wallet_id\` = ?, + \`index\` = ? + WHERE \`address\` = ?`, + [walletId, index, address], + ); + } +}; + +/** + * Get a wallet's address detail. + * + * @param mysql - Database connection + * @param walletId - Wallet id + * @param address - Address to get the detail + * @returns The details of the address {address, index, transactions} or null if not found + */ +export const getWalletAddressDetail = async (mysql: ServerlessMysql, walletId: string, address: string): Promise => { + const results: DbSelectResult = await mysql.query(` + SELECT * + FROM \`address\` + WHERE \`wallet_id\` = ? + AND \`address\` = ?`, + [walletId, address]); + + if (results.length > 0) { + const data = results[0]; + + const addressDetail: AddressInfo = { + address: data.address as string, + index: data.index as number, + transactions: data.transactions as number, + }; + + return addressDetail; + } + + return null; +}; + +/** + * Initialize a wallet's transaction history. + * + * @remarks + * This function adds entries to wallet_tx_history table, using data from address_tx_history. + * + * @param mysql - Database connection + * @param walletId - The wallet id + * @param addresses - The addresses that belong to this wallet + */ +export const initWalletTxHistory = async (mysql: ServerlessMysql, walletId: string, addresses: string[]): Promise => { + // XXX we could also get the addresses from the address table, but the caller probably has this info already + + if (addresses.length === 0) return; + + const results: DbSelectResult = await mysql.query( + `SELECT \`tx_id\`, + \`token_id\`, + SUM(\`balance\`) AS balance, + \`timestamp\` + FROM \`address_tx_history\` + WHERE \`address\` IN (?) + AND \`voided\` = FALSE + GROUP BY \`tx_id\`, + \`token_id\`, + \`timestamp\``, + [addresses], + ); + if (results.length === 0) return; + + const walletTxHistory = []; + for (const row of results) { + walletTxHistory.push([walletId, row.token_id, row.tx_id, row.balance, row.timestamp]); + } + await mysql.query( + `INSERT INTO \`wallet_tx_history\`(\`wallet_id\`, \`token_id\`, + \`tx_id\`, \`balance\`, + \`timestamp\`) + VALUES ?`, + [walletTxHistory], + ); +}; + +/** + * Initialize a wallet's balance. + * + * @remarks + * This function adds entries to wallet_balance table, using data from address_balance and address_tx_history. + * + * @param mysql - Database connection + * @param walletId - The wallet id + * @param addresses - The addresses that belong to this wallet + */ +export const initWalletBalance = async (mysql: ServerlessMysql, walletId: string, addresses: string[]): Promise => { + // XXX we could also do a join between address and address_balance tables so we don't + // need to receive the addresses, but the caller probably has this info already + const results1: DbSelectResult = await mysql.query( + `SELECT \`token_id\`, + SUM(\`total_received\`) AS \`total_received\`, + SUM(\`unlocked_balance\`) AS \`unlocked_balance\`, + SUM(\`locked_balance\`) AS \`locked_balance\`, + MIN(\`timelock_expires\`) AS \`timelock_expires\`, + BIT_OR(\`unlocked_authorities\`) AS \`unlocked_authorities\`, + BIT_OR(\`locked_authorities\`) AS \`locked_authorities\` + FROM \`address_balance\` + WHERE \`address\` + IN (?) + GROUP BY \`token_id\` + ORDER BY \`token_id\``, + [addresses], + ); + // we need to use table address_tx_history for the transaction count. We can't simply + // sum the transaction count for each address_balance, as they may share transactions + const results2: DbSelectResult = await mysql.query( + `SELECT \`token_id\`, + SUM(\`balance\`) AS \`balance\`, + COUNT(DISTINCT \`tx_id\`) AS \`transactions\` + FROM \`address_tx_history\` + WHERE \`address\` IN (?) + AND \`voided\` = FALSE + GROUP BY \`token_id\` + ORDER BY \`token_id\``, + [addresses], + ); + + assert.strictEqual(results1.length, results2.length); + + const balanceEntries = []; + for (let i = 0; i < results1.length; i++) { + // as both queries had ORDER BY, we should get the results in the same order + const row1 = results1[i]; + const row2 = results2[i]; + assert.strictEqual(row1.token_id, row2.token_id); + assert.strictEqual(row1.unlocked_balance + row1.locked_balance, row2.balance); + balanceEntries.push([ + walletId, + row1.token_id, + row1.total_received, + row1.unlocked_balance, + row1.locked_balance, + row1.timelock_expires, + row1.locked_authorities, + row1.unlocked_authorities, + row2.transactions, + ]); + } + if (balanceEntries.length > 0) { + await mysql.query( + `INSERT INTO \`wallet_balance\`(\`wallet_id\`, \`token_id\`, + \`total_received\`, + \`unlocked_balance\`, \`locked_balance\`, + \`timelock_expires\`, \`locked_authorities\`, + \`unlocked_authorities\`, \`transactions\`) + VALUES ?`, + [balanceEntries], + ); + } +}; + +/** + * Update a wallet's balance and tx history with a new transaction. + * + * @remarks + * When a new transaction arrives, it can change the balance and tx history for the wallets. This function + * updates the wallet_balance and wallet_tx_history tables with information from this transaction. + * + * @param mysql - Database connection + * @param txId - Transaction id + * @param timestamp - Transaction timestamp + * @param walletBalanceMap - Map with the transaction's balance for each wallet (by walletId) + */ +export const updateWalletTablesWithTx = async ( + mysql: ServerlessMysql, + txId: string, + timestamp: number, + walletBalanceMap: StringMap, +): Promise => { + const entries = []; + for (const [walletId, tokenBalanceMap] of Object.entries(walletBalanceMap)) { + for (const [token, tokenBalance] of tokenBalanceMap.iterator()) { + // on wallet_balance table, balance cannot be negative (it's unsigned). That's why we use balance + // as (tokenBalance < 0 ? 0 : tokenBalance). In case the wallet's balance in this tx is negative, + // there must necessarily be an entry already and we'll fall on the ON DUPLICATE KEY case, so the + // entry value won't be used. We'll just update balance = balance + tokenBalance + const entry = { + wallet_id: walletId, + token_id: token, + // totalAmountSent is the sum of the value of all outputs of this token on the tx being sent to this address + // which means it is the "total_received" for this wallet + total_received: tokenBalance.totalAmountSent, + unlocked_balance: (tokenBalance.unlockedAmount < 0 ? 0 : tokenBalance.unlockedAmount), + locked_balance: tokenBalance.lockedAmount, + unlocked_authorities: tokenBalance.unlockedAuthorities.toUnsignedInteger(), + locked_authorities: tokenBalance.lockedAuthorities.toUnsignedInteger(), + timelock_expires: tokenBalance.lockExpires, + transactions: 1, + }; + + // save the smaller value of timelock_expires, when not null + await mysql.query( + `INSERT INTO wallet_balance + SET ? + ON DUPLICATE KEY + UPDATE total_received = total_received + ?, + unlocked_balance = unlocked_balance + ?, + locked_balance = locked_balance + ?, + transactions = transactions + 1, + timelock_expires = CASE WHEN timelock_expires IS NULL THEN VALUES(timelock_expires) + WHEN VALUES(timelock_expires) IS NULL THEN timelock_expires + ELSE LEAST(timelock_expires, VALUES(timelock_expires)) + END, + unlocked_authorities = (unlocked_authorities | VALUES(unlocked_authorities)), + locked_authorities = locked_authorities | VALUES(locked_authorities)`, + [entry, tokenBalance.totalAmountSent, tokenBalance.unlockedAmount, tokenBalance.lockedAmount, walletId, token], + ); + + // same logic here as in the updateAddressTablesWithTx function + if (tokenBalance.unlockedAuthorities.hasNegativeValue()) { + // If we got here, it means that we spent an authority, so we need to update the table to refresh the current + // value. + // To do that, we get all unlocked_authorities from all addresses (querying by wallet and token_id) and + // bitwise OR them with each other. + await mysql.query( + `UPDATE \`wallet_balance\` + SET \`unlocked_authorities\` = ( + SELECT BIT_OR(\`unlocked_authorities\`) + FROM \`address_balance\` + WHERE \`address\` IN ( + SELECT \`address\` + FROM \`address\` + WHERE \`wallet_id\` = ?) + AND \`token_id\` = ?) + WHERE \`wallet_id\` = ? + AND \`token_id\` = ?`, + [walletId, token, walletId, token], + ); + } + + entries.push([walletId, token, txId, tokenBalance.total(), timestamp]); + } + } + + if (entries.length > 0) { + await mysql.query( + `INSERT INTO \`wallet_tx_history\` (\`wallet_id\`, \`token_id\`, + \`tx_id\`, \`balance\`, + \`timestamp\`) + VALUES ?`, + [entries], + ); + } +}; + +/** + * Add a tx outputs to the utxo table. + * + * @remarks + * This function receives a list of outputs and supposes they're all from the same block + * or transaction. So if heighlock is set, it'll be set to all outputs. + * + * @param mysql - Database connection + * @param txId - Transaction id + * @param outputs - The transaction outputs + * @param heightlock - Block heightlock + */ +export const addUtxos = async ( + mysql: ServerlessMysql, + txId: string, + outputs: TxOutputWithIndex[], + heightlock: number = null, +): Promise => { + // outputs might be empty if we're destroying authorities + if (outputs.length === 0) return; + + const entries = outputs.map( + (output) => { + let authorities = 0; + let value = output.value; + + if (isAuthority(output.token_data)) { + authorities = value; + value = 0; + } + + return [ + txId, + output.index, + output.token, + value, + authorities, + output.decoded.address, + output.decoded.timelock, + heightlock, + output.locked, + ]; + }, + ); + + // we are safe to ignore duplicates because our transaction might have already been in the mempool + await mysql.query( + `INSERT INTO \`tx_output\` (\`tx_id\`, \`index\`, \`token_id\`, + \`value\`, \`authorities\`, \`address\`, + \`timelock\`, \`heightlock\`, \`locked\`) + VALUES ? + ON DUPLICATE KEY UPDATE tx_id=tx_id`, + [entries], + ); +}; + +/** + * Alias for addOrUpdateTx + * + * @remarks + * This method is simply an alias for addOrUpdateTx in the current implementation. + * + * @param mysql - Database connection + * @param txId - Transaction id + * @param timestamp - The transaction timestamp + * @param version - The transaction version + * @param weight - The transaction weight + */ +export const updateTx = async ( + mysql: ServerlessMysql, + txId: string, + height: number, + timestamp: number, + version: number, + weight: number, +): Promise => addOrUpdateTx(mysql, txId, height, timestamp, version, weight); + +/** + * Add a tx to the transaction table. + * + * @remarks + * This method adds a transaction to the transaction table + * + * @param mysql - Database connection + * @param txId - Transaction id + * @param timestamp - The transaction timestamp + * @param version - The transaction version + * @param weight - the transaction weight + */ +export const addOrUpdateTx = async ( + mysql: ServerlessMysql, + txId: string, + height: number, + timestamp: number, + version: number, + weight: number, +): Promise => { + const entries = [[txId, height, timestamp, version, weight]]; + + await mysql.query( + `INSERT INTO \`transaction\` (tx_id, height, timestamp, version, weight) + VALUES ? + ON DUPLICATE KEY UPDATE height = ?`, + [entries, height], + ); +}; + +/** + * Remove a tx inputs from the utxo table. + * + * @param mysql - Database connection + * @param inputs - The transaction inputs + * @param txId - The transaction that spent these utxos + */ +export const updateTxOutputSpentBy = async (mysql: ServerlessMysql, inputs: TxInput[], txId: string): Promise => { + const entries = inputs.map((input) => [input.tx_id, input.index]); + // entries might be empty if there are no inputs + if (entries.length) { + // get the rows before deleting + + /* We are forcing this query to use the PRIMARY index because MySQL is not using the index when there is + * more than 185 elements in the IN query. I couldn't find a reason for that. Here is the EXPLAIN with exactly 185 + * elements: + * +----+-------------+-----------+------------+-------+---------------+---------+---------+-------------+------+ + * | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | + * +----+-------------+-----------+------------+-------+---------------+---------+---------+-------------+------+ + * | 1 | UPDATE | tx_output | NULL | range | PRIMARY | PRIMARY | 259 | const,const | 250 | + * +----+-------------+-----------+------------+-------+---------------+---------+---------+-------------+------+ + * + * And here is the EXPLAIN query with exactly 186 elements: + * +----+-------------+-----------+------------+-------+---------------+---------+---------+------+---------+ + * | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | + * +----+-------------+-----------+------------+-------+---------------+---------+---------+------+---------+ + * | 1 | UPDATE | tx_output | NULL | index | NULL | PRIMARY | 259 | NULL | 1933979 | + * +----+-------------+-----------+------------+-------+---------------+---------+---------+------+---------+ + */ + const result: OkPacket = await mysql.query( + `UPDATE \`tx_output\` USE INDEX (PRIMARY) + SET \`spent_by\` = ? + WHERE (\`tx_id\` ,\`index\`) + IN (?)`, + [txId, entries], + ); + + assert.strictEqual( + result.affectedRows, + inputs.length, + new Error('Not all informed UTXOs had their spentBy updated'), + ); + } +}; + +/** + * Get the requested tx output. + * + * @param mysql - Database connection + * @param txId - The tx id to search + * @param index - The index to search + * @param skipSpent - Skip spent tx_output (if we want only utxos) + * @returns The requested tx_output or null if it is not found + */ +export const getTxOutput = async ( + mysql: ServerlessMysql, + txId: string, + index: number, + skipSpent: boolean, +): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`tx_id\` = ? + AND \`index\` = ? + ${skipSpent ? 'AND `spent_by` IS NULL' : ''} + AND \`voided\` = FALSE`, + [txId, index], + ); + + if (!results.length || results.length === 0) { + return null; + } + + const result = results[0]; + + const txOutput: DbTxOutput = mapDbResultToDbTxOutput(result); + + return txOutput; +}; + +/** + * Get a random valid authority UTXO for a given token + * + * @param mysql - Database connection + * @param tokenId - The token id to search authorities for + * @param authority - The authority to search for, can be one of (TOKEN_MINT_MASK, TOKEN_MELT_MASK) + * + * @returns The requested UTXO + */ +export const getAuthorityUtxo = async ( + mysql: ServerlessMysql, + tokenId: string, + authority: number, +): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`authorities\` = ? + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE + AND \`token_id\` = ? + LIMIT 1`, + [authority, tokenId], + ); + + if (!results.length || results.length === 0) { + return null; + } + + const result = results[0]; + const utxo: DbTxOutput = mapDbResultToDbTxOutput(result); + + return utxo; +}; + +/** + * Get the requested UTXOs. + * + * @param mysql - Database connection + * @param utxosKeys - Information about the queried UTXOs, including tx_id and index + * @returns A list of UTXOs with all their properties + */ +export const getUtxos = async ( + mysql: ServerlessMysql, + utxosInfo: IWalletInput[], +): Promise => { + const entries = utxosInfo.map((utxo) => [utxo.txId, utxo.index]); + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`tx_output\` USE INDEX (PRIMARY) + WHERE (\`tx_id\`, \`index\`) + IN (?) + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE`, + [entries], + ); + + const utxos = results.map(mapDbResultToDbTxOutput); + + return utxos; +}; + +/** + * Get a wallet's UTXOs, sorted by value. + * + * @remarks + * Locked and authority UTXOs are not considered. + * + * @param mysql - Database connection + * @param walletId - The wallet id + * @param token - The token id + * @returns A list of UTXOs with all their properties + */ +export const getWalletSortedValueUtxos = async ( + mysql: ServerlessMysql, + walletId: string, + tokenId: string, +): Promise => { + const utxos = []; + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`address\` + IN ( + SELECT \`address\` + FROM \`address\` + WHERE \`wallet_id\` = ? + ) + AND \`token_id\` = ? + AND \`authorities\` = 0 + AND \`locked\` = FALSE + AND \`tx_proposal\` IS NULL + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE + ORDER BY \`value\` + DESC`, + [walletId, tokenId], + ); + for (const result of results) { + const utxo: DbTxOutput = { + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + locked: result.locked > 0, + }; + utxos.push(utxo); + } + return utxos; +}; + +/** + * Mark UTXOs as unlocked. + * + * @param mysql - Database connection + * @param utxos - List of UTXOs to unlock + */ +export const unlockUtxos = async (mysql: ServerlessMysql, utxos: DbTxOutput[]): Promise => { + if (utxos.length === 0) return; + const entries = utxos.map((utxo) => [utxo.txId, utxo.index]); + await mysql.query( + `UPDATE \`tx_output\` + SET \`locked\` = FALSE + WHERE (\`tx_id\` ,\`index\`) + IN (?)`, + [entries], + ); +}; + +/** + * Get tx inputs that are still marked as locked. + * + * @remarks + * At first, it doesn't make sense to talk about locked inputs. Any UTXO can only be spent after + * it's unlocked. However, in this service, we have a "lazy" unlock policy, only unlocking the UTXOs + * when the wallet owner requests its balance. Therefore, we might receive a transaction with a UTXO + * that is sill marked as locked in our database. That might happen if the user sends his transaction + * using a service other than this one. Otherwise the locked amount would have been updated before + * sending. + * + * @param mysql - Database connection + * @param inputs - The transaction inputs + * @returns The locked UTXOs + */ +export const getLockedUtxoFromInputs = async (mysql: ServerlessMysql, inputs: TxInput[]): Promise => { + const entries = inputs.map((input) => [input.tx_id, input.index]); + // entries might be empty if there are no inputs + if (entries.length) { + // get the rows before deleting + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`tx_output\` USE INDEX (PRIMARY) + WHERE (\`tx_id\` ,\`index\`) + IN (?) + AND \`locked\` = TRUE + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE`, + [entries], + ); + + return results.map((utxo) => ({ + txId: utxo.tx_id as string, + index: utxo.index as number, + tokenId: utxo.token_id as string, + address: utxo.address as string, + value: utxo.value as number, + authorities: utxo.authorities as number, + timelock: utxo.timelock as number, + heightlock: utxo.heightlock as number, + locked: (utxo.locked > 0), + })); + } + + return []; +}; + +/** + * Update addresses tables with a new transaction. + * + * @remarks + * When a new transaction arrives, it will change the balance and tx history for addresses. This function + * updates the address, address_balance and address_tx_history tables with information from this transaction. + * + * @param mysql - Database connection + * @param txId - Transaction id + * @param timestamp - Transaction timestamp + * @param addressBalanceMap - Map with the transaction's balance for each address + */ +export const updateAddressTablesWithTx = async ( + mysql: ServerlessMysql, + txId: string, + timestamp: number, + addressBalanceMap: StringMap, +): Promise => { + /* + * update address table + * + * If an address is not yet present, add entry with index = null, walletId = null and transactions = 1. + * Later, when the corresponding wallet is started, index and walletId will be updated. + * + * If address is already present, just increment the transactions counter. + */ + const addressEntries = Object.keys(addressBalanceMap).map((address) => [address, 1]); + await mysql.query( + `INSERT INTO \`address\`(\`address\`, \`transactions\`) + VALUES ? + ON DUPLICATE KEY UPDATE transactions = transactions + 1`, + [addressEntries], + ); + + const entries = []; + for (const [address, tokenMap] of Object.entries(addressBalanceMap)) { + for (const [token, tokenBalance] of tokenMap.iterator()) { + // update address_balance table or update balance and transactions if there's an entry already + const entry = { + address, + token_id: token, + // totalAmountSent is the sum of the value of all outputs of this token on the tx being sent to this address + // which means it is the "total_received" for this address + total_received: tokenBalance.totalAmountSent, + // if it's < 0, there must be an entry already, so it will execute "ON DUPLICATE KEY UPDATE" instead of setting it to 0 + unlocked_balance: (tokenBalance.unlockedAmount < 0 ? 0 : tokenBalance.unlockedAmount), + // this is never less than 0, as locked balance only changes when a tx is unlocked + locked_balance: tokenBalance.lockedAmount, + unlocked_authorities: tokenBalance.unlockedAuthorities.toUnsignedInteger(), + locked_authorities: tokenBalance.lockedAuthorities.toUnsignedInteger(), + timelock_expires: tokenBalance.lockExpires, + transactions: 1, + }; + // save the smaller value of timelock_expires, when not null + await mysql.query( + `INSERT INTO address_balance + SET ? + ON DUPLICATE KEY + UPDATE total_received = total_received + ?, + unlocked_balance = unlocked_balance + ?, + locked_balance = locked_balance + ?, + transactions = transactions + 1, + timelock_expires = CASE + WHEN timelock_expires IS NULL THEN VALUES(timelock_expires) + WHEN VALUES(timelock_expires) IS NULL THEN timelock_expires + ELSE LEAST(timelock_expires, VALUES(timelock_expires)) + END, + unlocked_authorities = (unlocked_authorities | VALUES(unlocked_authorities)), + locked_authorities = locked_authorities | VALUES(locked_authorities)`, + [entry, tokenBalance.totalAmountSent, tokenBalance.unlockedAmount, tokenBalance.lockedAmount, address, token], + ); + + // if we're removing any of the authorities, we need to refresh the authority columns. Unlike the values, + // we cannot only sum/subtract, as authorities are binary: you have it or you don't. We might be spending + // an authority output in this tx without creating a new one, but it doesn't mean this address does not + // have this authority anymore, as it might have other authority outputs + if (tokenBalance.unlockedAuthorities.hasNegativeValue()) { + await mysql.query( + `UPDATE \`address_balance\` + SET \`unlocked_authorities\` = ( + SELECT BIT_OR(\`authorities\`) + FROM \`tx_output\` + WHERE \`address\` = ? + AND \`token_id\` = ? + AND \`locked\` = FALSE + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE + ) + WHERE \`address\` = ? + AND \`token_id\` = ?`, + [address, token, address, token], + ); + } + // for locked authorities, it doesn't make sense to perform the same operation. The authority needs to be + // unlocked before it can be spent. In case we're just adding new locked authorities, this will be taken + // care by the first sql query. + + // update address_tx_history with one entry for each pair (address, token) + entries.push([address, txId, token, tokenBalance.total(), timestamp]); + } + } + + await mysql.query( + `INSERT INTO \`address_tx_history\`(\`address\`, \`tx_id\`, + \`token_id\`, \`balance\`, + \`timestamp\`) + VALUES ?`, + [entries], + ); +}; + +/** + * Update the unlocked and locked balances for addresses. + * + * @remarks + * The balance of an address might change as a locked amount becomes unlocked. This function updates + * the address_balance table, subtracting from the locked column and adding to the unlocked column. + * + * @param mysql - Database connection + * @param addressBalanceMap - A map of addresses and the unlocked balances + * @param updateTimelock - If this update is triggered by a timelock expiring, update the next expire timestamp + */ +export const updateAddressLockedBalance = async ( + mysql: ServerlessMysql, + addressBalanceMap: StringMap, + updateTimelocks = false, +): Promise => { + for (const [address, tokenBalanceMap] of Object.entries(addressBalanceMap)) { + for (const [token, tokenBalance] of tokenBalanceMap.iterator()) { + await mysql.query( + `UPDATE \`address_balance\` + SET \`unlocked_balance\` = \`unlocked_balance\` + ?, + \`locked_balance\` = \`locked_balance\` - ?, + \`unlocked_authorities\` = (unlocked_authorities | ?) + WHERE \`address\` = ? + AND \`token_id\` = ?`, [ + tokenBalance.unlockedAmount, + tokenBalance.unlockedAmount, + tokenBalance.unlockedAuthorities.toInteger(), + address, + token, + ], + ); + + // if any authority has been unlocked, we have to refresh the locked authorities + if (tokenBalance.unlockedAuthorities.toInteger() > 0) { + await mysql.query( + `UPDATE \`address_balance\` + SET \`locked_authorities\` = ( + SELECT BIT_OR(\`authorities\`) + FROM \`tx_output\` + WHERE \`address\` = ? + AND \`token_id\` = ? + AND \`locked\` = TRUE + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE) + WHERE \`address\` = ? + AND \`token_id\` = ?`, + [address, token, address, token], + ); + } + + // if this is being unlocked due to a timelock, also update the timelock_expires column + if (updateTimelocks) { + await mysql.query(` + UPDATE \`address_balance\` + SET \`timelock_expires\` = ( + SELECT MIN(\`timelock\`) + FROM \`tx_output\` + WHERE \`address\` = ? + AND \`token_id\` = ? + AND \`locked\` = TRUE + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE + ) + WHERE \`address\` = ? + AND \`token_id\` = ?`, + [address, token, address, token]); + } + } + } +}; + +/** + * Update the unlocked and locked balances for wallets. + * + * @remarks + * The balance of a wallet might change as a locked amount becomes unlocked. This function updates + * the wallet_balance table, subtracting from the locked column and adding to the unlocked column. + * + * @param mysql - Database connection + * @param walletBalanceMap - A map of walletId and the unlocked balances + * @param updateTimelocks - If this update is triggered by a timelock expiring, update the next lock expiration + */ +export const updateWalletLockedBalance = async ( + mysql: ServerlessMysql, + walletBalanceMap: StringMap, + updateTimelocks = false, +): Promise => { + for (const [walletId, tokenBalanceMap] of Object.entries(walletBalanceMap)) { + for (const [token, tokenBalance] of tokenBalanceMap.iterator()) { + await mysql.query( + `UPDATE \`wallet_balance\` + SET \`unlocked_balance\` = \`unlocked_balance\` + ?, + \`locked_balance\` = \`locked_balance\` - ?, + \`unlocked_authorities\` = (\`unlocked_authorities\` | ?) + WHERE \`wallet_id\` = ? + AND \`token_id\` = ?`, + [tokenBalance.unlockedAmount, tokenBalance.unlockedAmount, + tokenBalance.unlockedAuthorities.toInteger(), walletId, token], + ); + + // if any authority has been unlocked, we have to refresh the locked authorities + if (tokenBalance.unlockedAuthorities.toInteger() > 0) { + await mysql.query( + `UPDATE \`wallet_balance\` + SET \`locked_authorities\` = ( + SELECT BIT_OR(\`locked_authorities\`) + FROM \`address_balance\` + WHERE \`address\` IN ( + SELECT \`address\` + FROM \`address\` + WHERE \`wallet_id\` = ?) + AND \`token_id\` = ?) + WHERE \`wallet_id\` = ? + AND \`token_id\` = ?`, + [walletId, token, walletId, token], + ); + } + + // if this is being unlocked due to a timelock, also update the timelock_expires column + if (updateTimelocks) { + await mysql.query( + `UPDATE \`wallet_balance\` + SET \`timelock_expires\` = ( + SELECT MIN(\`timelock_expires\`) + FROM \`address_balance\` + WHERE \`address\` + IN ( + SELECT \`address\` + FROM \`address\` + WHERE \`wallet_id\` = ?) + AND \`token_id\` = ?) + WHERE \`wallet_id\` = ? AND \`token_id\` = ?`, + [walletId, token, walletId, token], + ); + } + } + } +}; + +/** + * Get a wallet's addresses. + * + * @param mysql - Database connection + * @param walletId - Wallet id + * @param filterAddresses - Optional parameter to filter addresses from the list + * @returns A list of addresses and their info (index and transactions) + */ +export const getWalletAddresses = async (mysql: ServerlessMysql, walletId: string, filterAddresses?: string[]): Promise => { + const addresses: AddressInfo[] = []; + const subQuery = filterAddresses ? ` + AND \`address\` IN (?) + ` : ''; + + const results: DbSelectResult = await mysql.query(` + SELECT * + FROM \`address\` + WHERE \`wallet_id\` = ? + ${subQuery} + ORDER BY \`index\` + ASC`, [walletId, filterAddresses]); + + for (const result of results) { + const address = { + address: result.address as string, + index: result.index as number, + transactions: result.transactions as number, + }; + addresses.push(address); + } + return addresses; +}; + +/** + * Get the empty addresses of a wallet after the last used address + * + * @param mysql - Database connection + * @param walletId - Wallet id + * @returns A list of addresses and their indexes + */ +export const getNewAddresses = async (mysql: ServerlessMysql, walletId: string): Promise => { + const addresses: ShortAddressInfo[] = []; + const resultsWallet: DbSelectResult = await mysql.query('SELECT * FROM `wallet` WHERE `id` = ?', walletId); + if (resultsWallet.length) { + const gapLimit = resultsWallet[0].max_gap as number; + const latestUsedIndex = resultsWallet[0].last_used_address_index as number; + // Select all addresses that are empty and the index is bigger than the last used address index + const results: DbSelectResult = await mysql.query(` + SELECT * + FROM \`address\` + WHERE \`wallet_id\` = ? + AND \`transactions\` = 0 + AND \`index\` > ? + ORDER BY \`index\` + ASC + LIMIT ?`, [walletId, latestUsedIndex, gapLimit]); + + for (const result of results) { + const index = result.index as number; + const address = { + address: result.address as string, + index, + addressPath: getAddressPath(index), + }; + addresses.push(address); + } + } + return addresses; +}; + +/** + * Get a wallet's balances. + * + * @remarks + * If tokenIds is given, get the balance for just those tokens. + * + * @param mysql - Database connection + * @param walletId - Wallet id + * @param tokenIds - A list of token ids + * @returns A list of balances. + */ +export const getWalletBalances = async (mysql: ServerlessMysql, walletId: string, tokenIds: string[] = []): Promise => { + const balances: WalletTokenBalance[] = []; + let subquery = 'SELECT * FROM `wallet_balance` WHERE `wallet_id` = ?'; + const params: unknown[] = [walletId]; + if (tokenIds.length > 0) { + subquery += ' AND `token_id` IN (?)'; + params.push(tokenIds); + } + + const query = ` + SELECT w.total_received AS total_received, + w.unlocked_balance AS unlocked_balance, + w.locked_balance AS locked_balance, + w.unlocked_authorities AS unlocked_authorities, + w.locked_authorities AS locked_authorities, + w.timelock_expires AS timelock_expires, + w.transactions AS transactions, + w.token_id AS token_id, + token.name AS name, + token.symbol AS symbol + FROM (${subquery}) w +INNER JOIN token ON w.token_id = token.id + `; + + const results: DbSelectResult = await mysql.query(query, params); + for (const result of results) { + const totalAmount = result.total_received as number; + const unlockedBalance = result.unlocked_balance as number; + const lockedBalance = result.locked_balance as number; + const unlockedAuthorities = new Authorities(result.unlocked_authorities as number); + const lockedAuthorities = new Authorities(result.locked_authorities as number); + const timelockExpires = result.timelock_expires as number; + + const balance = new WalletTokenBalance( + new TokenInfo(result.token_id as string, result.name as string, result.symbol as string), + new Balance(totalAmount, unlockedBalance, lockedBalance, timelockExpires, unlockedAuthorities, lockedAuthorities), + result.transactions as number, + ); + balances.push(balance); + } + + return balances; +}; + +/** + * Gets a list of tokens that a given wallet has ever interacted with + * + * @returns A list of tokens. + */ +export const getWalletTokens = async ( + mysql: ServerlessMysql, + walletId: string, +): Promise => { + const tokenList: string[] = []; + const results: DbSelectResult = await mysql.query( + `SELECT DISTINCT(token_id) + FROM \`wallet_tx_history\` + WHERE \`wallet_id\` = ?`, + [walletId], + ); + + for (const result of results) { + tokenList.push( result.token_id); + } + + return tokenList; +}; + +/** + * Get a wallet's transaction history for a token. + * + * @remarks + * Transactions are ordered by timestamp descending - i.e. most recent first. + * + * 'skip' determines how many transactions will be skipped from the beginning. + * + * 'count' determines how many transactions will be returned. + * + * @param mysql - Database connection + * @param walletId - Wallet id + * @param tokenId - Token id + * @param skip - Number of transactions to skip + * @param count - Number of transactions to return + * @returns A list of balances. + */ +export const getWalletTxHistory = async ( + mysql: ServerlessMysql, + walletId: string, + tokenId: string, + skip: number, + count: number, +): Promise => { + const history: TxTokenBalance[] = []; + const results: DbSelectResult = await mysql.query(` + SELECT wallet_tx_history.balance AS balance, + wallet_tx_history.timestamp AS timestamp, + wallet_tx_history.token_id AS token_id, + wallet_tx_history.tx_id AS tx_id, + wallet_tx_history.voided AS voided, + wallet_tx_history.wallet_id AS wallet_id, + transaction.version AS version + FROM wallet_tx_history +LEFT OUTER JOIN transaction ON transaction.tx_id = wallet_tx_history.tx_id + WHERE wallet_id = ? + AND token_id = ? + ORDER BY wallet_tx_history.timestamp + DESC + LIMIT ?, ?`, + [walletId, tokenId, skip, count]); + + for (const result of results) { + const tx: TxTokenBalance = { + txId: result.tx_id, + timestamp: result.timestamp, + voided: result.voided, + balance: result.balance, + version: result.version, + }; + history.push(tx); + } + return history; +}; + +/** + * Get the utxos that are locked at a certain height. + * + * @remarks + * UTXOs from blocks are locked by height. This function returns the ones that are locked at the given height. + * + * Also, these UTXOs might have a timelock. Even though this is not common, it is also considered. + * + * @param mysql - Database connection + * @param now - Current timestamp + * @param height - The block height queried + * @returns A list of UTXOs locked at the given height + */ +export const getUtxosLockedAtHeight = async ( + mysql: ServerlessMysql, + now: number, + height: number, +): Promise => { + const utxos = []; + if (height >= 0) { + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`heightlock\` = ? + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE + AND (\`timelock\` <= ? + OR \`timelock\` is NULL) + AND \`locked\` = 1`, + [height, now], + ); + for (const result of results) { + const utxo: DbTxOutput = { + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + locked: result.locked > 0, + }; + utxos.push(utxo); + } + } + return utxos; +}; + +/** + * Get UTXOs that can be unlocked for a given wallet. + * + * @remarks + * Get the UTXOs that are still marked as locked in the utxo table but whose locks (height and time) + * have already expired. + * + * @param mysql - Database connection + * @param walletId - The wallet's id + * @param now - The current timestamp + * @param currentHeight - Latest block height + * @returns The latest height + */ +export const getWalletUnlockedUtxos = async ( + mysql: ServerlessMysql, + walletId: string, + now: number, + currentHeight: number, +): Promise => { + const utxos = []; + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE (\`heightlock\` <= ? + OR \`heightlock\` is NULL) + AND (\`timelock\` <= ? + OR \`timelock\` is NULL) + AND \`locked\` = 1 + AND \`spent_by\` IS NULL + AND \`voided\` = FALSE + AND \`address\` IN ( + SELECT \`address\` + FROM \`address\` + WHERE \`wallet_id\` = ?)`, + [currentHeight, now, walletId], + ); + for (const result of results) { + const utxo: DbTxOutput = { + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + locked: result.locked > 0, + }; + utxos.push(utxo); + } + return utxos; +}; + +/** + * Update latest version_data on the database + * + * @param mysql - Database connection + * @param data - Latest version data to store + */ +export const updateVersionData = async (mysql: ServerlessMysql, data: FullNodeVersionData): Promise => { + const entry = { + id: 1, + timestamp: data.timestamp, + version: data.version, + network: data.network, + min_weight: data.minWeight, + min_tx_weight: data.minTxWeight, + min_tx_weight_coefficient: data.minTxWeightCoefficient, + min_tx_weight_k: data.minTxWeightK, + token_deposit_percentage: data.tokenDepositPercentage, + reward_spend_min_blocks: data.rewardSpendMinBlocks, + max_number_inputs: data.maxNumberInputs, + max_number_outputs: data.maxNumberOutputs, + }; + + await mysql.query( + 'INSERT INTO `version_data` SET ? ON DUPLICATE KEY UPDATE ?', + [entry, entry], + ); +}; + +/** + * Update latest version_check time + * + * @param mysql - Database connection + * @returns + */ +export const getVersionData = async (mysql: ServerlessMysql): Promise => { + const results: DbSelectResult = await mysql.query('SELECT * FROM `version_data` WHERE id = 1 LIMIT 1;'); + + if (results.length > 0) { + const data = results[0]; + + const entry: FullNodeVersionData = { + timestamp: data.timestamp as number, + version: data.version as string, + network: data.network as string, + minWeight: data.min_weight as number, + minTxWeight: data.min_tx_weight as number, + minTxWeightCoefficient: data.min_tx_weight_coefficient as number, + minTxWeightK: data.min_tx_weight_k as number, + tokenDepositPercentage: data.token_deposit_percentage as number, + rewardSpendMinBlocks: data.reward_spend_min_blocks as number, + maxNumberInputs: data.max_number_inputs as number, + maxNumberOutputs: data.max_number_outputs as number, + }; + + return entry; + } + + return null; +}; + +/** + * Get height info from database. + * + * @param mysql - Database connection + * @returns The latest height + */ +export const getLatestHeight = async (mysql: ServerlessMysql): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT \`height\` AS value + FROM \`transaction\` + WHERE version + IN (?) + ORDER BY height + DESC + LIMIT 1`, [BLOCK_VERSION], + ); + + if (results.length > 0 && results[0].value !== null) { + return results[0].value as number; + } + + // it should never come here, as genesis block should be added at startup + return 0; +}; + +/** + * Gets the best block from the database + * + * @param mysql - Database connection + * + * @returns The latest height + */ +export const getLatestBlockByHeight = async (mysql: ServerlessMysql): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`transaction\` + WHERE \`version\` IN (?) + ORDER BY height DESC + LIMIT 1`, [BLOCK_VERSION], + ); + + if (results.length > 0) { + return { + txId: results[0].tx_id as string, + height: results[0].height as number, + timestamp: results[0].timestamp as number, + }; + } + + return null; +}; + +/** + * Get block by height + * + * @param mysql - Database connection + * @param height - The height to query + * + * @returns The latest height + */ +export const getBlockByHeight = async (mysql: ServerlessMysql, height: number): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`transaction\` + WHERE \`height\` = ? + AND \`version\` IN (?) + LIMIT 1`, [height, BLOCK_VERSION], + ); + + if (results.length > 0) { + return { + txId: results[0].tx_id as string, + height: results[0].height as number, + timestamp: results[0].timestamp as number, + }; + } + + return null; +}; + +/** + * Store the token information. + * + * @param mysql - Database connection + * @param tokenId - The token's id + * @param tokenName - The token's name + * @param tokenSymbol - The token's symbol + */ +export const storeTokenInformation = async ( + mysql: ServerlessMysql, + tokenId: string, + tokenName: string, + tokenSymbol: string, +): Promise => { + const entry = { id: tokenId, name: tokenName, symbol: tokenSymbol }; + await mysql.query( + 'INSERT INTO `token` SET ?', + [entry], + ); +}; + +/** + * Get the token information. + * + * @param mysql - Database connection + * @param tokenId - The token's id + * @returns The token information (or null if id is not found) + */ +export const getTokenInformation = async ( + mysql: ServerlessMysql, + tokenId: string, +): Promise => { + const results: DbSelectResult = await mysql.query( + 'SELECT * FROM `token` WHERE `id` = ?', + [tokenId], + ); + if (results.length === 0) return null; + return new TokenInfo(tokenId, results[0].name as string, results[0].symbol as string); +}; + +/** + * Get the unused addresses for a wallet. + * + * @remarks + * An unsued address is an address with 0 transactions. Addresses are ordered by index, ascending. + * + * @param mysql - Database connection + * @param walletId - The wallet's id + * @returns List of unused addresses + */ +export const getUnusedAddresses = async (mysql: ServerlessMysql, walletId: string): Promise => { + const addresses = []; + const results: DbSelectResult = await mysql.query( + 'SELECT `address` FROM `address` WHERE `wallet_id` = ? AND `transactions` = 0 ORDER BY `index` ASC', + [walletId], + ); + + for (const entry of results) { + const address = entry.address as string; + addresses.push(address); + } + return addresses; +}; + +/** + * Mark the given UTXOs with the txProposalId. + * + * @param mysql - Database connection + * @param txProposalId - The transaction proposal id + * @param utxos - The UTXOs to be marked with the proposal id + */ +export const markUtxosWithProposalId = async (mysql: ServerlessMysql, txProposalId: string, utxos: DbTxOutput[]): Promise => { + const entries = utxos.map((utxo, index) => ([utxo.txId, utxo.index, '', '', 0, 0, null, null, false, txProposalId, index, null, 0])); + await mysql.query( + `INSERT INTO \`tx_output\` + VALUES ? + ON DUPLICATE KEY\ + UPDATE \`tx_proposal\` = VALUES(\`tx_proposal\`), + \`tx_proposal_index\` = VALUES(\`tx_proposal_index\`)`, + [entries], + ); +}; + +/** + * Create a tx proposal on the database. + * + * @param mysql - Database connection + * @param txProposalId - The transaction proposal id + * @param walletId - The wallet associated with this proposal + * @param now - The current timestamp + */ +export const createTxProposal = async ( + mysql: ServerlessMysql, + txProposalId: string, + walletId: string, + now: number, +): Promise => { + const entry = { id: txProposalId, wallet_id: walletId, status: TxProposalStatus.OPEN, created_at: now }; + await mysql.query( + 'INSERT INTO `tx_proposal` SET ?', + [entry], + ); +}; + +/** + * Update a list of tx proposals. + * + * @param mysql - Database connection + * @param txProposalIds - The transaction proposal ids + * @param now - The current timestamp + * @param status - The new status + */ +export const updateTxProposal = async ( + mysql: ServerlessMysql, + txProposalIds: string[], + now: number, + status: TxProposalStatus, +): Promise => { + await mysql.query(` + UPDATE \`tx_proposal\` + SET \`updated_at\` = ?, + \`status\` = ? + WHERE \`id\` IN (?)`, [ + now, + status, + txProposalIds, + ]); +}; + +/** + * Get a tx proposal. + * + * @param mysql - Database connection + * @param txProposalId - The transaction proposal id + * @param now - The current timestamp + */ +export const getTxProposal = async ( + mysql: ServerlessMysql, + txProposalId: string, +): Promise => { + const results: DbSelectResult = await mysql.query( + 'SELECT * FROM `tx_proposal` WHERE `id` = ?', + [txProposalId], + ); + if (results.length === 0) return null; + return { + id: txProposalId, + walletId: results[0].wallet_id as string, + status: results[0].status as TxProposalStatus, + createdAt: results[0].created_at as number, + updatedAt: results[0].updated_at as number, + }; +}; + +/** + * When a tx proposal is cancelled we must release the utxos to be used by others + * + * @param mysql - Database connection + * @param txProposalId - The transaction proposal id + */ +export const releaseTxProposalUtxos = async ( + mysql: ServerlessMysql, + txProposalIds: string[], +): Promise => { + const result: OkPacket = await mysql.query( + `UPDATE \`tx_output\` + SET \`tx_proposal\` = NULL, + \`tx_proposal_index\` = NULL + WHERE \`tx_proposal\` IN (?)`, + [txProposalIds], + ); + + assert.strictEqual( + result.affectedRows, + txProposalIds.length, + 'Not all utxos were correctly updated', + ); +}; + +/** + * Get txs after a given height + * + * @param mysql - Database connection + * @param height - The height to search + + * @returns A list of txs + */ +export const getTxsAfterHeight = async ( + mysql: ServerlessMysql, + height: number, +): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`transaction\` + WHERE \`height\` > ? + AND \`voided\` = FALSE`, + [height], + ); + + return getTxsFromDBResult(results); +}; + +/** + * Get a list of all tx outputs from transactions + * + * @param mysql - Database connection + * @param transactions - The list of transactions + + * @returns A list of tx outputs + */ +export const getTxOutputs = async ( + mysql: ServerlessMysql, + transactions: Tx[], +): Promise => { + const txIds = transactions.map((tx) => tx.txId); + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`tx_id\` IN (?)`, + [txIds], + ); + + const utxos = []; + for (const result of results) { + const utxo: DbTxOutput = { + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + locked: result.locked > 0, + txProposalId: result.tx_proposal as string, + txProposalIndex: result.tx_proposal_index as number, + spentBy: result.spent_by ? result.spent_by as string : null, + }; + utxos.push(utxo); + } + + return utxos; +}; + +/** + * Get a list of transactions from their txIds + * + * @param mysql - Database connection + * @param txIds - The list of transaction ids + + * @returns A list of transactions + */ +export const getTransactionsById = async ( + mysql: ServerlessMysql, + txIds: string[], +): Promise => { + if (txIds.length === 0) { + return []; + } + + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`transaction\` + WHERE \`tx_id\` IN (?) + AND \`voided\` = FALSE`, + [txIds], + ); + + return getTxsFromDBResult(results); +}; + +/** + * Get a list of tx outputs from their spent_by txId + * + * @param mysql - Database connection + * @param txIds - The list of transactions that spent the tx_outputs we are querying + + * @returns A list of tx_outputs + */ +export const getTxOutputsBySpent = async ( + mysql: ServerlessMysql, + txIds: string[], +): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`spent_by\` IN (?)`, + [txIds], + ); + + const utxos = []; + for (const result of results) { + const utxo: DbTxOutput = { + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + locked: result.locked > 0, + txProposalId: result.tx_proposal as string, + txProposalIndex: result.tx_proposal_index as number, + spentBy: result.spent_by ? result.spent_by as string : null, + }; + + utxos.push(utxo); + } + + return utxos; +}; + +/** + * Set a list of tx_outputs as unspent + * + * @param mysql - Database connection + * @param txOutputs - The list of tx_outputs to unspend + */ +export const unspendUtxos = async ( + mysql: ServerlessMysql, + txOutputs: DbTxOutput[], +): Promise => { + const txIdIndexList = txOutputs.map((txOutput) => [txOutput.txId, txOutput.index]); + + await mysql.query( + `UPDATE \`tx_output\` + SET \`spent_by\` = NULL + WHERE (\`tx_id\`, \`index\`) IN (?)`, + [txIdIndexList], + ); +}; + +/** + * Remove height from transactions we want to send back to the `mempool` + * + * @param mysql - Database connection + * @param txs - The list of transactions to remove height + */ +export const removeTxsHeight = async ( + mysql: ServerlessMysql, + txs: Tx[], +): Promise => { + const txIds = txs.map((tx) => tx.txId); + + await mysql.query( + `UPDATE \`transaction\` + SET \`height\` = NULL + WHERE \`tx_id\` IN (?)`, + [txIds], + ); +}; + +/** + * Deletes utxos from the tx_outputs table + * + * @param mysql - Database connection + * @param utxos - The list of utxos to delete from the database + */ +export const markUtxosAsVoided = async ( + mysql: ServerlessMysql, + utxos: DbTxOutput[], +): Promise => { + const txIds = utxos.map((tx) => tx.txId); + + await mysql.query(` + UPDATE \`tx_output\` + SET \`voided\` = TRUE + WHERE \`tx_id\` IN (?)`, + [txIds]); +}; + +/** + * Delete all blocks starting from a given height + * + * @param mysql - Database connection + * @param height - The height to start deleting from + */ +export const deleteBlocksAfterHeight = async ( + mysql: ServerlessMysql, + height: number, +): Promise => { + await mysql.query( + `DELETE FROM \`transaction\` + WHERE height > ? + AND version IN (?)`, + [height, BLOCK_VERSION], + ); +}; + +/** + * Marks transactions as voided on the database + * + * @param mysql - Database connection + * @param transactions - The list of transactions to remove from database + */ +export const markTxsAsVoided = async ( + mysql: ServerlessMysql, + transactions: Tx[], +): Promise => { + const txIds = transactions.map((tx) => tx.txId); + + await mysql.query( + `UPDATE \`transaction\` + SET \`voided\` = TRUE + WHERE \`tx_id\` IN (?)`, + [txIds], + ); +}; + +/** + * Remove all records from address_tx_history that belong to the transaction list + * + * @param mysql - Database connection + * @param transactions - The list of transactions to search + */ +export const markAddressTxHistoryAsVoided = async ( + mysql: ServerlessMysql, + transactions: Tx[], +): Promise => { + const txIds = transactions.map((tx) => tx.txId); + + await mysql.query( + `UPDATE \`address_tx_history\` + SET \`voided\` = TRUE + WHERE \`tx_id\` IN (?)`, + [txIds], + ); +}; + +/** + * Remove all records from wallet_tx_history that belong to the transaction list + * + * @param mysql - Database connection + * @param transactions - The list of transactions to search + */ +export const markWalletTxHistoryAsVoided = async ( + mysql: ServerlessMysql, + transactions: Tx[], +): Promise => { + const txIds = transactions.map((tx) => tx.txId); + + await mysql.query( + `UPDATE \`wallet_tx_history\` + SET \`voided\` = TRUE + WHERE \`tx_id\` IN (?)`, + [txIds], + ); +}; + +/** + * Rebuilds the address_balance table for the given addresses from + * the tx_output table + + * @param mysql - Database connection + * @param addresses - The list of addresses to rebuild + * @param txList - The list of affected transactions, to rebuild the transaction count + */ +export const rebuildAddressBalancesFromUtxos = async ( + mysql: ServerlessMysql, + addresses: string[], + txList: string[], +): Promise => { + if (txList.length === 0) { + // This should never happen, we should throw so the re-org is rolled back + // and an error is triggered for manual inspection + throw new Error('Attempted to rebuild address balances but no transactions were affected'); + } + // first we need to store the transactions count before deleting + const oldAddressTokenTransactions: DbSelectResult = await mysql.query( + `SELECT \`address\`, \`token_id\` AS tokenId, \`transactions\`, \`total_received\` as \`totalReceived\` + FROM \`address_balance\` + WHERE \`address\` IN (?)`, + [addresses], + ); + + // delete affected address_balances + await mysql.query( + `UPDATE \`address_balance\` + SET \`unlocked_balance\` = 0, + \`locked_balance\` = 0, + \`locked_authorities\` = 0, + \`unlocked_authorities\` = 0, + \`timelock_expires\` = NULL, + \`transactions\` = 0 + WHERE \`address\` IN (?)`, + [addresses], + ); + + // update address balances with unlocked utxos + await mysql.query(` + INSERT INTO address_balance ( + \`address\`, + \`token_id\`, + \`unlocked_balance\`, + \`locked_balance\`, + \`unlocked_authorities\`, + \`locked_authorities\`, + \`timelock_expires\`, + \`transactions\` + ) + SELECT address, + token_id, + SUM(\`value\`), -- unlocked_balance + 0, + BIT_OR(\`authorities\`), -- unlocked_authorities + 0, -- locked_authorities + NULL, -- timelock_expires + 0 -- transactions + FROM \`tx_output\` + WHERE spent_by IS NULL + AND voided = FALSE + AND locked = FALSE + AND address IN (?) + GROUP BY address, token_id + ON DUPLICATE KEY UPDATE + unlocked_balance = VALUES(unlocked_balance), + unlocked_authorities = VALUES(unlocked_authorities) + `, [addresses]); + + // update address balances with locked utxos + await mysql.query(` + INSERT INTO \`address_balance\` ( + \`address\`, + \`token_id\`, + \`unlocked_balance\`, + \`locked_balance\`, + \`locked_authorities\`, + \`timelock_expires\`, + \`transactions\` + ) + SELECT address, + token_id, + 0 AS unlocked_balance, + SUM(\`value\`) AS locked_balance, + BIT_OR(\`authorities\`) AS locked_authorities, + MIN(\`timelock\`) AS timelock_expires, + 0 -- transactions + FROM \`tx_output\` + WHERE spent_by IS NULL + AND voided = FALSE + AND locked = TRUE + AND address IN (?) + GROUP BY \`address\`, \`token_id\` + ON DUPLICATE KEY UPDATE + locked_balance = VALUES(locked_balance), + locked_authorities = VALUES(locked_authorities), + timelock_expires = VALUES(timelock_expires) + `, [addresses]); + + const addressTransactionCount: StringMap = await getAffectedAddressTxCountFromTxList(mysql, txList); + const addressTotalReceived: StringMap = await getAffectedAddressTotalReceivedFromTxList(mysql, txList); + const tokenTransactionCount: StringMap = await getAffectedTokenTxCountFromTxList(mysql, txList); + + const finalValues = oldAddressTokenTransactions.map(({ address, tokenId, transactions, totalReceived }) => { + const diffTransactions = addressTransactionCount[`${address}_${tokenId}`] || 0; + const diffTotalReceived = addressTotalReceived[`${address}_${tokenId}`] || 0; + + return [transactions as number - diffTransactions, totalReceived as number - diffTotalReceived, address, tokenId]; + }); + + // update address balances with the correct amount of transactions + // We have to run multiple updates because we don't want to insert new rows to the table (which would be done + // if we used the INSERT ... ON CONFLICT syntax) + for (const item of finalValues) { + await mysql.query(` + UPDATE \`address_balance\` + SET \`transactions\` = ?, + \`total_received\` = ? + WHERE \`address\` = ? + AND \`token_id\` = ? + `, item); + } + + // update token table with the correct amount of transactions + for (const token of Object.keys(tokenTransactionCount)) { + await mysql.query(` + UPDATE \`token\` + SET \`transactions\` = \`transactions\` - ? + WHERE \`id\` = ? + `, [tokenTransactionCount[token], token]); + } +}; + +/** + * Retrieves a transaction from the database given a txId + * + * @param mysql - Database connection + * @param txId - The transaction id to search for + */ +export const fetchTx = async ( + mysql: ServerlessMysql, + txId: string, +): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`transaction\` + WHERE \`tx_id\` = ? + AND \`voided\` = FALSE`, + [txId], + ); + + const txResult = getTxsFromDBResult(results); + return get(txResult, '[0]', null); +}; + +/** + * Retrieves a list of `AddressBalance`s from a list of addresses + * + * @param mysql - Database connection + * @param addresses - The addresses to query + */ +export const fetchAddressBalance = async ( + mysql: ServerlessMysql, + addresses: string[], +): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`address_balance\` + WHERE \`address\` IN (?) + ORDER BY \`address\`, \`token_id\``, + [addresses], + ); + + return results.map((result): AddressBalance => ({ + address: result.address as string, + tokenId: result.token_id as string, + unlockedBalance: result.unlocked_balance as number, + lockedBalance: result.locked_balance as number, + lockedAuthorities: result.locked_authorities as number, + unlockedAuthorities: result.unlocked_authorities as number, + timelockExpires: result.timelock_expires as number, + transactions: result.transactions as number, + })); +}; + +/** + * Retrieves a list of `AddressTotalBalance`s from a list of addresses + * + * @param mysql - Database connection + * @param addresses - The addresses to query + */ +export const fetchAddressTxHistorySum = async ( + mysql: ServerlessMysql, + addresses: string[], +): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT address, + token_id, + SUM(\`balance\`) AS balance, + COUNT(\`tx_id\`) AS transactions + FROM \`address_tx_history\` + WHERE \`address\` IN (?) + AND \`voided\` = FALSE + GROUP BY address, token_id + ORDER BY address, token_id`, + [addresses], + ); + + return results.map((result): AddressTotalBalance => ({ + address: result.address as string, + tokenId: result.token_id as string, + balance: result.balance as number, + transactions: result.transactions as number, + })); +}; + +/** + * Retrieves a filtered list of tx_outputs + * + * @param mysql - Database connection + * @param filters - Filters to apply on the tx_output query + */ +export const filterTxOutputs = async ( + mysql: ServerlessMysql, + filters: IFilterTxOutput = { addresses: [] }, +): Promise => { + const finalFilters = { + addresses: [], + tokenId: '00', + authority: 0, + ignoreLocked: false, + skipSpent: true, + biggerThan: -1, + smallerThan: constants.MAX_OUTPUT_VALUE + 1, + ...filters, + }; + + if (finalFilters.addresses.length === 0) { + throw new Error('Addresses can\'t be empty.'); + } + + const queryParams: any[] = [ + finalFilters.addresses, + finalFilters.tokenId, + ]; + + if (finalFilters.authority === 0) { + queryParams.push(finalFilters.smallerThan); + queryParams.push(finalFilters.biggerThan); + } else { + queryParams.push(finalFilters.authority); + } + + queryParams.push(finalFilters.maxOutputs); + + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`address\` + IN (?) + AND \`token_id\` = ? + ${finalFilters.authority !== 0 ? 'AND `authorities` & ? > 0' : 'AND `authorities` = 0'} + ${finalFilters.ignoreLocked ? 'AND `locked` = FALSE' : ''} + ${finalFilters.authority === 0 ? 'AND value < ?' : ''} + ${finalFilters.authority === 0 ? 'AND value > ?' : ''} + ${finalFilters.skipSpent ? 'AND `spent_by` IS NULL' : ''} + ${finalFilters.skipSpent ? 'AND `tx_proposal` IS NULL' : ''} + AND \`voided\` = FALSE + ORDER BY \`value\` DESC + ${finalFilters.maxOutputs ? 'LIMIT ?' : ''} + `, + queryParams, + ); + + const utxos: DbTxOutput[] = results.map(mapDbResultToDbTxOutput); + + return utxos; +}; + +/** + * Maps the result from the database to DbTxOutput + * + * @param results - The tx_output results from the database + * @returns A list of tx_outputs mapped to the DbTxOutput type + */ +export const mapDbResultToDbTxOutput = (result: any): DbTxOutput => ({ + txId: result.tx_id as string, + index: result.index as number, + tokenId: result.token_id as string, + address: result.address as string, + value: result.value as number, + authorities: result.authorities as number, + timelock: result.timelock as number, + heightlock: result.heightlock as number, + locked: result.locked > 0, + txProposalId: result.tx_proposal as string, + txProposalIndex: result.tx_proposal_index as number, + spentBy: result.spent_by as string, +}); + +/** + * Get tx proposal inputs. + * + * @remarks + * The inputs are taken from the utxo table. + * + * @param mysql - Database connection + * @param txProposalId - The transaction proposal id + * @returns A list of inputs. + */ +export const getTxProposalInputs = async ( + mysql: ServerlessMysql, + txProposalId: string, +): Promise => { + const inputs = []; + const results: DbSelectResult = await mysql.query( + 'SELECT * FROM `tx_output` WHERE `tx_proposal` = ? ORDER BY `tx_proposal_index` ASC', + [txProposalId], + ); + for (const result of results) { + const input: IWalletInput = { + txId: result.tx_id as string, + index: result.index as number, + }; + inputs.push(input); + } + return inputs; +}; + +/** + * Get mempool txs before a date + * + * @param mysql - Database connection + * @param date - The date to search for + + * @returns A list of txs + */ +export const getMempoolTransactionsBeforeDate = async ( + mysql: ServerlessMysql, + date: number, +): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT * + FROM \`transaction\` + WHERE \`timestamp\` < ? + AND \`voided\` = FALSE + AND \`height\` IS NULL`, + [date], + ); + + return getTxsFromDBResult(results); +}; + +/** + * Add a miner to the database + * + * @param mysql - Database connection + */ +export const addMiner = async ( + mysql: ServerlessMysql, + address: string, + txId: string, +): Promise => { + await mysql.query( + `INSERT INTO \`miner\` (address, first_block, last_block, count) + VALUES (?, ?, ?, 1) + ON DUPLICATE KEY UPDATE last_block = ?, count = count + 1`, + [address, txId, txId, txId], + ); +}; + +/** + * Get the list of miners on database + * + * @param mysql - Database connection + + * @returns A list of strings with miners addresses + */ +export const getMinersList = async ( + mysql: ServerlessMysql, +): Promise => { + const results: DbSelectResult = await mysql.query(` + SELECT address, first_block, last_block, count + FROM miner; + `); + + const minerList: Miner[] = []; + + for (const result of results) { + minerList.push({ + address: result.address as string, + firstBlock: result.first_block as string, + lastBlock: result.last_block as string, + count: result.count as number, + }); + } + + return minerList; +}; + +/** + * Get the total sum of a token's utxos, excluding the burned and voided ones + * + * @param mysql - Database connection + + * @returns The calculated sum + */ +export const getTotalSupply = async ( + mysql: ServerlessMysql, + tokenId: string, +): Promise => { + const results: DbSelectResult = await mysql.query(` + SELECT SUM(value) as value + FROM tx_output + WHERE spent_by IS NULL + AND token_id = ? + AND voided = FALSE + AND address != '${BURN_ADDRESS}' + `, [tokenId]); + + if (!results.length) { + // This should never happen. + await addAlert( + 'Total supply query returned no results', + '-', + Severity.MINOR, + { tokenId }, + ); + throw new Error('Total supply query returned no results'); + } + + return results[0].value as number; +}; + +/** + * Get from database utxos that must be unlocked because their timelocks expired + * + * @param mysql - Database connection + * @param now - Current timestamp + + * @returns A list of timelocked utxos + */ +export const getExpiredTimelocksUtxos = async ( + mysql: ServerlessMysql, + now: number, +): Promise => { + const results: DbSelectResult = await mysql.query(` + SELECT * + FROM tx_output + WHERE locked = TRUE + AND timelock IS NOT NULL + AND timelock < ? + `, [now]); + + const lockedUtxos: DbTxOutput[] = results.map(mapDbResultToDbTxOutput); + + return lockedUtxos; +}; + +/** + * Get the total sum of transactions for a given tokenId + * + * @param mysql - Database connection + * @param tokenId - The token id to fetch transactions + + * @returns The calculated total sum of transactions + */ +export const getTotalTransactions = async ( + mysql: ServerlessMysql, + tokenId: string, +): Promise => { + const results: DbSelectResult = await mysql.query(` + SELECT COUNT(DISTINCT(tx_id)) AS count + FROM address_tx_history + WHERE token_id = ? + AND voided = FALSE + `, [tokenId]); + + if (!results.length) { + // This should never happen. + await addAlert( + 'Total transactions query returned no results', + '-', + Severity.MINOR, + { tokenId }, + ); + throw new Error('Total transactions query returned no results'); + } + + return results[0].count as number; +}; + +/** + * Get the available authority utxos for a given token + * + * @param mysql - Database connection + * @param tokenId - The token id to fetch authorities + + * @returns A list of authority utxos + */ +export const getAvailableAuthorities = async ( + mysql: ServerlessMysql, + tokenId: string, +): Promise => { + /* We should set the LIMIT to a reasonable value to prevent users from abusing + * this API by creating thousands of authority outputs and querying this + * + * Currently the only use for this query is on the wallet-desktop to display + * if the token is "mintable" and/or"meltable", we don't display a list of those + * utxos so it is safe to set this limit. + */ + const results: DbSelectResult = await mysql.query(` + SELECT * + FROM tx_output + WHERE authorities > 0 + AND token_id = ? + AND voided = FALSE + AND locked = FALSE + AND spent_by IS NULL + `, [tokenId]); + + const utxos = results.map(mapDbResultToDbTxOutput); + + return utxos; +}; + +/** + * Get the number of transactions for each token from the address_tx_history table + * given a list of transactions + * + * @param mysql - Database connection + * @param txList - A list of affected transactions to get the addresses token transaction count + + * @returns A Map with address_tokenId as key and the transaction count as values + */ +export const getAffectedAddressTxCountFromTxList = async ( + mysql: ServerlessMysql, + txList: string[], +): Promise> => { + const results: DbSelectResult = await mysql.query(` + SELECT address, COUNT(DISTINCT(tx_id)) AS txCount, token_id as tokenId + FROM address_tx_history + WHERE tx_id IN (?) + AND voided = TRUE + GROUP BY address, token_id + `, [txList]); + + const addressTransactions = results.reduce((acc, result) => { + const address = result.address as string; + const txCount = result.txCount as number; + const tokenId = result.tokenId as string; + + acc[`${address}_${tokenId}`] = txCount; + + return acc; + }, {}); + + return addressTransactions as StringMap; +}; + +/** + * Get the number of affected transactions for each token from the address_tx_history table + * given a list of transactions + * + * @param mysql - Database connection + * @param txList - A list of affected transactions to get the token tx count + + * @returns A Map with tokenId as key and the transaction count as values + */ +export const getAffectedTokenTxCountFromTxList = async ( + mysql: ServerlessMysql, + txList: string[], +): Promise> => { + const results: DbSelectResult = await mysql.query(` + SELECT token_id AS tokenId, COUNT(DISTINCT(tx_id)) AS txCount + FROM address_tx_history + WHERE tx_id IN (?) + AND voided = TRUE + GROUP BY token_id + `, [txList]); + + const tokenTransactions = results.reduce((acc, result) => { + const tokenId = result.tokenId as string; + const txCount = result.txCount as number; + + acc[tokenId] = txCount; + + return acc; + }, {}); + + return tokenTransactions as StringMap; +}; + +/** + * Get the affected total_received for each address/token pair given a list of transactions + * + * @param mysql - Database connection + * @param txList - A list of affected transactions + + * @returns {Promise>} A Map with address_tokenId as key and the affected total_received as values + */ +export const getAffectedAddressTotalReceivedFromTxList = async ( + mysql: ServerlessMysql, + txList: string[], +): Promise> => { + const results: DbSelectResult = await mysql.query(` + SELECT address, token_id as tokenId, SUM(value) as total + FROM tx_output + WHERE tx_id IN (?) + AND voided = TRUE + GROUP BY address, token_id + `, [txList]); + + const addressTotalReceivedMap = results.reduce((acc, result) => { + const address = result.address as string; + const total = result.total as number; + const tokenId = result.tokenId as string; + + acc[`${address}_${tokenId}`] = total; + + return acc; + }, {}); + + return addressTotalReceivedMap as StringMap; +}; + +/** + * Increment a list of tokens transactions count + * + * @param mysql - Database connection + * @param tokenList - The list of tokens to increment + */ +export const incrementTokensTxCount = async ( + mysql: ServerlessMysql, + tokenList: string[], +): Promise => { + await mysql.query(` + UPDATE \`token\` + SET \`transactions\` = \`transactions\` + 1 + WHERE \`id\` IN (?) + `, [tokenList]); +}; + +/** + * Verify the existence of a device registered for a given wallet. + * + * @param mysql - Database connection + * @param deviceId - The device to verify existence + * @param walletId - The wallet linked to device + */ +export const existsPushDevice = async ( + mysql: ServerlessMysql, + deviceId: string, + walletId: string, +) : Promise => { + const [{ count }] = await mysql.query( + ` + SELECT COUNT(1) as \`count\` + FROM \`push_devices\` pd + WHERE device_id = ? + AND wallet_id = ?`, + [deviceId, walletId], + ) as unknown as Array<{count}>; + + return count > 0; +}; + +/** + * Register a device to a wallet for push notification. + * + * @param mysql - Database connection + * @param input - Input of push device register + */ +export const registerPushDevice = async ( + mysql: ServerlessMysql, + input: { + deviceId: string, + walletId: string, + pushProvider: string, + enablePush: boolean, + enableShowAmounts: boolean, + }, +) : Promise => { + await mysql.query( + ` + INSERT + INTO \`push_devices\` ( + device_id + , wallet_id + , push_provider + , enable_push + , enable_show_amounts) + VALUES (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + updated_at = CURRENT_TIMESTAMP`, + [input.deviceId, input.walletId, input.pushProvider, input.enablePush, input.enableShowAmounts], + ); +}; + +/** + * Remove any record of push notification device given a device ID. + * + * @param mysql - Database connection + * @param deviceId - The device ID + */ +export const removeAllPushDevicesByDeviceId = async (mysql: ServerlessMysql, deviceId: string): Promise => { + await mysql.query( + ` + DELETE + FROM \`push_devices\` + WHERE + device_id = ? + `, + [deviceId], + ); +}; + +/** + * Update existing push device given a wallet. + * + * @param mysql - Database connection + * @param input - Input of push device register + */ +export const updatePushDevice = async ( + mysql: ServerlessMysql, + input: { + deviceId: string, + walletId: string, + enablePush: boolean, + enableShowAmounts: boolean, + }, +) : Promise => { + await mysql.query( + ` + UPDATE \`push_devices\` + SET enable_push = ? + , enable_show_amounts = ? + WHERE device_id = ? + AND wallet_id = ?`, + [input.enablePush, input.enableShowAmounts, input.deviceId, input.walletId], + ); +}; + +/** + * Unregister push device for a given wallet. + * + * @param mysql - Database connection + * @param deviceId - The device to unregister + * @param walletId - The wallet linked to device + */ +export const unregisterPushDevice = async ( + mysql: ServerlessMysql, + deviceId: string, + walletId?: string, +) : Promise => { + if (walletId) { + await mysql.query( + ` + DELETE + FROM \`push_devices\` + WHERE device_id = ? + AND wallet_id = ?`, + [deviceId, walletId], + ); + } else { + await mysql.query( + ` + DELETE + FROM \`push_devices\` + WHERE device_id = ?`, + [deviceId], + ); + } +}; + +/** + * Get a transaction by its ID. + * + * @param mysql - Database connection + * @param txId - A transaction ID + * @param walletId - The wallet related to the transaction + * @returns A list of tokens for a transaction if found, return an empty list otherwise + */ +export const getTransactionById = async ( + mysql: ServerlessMysql, + txId: string, + walletId: string, +): Promise => { + const result = await mysql.query(` + SELECT + transaction.tx_id AS tx_id + , transaction.timestamp AS timestamp + , transaction.version AS version + , transaction.voided AS voided + , transaction.height AS height + , transaction.weight AS weight + , wallet_tx_history.balance AS balance + , wallet_tx_history.token_id AS token_id + , token.name AS name + , token.symbol AS symbol + FROM wallet_tx_history + INNER JOIN transaction ON transaction.tx_id = wallet_tx_history.tx_id + INNER JOIN token ON wallet_tx_history.token_id = token.id + WHERE transaction.tx_id = ? + AND transaction.voided = FALSE + AND wallet_tx_history.wallet_id = ?`, + // eslint-disable-next-line camelcase + [txId, walletId]) as Array<{tx_id, timestamp, version, voided, weight, balance, token_id, name, symbol }>; + + const txTokens = []; + result.forEach((eachTxToken) => { + const txToken = { + txId: eachTxToken.tx_id, + timestamp: eachTxToken.timestamp, + version: eachTxToken.version, + voided: !!eachTxToken.voided, + weight: eachTxToken.weight, + balance: eachTxToken.balance, + tokenId: eachTxToken.token_id, + tokenName: eachTxToken.name, + tokenSymbol: eachTxToken.symbol, + } as TxByIdToken; + txTokens.push(txToken); + }); + + return txTokens; +}; + +/** +* Verify the existence of a wallet by its ID. +* +* @param mysql - Database connection +* @param walletId - The wallet linked to device +*/ +export const existsWallet = async ( + mysql: ServerlessMysql, + walletId: string, +) : Promise => { + const [{ count }] = (await mysql.query( + ` + SELECT COUNT(1) as \`count\` + FROM \`wallet\` pd + WHERE id = ?`, + [walletId], + )) as unknown as Array<{ count }>; + + return count > 0; +}; + +/** + * Get registered push device by deviceId. + * + * @param mysql - Database connection + * @param deviceId - The device to verify existence + */ +export const getPushDevice = async ( + mysql: ServerlessMysql, + deviceId: string, +) : Promise => { + const [pushDevice] = await mysql.query( + ` + SELECT * + FROM \`push_devices\` + WHERE device_id = ?`, + [deviceId], + // eslint-disable-next-line camelcase + ) as Array<{wallet_id, device_id, push_provider, enable_push, enable_show_amounts}>; + + if (!pushDevice) { + return null; + } + + return { + walletId: pushDevice.wallet_id, + deviceId: pushDevice.device_id, + pushProvider: pushDevice.push_provider, + enablePush: !!pushDevice.enable_push, + enableShowAmounts: !!pushDevice.enable_show_amounts, + } as PushDevice; +}; + +/** + * Get a push device settings list given a list of wallet ids. + * + * @param mysql - Database connection + * @param walletIdList - A list of wallet ids + * @returns - a list of push device settings + */ +export const getPushDeviceSettingsList = async ( + mysql: ServerlessMysql, + walletIdList: string[], +) : Promise => { + const pushDeviceSettingsResult = await mysql.query( + ` + SELECT wallet_id + , device_id + , enable_push + , enable_show_amounts + FROM \`push_devices\` + WHERE wallet_id in (?)`, + [walletIdList], + // eslint-disable-next-line camelcase + ) as Array<{wallet_id, device_id, enable_push, enable_show_amounts}>; + + const pushDeviceSettignsList = pushDeviceSettingsResult.map((each) => ({ + walletId: each.wallet_id, + deviceId: each.device_id, + enablePush: !!each.enable_push, + enableShowAmounts: !!each.enable_show_amounts, + } as PushDeviceSettings)); + + return pushDeviceSettignsList; +}; + +/** + * Count the quantity of stale push devices from now. + * + * @param mysql - Database connection + * @returns - total of stale device from now + */ +export const countStalePushDevices = async (mysql): Promise => { + const [{ count }] = await mysql.query( + ` + SELECT COUNT(device_id) as count + FROM \`push_devices\` + WHERE UNIX_TIMESTAMP(updated_at) < UNIX_TIMESTAMP(date_sub(now(), interval 1 month))`, + ) as Array<{ count }>; + return count; +}; + +/** + * Delete stale push devices from now. + * + * @param mysql - Database connection + */ +export const deleteStalePushDevices = async (mysql) => { + await mysql.query( + ` + DELETE + FROM \`push_devices\` + WHERE UNIX_TIMESTAMP(updated_at) < UNIX_TIMESTAMP(date_sub(now(), interval 1 month))`, + ); +}; + +/** + * Get token symbol map, correlating token id to its symbol. + * + * @param mysql - Database connection + * @param tokenIdList - A list of token id + * @returns The token information (or null if id is not found) + */ +export const getTokenSymbols = async ( + mysql: ServerlessMysql, + tokenIdList: string[], +): Promise> => { + if (tokenIdList.length === 0) return null; + + const results: DbSelectResult = await mysql.query( + 'SELECT `id`, `symbol` FROM `token` WHERE `id` IN (?)', + [tokenIdList], + ); + + if (results.length === 0) return null; + return results.reduce((prev: Record, token: { id: string, symbol: string}) => { + // eslint-disable-next-line no-param-reassign + prev[token.id] = token.symbol; + return prev; + }, {}) as unknown as StringMap; +}; + +/** + * Fetches all txProposals that are either in the OPEN, SEND_ERROR or CANCELLED status + * + * @param mysql - Database connection + * + * @returns The number of txProposals before a given date + */ +export const getUnsentTxProposals = async ( + mysql: ServerlessMysql, + txProposalsBefore: number, +): Promise => { + const result = await mysql.query<{ id: string }[]>( + ` + SELECT id + FROM \`tx_proposal\` + WHERE created_at < ? + AND status IN (?)`, + [txProposalsBefore, [ + TxProposalStatus.OPEN, + TxProposalStatus.SEND_ERROR, + TxProposalStatus.CANCELLED, + ]], + ); + + return result.map((row) => row.id); +}; + +/** + * Gets a specific address from an index and a walletId + * + * @param mysql - Database connection + * @param walletId - The wallet id to search for + * @param index - The address index to search for + * + * @returns An object containing the address, its index and the number of transactions + */ +export const getAddressAtIndex = async ( + mysql: ServerlessMysql, + walletId: string, + index: number, +): Promise => { + const addresses = await mysql.query( + ` + SELECT \`address\`, \`index\`, \`transactions\` + FROM \`address\` pd + WHERE \`index\` = ? + AND \`wallet_id\` = ? + LIMIT 1`, + [walletId, index], + ); + + if (addresses.length <= 0) { + return null; + } + + return { + address: addresses[0].address as string, + index: addresses[0].index as number, + transactions: addresses[0].transactions as number, + } as AddressInfo; +}; diff --git a/packages/wallet-service/src/db/utils.ts b/packages/wallet-service/src/db/utils.ts new file mode 100644 index 00000000..df7f3442 --- /dev/null +++ b/packages/wallet-service/src/db/utils.ts @@ -0,0 +1,189 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* eslint-disable max-classes-per-file */ +import { ServerlessMysql } from 'serverless-mysql'; +import { getWalletId } from '@src/utils'; +import { + WalletStatus, + Wallet, + Tx, + DbSelectResult, + TokenBalanceMap, + TokenBalanceValue, + WalletBalanceValue, + StringMap, + WalletBalance, +} from '@src/types'; + +/** + * Begins a transaction on the current connection + * + * @param mysql - Database connection + */ +export const beginTransaction = async ( + mysql: ServerlessMysql, +): Promise => { + await mysql.query('START TRANSACTION'); +}; + +/** + * Commits the transaction opened on the current connection + * + * @param mysql - Database connection + */ +export const commitTransaction = async ( + mysql: ServerlessMysql, +): Promise => { + await mysql.query('COMMIT'); +}; + +/** + * Rollback the transaction opened on the current connection + * + * @param mysql - Database connection + */ +export const rollbackTransaction = async ( + mysql: ServerlessMysql, +): Promise => { + await mysql.query('ROLLBACK'); +}; + +/* eslint-disable-next-line @typescript-eslint/ban-types */ +export async function transactionDecorator(_mysql: ServerlessMysql, wrapped: Function): Promise { + return async function wrapper(...args) { + try { + await beginTransaction(_mysql); + await wrapped.apply(this, args); + await commitTransaction(_mysql); + } catch (e) { + await rollbackTransaction(_mysql); + + // propagate the error + throw e; + } + }; +} + +/** + * Returns a Wallet object from a db result row + * + * @param result - The result row to map to a Wallet object + */ +export const getWalletFromDbEntry = (entry: Record): Wallet => ({ + walletId: getWalletId(entry.xpubkey as string), + xpubkey: entry.xpubkey as string, + authXpubkey: entry.auth_xpubkey as string, + status: entry.status as WalletStatus, + retryCount: entry.retry_count as number, + maxGap: entry.max_gap as number, + createdAt: entry.created_at as number, + readyAt: entry.ready_at as number, +}); + +/** + * Receive a DbSelectResult with multiple records and transform it in an array of Tx + * + * @param results + * @returns Txs converted from DbSelectResult + */ +export const getTxsFromDBResult = (results: DbSelectResult): Tx[] => { + const transactions = []; + + for (const result of results) { + const tx: Tx = _mapTxRecord2Tx(result); + + transactions.push(tx); + } + + return transactions; +}; + +/** + * Receive a DbSelectResult with one record and transform it in a Tx + * + * @param results + * @returns Tx converted from DbSelectResult + */ +export const getTxFromDBResult = (result: DbSelectResult): Tx => { + const { 0: row } = result; + return _mapTxRecord2Tx(row); +}; + +const _mapTxRecord2Tx = (record: Record): Tx => ( + { + txId: record.tx_id as string, + timestamp: record.timestamp as number, + version: record.version as number, + voided: record.voided === 1, + height: record.height as number, + weight: record.weight as number, + } +); + +export class FromTokenBalanceMapToBalanceValueList { + /** + * Convert the map of token balance instance into a map of token balance value. + * It also hydrate each token balance value with token symbol. + * + * @param tokenBalanceMap - Map of token balance instance + * @param tokenSymbolsMap - Map token's id to its symbol + * @returns a map of token balance value + */ + static convert(tokenBalanceMap: TokenBalanceMap, tokenSymbolsMap: StringMap): TokenBalanceValue[] { + const entryBalances = Object.entries(tokenBalanceMap.map); + const balances = entryBalances.map(([tokenId, balance]) => ({ + tokenId, + tokenSymbol: tokenSymbolsMap[tokenId], + lockedAmount: balance.lockedAmount, + lockedAuthorities: balance.lockedAuthorities.toJSON(), + lockExpires: balance.lockExpires, + unlockedAmount: balance.unlockedAmount, + unlockedAuthorities: balance.unlockedAuthorities.toJSON(), + totalAmountSent: balance.totalAmountSent, + total: balance.total(), + } as TokenBalanceValue)); + return balances; + } +} + +export const sortBalanceValueByAbsTotal = (balanceA: TokenBalanceValue, balanceB: TokenBalanceValue): number => { + if (Math.abs(balanceA.total) - Math.abs(balanceB.total) >= 0) return -1; + return 0; +}; + +export class WalletBalanceMapConverter { + /** + * Convert the map of wallet balance instance into a map of wallet balance value. + * + * @param walletBalanceMap - Map wallet's id to its balance + * @param tokenSymbolsMap - Map token's id to its symbol + * @returns a map of wallet id to its balance value + */ + static toValue(walletBalanceMap: StringMap, tokenSymbolsMap: StringMap): StringMap { + const walletBalanceEntries = Object.entries(walletBalanceMap); + + const walletBalanceValueMap: StringMap = {}; + for (const [walletId, walletBalance] of walletBalanceEntries) { + const sortedTokenBalanceList = FromTokenBalanceMapToBalanceValueList + // hydrate token balance value with token symbol while convert to value + .convert(walletBalance.walletBalanceForTx, tokenSymbolsMap) + .sort(sortBalanceValueByAbsTotal); + + walletBalanceValueMap[walletId] = { + addresses: walletBalance.addresses, + txId: walletBalance.txId, + walletId: walletBalance.walletId, + walletBalanceForTx: sortedTokenBalanceList, + }; + } + + return walletBalanceValueMap; + } +} + +export const stringMapIterator = (stringMap: StringMap): [string, unknown][] => (Object.entries(stringMap)); diff --git a/packages/wallet-service/src/fullnode.ts b/packages/wallet-service/src/fullnode.ts new file mode 100644 index 00000000..318e610c --- /dev/null +++ b/packages/wallet-service/src/fullnode.ts @@ -0,0 +1,85 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import axios from 'axios'; + +export const BASE_URL = process.env.DEFAULT_SERVER; +export const TIMEOUT = 10000; + +/** + * Creates a handler for requesting data from the fullnode + * + * @param baseURL - The base URL for the full-node. Defaults to `env.DEFAULT_SERVER` + */ +export const create = (baseURL = BASE_URL): any => { + const api = axios.create({ + baseURL, + headers: {}, + timeout: TIMEOUT, + }); + + const downloadTx = async (txId: string) => { + const response = await api.get(`transaction?id=${txId}`, { + data: null, + headers: { 'content-type': 'application/json' }, + }); + + return response.data; + }; + + const getConfirmationData = async (txId: string) => { + const response = await api.get(`transaction_acc_weight?id=${txId}`, { + data: null, + headers: { 'content-type': 'application/json' }, + }); + + return response.data; + }; + + const queryGraphvizNeighbours = async ( + txId: string, + graphType: string, + maxLevel: number, + ) => { + const url = `graphviz/neighbours.dot/?tx=${txId}&graph_type=${graphType}&max_level=${maxLevel}`; + const response = await api.get(url, { + data: null, + headers: { 'content-type': 'application/json' }, + }); + + return response.data; + }; + + const getStatus = async () => { + const response = await api.get('status', { + data: null, + headers: { 'content-type': 'application/json' }, + }); + + return response.data; + } + + const getHealth = async () => { + const response = await api.get('health', { + data: null, + headers: { 'content-type': 'application/json' }, + }); + + return response.data; + } + + return { + api, // exported so we can mock it on the tests + downloadTx, + getConfirmationData, + queryGraphvizNeighbours, + getStatus, + getHealth + }; +}; + +export default create(); diff --git a/packages/wallet-service/src/height.ts b/packages/wallet-service/src/height.ts new file mode 100644 index 00000000..dc2a3958 --- /dev/null +++ b/packages/wallet-service/src/height.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { APIGatewayProxyHandler } from 'aws-lambda'; +import 'source-map-support/register'; + +import { getLatestHeight, getBlockByHeight } from '@src/db'; +import { closeDbConnection, getDbConnection } from '@src/utils'; + +const mysql = getDbConnection(); + +/* + * Get the service's current best block + * + * This lambda is called by API Gateway on GET /best_block + */ +export const getLatestBlock: APIGatewayProxyHandler = async () => { + const height = await getLatestHeight(mysql); + const block = await getBlockByHeight(mysql, height); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + block, + }), + }; +}; diff --git a/packages/wallet-service/src/logger.ts b/packages/wallet-service/src/logger.ts new file mode 100644 index 00000000..57d01871 --- /dev/null +++ b/packages/wallet-service/src/logger.ts @@ -0,0 +1,11 @@ +import { createLogger, format, transports, Logger } from 'winston'; + +const createDefaultLogger = (): Logger => createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: format.json(), + transports: [ + new transports.Console(), + ], +}); + +export default createDefaultLogger; diff --git a/packages/wallet-service/src/mempool.ts b/packages/wallet-service/src/mempool.ts new file mode 100644 index 00000000..94357dd2 --- /dev/null +++ b/packages/wallet-service/src/mempool.ts @@ -0,0 +1,95 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import 'source-map-support/register'; +import { + getLatestBlockByHeight, + getMempoolTransactionsBeforeDate, + updateTx, +} from '@src/db'; +import { Block, Severity, Tx } from '@src/types'; +import { handleVoided } from '@src/commons'; +import { + isTxVoided, + fetchBlockHeight, + closeDbConnection, + getDbConnection, +} from '@src/utils'; +import createDefaultLogger from '@src/logger'; +import { addAlert } from '@src/utils/alerting.utils'; + +const mysql = getDbConnection(); + +/** + * Function called to void unconfirmed transactions on the database + * + * @remarks + * This is a lambda function that should be triggered by an scheduled event. This will run by default on every + * 20 minutes (configurable on serverless.yml) and will query for transactions older than 20 minutes that are not + * confirmed by a block and are not voided. + */ +export const onHandleOldVoidedTxs = async (): Promise => { + const logger = createDefaultLogger(); + + const VOIDED_TX_OFFSET: number = parseInt(process.env.VOIDED_TX_OFFSET, 10) * 60; // env is in minutes + const bestBlock: Block = await getLatestBlockByHeight(mysql); + const bestBlockTimestamp = bestBlock.timestamp; + + const date: number = bestBlockTimestamp - VOIDED_TX_OFFSET; + + // Fetch voided transactions that are older than 20m + const voidedTransactions: Tx[] = await getMempoolTransactionsBeforeDate(mysql, date); + logger.debug(`Found ${voidedTransactions.length} voided transactions older than ${process.env.VOIDED_TX_OFFSET}m from the best block`, { + voidedTransactions, + }); + + /* This loop will check if all transactions are in fact voided on the fullnode and try to fix it (by updating the height) if + * they are not. + */ + for (const tx of voidedTransactions) { + const [isVoided, transaction] = await isTxVoided(tx.txId); + logger.debug(`Is transaction ${tx.txId} voided? ${isVoided}`); + + /* This will alarm if the transaction is not yet confirmed on our database and is not voided since + * this indicates an issue with our sync mechanism. + * + * It will also try to correct it by fetching the height that confirms it and updating the transaction on our database. + */ + if (!isVoided) { + await addAlert( + 'Error on mempool', + `Transaction ${tx.txId} is not yet confirmed on our database but it is not voided on the fullnode.`, + Severity.MAJOR, + { Tx: transaction }, + ); + logger.error(`Transaction ${tx.txId} is not yet confirmed on our database but it is not voided on the fullnode.`); + // Check if it is confirmed by a block + if (transaction.meta.first_block) { + /* Here we are sure that we really did lose the confirmation. We should fetch the height that confirmed it and update + * the transaction. + * + * This might fail as it will do a http request to the fullnode, we will catch the error, log and continue as it will + * automatically try again on the next schedule run. + */ + try { + // This will also throw if the height was not found on the requested block + const [height] = await fetchBlockHeight(transaction.meta.first_block, logger); + + // Balances have already been calculated as this transaction was on the mempool, we are safe to just update the height + await updateTx(mysql, tx.txId, height, tx.timestamp, tx.version, tx.weight); + } catch (e) { + logger.error(`Error confirming transaction ${tx.txId} height`); + logger.error(e); + } + } + } else { + await handleVoided(mysql, logger, tx); + } + } + + await closeDbConnection(mysql); +}; diff --git a/packages/wallet-service/src/metrics.ts b/packages/wallet-service/src/metrics.ts new file mode 100644 index 00000000..bdf45e72 --- /dev/null +++ b/packages/wallet-service/src/metrics.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { APIGatewayProxyHandler } from 'aws-lambda'; +import promClient from 'prom-client'; +import 'source-map-support/register'; + +import { getLatestHeight } from '@src/db'; +import { closeDbConnection, getDbConnection } from '@src/utils'; + +const mysql = getDbConnection(); + +// Default labels +const defaultLabels = { + network: process.env.NETWORK, + environment: process.env.STAGE, +}; +promClient.register.setDefaultLabels(defaultLabels); + +// Best block height metric +new promClient.Gauge({ // eslint-disable-line no-new + name: 'wallet_service:best_block_height', + help: 'The height of the latest block received', + async collect() { + const height = await getLatestHeight(mysql); + this.set(height); + }, +}); + +/* + * Returns all registered metrics in Prometheus format + * + * This lambda is called by API Gateway on GET /metrics + */ +export const getMetrics: APIGatewayProxyHandler = async () => { + const body = await promClient.register.metrics(); + await closeDbConnection(mysql); + + return { + statusCode: 200, + body, + }; +}; diff --git a/packages/wallet-service/src/redis.ts b/packages/wallet-service/src/redis.ts new file mode 100644 index 00000000..02328a68 --- /dev/null +++ b/packages/wallet-service/src/redis.ts @@ -0,0 +1,140 @@ +import { + WsConnectionInfo, + RedisConfig, +} from '@src/types'; + +import redis from 'redis'; +import { promisify } from 'util'; + +const redisConfig: RedisConfig = { + url: process.env.REDIS_URL, + password: process.env.REDIS_PASSWORD, +}; + +export const svcPrefix = 'walletsvc'; + +export const getRedisClient = (): redis.RedisClient => redis.createClient(redisConfig); + +export const closeRedisClient = ( + client: redis.RedisClient, +): Promise => { + const quit = promisify(client.quit).bind(client); + return quit(); +}; + +/* + * Ping the redis server. If it responds with PONG, it's alive. + * Reference: https://redis.io/commands/ping/ + */ +export const ping = ( + client: redis.RedisClient, +): Promise => { + const pingAsync = promisify(client.ping).bind(client); + return pingAsync(); +}; + +export const scanAll = async ( + client: redis.RedisClient, + pattern: string, +): Promise => { + const scanAsync = promisify(client.scan).bind(client); + const found = []; + let cursor = '0'; + do { + const reply = await scanAsync(cursor, 'MATCH', pattern); + cursor = reply[0]; + found.push(...reply[1]); + } while (cursor !== '0'); + + return found; +}; + +/* Create the connection entry + * */ +export const initWsConnection = async ( + client: redis.RedisClient, + connInfo: WsConnectionInfo, +): Promise => { + const setAsync = promisify(client.set).bind(client); + return setAsync(`${svcPrefix}:conn:${connInfo.id}`, connInfo.url); +}; + +/* Delete all keys for the connection + * */ +export const endWsConnection = async ( + client: redis.RedisClient, + connectionID: string, +): Promise => { + // multi not exactly needed (mainly used for transactions) + // but it gives a nice way to rollback if any errors occur in any command + // see: https://github.com/NodeRedis/node-redis#clientmulticommands + // and: https://redis.io/topics/transactions + // alternative: execute each command and check for errors individually + const multi = client.multi(); + multi.del(`${svcPrefix}:conn:${connectionID}`); + // with scanGen: for await (const key of scanGen(patt)) multi.del(key); + await scanAll(client, `${svcPrefix}:chan:*:${connectionID}`).then((keys) => { + for (const key of keys) { + multi.del(key); + } + }); + multi.exec(); +}; + +export const wsJoinChannel = async ( + client: redis.RedisClient, + connInfo: WsConnectionInfo, + channel: string, +): Promise => { + const setAsync = promisify(client.set).bind(client); + return setAsync(`${svcPrefix}:chan:${channel}:${connInfo.id}`, connInfo.url); +}; + +export const wsJoinWallet = async ( + client: redis.RedisClient, + connInfo: WsConnectionInfo, + walletID: string, +): Promise => wsJoinChannel(client, connInfo, `wallet-${walletID}`); + +export const wsGetConnection = async ( + client: redis.RedisClient, + connectionID: string, +): Promise => { + const getAsync = promisify(client.get).bind(client); + return getAsync(`${svcPrefix}:conn:${connectionID}`); +}; + +// get all connections +export const wsGetAllConnections = async ( + client: redis.RedisClient, +): Promise => { + const getAsync = promisify(client.get).bind(client); + const found: WsConnectionInfo[] = []; + const keys = await scanAll(client, `${svcPrefix}:conn:*`); + for (const key of keys) { + const value = await getAsync(key); + found.push({ id: key.split(':').pop(), url: value }); + } + return found; +}; + +// get all connections listening to a channel +export const wsGetChannelConnections = async ( + client: redis.RedisClient, + channel: string, +): Promise => { + const getAsync = promisify(client.get).bind(client); + const found: WsConnectionInfo[] = []; + const keys = await scanAll(client, `${svcPrefix}:chan:${channel}:*`); + for (const key of keys) { + const value = await getAsync(key); + found.push({ id: key.split(':').pop(), url: value }); + } + return found; +}; + +// get all connections related to a walletID +export const wsGetWalletConnections = async ( + client: redis.RedisClient, + walletID: string, +): Promise => wsGetChannelConnections(client, `wallet-${walletID}`); diff --git a/packages/wallet-service/src/txProcessor.ts b/packages/wallet-service/src/txProcessor.ts new file mode 100644 index 00000000..fd20bdad --- /dev/null +++ b/packages/wallet-service/src/txProcessor.ts @@ -0,0 +1,478 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs'; +import { APIGatewayProxyHandler, APIGatewayProxyResult, Handler, SQSEvent } from 'aws-lambda'; +import 'source-map-support/register'; +import hathorLib from '@hathor/wallet-lib'; +import { + getAddressBalanceMap, + getWalletBalanceMap, + markLockedOutputs, + unlockUtxos, + unlockTimelockedUtxos, + searchForLatestValidBlock, + getTokenListFromInputsAndOutputs, + handleReorg, + handleVoided, + prepareOutputs, + getWalletBalancesForTx, +} from '@src/commons'; +import { Logger } from 'winston'; +import { + addNewAddresses, + addUtxos, + addOrUpdateTx, + updateTx, + generateAddresses, + getAddressWalletInfo, + getLockedUtxoFromInputs, + getUtxosLockedAtHeight, + updateTxOutputSpentBy, + storeTokenInformation, + updateAddressTablesWithTx, + updateWalletTablesWithTx, + incrementTokensTxCount, + fetchTx, + addMiner, + cleanupVoidedTx, + checkTxWasVoided, +} from '@src/db'; +import { + transactionDecorator, +} from '@src/db/utils'; +import { + TxOutputWithIndex, + StringMap, + Transaction, + TokenBalanceMap, + Wallet, + Tx, + Severity, +} from '@src/types'; +import { + closeDbConnection, + getDbConnection, + getUnixTimestamp, +} from '@src/utils'; +import createDefaultLogger from '@src/logger'; +import { NftUtils } from '@src/utils/nft.utils'; +import { PushNotificationUtils, isPushNotificationEnabled } from '@src/utils/pushnotification.utils'; +import { addAlert } from '@src/utils/alerting.utils'; + +const mysql = getDbConnection(); + +export const IGNORE_TXS = { + mainnet: [ + '000006cb93385b8b87a545a1cbb6197e6caff600c12cc12fc54250d39c8088fc', + '0002d4d2a15def7604688e1878ab681142a7b155cbe52a6b4e031250ae96db0a', + '0002ad8d1519daaddc8e1a37b14aac0b045129c01832281fb1c02d873c7abbf9', + ], + testnet: [ + '0000033139d08176d1051fb3a272c3610457f0c7f686afbe0afe3d37f966db85', + '00e161a6b0bee1781ea9300680913fb76fd0fac4acab527cd9626cc1514abdc9', + '00975897028ceb037307327c953f5e7ad4d3f42402d71bd3d11ecb63ac39f01a', + ], +}; + +/** + * Function called when a new transaction arrives. + * + * @remarks + * This is a lambda function that should be triggered by an SQS event. The queue might batch + * messages, so we expect a list of transactions. This function only parses the SQS event and + * calls the appropriate function to handle the transaction. + * + * @param event - The SQS event + * @deprecated + */ +export const onNewTxEvent = async (event: SQSEvent): Promise => { + const logger: Logger = createDefaultLogger(); + + // TODO not sure if it should be 'now' or max(now, tx.timestamp), as we allow some flexibility for timestamps + const now = getUnixTimestamp(); + const blockRewardLock = parseInt(process.env.BLOCK_REWARD_LOCK, 10); + + for (const evt of event.Records) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await addNewTx(logger, evt.body, now, blockRewardLock); + } + + await closeDbConnection(mysql); + + // TODO delete message from queue + // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html + // When a consumer receives and processes a message from a queue, the message remains in the queue. + // Amazon SQS doesn't automatically delete the message. Thus, the consumer must delete the message from the + // queue after receiving and processing it. + + return { + statusCode: 200, + body: JSON.stringify({ message: 'Added new transactions' }), + }; +}; + +/** + * Function called when to process new transactions or blocks. + * + * @remarks + * This is a lambda function that should be invoked using the aws-sdk. + */ +export const onNewTxRequest: APIGatewayProxyHandler = async (event, context) => { + const logger = createDefaultLogger(); + + // Logs the request id on every line so we can see all logs from a request + logger.defaultMeta = { + requestId: context.awsRequestId, + }; + + const now = getUnixTimestamp(); + const blockRewardLock = parseInt(process.env.BLOCK_REWARD_LOCK, 10); + const tx = (event.body as unknown) as Transaction; + + // Critical processing: add the transaction to the database. + try { + await addNewTx(logger, tx, now, blockRewardLock); + } catch (e) { + // eslint-disable-next-line + logger.error('Errored on onNewTxRequest: ', e); + await addAlert( + 'Error on onNewTxRequest', + 'Erroed on onNewTxRequest lambda', + Severity.MINOR, + { TxId: tx.tx_id, error: e.message }, + ); + + return { + statusCode: 500, + body: JSON.stringify({ + success: false, + message: 'Tx processor failed', + }), + }; + } + + // Validating for NFTs only after the tx is successfully added + if (NftUtils.shouldInvokeNftHandlerForTx(tx)) { + // This process is not critical, so we run it in a fire-and-forget manner, not waiting for the promise. + // In case of errors, just log the asynchronous exception and take no action on it. + NftUtils.invokeNftHandlerLambda(tx.tx_id) + .catch((err) => logger.error('[ALERT] Errored on nftHandlerLambda invocation', err)); + } + + if (isPushNotificationEnabled()) { + const walletBalanceMap = await getWalletBalancesForTx(mysql, tx); + const { length: hasAffectWallets } = Object.keys(walletBalanceMap); + if (hasAffectWallets) { + PushNotificationUtils.invokeOnTxPushNotificationRequestedLambda(walletBalanceMap) + .catch((err: Error) => logger.error('Errored on invokeOnTxPushNotificationRequestedLambda invocation', err)); + } + } + + return { + statusCode: 200, + body: JSON.stringify({ success: true }), + }; +}; + +/** + * Function called when a reorg is detected on the wallet-service daemon + * + * @remarks + * This is a lambda function that should be invoked using the aws-sdk. + */ +export const onHandleReorgRequest: APIGatewayProxyHandler = async (_event, context) => { + const logger = createDefaultLogger(); + + logger.defaultMeta = { + requestId: context.awsRequestId, + }; + + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + /* eslint-disable-next-line @typescript-eslint/ban-types */ + const wrappedHandleReorg = await transactionDecorator(mysql, handleReorg); + + await wrappedHandleReorg(mysql, logger); + + return { + statusCode: 200, + body: JSON.stringify({ success: true }), + }; + } catch (e) { + // eslint-disable-next-line + logger.error('Errored on onHandleReorgRequest: ', e); + return { + statusCode: 500, + body: JSON.stringify({ + success: false, + message: 'Reorg failed.', + }), + }; + } +}; + +/** + * Function called to search for the latest valid block + * + * @remarks + * This is a lambda function that should be invoked using the aws-sdk. + */ +export const onSearchForLatestValidBlockRequest: APIGatewayProxyHandler = async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const latestValidBlock = await searchForLatestValidBlock(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ success: true, latestValidBlock }), + }; +}; + +export const handleVoidedTx = async (tx: Transaction): Promise => { + const txId = tx.tx_id; + const transaction: Tx = await fetchTx(mysql, txId); + const logger = createDefaultLogger(); + logger.defaultMeta = { + txId, + }; + + if (!transaction) { + throw new Error(`Transaction ${txId} not found.`); + } + + await handleVoided(mysql, logger, transaction); +}; + +/** + * This intermediary handler is responsible for making the final validations and calling + * the Explorer Service to update a NFT metadata, if needed. + * + * @remarks + * This is a lambda function that should be invoked using the aws-sdk. + */ +export const onNewNftEvent: Handler< + { nftUid: string }, + { success: boolean, message?: string } +> = async (event, context) => { + const logger = createDefaultLogger(); + + // Logs the request id on every line, so we can see all logs from a request + logger.defaultMeta = { + requestId: context.awsRequestId, + }; + + // An invalid event object is a signal of a greater communication problem and should be thrown + if (!event.nftUid) { + throw new Error('Missing mandatory parameter nftUid'); + } + + try { + // Checks existing metadata on this transaction and updates it if necessary + await NftUtils.createOrUpdateNftMetadata(event.nftUid); + } catch (e) { + logger.error('Errored on onNewNftEvent: ', e); + + // No errors should be thrown from the process, only logged and returned gracefully as a success: false + return { + success: false, + message: `onNewNftEvent failed for token ${(event.nftUid)}`, + }; + } + + return { + success: true, + }; +}; + +/** + * Add a new transaction or block, updating the proper tables. + * + * @param tx - The transaction or block + * @param now - Current timestamp + * @param blockRewardLock - The block reward lock + */ +const _unsafeAddNewTx = async (_logger: Logger, tx: Transaction, now: number, blockRewardLock: number): Promise => { + const txId = tx.tx_id; + const network = process.env.NETWORK; + + // add the tx id to all logs from this method, so we can search by txId on CloudWatch + const logger = _logger; + logger.defaultMeta = { + ...logger.defaultMeta, + txId, + }; + + logger.debug(`Transaction ${txId} received`, { + tx, + }); + + // we should ignore genesis transactions as they have no parents, inputs and outputs and we expect the service + // to already have the pre-mine utxos on its database. + if (network in IGNORE_TXS) { + if (IGNORE_TXS[network].includes(txId)) { + throw new Error('Rejecting tx as it is part of the genesis transactions.'); + } + } + + const dbTx: Tx = await fetchTx(mysql, txId); + + // check if we already have the tx on our database: + if (dbTx) { + // ignore tx if we already have it confirmed on our database + if (dbTx.height) { + logger.debug(`Ignoring ${txId} as it already has height on the database`, { + txId, + }); + return; + } + + // set height and break out because it was already on the mempool + // so we can consider that our balances have already been calculated + // and the utxos were already inserted + await updateTx(mysql, txId, tx.height, tx.timestamp, tx.version, tx.weight); + + return; + } + + // check if this tx was already on the database in the past and got voided: + const voidedTx = await checkTxWasVoided(mysql, txId); + + if (voidedTx) { + logger.info(`Transaction ${txId} received and was voided on database`, { + tx, + }); + // this tx was already in the database in the past as voided and is now valid + // again, we need to cleanup the tx_output and address_tx_history tables so we + // can safely add it again. Balances were already re-calculated on the handleReorg + // method, so we don't need to handle that here. + await cleanupVoidedTx(mysql, txId); + } + + let heightlock = null; + if (tx.version === hathorLib.constants.BLOCK_VERSION + || tx.version === hathorLib.constants.MERGED_MINED_BLOCK_VERSION) { + // unlock older blocks + const utxos = await getUtxosLockedAtHeight(mysql, now, tx.height); + logger.debug(`Block transaction, unlocking ${utxos.length} locked utxos at height ${tx.height}`, { + unlockedUtxos: utxos, + }); + await unlockUtxos(mysql, utxos, false); + + // set heightlock + heightlock = tx.height + blockRewardLock; + + // get the first output address + const blockRewardOutput = tx.outputs[0]; + + // add miner to the miners table + await addMiner(mysql, blockRewardOutput.decoded.address, tx.tx_id); + + // here we check if we have any utxos on our database that is locked but + // has its timelock < now + // + // we've decided to do this here considering that it is acceptable to have + // a delay between the actual timelock expiration time and the next block + // (that will unlock it). This delay is only perceived on the wallet as the + // sync mechanism will unlock the timelocked utxos as soon as they are seen + // on a received transaction. + await unlockTimelockedUtxos(mysql, now); + } + + if (tx.version === hathorLib.constants.CREATE_TOKEN_TX_VERSION) { + await storeTokenInformation(mysql, tx.tx_id, tx.token_name, tx.token_symbol); + } + + const outputs: TxOutputWithIndex[] = prepareOutputs(tx.outputs, txId, logger); + + // check if any of the inputs are still marked as locked and update tables accordingly. + // See remarks on getLockedUtxoFromInputs for more explanation. It's important to perform this + // before updating the balances + const lockedInputs = await getLockedUtxoFromInputs(mysql, tx.inputs); + await unlockUtxos(mysql, lockedInputs, true); + + // add transaction outputs to the tx_outputs table + markLockedOutputs(outputs, now, heightlock !== null); + logger.debug(`Adding ${txId} to database`); + await addOrUpdateTx(mysql, txId, tx.height, tx.timestamp, tx.version, tx.weight); + logger.debug(`Adding ${outputs.length} utxos to database`); + await addUtxos(mysql, txId, outputs, heightlock); + + // mark the tx_outputs used in the transaction (tx.inputs) as spent by txId + logger.debug(`Marking ${tx.inputs.length} tx_outputs as spent`, { + inputs: tx.inputs, + }); + await updateTxOutputSpentBy(mysql, tx.inputs, txId); + + // get balance of each token for each address + const addressBalanceMap: StringMap = getAddressBalanceMap(tx.inputs, outputs); + logger.debug('Updating address_balance and address_tx_history tables', { + addressBalanceMap, + }); + + const tokenList: string[] = getTokenListFromInputsAndOutputs(tx.inputs, outputs); + + // Update transaction count with the new tx + await incrementTokensTxCount(mysql, tokenList); + + // update address tables (address, address_balance, address_tx_history) + await updateAddressTablesWithTx(mysql, txId, tx.timestamp, addressBalanceMap); + + // for the addresses present on the tx, check if there are any wallets associated + const addressWalletMap: StringMap = await getAddressWalletInfo(mysql, Object.keys(addressBalanceMap)); + + // for each already started wallet, update databases + const seenWallets = new Set(); + for (const wallet of Object.values(addressWalletMap)) { + const walletId = wallet.walletId; + + // this map might contain duplicate wallet values, as 2 different addresses might belong to the same wallet + if (seenWallets.has(walletId)) continue; + seenWallets.add(walletId); + const { newAddresses, lastUsedAddressIndex } = await generateAddresses(mysql, wallet.xpubkey, wallet.maxGap); + // might need to generate new addresses to keep maxGap + await addNewAddresses(mysql, walletId, newAddresses, lastUsedAddressIndex); + // update existing addresses' walletId and index + } + // update wallet_balance and wallet_tx_history tables + const walletBalanceMap: StringMap = getWalletBalanceMap(addressWalletMap, addressBalanceMap); + logger.debug('Updating wallet_balance and wallet_tx_history tables', { + walletBalanceMap, + }); + await updateWalletTablesWithTx(mysql, txId, tx.timestamp, walletBalanceMap); + + const queueUrl = process.env.NEW_TX_SQS; + if (!queueUrl) return; + + const client = new SQSClient({}); + const command = new SendMessageCommand({ + QueueUrl: queueUrl, + MessageBody: JSON.stringify({ + wallets: Array.from(seenWallets), + tx, + }), + }); + + await client.send(command); +}; + +/** + * Add a new transaction or block, updating the proper tables. + * @remarks This is a wrapper for _unsafeAddNewTx that adds automatic transaction and rollback on failure + * + * @param tx - The transaction or block + * @param now - Current timestamp + * @param blockRewardLock - The block reward lock + */ +export const addNewTx = async (logger: Logger, tx: Transaction, now: number, blockRewardLock: number): Promise => { + /* eslint-disable-next-line @typescript-eslint/ban-types */ + const wrappedAddNewTx = await transactionDecorator(mysql, _unsafeAddNewTx); + + return wrappedAddNewTx(logger, tx, now, blockRewardLock); +}; diff --git a/packages/wallet-service/src/types.ts b/packages/wallet-service/src/types.ts new file mode 100644 index 00000000..f9fc1f58 --- /dev/null +++ b/packages/wallet-service/src/types.ts @@ -0,0 +1,812 @@ +/* eslint-disable max-classes-per-file */ + +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import hathorLib from '@hathor/wallet-lib'; +// eslint-disable-next-line +import { isAuthority } from '@src/utils'; + +import { + APIGatewayProxyEvent, + APIGatewayProxyResult, + Context, + Callback, +} from 'aws-lambda'; + +export interface StringMap { + [x: string]: T; +} + +export type AddressIndexMap = StringMap; + +export interface GenerateAddresses { + addresses: string[]; + existingAddresses: StringMap; + newAddresses: StringMap; + lastUsedAddressIndex: number; +} + +export enum TxProposalStatus { + OPEN = 'open', + SENT = 'sent', + SEND_ERROR = 'send_error', + CANCELLED = 'cancelled', +} + +export interface FullNodeVersionData { + timestamp: number; + version: string; + network: string; + minWeight: number; + minTxWeight: number; + minTxWeightCoefficient: number; + minTxWeightK: number; + tokenDepositPercentage: number; + rewardSpendMinBlocks: number; + maxNumberInputs: number; + maxNumberOutputs: number; +} + +export interface TxProposal { + id: string; + walletId: string; + status: TxProposalStatus; + createdAt: number; + updatedAt: number; +} + +export enum WalletStatus { + CREATING = 'creating', + READY = 'ready', + ERROR = 'error', +} + +export interface Wallet { + walletId: string; + xpubkey: string; + authXpubkey: string, + maxGap: number; + status?: WalletStatus; + retryCount?: number; + createdAt?: number; + readyAt?: number; +} + +export interface AddressInfo { + address: string; + index: number; + transactions: number; +} + +export interface ShortAddressInfo { + address: string; + index: number; + addressPath: string; +} + +export interface TokenBalance { + tokenId: string; + balance: Balance; + transactions: number; +} + +export class TokenInfo { + id: string; + + name: string; + + symbol: string; + + transactions: number; + + constructor(id: string, name: string, symbol: string, transactions?: number) { + this.id = id; + this.name = name; + this.symbol = symbol; + this.transactions = transactions || 0; + + const hathorConfig = hathorLib.constants.HATHOR_TOKEN_CONFIG; + + if (this.id === hathorConfig.uid) { + this.name = hathorConfig.name; + this.symbol = hathorConfig.symbol; + } + } + + toJSON(): Record { + return { + id: this.id, + name: this.name, + symbol: this.symbol, + }; + } +} + +export class Authorities { + /** + * Supporting up to 8 authorities (but we only have mint and melt at the moment) + */ + static LENGTH = 8; + + array: number[]; + + constructor(authorities?: number | number[]) { + let tmp = []; + if (authorities instanceof Array) { + tmp = authorities; + } else if (authorities != null) { + tmp = Authorities.intToArray(authorities); + } + + this.array = new Array(Authorities.LENGTH - tmp.length).fill(0).concat(tmp); + } + + /** + * Get the integer representation of this authority. + * + * @remarks + * Uses the array to calculate the final number. Examples: + * [0, 0, 0, 0, 1, 1, 0, 1] = 0b00001101 = 13 + * [0, 0, 1, 0, 0, 0, 0, 1] = 0b00100001 = 33 + * + * @returns The integer representation + */ + toInteger(): number { + let n = 0; + for (let i = 0; i < this.array.length; i++) { + if (this.array[i] === 0) continue; + + n += this.array[i] * (2 ** (this.array.length - i - 1)); + } + return n; + } + + toUnsignedInteger(): number { + return Math.abs(this.toInteger()); + } + + clone(): Authorities { + return new Authorities(this.array); + } + + /** + * Return a new object inverting each authority value sign. + * + * @remarks + * If value is set to 1, it becomes -1 and vice versa. Value 0 remains unchanged. + * + * @returns A new Authority object with the values inverted + */ + toNegative(): Authorities { + const finalAuthorities = this.array.map((value) => { + // This if is needed because Javascript uses the IEEE_754 standard and has negative and positive zeros, + // so (-1) * 0 would return -0. Apparently -0 === 0 is true on most cases, so there wouldn't be a problem, + // but we will leave this here to be safe. + // https://en.wikipedia.org/wiki/IEEE_754 + if (value === 0) return 0; + + return (-1) * value; + }); + return new Authorities(finalAuthorities); + } + + /** + * Return if any of the authorities has a negative value. + * + * @remarks + * Negative values for an authority only make sense when dealing with balances of a + * transaction. So if we consume an authority in the inputs but do not create the same + * one in the output, it will have value -1. + * + * @returns `true` if any authority is less than 0; `false` otherwise + */ + hasNegativeValue(): boolean { + return this.array.some((authority) => authority < 0); + } + + /** + * Transform an integer into an array, considering 1 array element per bit. + * + * @returns The array given an integer + */ + static intToArray(authorities: number): number[] { + const ret = []; + for (const c of authorities.toString(2)) { + ret.push(parseInt(c, 10)); + } + return ret; + } + + /** + * Merge two authorities. + * + * @remarks + * The process is done individualy for each authority value. Each a1[n] and a2[n] are compared. + * If both values are the same, the final value is the same. If one is 1 and the other -1, final + * value is 0. + * + * @returns A new object with the merged values + */ + static merge(a1: Authorities, a2: Authorities): Authorities { + return new Authorities(a1.array.map((value, index) => Math.sign(value + a2.array[index]))); + } + + toJSON(): Record { + const authorities = this.toInteger(); + return { + mint: (authorities & hathorLib.constants.TOKEN_MINT_MASK) > 0, // eslint-disable-line no-bitwise + melt: (authorities & hathorLib.constants.TOKEN_MELT_MASK) > 0, // eslint-disable-line no-bitwise + }; + } +} + +export class Balance { + totalAmountSent: number; + + lockedAmount: number; + + unlockedAmount: number; + + lockedAuthorities: Authorities; + + unlockedAuthorities: Authorities; + + lockExpires: number | null; + + constructor(totalAmountSent = 0, unlockedAmount = 0, lockedAmount = 0, lockExpires = null, unlockedAuthorities = null, lockedAuthorities = null) { + this.totalAmountSent = totalAmountSent; + this.unlockedAmount = unlockedAmount; + this.lockedAmount = lockedAmount; + this.lockExpires = lockExpires; + this.unlockedAuthorities = unlockedAuthorities || new Authorities(); + this.lockedAuthorities = lockedAuthorities || new Authorities(); + } + + /** + * Get the total balance, sum of unlocked and locked amounts. + * + * @returns The total balance + */ + total(): number { + return this.unlockedAmount + this.lockedAmount; + } + + /** + * Get all authorities, combination of unlocked and locked. + * + * @returns The combined authorities + */ + authorities(): Authorities { + return Authorities.merge(this.unlockedAuthorities, this.lockedAuthorities); + } + + /** + * Clone this Balance object. + * + * @returns A new Balance object with the same information + */ + clone(): Balance { + return new Balance( + this.totalAmountSent, + this.unlockedAmount, + this.lockedAmount, + this.lockExpires, + this.unlockedAuthorities.clone(), + this.lockedAuthorities.clone(), + ); + } + + /** + * Merge two balances. + * + * @remarks + * In case lockExpires is set, it returns the lowest one. + * + * @param b1 - First balance + * @param b2 - Second balance + * @returns The sum of both balances and authorities + */ + static merge(b1: Balance, b2: Balance): Balance { + let lockExpires = null; + if (b1.lockExpires === null) { + lockExpires = b2.lockExpires; + } else if (b2.lockExpires === null) { + lockExpires = b1.lockExpires; + } else { + lockExpires = Math.min(b1.lockExpires, b2.lockExpires); + } + return new Balance( + b1.totalAmountSent + b2.totalAmountSent, + b1.unlockedAmount + b2.unlockedAmount, + b1.lockedAmount + b2.lockedAmount, + lockExpires, + Authorities.merge(b1.unlockedAuthorities, b2.unlockedAuthorities), + Authorities.merge(b1.lockedAuthorities, b2.lockedAuthorities), + ); + } +} + +export type TokenBalanceValue = { + tokenId: string, + tokenSymbol: string, + totalAmountSent: number; + lockedAmount: number; + unlockedAmount: number; + lockedAuthorities: Record; + unlockedAuthorities: Record; + lockExpires: number | null; + total: number; +} + +export class WalletTokenBalance { + token: TokenInfo; + + balance: Balance; + + transactions: number; + + constructor(token: TokenInfo, balance: Balance, transactions: number) { + this.token = token; + this.balance = balance; + this.transactions = transactions; + } + + toJSON(): Record { + return { + token: this.token, + transactions: this.transactions, + balance: { + unlocked: this.balance.unlockedAmount, + locked: this.balance.lockedAmount, + }, + tokenAuthorities: { + unlocked: this.balance.unlockedAuthorities, + locked: this.balance.lockedAuthorities, + }, + lockExpires: this.balance.lockExpires, + }; + } +} + +export interface TxTokenBalance { + txId: string; + timestamp: number; + voided: boolean; + balance: Balance; + version: number; +} + +export class TokenBalanceMap { + map: StringMap; + + constructor() { + this.map = {}; + } + + get(tokenId: string): Balance { + // if the token is not present, return 0 instead of undefined + return this.map[tokenId] || new Balance(0, 0, 0); + } + + set(tokenId: string, balance: Balance): void { + this.map[tokenId] = balance; + } + + getTokens(): string[] { + return Object.keys(this.map); + } + + iterator(): [string, Balance][] { + return Object.entries(this.map); + } + + clone(): TokenBalanceMap { + const cloned = new TokenBalanceMap(); + for (const [token, balance] of this.iterator()) { + cloned.set(token, balance.clone()); + } + return cloned; + } + + /** + * Return a TokenBalanceMap from js object. + * + * @remarks + * Js object is expected to have the format: + * ``` + * { + * token1: {unlocked: n, locked: m}, + * token2: {unlocked: a, locked: b, lockExpires: c}, + * token3: {unlocked: x, locked: y, unlockedAuthorities: z, lockedAuthorities: w}, + * } + * ``` + * + * @param tokenBalanceMap - The js object to convert to a TokenBalanceMap + * @returns - The new TokenBalanceMap object + */ + static fromStringMap(tokenBalanceMap: StringMap>): TokenBalanceMap { + const obj = new TokenBalanceMap(); + for (const [tokenId, balance] of Object.entries(tokenBalanceMap)) { + obj.set(tokenId, new Balance(balance.totalSent as number, balance.unlocked as number, balance.locked as number, balance.lockExpires || null, + balance.unlockedAuthorities, balance.lockedAuthorities)); + } + return obj; + } + + /** + * Merge two TokenBalanceMap objects, merging the balances for each token. + * + * @param balanceMap1 - First TokenBalanceMap + * @param balanceMap2 - Second TokenBalanceMap + * @returns The merged TokenBalanceMap + */ + static merge(balanceMap1: TokenBalanceMap, balanceMap2: TokenBalanceMap): TokenBalanceMap { + if (!balanceMap1) return balanceMap2.clone(); + if (!balanceMap2) return balanceMap1.clone(); + const mergedMap = balanceMap1.clone(); + for (const [token, balance] of balanceMap2.iterator()) { + const finalBalance = Balance.merge(mergedMap.get(token), balance); + mergedMap.set(token, finalBalance); + } + return mergedMap; + } + + /** + * Create a TokenBalanceMap from a TxOutput. + * + * @param output - The transaction output + * @returns The TokenBalanceMap object + */ + static fromTxOutput(output: TxOutput): TokenBalanceMap { + // TODO check if output.decoded exists, else return null + const token = output.token; + const value = output.value; + const obj = new TokenBalanceMap(); + + if (output.locked) { + if (isAuthority(output.token_data)) { + obj.set(token, new Balance(0, 0, 0, output.decoded.timelock, 0, new Authorities(output.value))); + } else { + obj.set(token, new Balance(value, 0, value, output.decoded.timelock, 0, 0)); + } + } else if (isAuthority(output.token_data)) { + obj.set(token, new Balance(0, 0, 0, null, new Authorities(output.value), 0)); + } else { + obj.set(token, new Balance(value, value, 0, null)); + } + + return obj; + } + + /** + * Create a TokenBalanceMap from a TxInput. + * + * @remarks + * It will have only one token entry and balance will be negative. + * + * @param input - The transaction input + * @returns The TokenBalanceMap object + */ + static fromTxInput(input: TxInput): TokenBalanceMap { + const token = input.token; + const obj = new TokenBalanceMap(); + + if (isAuthority(input.token_data)) { + // for inputs, the authorities will have a value of -1 when set + const authorities = new Authorities(input.value); + obj.set(token, new Balance(0, 0, 0, null, authorities.toNegative(), new Authorities(0))); + } else { + obj.set(token, new Balance(0, -input.value, 0, null)); + } + return obj; + } +} + +/** + * Return type from ServerlessMysql#query after performing a SQL SELECT + * (Array of objects containing the requested table fields.) + */ +export type DbSelectResult = Array>; + +/** + * Hathor types + */ + +export interface DecodedOutput { + type: string; + address: string; + timelock: number | null; +} + +export interface TxOutput { + value: number; + script: string; + token: string; + decoded: DecodedOutput; + // eslint-disable-next-line camelcase + spent_by: string | null; + // eslint-disable-next-line camelcase + token_data: number; + locked?: boolean; +} + +export interface TxOutputWithIndex extends TxOutput { + index: number; +} + +export interface TxInput { + // eslint-disable-next-line camelcase + tx_id: string; + index: number; + value: number; + // eslint-disable-next-line camelcase + token_data: number; + script: string; + token: string; + decoded: DecodedOutput; +} + +export interface Transaction { + // eslint-disable-next-line camelcase + tx_id: string; + nonce: number; + timestamp: number; + // eslint-disable-next-line camelcase + signal_bits: number; + version: number; + weight: number; + parents: string[]; + inputs: TxInput[]; + outputs: TxOutput[]; + height?: number; + // eslint-disable-next-line camelcase + token_name?: string; + // eslint-disable-next-line camelcase + token_symbol?: string; +} + +export interface IWalletOutput { + address: string; + value: number; + token: string; + tokenData: number; + timelock: number; +} + +export interface IWalletInput { + txId: string; + index: number; +} + +export interface ApiResponse { + success: boolean; + message: string; +} + +export type WsConnectionInfo = { + id: string; + url: string; +} + +export type RedisConfig = { + url: string; + password?: string; +}; + +export interface Tx { + txId: string; + timestamp: number; + version: number; + voided: boolean; + height?: number | null; + weight: number; +} + +export interface AddressBalance { + address: string; + tokenId: string; + unlockedBalance: number; + lockedBalance: number; + unlockedAuthorities: number; + lockedAuthorities: number; + timelockExpires: number; + transactions: number; +} + +export interface AddressTotalBalance { + address: string; + tokenId: string; + balance: number; + transactions: number; +} + +export interface DbTxOutput { + txId: string; + index: number; + tokenId: string; + address: string; + value: number; + authorities: number; + timelock: number | null; + heightlock: number | null; + locked: boolean; + spentBy?: string | null; + txProposalId?: string; + txProposalIndex?: number; + voided?: boolean | null; +} + +export interface Block { + txId: string; + height: number; + timestamp: number; +} + +// maybe use templates +export type WalletProxyHandler = ( + walletId: string, + event?: APIGatewayProxyEvent, + context?: Context, + callback?: Callback +) => Promise; + +export interface IFilterTxOutput { + addresses: string[]; + tokenId?: string; + authority?: number; + ignoreLocked?: boolean; + biggerThan?: number; + smallerThan?: number; + maxOutputs?: number; + skipSpent?: boolean; + txId?: string; + index?: number; +} + +export enum InputSelectionAlgo { + USE_LARGER_UTXOS = 'use-larger-utxos', +} + +export interface IWalletInsufficientFunds { + tokenId: string; + requested: number; + available: number; +} + +export interface DbTxOutputWithPath extends DbTxOutput { + addressPath: string; +} + +export interface Miner { + address: string; + firstBlock: string; + lastBlock: string; + count: number; +} + +export enum PushProvider { + IOS = 'ios', + ANDROID = 'android' +} + +export interface PushRegister { + pushProvider: PushProvider, + deviceId: string, + enablePush?: boolean, + enableShowAmounts?: boolean +} + +export interface PushUpdate { + deviceId: string, + enablePush?: boolean, + enableShowAmounts?: boolean +} + +export interface PushDelete { + deviceId: string, +} + +export interface AddressAtIndexRequest { + index?: number, +} + +export interface TxByIdRequest { + txId: string, +} + +export interface TxByIdToken { + txId: string; + timestamp: number; + version: number; + voided: boolean; + weight: number; + balance: Balance; + tokenId: string; + tokenName: string; + tokenSymbol: string; +} + +export interface ParamValidationResult { + error: boolean; + details?: { message: string, path: (string | number)[] }[], + value?: ValueType; +} + +export interface GraphvizParams { + txId: string; + graphType: string; + maxLevel: number; +} + +export interface GetTxByIdParams { + txId: string; +} + +export interface GetConfirmationDataParams { + txId: string; +} + +export interface SendNotificationToDevice { + deviceId: string, + /** + * A string map used to send data in the notification message. + * @see LocalizeMetadataNotification + * + * @example + * { + * "titleLocKey": "new_transaction_received_title", + * "bodyLocKey": "new_transaction_received_description_with_tokens", + * "bodyLocArgs": "['13 HTR', '8 TNT', '2']" + * } + */ + metadata: Record, +} + +export type LocalizeMetadataNotification = { + titleLocKey: string, + titleLocArgs: string, + bodyLocKey: string, + bodyLocArgs: string, +} + +export interface PushDevice { + walletId: string, + deviceId: string, + pushProvider: PushProvider, + enablePush: boolean, + enableShowAmounts: boolean +} + +export type PushDeviceSettings = Omit; + +export interface WalletBalance { + txId: string, + walletId: string, + addresses: string[], + walletBalanceForTx: TokenBalanceMap, +} + +export interface WalletBalanceValue { + txId: string, + walletId: string, + addresses: string[], + walletBalanceForTx: TokenBalanceValue[], +} + +/** + * Alerts should follow the on-call guide for alerting, see + * https://github.com/HathorNetwork/ops-tools/blob/master/docs/on-call/guide.md#alert-severitypriority + */ +export enum Severity { + CRITICAL = 'critical', + MAJOR = 'major', + MEDIUM = 'medium', + MINOR = 'minor', + WARNING = 'warning', + INFO = 'info', +} diff --git a/packages/wallet-service/src/utils.ts b/packages/wallet-service/src/utils.ts new file mode 100644 index 00000000..d32ab07a --- /dev/null +++ b/packages/wallet-service/src/utils.ts @@ -0,0 +1,387 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { BinaryToTextEncoding, createHash } from 'crypto'; + +import { Logger } from 'winston'; +import serverlessMysql, { ServerlessMysql } from 'serverless-mysql'; +import hathorLib from '@hathor/wallet-lib'; +import fullnode from '@src/fullnode'; +import * as bitcoin from 'bitcoinjs-lib'; +import * as bitcoinMessage from 'bitcoinjs-message'; +import * as ecc from 'tiny-secp256k1'; +import BIP32Factory from 'bip32'; + +const bip32 = BIP32Factory(ecc); + +/* TODO: We should remove this as soon as the wallet-lib is refactored +* (https://github.com/HathorNetwork/hathor-wallet-lib/issues/122) +*/ +export class CustomStorage { + store: unknown; + + constructor() { + this.preStart(); + } + + getItem(key: string): string { + return this.store[key]; + } + + setItem(key: string, value: string): string { + this.store[key] = value; + + return value; + } + + removeItem(key: string): string { + delete this.store[key]; + + return key; + } + + clear(): void { + this.store = {}; + } + + preStart(): void { + this.store = { + 'wallet:server': process.env.DEFAULT_SERVER || hathorLib.constants.DEFAULT_SERVER, + 'wallet:defaultServer': process.env.DEFAULT_SERVER || hathorLib.constants.DEFAULT_SERVER, + }; + } +} + +hathorLib.network.setNetwork(process.env.NETWORK); +hathorLib.storage.setStore(new CustomStorage()); + +const libNetwork = hathorLib.network.getNetwork(); +const hathorNetwork = { + messagePrefix: '\x18Hathor Signed Message:\n', + bech32: hathorLib.network.bech32prefix, + bip32: { + public: libNetwork.xpubkey, + private: libNetwork.xprivkey, + }, + pubKeyHash: libNetwork.pubkeyhash, + scriptHash: libNetwork.scripthash, + wif: libNetwork.privatekey, +}; + +/** + * Calculate the double sha256 hash of the data. + * + * @remarks + * If encoding is provided a string will be returned; otherwise a Buffer is returned. + * + * @param data - Data to be hashed + * @param encoding - The encoding of the returned object + * @returns The sha256d hash of the data + */ +export const sha256d = (data: string, encoding: BinaryToTextEncoding): string => { + const hash1 = createHash('sha256'); + hash1.update(data); + const hash2 = createHash('sha256'); + hash2.update(hash1.digest()); + return hash2.digest(encoding); +}; + +/** + * Get the wallet id given the xpubkey. + * + * @param xpubkey - The xpubkey + * @returns The wallet id + */ +export const getWalletId = (xpubkey: string): string => ( + sha256d(xpubkey, 'hex') +); + +/** + * Get the current Unix timestamp, in seconds. + * + * @returns The current Unix timestamp in seconds + */ +export const getUnixTimestamp = (): number => ( + Math.round((new Date()).getTime() / 1000) +); + +/** + * Get a database connection. + * + * @returns The database connection + */ +export const getDbConnection = (): ServerlessMysql => ( + serverlessMysql({ + config: { + host: process.env.DB_ENDPOINT, + database: process.env.DB_NAME, + user: process.env.DB_USER, + port: parseInt(process.env.DB_PORT, 10), + // TODO if not on local env, get IAM token + // https://aws.amazon.com/blogs/database/iam-role-based-authentication-to-amazon-aurora-from-serverless-applications/ + password: process.env.DB_PASS, + }, + }) +); + +export const closeDbConnection = async (mysql: ServerlessMysql): Promise => { + if (process.env.STAGE === 'local') { + // mysql.end() leaves the function hanging in the local environment. Some issues: + // https://github.com/jeremydaly/serverless-mysql/issues/61 + // https://github.com/jeremydaly/serverless-mysql/issues/79 + // + // It seems that's the expected behavior for local environment: + // https://github.com/serverless/serverless/issues/470#issuecomment-205372006 + await mysql.quit(); + } else { + await mysql.end(); + } +}; + +export const isAuthority = (tokenData: number): boolean => ( + (tokenData & hathorLib.constants.TOKEN_AUTHORITY_MASK) > 0 // eslint-disable-line no-bitwise +); + +/** + * Shuffle an array in place. + * + * @remarks + * Got it from https://stackoverflow.com/a/6274381. + * + * @param array - An array containing the items + */ +export const arrayShuffle = (array: T[]): T[] => { + /* eslint-disable no-param-reassign */ + let j; + let x; + let i; + for (i = array.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + x = array[i]; + array[i] = array[j]; + array[j] = x; + } + return array; + /* eslint-enable no-param-reassign */ +}; + +/** + * Requests the fullnode for the requested transaction information and returns + * if it is voided or not and the downloaded object + * + * @param txId - The transaction id + * + * @returns A tuple with the result and the downloaded transaction + */ +export const isTxVoided = async (txId: string): Promise<[boolean, any]> => { + const transaction = await fullnode.downloadTx(txId); + + if (!transaction.meta.voided_by || transaction.meta.voided_by.length === 0) { + return [false, transaction]; + } + + return [true, transaction]; +}; + +/** + * Requests the fullnode for a block and returns a tuple with the height and the + * downloaded block + * + * @param txId - The transaction id + * + * @returns A tuple with the result and the downloaded transaction + */ +export const fetchBlockHeight = async (txId: string, logger: Logger): Promise<[number, any]> => { + const transaction = await fullnode.downloadTx(txId); + + if (!transaction.tx.height) { + logger.error(JSON.stringify(transaction)); + throw new Error(`Block ${txId} has no height.`); + } + + return [transaction.tx.height, transaction]; +}; + +/** + * Creates default address path from address index + * + * @returns {string} The address path + */ +export const getAddressPath = (index: number): string => ( + `m/44'/${hathorLib.constants.HATHOR_BIP44_CODE}'/0'/0/${index}` +); + +/** + * Verifies that the expected first address (received as a param) is the same as one + * derived from the xpubkey param on the change 0 path + * + * @param expectedFirstAddress - The expected first address + * @param xpubkey - The xpubkey to derive the change 0 path + * + * @returns A tuple with the first value being the result of the comparison and the second value the firstAddress derived + */ +export const confirmFirstAddress = (expectedFirstAddress: string, xpubkey: string): [boolean, string] => { + // First derive xpub to change 0 path + const derivedXpub = xpubDeriveChild(xpubkey, 0); + // Then get first address + const firstAddress = getAddressAtIndex(derivedXpub, 0); + + return [ + firstAddress === expectedFirstAddress, + firstAddress, + ]; +}; + +/** + * A constant for the max shift for the timestamp used in auth + */ +export const AUTH_MAX_TIMESTAMP_SHIFT_IN_SECONDS = 30; + +/** + * Verifies that the timestamp has not shifted for more than AUTH_MAX_TIMESTAMP_SHIFT_IN_SECONDS + * + * @param timestamp - The timestamp to check, in **seconds** + * @param now - The current timestamp + * + * @returns A tuple with the first value being the result of the comparison and the second value the firstAddress derived + */ +export const validateAuthTimestamp = (timestamp: number, now: number): [boolean, number] => { + const timestampShiftInSeconds = Math.floor(Math.abs(now - timestamp)); + + return [timestampShiftInSeconds < AUTH_MAX_TIMESTAMP_SHIFT_IN_SECONDS, timestampShiftInSeconds]; +}; + +/** + * Returns an address from a xpubkey on a specific index + * + * @param xpubkey - The xpubkey + * @param index - The address index to derive + * + * @returns The derived address + */ +export const getAddressAtIndex = (xpubkey: string, addressIndex: number): string => { + const node = bip32.fromBase58(xpubkey).derive(addressIndex); + return bitcoin.payments.p2pkh({ + pubkey: node.publicKey, + network: hathorNetwork, + }).address; +}; + +/** + * Get Hathor addresses in bulk, passing the start index and quantity of addresses to be generated + * + * @example + * ``` + * getAddresses('myxpub', 2, 3) => { + * 'address2': 2, + * 'address3': 3, + * 'address4': 4, + * } + * ``` + * + * @param xpubkey The xpubkey + * @param startIndex Generate addresses starting from this index + * @param quantity Amount of addresses to generate + * + * @return An object with the generated addresses and corresponding index (string => number) + * + * @memberof Wallet + * @inner + */ +export const getAddresses = (xpubkey: string, startIndex: number, quantity: number): {[key: string]: number} => { + const addrMap = {}; + + for (let index = startIndex; index < startIndex + quantity; index++) { + const address = getAddressAtIndex(xpubkey, index); + addrMap[address] = index; + } + + return addrMap; +}; + +/** + * Derives a xpubkey at a specific index + * + * @param xpubkey - The xpubkey + * @param index - The index to derive + * + * @returns The derived xpubkey + */ +export const xpubDeriveChild = (xpubkey: string, index: number): string => ( + bip32.fromBase58(xpubkey).derive(index).toBase58() +); + +/** + * Verify a signature for a given timestamp and xpubkey + * + * @param signature - The signature done by the xpriv of the wallet + * @param timestamp - Unix Timestamp of the signature + * @param address - The address of the xpubkey used to create the walletId + * @param walletId - The walletId, a sha512d of the xpubkey + * + * @returns true if the signature matches the other params + */ +export const verifySignature = ( + signature: string, + timestamp: number, + address: string, + walletId: string, +): boolean => { + try { + const message = String(timestamp).concat(walletId).concat(address); + + return bitcoinMessage.verify( + message, + address, + Buffer.from(signature, 'base64'), + // Different from bitcore-lib, bitcoinjs-lib does not prefix the messagePrefix + // length on the message, so we need to do this by using a "End of Transmission + // Block" with the length (22) in hex (17). This is the same thing that is done + // for the default Bitcoin message (\u0018Bitcoin Signed Message:\n). + '\u0017Hathor Signed Message:\n', + ); + } catch (e) { + // Since this will try to verify the signature passing user input, the verify method might + // throw, we can just return false in this case. + return false; + } +}; + +/** + * Returns an address (as a string) from a string xpubkey + * + * @param xpubkey - The xpubkey + * + * @returns the address derived from the xpubkey + */ +export const getAddressFromXpub = (xpubkey: string): string => { + const node = bip32.fromBase58(xpubkey); + + return bitcoin.payments.p2pkh({ + pubkey: node.publicKey, + network: hathorNetwork, + }).address; +}; + +/** + * Validates if a list of env variables are set in the environment. Throw if at least + * one of them is missing + * + * @param envVariables - A list of variables to check + */ +export const assertEnvVariablesExistence = (envVariables: string[]): void => { + const missingList = []; + for (const envVariable of envVariables) { + if (!(envVariable in process.env) || process.env[envVariable].length === 0) { + missingList.push(envVariable); + } + } + + if (missingList.length > 0) { + throw new Error(`Env missing the following variables ${missingList.join(', ')}`); + } +}; diff --git a/packages/wallet-service/src/utils/alerting.utils.ts b/packages/wallet-service/src/utils/alerting.utils.ts new file mode 100644 index 00000000..93586706 --- /dev/null +++ b/packages/wallet-service/src/utils/alerting.utils.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs'; +import { Severity } from '@src/types'; +import { assertEnvVariablesExistence } from '@src/utils'; +import createDefaultLogger from '@src/logger'; + +assertEnvVariablesExistence([ + 'NETWORK', + 'APPLICATION_NAME', + 'ACCOUNT_ID', + 'ALERT_MANAGER_REGION', + 'ALERT_MANAGER_TOPIC', +]); + +/** + * Adds a message to the SQS alerting queue + * + * @param fnName - The lambda function name + * @param payload - The payload to be sent + */ +export const addAlert = async ( + title: string, + message: string, + severity: Severity, + metadata?: unknown, +): Promise => { + const logger = createDefaultLogger(); + const preparedMessage = { + title, + message, + severity, + metadata, + environment: process.env.NETWORK, + application: process.env.APPLICATION_NAME, + }; + + const { + ACCOUNT_ID, + ALERT_MANAGER_REGION, + ALERT_MANAGER_TOPIC, + } = process.env; + + const QUEUE_URL = `https://sqs.${ALERT_MANAGER_REGION}.amazonaws.com/${ACCOUNT_ID}/${ALERT_MANAGER_TOPIC}`; + + const client = new SQSClient({}); + const command = new SendMessageCommand({ + QueueUrl: QUEUE_URL, + MessageBody: JSON.stringify(preparedMessage), + MessageAttributes: { + None: { + DataType: 'String', + StringValue: '--', + }, + }, + }); + + try { + await client.send(command); + } catch(err) { + logger.error('[ALERT] Erroed while sending message to the alert sqs queue', err); + } +}; diff --git a/packages/wallet-service/src/utils/nft.utils.ts b/packages/wallet-service/src/utils/nft.utils.ts new file mode 100644 index 00000000..838e2096 --- /dev/null +++ b/packages/wallet-service/src/utils/nft.utils.ts @@ -0,0 +1,166 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { LambdaClient, InvokeCommand, InvokeCommandOutput } from '@aws-sdk/client-lambda'; +import { addAlert } from '@src/utils/alerting.utils'; +import { Transaction, Severity } from '@src/types'; +import hathorLib from '@hathor/wallet-lib'; +import createDefaultLogger from '@src/logger'; + +export const MAX_METADATA_UPDATE_RETRIES: number = parseInt(process.env.MAX_METADATA_UPDATE_RETRIES || '3', 10); + +/** + * A helper for generating and updating a NFT Token's metadata. + */ + +/** This env-var based feature toggle can be used to disable this feature */ +export const isNftAutoReviewEnabled = (): boolean => process.env.NFT_AUTO_REVIEW_ENABLED === 'true'; + +export class NftUtils { + /** + * Returns whether we should invoke our NFT handler for this tx + * @param {Transaction} tx + * @returns {boolean} + */ + static shouldInvokeNftHandlerForTx(tx: Transaction): boolean { + return isNftAutoReviewEnabled() && this.isTransactionNFTCreation(tx); + } + + /** + * Returns if the transaction in the parameter is a NFT Creation. + * @param {Transaction} tx + * @returns {boolean} + */ + static isTransactionNFTCreation(tx: Transaction): boolean { + /* + * To fully check if a transaction is a NFT creation, we need to instantiate a new Transaction object in the lib. + * So first we do some very fast checks to filter the bulk of the requests for NFTs with minimum processing. + */ + if ( + tx.version !== hathorLib.constants.CREATE_TOKEN_TX_VERSION // Must be a token creation tx + || !tx.token_name // Must have a token name + || !tx.token_symbol // Must have a token symbol + ) { + return false; + } + + // Continue with a deeper validation + const logger = createDefaultLogger(); + let isNftCreationTx: boolean; + let libTx: hathorLib.CreateTokenTransaction; + + // Transaction parsing failures should be alerted + try { + libTx = hathorLib.helpersUtils.createTxFromHistoryObject(tx); + } catch (ex) { + logger.error('[ALERT] Error when parsing transaction on isTransactionNFTCreation', { + transaction: tx, + error: ex, + }); + + // isTransactionNFTCreation should never throw. We will just raise an alert and exit gracefully. + return false; + } + + // Validate the token: the validateNft will throw if the transaction is not a NFT Creation + try { + libTx.validateNft(new hathorLib.Network(process.env.NETWORK)); + isNftCreationTx = true; + } catch (ex) { + isNftCreationTx = false; + } + + return isNftCreationTx; + } + + /** + * Calls the token metadata on the Explorer Service API to update a token's metadata + * @param {string} nftUid + * @param {Record} metadata + */ + static async _updateMetadata(nftUid: string, metadata: Record): Promise { + const client = new LambdaClient({ + endpoint: process.env.EXPLORER_SERVICE_LAMBDA_ENDPOINT, + region: 'local', + }); + const command = new InvokeCommand({ + FunctionName: `hathor-explorer-service-${process.env.EXPLORER_SERVICE_STAGE}-create_or_update_dag_metadata`, + InvocationType: 'Event', + Payload: JSON.stringify({ + id: nftUid, + metadata, + }), + }); + + const logger = createDefaultLogger(); + let retryCount = 0; + while (retryCount < MAX_METADATA_UPDATE_RETRIES) { + // invoke lambda asynchronously to metadata update + const response: InvokeCommandOutput = await client.send(command); + // Event InvocationType returns 202 for a successful invokation + if (response.StatusCode === 202) { + // End the loop successfully + return response; + } + + logger.warn('Failed metadata update', { + nftUid, + retryCount, + statusCode: response.StatusCode, + message: response.Payload.toString(), + }); + ++retryCount; + } + + // Exceeded retry limit + throw new Error(`Metadata update failed for tx_id: ${nftUid}.`); + } + + /** + * Identifies if the metadata for a NFT needs updating and, if it does, update it. + * @param {string} nftUid + * @returns {Promise} No data is returned after a successful update or skip + */ + static async createOrUpdateNftMetadata(nftUid: string): Promise { + // The explorer service automatically merges the metadata content if it already exists. + const newMetadata = { + id: nftUid, + nft: true, + }; + await NftUtils._updateMetadata(nftUid, newMetadata); + } + + /** + * Invokes this application's own intermediary lambda `onNewNftEvent`. + * This is to improve the failure tolerance on this non-critical step of the sync loop. + */ + static async invokeNftHandlerLambda(txId: string): Promise { + const client = new LambdaClient({ + endpoint: process.env.WALLET_SERVICE_LAMBDA_ENDPOINT, + region: 'local', + }); + // invoke lambda asynchronously to metadata update + const command = new InvokeCommand({ + FunctionName: `hathor-wallet-service-${process.env.STAGE}-onNewNftEvent`, + InvocationType: 'Event', + Payload: JSON.stringify({ nftUid: txId }), + }); + + const response: InvokeCommandOutput = await client.send(command); + + // Event InvocationType returns 202 for a successful invokation + if (response.StatusCode !== 202) { + addAlert( + 'Error on NFTHandler lambda', + 'Erroed on invokeNftHandlerLambda invocation', + Severity.MINOR, + { TxId: txId }, + ); + throw new Error(`onNewNftEvent lambda invoke failed for tx: ${txId}`); + } + } +} diff --git a/packages/wallet-service/src/utils/pushnotification.utils.ts b/packages/wallet-service/src/utils/pushnotification.utils.ts new file mode 100644 index 00000000..ea67b388 --- /dev/null +++ b/packages/wallet-service/src/utils/pushnotification.utils.ts @@ -0,0 +1,289 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { LambdaClient, InvokeCommand, InvokeCommandOutput } from '@aws-sdk/client-lambda'; +import { PushProvider, Severity, SendNotificationToDevice, StringMap, WalletBalanceValue } from '@src/types'; +import fcmAdmin, { credential, messaging, ServiceAccount } from 'firebase-admin'; +import { MulticastMessage } from 'firebase-admin/messaging'; +import createDefaultLogger from '@src/logger'; +import { assertEnvVariablesExistence } from '@src/utils'; +import { addAlert } from '@src/utils/alerting.utils'; + +const logger = createDefaultLogger(); + +try { + assertEnvVariablesExistence([ + 'WALLET_SERVICE_LAMBDA_ENDPOINT', + 'STAGE', + 'FIREBASE_PROJECT_ID', + 'FIREBASE_PRIVATE_KEY_ID', + 'FIREBASE_PRIVATE_KEY', + 'FIREBASE_CLIENT_EMAIL', + 'FIREBASE_CLIENT_ID', + 'FIREBASE_AUTH_URI', + 'FIREBASE_TOKEN_URI', + 'FIREBASE_AUTH_PROVIDER_X509_CERT_URL', + 'FIREBASE_CLIENT_X509_CERT_URL', + ]); +} catch (e) { + logger.error(e); + + addAlert( + 'Lambda missing env variables', + e.message, // This should contain the list of env variables that are missing + Severity.MINOR, + ); +} + +export function buildFunctionName(functionName: string): string { + return `hathor-wallet-service-${process.env.STAGE}-${functionName}`; +} + +export enum FunctionName { + SEND_NOTIFICATION_TO_DEVICE = 'sendNotificationToDevice', + ON_TX_PUSH_NOTIFICATION_REQUESTED = 'txPushRequested', +} + +const STAGE = process.env.STAGE; +const WALLET_SERVICE_LAMBDA_ENDPOINT = process.env.WALLET_SERVICE_LAMBDA_ENDPOINT; +const SEND_NOTIFICATION_FUNCTION_NAME = buildFunctionName(FunctionName.SEND_NOTIFICATION_TO_DEVICE); +const ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME = buildFunctionName(FunctionName.ON_TX_PUSH_NOTIFICATION_REQUESTED); +const FIREBASE_PROJECT_ID = process.env.FIREBASE_PROJECT_ID; +const FIREBASE_PRIVATE_KEY_ID = process.env.FIREBASE_PRIVATE_KEY_ID; +const FIREBASE_CLIENT_EMAIL = process.env.FIREBASE_CLIENT_EMAIL; +const FIREBASE_CLIENT_ID = process.env.FIREBASE_CLIENT_ID; +const FIREBASE_AUTH_URI = process.env.FIREBASE_AUTH_URI; +const FIREBASE_TOKEN_URI = process.env.FIREBASE_TOKEN_URI; +const FIREBASE_AUTH_PROVIDER_X509_CERT_URL = process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL; +const FIREBASE_CLIENT_X509_CERT_URL = process.env.FIREBASE_CLIENT_X509_CERT_URL; +const FIREBASE_PRIVATE_KEY = (() => { + try { + /** + * To fix the error 'Error: Invalid PEM formatted message.', + * when initializing the firebase admin app, we need to replace + * the escaped line break with an unescaped line break. + * https://github.com/gladly-team/next-firebase-auth/discussions/95#discussioncomment-2891225 + */ + const privateKey = process.env.FIREBASE_PRIVATE_KEY; + return privateKey + ? privateKey.replace(/\\n/gm, '\n') + : null; + } catch (error) { + logger.error('[ALERT] Error while parsing the env.FIREBASE_PRIVATE_KEY.'); + return null; + } +})(); + +/** Local feature toggle that disable the push notification by default */ +const PUSH_NOTIFICATION_ENABLED = process.env.PUSH_NOTIFICATION_ENABLED; +/** + * Controls which providers are allowed to send notification when it is enabled + * @example + * PUSH_ALLOWED_PROVIDERS=android,ios + * @remarks + * In the test this constant works like the environment variable constants. + * It needs to be reloaded after changing the underlying environment variable + * `process.env.PUSH_ALLOWED_PROVIDERS`. + * + * @example Reload the constant by reloading the module: + * ```ts + // reload module + const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + * ``` + * */ +const PUSH_ALLOWED_PROVIDERS = (() => { + const providers = process.env.PUSH_ALLOWED_PROVIDERS; + if (!providers) { + // If no providers are set, we allow android by default, but alert the environment variable is empty + logger.error('[ALERT] env.PUSH_ALLOWED_PROVIDERS is empty.'); + return [PushProvider.ANDROID]; + } + return providers.split(','); +})(); + +export const isPushProviderAllowed = (provider: string): boolean => PUSH_ALLOWED_PROVIDERS.includes(provider); + +export const isPushNotificationEnabled = (): boolean => PUSH_NOTIFICATION_ENABLED === 'true'; + +const serviceAccount = { + type: 'service_account', + project_id: FIREBASE_PROJECT_ID, + private_key_id: FIREBASE_PRIVATE_KEY_ID, + private_key: FIREBASE_PRIVATE_KEY, + client_email: FIREBASE_CLIENT_EMAIL, + client_id: FIREBASE_CLIENT_ID, + auth_uri: FIREBASE_AUTH_URI, + token_uri: FIREBASE_TOKEN_URI, + auth_provider_x509_cert_url: FIREBASE_AUTH_PROVIDER_X509_CERT_URL, + client_x509_cert_url: FIREBASE_CLIENT_X509_CERT_URL, +}; + +let firebaseInitialized = false; +if (isPushNotificationEnabled()) { + try { + fcmAdmin.initializeApp({ + credential: credential.cert(serviceAccount as ServiceAccount), + projectId: FIREBASE_PROJECT_ID, + }); + firebaseInitialized = true; + } catch (error) { + logger.error(`Error initializing Firebase Admin SDK. ErrorMessage: ${error.message}`, error); + } +} + +export const isFirebaseInitialized = (): boolean => firebaseInitialized; + +export enum PushNotificationError { + UNKNOWN = 'unknown', + INVALID_DEVICE_ID = 'invalid-device-id', +} + +export class PushNotificationUtils { + public static async sendToFcm(notification: SendNotificationToDevice): Promise<{ success: boolean, errorMessage?: string }> { + if (!isFirebaseInitialized()) { + return { success: false, errorMessage: 'Firebase not initialized.' }; + } + + const message: MulticastMessage = { + tokens: [notification.deviceId], + data: notification.metadata, + android: { + /** + * When the application is in background the OS treat data messages as low priority by default. + * We can change priority to 'high' to attempt deliver the message as soon as possible, + * however FCM can adapt the delivery of the message over time in response to user engagement. + * + * @remarks + * On iOS we can change the priority with the following code. + * + * @code + * { + * ...android, + * apns: { + * payload: { aps: { contentAvailable: true } }, + * }, + * } + */ + priority: 'high', + }, + apns: { + headers: { + /** + * FCM requires priority 5 for data message, other priority is reject with error. + * See https://firebase.google.com/docs/cloud-messaging/concept-options#setting-the-priority-of-a-message + * + */ + 'apns-priority': '5', + }, + payload: { + /** + * Background notification flag. + * It is labeled as low priority and may not be delivered by the platform. + * It is subject to severe throttling. + * + * See Push Background: + * https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app#overview + * + * See Payload key reference: + * https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification#2943360 + */ + aps: { + 'content-available': 1, + }, + }, + }, + }; + const multicastResult = await messaging().sendMulticast(message); + + if (multicastResult.failureCount === 0) { + return { success: true }; + } + + const { 0: { error } } = multicastResult.responses; + if (/token-not-registered/.test(error?.code || '')) { + return { success: false, errorMessage: PushNotificationError.INVALID_DEVICE_ID }; + } + + await addAlert( + 'Error on PushNotificationUtils', + 'Error while calling sendMulticast(message) of Firebase Cloud Message.', + Severity.MAJOR, + { error }, + ); + logger.error('Error while calling sendMulticast(message) of Firebase Cloud Message.', { error }); + return { success: false, errorMessage: PushNotificationError.UNKNOWN }; + } + + /** + * Invokes this application's own intermediary lambda `PushSendNotificationToDevice`. + */ + static async invokeSendNotificationHandlerLambda(notification: SendNotificationToDevice): Promise { + if (!WALLET_SERVICE_LAMBDA_ENDPOINT && !STAGE) { + throw new Error('Environment variables WALLET_SERVICE_LAMBDA_ENDPOINT and STAGE are not set.'); + } + + const client = new LambdaClient({ + endpoint: WALLET_SERVICE_LAMBDA_ENDPOINT, + region: 'local', + }); + + const command = new InvokeCommand({ + FunctionName: SEND_NOTIFICATION_FUNCTION_NAME, + InvocationType: 'Event', + Payload: JSON.stringify(notification), + }); + + const response: InvokeCommandOutput = await client.send(command); + + // Event InvocationType returns 202 for a successful invokation + if (response.StatusCode !== 202) { + await addAlert( + 'Error on PushNotificationUtils', + `${SEND_NOTIFICATION_FUNCTION_NAME} lambda invoke failed for device: ${notification.deviceId}`, + Severity.MINOR, + { DeviceId: notification.deviceId }, + ); + throw new Error(`${SEND_NOTIFICATION_FUNCTION_NAME} lambda invoke failed for device: ${notification.deviceId}`); + } + } + + /** + * Invokes this application's own intermediary lambda `OnTxPushNotificationRequestedLambda`. + * @param walletBalanceValueMap - a map of walletId linked to its wallet balance data. + */ + static async invokeOnTxPushNotificationRequestedLambda(walletBalanceValueMap: StringMap): Promise { + if (!isPushNotificationEnabled()) { + logger.debug('Push notification is disabled. Skipping invocation of OnTxPushNotificationRequestedLambda lambda.'); + return; + } + + const client = new LambdaClient({ + endpoint: WALLET_SERVICE_LAMBDA_ENDPOINT, + region: 'local', + }); + + const command = new InvokeCommand({ + FunctionName: ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME, + InvocationType: 'Event', + Payload: JSON.stringify(walletBalanceValueMap), + }); + + const response: InvokeCommandOutput = await client.send(command); + + // Event InvocationType returns 202 for a successful invokation + const walletIdList = Object.keys(walletBalanceValueMap); + if (response.StatusCode !== 202) { + await addAlert( + 'Error on PushNotificationUtils', + `${ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME} lambda invoke failed for wallets`, + Severity.MINOR, + { Wallets: walletIdList }, + ); + throw new Error(`${ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME} lambda invoke failed for wallets: ${walletIdList}`); + } + } +} diff --git a/packages/wallet-service/src/ws/admin.ts b/packages/wallet-service/src/ws/admin.ts new file mode 100644 index 00000000..02ff8bc0 --- /dev/null +++ b/packages/wallet-service/src/ws/admin.ts @@ -0,0 +1,108 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Handler } from 'aws-lambda'; +import Joi from 'joi'; + +import { ApiError } from '@src/api/errors'; +import { + sendMessageToClient, + disconnectClient, +} from '@src/ws/utils'; + +import { + getRedisClient, + closeRedisClient, + wsGetConnection, + wsGetAllConnections, + wsGetWalletConnections, +} from '@src/redis'; + +const multicastSchema = Joi.object({ + wallets: Joi.array() + .items(Joi.string()) + .required(), + payload: Joi.object().required(), +}); + +const disconnectSchema = Joi.object({ + connections: Joi.array() + .items(Joi.string()) + .required(), +}); + +export const broadcast: Handler = async (event) => { + const redisClient = getRedisClient(); + const connections = await wsGetAllConnections(redisClient); + await Promise.all(connections.map((connInfo) => ( + sendMessageToClient(redisClient, connInfo, event) + ))); + await closeRedisClient(redisClient); + return { + success: true, + message: 'ok', + }; +}; + +export const multicast: Handler = async (event) => { + const { value, error } = multicastSchema.validate(event, { + abortEarly: false, + convert: false, + }); + + if (error) { + return { + success: false, + message: ApiError.INVALID_BODY, + }; + } + + const wallets = value.wallets; + const payload = value.payload; + + const redisClient = getRedisClient(); + + // for each wallet, get connections and send payload to each connection of each wallet + await Promise.all(wallets.map((walletId) => ( + wsGetWalletConnections(redisClient, walletId).then((connections) => ( + Promise.all(connections.map((connInfo) => ( + sendMessageToClient(redisClient, connInfo, payload) + ))) + )) + ))); + await closeRedisClient(redisClient); + return { + success: true, + message: 'ok', + }; +}; + +export const disconnect: Handler = async (event) => { + const { value, error } = disconnectSchema.validate(event, { + abortEarly: false, + convert: false, + }); + + if (error) { + return { + success: false, + message: ApiError.INVALID_BODY, + }; + } + + const connectionIds = value.connections; + + const redisClient = getRedisClient(); + await Promise.all(connectionIds.map((connId) => ( + wsGetConnection(redisClient, connId).then((connURL) => disconnectClient(redisClient, { id: connId, url: connURL })) + ))); + await closeRedisClient(redisClient); + return { + success: true, + message: 'ok', + }; +}; diff --git a/packages/wallet-service/src/ws/connection.ts b/packages/wallet-service/src/ws/connection.ts new file mode 100644 index 00000000..1423b97e --- /dev/null +++ b/packages/wallet-service/src/ws/connection.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + APIGatewayProxyEvent, + APIGatewayProxyResult, +} from 'aws-lambda'; +import { + connectionInfoFromEvent, + sendMessageToClient, + DEFAULT_API_GATEWAY_RESPONSE, +} from '@src/ws/utils'; +import { + getRedisClient, + closeRedisClient, + initWsConnection, + endWsConnection, +} from '@src/redis'; +import { Severity } from '@src/types'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import createDefaultLogger from '@src/logger'; +import { addAlert } from '@src/utils/alerting.utils'; + +const mysql = getDbConnection(); +const logger = createDefaultLogger(); + +export const connect = async ( + event: APIGatewayProxyEvent, +): Promise => { + try { + const redisClient = getRedisClient(); + const routeKey = event.requestContext.routeKey; + // info needed to send response to client + const connInfo = connectionInfoFromEvent(event); + + if (routeKey === '$connect') { + await initWsConnection(redisClient, connInfo); + } + + if (routeKey === '$disconnect') { + await endWsConnection(redisClient, connInfo.id); + } + + if (routeKey === 'ping') { + await sendMessageToClient(redisClient, connInfo, { type: 'pong' }); + } + + await closeRedisClient(redisClient); + await closeDbConnection(mysql); + } catch (e) { + await addAlert( + 'Captured error on connect websocket lambda', + '-', + Severity.MINOR, + { error: e.message }, + ); + + logger.error('Captured error on connect websocket lambda', e); + } + + return DEFAULT_API_GATEWAY_RESPONSE; +}; diff --git a/packages/wallet-service/src/ws/join.ts b/packages/wallet-service/src/ws/join.ts new file mode 100644 index 00000000..8f0fc0aa --- /dev/null +++ b/packages/wallet-service/src/ws/join.ts @@ -0,0 +1,102 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; +import { ServerlessMysql } from 'serverless-mysql'; +import { RedisClient } from 'redis'; +import Joi from 'joi'; + +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { + connectionInfoFromEvent, + sendMessageToClient, + DEFAULT_API_GATEWAY_RESPONSE, +} from '@src/ws/utils'; +import { + getRedisClient, + closeRedisClient, + wsJoinWallet, +} from '@src/redis'; +import { WsConnectionInfo } from '@src/types'; +import { getWallet } from '@src/db'; + +const mysql = getDbConnection(); + +const joinSchema = Joi.object({ + action: Joi.string() + .required(), + id: Joi.string() + .required(), +}); + +const parseBody = (body: string) => { + try { + return JSON.parse(body); + } catch (e) { + return null; + } +}; + +export const handler = async ( + event: APIGatewayProxyEvent, +): Promise => { + const redisClient = getRedisClient(); + const connInfo = connectionInfoFromEvent(event); + + await joinWallet(event, connInfo, mysql, redisClient); + await closeDbConnection(mysql); + await closeRedisClient(redisClient); + + // Since this is served by ApiGateway, we need to return a APIGatewayProxyResult + return DEFAULT_API_GATEWAY_RESPONSE; +}; + +const joinWallet = async ( + event: APIGatewayProxyEvent, + connInfo: WsConnectionInfo, + _mysql: ServerlessMysql, + _client: RedisClient, +): Promise => { + // parse body and extract wallet + const body = parseBody(event.body); + const { value, error } = joinSchema.validate(body, { + abortEarly: false, + convert: true, + }); + + if (error) { + await sendMessageToClient(_client, connInfo, { + type: 'error', + message: 'Invalid parameters', + }); + return DEFAULT_API_GATEWAY_RESPONSE; + } + + // TODO: Verify ownership of the wallet upon subscription. + // How: Do not pass walletId directly, use jwt token (same as the api bearer token) + // and validate the token, then use the walletId inside the token. + const walletId = value.id; + + const wallet = await getWallet(_mysql, walletId); + if (wallet === null) { + // wallet does not exist, but should we return an error? + await sendMessageToClient(_client, connInfo, { + type: 'error', + message: 'Invalid parameters', + }); + return DEFAULT_API_GATEWAY_RESPONSE; + } + + await wsJoinWallet(_client, connInfo, walletId); + await sendMessageToClient(_client, connInfo, { + type: 'join-success', + message: 'Listening', + id: walletId, + }); + + return DEFAULT_API_GATEWAY_RESPONSE; +}; diff --git a/packages/wallet-service/src/ws/txNotify.ts b/packages/wallet-service/src/ws/txNotify.ts new file mode 100644 index 00000000..3068d96b --- /dev/null +++ b/packages/wallet-service/src/ws/txNotify.ts @@ -0,0 +1,132 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { SQSHandler } from 'aws-lambda'; +import createDefaultLogger from '@src/logger'; +import Joi from 'joi'; + +import { sendMessageToClient } from '@src/ws/utils'; +import { + wsGetWalletConnections, + getRedisClient, + closeRedisClient, +} from '@src/redis'; + +const logger = createDefaultLogger(); + +const parseBody = (body: string) => { + try { + return JSON.parse(body); + } catch (e) { + return null; + } +}; + +const newTxbodySchema = Joi.object({ + wallets: Joi.array() + .items(Joi.string()) + .min(1) + .required(), + tx: Joi.object().required(), +}); + +const updateTxbodySchema = Joi.object({ + wallets: Joi.array() + .items(Joi.string()) + .min(1) + .required(), + update: Joi.object({ + tx_id: Joi.string().required(), + is_voided: Joi.boolean(), + }) + .required(), +}); + +export const onNewTx: SQSHandler = async (event) => { + const redisClient = getRedisClient(); + const promises = []; + + for (const evt of event.Records) { + const body = parseBody(evt.body); + const { value, error } = newTxbodySchema.validate(body, { + abortEarly: false, + convert: true, + }); + + if (error) { + // invalid event bodies will noop + logger.error('Error parsing body'); + logger.error(error); + + continue; + } + + const wallets = value.wallets; + const tx = value.tx; + + const payload = { + type: 'new-tx', + data: tx, + }; + + // This will create a promise that for each walletId on wallets it will search for all open connections + // and for each connection send the payload (the JSON representation of the tx) using sendMessageToClient + promises.push( + Promise.all(wallets.map((walletId) => ( + wsGetWalletConnections(redisClient, walletId).then((connections) => ( + Promise.all(connections.map((connInfo) => ( + sendMessageToClient(redisClient, connInfo, payload) + ))) + )) + ))), + ); + } + // Wait all messages from all events to be sent + await Promise.all(promises); + // And close the redisClient + await closeRedisClient(redisClient); +}; + +export const onUpdateTx: SQSHandler = async (event) => { + const redisClient = getRedisClient(); + const promises = []; + + for (const evt of event.Records) { + const body = parseBody(evt.body); + const { value, error } = updateTxbodySchema.validate(body, { + abortEarly: false, + convert: true, + }); + + if (error) { + // invalid event bodies will noop + // maybe log errors + continue; + } + + const wallets = value.wallets; + const updateBody = value.update; + const payload = { + type: 'update-tx', + data: updateBody, + }; + + // Same logic as onNewTx, but sending `updateBody` as payload + promises.push( + Promise.all(wallets.map((walletId) => ( + wsGetWalletConnections(redisClient, walletId).then((connections) => ( + Promise.all(connections.map((connInfo) => ( + sendMessageToClient(redisClient, connInfo, payload) + ))) + )) + ))), + ); + } + // Wait all messages from all events to be sent + await Promise.all(promises); + await closeRedisClient(redisClient); +}; diff --git a/packages/wallet-service/src/ws/utils.ts b/packages/wallet-service/src/ws/utils.ts new file mode 100644 index 00000000..0a05ba6d --- /dev/null +++ b/packages/wallet-service/src/ws/utils.ts @@ -0,0 +1,93 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; +import { RedisClient } from 'redis'; +import { addAlert } from '@src/utils/alerting.utils'; +import { + ApiGatewayManagementApiClient, + PostToConnectionCommand, + PostToConnectionCommandOutput, + DeleteConnectionCommand, + DeleteConnectionCommandOutput, +} from '@aws-sdk/client-apigatewaymanagementapi'; +import util from 'util'; + +import { WsConnectionInfo, Severity } from '@src/types'; +import { endWsConnection } from '@src/redis'; + +export const connectionInfoFromEvent = ( + event: APIGatewayProxyEvent, +): WsConnectionInfo => { + const connID = event.requestContext.connectionId; + if (process.env.IS_OFFLINE === 'true') { + // This will enter when running the service on serverless offline mode + return { + id: connID, + url: 'http://localhost:3001', + }; + } + + const domain = process.env.WS_DOMAIN; + + if (!domain) { + addAlert( + 'Erroed while fetching connection info', + 'Domain not on env variables', + Severity.MINOR, + ); + + // Throw so we receive an alert telling us that something is wrong with the env variable + // instead of trying to invoke a lambda at https://undefined + throw new Error('Domain not on env variables'); + } + + return { + id: connID, + url: util.format('https://%s', domain), + }; +}; + +export const sendMessageToClient = async ( + client: RedisClient, + connInfo: WsConnectionInfo, + payload: any, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any +): Promise => { // eslint-disable-line @typescript-eslint/no-explicit-any + const apiGwClient = new ApiGatewayManagementApiClient({ + endpoint: connInfo.url, + }); + + const command = new PostToConnectionCommand({ + ConnectionId: connInfo.id, + Data: JSON.stringify(payload), + }); + + const response: PostToConnectionCommandOutput = await apiGwClient.send(command); + // http GONE(410) means client is disconnected, but still exists on our connection store + if (response.$metadata.httpStatusCode === 410) { + // cleanup connection and subscriptions from redis if GONE + return endWsConnection(client, connInfo.id); + } +}; + +export const disconnectClient = async ( + client: RedisClient, + connInfo: WsConnectionInfo, +): Promise => { // eslint-disable-line @typescript-eslint/no-explicit-any + const apiGwClient = new ApiGatewayManagementApiClient({ + endpoint: connInfo.url, + }); + + const command = new DeleteConnectionCommand({ + ConnectionId: connInfo.id, + }); + + const response: DeleteConnectionCommandOutput = await apiGwClient.send(command); + + if (response.$metadata.httpStatusCode === 410) { + // cleanup connection and subscriptions from redis if GONE + return endWsConnection(client, connInfo.id); + } +}; + +export const DEFAULT_API_GATEWAY_RESPONSE: APIGatewayProxyResult = { + statusCode: 200, + body: '', +}; diff --git a/packages/wallet-service/tests/api.test.ts b/packages/wallet-service/tests/api.test.ts new file mode 100644 index 00000000..4290358a --- /dev/null +++ b/packages/wallet-service/tests/api.test.ts @@ -0,0 +1,2177 @@ +import { APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda'; + +import { get as addressesGet, checkMine } from '@src/api/addresses'; +import { get as newAddressesGet } from '@src/api/newAddresses'; +import { get as balancesGet } from '@src/api/balances'; +import { get as txHistoryGet } from '@src/api/txhistory'; +import { get as walletTokensGet, getTokenDetails } from '@src/api/tokens'; +import { get as getVersionDataGet } from '@src/api/version'; +import { + getTransactionById, + getConfirmationData, + queryGraphvizNeighbours, +} from '@src/api/fullnodeProxy'; +import { create as txProposalCreate } from '@src/api/txProposalCreate'; +import { send as txProposalSend } from '@src/api/txProposalSend'; +import { destroy as txProposalDestroy } from '@src/api/txProposalDestroy'; +import { getFilteredUtxos, getFilteredTxOutputs } from '@src/api/txOutputs'; +import { + get as walletGet, + load as walletLoad, + loadWallet, + changeAuthXpub, +} from '@src/api/wallet'; +import { + updateVersionData, +} from '@src/db'; +import * as Wallet from '@src/api/wallet'; +import * as Db from '@src/db'; +import { ApiError } from '@src/api/errors'; +import { closeDbConnection, getDbConnection, getUnixTimestamp, getWalletId } from '@src/utils'; +import { STATUS_CODE_TABLE } from '@src/api/utils'; +import { WalletStatus, FullNodeVersionData } from '@src/types'; +import { walletUtils, constants, network, HathorWalletServiceWallet } from '@hathor/wallet-lib'; +import bitcore from 'bitcore-lib'; +import { + ADDRESSES, + TX_IDS, + XPUBKEY, + AUTH_XPUBKEY, + TEST_SEED, + addToAddressTable, + addToAddressBalanceTable, + addToAddressTxHistoryTable, + addToTokenTable, + addToUtxoTable, + addToWalletBalanceTable, + addToWalletTable, + addToWalletTxHistoryTable, + addToTransactionTable, + cleanDatabase, + makeGatewayEvent, + makeGatewayEventWithAuthorizer, + getAuthData, + getXPrivKeyFromSeed, +} from '@tests/utils'; +import fullnode from '@src/fullnode'; +import { getHealthcheck } from '@src/api/healthcheck'; +import { ping } from "@src/redis"; + +// Monkey patch bitcore-lib + +bitcore.Message.MAGIC_BYTES = Buffer.from('Hathor Signed Message:\n'); + +const mysql = getDbConnection(); + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + +afterAll(async () => { + await closeDbConnection(mysql); +}); + +const _testCORSHeaders = async (fn: APIGatewayProxyHandler, walletId: string, params = null) => { + const event = makeGatewayEventWithAuthorizer(walletId, params); + // This is a hack to force middy to include the CORS headers, we can't know what http method our request + // uses as it is only defined on serverless.yml + event.httpMethod = 'XXX'; + const result = await fn(event, null, null) as APIGatewayProxyResult; + + expect(result.headers).toStrictEqual( + expect.objectContaining({ + 'Access-Control-Allow-Origin': '*', // This is the default origin makeGatewayEventWithAuthorizer returns on headers + }), + ); +}; + +const _testInvalidPayload = async (fn: APIGatewayProxyHandler, errorMessages: string[] = [], walletId: string, params = null) => { + const event = makeGatewayEventWithAuthorizer(walletId, params); + const result = await fn(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); + + const messages = returnBody.details.map((detail) => detail.message); + + expect(messages).toHaveLength(errorMessages.length); + expect(messages).toStrictEqual(errorMessages); +}; + +const _testMissingWallet = async (fn: APIGatewayProxyHandler, walletId: string, body = null) => { + const event = makeGatewayEventWithAuthorizer(walletId, null, body && JSON.stringify(body)); + const result = await fn(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(404); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.WALLET_NOT_FOUND); +}; + +const _testWalletNotReady = async (fn: APIGatewayProxyHandler) => { + const walletId = 'wallet-not-started'; + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: 'aaaa', + authXpubkey: AUTH_XPUBKEY, + status: 'creating', + maxGap: 5, + createdAt: 10000, + readyAt: null, + }]); + const event = makeGatewayEventWithAuthorizer(walletId, null); + const result = await fn(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.WALLET_NOT_READY); +}; + +test('GET /addresses', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const addresses = [ + { address: ADDRESSES[0], index: 0, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[1], index: 1, walletId: 'my-wallet', transactions: 0 }, + ]; + + await addToAddressTable(mysql, addresses); + + // missing wallet + await _testMissingWallet(addressesGet, 'some-wallet'); + + // wallet not ready + await _testWalletNotReady(addressesGet); + + await _testCORSHeaders(addressesGet, 'my-wallet', {}); + + // success case + let event = makeGatewayEventWithAuthorizer('my-wallet', null); + let result = await addressesGet(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.addresses).toHaveLength(2); + expect(returnBody.addresses).toContainEqual({ + address: addresses[0].address, + index: addresses[0].index, + transactions: addresses[0].transactions, + }); + expect(returnBody.addresses).toContainEqual({ + address: addresses[1].address, + index: addresses[1].index, + transactions: addresses[1].transactions, + }); + + // we should error on invalid index parameter + event = makeGatewayEventWithAuthorizer('my-wallet', { + index: '-50', + }); + result = await addressesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(STATUS_CODE_TABLE[ApiError.INVALID_PAYLOAD]); + expect(returnBody.details).toHaveLength(1); + expect(returnBody.details[0].message) + .toMatchInlineSnapshot('"\"index\" must be greater than or equal to 0"'); + + // we should be able to filter for a specific index + event = makeGatewayEventWithAuthorizer('my-wallet', { + index: String(addresses[0].index), + }); + result = await addressesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.addresses).toHaveLength(1); + expect(returnBody.addresses).toStrictEqual([{ + address: addresses[0].address, + index: addresses[0].index, + transactions: addresses[0].transactions, + }]); + + // we should receive ApiError.ADDRESS_NOT_FOUND if the address was not found + event = makeGatewayEventWithAuthorizer('my-wallet', { + index: '150', + }); + result = await addressesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(404); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.ADDRESS_NOT_FOUND); +}); + +test('GET /addresses/check_mine', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + await addToAddressTable(mysql, [ + { address: ADDRESSES[0], index: 0, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[1], index: 1, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[2], index: 3, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[3], index: 4, walletId: 'my-wallet', transactions: 0 }, + ]); + + // missing wallet + await _testMissingWallet(newAddressesGet, 'some-wallet'); + + // wallet not ready + await _testWalletNotReady(checkMine); + + await _testCORSHeaders(checkMine, 'my-wallet', {}); + + // success case + + let event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + addresses: [ + ADDRESSES[0], + ADDRESSES[1], + ADDRESSES[8], + ], + })); + let result = await checkMine(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(Object.keys(returnBody.addresses)).toHaveLength(3); + expect(returnBody.addresses).toStrictEqual({ + [ADDRESSES[0]]: true, + [ADDRESSES[1]]: true, + [ADDRESSES[8]]: false, + }); + + // validation error, invalid json + + event = makeGatewayEventWithAuthorizer('my-wallet', null, 'invalid-json'); + result = await checkMine(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); + expect(returnBody.details).toHaveLength(1); + expect(returnBody.details[0].message).toStrictEqual('"value" must be of type object'); + + // validation error, addresses shouldn't be empty + + event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + addresses: [], + })); + result = await checkMine(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); + expect(returnBody.details).toHaveLength(1); + expect(returnBody.details[0].message).toStrictEqual('"addresses" must contain at least 1 items'); + + // validation error, invalid address + + event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + addresses: [ + 'invalid-address', + ], + })); + result = await checkMine(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); + expect(returnBody.details).toHaveLength(2); + expect(returnBody.details[0].message).toStrictEqual('"addresses[0]" with value "invalid-address" fails to match the required pattern: /^[A-HJ-NP-Za-km-z1-9]*$/'); + expect(returnBody.details[1].message).toStrictEqual('"addresses[0]" length must be at least 34 characters long'); +}); + +test('GET /addresses/new', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + highestUsedIndex: 4, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [ + { address: ADDRESSES[0], index: 0, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[1], index: 1, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[2], index: 2, walletId: 'my-wallet', transactions: 2 }, + { address: ADDRESSES[3], index: 3, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[4], index: 4, walletId: 'my-wallet', transactions: 3 }, + { address: ADDRESSES[5], index: 5, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[6], index: 6, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[7], index: 7, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[8], index: 8, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[9], index: 0, walletId: null, transactions: 0 }, + { address: ADDRESSES[10], index: 0, walletId: 'test', transactions: 0 }, + ]); + + // missing wallet + await _testMissingWallet(newAddressesGet, 'some-wallet'); + + // wallet not ready + await _testWalletNotReady(newAddressesGet); + + await _testCORSHeaders(newAddressesGet, 'some-wallet', {}); + + // success case + const event = makeGatewayEventWithAuthorizer('my-wallet', null); + const result = await newAddressesGet(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.addresses).toHaveLength(4); + expect(returnBody.addresses).toContainEqual({ address: ADDRESSES[5], index: 5, addressPath: "m/44'/280'/0'/0/5" }); + expect(returnBody.addresses).toContainEqual({ address: ADDRESSES[6], index: 6, addressPath: "m/44'/280'/0'/0/6" }); + expect(returnBody.addresses).toContainEqual({ address: ADDRESSES[7], index: 7, addressPath: "m/44'/280'/0'/0/7" }); + expect(returnBody.addresses).toContainEqual({ address: ADDRESSES[8], index: 8, addressPath: "m/44'/280'/0'/0/8" }); +}); + +test('GET /addresses/new with no transactions', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + await addToAddressTable(mysql, [ + { address: ADDRESSES[0], index: 0, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[1], index: 1, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[2], index: 2, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[3], index: 3, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[4], index: 4, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[5], index: 5, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[6], index: 6, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[7], index: 7, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[8], index: 8, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[9], index: 0, walletId: null, transactions: 0 }, + { address: ADDRESSES[10], index: 0, walletId: 'test', transactions: 0 }, + ]); + + // missing wallet + await _testMissingWallet(newAddressesGet, 'some-wallet'); + + // wallet not ready + await _testWalletNotReady(newAddressesGet); + + // success case + const event = makeGatewayEventWithAuthorizer('my-wallet', null); + const result = await newAddressesGet(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.addresses).toHaveLength(5); // max gap for this wallet is 5 + expect(returnBody.addresses).toContainEqual({ address: ADDRESSES[0], index: 0, addressPath: "m/44'/280'/0'/0/0" }); + expect(returnBody.addresses).toContainEqual({ address: ADDRESSES[1], index: 1, addressPath: "m/44'/280'/0'/0/1" }); + expect(returnBody.addresses).toContainEqual({ address: ADDRESSES[2], index: 2, addressPath: "m/44'/280'/0'/0/2" }); + expect(returnBody.addresses).toContainEqual({ address: ADDRESSES[3], index: 3, addressPath: "m/44'/280'/0'/0/3" }); + expect(returnBody.addresses).toContainEqual({ address: ADDRESSES[4], index: 4, addressPath: "m/44'/280'/0'/0/4" }); +}); + +test('GET /balances', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + // add the hathor token as it will be deleted by the beforeAll + const htrToken = { id: '00', name: 'Hathor', symbol: 'HTR' }; + // add tokens + const token1 = { id: 'token1', name: 'MyToken1', symbol: 'MT1' }; + const token2 = { id: 'token2', name: 'MyToken2', symbol: 'MT2' }; + const token3 = { id: 'token3', name: 'MyToken3', symbol: 'MT3' }; + const token4 = { id: 'token4', name: 'MyToken4', symbol: 'MT4' }; + await addToTokenTable(mysql, [ + { ...htrToken, transactions: 0 }, + { id: token1.id, name: token1.name, symbol: token1.symbol, transactions: 0 }, + { id: token2.id, name: token2.name, symbol: token2.symbol, transactions: 0 }, + { id: token3.id, name: token3.name, symbol: token3.symbol, transactions: 0 }, + { id: token4.id, name: token4.name, symbol: token4.symbol, transactions: 0 }, + ]); + + // missing wallet + await _testMissingWallet(balancesGet, 'some-wallet'); + + // wallet not ready + await _testWalletNotReady(balancesGet); + + // check CORS headers + await _testCORSHeaders(balancesGet, 'my-wallet', {}); + + // success but no balances + let event = makeGatewayEventWithAuthorizer('my-wallet', null); + let result = await balancesGet(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.balances).toHaveLength(0); + + // add 2 balances + const lockExpires = getUnixTimestamp() + 200; + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token1', + unlockedBalance: 10, + lockedBalance: 0, + unlockedAuthorities: 0b01, + lockedAuthorities: 0b10, + timelockExpires: null, + transactions: 3, + }, { + walletId: 'my-wallet', + tokenId: 'token2', + unlockedBalance: 3, + lockedBalance: 2, + unlockedAuthorities: 0b00, + lockedAuthorities: 0b11, + timelockExpires: lockExpires, + transactions: 1, + }]); + + // get all balances + event = makeGatewayEventWithAuthorizer('my-wallet', null); + result = await balancesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.balances).toHaveLength(2); + expect(returnBody.balances).toContainEqual({ + token: token1, + transactions: 3, + balance: { unlocked: 10, locked: 0 }, + lockExpires: null, + tokenAuthorities: { unlocked: { mint: true, melt: false }, locked: { mint: false, melt: true } }, + }); + expect(returnBody.balances).toContainEqual({ + token: token2, + transactions: 1, + balance: { unlocked: 3, locked: 2 }, + lockExpires, + tokenAuthorities: { unlocked: { mint: false, melt: false }, locked: { mint: true, melt: true } }, + }); + + // get token1 balance + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: 'token1' }); + result = await balancesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.balances).toHaveLength(1); + expect(returnBody.balances).toContainEqual({ + token: token1, + transactions: 3, + balance: { unlocked: 10, locked: 0 }, + lockExpires: null, + tokenAuthorities: { unlocked: { mint: true, melt: false }, locked: { mint: false, melt: true } }, + }); + + // balance that needs to be refreshed + const lockExpires2 = getUnixTimestamp() - 200; + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + await addToAddressBalanceTable(mysql, [[ADDRESSES[0], 'token3', 5, 1, lockExpires2, 2, 0, 0, 10]]); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token3', + unlockedBalance: 5, + lockedBalance: 1, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: lockExpires2, + transactions: 2, + }]); + await addToUtxoTable(mysql, [{ + txId: 'txId', + index: 0, + tokenId: 'token3', + address: ADDRESSES[0], + value: 1, + authorities: 0, + timelock: lockExpires2, + heightlock: null, + locked: true, + spentBy: null, + }]); + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: 'token3' }); + result = await balancesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.balances).toHaveLength(1); + expect(returnBody.balances).toContainEqual({ + token: token3, + transactions: 2, + balance: { unlocked: 6, locked: 0 }, + lockExpires: null, + tokenAuthorities: { unlocked: { mint: false, melt: false }, locked: { mint: false, melt: false } }, + }); + + // balance that needs to be refreshed, but there's another locked utxo in the future + await addToAddressBalanceTable(mysql, [[ADDRESSES[0], 'token4', 10, 5, lockExpires2, 3, 0, 0, 30]]); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token4', + unlockedBalance: 10, + lockedBalance: 5, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: lockExpires2, + transactions: 3, + }]); + await addToUtxoTable(mysql, [{ + txId: 'txId2', + index: 0, + tokenId: 'token4', + address: ADDRESSES[0], + value: 3, + authorities: 0, + timelock: lockExpires2, + heightlock: null, + locked: true, + spentBy: null, + }, { + txId: 'txId3', + index: 0, + tokenId: 'token4', + address: ADDRESSES[0], + value: 2, + authorities: 0, + timelock: lockExpires, + heightlock: null, + locked: true, + spentBy: null, + }]); + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: 'token4' }); + result = await balancesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.balances).toHaveLength(1); + expect(returnBody.balances).toContainEqual({ + token: token4, + transactions: 3, + balance: { unlocked: 13, locked: 2 }, + lockExpires, + tokenAuthorities: { unlocked: { mint: false, melt: false }, locked: { mint: false, melt: false } }, + }); + + // add HTR balance + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: '00', + unlockedBalance: 10, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 3, + }]); + + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: '00' }); + result = await balancesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.balances).toHaveLength(1); + expect(returnBody.balances).toContainEqual({ + token: { id: '00', name: 'Hathor', symbol: 'HTR' }, + transactions: 3, + balance: { unlocked: 10, locked: 0 }, + lockExpires: null, + tokenAuthorities: { unlocked: { mint: false, melt: false }, locked: { mint: false, melt: false } }, + }); +}); + +test('GET /txhistory', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToWalletTxHistoryTable(mysql, [ + ['my-wallet', 'tx1', '00', 5, 1000, false], + ['my-wallet', 'tx1', 'token2', '7', 1000, false], + ['my-wallet', 'tx2', '00', 7, 1001, false], + ['my-wallet', 'tx2', 'token3', 7, 1001, true], + ]); + await addToTransactionTable(mysql, [ + ['tx1', 100, 2, false, null, 60], + ['tx2', 100, 3, false, null, 60], + ]); + + // check CORS headers + await _testCORSHeaders(txHistoryGet, 'my-wallet', {}); + + // missing wallet + await _testMissingWallet(txHistoryGet, 'some-wallet'); + + // wallet not ready + await _testWalletNotReady(txHistoryGet); + + // invalid 'skip' param + await _testInvalidPayload(txHistoryGet, ['"skip" must be a number'], 'my-wallet', { skip: 'aaa' }); + + // invalid 'count' param + await _testInvalidPayload(txHistoryGet, ['"count" must be a number'], 'my-wallet', { count: 'aaa' }); + + // without token in parameters, use htr + let event = makeGatewayEventWithAuthorizer('my-wallet', null); + let result = await txHistoryGet(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.history).toHaveLength(2); + expect(returnBody.history).toContainEqual({ txId: 'tx1', timestamp: 1000, balance: 5, voided: 0, version: 2 }); + expect(returnBody.history).toContainEqual({ txId: 'tx2', timestamp: 1001, balance: 7, voided: 0, version: 3 }); + + // with count just 1, return only the most recent tx + event = makeGatewayEventWithAuthorizer('my-wallet', { count: '1' }); + result = await txHistoryGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.count).toBe(1); + expect(returnBody.history).toHaveLength(1); + expect(returnBody.history).toContainEqual({ txId: 'tx2', timestamp: 1001, balance: 7, voided: 0, version: 3 }); + + // skip first item + event = makeGatewayEventWithAuthorizer('my-wallet', { skip: '1' }); + result = await txHistoryGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.skip).toBe(1); + expect(returnBody.history).toHaveLength(1); + expect(returnBody.history).toContainEqual({ txId: 'tx1', timestamp: 1000, balance: 5, voided: 0, version: 2 }); + + // use other token id + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: 'token2' }); + result = await txHistoryGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.history).toHaveLength(1); + expect(returnBody.history).toContainEqual({ txId: 'tx1', timestamp: 1000, balance: 7, voided: 0, version: 2 }); + + // it should also return voided transactions + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: 'token3' }); + result = await txHistoryGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.history).toHaveLength(1); + expect(returnBody.history).toContainEqual({ txId: 'tx2', timestamp: 1001, balance: 7, voided: 1, version: 3 }); +}); + +test('GET /wallet', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + // check CORS headers + await _testCORSHeaders(walletGet, 'some-wallet', {}); + + // missing wallet + await _testMissingWallet(walletGet, 'some-wallet'); + + // get all balances + const event = makeGatewayEventWithAuthorizer('my-wallet', null); + const result = await walletGet(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.status).toStrictEqual({ + walletId: getWalletId(XPUBKEY), + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + status: 'ready', + maxGap: 5, + retryCount: 0, + createdAt: 10000, + readyAt: 10001, + }); +}); + +test('POST /wallet', async () => { + expect.hasAssertions(); + + // check CORS headers + await _testCORSHeaders(walletLoad, null, {}); + + // invalid body + let event = makeGatewayEvent({}); + let result = await walletLoad(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); + + event = makeGatewayEvent({}, 'aaa'); + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); + expect(returnBody.details).toHaveLength(1); + expect(returnBody.details[0].message).toStrictEqual('"value" must be of type object'); + + // missing xpubkey, auth_xpubkey, signatures and timestamp + event = makeGatewayEvent({}, JSON.stringify({ param1: 'aaa', firstAddress: 'a' })); + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); + expect(returnBody.details).toHaveLength(6); + expect(returnBody.details[0].message).toStrictEqual('"xpubkey" is required'); + expect(returnBody.details[1].message).toStrictEqual('"authXpubkey" is required'); + expect(returnBody.details[2].message).toStrictEqual('"xpubkeySignature" is required'); + expect(returnBody.details[3].message).toStrictEqual('"authXpubkeySignature" is required'); + expect(returnBody.details[4].message).toStrictEqual('"timestamp" is required'); + expect(returnBody.details[5].message).toStrictEqual('"param1" is not allowed'); + + // get the first address + const xpubChangeDerivation = walletUtils.xpubDeriveChild(XPUBKEY, 0); + const firstAddress = walletUtils.getAddressAtIndex(xpubChangeDerivation, 0, process.env.NETWORK); + + // Wrong first address + event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + xpubkeySignature: 'xpubkeySignature', + authXpubkeySignature: 'authXpubkeySignature', + timestamp: Math.floor(Date.now() / 1000), + firstAddress: 'a', + })); + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); + expect(returnBody.message).toStrictEqual(`Expected first address to be a but it is ${firstAddress}`); + + // Clean database so our pubkey is free to be used again: + + await cleanDatabase(mysql); + + const spy = jest.spyOn(Wallet, 'invokeLoadWalletAsync'); + + const mockImplementationSuccess = jest.fn(() => Promise.resolve()); + const mockImplementationFailure = jest.fn(() => Promise.reject(new Error('error!'))); + + let mockFn = spy.mockImplementation(mockImplementationSuccess); + + // we need signatures for both the account path and the purpose path: + const now = Math.floor(Date.now() / 1000); + const walletId = getWalletId(XPUBKEY); + const xpriv = getXPrivKeyFromSeed(TEST_SEED, { + passphrase: '', + networkName: process.env.NETWORK, + }); + + // account path + const accountDerivationIndex = '0\''; + + const derivedPrivKey = walletUtils.deriveXpriv(xpriv, accountDerivationIndex); + const address = derivedPrivKey.publicKey.toAddress(network.getNetwork()).toString(); + const message = new bitcore.Message(String(now).concat(walletId).concat(address)); + const xpubkeySignature = message.sign(derivedPrivKey.privateKey); + + // auth purpose path (m/280'/280') + const authDerivedPrivKey = HathorWalletServiceWallet.deriveAuthPrivateKey(xpriv); + const authAddress = authDerivedPrivKey.publicKey.toAddress(network.getNetwork()); + const authMessage = new bitcore.Message(String(now).concat(walletId).concat(authAddress)); + const authXpubkeySignature = authMessage.sign(authDerivedPrivKey.privateKey); + + // Load success + event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + xpubkeySignature, + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature, + firstAddress, + timestamp: now, + })); + + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + expect(result.statusCode).toBe(200); + + // already loaded + event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + xpubkeySignature, + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature, + firstAddress, + timestamp: now, + })); + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.WALLET_ALREADY_LOADED); + + await cleanDatabase(mysql); + + mockFn = spy.mockImplementation(mockImplementationFailure); + + // fail load and then retry + event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + xpubkeySignature, + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature, + firstAddress, + timestamp: now, + })); + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + + // wallet should be in error state: + event = makeGatewayEventWithAuthorizer(returnBody.status.walletId, null); + result = await walletGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.status.status).toStrictEqual('error'); + + // retrying should succeed + mockFn = spy.mockImplementation(mockImplementationSuccess); + + event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + xpubkeySignature, + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature, + firstAddress, + timestamp: now, + })); + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + // XXX: invoking lambdas is not working on serverless-offline, so for now we are considering a call to the mocked lambda a success: + expect(mockFn).toHaveBeenCalledWith(XPUBKEY, 10); +}, 30000); + +test('POST /wallet should fail with ApiError.WALLET_MAX_RETRIES when max retries are reached', async () => { + expect.hasAssertions(); + + // get the first address + const xpubChangeDerivation = walletUtils.xpubDeriveChild(XPUBKEY, 0); + const firstAddress = walletUtils.getAddressAtIndex(xpubChangeDerivation, 0, process.env.NETWORK); + + // we need signatures for both the account path and the purpose path: + const now = Math.floor(Date.now() / 1000); + const walletId = getWalletId(XPUBKEY); + + const xpriv = getXPrivKeyFromSeed(TEST_SEED, { + passphrase: '', + networkName: process.env.NETWORK, + }); + + // account path + const accountDerivationIndex = '0\''; + + const derivedPrivKey = walletUtils.deriveXpriv(xpriv, accountDerivationIndex); + const address = derivedPrivKey.publicKey.toAddress(network.getNetwork()).toString(); + const message = new bitcore.Message(String(now).concat(walletId).concat(address)); + const xpubkeySignature = message.sign(derivedPrivKey.privateKey); + + // auth purpose path (m/280'/280') + const authDerivedPrivKey = HathorWalletServiceWallet.deriveAuthPrivateKey(xpriv); + const authAddress = authDerivedPrivKey.publicKey.toAddress(network.getNetwork()); + const authMessage = new bitcore.Message(String(now).concat(walletId).concat(authAddress)); + const authXpubkeySignature = authMessage.sign(authDerivedPrivKey.privateKey); + + const spy = jest.spyOn(Wallet, 'invokeLoadWalletAsync'); + const mockImplementationFailure = jest.fn(() => Promise.reject(new Error('error!'))); + spy.mockImplementation(mockImplementationFailure); + + const params = { + xpubkey: XPUBKEY, + xpubkeySignature, + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature, + firstAddress, + timestamp: now, + }; + + // Load failure + let event = makeGatewayEvent({}, JSON.stringify(params)); + let result = await walletLoad(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.status.status).toStrictEqual(WalletStatus.ERROR); + expect(returnBody.status.retryCount).toStrictEqual(1); + + event = makeGatewayEvent({}, JSON.stringify(params)); + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.status.status).toStrictEqual(WalletStatus.ERROR); + expect(returnBody.status.retryCount).toStrictEqual(2); + + event = makeGatewayEvent({}, JSON.stringify(params)); + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.status.status).toStrictEqual(WalletStatus.ERROR); + expect(returnBody.status.retryCount).toStrictEqual(3); + + event = makeGatewayEvent({}, JSON.stringify(params)); + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.status.status).toStrictEqual(WalletStatus.ERROR); + expect(returnBody.status.retryCount).toStrictEqual(4); + + event = makeGatewayEvent({}, JSON.stringify(params)); + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.status.status).toStrictEqual(WalletStatus.ERROR); + expect(returnBody.status.retryCount).toStrictEqual(5); + + event = makeGatewayEvent({}, JSON.stringify(params)); + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(400); + expect(returnBody.status.status).toStrictEqual(WalletStatus.ERROR); + expect(returnBody.error).toStrictEqual(ApiError.WALLET_MAX_RETRIES); + expect(returnBody.status.retryCount).toStrictEqual(5); +}, 30000); // This is huge for a test, but bitcore-lib takes too long + +test('POST /wallet/init should validate attributes properly', async () => { + expect.hasAssertions(); + + const params = { + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + }; + + const event = makeGatewayEvent({}, JSON.stringify(params)); + const result = await walletLoad(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.details).toHaveLength(4); + expect(returnBody.details[0].message).toStrictEqual('"xpubkeySignature" is required'); + expect(returnBody.details[1].message).toStrictEqual('"authXpubkeySignature" is required'); + expect(returnBody.details[2].message).toStrictEqual('"timestamp" is required'); + expect(returnBody.details[3].message).toStrictEqual('"firstAddress" is required'); +}); + +test('PUT /wallet/auth', async () => { + expect.hasAssertions(); + + // check CORS headers + await _testCORSHeaders(changeAuthXpub, null, null); +}); + +test('PUT /wallet/auth should validate attributes properly', async () => { + expect.hasAssertions(); + + const params = { + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + }; + + const event = makeGatewayEvent({}, JSON.stringify(params)); + const result = await changeAuthXpub(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.details).toHaveLength(4); + expect(returnBody.details[0].message).toStrictEqual('"xpubkeySignature" is required'); + expect(returnBody.details[1].message).toStrictEqual('"authXpubkeySignature" is required'); + expect(returnBody.details[2].message).toStrictEqual('"timestamp" is required'); + expect(returnBody.details[3].message).toStrictEqual('"firstAddress" is required'); +}); + +test('PUT /wallet/auth should fail if wallet is not yet started', async () => { + expect.hasAssertions(); + + const event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + xpubkeySignature: 'xpubkey-signature', + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature: 'auth-xpubkey-signature', + firstAddress: ADDRESSES[0], + timestamp: Math.floor(Date.now() / 1000), + })); + + const result = await changeAuthXpub(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(404); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.error).toStrictEqual(ApiError.WALLET_NOT_FOUND); +}); + +test('changeAuthXpub should fail if timestamp is shifted for more than 30s', async () => { + expect.hasAssertions(); + + const event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + xpubkeySignature: 'xpubkey-signature', + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature: 'auth-xpubkey-signature', + firstAddress: ADDRESSES[0], + timestamp: Math.floor(Date.now() / 1000) - 40, + })); + + const result = await changeAuthXpub(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); + expect(returnBody.details).toHaveLength(1); + expect(returnBody.details[0].message).toBe('The timestamp is shifted 40(s). Limit is 30(s).'); +}); + +test('loadWallet should fail if signatures do not match', async () => { + expect.hasAssertions(); + + const event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + xpubkeySignature: 'xpubkey-signature', + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature: 'auth-xpubkey-signature', + firstAddress: ADDRESSES[0], + timestamp: Math.floor(Date.now() / 1000), + })); + + const result = await walletLoad(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(403); + expect(returnBody.success).toBe(false); + expect(returnBody.details[0].message).toBe('Signatures are not valid'); +}); + +test('changeAuthXpub should fail if signatures do not match', async () => { + expect.hasAssertions(); + + const walletId = getWalletId(XPUBKEY); + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + status: 'creating', + maxGap: 5, + createdAt: 10000, + readyAt: null, + }]); + + const event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + xpubkeySignature: 'xpubkey-signature', + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature: 'auth-xpubkey-signature', + firstAddress: ADDRESSES[0], + timestamp: Math.floor(Date.now() / 1000), + })); + + const result = await changeAuthXpub(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(403); + expect(returnBody.success).toBe(false); + expect(returnBody.details[0].message).toBe('Signatures are not valid'); +}); + +test('PUT /wallet/auth should change the auth_xpub only after validating both the xpub and the auth_xpubkey', async () => { + expect.hasAssertions(); + + // get the first address + const xpubChangeDerivation = walletUtils.xpubDeriveChild(XPUBKEY, 0); + const firstAddress = walletUtils.getAddressAtIndex(xpubChangeDerivation, 0, process.env.NETWORK); + + // we need signatures for both the account path and the purpose path: + const now = Math.floor(Date.now() / 1000); + const walletId = getWalletId(XPUBKEY); + const xpriv = getXPrivKeyFromSeed(TEST_SEED, { + passphrase: '', + networkName: process.env.NETWORK, + }); + + // account path + const accountDerivationIndex = '0\''; + + const derivedPrivKey = walletUtils.deriveXpriv(xpriv, accountDerivationIndex); + const address = derivedPrivKey.publicKey.toAddress(network.getNetwork()).toString(); + const message = new bitcore.Message(String(now).concat(walletId).concat(address)); + + expect(1).toStrictEqual(1); +}, 30000); + +test('loadWallet API should fail if a wrong signature is sent', async () => { + expect.hasAssertions(); + + const xpubChangeDerivation = walletUtils.xpubDeriveChild(XPUBKEY, 0); + const firstAddress = walletUtils.getAddressAtIndex(xpubChangeDerivation, 0, process.env.NETWORK); + + const now = Math.floor(Date.now() / 1000); + const walletId = getWalletId(XPUBKEY); + const xpriv = getXPrivKeyFromSeed(TEST_SEED, { + passphrase: '', + networkName: process.env.NETWORK, + }); + + const invalidXpubkeySignature = 'WRONG_XPUBKEY_SIGNATURE'; + const invalidAuthXpubkeySignature = 'WRONG_AUTH_XPUBKEY_SIGNATURE'; + + // account path + const accountDerivationIndex = '0\''; + + const derivedPrivKey = walletUtils.deriveXpriv(xpriv, accountDerivationIndex); + const address = derivedPrivKey.publicKey.toAddress(network.getNetwork()).toString(); + const message = new bitcore.Message(String(now).concat(walletId).concat(address)); + const xpubkeySignature = message.sign(derivedPrivKey.privateKey); + + // auth purpose path (m/280'/280') + const authDerivedPrivKey = HathorWalletServiceWallet.deriveAuthPrivateKey(xpriv); + const authAddress = authDerivedPrivKey.publicKey.toAddress(network.getNetwork()); + const authMessage = new bitcore.Message(String(now).concat(walletId).concat(authAddress)); + const authXpubkeySignature = authMessage.sign(authDerivedPrivKey.privateKey); + + const loadWalletAsyncSpy = jest.spyOn(Wallet, 'invokeLoadWalletAsync'); + const mockImplementationSuccess = jest.fn(() => Promise.resolve()); + loadWalletAsyncSpy.mockImplementation(mockImplementationSuccess); + + let event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + xpubkeySignature: invalidXpubkeySignature, + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature, + firstAddress, + timestamp: now, + })); + let result = await walletLoad(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(403); + expect(returnBody.success).toStrictEqual(false); + + event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + xpubkeySignature, + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature: invalidAuthXpubkeySignature, + firstAddress, + timestamp: now, + })); + result = await walletLoad(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(403); + expect(returnBody.success).toStrictEqual(false); +}, 30000); + +test('loadWallet should fail if timestamp is shifted for more than 30s', async () => { + expect.hasAssertions(); + + const event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + xpubkeySignature: 'xpubkey-signature', + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature: 'auth-xpubkey-signature', + firstAddress: ADDRESSES[0], + timestamp: Math.floor(Date.now() / 1000) - 40, + })); + + const result = await walletLoad(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); + expect(returnBody.details).toHaveLength(1); + expect(returnBody.details[0].message).toBe('The timestamp is shifted 40(s). Limit is 30(s).'); +}); + +test('loadWallet should update wallet status to ERROR if an error occurs', async () => { + expect.hasAssertions(); + + const now = Math.floor(Date.now() / 1000); + const { + walletId, + xpubkey, + xpubkeySignature, + authXpubkey, + authXpubkeySignature, + firstAddress, + } = getAuthData(now); + + const loadWalletAsyncSpy = jest.spyOn(Wallet, 'invokeLoadWalletAsync'); + const mockImplementationSuccess = jest.fn(() => Promise.resolve()); + loadWalletAsyncSpy.mockImplementation(mockImplementationSuccess); + + // wallet should be 'creating' + const event = makeGatewayEvent({}, JSON.stringify({ + xpubkey, + xpubkeySignature, + authXpubkey, + authXpubkeySignature, + firstAddress, + timestamp: now, + })); + const result = await walletLoad(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.status.status).toStrictEqual(WalletStatus.CREATING); + + const dbSpy = jest.spyOn(Db, 'addNewAddresses'); + const mockImplementationFailure = jest.fn(() => Promise.reject(new Error('error!'))); + dbSpy.mockImplementation(mockImplementationFailure); + + const loadEvent = { xpubkey: XPUBKEY, maxGap: 10 }; + + const noop = () => false; + + // mocking an event call from aws + await loadWallet(loadEvent, { + callbackWaitsForEmptyEventLoop: true, + logGroupName: '/aws/lambda/mock-lambda', + logStreamName: '2018/11/29/[$LATEST]xxxxxxxxxxxb', + functionName: 'loadWalletAsync', + memoryLimitInMB: '1024', + functionVersion: '$LATEST', + awsRequestId: 'xxxxxx-xxxxx-11e8-xxxx-xxxxxxxxx', + invokedFunctionArn: 'arn:aws:lambda:us-east-1:xxxxxxxx:function:loadWalletAsync', + getRemainingTimeInMillis: () => 1000, + done: noop, + fail: noop, + succeed: noop, + }, noop); + + const wallet = await Db.getWallet(mysql, walletId); + + expect(wallet.status).toStrictEqual(WalletStatus.ERROR); +}, 30000); + +test('GET /wallet/tokens', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToWalletTxHistoryTable(mysql, [ + ['my-wallet', 'tx1', '00', 5, 1000, false], + ['my-wallet', 'tx1', 'token2', '7', 1000, false], + ['my-wallet', 'tx2', '00', 7, 1001, false], + ['my-wallet', 'tx2', 'token3', 7, 1001, true], + ]); + + // check CORS headers + await _testCORSHeaders(walletTokensGet, null, null); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null); + const result = await walletTokensGet(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.tokens).toStrictEqual(['00', 'token2', 'token3']); +}); + +test('GET /wallet/tokens/token_id/details', async () => { + expect.hasAssertions(); + + // check CORS headers + await _testCORSHeaders(getTokenDetails, null, null); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + let event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: TX_IDS[0] }); + let result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(404); + expect(returnBody.success).toBe(false); + expect(returnBody.details[0]).toStrictEqual({ message: 'Token not found' }); + + event = makeGatewayEventWithAuthorizer('my-wallet', null); + result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.details[0]).toStrictEqual({ message: '"token_id" is required', path: ['token_id'] }); + + // add tokens + const token1 = { id: TX_IDS[1], name: 'MyToken1', symbol: 'MT1' }; + const token2 = { id: TX_IDS[2], name: 'MyToken2', symbol: 'MT2' }; + + await addToTokenTable(mysql, [ + { id: token1.id, name: token1.name, symbol: token1.symbol, transactions: 0 }, + { id: token2.id, name: token2.name, symbol: token2.symbol, transactions: 0 }, + ]); + + await addToUtxoTable(mysql, [{ + // Total tokens created + txId: 'txId', + index: 0, + tokenId: token1.id, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + // Mint UTXO: + txId: 'txId', + index: 1, + tokenId: token1.id, + address: ADDRESSES[0], + value: 0, + authorities: constants.TOKEN_MINT_MASK, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + // Another Mint UTXO + txId: 'txId', + index: 2, + tokenId: token1.id, + address: ADDRESSES[0], + value: 0, + authorities: constants.TOKEN_MINT_MASK, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + // Total tokens created + txId: 'txId2', + index: 0, + tokenId: token2.id, + address: ADDRESSES[0], + value: 250, + authorities: 0, + timelock: null, + heightlock: null, + locked: true, + spentBy: null, + }, { + // Locked utxo + txId: 'txId2', + index: 1, + tokenId: token2.id, + address: ADDRESSES[0], + value: 0, + authorities: constants.TOKEN_MINT_MASK, + timelock: 1000, + heightlock: null, + locked: true, + spentBy: null, + }, { + // Spent utxo + txId: 'txId2', + index: 2, + tokenId: token2.id, + address: ADDRESSES[0], + value: 0, + authorities: constants.TOKEN_MINT_MASK, + timelock: 1000, + heightlock: null, + locked: true, + spentBy: 'txid2', + }, { + txId: 'txId3', + index: 0, + tokenId: token2.id, + address: ADDRESSES[0], + value: 0, + authorities: constants.TOKEN_MINT_MASK, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + // Melt UTXO + txId: 'txId3', + index: 1, + tokenId: token2.id, + address: ADDRESSES[0], + value: 0, + authorities: constants.TOKEN_MELT_MASK, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]); + + await addToAddressTxHistoryTable(mysql, [ + { address: ADDRESSES[0], txId: 'txId', tokenId: token1.id, balance: 100, timestamp: 0 }, + { address: ADDRESSES[0], txId: 'txId2', tokenId: token2.id, balance: 250, timestamp: 0 }, + { address: ADDRESSES[0], txId: 'txId3', tokenId: token2.id, balance: 0, timestamp: 0 }, + ]); + + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: token1.id }); + result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.details.totalSupply).toStrictEqual(100); + expect(returnBody.details.totalTransactions).toStrictEqual(1); + expect(returnBody.details.authorities.mint).toStrictEqual(true); + expect(returnBody.details.authorities.melt).toStrictEqual(false); + expect(returnBody.details.tokenInfo).toStrictEqual(token1); + + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: token2.id }); + result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.details.totalSupply).toStrictEqual(250); + expect(returnBody.details.totalTransactions).toStrictEqual(2); + expect(returnBody.details.authorities.mint).toStrictEqual(true); + expect(returnBody.details.authorities.melt).toStrictEqual(true); + expect(returnBody.details.tokenInfo).toStrictEqual(token2); + + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: constants.HATHOR_TOKEN_CONFIG.uid }); + result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.details).toMatchInlineSnapshot(` + [ + { + "message": "\"token_id\" length must be at least 64 characters long", + "path": [ + "token_id", + ], + }, + ] + `); + + const oldHathorTokenConfig = constants.HATHOR_TOKEN_CONFIG.uid; + + constants.HATHOR_TOKEN_CONFIG.uid = TX_IDS[4]; + + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: constants.HATHOR_TOKEN_CONFIG.uid }); + result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.details).toStrictEqual([{ message: 'Invalid tokenId' }]); + + constants.HATHOR_TOKEN_CONFIG.uid = oldHathorTokenConfig; +}); + +test('GET /wallet/utxos', async () => { + expect.hasAssertions(); + + await _testCORSHeaders(getFilteredUtxos, null, null); +}); + +test('GET /wallet/tx_outputs', async () => { + expect.hasAssertions(); + + await _testCORSHeaders(getFilteredTxOutputs, null, null); +}); + +test('POST /tx/proposal', async () => { + expect.hasAssertions(); + + await _testCORSHeaders(txProposalCreate, null, null); +}); + +test('PUT /tx/proposal/{txProposalId}', async () => { + expect.hasAssertions(); + + await _testCORSHeaders(txProposalSend, null, null); +}); + +test('DELETE /tx/proposal/{txProposalId}', async () => { + expect.hasAssertions(); + + await _testCORSHeaders(txProposalDestroy, null, null); +}); + +test('GET /version', async () => { + expect.hasAssertions(); + + const mockData: FullNodeVersionData = { + timestamp: 1614875031449, + version: '0.38.0', + network: 'mainnet', + minWeight: 14, + minTxWeight: 14, + minTxWeightCoefficient: 1.6, + minTxWeightK: 100, + tokenDepositPercentage: 0.01, + rewardSpendMinBlocks: 300, + maxNumberInputs: 255, + maxNumberOutputs: 255, + }; + + await updateVersionData(mysql, mockData); + + const event = makeGatewayEvent({}); + const result = await getVersionDataGet(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.data).toStrictEqual(mockData); +}); + +test('GET /wallet/proxy/transactions/{txId}', async () => { + expect.hasAssertions(); + + const mockData = { + success: true, + tx: { + hash: '000011f5cd1c2bcb7e5e91567666042d8681deeca96263bca60f10c528b9af32', + nonce: '16651564', + timestamp: 1672930233, + version: 1, + weight: 18.173170552208116, + parents: [ + '000021de2f105caa2daa9979bdb591a5860b6482a82ed4d7987496c30dbd4496', + '000007cf2c382898af0f9fd963b2a34279370b522e7816371b778f4a80951ca8', + ], + inputs: [{ + value: 2, + token_data: 129, + script: 'dqkUuRVulIYgVepEURsh05y3F4ztyJaIrA==', + decoded: { + type: 'P2PKH', + address: 'HPPm4x85cytT9UmSk9MfgQEDfX295JKmiT', + timelock: null, + value: 2, + token_data: 129, + }, + tx_id: '000028a7886b410958014a61924920b12c667945f2e1c20a986e230fb92afdfc', + index: 1, + }], + outputs: [{ + value: 2, + token_data: 129, + script: 'dqkUBAsAnZEAjdjFegyP0eo6WClFKeCIrA==', + decoded: { + type: 'P2PKH', + address: 'H6tWGa8kY5uu3Hz9s4yqV63SCdd3yaXXmX', + timelock: null, + value: 2, + token_data: 129, + }, + }], + tokens: [{ + uid: '00003feaf0adb971ef05ad381f5a6c0364c52145617f8f3a8464048c43378628', + name: 'TEST TOKEN', + symbol: 'TEST', + }], + raw: '', + }, + meta: { + hash: '000011f5cd1c2bcb7e5e91567666042d8681deeca96263bca60f10c528b9af32', + spent_outputs: [ + [0, []], + [1, []], + ], + received_by: [], + children: [ + '00000000000000000a3df6f146fef03b5044b0e415c4d85a702a72cae133d17b', + '00000000000000000f091b4d3088aa568ca4a60e4aa67d9e881b3ae60cb846c7', + ], + conflict_with: [], + voided_by: [], + twins: [], + accumulated_weight: 18.173170552208116, + score: 0, + height: 0, + min_height: 3074721, + first_block: '00000000000000000a3df6f146fef03b5044b0e415c4d85a702a72cae133d17b', + validation: 'full', + first_block_height: 3140266, + }, + spent_outputs: {}, + }; + + const spy = jest.spyOn(fullnode, 'downloadTx'); + + const mockFullnodeResponse = jest.fn(() => Promise.resolve(mockData)); + spy.mockImplementation(mockFullnodeResponse); + + let event = makeGatewayEventWithAuthorizer('my-wallet', { + txId: '000011f5cd1c2bcb7e5e91567666042d8681deeca96263bca60f10c528b9af32', + }); + let result = await getTransactionById(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody).toStrictEqual(mockData); + + event = makeGatewayEventWithAuthorizer('my-wallet', null); + result = await getTransactionById(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody).toMatchInlineSnapshot(` + { + "details": [ + { + "message": "\"txId\" is required", + "path": [ + "txId", + ], + }, + ], + "error": "invalid-payload", + "success": false, + } + `); +}); + +test('GET /wallet/proxy/{txId}/confirmation_data', async () => { + expect.hasAssertions(); + + const mockData = { + success: true, + accumulated_weight: 67.45956109191802, + accumulated_bigger: true, + stop_value: 67.45416781056525, + confirmation_level: 1, + }; + + const spy = jest.spyOn(fullnode, 'getConfirmationData'); + + const mockFullnodeResponse = jest.fn(() => Promise.resolve(mockData)); + spy.mockImplementation(mockFullnodeResponse); + + let event = makeGatewayEventWithAuthorizer('my-wallet', { + txId: '000011f5cd1c2bcb7e5e91567666042d8681deeca96263bca60f10c528b9af32', + }); + let result = await getConfirmationData(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody).toStrictEqual(mockData); + + // Missing txId + event = makeGatewayEventWithAuthorizer('my-wallet', null); + result = await getConfirmationData(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody).toMatchInlineSnapshot(` + { + "details": [ + { + "message": "\"txId\" is required", + "path": [ + "txId", + ], + }, + ], + "error": "invalid-payload", + "success": false, + } + `); +}); + +test('GET /wallet/proxy/graphviz/neighbours', async () => { + expect.hasAssertions(); + + const mockData = 'digraph {}'; + + const spy = jest.spyOn(fullnode, 'queryGraphvizNeighbours'); + + const mockFullnodeResponse = jest.fn(() => Promise.resolve(mockData)); + spy.mockImplementation(mockFullnodeResponse); + + let event = makeGatewayEventWithAuthorizer('my-wallet', { + txId: '000011f5cd1c2bcb7e5e91567666042d8681deeca96263bca60f10c528b9af32', + graphType: 'verification', + maxLevel: '1', + }); + let result = await queryGraphvizNeighbours(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody).toStrictEqual(mockData); + + // Missing a single attribute + event = makeGatewayEventWithAuthorizer('my-wallet', { + txId: '000011f5cd1c2bcb7e5e91567666042d8681deeca96263bca60f10c528b9af32', + graphType: 'verification', + }); + + result = await queryGraphvizNeighbours(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(400); + expect(returnBody).toMatchInlineSnapshot(` + { + "details": [ + { + "message": "\"maxLevel\" is required", + "path": [ + "maxLevel", + ], + }, + ], + "error": "invalid-payload", + "success": false, + } + `); + + // Missing all attributes + event = makeGatewayEventWithAuthorizer('my-wallet', null); + result = await queryGraphvizNeighbours(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(400); + expect(returnBody).toMatchInlineSnapshot(` + { + "details": [ + { + "message": "\"txId\" is required", + "path": [ + "txId", + ], + }, + { + "message": "\"graphType\" is required", + "path": [ + "graphType", + ], + }, + { + "message": "\"maxLevel\" is required", + "path": [ + "maxLevel", + ], + }, + ], + "error": "invalid-payload", + "success": false, + } + `); +}); + +describe('GET /health', () => { + test('success case', async () => { + expect.hasAssertions(); + + // Mock fullnode status response + const mockStatus = { + 'dag': { + 'best_block': { + 'height': 321, + } + } + }; + jest.spyOn(fullnode, 'getStatus').mockResolvedValue(mockStatus); + + // Mock fullnode health response + const mockHealth = { + 'status': 'pass' + }; + jest.spyOn(fullnode, 'getHealth').mockResolvedValue(mockHealth); + + // Mock database content + await addToTransactionTable(mysql, [ + ['tx1', 100, 2, false, 123, 60], + ['tx2', 100, 3, false, 321, 60], + ]); + + // Run healthcheck + const event = makeGatewayEvent({}); + const result = await getHealthcheck(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + // Assertions + expect(result.statusCode).toBe(200); + expect(returnBody).toStrictEqual({ + status: 'pass', + description: 'Health status of hathor-wallet-service', + httpStatusCode: 200, + checks: { + 'mysql:block_height': [{ + 'affectsServiceHealth': true, + 'componentName': 'mysql:block_height', + 'componentType': 'internal', + 'output': 'Database and fullnode heights are within 5 blocks difference', + 'status': 'pass', + 'time': expect.any(String), + }], + 'redis:connection': [{ + 'affectsServiceHealth': true, + 'componentName': 'redis:connection', + 'componentType': 'datastore', + 'output': 'Redis connection is up', + 'status': 'pass', + 'time': expect.any(String), + }], + 'fullnode:health': [{ + 'affectsServiceHealth': true, + 'componentName': 'fullnode:health', + 'componentType': 'http', + 'output': 'Fullnode is healthy', + 'status': 'pass', + 'time': expect.any(String), + }], + } + }); + }); + + test('height mismatch between db and fullnode', async () => { + expect.hasAssertions(); + + const FULLNODE_HEIGHT = 456; + const DB_HEIGHT = FULLNODE_HEIGHT - 6; + + // Mock fullnode response + const mockStatus = { + 'dag': { + 'best_block': { + 'height': FULLNODE_HEIGHT, + } + } + }; + jest.spyOn(fullnode, 'getStatus').mockResolvedValue(mockStatus); + + // Mock fullnode health response + const mockHealth = { + 'status': 'pass' + }; + jest.spyOn(fullnode, 'getHealth').mockResolvedValue(mockHealth); + + // Mock database content + await addToTransactionTable(mysql, [ + ['tx1', 100, 2, false, DB_HEIGHT - 1, 60], + ['tx2', 100, 3, false, DB_HEIGHT, 60], + ]); + + // Run healthcheck + const event = makeGatewayEvent({}); + const result = await getHealthcheck(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + // Assertions + expect(result.statusCode).toBe(503); + expect(returnBody).toStrictEqual({ + status: 'fail', + description: 'Health status of hathor-wallet-service', + httpStatusCode: 503, + checks: { + 'mysql:block_height': [{ + 'affectsServiceHealth': true, + 'componentName': 'mysql:block_height', + 'componentType': 'internal', + 'output': 'Database height is 450 but fullnode height is 456', + 'status': 'fail', + 'time': expect.any(String), + }], + 'redis:connection': [{ + 'affectsServiceHealth': true, + 'componentName': 'redis:connection', + 'componentType': 'datastore', + 'output': 'Redis connection is up', + 'status': 'pass', + 'time': expect.any(String), + }], + 'fullnode:health': [{ + 'affectsServiceHealth': true, + 'componentName': 'fullnode:health', + 'componentType': 'http', + 'output': 'Fullnode is healthy', + 'status': 'pass', + 'time': expect.any(String), + }], + } + }); + }); + + test('exception when getting fullnode height', async () => { + expect.hasAssertions(); + + // Mock fullnode response + jest.spyOn(fullnode, 'getStatus').mockRejectedValue(new Error('Fullnode exploded!')); + + // Mock fullnode health response + const mockHealth = { + 'status': 'pass' + }; + jest.spyOn(fullnode, 'getHealth').mockResolvedValue(mockHealth); + + // Mock database content + await addToTransactionTable(mysql, [ + ['tx1', 100, 2, false, 123, 60], + ['tx2', 100, 3, false, 321, 60], + ]); + + // Run healthcheck + const event = makeGatewayEvent({}); + const result = await getHealthcheck(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + // Assertions + expect(result.statusCode).toBe(503); + expect(returnBody).toStrictEqual({ + status: 'fail', + description: 'Health status of hathor-wallet-service', + httpStatusCode: 503, + checks: { + 'mysql:block_height': [{ + 'affectsServiceHealth': true, + 'componentName': 'mysql:block_height', + 'componentType': 'internal', + 'output': 'Error checking database and fullnode height: Fullnode exploded!', + 'status': 'fail', + 'time': expect.any(String), + }], + 'redis:connection': [{ + 'affectsServiceHealth': true, + 'componentName': 'redis:connection', + 'componentType': 'datastore', + 'output': 'Redis connection is up', + 'status': 'pass', + 'time': expect.any(String), + }], + 'fullnode:health': [{ + 'affectsServiceHealth': true, + 'componentName': 'fullnode:health', + 'componentType': 'http', + 'output': 'Fullnode is healthy', + 'status': 'pass', + 'time': expect.any(String), + }], + } + }); + }); + + test('exception when getting fullnode health', async () => { + expect.hasAssertions(); + + // Mock fullnode response + const mockStatus = { + 'dag': { + 'best_block': { + 'height': 321, + } + } + }; + jest.spyOn(fullnode, 'getStatus').mockResolvedValue(mockStatus); + + // Mock fullnode health response + jest.spyOn(fullnode, 'getHealth').mockRejectedValue(new Error('Fullnode exploded!')); + + // Mock database content + await addToTransactionTable(mysql, [ + ['tx1', 100, 2, false, 123, 60], + ['tx2', 100, 3, false, 321, 60], + ]); + + // Run healthcheck + const event = makeGatewayEvent({}); + const result = await getHealthcheck(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + // Assertions + expect(result.statusCode).toBe(503); + expect(returnBody).toStrictEqual({ + status: 'fail', + description: 'Health status of hathor-wallet-service', + httpStatusCode: 503, + checks: { + 'mysql:block_height': [{ + 'affectsServiceHealth': true, + 'componentName': 'mysql:block_height', + 'componentType': 'internal', + 'output': 'Database and fullnode heights are within 5 blocks difference', + 'status': 'pass', + 'time': expect.any(String), + }], + 'redis:connection': [{ + 'affectsServiceHealth': true, + 'componentName': 'redis:connection', + 'componentType': 'datastore', + 'output': 'Redis connection is up', + 'status': 'pass', + 'time': expect.any(String), + }], + 'fullnode:health': [{ + 'affectsServiceHealth': true, + 'componentName': 'fullnode:health', + 'componentType': 'http', + 'output': 'Error checking fullnode health: Fullnode exploded!', + 'status': 'fail', + 'time': expect.any(String), + }], + } + }); + }); + + test('fullnode reports unhealthy', async () => { + expect.hasAssertions(); + + // Mock fullnode response + const mockStatus = { + 'dag': { + 'best_block': { + 'height': 321, + } + } + }; + jest.spyOn(fullnode, 'getStatus').mockResolvedValue(mockStatus); + + // Mock fullnode health response + const mockHealth = { + 'status': 'fail', + 'output': 'Fullnode exploded!', + 'checks': { + 'sync': { + 'status': 'fail', + 'output': 'Sync is not working', + } + } + }; + jest.spyOn(fullnode, 'getHealth').mockResolvedValue(mockHealth); + + // Mock database content + await addToTransactionTable(mysql, [ + ['tx1', 100, 2, false, 123, 60], + ['tx2', 100, 3, false, 321, 60], + ]); + + // Run healthcheck + const event = makeGatewayEvent({}); + const result = await getHealthcheck(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + // Assertions + expect(result.statusCode).toBe(503); + expect(returnBody).toStrictEqual({ + status: 'fail', + description: 'Health status of hathor-wallet-service', + httpStatusCode: 503, + checks: { + 'mysql:block_height': [{ + 'affectsServiceHealth': true, + 'componentName': 'mysql:block_height', + 'componentType': 'internal', + 'output': 'Database and fullnode heights are within 5 blocks difference', + 'status': 'pass', + 'time': expect.any(String), + }], + 'redis:connection': [{ + 'affectsServiceHealth': true, + 'componentName': 'redis:connection', + 'componentType': 'datastore', + 'output': 'Redis connection is up', + 'status': 'pass', + 'time': expect.any(String), + }], + 'fullnode:health': [{ + 'affectsServiceHealth': true, + 'componentName': 'fullnode:health', + 'componentType': 'http', + 'output': 'Fullnode is unhealthy: {"status":"fail","output":"Fullnode exploded!","checks":{"sync":{"status":"fail","output":"Sync is not working"}}}', + 'status': 'fail', + 'time': expect.any(String), + }], + } + }); + }); +}); diff --git a/packages/wallet-service/tests/commons.test.ts b/packages/wallet-service/tests/commons.test.ts new file mode 100644 index 00000000..d9e6b9a1 --- /dev/null +++ b/packages/wallet-service/tests/commons.test.ts @@ -0,0 +1,1175 @@ +import eventTemplate from '@events/eventTemplate.json'; +import { + getAddressBalanceMap, + getWalletBalanceMap, + markLockedOutputs, + unlockUtxos, + unlockTimelockedUtxos, + maybeRefreshWalletConstants, + searchForLatestValidBlock, + getWalletBalancesForTx, +} from '@src/commons'; +import { + FullNodeVersionData, + Authorities, + Balance, + TokenBalanceMap, + DbTxOutput, + Block, + TxInput, + TxOutput, + Transaction, +} from '@src/types'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { + addToAddressTable, + addToAddressBalanceTable, + addToUtxoTable, + addToWalletTable, + addToWalletBalanceTable, + cleanDatabase, + checkUtxoTable, + checkAddressBalanceTable, + checkWalletBalanceTable, + createInput, + createOutput, + TX_IDS, + XPUBKEY, + AUTH_XPUBKEY, +} from '@tests/utils'; +import { + updateVersionData, + addOrUpdateTx, + updateWalletTablesWithTx, + createWallet, + updateTxOutputSpentBy, + addUtxos, + storeTokenInformation, +} from '@src/db'; +import * as Utils from '@src/utils'; +import hathorLib from '@hathor/wallet-lib'; + +const mysql = getDbConnection(); +const OLD_ENV = process.env; + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + +beforeAll(async () => { + // modify env so block reward is unlocked after 1 new block (overrides .env file) + jest.resetModules(); + process.env = { ...OLD_ENV }; + process.env.BLOCK_REWARD_LOCK = '1'; +}); + +afterAll(async () => { + await closeDbConnection(mysql); + // restore old env + process.env = OLD_ENV; +}); + +test('markLockedOutputs and getAddressBalanceMap', () => { + expect.hasAssertions(); + const evt = JSON.parse(JSON.stringify(eventTemplate)); + const tx = evt.Records[0].body; + const now = 20000; + tx.tx_id = 'txId1'; + tx.timestamp = 0; + tx.inputs = [ + createInput(10, 'address1', 'inputTx', 0, 'token1'), + createInput(5, 'address1', 'inputTx', 0, 'token1'), + createInput(7, 'address1', 'inputTx', 1, 'token2'), + createInput(3, 'address2', 'inputTx', 2, 'token1'), + ]; + tx.outputs = [ + createOutput(0, 5, 'address1', 'token1'), + createOutput(1, 2, 'address1', 'token3'), + createOutput(2, 11, 'address2', 'token1'), + ]; + const map1 = new TokenBalanceMap(); + map1.set('token1', new Balance(5, -10, 0)); + map1.set('token2', new Balance(0, -7, 0)); + map1.set('token3', new Balance(2, 2, 0)); + const map2 = new TokenBalanceMap(); + map2.set('token1', new Balance(11, 8, 0)); + const expectedAddrMap = { + address1: map1, + address2: map2, + }; + + markLockedOutputs(tx.outputs, now, false); + for (const output of tx.outputs) { + expect(output.locked).toBe(false); + } + + const addrMap = getAddressBalanceMap(tx.inputs, tx.outputs); + expect(addrMap).toStrictEqual(expectedAddrMap); + + // update tx to contain outputs with timelock + tx.outputs[0].decoded.timelock = now - 1; // won't be locked + tx.outputs[1].decoded.timelock = now; // won't be locked + tx.outputs[2].decoded.timelock = now + 1; // locked + + // should mark the corresponding output as locked + markLockedOutputs(tx.outputs, now, false); + expect(tx.outputs[0].locked).toBe(false); + expect(tx.outputs[1].locked).toBe(false); + expect(tx.outputs[2].locked).toBe(true); + + // check balance + map2.set('token1', new Balance(11, -3, 11, now + 1)); + const addrMap2 = getAddressBalanceMap(tx.inputs, tx.outputs); + expect(addrMap2).toStrictEqual(expectedAddrMap); + + // a block will have its rewards locked, even with no timelock + tx.inputs = []; + tx.outputs = [ + createOutput(0, 100, 'address1', 'token1'), + ]; + markLockedOutputs(tx.outputs, now, true); + for (const output of tx.outputs) { + expect(output.locked).toBe(true); + } + const addrMap3 = getAddressBalanceMap(tx.inputs, tx.outputs); + const map3 = new TokenBalanceMap(); + map3.set('token1', new Balance(100, 0, 100)); + const expectedAddrMap2 = { + address1: map3, + }; + expect(addrMap3).toStrictEqual(expectedAddrMap2); + + // tx with authorities + tx.inputs = [ + createInput(0b01, 'address1', 'inputTx', 0, 'token1', null, 129), + createInput(0b10, 'address1', 'inputTx', 1, 'token2', null, 129), + ]; + tx.outputs = [ + createOutput(0, 0b01, 'address1', 'token1', null, false, 129), + createOutput(1, 0b10, 'address1', 'token2', 1000, true, 129), + ]; + const map4 = new TokenBalanceMap(); + map4.set('token1', new Balance(0, 0, 0, null)); + map4.set('token2', new Balance(0, 0, 0, 1000, new Authorities([-1, 0]), new Authorities([1, 0]))); + const expectedAddrMap4 = { + address1: map4, + }; + const addrMap4 = getAddressBalanceMap(tx.inputs, tx.outputs); + expect(addrMap4).toStrictEqual(expectedAddrMap4); +}); + +test('getWalletBalanceMap', () => { + expect.hasAssertions(); + const mapAddress1 = new TokenBalanceMap(); + mapAddress1.set('token1', new Balance(1, -10, 0)); + mapAddress1.set('token2', new Balance(0, -7, 0)); + mapAddress1.set('token3', new Balance(27, 2, 0)); + const mapAddress2 = new TokenBalanceMap(); + mapAddress2.set('token1', new Balance(10, 8, 0)); + const mapAddress3 = new TokenBalanceMap(); + mapAddress3.set('token2', new Balance(4, 2, 0)); + mapAddress3.set('token3', new Balance(12, 6, 0)); + const mapAddress4 = new TokenBalanceMap(); + mapAddress4.set('token1', new Balance(10, 2, 0)); + mapAddress4.set('token2', new Balance(14, 9, 1, 500)); + const mapAddress5 = new TokenBalanceMap(); + mapAddress5.set('token1', new Balance(20, 11, 0)); + const addressBalanceMap = { + address1: mapAddress1, + address2: mapAddress2, + address3: mapAddress3, + address4: mapAddress4, + address5: mapAddress5, // doesn't belong to any started wallet + }; + const walletAddressMap = { + address1: { walletId: 'wallet1', xpubkey: 'xpubkey1', authXpubkey: 'authxpubkey1', maxGap: 5 }, + address2: { walletId: 'wallet1', xpubkey: 'xpubkey1', authXpubkey: 'authxpubkey1', maxGap: 5 }, + address4: { walletId: 'wallet1', xpubkey: 'xpubkey1', authXpubkey: 'authxpubkey1', maxGap: 5 }, + address3: { walletId: 'wallet2', xpubkey: 'xpubkey2', authXpubkey: 'authxpubkey2', maxGap: 5 }, + }; + const mapWallet1 = new TokenBalanceMap(); + mapWallet1.set('token1', new Balance(21, 0, 0)); + mapWallet1.set('token2', new Balance(14, 2, 1, 500)); + mapWallet1.set('token3', new Balance(27, 2, 0)); + const mapWallet2 = new TokenBalanceMap(); + mapWallet2.set('token2', new Balance(4, 2, 0)); + mapWallet2.set('token3', new Balance(12, 6, 0)); + const expectedWalletBalanceMap = { + wallet1: mapWallet1, + wallet2: mapWallet2, + }; + const walletBalanceMap = getWalletBalanceMap(walletAddressMap, addressBalanceMap); + expect(walletBalanceMap).toStrictEqual(expectedWalletBalanceMap); + + // if walletAddressMap is empty, should also return an empty object + const walletBalanceMap2 = getWalletBalanceMap({}, addressBalanceMap); + expect(walletBalanceMap2).toStrictEqual({}); +}); + +test('unlockUtxos', async () => { + expect.hasAssertions(); + const reward = 6400; + const txId1 = 'txId1'; + const txId2 = 'txId2'; + const txId3 = 'txId3'; + const txId4 = 'txId4'; + const txId5 = 'txId5'; + const token = 'tokenId'; + const addr = 'address'; + const walletId = 'walletId'; + const now = 1000; + await addToUtxoTable(mysql, [ + // blocks with heightlock + { + txId: txId1, + index: 0, + tokenId: token, + address: addr, + value: reward, + authorities: 0, + timelock: null, + heightlock: 3, + locked: true, + spentBy: null, + }, { + txId: txId2, + index: 0, + tokenId: token, + address: addr, + value: reward, + authorities: 0, + timelock: null, + heightlock: 4, + locked: true, + spentBy: null, + }, + // some transactions with timelock + { + txId: txId3, + index: 0, + tokenId: token, + address: addr, + value: 2500, + authorities: 0, + timelock: now, + heightlock: null, + locked: true, + spentBy: null, + }, { + txId: txId4, + index: 0, + tokenId: token, + address: addr, + value: 2500, + authorities: 0, + timelock: now * 2, + heightlock: null, + locked: true, + spentBy: null, + }, { + txId: txId5, + index: 0, + tokenId: token, + address: addr, + value: 0, + authorities: 0b10, + timelock: now * 3, + heightlock: null, + locked: true, + spentBy: null, + }, + ]); + + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: 'xpub', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 10, + createdAt: now, + readyAt: now + 1, + }]); + + await addToAddressTable(mysql, [{ + address: addr, + index: 0, + walletId, + transactions: 1, + }]); + + await addToAddressBalanceTable(mysql, [ + [addr, token, 0, 2 * reward + 5000, now, 5, 0, 0b10, 4 * reward + 5000], + ]); + + await addToWalletBalanceTable(mysql, [{ + walletId, + tokenId: token, + unlockedBalance: 0, + lockedBalance: 2 * reward + 5000, + unlockedAuthorities: 0, + lockedAuthorities: 0b10, + timelockExpires: now, + transactions: 5, + }]); + + const utxo: DbTxOutput = { + txId: txId1, + index: 0, + tokenId: token, + address: addr, + value: reward, + authorities: 0, + timelock: null, + heightlock: 3, + locked: true, + }; + + // unlock txId1 + await unlockUtxos(mysql, [utxo], false); + await expect( + checkUtxoTable(mysql, 5, txId1, 0, utxo.tokenId, utxo.address, utxo.value, 0, utxo.timelock, utxo.heightlock, false), + ).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, reward, reward + 5000, now, 5, 0, 0b10)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, reward, reward + 5000, now, 5, 0, 0b10)).resolves.toBe(true); + + // unlock txId2 + utxo.txId = txId2; + utxo.heightlock = 4; + await unlockUtxos(mysql, [utxo], false); + await expect( + checkUtxoTable(mysql, 5, txId2, 0, utxo.tokenId, utxo.address, utxo.value, 0, utxo.timelock, utxo.heightlock, false), + ).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 2 * reward, 5000, now, 5, 0, 0b10)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 2 * reward, 5000, now, 5, 0, 0b10)).resolves.toBe(true); + + // unlock txId3, txId4 is still locked + utxo.txId = txId3; + utxo.value = 2500; + utxo.timelock = now; + utxo.heightlock = null; + await unlockUtxos(mysql, [utxo], true); + await expect( + checkUtxoTable(mysql, 5, txId3, 0, utxo.tokenId, utxo.address, utxo.value, 0, utxo.timelock, utxo.heightlock, false), + ).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 2 * reward + 2500, 2500, 2 * now, 5, 0, 0b10)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 2 * reward + 2500, 2500, 2 * now, 5, 0, 0b10)).resolves.toBe(true); + + // unlock txId4 + utxo.txId = txId4; + utxo.value = 2500; + utxo.timelock = now * 2; + utxo.heightlock = null; + await unlockUtxos(mysql, [utxo], true); + await expect( + checkUtxoTable(mysql, 5, txId4, 0, utxo.tokenId, utxo.address, utxo.value, 0, utxo.timelock, utxo.heightlock, false), + ).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 2 * reward + 5000, 0, 3 * now, 5, 0, 0b10)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 2 * reward + 5000, 0, 3 * now, 5, 0, 0b10)).resolves.toBe(true); + + // unlock txId5 + utxo.txId = txId5; + utxo.value = 0; + utxo.authorities = 0b10; + utxo.timelock = now * 3; + utxo.heightlock = null; + await unlockUtxos(mysql, [utxo], true); + await expect( + checkUtxoTable(mysql, 5, txId5, 0, utxo.tokenId, utxo.address, utxo.value, utxo.authorities, utxo.timelock, utxo.heightlock, false), + ).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 2 * reward + 5000, 0, null, 5, 0b10, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 2 * reward + 5000, 0, null, 5, 0b10, 0)).resolves.toBe(true); +}); + +test('unlockTimelockedUtxos', async () => { + expect.hasAssertions(); + + const reward = 6400; + const txId1 = 'txId1'; + const txId2 = 'txId2'; + const txId3 = 'txId3'; + const token = 'tokenId'; + const addr = 'address'; + const walletId = 'walletId'; + const now = 1000; + await addToUtxoTable(mysql, [{ + txId: txId1, + index: 0, + tokenId: token, + address: addr, + value: 2500, + authorities: 0, + timelock: now, + heightlock: null, + locked: true, + spentBy: null, + }, { + txId: txId2, + index: 0, + tokenId: token, + address: addr, + value: 2500, + authorities: 0, + timelock: now * 2, + heightlock: null, + locked: true, + spentBy: null, + }, { + txId: txId3, + index: 0, + tokenId: token, + address: addr, + value: 0, + authorities: 0b10, + timelock: now * 3, + heightlock: null, + locked: true, + spentBy: null, + }]); + + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: 'xpub', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 10, + createdAt: now, + readyAt: now + 1, + }]); + + await addToAddressTable(mysql, [{ + address: addr, + index: 0, + walletId, + transactions: 3, + }]); + + await addToAddressBalanceTable(mysql, [ + [addr, token, 0, 5000, now, 3, 0, 0b10, 10000], + ]); + + await addToWalletBalanceTable(mysql, [{ + walletId, + tokenId: token, + unlockedBalance: 0, + lockedBalance: 5000, + unlockedAuthorities: 0, + lockedAuthorities: 0b10, + timelockExpires: now, + transactions: 3, + }]); + + const utxo: DbTxOutput = { + txId: txId1, + index: 0, + tokenId: token, + address: addr, + value: reward, + authorities: 0, + timelock: null, + heightlock: 3, + locked: true, + }; + + // unlock txId1, txId2 is still locked + utxo.txId = txId1; + utxo.value = 2500; + utxo.timelock = now; + utxo.heightlock = null; + await unlockTimelockedUtxos(mysql, now + 1); + await expect( + checkUtxoTable(mysql, 3, txId1, 0, utxo.tokenId, utxo.address, utxo.value, 0, utxo.timelock, utxo.heightlock, false), + ).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 2500, 2500, 2 * now, 3, 0, 0b10)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 2500, 2500, 2 * now, 3, 0, 0b10)).resolves.toBe(true); + + // unlock txId2 + utxo.txId = txId2; + utxo.value = 2500; + utxo.timelock = now * 2; + utxo.heightlock = null; + await unlockTimelockedUtxos(mysql, (now * 2) + 1); + await expect( + checkUtxoTable(mysql, 3, txId2, 0, utxo.tokenId, utxo.address, utxo.value, 0, utxo.timelock, utxo.heightlock, false), + ).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 5000, 0, 3 * now, 3, 0, 0b10)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 5000, 0, 3 * now, 3, 0, 0b10)).resolves.toBe(true); + + // unlock txId3 + utxo.txId = txId3; + utxo.value = 0; + utxo.authorities = 0b10; + utxo.timelock = now * 3; + utxo.heightlock = null; + await unlockTimelockedUtxos(mysql, (now * 3) + 1); + await expect( + checkUtxoTable(mysql, 3, txId3, 0, utxo.tokenId, utxo.address, utxo.value, utxo.authorities, utxo.timelock, utxo.heightlock, false), + ).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 5000, 0, null, 3, 0b10, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 5000, 0, null, 3, 0b10, 0)).resolves.toBe(true); +}); + +test('maybeRefreshWalletConstants with an uninitialized version_data database should call hathorLib.version.checkApiVersion()', async () => { + expect.hasAssertions(); + + const spy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); + + const mockGet = jest.fn(() => Promise.resolve({ + data: { + success: true, + version: '0.38.0', + network: 'mainnet', + min_weight: 14, + min_tx_weight: 14, + min_tx_weight_coefficient: 1.6, + min_tx_weight_k: 100, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 255, + }, + })); + + spy.mockReturnValue({ + post: () => Promise.resolve({ + data: { + success: true, + }, + }), + get: mockGet, + }); + + await maybeRefreshWalletConstants(mysql); + + expect(mockGet).toHaveBeenCalledTimes(1); +}); + +test('maybeRefreshWalletConstants with an initialized version_data database should query data from the database', async () => { + expect.hasAssertions(); + + const axiosSpy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); + const mockGet = jest.fn(() => Promise.resolve({ data: {} })); + + axiosSpy.mockReturnValue({ get: mockGet }); + + const mockedVersionData: FullNodeVersionData = { + timestamp: new Date().getTime(), + version: '0.38.0', + network: 'mainnet', + minWeight: 14, + minTxWeight: 14, + minTxWeightCoefficient: 1.6, + minTxWeightK: 100, + tokenDepositPercentage: 0.01, + rewardSpendMinBlocks: 300, + maxNumberInputs: 255, + maxNumberOutputs: 255, + }; + + await updateVersionData(mysql, mockedVersionData); + + await maybeRefreshWalletConstants(mysql); + + const { + txMinWeight, + txWeightCoefficient, + txMinWeightK, + } = hathorLib.transaction.getTransactionWeightConstants(); + + const maxNumberInputs = hathorLib.transaction.getMaxInputsConstant(); + const maxNumberOutputs = hathorLib.transaction.getMaxOutputsConstant(); + + expect(mockGet).toHaveBeenCalledTimes(0); + expect(txMinWeight).toStrictEqual(mockedVersionData.minTxWeight); + expect(txWeightCoefficient).toStrictEqual(mockedVersionData.minTxWeightCoefficient); + expect(txMinWeightK).toStrictEqual(mockedVersionData.minTxWeightK); + expect(maxNumberInputs).toStrictEqual(mockedVersionData.maxNumberInputs); + expect(maxNumberOutputs).toStrictEqual(mockedVersionData.maxNumberOutputs); +}); + +test('searchForLatestValidBlock should find the first voided block', async () => { + expect.hasAssertions(); + + const spy = jest.spyOn(Utils, 'isTxVoided'); + + const mockImplementation = jest.fn(async (block: string): Promise<[boolean, any]> => { + const voidedList = [ + '0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7', + '00000649d769de25fcca204faaa23d4974d00fcb01130ab3f736fade4013598d', + '000002e185a37162bbcb1ec43576056638f0fad43648ae070194d1e1105f339a', + '00000597288221301f856e245579e7d32cea3e257330f9cb10178bb487b343e5', + ]; + + if (voidedList.indexOf(block) > -1) { + return [true, {}]; + } + + return [false, {}]; + }); + + spy.mockImplementation(mockImplementation); + + const mockData: Block[] = TX_IDS.map((tx, index) => ({ + txId: tx, + height: index, + timestamp: 0, + })); + + for (let i = 0; i < mockData.length; i++) { + await addOrUpdateTx(mysql, mockData[i].txId, mockData[i].height, i, 0, 60); + } + + const result = await searchForLatestValidBlock(mysql); + + expect(result.txId).toStrictEqual('000005cbcb8b29f74446a260cd7d36fab3cba1295ac9fe904795d7b064e0e53c'); +}); + +describe('getWalletBalancesForTx', () => { + it('should return an empty list when tx has no started wallet', async () => { + expect.hasAssertions(); + + // create a wallet + const wallet1 = { + id: 'wallet1', + }; + await createWallet(mysql, wallet1.id, XPUBKEY, AUTH_XPUBKEY, 5); + + const addr1 = 'addr1'; + const addr2 = 'addr2'; + + // add a transaction + const tx1 = { + id: 'txId1', + height: 1, + timestamp: 10, + version: 3, + weight: 65.4321, + }; + await addOrUpdateTx(mysql, tx1.id, tx1.height, tx1.timestamp, tx1.version, tx1.weight); + + // instantiate a token + const token1 = { + id: 'token1', + name: 'Token 1', + symbol: 'T1', + }; + await storeTokenInformation(mysql, token1.id, token1.name, token1.symbol); + + // transaction base + const utxos = [ + { index: 0, value: 5, address: addr1, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 1, value: 5, address: addr2, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + ]; + + // instantiate outputs + const outputs = utxos.map((utxo) => createOutput( + utxo.index, + utxo.value, + utxo.address, + utxo.tokenId, + utxo.timelock, + utxo.locked, + utxo.tokenData, + )); + await addUtxos(mysql, tx1.id, outputs); + + // instantiate inputs + const input = createInput( + utxos[0].value, + utxos[0].address, + tx1.id, + utxos[0].index, + utxos[0].tokenId, + utxos[0].timelock, + utxos[0].tokenData, + ) as TxInput; + await updateTxOutputSpentBy(mysql, [input], tx1.id); + + const tx = { + tx_id: tx1.id, + nonce: 10, + timestamp: tx1.timestamp, + version: tx1.version, + weight: tx1.weight, + parents: [], + inputs: [input], + outputs: [outputs[1] as TxOutput], + height: tx1.height, + token_name: token1.name, + token_symbol: token1.symbol, + } as Transaction; + const result = await getWalletBalancesForTx(mysql, tx); + + expect(result).toStrictEqual({}); + }); + + it('should return a list of WalletBalance when tx has a started wallet', async () => { + expect.hasAssertions(); + + // create a wallet + const wallet1 = { + id: 'wallet1', + }; + await createWallet(mysql, wallet1.id, XPUBKEY, AUTH_XPUBKEY, 5); + + // register an andress to started wallet + const addr1 = 'addr1'; + await addToAddressTable(mysql, [ + { address: addr1, index: 0, walletId: wallet1.id, transactions: 0 }, + ]); + // address without started wallet + const addr2 = 'addr2'; + + // add a transaction + const tx1 = { + id: 'txId1', + height: 1, + timestamp: 10, + version: 3, + weight: 65.4321, + }; + // The persistence is not necessary, but used for state consistency + await addOrUpdateTx(mysql, tx1.id, tx1.height, tx1.timestamp, tx1.version, tx1.weight); + + // instantiate a token + const token1 = { + id: 'token1', + name: 'Token 1', + symbol: 'T1', + }; + await storeTokenInformation(mysql, token1.id, token1.name, token1.symbol); + + // instantiate token balance + const balanceToken1 = { + unlocked: 5, + locked: 0, + lockExpires: null, + transactions: 1, + unlockedAuthorities: new Authorities(0b01), + lockedAuthorities: 0, + }; + + // update wallet state by tx1 + const walletBalanceMap1 = { + [wallet1.id]: TokenBalanceMap.fromStringMap({ + [token1.id]: balanceToken1, + }), + }; + await updateWalletTablesWithTx(mysql, tx1.id, tx1.timestamp, walletBalanceMap1); + + // transaction base + const utxos = [ + { index: 0, value: 5, address: addr1, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 1, value: 5, address: addr2, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + ]; + + // instantiate outputs + const outputs = utxos.map((utxo) => createOutput( + utxo.index, + utxo.value, + utxo.address, + utxo.tokenId, + utxo.timelock, + utxo.locked, + utxo.tokenData, + )); + await addUtxos(mysql, tx1.id, outputs); + + // instantiate inputs + const input = createInput( + utxos[0].value, + utxos[0].address, + tx1.id, + utxos[0].index, + utxos[0].tokenId, + utxos[0].timelock, + utxos[0].tokenData, + ) as TxInput; + await updateTxOutputSpentBy(mysql, [input], tx1.id); + + const tx = { + tx_id: tx1.id, + nonce: 10, + timestamp: tx1.timestamp, + version: tx1.version, + weight: tx1.weight, + parents: [], + inputs: [input], + outputs: [outputs[1] as TxOutput], + height: tx1.height, + token_name: token1.name, + token_symbol: token1.symbol, + } as Transaction; + const result = await getWalletBalancesForTx(mysql, tx); + + expect(result).toStrictEqual({ + wallet1: { + walletId: 'wallet1', + addresses: [ + 'addr1', + ], + txId: 'txId1', + walletBalanceForTx: [ + { + tokenId: 'token1', + tokenSymbol: 'T1', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + totalAmountSent: 0, + unlockedAmount: -5, + unlockedAuthorities: { + melt: false, + mint: false, + }, + total: -5, + }, + ], + }, + }); + }); + + describe('should be sorted by absolute token balance', () => { + it('sending token', async () => { + expect.hasAssertions(); + // create a wallet + const wallet1 = { + id: 'wallet1', + }; + await createWallet(mysql, wallet1.id, XPUBKEY, AUTH_XPUBKEY, 5); + + // register an andress to started wallet + const addr1 = 'addr1'; + await addToAddressTable(mysql, [ + { address: addr1, index: 0, walletId: wallet1.id, transactions: 0 }, + ]); + // address without started wallet + const addr2 = 'addr2'; + + // add a transaction + const tx1 = { + id: 'txId1', + height: 1, + timestamp: 10, + version: 3, + weight: 65.4321, + }; + // The persistence is not necessary, but used for state consistency + await addOrUpdateTx(mysql, tx1.id, tx1.height, tx1.timestamp, tx1.version, tx1.weight); + + // instantiate a token + const token1 = { + id: 'token1', + name: 'Token 1', + symbol: 'T1', + }; + await storeTokenInformation(mysql, token1.id, token1.name, token1.symbol); + const token2 = { + id: 'token2', + name: 'Token 2', + symbol: 'T2', + }; + await storeTokenInformation(mysql, token2.id, token2.name, token2.symbol); + + // instantiate token balance + const balanceToken1 = { + unlocked: 5, + locked: 0, + lockExpires: null, + transactions: 1, + unlockedAuthorities: new Authorities(0b01), + lockedAuthorities: 0, + }; + const balanceToken2 = { + unlocked: 10, + locked: 0, + lockExpires: null, + transactions: 1, + unlockedAuthorities: new Authorities(0b01), + lockedAuthorities: 0, + }; + + // update wallet state by tx1 + const wallet1BalanceMap = { + [wallet1.id]: TokenBalanceMap.fromStringMap({ + [token1.id]: balanceToken1, + [token2.id]: balanceToken2, + }), + }; + await updateWalletTablesWithTx(mysql, tx1.id, tx1.timestamp, wallet1BalanceMap); + + // transaction base + const utxos = [ + { index: 0, value: 5, address: addr1, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 1, value: 5, address: addr2, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 2, value: 10, address: addr1, tokenId: token2.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + ]; + + // instantiate outputs + const outputs = utxos.map((utxo) => createOutput( + utxo.index, + utxo.value, + utxo.address, + utxo.tokenId, + utxo.timelock, + utxo.locked, + utxo.tokenData, + )); + await addUtxos(mysql, tx1.id, outputs); + + // instantiate inputs + const inputToken1 = createInput( + utxos[0].value, + utxos[0].address, + tx1.id, + utxos[0].index, + utxos[0].tokenId, + utxos[0].timelock, + utxos[0].tokenData, + ) as TxInput; + const inputToken2 = createInput( + utxos[2].value, + utxos[2].address, + tx1.id, + utxos[2].index, + utxos[2].tokenId, + utxos[2].timelock, + utxos[2].tokenData, + ) as TxInput; + + const tx = { + tx_id: tx1.id, + nonce: 10, + timestamp: tx1.timestamp, + version: tx1.version, + weight: tx1.weight, + parents: [], + inputs: [inputToken1, inputToken2], + outputs: [outputs[1] as TxOutput], + height: tx1.height, + token_name: token1.name, + token_symbol: token1.symbol, + } as Transaction; + const result = await getWalletBalancesForTx(mysql, tx); + + expect(result).toStrictEqual({ + wallet1: { + walletId: 'wallet1', + addresses: [ + 'addr1', + ], + txId: 'txId1', + walletBalanceForTx: [ + { + tokenId: 'token2', + tokenSymbol: 'T2', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + total: -10, + totalAmountSent: 0, + unlockedAmount: -10, + unlockedAuthorities: { + melt: false, + mint: false, + }, + }, + { + tokenId: 'token1', + tokenSymbol: 'T1', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + totalAmountSent: 0, + unlockedAmount: -5, + unlockedAuthorities: { + melt: false, + mint: false, + }, + total: -5, + }, + ], + }, + }); + }); + + it('receiving token', async () => { + expect.hasAssertions(); + + // create a wallet + const wallet1 = { + id: 'wallet1', + }; + await createWallet(mysql, wallet1.id, XPUBKEY, AUTH_XPUBKEY, 5); + + // register an andress to started wallet + const addr1 = 'addr1'; + // address without started wallet + const addr2 = 'addr2'; + await addToAddressTable(mysql, [ + { address: addr2, index: 0, walletId: wallet1.id, transactions: 0 }, + ]); + + // add a transaction + const tx1 = { + id: 'txId1', + height: 1, + timestamp: 10, + version: 3, + weight: 65.4321, + }; + // The persistence is not necessary, but used for state consistency + await addOrUpdateTx(mysql, tx1.id, tx1.height, tx1.timestamp, tx1.version, tx1.weight); + + // instantiate a token + const token1 = { + id: 'token1', + name: 'Token 1', + symbol: 'T1', + }; + await storeTokenInformation(mysql, token1.id, token1.name, token1.symbol); + const token2 = { + id: 'token2', + name: 'Token 2', + symbol: 'T2', + }; + await storeTokenInformation(mysql, token2.id, token2.name, token2.symbol); + + // instantiate token balance + const balanceToken1 = { + unlocked: 5, + locked: 0, + lockExpires: null, + transactions: 1, + unlockedAuthorities: new Authorities(0b01), + lockedAuthorities: 0, + }; + const balanceToken2 = { + unlocked: 10, + locked: 0, + lockExpires: null, + transactions: 1, + unlockedAuthorities: new Authorities(0b01), + lockedAuthorities: 0, + }; + + // update wallet state by tx1 + const wallet1BalanceMap = { + [wallet1.id]: TokenBalanceMap.fromStringMap({ + [token1.id]: balanceToken1, + [token2.id]: balanceToken2, + }), + }; + await updateWalletTablesWithTx(mysql, tx1.id, tx1.timestamp, wallet1BalanceMap); + + // transaction base + const utxos = [ + { index: 0, value: 5, address: addr1, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 1, value: 5, address: addr2, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 2, value: 10, address: addr1, tokenId: token2.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 3, value: 10, address: addr2, tokenId: token2.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + ]; + + // instantiate outputs + const outputs = utxos.map((utxo) => createOutput( + utxo.index, + utxo.value, + utxo.address, + utxo.tokenId, + utxo.timelock, + utxo.locked, + utxo.tokenData, + )); + await addUtxos(mysql, tx1.id, outputs); + + // instantiate inputs + const inputToken1 = createInput( + utxos[0].value, + utxos[0].address, + tx1.id, + utxos[0].index, + utxos[0].tokenId, + utxos[0].timelock, + utxos[0].tokenData, + ) as TxInput; + const inputToken2 = createInput( + utxos[2].value, + utxos[2].address, + tx1.id, + utxos[2].index, + utxos[2].tokenId, + utxos[2].timelock, + utxos[2].tokenData, + ) as TxInput; + + const tx = { + tx_id: tx1.id, + nonce: 10, + timestamp: tx1.timestamp, + version: tx1.version, + weight: tx1.weight, + parents: [], + inputs: [inputToken1, inputToken2], + outputs: [outputs[1] as TxOutput, outputs[3] as TxOutput], + height: tx1.height, + token_name: token1.name, + token_symbol: token1.symbol, + } as Transaction; + const result = await getWalletBalancesForTx(mysql, tx); + + expect(result).toStrictEqual({ + wallet1: { + walletId: 'wallet1', + addresses: [ + 'addr2', + ], + txId: 'txId1', + walletBalanceForTx: [ + { + tokenId: 'token2', + tokenSymbol: 'T2', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + total: 10, + totalAmountSent: 10, + unlockedAmount: 10, + unlockedAuthorities: { + melt: false, + mint: false, + }, + }, + { + tokenId: 'token1', + tokenSymbol: 'T1', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + totalAmountSent: 5, + unlockedAmount: 5, + unlockedAuthorities: { + melt: false, + mint: false, + }, + total: 5, + }, + ], + }, + }); + }); + }); +}); diff --git a/packages/wallet-service/tests/db.test.ts b/packages/wallet-service/tests/db.test.ts new file mode 100644 index 00000000..d29c4cf5 --- /dev/null +++ b/packages/wallet-service/tests/db.test.ts @@ -0,0 +1,3655 @@ +import { logger } from '@tests/winston.mock'; +import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; +import { v4 as uuidv4 } from 'uuid'; +import { + addNewAddresses, + addUtxos, + createTxProposal, + createWallet, + generateAddresses, + getAddressWalletInfo, + getBlockByHeight, + getLatestHeight, + getTokenInformation, + getLockedUtxoFromInputs, + getTxProposal, + getUnusedAddresses, + getUtxos, + getAuthorityUtxo, + getUtxosLockedAtHeight, + getWallet, + getWalletAddressDetail, + getWalletAddresses, + getWalletTokens, + getWalletBalances, + getWalletSortedValueUtxos, + getVersionData, + getTxOutputsBySpent, + getTxOutput, + getTransactionsById, + getTxsAfterHeight, + getAddressAtIndex, + initWalletBalance, + initWalletTxHistory, + markUtxosWithProposalId, + updateTxOutputSpentBy, + storeTokenInformation, + unlockUtxos, + updateAddressLockedBalance, + updateAddressTablesWithTx, + updateExistingAddresses, + updateTxProposal, + updateWalletLockedBalance, + updateWalletStatus, + updateWalletAuthXpub, + updateWalletTablesWithTx, + updateVersionData, + fetchAddressTxHistorySum, + fetchAddressBalance, + addOrUpdateTx, + updateTx, + fetchTx, + markTxsAsVoided, + removeTxsHeight, + rebuildAddressBalancesFromUtxos, + markAddressTxHistoryAsVoided, + deleteBlocksAfterHeight, + markUtxosAsVoided, + unspendUtxos, + filterTxOutputs, + getTxProposalInputs, + addMiner, + getMinersList, + getTotalSupply, + getExpiredTimelocksUtxos, + getTotalTransactions, + getAvailableAuthorities, + getAffectedAddressTxCountFromTxList, + incrementTokensTxCount, + registerPushDevice, + existsPushDevice, + updatePushDevice, + unregisterPushDevice, + getTransactionById, + getPushDevice, + removeAllPushDevicesByDeviceId, + existsWallet, + getPushDeviceSettingsList, + getTokenSymbols, + countStalePushDevices, + deleteStalePushDevices, + releaseTxProposalUtxos, + getUnsentTxProposals, + getLatestBlockByHeight, + cleanupVoidedTx, + checkTxWasVoided, + getWalletTxHistory, +} from '@src/db'; +import * as Db from '@src/db'; +import { cleanUnsentTxProposalsUtxos } from '@src/db/cronRoutines'; +import { + beginTransaction, + rollbackTransaction, + commitTransaction, +} from '@src/db/utils'; +import { + Authorities, + TokenBalanceMap, + TokenInfo, + TxProposalStatus, + WalletStatus, + FullNodeVersionData, + Tx, + DbTxOutput, + PushDevice, + PushProvider, + Severity, + Block, + AddressInfo, +} from '@src/types'; +import { + closeDbConnection, + getDbConnection, + getUnixTimestamp, + isAuthority, + getWalletId, +} from '@src/utils'; +import { + ADDRESSES, + XPUBKEY, + AUTH_XPUBKEY, + addToAddressBalanceTable, + addToAddressTable, + addToAddressTxHistoryTable, + addToTokenTable, + addToUtxoTable, + addToWalletBalanceTable, + addToWalletTxHistoryTable, + addToWalletTable, + cleanDatabase, + checkAddressBalanceTable, + checkAddressTable, + checkAddressTxHistoryTable, + checkVersionDataTable, + checkUtxoTable, + checkWalletBalanceTable, + checkWalletTxHistoryTable, + createOutput, + createInput, + countTxOutputTable, + checkTokenTable, + checkPushDevicesTable, + buildPushRegister, + insertPushDevice, + daysAgo, + addToTransactionTable, +} from '@tests/utils'; +import { AddressTxHistoryTableEntry } from '@tests/types'; + +import { constants } from '@hathor/wallet-lib'; + +const mysql = getDbConnection(); + +const addrMap = {}; +for (const [index, address] of ADDRESSES.entries()) { + addrMap[address] = index; +} + +beforeEach(async () => { + jest.resetModules(); + await cleanDatabase(mysql); +}); + +afterAll(async () => { + await closeDbConnection(mysql); +}); + +test('generateAddresses', async () => { + expect.hasAssertions(); + const maxGap = 5; + const address0 = ADDRESSES[0]; + + // check first with no addresses on database, so it should return only maxGap addresses + let addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); + + expect(addressesInfo.addresses).toHaveLength(maxGap); + expect(addressesInfo.existingAddresses).toStrictEqual({}); + expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(maxGap); + expect(addressesInfo.addresses[0]).toBe(address0); + + // add first address with no transactions. As it's not used, we should still only generate maxGap addresses + await addToAddressTable(mysql, [{ + address: address0, + index: 0, + walletId: null, + transactions: 0, + }]); + addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); + expect(addressesInfo.addresses).toHaveLength(maxGap); + expect(addressesInfo.existingAddresses).toStrictEqual({ [address0]: 0 }); + expect(addressesInfo.lastUsedAddressIndex).toStrictEqual(-1); + + let totalLength = Object.keys(addressesInfo.addresses).length; + let existingLength = Object.keys(addressesInfo.existingAddresses).length; + expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(totalLength - existingLength); + expect(addressesInfo.addresses[0]).toBe(address0); + + // mark address as used and check again + let usedIndex = 0; + await mysql.query('UPDATE `address` SET `transactions` = ? WHERE `address` = ?', [1, address0]); + addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); + expect(addressesInfo.addresses).toHaveLength(maxGap + usedIndex + 1); + expect(addressesInfo.existingAddresses).toStrictEqual({ [address0]: 0 }); + expect(addressesInfo.lastUsedAddressIndex).toStrictEqual(0); + + totalLength = Object.keys(addressesInfo.addresses).length; + existingLength = Object.keys(addressesInfo.existingAddresses).length; + expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(totalLength - existingLength); + + // add address with index 1 as used + usedIndex = 1; + const address1 = ADDRESSES[1]; + await addToAddressTable(mysql, [{ + address: address1, + index: usedIndex, + walletId: null, + transactions: 1, + }]); + addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); + expect(addressesInfo.addresses).toHaveLength(maxGap + usedIndex + 1); + expect(addressesInfo.existingAddresses).toStrictEqual({ [address0]: 0, [address1]: 1 }); + expect(addressesInfo.lastUsedAddressIndex).toStrictEqual(1); + totalLength = Object.keys(addressesInfo.addresses).length; + existingLength = Object.keys(addressesInfo.existingAddresses).length; + expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(totalLength - existingLength); + + // add address with index 4 as used + usedIndex = 4; + const address4 = ADDRESSES[4]; + await addToAddressTable(mysql, [{ + address: address4, + index: usedIndex, + walletId: null, + transactions: 1, + }]); + addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); + expect(addressesInfo.addresses).toHaveLength(maxGap + usedIndex + 1); + expect(addressesInfo.existingAddresses).toStrictEqual({ [address0]: 0, [address1]: 1, [address4]: 4 }); + expect(addressesInfo.lastUsedAddressIndex).toStrictEqual(4); + totalLength = Object.keys(addressesInfo.addresses).length; + existingLength = Object.keys(addressesInfo.existingAddresses).length; + expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(totalLength - existingLength); + + // make sure no address was skipped from being generated + for (const [index, address] of addressesInfo.addresses.entries()) { + expect(ADDRESSES[index]).toBe(address); + } +}, 25000); + +test('getAddressWalletInfo', async () => { + expect.hasAssertions(); + const wallet1 = { walletId: 'wallet1', xpubkey: 'xpubkey1', authXpubkey: 'authXpubkey', maxGap: 5 }; + const wallet2 = { walletId: 'wallet2', xpubkey: 'xpubkey2', authXpubkey: 'authXpubkey2', maxGap: 5 }; + const finalMap = { + addr1: wallet1, + addr2: wallet1, + addr3: wallet2, + }; + + // populate address table + for (const [address, wallet] of Object.entries(finalMap)) { + await addToAddressTable(mysql, [{ + address, + index: 0, + walletId: wallet.walletId, + transactions: 0, + }]); + } + // add address that won't be requested on walletAddressMap + await addToAddressTable(mysql, [{ + address: 'addr4', + index: 0, + walletId: 'wallet3', + transactions: 0, + }]); + + // populate wallet table + for (const wallet of Object.values(finalMap)) { + const entry = { + id: wallet.walletId, + xpubkey: wallet.xpubkey, + auth_xpubkey: wallet.authXpubkey, + status: WalletStatus.READY, + max_gap: wallet.maxGap, + created_at: 0, + ready_at: 0, + }; + await mysql.query('INSERT INTO `wallet` SET ? ON DUPLICATE KEY UPDATE id=id', [entry]); + } + // add wallet that should not be on the results + await addToWalletTable(mysql, [{ + id: 'wallet3', + xpubkey: 'xpubkey3', + authXpubkey: 'authxpubkey3', + status: WalletStatus.READY, + maxGap: 5, + createdAt: 0, + readyAt: 0, + }]); + + const addressWalletMap = await getAddressWalletInfo(mysql, Object.keys(finalMap)); + expect(addressWalletMap).toStrictEqual(finalMap); +}); + +test('updateWalletAuthXpub', async () => { + expect.hasAssertions(); + const walletId = 'walletId'; + + // add the wallet to database + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 20); + await updateWalletAuthXpub(mysql, walletId, 'new_auth_xpubkey'); + + const wallet = await getWallet(mysql, walletId); + expect(wallet.authXpubkey).toStrictEqual('new_auth_xpubkey'); +}); + +test('getWallet, createWallet and updateWalletStatus', async () => { + expect.hasAssertions(); + const walletId = getWalletId(XPUBKEY); + // if there are no entries, should return null + let ret = await getWallet(mysql, walletId); + expect(ret).toBeNull(); + + // add entry to database + let timestamp = getUnixTimestamp(); + const createRet = await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + // get status + ret = await getWallet(mysql, walletId); + expect(ret).toStrictEqual(createRet); + expect(ret.status).toBe(WalletStatus.CREATING); + expect(ret.xpubkey).toBe(XPUBKEY); + expect(ret.maxGap).toBe(5); + expect(ret.createdAt).toBeGreaterThanOrEqual(timestamp); + expect(ret.readyAt).toBeNull(); + + // update wallet status to ready + timestamp = ret.createdAt; + await updateWalletStatus(mysql, walletId, WalletStatus.READY); + ret = await getWallet(mysql, walletId); + expect(ret.status).toBe(WalletStatus.READY); + expect(ret.xpubkey).toBe(XPUBKEY); + expect(ret.maxGap).toBe(5); + expect(ret.createdAt).toBe(timestamp); + expect(ret.readyAt).toBeGreaterThanOrEqual(timestamp); +}); + +test('addNewAddresses', async () => { + expect.hasAssertions(); + const walletId = 'walletId'; + + // test adding empty dict + await addNewAddresses(mysql, walletId, {}, -1); + await expect(checkAddressTable(mysql, 0)).resolves.toBe(true); + + // add some addresses + await addNewAddresses(mysql, walletId, addrMap, -1); + for (const [index, address] of ADDRESSES.entries()) { + await expect(checkAddressTable(mysql, ADDRESSES.length, address, index, walletId, 0)).resolves.toBe(true); + } +}); + +test('updateExistingAddresses', async () => { + expect.hasAssertions(); + const walletId = 'walletId'; + + // test adding empty dict + await updateExistingAddresses(mysql, walletId, {}); + await expect(checkAddressTable(mysql, 0)).resolves.toBe(true); + + // first add some addresses to database, without walletId and index + const newAddrMap = {}; + for (const address of ADDRESSES) { + newAddrMap[address] = null; + } + await addNewAddresses(mysql, null, newAddrMap, -1); + for (const address of ADDRESSES) { + await expect(checkAddressTable(mysql, ADDRESSES.length, address, null, null, 0)).resolves.toBe(true); + } + + // now update addresses with walletId + await updateExistingAddresses(mysql, walletId, addrMap); + for (const [index, address] of ADDRESSES.entries()) { + await expect(checkAddressTable(mysql, ADDRESSES.length, address, index, walletId, 0)).resolves.toBe(true); + } +}); + +test('initWalletTxHistory', async () => { + expect.hasAssertions(); + const walletId = 'walletId'; + const addr1 = 'addr1'; + const addr2 = 'addr2'; + const addr3 = 'addr3'; + const token1 = 'token1'; + const token2 = 'token2'; + const token3 = 'token3'; + const txId1 = 'txId1'; + const txId2 = 'txId2'; + const timestamp1 = 10; + const timestamp2 = 20; + + /* + * addr1 and addr2 belong to our wallet, while addr3 does not. We are adding this last + * address to make sure the wallet history will only get the balance from its own addresses + * + * These transactions are not valid under network rules, but here we only want to test the + * database updates and final values + * + * tx1: + * . addr1: receive 10 token1 and 7 token2 (+10 token1, +7 token2); + * . addr2: receive 5 token2 (+5 token2); + * . addr3: receive 3 token1 (+3 token1); + * tx2: + * . addr1: send 1 token1 and receive 3 token3 (-1 token1, +3 token3); + * . addr2: send 5 token2 (-5 token2); + * . addr3: receive 3 token1 (+3 token1); + * + * Final entries for wallet_tx_history will be: + * . txId1 token1 +10 + * . txId1 token2 +12 + * . txId2 token1 -1 + * . txId2 token2 -5 + * . txId2 token3 +3 + */ + + // with empty addresses it shouldn't add anything + await initWalletTxHistory(mysql, walletId, []); + await expect(checkWalletTxHistoryTable(mysql, 0)).resolves.toBe(true); + + const entries = [ + { address: addr1, txId: txId1, tokenId: token1, balance: 10, timestamp: timestamp1 }, + { address: addr1, txId: txId1, tokenId: token2, balance: 7, timestamp: timestamp1 }, + { address: addr2, txId: txId1, tokenId: token2, balance: 5, timestamp: timestamp1 }, + { address: addr3, txId: txId1, tokenId: token1, balance: 3, timestamp: timestamp1 }, + { address: addr1, txId: txId2, tokenId: token1, balance: -1, timestamp: timestamp2 }, + { address: addr1, txId: txId2, tokenId: token3, balance: 3, timestamp: timestamp2 }, + { address: addr2, txId: txId2, tokenId: token2, balance: -5, timestamp: timestamp2 }, + { address: addr3, txId: txId2, tokenId: token1, balance: 3, timestamp: timestamp2 }, + ]; + await addToAddressTxHistoryTable(mysql, entries); + + await initWalletTxHistory(mysql, walletId, [addr1, addr2]); + + // check wallet_tx_history entries + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token1, txId1, 10, timestamp1)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token2, txId1, 12, timestamp1)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token1, txId2, -1, timestamp2)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token2, txId2, -5, timestamp2)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token3, txId2, 3, timestamp2)).resolves.toBe(true); +}); + +test('initWalletBalance', async () => { + expect.hasAssertions(); + const walletId = 'walletId'; + const addr1 = 'addr1'; + const addr2 = 'addr2'; + const addr3 = 'addr3'; + const token1 = 'token1'; + const token2 = 'token2'; + const tx1 = 'tx1'; + const tx2 = 'tx2'; + const tx3 = 'tx3'; + const ts1 = 0; + const ts2 = 10; + const ts3 = 20; + const timelock = 500; + + /* + * addr1 and addr2 belong to our wallet, while addr3 does not. We are adding this last + * address to make sure the wallet will only get the balance from its own addresses + */ + const historyEntries = [ + { address: addr1, txId: tx1, tokenId: token1, balance: 10, timestamp: ts1 }, + { address: addr1, txId: tx2, tokenId: token1, balance: -8, timestamp: ts2 }, + { address: addr1, txId: tx1, tokenId: token2, balance: 5, timestamp: ts1 }, + { address: addr2, txId: tx1, tokenId: token1, balance: 3, timestamp: ts1 }, + { address: addr2, txId: tx3, tokenId: token1, balance: 4, timestamp: ts3 }, + { address: addr2, txId: tx2, tokenId: token2, balance: 2, timestamp: ts2 }, + { address: addr3, txId: tx1, tokenId: token1, balance: 1, timestamp: ts1 }, + { address: addr3, txId: tx3, tokenId: token2, balance: 11, timestamp: ts3 }, + ]; + const addressEntries = [ + // address, tokenId, unlocked, locked, lockExpires, transactions, unlocked_authorities, locked_authorities, total_received + [addr1, token1, 2, 0, null, 2, 1, 0, 4], + [addr1, token2, 1, 4, timelock, 1, 2, 0, 5], + [addr2, token1, 5, 2, null, 2, 2, 0, 20], + [addr2, token2, 0, 2, null, 1, 0, 0, 2], + [addr3, token1, 0, 1, null, 1, 0, 0, 1], + [addr3, token2, 10, 1, null, 1, 0, 0, 11], + ]; + + await addToAddressTxHistoryTable(mysql, historyEntries); + await addToAddressBalanceTable(mysql, addressEntries); + + await initWalletBalance(mysql, walletId, [addr1, addr2]); + + // check balance entries + await expect(checkWalletBalanceTable(mysql, 2, walletId, token1, 7, 2, null, 3, 3)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 2, walletId, token2, 1, 6, timelock, 2, 2)).resolves.toBe(true); +}); + +test('updateWalletTablesWithTx', async () => { + expect.hasAssertions(); + const walletId = 'walletId'; + const walletId2 = 'walletId2'; + const token1 = 'token1'; + const token2 = 'token2'; + const tx1 = 'txId1'; + const tx2 = 'txId2'; + const tx3 = 'txId3'; + const ts1 = 10; + const ts2 = 20; + const ts3 = 30; + + await addToAddressTable(mysql, [ + { address: 'addr1', index: 0, walletId, transactions: 1 }, + { address: 'addr2', index: 1, walletId, transactions: 1 }, + { address: 'addr3', index: 2, walletId, transactions: 1 }, + { address: 'addr4', index: 0, walletId: walletId2, transactions: 1 }, + ]); + + // add tx1 + const walletBalanceMap1 = { + walletId: TokenBalanceMap.fromStringMap({ token1: { unlocked: 5, locked: 0, unlockedAuthorities: new Authorities(0b01) } }), + }; + await updateWalletTablesWithTx(mysql, tx1, ts1, walletBalanceMap1); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token1, 5, 0, null, 1, 0b01, 0)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 1, walletId, token1, tx1, 5, ts1)).resolves.toBe(true); + + // add tx2 + const walletBalanceMap2 = { + walletId: TokenBalanceMap.fromStringMap( + { + token1: { unlocked: -2, locked: 1, lockExpires: 500, unlockedAuthorities: new Authorities(0b11) }, + token2: { unlocked: 7, locked: 0 }, + }, + ), + }; + await updateWalletTablesWithTx(mysql, tx2, ts2, walletBalanceMap2); + await expect(checkWalletBalanceTable(mysql, 2, walletId, token1, 3, 1, 500, 2, 0b11, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 2, walletId, token2, 7, 0, null, 1)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 3, walletId, token1, tx1, 5, ts1)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 3, walletId, token1, tx2, -1, ts2)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 3, walletId, token2, tx2, 7, ts2)).resolves.toBe(true); + + // add tx3 + const walletBalanceMap3 = { + walletId: TokenBalanceMap.fromStringMap({ token1: { unlocked: 1, locked: 2, lockExpires: 200, unlockedAuthorities: new Authorities([-1, -1]) } }), + walletId2: TokenBalanceMap.fromStringMap({ token2: { unlocked: 10, locked: 0 } }), + }; + // the tx above removes an authority, which will trigger a "refresh" on the available authorities. + // Let's pretend there's another utxo with some authorities as well + await addToAddressTable(mysql, [{ + address: 'address1', + index: 0, + walletId, + transactions: 1, + }]); + await addToAddressBalanceTable(mysql, [['address1', token1, 0, 0, null, 1, 0b10, 0, 0]]); + + await updateWalletTablesWithTx(mysql, tx3, ts3, walletBalanceMap3); + await expect(checkWalletBalanceTable(mysql, 3, walletId, token1, 4, 3, 200, 3, 0b10, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, walletId, token2, 7, 0, null, 1)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, walletId2, token2, 10, 0, null, 1)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token1, tx1, 5, ts1)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token1, tx2, -1, ts2)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token2, tx2, 7, ts2)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId, token1, tx3, 3, ts3)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 5, walletId2, token2, tx3, 10, ts3)).resolves.toBe(true); +}); + +test('addUtxos, getUtxos, unlockUtxos, updateTxOutputSpentBy, unspendUtxos, getTxOutput, getTxOutputsBySpent and markUtxosAsVoided', async () => { + expect.hasAssertions(); + + const txId = 'txId'; + const utxos = [ + { value: 5, address: 'address1', tokenId: 'token1', locked: false }, + { value: 15, address: 'address1', tokenId: 'token1', locked: false }, + { value: 25, address: 'address2', tokenId: 'token2', timelock: 500, locked: true }, + { value: 35, address: 'address2', tokenId: 'token1', locked: false }, + // authority utxo + { value: 0b11, address: 'address1', tokenId: 'token1', locked: false, tokenData: 129 }, + ]; + + // empty list should be fine + await addUtxos(mysql, txId, []); + + // add to utxo table + const outputs = utxos.map((utxo, index) => createOutput( + index, + utxo.value, + utxo.address, + utxo.tokenId, + utxo.timelock || null, + utxo.locked, + utxo.tokenData || 0, + )); + await addUtxos(mysql, txId, outputs); + + for (const [_, output] of outputs.entries()) { + let { value } = output; + const { token, decoded } = output; + let authorities = 0; + if (isAuthority(output.token_data)) { + authorities = value; + value = 0; + } + await expect( + checkUtxoTable(mysql, utxos.length, txId, output.index, token, decoded.address, value, authorities, decoded.timelock, null, output.locked), + ).resolves.toBe(true); + } + + // getUtxos + let results = await getUtxos(mysql, utxos.map((_utxo, index) => ({ txId, index }))); + expect(results).toHaveLength(utxos.length); + // fetch only 2 + results = await getUtxos(mysql, [{ txId, index: 0 }, { txId, index: 1 }]); + expect(results).toHaveLength(2); + + // get an unspent tx output + expect(await getTxOutput(mysql, txId, 0, true)).toStrictEqual({ + txId: 'txId', + index: 0, + tokenId: utxos[0].tokenId, + address: utxos[0].address, + value: utxos[0].value, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + txProposalId: null, + txProposalIndex: null, + }); + + // empty list should be fine + await unlockUtxos(mysql, []); + + const inputs = utxos.map((utxo, index) => createInput(utxo.value, utxo.address, txId, index, utxo.tokenId, utxo.timelock)); + + // set tx_outputs as spent + await updateTxOutputSpentBy(mysql, inputs, txId); + + // get a spent tx output + expect(await getTxOutput(mysql, txId, 0, false)).toStrictEqual({ + txId: 'txId', + index: 0, + tokenId: utxos[0].tokenId, + address: utxos[0].address, + value: utxos[0].value, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: txId, + txProposalId: null, + txProposalIndex: null, + }); + + // if the tx output is not found, it should return null + expect(await getTxOutput(mysql, 'unknown-tx-id', 0, false)).toBeNull(); + + await expect(checkUtxoTable(mysql, 0)).resolves.toBe(true); + + const spentTxOutputs = await getTxOutputsBySpent(mysql, [txId]); + expect(spentTxOutputs).toHaveLength(5); + + const txOutputs = utxos.map((utxo, index) => ({ + ...utxo, + txId, + authorities: 0, + heightlock: null, + timelock: null, + index, + })); + + await unspendUtxos(mysql, txOutputs); + + for (const [index, output] of outputs.entries()) { + let { value } = output; + const { token, decoded } = output; + let authorities = 0; + if (isAuthority(output.token_data)) { + authorities = value; + value = 0; + } + await expect( + checkUtxoTable(mysql, utxos.length, txId, index, token, decoded.address, value, authorities, decoded.timelock, null, output.locked), + ).resolves.toBe(true); + } + + // unlock the locked one + const first = { + txId, + index: 2, + tokenId: 'token2', + address: 'address2', + value: 25, + authorities: 0, + timelock: 500, + heightlock: null, + locked: true, + }; + await unlockUtxos(mysql, [first]); + await expect(checkUtxoTable( + mysql, utxos.length, first.txId, first.index, first.tokenId, first.address, first.value, 0, first.timelock, first.heightlock, false, + )).resolves.toBe(true); + + const countBeforeDelete = await countTxOutputTable(mysql); + expect(countBeforeDelete).toStrictEqual(5); + + await markUtxosAsVoided(mysql, txOutputs); + + const countAfterDelete = await countTxOutputTable(mysql); + expect(countAfterDelete).toStrictEqual(0); +}); + +test('getLockedUtxoFromInputs', async () => { + expect.hasAssertions(); + const txId = 'txId'; + const utxos = [ + { value: 5, address: 'address1', token: 'token1', locked: false }, + { value: 25, address: 'address2', token: 'token2', timelock: 500, locked: true }, + { value: 35, address: 'address2', token: 'token1', locked: false }, + ]; + + // add to utxo table + const outputs = utxos.map((utxo, index) => createOutput(index, utxo.value, utxo.address, utxo.token, utxo.timelock || null, utxo.locked)); + await addUtxos(mysql, txId, outputs); + for (const [index, output] of outputs.entries()) { + const { token, decoded, value } = output; + await expect(checkUtxoTable(mysql, 3, txId, index, token, decoded.address, value, 0, decoded.timelock, null, output.locked)).resolves.toBe(true); + } + + const inputs = utxos.map((utxo, index) => createInput(utxo.value, utxo.address, txId, index, utxo.token, utxo.timelock)); + const results = await getLockedUtxoFromInputs(mysql, inputs); + expect(results).toHaveLength(1); + expect(results[0].value).toBe(25); +}); + +test('updateAddressTablesWithTx', async () => { + expect.hasAssertions(); + const address1 = 'address1'; + const address2 = 'address2'; + const token1 = 'token1'; + const token2 = 'token2'; + const token3 = 'token3'; + // we'll add address1 to the address table already, as if it had already received another transaction + await addToAddressTable(mysql, [ + { address: address1, index: null, walletId: null, transactions: 1 }, + ]); + + const txId1 = 'txId1'; + const timestamp1 = 10; + const addrMap1 = { + address1: TokenBalanceMap.fromStringMap({ + token1: { unlocked: 10, locked: 0 }, + token2: { unlocked: 7, locked: 0 }, + token3: { unlocked: 2, locked: 0, unlockedAuthorities: new Authorities(0b01) }, + }), + address2: TokenBalanceMap.fromStringMap({ token1: { unlocked: 8, locked: 0, unlockedAuthorities: new Authorities(0b01) } }), + }; + + await updateAddressTablesWithTx(mysql, txId1, timestamp1, addrMap1); + await expect(checkAddressTable(mysql, 2, address1, null, null, 2)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 2, address2, null, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address1, token1, 10, 0, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address1, token2, 7, 0, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address1, token3, 2, 0, null, 1, 0b01, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address2, token1, 8, 0, null, 1, 0b01, 0)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 4, address1, txId1, token1, 10, timestamp1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 4, address1, txId1, token2, 7, timestamp1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 4, address1, txId1, token3, 2, timestamp1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 4, address2, txId1, token1, 8, timestamp1)).resolves.toBe(true); + + // this tx removes an authority for address1,token3 + const txId2 = 'txId2'; + const timestamp2 = 15; + const addrMap2 = { + address1: TokenBalanceMap.fromStringMap({ token1: { unlocked: -5, locked: 0 }, + token3: { unlocked: 6, locked: 0, unlockedAuthorities: new Authorities([-1]) } }), + address2: TokenBalanceMap.fromStringMap({ token1: { unlocked: 8, locked: 0, unlockedAuthorities: new Authorities(0b10) }, + token2: { unlocked: 3, locked: 0 } }), + }; + + await updateAddressTablesWithTx(mysql, txId2, timestamp2, addrMap2); + await expect(checkAddressTable(mysql, 2, address1, null, null, 3)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 2, address2, null, null, 2)).resolves.toBe(true); + // final balance for each (address,token) + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5, 0, null, 2)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token2', 7, 0, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token3', 8, 0, null, 2, 0, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address2, 'token1', 16, 0, null, 2, 0b11, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address2, 'token2', 3, 0, null, 1)).resolves.toBe(true); + // tx history + await expect(checkAddressTxHistoryTable(mysql, 8, address1, txId2, token1, -5, timestamp2)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, address1, txId2, token3, 6, timestamp2)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, address2, txId2, token1, 8, timestamp2)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, address2, txId2, token2, 3, timestamp2)).resolves.toBe(true); + // make sure entries in address_tx_history from txId1 haven't been changed + await expect(checkAddressTxHistoryTable(mysql, 8, address1, txId1, token1, 10, timestamp1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, address1, txId1, token2, 7, timestamp1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, address1, txId1, token3, 2, timestamp1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, address2, txId1, token1, 8, timestamp1)).resolves.toBe(true); + + // a tx with timelock + const txId3 = 'txId3'; + const timestamp3 = 20; + const lockExpires = 5000; + const addrMap3 = { + address1: TokenBalanceMap.fromStringMap({ token1: { unlocked: 0, locked: 3, lockExpires } }), + }; + await updateAddressTablesWithTx(mysql, txId3, timestamp3, addrMap3); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5, 3, lockExpires, 3)).resolves.toBe(true); + + // another tx, with higher timelock + const txId4 = 'txId4'; + const timestamp4 = 25; + const addrMap4 = { + address1: TokenBalanceMap.fromStringMap({ token1: { unlocked: 0, locked: 2, lockExpires: lockExpires + 1 } }), + }; + await updateAddressTablesWithTx(mysql, txId4, timestamp4, addrMap4); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5, 5, lockExpires, 4)).resolves.toBe(true); + + // another tx, with lower timelock + const txId5 = 'txId5'; + const timestamp5 = 25; + const addrMap5 = { + address1: TokenBalanceMap.fromStringMap({ token1: { unlocked: 0, locked: 2, lockExpires: lockExpires - 1 } }), + }; + await updateAddressTablesWithTx(mysql, txId5, timestamp5, addrMap5); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5, 7, lockExpires - 1, 5)).resolves.toBe(true); +}); + +test('getWalletTokens', async () => { + expect.hasAssertions(); + const wallet1 = 'wallet1'; + const wallet2 = 'wallet2'; + + await addToWalletTxHistoryTable(mysql, [ + [wallet1, 'tx1', '00', 5, 1000, false], + [wallet1, 'tx1', 'token2', 70, 1000, false], + [wallet1, 'tx2', 'token3', 10, 1001, false], + [wallet1, 'tx3', 'token4', 25, 1001, false], + [wallet1, 'tx4', 'token2', 30, 1001, false], + [wallet2, 'tx5', '00', 35, 1001, false], + [wallet2, 'tx6', 'token2', 31, 1001, false], + ]); + + const wallet1Tokens = await getWalletTokens(mysql, wallet1); + const wallet2Tokens = await getWalletTokens(mysql, wallet2); + + expect(wallet1Tokens).toHaveLength(4); + expect(wallet2Tokens).toHaveLength(2); +}); + +test('getWalletAddresses', async () => { + expect.hasAssertions(); + const walletId = 'walletId'; + const lastIndex = 5; + // add some addresses into db + const entries = []; + for (let i = 0; i < lastIndex; i++) { + entries.push({ + address: ADDRESSES[i], + index: i, + walletId, + transactions: 0, + }); + } + // add entry to beginning of array, to test if method will return addresses ordered + entries.unshift({ + address: ADDRESSES[lastIndex], + index: lastIndex, + walletId, + transactions: 0, + }); + await addToAddressTable(mysql, entries); + + const returnedAddresses = await getWalletAddresses(mysql, walletId); + expect(returnedAddresses).toHaveLength(lastIndex + 1); + for (const [i, address] of returnedAddresses.entries()) { + expect(i).toBe(address.index); + expect(address.address).toBe(ADDRESSES[i]); + } + + // if we pass the filterAddresses optional parameter, we should receive just these + const filteredReturnedAddresses = await getWalletAddresses(mysql, walletId, [ + ADDRESSES[0], + ADDRESSES[2], + ADDRESSES[3], + ]); + + expect(filteredReturnedAddresses).toHaveLength(3); + expect(filteredReturnedAddresses[0].address).toBe(ADDRESSES[0]); + expect(filteredReturnedAddresses[1].address).toBe(ADDRESSES[2]); + expect(filteredReturnedAddresses[2].address).toBe(ADDRESSES[3]); +}); + +test('getWalletAddressDetail', async () => { + expect.hasAssertions(); + const walletId = 'walletId'; + const lastIndex = 5; + // add some addresses into db + const entries = []; + for (let i = 0; i < lastIndex; i++) { + entries.push({ + address: ADDRESSES[i], + index: i, + walletId, + transactions: 0, + }); + } + await addToAddressTable(mysql, entries); + + const detail0 = await getWalletAddressDetail(mysql, walletId, ADDRESSES[0]); + expect(detail0.address).toBe(ADDRESSES[0]); + expect(detail0.index).toBe(0); + expect(detail0.transactions).toBe(0); + + const detail3 = await getWalletAddressDetail(mysql, walletId, ADDRESSES[3]); + expect(detail3.address).toBe(ADDRESSES[3]); + expect(detail3.index).toBe(3); + expect(detail3.transactions).toBe(0); + + const detailNull = await getWalletAddressDetail(mysql, walletId, ADDRESSES[8]); + expect(detailNull).toBeNull(); +}); + +test('getWalletBalances', async () => { + expect.hasAssertions(); + const walletId = 'walletId'; + const token1 = new TokenInfo('token1', 'MyToken1', 'MT1'); + const token2 = new TokenInfo('token2', 'MyToken2', 'MT2'); + const now = 1000; + // add some balances into db + + await addToWalletBalanceTable(mysql, [{ + walletId, + tokenId: token1.id, + unlockedBalance: 10, + lockedBalance: 4, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: now, + transactions: 1, + }, { + walletId, + tokenId: token2.id, + unlockedBalance: 20, + lockedBalance: 5, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: now, + transactions: 2, + }, { + walletId: 'otherId', + tokenId: token1.id, + unlockedBalance: 30, + lockedBalance: 1, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: now, + transactions: 3, + }]); + + await addToTokenTable(mysql, [ + { id: token1.id, name: token1.name, symbol: token1.symbol, transactions: 0 }, + { id: token2.id, name: token2.name, symbol: token2.symbol, transactions: 0 }, + ]); + + // first test fetching all tokens + let returnedBalances = await getWalletBalances(mysql, walletId); + expect(returnedBalances).toHaveLength(2); + for (const balance of returnedBalances) { + if (balance.token.id === token1.id) { + expect(balance.token).toStrictEqual(token1); + expect(balance.balance.unlockedAmount).toBe(10); + expect(balance.balance.lockedAmount).toBe(4); + expect(balance.balance.lockExpires).toBe(now); + expect(balance.transactions).toBe(1); + } else { + expect(balance.token).toStrictEqual(token2); + expect(balance.balance.unlockedAmount).toBe(20); + expect(balance.balance.lockedAmount).toBe(5); + expect(balance.transactions).toBe(2); + expect(balance.balance.lockExpires).toBe(now); + } + } + + // fetch both tokens explicitly + returnedBalances = await getWalletBalances(mysql, walletId, [token1.id, token2.id]); + expect(returnedBalances).toHaveLength(2); + + // fetch only balance for token2 + returnedBalances = await getWalletBalances(mysql, walletId, [token2.id]); + expect(returnedBalances).toHaveLength(1); + expect(returnedBalances[0].token).toStrictEqual(token2); + expect(returnedBalances[0].balance.unlockedAmount).toBe(20); + expect(returnedBalances[0].balance.lockedAmount).toBe(5); + expect(returnedBalances[0].balance.lockExpires).toBe(now); + expect(returnedBalances[0].transactions).toBe(2); + + // fetch balance for non existing token + returnedBalances = await getWalletBalances(mysql, walletId, ['otherToken']); + expect(returnedBalances).toHaveLength(0); +}); + +test('getUtxosLockedAtHeight', async () => { + expect.hasAssertions(); + + const txId = 'txId'; + const txId2 = 'txId2'; + const utxos = [ + // no locks + { value: 5, address: 'address1', token: 'token1', locked: false }, + // only timelock + { value: 25, address: 'address2', token: 'token2', timelock: 50, locked: false }, + + ]; + const utxos2 = [ + // only heightlock + { value: 35, address: 'address2', token: 'token1', timelock: null, locked: true }, + // timelock and heightlock + { value: 45, address: 'address2', token: 'token1', timelock: 100, locked: true }, + { value: 55, address: 'address2', token: 'token1', timelock: 1000, locked: true }, + ]; + + // add to utxo table + const outputs = utxos.map((utxo, index) => createOutput(index, utxo.value, utxo.address, utxo.token, utxo.timelock, utxo.locked)); + await addUtxos(mysql, txId, outputs, null); + const outputs2 = utxos2.map((utxo, index) => createOutput(index, utxo.value, utxo.address, utxo.token, utxo.timelock, utxo.locked)); + await addUtxos(mysql, txId2, outputs2, 10); + + // fetch on timestamp=99 and heightlock=10. Should return: + // { value: 35, address: 'address2', token: 'token1', timelock: null}, + let results = await getUtxosLockedAtHeight(mysql, 99, 10); + expect(results).toHaveLength(1); + expect(results[0].value).toBe(35); + + // fetch on timestamp=100 and heightlock=10. Should return: + // { value: 35, address: 'address2', token: 'token1', timelock: null}, + // { value: 45, address: 'address2', token: 'token1', timelock: 100}, + results = await getUtxosLockedAtHeight(mysql, 100, 10); + expect(results).toHaveLength(2); + expect([35, 45]).toContain(results[0].value); + expect([35, 45]).toContain(results[1].value); + + // fetch on timestamp=100 and heightlock=9. Should return empty + results = await getUtxosLockedAtHeight(mysql, 1000, 9); + expect(results).toStrictEqual([]); + + // unlockedHeight < 0. This means the block is still very early after genesis and no blocks have been unlocked + results = await getUtxosLockedAtHeight(mysql, 1000, -2); + expect(results).toStrictEqual([]); +}); + +test('updateAddressLockedBalance', async () => { + expect.hasAssertions(); + + const addr1 = 'address1'; + const addr2 = 'address2'; + const tokenId = 'tokenId'; + const otherToken = 'otherToken'; + const entries = [ + [addr1, tokenId, 50, 20, null, 3, 0, 0b01, 70], + [addr2, tokenId, 0, 5, null, 1, 0, 0, 10], + [addr1, otherToken, 5, 5, null, 1, 0, 0, 10], + ]; + await addToAddressBalanceTable(mysql, entries); + + const addr1Map = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 10, locked: 0, unlockedAuthorities: new Authorities(0b01) } }); + const addr2Map = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 5, locked: 0 } }); + await updateAddressLockedBalance(mysql, { [addr1]: addr1Map, [addr2]: addr2Map }); + await expect(checkAddressBalanceTable(mysql, 3, addr1, tokenId, 60, 10, null, 3, 0b01, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 3, addr2, tokenId, 5, 0, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 3, addr1, otherToken, 5, 5, null, 1)).resolves.toBe(true); + + // now pretend there's another locked authority, so final balance of locked authorities should be updated accordingly + await addToUtxoTable(mysql, [{ + txId: 'txId', + index: 0, + tokenId, + address: addr1, + value: 0, + authorities: 0b01, + timelock: 10000, + heightlock: null, + locked: true, + spentBy: null, + }]); + const newMap = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 0, locked: 0, unlockedAuthorities: new Authorities(0b10) } }); + await updateAddressLockedBalance(mysql, { [addr1]: newMap }); + await expect(checkAddressBalanceTable(mysql, 3, addr1, tokenId, 60, 10, null, 3, 0b11, 0b01)).resolves.toBe(true); +}); + +test('updateWalletLockedBalance', async () => { + expect.hasAssertions(); + + const wallet1 = 'wallet1'; + const wallet2 = 'wallet2'; + const tokenId = 'tokenId'; + const otherToken = 'otherToken'; + const now = 1000; + + const entries = [{ + walletId: wallet1, + tokenId, + unlockedBalance: 10, + lockedBalance: 20, + unlockedAuthorities: 0b01, + lockedAuthorities: 0, + timelockExpires: now, + transactions: 5, + }, { + walletId: wallet2, + tokenId, + unlockedBalance: 0, + lockedBalance: 100, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: now, + transactions: 4, + }, { + walletId: wallet1, + tokenId: otherToken, + unlockedBalance: 1, + lockedBalance: 2, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]; + await addToWalletBalanceTable(mysql, entries); + + const wallet1Map = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 15, locked: 0, unlockedAuthorities: new Authorities(0b11) } }); + const wallet2Map = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 50, locked: 0 } }); + await updateWalletLockedBalance(mysql, { [wallet1]: wallet1Map, [wallet2]: wallet2Map }); + await expect(checkWalletBalanceTable(mysql, 3, wallet1, tokenId, 25, 5, now, 5, 0b11, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, wallet2, tokenId, 50, 50, now, 4)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, wallet1, otherToken, 1, 2, null, 1)).resolves.toBe(true); + + // now pretend there's another locked authority, so final balance of locked authorities should be updated accordingly + await addToAddressTable(mysql, [{ + address: 'address1', + index: 0, + walletId: wallet1, + transactions: 1, + }]); + await addToAddressBalanceTable(mysql, [['address1', tokenId, 0, 0, null, 1, 0, 0b01, 0]]); + const newMap = TokenBalanceMap.fromStringMap({ [tokenId]: { unlocked: 0, locked: 0, unlockedAuthorities: new Authorities(0b10) } }); + await updateWalletLockedBalance(mysql, { [wallet1]: newMap }); + await expect(checkWalletBalanceTable(mysql, 3, wallet1, tokenId, 25, 5, now, 5, 0b11, 0b01)).resolves.toBe(true); +}); + +test('addOrUpdateTx should add weight to a tx', async () => { + expect.hasAssertions(); + + await addOrUpdateTx(mysql, 'txId1', null, 1, 1, 65.4321); + const txs = await getTransactionsById(mysql, ['txId1']); + + expect(txs[0].weight).toStrictEqual(65.4321); +}); + +test('updateTx should add height to a tx', async () => { + expect.hasAssertions(); + + await addOrUpdateTx(mysql, 'txId1', null, 1, 1, 60); + await updateTx(mysql, 'txId1', 5, 1, 1, 60); + + const txs = await getTransactionsById(mysql, ['txId1']); + const tx = txs[0]; + + expect(tx.txId).toStrictEqual('txId1'); + expect(tx.height).toStrictEqual(5); +}); + +test('getLatestBlockByHeight', async () => { + expect.hasAssertions(); + + // It should return null when the database has no blocks + const nullBestBlock: Block = await getLatestBlockByHeight(mysql); + expect(nullBestBlock).toBeNull(); + + await addOrUpdateTx(mysql, 'block0', 0, 0, 0, 60); + await addOrUpdateTx(mysql, 'block1', 1, 0, 0, 60); + await addOrUpdateTx(mysql, 'block2', 2, 0, 0, 60); + await addOrUpdateTx(mysql, 'block3', 3, 0, 0, 60); + await addOrUpdateTx(mysql, 'tx1', 3, 0, 1, 60); // Tx + // Confirmed by a block we don't have, this is an impossible situation, but + // works for the test: + await addOrUpdateTx(mysql, 'tx2', 4, 0, 1, 60); + + const bestBlock: Block = await getLatestBlockByHeight(mysql); + + expect(bestBlock.height).toStrictEqual(3); + expect(bestBlock.txId).toStrictEqual('block3'); +}); + +test('getLatestHeight, getTxsAfterHeight, deleteBlocksAfterHeight and removeTxsHeight', async () => { + expect.hasAssertions(); + + await addOrUpdateTx(mysql, 'txId0', 0, 1, 0, 60); + + expect(await getLatestHeight(mysql)).toBe(0); + + await addOrUpdateTx(mysql, 'txId5', 5, 2, 0, 60); + + expect(await getLatestHeight(mysql)).toBe(5); + + await addOrUpdateTx(mysql, 'txId7', 7, 3, 0, 60); + + expect(await getLatestHeight(mysql)).toBe(7); + + await addOrUpdateTx(mysql, 'txId8', 8, 4, 0, 60); + await addOrUpdateTx(mysql, 'txId9', 9, 5, 0, 60); + await addOrUpdateTx(mysql, 'txId10', 10, 6, 0, 60); + + const txsAfterHeight = await getTxsAfterHeight(mysql, 6); + + expect(txsAfterHeight).toHaveLength(4); + + expect(await getLatestHeight(mysql)).toBe(10); + + await deleteBlocksAfterHeight(mysql, 7); + + expect(await getLatestHeight(mysql)).toBe(7); + + // add the transactions again + await addOrUpdateTx(mysql, 'txId8', 8, 4, 0, 60); + await addOrUpdateTx(mysql, 'txId9', 9, 5, 0, 60); + await addOrUpdateTx(mysql, 'txId10', 10, 6, 0, 60); + + // remove their height + const transactions = await getTransactionsById(mysql, ['txId8', 'txId9', 'txId10']); + await removeTxsHeight(mysql, transactions); + + expect(await getLatestHeight(mysql)).toBe(7); +}); + +test('getLatestHeight with no blocks on database should return 0', async () => { + expect.hasAssertions(); + + expect(await getLatestHeight(mysql)).toBe(0); +}); + +test('getBlockByHeight should return null if a block is not found', async () => { + expect.hasAssertions(); + + expect(await getBlockByHeight(mysql, 100000)).toBeNull(); +}); + +test('storeTokenInformation and getTokenInformation', async () => { + expect.hasAssertions(); + + expect(await getTokenInformation(mysql, 'invalid')).toBeNull(); + + const info = new TokenInfo('tokenId', 'tokenName', 'TKNS'); + storeTokenInformation(mysql, info.id, info.name, info.symbol); + + expect(info).toStrictEqual(await getTokenInformation(mysql, info.id)); +}); + +test('validateTokenTimestamps', async () => { + expect.hasAssertions(); + + const info = new TokenInfo('tokenId', 'tokenName', 'TKNS'); + storeTokenInformation(mysql, info.id, info.name, info.symbol); + let result = await mysql.query('SELECT * FROM `token` WHERE `id` = ?', [info.id]); + + expect(result[0].created_at).toStrictEqual(result[0].updated_at); + + await new Promise((r) => setTimeout(r, 1100)); + await mysql.query('UPDATE `token` SET name = ? WHERE `id` = ?', ['newName', info.id]); + result = await mysql.query('SELECT * FROM `token` WHERE `id` = ?', [info.id]); + + // After updating the entry, the created_at and updated_at must be different + expect(result[0].created_at).not.toStrictEqual(result[0].updated_at); +}); + +test('getWalletSortedValueUtxos', async () => { + expect.hasAssertions(); + + const addr1 = 'addr1'; + const addr2 = 'addr2'; + const walletId = 'walletId'; + const tokenId = 'tokenId'; + const txId = 'txId'; + await addToAddressTable(mysql, [{ + address: addr1, + index: 0, + walletId, + transactions: 1, + }, { + address: addr2, + index: 1, + walletId, + transactions: 1, + }]); + await addToUtxoTable(mysql, [ + // authority utxos should be ignored + { + txId, + index: 0, + tokenId, + address: addr1, + value: 0, + authorities: 0b01, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, + // locked utxos should be ignored + { + txId, + index: 1, + tokenId, + address: addr1, + value: 10, + authorities: 0, + timelock: 10000, + heightlock: null, + locked: true, + spentBy: null, + }, + // another wallet + { + txId, + index: 2, + tokenId, + address: 'otherAddr', + value: 10, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, + // another token + { + txId, + index: 3, + tokenId: 'tokenId2', + address: addr1, + value: 5, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, + // these sould be fetched + { + txId, + index: 4, + tokenId, + address: addr1, + value: 4, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, + { + txId, + index: 5, + tokenId, + address: addr2, + value: 1, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, + { + txId, + index: 6, + tokenId, + address: addr1, + value: 7, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, + ]); + + const utxos = await getWalletSortedValueUtxos(mysql, walletId, tokenId); + expect(utxos).toHaveLength(3); + expect(utxos[0]).toStrictEqual({ + txId, index: 6, tokenId, address: addr1, value: 7, authorities: 0, timelock: null, heightlock: null, locked: false, + }); + expect(utxos[1]).toStrictEqual({ + txId, index: 4, tokenId, address: addr1, value: 4, authorities: 0, timelock: null, heightlock: null, locked: false, + }); + expect(utxos[2]).toStrictEqual({ + txId, index: 5, tokenId, address: addr2, value: 1, authorities: 0, timelock: null, heightlock: null, locked: false, + }); +}); + +test('getUnusedAddresses', async () => { + expect.hasAssertions(); + + const walletId = 'walletId'; + const walletId2 = 'walletId2'; + await addToAddressTable(mysql, [ + { address: 'addr2', index: 1, walletId, transactions: 0 }, + { address: 'addr3', index: 2, walletId, transactions: 2 }, + { address: 'addr1', index: 0, walletId, transactions: 0 }, + { address: 'addr4', index: 0, walletId: walletId2, transactions: 1 }, + { address: 'addr5', index: 1, walletId: walletId2, transactions: 1 }, + ]); + + let addresses = await getUnusedAddresses(mysql, walletId); + expect(addresses).toHaveLength(2); + expect(addresses[0]).toBe('addr1'); + expect(addresses[1]).toBe('addr2'); + + addresses = await getUnusedAddresses(mysql, walletId2); + expect(addresses).toHaveLength(0); +}); + +test('markUtxosWithProposalId and getTxProposalInputs', async () => { + expect.hasAssertions(); + + const txId = 'txId'; + const tokenId = 'tokenId'; + const address = 'address'; + const txProposalId = 'txProposalId'; + + const utxos = [{ + txId, + index: 0, + tokenId, + address, + value: 5, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }, { + txId, + index: 1, + tokenId, + address, + value: 15, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }, { + txId, + index: 2, + tokenId, + address, + value: 25, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }]; + + // add to utxo table + const outputs = utxos.map((utxo, index) => createOutput(index, utxo.value, utxo.address, utxo.tokenId, utxo.timelock, utxo.locked)); + await addUtxos(mysql, txId, outputs); + + // we'll only mark utxos with indexes 0 and 2 + await markUtxosWithProposalId(mysql, txProposalId, utxos.filter((utxo) => utxo.index !== 1)); + let proposalIndex = 0; + utxos.forEach((utxo) => { + utxo.txProposalId = utxo.index !== 1 ? txProposalId : null; // eslint-disable-line no-param-reassign + utxo.txProposalIndex = utxo.index !== 1 ? proposalIndex++ : null; // eslint-disable-line no-param-reassign + }); + + const finalUtxos = await getUtxos(mysql, utxos.map((utxo) => ({ txId, index: utxo.index }))); + expect(utxos).toStrictEqual(finalUtxos); + + // getTxProposalInputs + // utxo with index 1 should not be returned + const inputs = [{ txId, index: 0 }, { txId, index: 2 }]; + expect(await getTxProposalInputs(mysql, txProposalId)).toStrictEqual(inputs); +}); + +test('createTxProposal, updateTxProposal, getTxProposal, countUnsentTxProposals, releaseTxProposalUtxos', async () => { + expect.hasAssertions(); + + const now = getUnixTimestamp(); + const txProposalId = uuidv4(); + const walletId = 'walletId'; + + await createTxProposal(mysql, txProposalId, walletId, now); + let txProposal = await getTxProposal(mysql, txProposalId); + expect(txProposal).toStrictEqual({ id: txProposalId, walletId, status: TxProposalStatus.OPEN, createdAt: now, updatedAt: null }); + + // update + await updateTxProposal(mysql, [txProposalId], now + 7, TxProposalStatus.SENT); + txProposal = await getTxProposal(mysql, txProposalId); + expect(txProposal).toStrictEqual({ id: txProposalId, walletId, status: TxProposalStatus.SENT, createdAt: now, updatedAt: now + 7 }); + + // tx proposal not found + expect(await getTxProposal(mysql, 'aaa')).toBeNull(); + + const txProposalId1: string = uuidv4() as string; + const txProposalId2: string = uuidv4() as string; + const txProposalId3: string = uuidv4() as string; + const txProposalId4: string = uuidv4() as string; + + // Create old tx proposals + await createTxProposal(mysql, txProposalId1, walletId, 1); + await createTxProposal(mysql, txProposalId2, walletId, 1); + await createTxProposal(mysql, txProposalId3, walletId, 1); + + // Create a new tx proposal, that won't be removed + await createTxProposal(mysql, txProposalId4, walletId, now); + + const txProposalsBefore = now - (5 * 60); // 5 minutes in seconds + + // Fetch the list of unsent tx proposals + const unsentTxProposals = await getUnsentTxProposals(mysql, txProposalsBefore); + expect(unsentTxProposals).toContain(txProposalId1); + expect(unsentTxProposals).toContain(txProposalId2); + expect(unsentTxProposals).toContain(txProposalId3); + + // The new tx proposal should not be in the unsent list + expect(unsentTxProposals).not.toContain(txProposalId4); + + // Add utxos for the unsent tx proposals so we can check if they got cleaned up + await addToUtxoTable(mysql, [{ + txId: 'tx1', + index: 0, + tokenId: '00', + address: 'address1', + value: 5, + authorities: 0, + timelock: 0, + heightlock: 0, + locked: false, + spentBy: null, + txProposalId: txProposalId1, + txProposalIndex: 0, + }, { + txId: 'tx2', + index: 0, + tokenId: '00', + address: 'address1', + value: 5, + authorities: 0, + timelock: 0, + heightlock: 0, + locked: false, + spentBy: null, + txProposalId: txProposalId2, + txProposalIndex: 0, + }, { + txId: 'tx3', + index: 0, + tokenId: '00', + address: 'address1', + value: 5, + authorities: 0, + timelock: 0, + heightlock: 0, + locked: false, + spentBy: null, + txProposalId: txProposalId3, + txProposalIndex: 0, + }]); + + // Release txProposalUtxos should properly release the utxos. This method will throw an error if the + // updated count is different from the sent tx proposals count. + await releaseTxProposalUtxos(mysql, [txProposalId1, txProposalId2, txProposalId3]); + await expect(releaseTxProposalUtxos(mysql, ['invalid-tx-proposal'])).rejects.toMatchInlineSnapshot('[AssertionError: Not all utxos were correctly updated]'); +}); + +test('updateVersionData', async () => { + expect.hasAssertions(); + + const mockData: FullNodeVersionData = { + timestamp: 1614875031449, + version: '0.38.0', + network: 'mainnet', + minWeight: 14, + minTxWeight: 14, + minTxWeightCoefficient: 1.6, + minTxWeightK: 100, + tokenDepositPercentage: 0.01, + rewardSpendMinBlocks: 300, + maxNumberInputs: 255, + maxNumberOutputs: 255, + }; + + const mockData2: FullNodeVersionData = { + ...mockData, + version: '0.39.1', + }; + + const mockData3: FullNodeVersionData = { + ...mockData, + version: '0.39.2', + }; + + await updateVersionData(mysql, mockData); + await updateVersionData(mysql, mockData2); + await updateVersionData(mysql, mockData3); + + await expect( + checkVersionDataTable(mysql, mockData3), + ).resolves.toBe(true); +}); + +test('getVersionData', async () => { + expect.hasAssertions(); + + const mockData: FullNodeVersionData = { + timestamp: 1614875031449, + version: '0.38.0', + network: 'mainnet', + minWeight: 14, + minTxWeight: 14, + minTxWeightCoefficient: 1.6, + minTxWeightK: 100, + tokenDepositPercentage: 0.01, + rewardSpendMinBlocks: 300, + maxNumberInputs: 255, + maxNumberOutputs: 255, + }; + + await updateVersionData(mysql, mockData); + + const versionData: FullNodeVersionData = await getVersionData(mysql); + + expect(Object.entries(versionData).toString()).toStrictEqual(Object.entries(mockData).toString()); +}); + +test('fetchAddressTxHistorySum', async () => { + expect.hasAssertions(); + + const addr1 = 'addr1'; + const addr2 = 'addr2'; + const token1 = 'token1'; + const token2 = 'token2'; + const txId1 = 'txId1'; + const txId2 = 'txId2'; + const txId3 = 'txId3'; + const timestamp1 = 10; + const timestamp2 = 20; + const entries = [ + { address: addr1, txId: txId1, tokenId: token1, balance: 10, timestamp: timestamp1 }, + { address: addr1, txId: txId2, tokenId: token1, balance: 20, timestamp: timestamp2 }, + { address: addr1, txId: txId3, tokenId: token1, balance: 30, timestamp: timestamp2 }, + // total: 60 + { address: addr2, txId: txId1, tokenId: token2, balance: 20, timestamp: timestamp1 }, + { address: addr2, txId: txId2, tokenId: token2, balance: 20, timestamp: timestamp2 }, + { address: addr2, txId: txId3, tokenId: token2, balance: 10, timestamp: timestamp2 }, + // total: 50 + ]; + + await addToAddressTxHistoryTable(mysql, entries); + + const history = await fetchAddressTxHistorySum(mysql, [addr1, addr2]); + + expect(history[0].balance).toStrictEqual(60); + expect(history[1].balance).toStrictEqual(50); +}); + +test('fetchAddressBalance', async () => { + expect.hasAssertions(); + + const addr1 = 'addr1'; + const addr2 = 'addr2'; + const addr3 = 'addr3'; + const token1 = 'token1'; + const token2 = 'token2'; + const timelock = 500; + + const addressEntries = [ + // address, tokenId, unlocked, locked, lockExpires, transactions + [addr1, token1, 2, 0, null, 2, 0, 0, 4], + [addr1, token2, 1, 4, timelock, 1, 0, 0, 5], + [addr2, token1, 5, 2, null, 2, 0, 0, 10], + [addr2, token2, 0, 2, null, 1, 0, 0, 2], + [addr3, token1, 0, 1, null, 1, 0, 0, 1], + [addr3, token2, 10, 1, null, 1, 0, 0, 11], + ]; + + await addToAddressBalanceTable(mysql, addressEntries); + + const addressBalances = await fetchAddressBalance(mysql, [addr1, addr2, addr3]); + + expect(addressBalances[0].address).toStrictEqual('addr1'); + expect(addressBalances[0].tokenId).toStrictEqual('token1'); + expect(addressBalances[0].unlockedBalance).toStrictEqual(2); + expect(addressBalances[0].lockedBalance).toStrictEqual(0); + expect(addressBalances[1].address).toStrictEqual('addr1'); + expect(addressBalances[1].tokenId).toStrictEqual('token2'); + expect(addressBalances[1].unlockedBalance).toStrictEqual(1); + expect(addressBalances[1].lockedBalance).toStrictEqual(4); + + expect(addressBalances[2].address).toStrictEqual('addr2'); + expect(addressBalances[2].tokenId).toStrictEqual('token1'); + expect(addressBalances[2].unlockedBalance).toStrictEqual(5); + expect(addressBalances[2].lockedBalance).toStrictEqual(2); + expect(addressBalances[3].address).toStrictEqual('addr2'); + expect(addressBalances[3].tokenId).toStrictEqual('token2'); + expect(addressBalances[3].unlockedBalance).toStrictEqual(0); + expect(addressBalances[3].lockedBalance).toStrictEqual(2); + + expect(addressBalances[4].address).toStrictEqual('addr3'); + expect(addressBalances[4].tokenId).toStrictEqual('token1'); + expect(addressBalances[4].unlockedBalance).toStrictEqual(0); + expect(addressBalances[4].lockedBalance).toStrictEqual(1); + expect(addressBalances[5].address).toStrictEqual('addr3'); + expect(addressBalances[5].tokenId).toStrictEqual('token2'); + expect(addressBalances[5].unlockedBalance).toStrictEqual(10); + expect(addressBalances[5].lockedBalance).toStrictEqual(1); +}); + +test('addTx, fetchTx, getTransactionsById and markTxsAsVoided', async () => { + expect.hasAssertions(); + + const txId1 = 'txId1'; + const txId2 = 'txId2'; + const txId3 = 'txId3'; + const txId4 = 'txId4'; + const txId5 = 'txId5'; + const timestamp = 10; + + const tx1: Tx = { + txId: txId1, + height: 15, + timestamp, + version: 0, + voided: false, + weight: 60, + }; + + await addOrUpdateTx(mysql, tx1.txId, tx1.height, tx1.timestamp, tx1.version, tx1.weight); + + expect(await fetchTx(mysql, txId1)).toStrictEqual(tx1); + + const tx2 = { ...tx1, txId: txId2 }; + await addOrUpdateTx(mysql, tx2.txId, tx2.height, tx2.timestamp, tx2.version, tx2.weight); + + const tx3 = { ...tx1, txId: txId3 }; + await addOrUpdateTx(mysql, tx3.txId, tx3.height, tx3.timestamp, tx3.version, tx3.weight); + + const tx4 = { ...tx1, txId: txId4 }; + await addOrUpdateTx(mysql, tx4.txId, tx4.height, tx4.timestamp, tx4.version, tx4.weight); + + const tx5 = { ...tx1, txId: txId5 }; + await addOrUpdateTx(mysql, tx5.txId, tx5.height, tx5.timestamp, tx5.version, tx5.weight); + + const transactions = await getTransactionsById(mysql, [txId1, txId2, txId3, txId4, txId5]); + + expect(transactions).toHaveLength(5); + + await markTxsAsVoided(mysql, [tx1, tx2, tx3, tx4, tx5]); + + expect(await fetchTx(mysql, txId1)).toBeNull(); + expect(await fetchTx(mysql, txId2)).toBeNull(); + expect(await fetchTx(mysql, txId3)).toBeNull(); + expect(await fetchTx(mysql, txId4)).toBeNull(); + expect(await fetchTx(mysql, txId5)).toBeNull(); +}); + +test('checkTxWasVoided', async () => { + expect.hasAssertions(); + + const tx1 = 'tx1'; + const tx2 = 'tx2'; + const address1 = 'address1'; + const address2 = 'address2'; + + await addToAddressTxHistoryTable(mysql, [{ + address: address1, + txId: tx1, + tokenId: '00', + balance: 0, + timestamp: 1, + voided: true, + }, { + address: address2, + txId: tx2, + tokenId: '00', + balance: 0, + timestamp: 1, + voided: false, + }]); + + expect(await checkTxWasVoided(mysql, tx1)).toStrictEqual(true); + expect(await checkTxWasVoided(mysql, tx2)).toStrictEqual(false); +}); + +test('cleanupVoidedTx', async () => { + expect.hasAssertions(); + const txId = 'txId1'; + const txId2 = 'txId2'; + const addr1 = 'addr1'; + const walletId = 'walletid'; + const tokenId = '00'; + + await addToUtxoTable(mysql, [{ + txId, + index: 0, + tokenId, + address: addr1, + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + voided: true, + }]); + + await addToAddressTxHistoryTable(mysql, [{ + address: addr1, + txId, + tokenId, + balance: 0, + timestamp: 1, + voided: true, + }]); + + await addToWalletTxHistoryTable(mysql, [ + [walletId, txId, tokenId, 0, 0, true], + ]); + + await cleanupVoidedTx(mysql, txId); + + expect(await getTxOutput(mysql, txId, 0, false)).toBeNull(); + expect(await getWalletTxHistory(mysql, walletId, tokenId, 0, 10)).toHaveLength(0); + expect(await checkAddressTxHistoryTable( + mysql, + 0, + addr1, + txId, + tokenId, + 0, + 1, + )).toStrictEqual(true); + + // It shouldn't do anything on non-voided transactions + + await addToTransactionTable(mysql, [ + [txId2, 0, 1, false, 0, 0], + ]); + + const utxo2 = { + txId: txId2, + index: 0, + tokenId, + address: addr1, + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }; + + await addToUtxoTable(mysql, [utxo2]); + + await addToAddressTxHistoryTable(mysql, [{ + txId: txId2, + timestamp: 1, + address: addr1, + tokenId, + balance: 0, + voided: false, + }]); + + await addToWalletTxHistoryTable(mysql, [ + [walletId, txId2, tokenId, 0, 0, false], + ]); + + await cleanupVoidedTx(mysql, txId2); + + expect(await getWalletTxHistory(mysql, walletId, tokenId, 0, 10)).toHaveLength(1); + expect(await getTxOutput(mysql, txId2, 0, false)).toStrictEqual(utxo2); + expect(await checkAddressTxHistoryTable( + mysql, + 1, + addr1, + txId2, + tokenId, + 0, + 1, + )).toStrictEqual(true); +}); + +test('rebuildAddressBalancesFromUtxos', async () => { + expect.hasAssertions(); + + const addr1 = 'address1'; + const addr2 = 'address2'; + const txId = 'tx1'; + const txId2 = 'tx2'; + const txId3 = 'tx3'; + const txId4 = 'tx4'; + const token1 = 'token1'; + const token2 = 'token2'; + const timestamp1 = 10; + + const utxosTx1 = [ + { value: 5, address: addr1, token: token1, locked: false, spentBy: null }, + { value: 15, address: addr1, token: token1, locked: false, spentBy: null }, + { value: 75, address: addr2, token: token1, heightlock: 70, locked: true, spentBy: null }, + { value: 150, address: addr2, token: token1, heightlock: 70, locked: true, spentBy: null }, + { value: 35, address: addr2, token: token1, locked: false, spentBy: null }, + + { value: 25, address: addr2, token: token2, timelock: 500, locked: true, spentBy: null }, + + // authority utxo + { value: 0b11, address: addr1, token: token1, locked: false, tokenData: 129, spentBy: null }, + ]; + + const utxosTx2 = [ + // spent utxos + { value: 80, address: addr2, token: token1, heightlock: 70, locked: false, spentBy: null }, + { value: 90, address: addr2, token: token1, heightlock: 70, locked: false, spentBy: null }, + ]; + + const utxosTx3 = [ + // spent utxos + { value: 5, address: addr2, token: token1, heightlock: 70, locked: false, spentBy: null }, + { value: 10, address: addr2, token: token1, heightlock: 70, locked: false, spentBy: null }, + ]; + + const utxosTx4 = [ + // spent utxos + { value: 20, address: addr1, token: token1, heightlock: 70, locked: false, spentBy: null }, + { value: 1, address: addr1, token: token1, heightlock: 70, locked: false, spentBy: null }, + ]; + + const mapUtxoListToOutput = (utxoList: any[]) => utxoList.map((utxo, index) => createOutput( + index, + utxo.value, + utxo.address, + utxo.token, + utxo.timelock || null, + utxo.locked, + utxo.tokenData || 0, + utxo.spentBy, + )); + + await addUtxos(mysql, txId, mapUtxoListToOutput(utxosTx1)); + await addUtxos(mysql, txId2, mapUtxoListToOutput(utxosTx2)); + await addUtxos(mysql, txId3, mapUtxoListToOutput(utxosTx3)); + await addUtxos(mysql, txId4, mapUtxoListToOutput(utxosTx4)); + + // We need to have a address_balance row before rebuilding as rebuildAddressBalancesFromUtxos will + // subtract the number of affected transactions from it. + // Since the actual balances are rebuilt from the utxos and we are only modifying the transactions count, + // we can safely set all balances and authorities to 0. + + const addressEntries = [ + // address, tokenId, unlocked, locked, lockExpires, transactions, unlocked_authorities, locked_authorities + [addr1, token1, 0, 0, null, 2, 0, 0, 0], + [addr2, token1, 0, 0, null, 3, 0, 0, 0], + [addr2, token2, 0, 0, null, 1, 0, 0, 0], + ]; + + await addToAddressBalanceTable(mysql, addressEntries); + + const txHistory = [ + { address: addr1, txId, tokenId: token1, balance: 20, timestamp: timestamp1 }, + { address: addr1, txId: txId4, tokenId: token1, balance: 21, timestamp: timestamp1, voided: true }, + + { address: addr2, txId, tokenId: token1, balance: 260, timestamp: timestamp1 }, + { address: addr2, txId, tokenId: token2, balance: 25, timestamp: timestamp1 }, + { address: addr2, txId: txId2, tokenId: token1, balance: 80, timestamp: timestamp1 }, + { address: addr2, txId: txId3, tokenId: token1, balance: 15, timestamp: timestamp1, voided: true }, + ]; + + await addToAddressTxHistoryTable(mysql, txHistory); + + // add to the token table + await addToTokenTable(mysql, [ + { id: token1, name: 'token1', symbol: 'TKN1', transactions: 2 }, + ]); + + await expect(checkTokenTable(mysql, 1, [{ + tokenId: token1, + tokenSymbol: 'TKN1', + tokenName: 'token1', + transactions: 2, + }])).resolves.toBe(true); + + // We are only using the txList parameter on `transactions` recalculation, so our balance + // checks should include txId3 and txId4, but the transaction count should not. + await rebuildAddressBalancesFromUtxos(mysql, [addr1, addr2], [txId3, txId4]); + + const addressBalances = await fetchAddressBalance(mysql, [addr1, addr2]); + + expect(addressBalances[0].unlockedBalance).toStrictEqual(41); + expect(addressBalances[0].unlockedAuthorities).toStrictEqual(0b11); + expect(addressBalances[0].address).toStrictEqual(addr1); + expect(addressBalances[0].transactions).toStrictEqual(1); + expect(addressBalances[0].tokenId).toStrictEqual('token1'); + + expect(addressBalances[1].unlockedBalance).toStrictEqual(220); + expect(addressBalances[1].lockedBalance).toStrictEqual(225); + expect(addressBalances[1].address).toStrictEqual(addr2); + expect(addressBalances[1].transactions).toStrictEqual(2); + expect(addressBalances[1].tokenId).toStrictEqual('token1'); + + expect(addressBalances[2].lockedBalance).toStrictEqual(25); + expect(addressBalances[2].address).toStrictEqual(addr2); + expect(addressBalances[2].transactions).toStrictEqual(1); + expect(addressBalances[2].tokenId).toStrictEqual('token2'); + + await expect(checkTokenTable(mysql, 1, [{ + tokenId: token1, + tokenSymbol: 'TKN1', + tokenName: 'token1', + transactions: 0, + }])).resolves.toBe(true); +}); + +test('markAddressTxHistoryAsVoided', async () => { + expect.hasAssertions(); + + const addr1 = 'address1'; + const addr2 = 'address2'; + const txId1 = 'tx1'; + const txId2 = 'tx2'; + const txId3 = 'tx3'; + const token1 = 'token1'; + const token2 = 'token2'; + const timestamp1 = 10; + const timestamp2 = 20; + + const entries = [ + { address: addr1, txId: txId1, tokenId: token1, balance: 10, timestamp: timestamp1 }, + { address: addr1, txId: txId2, tokenId: token1, balance: 20, timestamp: timestamp2 }, + { address: addr1, txId: txId3, tokenId: token1, balance: 30, timestamp: timestamp2 }, + // total: 60 + { address: addr2, txId: txId1, tokenId: token2, balance: 20, timestamp: timestamp1 }, + { address: addr2, txId: txId2, tokenId: token2, balance: 20, timestamp: timestamp2 }, + { address: addr2, txId: txId3, tokenId: token2, balance: 10, timestamp: timestamp2 }, + // total: 50 + ]; + + await addToAddressTxHistoryTable(mysql, entries); + + const history = await fetchAddressTxHistorySum(mysql, [addr1, addr2]); + + expect(history).toHaveLength(2); + + await markAddressTxHistoryAsVoided(mysql, [{ + txId: txId1, + timestamp: timestamp1, + version: 0, + voided: false, + weight: 60, + }, { + txId: txId2, + timestamp: timestamp1, + version: 0, + voided: false, + weight: 60, + }, { + txId: txId3, + timestamp: timestamp1, + version: 0, + voided: false, + weight: 60, + }]); + + const history2 = await fetchAddressTxHistorySum(mysql, [addr1, addr2]); + + expect(history2).toHaveLength(0); +}); + +test('filterTxOutputs', async () => { + expect.hasAssertions(); + + const addr1 = 'addr1'; + const addr2 = 'addr2'; + const walletId = 'walletId'; + const tokenId = 'tokenId'; + const txId = 'txId'; + const txId2 = 'txId2'; + const txId3 = 'txId3'; + + await addToAddressTable(mysql, [{ + address: addr1, + index: 0, + walletId, + transactions: 1, + }, { + address: addr2, + index: 1, + walletId, + transactions: 1, + }]); + + await addToUtxoTable(mysql, [{ + txId: txId3, + index: 0, + tokenId: '00', + address: addr1, + value: 6000, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId, + index: 0, + tokenId, + address: addr1, + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: txId2, + index: 0, + tokenId, + address: addr1, + value: 500, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: txId2, + index: 1, + tokenId, + address: addr1, + value: 1000, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + // locked utxo: + txId: txId2, + index: 2, + tokenId, + address: addr2, + value: 1500, + authorities: 0, + timelock: null, + heightlock: null, + locked: true, + spentBy: null, + }, { + // authority utxo: + txId: txId2, + index: 3, + tokenId, + address: addr2, + value: 0, + authorities: 0b01, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + // another authority utxo: + txId: txId2, + index: 4, + tokenId, + address: addr2, + value: 0, + authorities: 0b01, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]); + + // filter all hathor utxos from addr1 and addr2 + let utxos = await filterTxOutputs(mysql, { addresses: [addr1, addr2] }); + expect(utxos).toHaveLength(1); + + // filter all 'tokenId' utxos from addr1 and addr2 + utxos = await filterTxOutputs(mysql, { addresses: [addr1, addr2], tokenId }); + expect(utxos).toHaveLength(4); + + // filter all 'tokenId' utxos from addr1 and addr2 that are not locked + utxos = await filterTxOutputs(mysql, { addresses: [addr1, addr2], tokenId, ignoreLocked: true }); + expect(utxos).toHaveLength(3); + + // filter all authority utxos from addr1 and addr2 + utxos = await filterTxOutputs(mysql, { addresses: [addr1, addr2], tokenId, authority: 0b01 }); + expect(utxos).toHaveLength(2); + + // filter all utxos between 100 and 1500 + utxos = await filterTxOutputs(mysql, { addresses: [addr1, addr2], tokenId, biggerThan: 100, smallerThan: 1500 }); + expect(utxos).toHaveLength(2); + expect(utxos[0]).toStrictEqual({ + txId: txId2, + index: 1, + tokenId, + address: addr1, + value: 1000, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }); + expect(utxos[1]).toStrictEqual({ + txId: txId2, + index: 0, + tokenId, + address: addr1, + value: 500, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }); + + // limit to 2 utxos, should return the largest 2 ordered by value + utxos = await filterTxOutputs(mysql, { addresses: [addr1, addr2], tokenId, maxOutputs: 2 }); + expect(utxos).toHaveLength(2); + expect(utxos[0]).toStrictEqual({ + txId: txId2, + index: 2, + tokenId, + address: addr2, + value: 1500, + authorities: 0, + timelock: null, + heightlock: null, + locked: true, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }); + expect(utxos[1]).toStrictEqual({ + txId: txId2, + index: 1, + tokenId, + address: addr1, + value: 1000, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }); + + // authorities != 0 and maxOutputs == 1 should return only one authority utxo + utxos = await filterTxOutputs(mysql, { addresses: [addr1, addr2], biggerThan: 0, smallerThan: 3, authority: 1, tokenId, maxOutputs: 1 }); + + expect(utxos).toHaveLength(1); +}); + +test('filterTxOutputs should throw if addresses are empty', async () => { + expect.hasAssertions(); + + await expect(filterTxOutputs(mysql, { addresses: [] })).rejects.toThrow('Addresses can\'t be empty.'); +}); + +test('beginTransaction, commitTransaction, rollbackTransaction', async () => { + expect.hasAssertions(); + + const addr1 = 'addr1'; + const addr2 = 'addr2'; + const tokenId = 'tokenId'; + const txId = 'txId'; + + await beginTransaction(mysql); + + await addToUtxoTable(mysql, [{ + txId, + index: 0, + tokenId, + address: addr1, + value: 0, + authorities: 0b01, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId, + index: 1, + tokenId, + address: addr1, + value: 10, + authorities: 0, + timelock: 10000, + heightlock: null, + locked: true, + spentBy: null, + }, { + txId, + index: 2, + tokenId, + address: 'otherAddr', + value: 10, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]); + + await commitTransaction(mysql); + + await expect(checkUtxoTable(mysql, 3, txId, 0, tokenId, addr1, 0, 0b01, null, null, false)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 3, txId, 1, tokenId, addr1, 10, 0, 10000, null, true)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 3, txId, 2, tokenId, 'otherAddr', 10, 0, null, null, false)).resolves.toBe(true); + + await beginTransaction(mysql); + + await addToUtxoTable(mysql, [{ + txId, + index: 3, + tokenId: 'tokenId2', + address: addr1, + value: 5, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId, + index: 4, + tokenId, + address: addr1, + value: 4, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId, + index: 5, + tokenId, + address: addr2, + value: 1, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId, + index: 6, + tokenId, + address: addr1, + value: 7, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]); + + await rollbackTransaction(mysql); + + // check if the database still has 3 elements only + await expect(checkUtxoTable(mysql, 3, txId, 2, tokenId, 'otherAddr', 10, 0, null, null, false)).resolves.toBe(true); +}); + +test('getMinersList', async () => { + expect.hasAssertions(); + + await addMiner(mysql, 'address1', 'txId1'); + await addMiner(mysql, 'address2', 'txId2'); + await addMiner(mysql, 'address3', 'txId3'); + + let results = await getMinersList(mysql); + + expect(results).toHaveLength(3); + expect(new Set(results)).toStrictEqual(new Set([ + { address: 'address1', firstBlock: 'txId1', lastBlock: 'txId1', count: 1 }, + { address: 'address2', firstBlock: 'txId2', lastBlock: 'txId2', count: 1 }, + { address: 'address3', firstBlock: 'txId3', lastBlock: 'txId3', count: 1 }, + ])); + + await addMiner(mysql, 'address3', 'txId4'); + await addMiner(mysql, 'address3', 'txId5'); + + results = await getMinersList(mysql); + + expect(results).toHaveLength(3); + + expect(new Set(results)).toStrictEqual(new Set([ + { address: 'address1', firstBlock: 'txId1', lastBlock: 'txId1', count: 1 }, + { address: 'address2', firstBlock: 'txId2', lastBlock: 'txId2', count: 1 }, + { address: 'address3', firstBlock: 'txId3', lastBlock: 'txId5', count: 3 }, + ])); +}); + +test('getTotalSupply', async () => { + expect.hasAssertions(); + + const txId = 'txId'; + const utxos = [ + { value: 500, address: 'HDeadDeadDeadDeadDeadDeadDeagTPgmn', tokenId: '00', locked: false }, + { value: 5, address: 'address1', tokenId: '00', locked: false }, + { value: 15, address: 'address1', tokenId: '00', locked: false }, + { value: 25, address: 'address2', tokenId: 'token2', timelock: 500, locked: true }, + { value: 35, address: 'address2', tokenId: 'token1', locked: false }, + // authority utxo + { value: 0b11, address: 'address1', tokenId: 'token1', locked: false, tokenData: 129 }, + ]; + + // add to utxo table + const outputs = utxos.map((utxo, index) => createOutput( + index, + utxo.value, + utxo.address, + utxo.tokenId, + utxo.timelock || null, + utxo.locked, + utxo.tokenData || 0, + )); + + await addUtxos(mysql, txId, outputs); + + expect(await getTotalSupply(mysql, '00')).toStrictEqual(20); + expect(await getTotalSupply(mysql, 'token2')).toStrictEqual(25); + expect(await getTotalSupply(mysql, 'token1')).toStrictEqual(35); + + const mysqlQuerySpy = jest.spyOn(mysql, 'query'); + mysqlQuerySpy.mockImplementationOnce(() => Promise.resolve({ length: null })); + + await expect(getTotalSupply(mysql, 'undefined-token')).rejects.toThrow('Total supply query returned no results'); + expect(mockedAddAlert).toHaveBeenCalledWith( + 'Total supply query returned no results', + '-', + Severity.MINOR, + { tokenId: 'undefined-token' }, + ); +}); + +test('getExpiredTimelocksUtxos', async () => { + expect.hasAssertions(); + + const txId = 'txId'; + const utxos = [ + { value: 5, address: 'address1', tokenId: 'token1', locked: true }, + { value: 15, address: 'address1', tokenId: 'token1', locked: true }, + { value: 25, address: 'address2', tokenId: 'token2', timelock: 100, locked: true }, + { value: 35, address: 'address2', tokenId: 'token1', timelock: 200, locked: true }, + // authority utxo + { value: 0b11, address: 'address1', tokenId: 'token1', timelock: 300, locked: true, tokenData: 129 }, + ]; + + // empty list should be fine + await addUtxos(mysql, txId, []); + + // add to utxo table + const outputs = utxos.map((utxo, index) => createOutput( + index, + utxo.value, + utxo.address, + utxo.tokenId, + utxo.timelock || null, + utxo.locked, + utxo.tokenData || 0, + )); + + await addUtxos(mysql, txId, outputs); + + const unlockedUtxos0: DbTxOutput[] = await getExpiredTimelocksUtxos(mysql, 100); + const unlockedUtxos1: DbTxOutput[] = await getExpiredTimelocksUtxos(mysql, 101); + const unlockedUtxos2: DbTxOutput[] = await getExpiredTimelocksUtxos(mysql, 201); + const unlockedUtxos3: DbTxOutput[] = await getExpiredTimelocksUtxos(mysql, 301); + + expect(unlockedUtxos0).toHaveLength(0); + expect(unlockedUtxos1).toHaveLength(1); + expect(unlockedUtxos1[0].value).toStrictEqual(outputs[2].value); + expect(unlockedUtxos2).toHaveLength(2); + expect(unlockedUtxos2[1].value).toStrictEqual(outputs[3].value); + expect(unlockedUtxos3).toHaveLength(3); + // last one is an authority utxo + expect(unlockedUtxos3[2].authorities).toStrictEqual(outputs[4].value); +}); + +test('getTotalTransactions', async () => { + expect.hasAssertions(); + + await addToAddressTxHistoryTable(mysql, [ + { address: 'address1', txId: 'txId1', tokenId: 'token1', balance: -5, timestamp: 1000 }, + { address: 'address1', txId: 'txId2', tokenId: 'token1', balance: 5, timestamp: 1000 }, + { address: 'address1', txId: 'txId3', tokenId: 'token1', balance: 10, timestamp: 1000 }, + { address: 'address2', txId: 'txId4', tokenId: 'token2', balance: -5, timestamp: 1000 }, + { address: 'address2', txId: 'txId5', tokenId: 'token2', balance: 50, timestamp: 1000 }, + ]); + + expect(await getTotalTransactions(mysql, 'token1')).toStrictEqual(3); + expect(await getTotalTransactions(mysql, 'token2')).toStrictEqual(2); + + const mysqlQuerySpy = jest.spyOn(mysql, 'query'); + mysqlQuerySpy.mockImplementationOnce(() => Promise.resolve({ length: null })); + + await expect(getTotalTransactions(mysql, 'undefined-token')).rejects.toThrow('Total transactions query returned no results'); + expect(mockedAddAlert).toHaveBeenCalledWith( + 'Total transactions query returned no results', + '-', + Severity.MINOR, + { tokenId: 'undefined-token' }, + ); +}); + +test('getAvailableAuthorities', async () => { + expect.hasAssertions(); + + const addr1 = 'addr1'; + const addr2 = 'addr1'; + const tokenId = 'token1'; + const tokenId2 = 'token2'; + + await addToUtxoTable(mysql, [{ + txId: 'txId', + index: 0, + tokenId, + address: addr1, + value: 0, + authorities: 0b01, + timelock: null, + heightlock: null, + locked: false, + spentBy: 'tx1', + }, { + txId: 'txId', + index: 1, + tokenId, + address: addr1, + value: 0, + authorities: 0b11, + timelock: 1000, + heightlock: null, + locked: true, + spentBy: null, + }, { + txId: 'txId', + index: 2, + tokenId, + address: addr1, + value: 0, + authorities: 0b10, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: 'txId', + index: 3, + tokenId: tokenId2, + address: addr2, + value: 0, + authorities: 0b01, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]); + + expect(await getAvailableAuthorities(mysql, 'token1')).toHaveLength(1); + expect(await getAvailableAuthorities(mysql, 'token2')).toHaveLength(1); +}); + +test('getUtxo, getAuthorityUtxo', async () => { + expect.hasAssertions(); + + const tokenId = 'tokenId'; + const addr1 = 'addr1'; + + await addToUtxoTable(mysql, [{ + txId: 'txId', + index: 0, + tokenId, + address: addr1, + value: 0, + authorities: constants.TOKEN_MINT_MASK, + timelock: 10000, + heightlock: null, + locked: true, + spentBy: null, + }]); + await addToUtxoTable(mysql, [{ + txId: 'txId', + index: 1, + tokenId, + address: addr1, + value: 0, + authorities: constants.TOKEN_MELT_MASK, + timelock: 10000, + heightlock: null, + locked: true, + spentBy: null, + }]); + + const utxo = await getTxOutput(mysql, 'txId', 0, true); + expect(utxo).toStrictEqual({ + txId: 'txId', + index: 0, + tokenId, + address: addr1, + value: 0, + authorities: constants.TOKEN_MINT_MASK, + timelock: 10000, + heightlock: null, + locked: true, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }); + + const mintUtxo = await getAuthorityUtxo(mysql, tokenId, constants.TOKEN_MINT_MASK); + const meltUtxo = await getAuthorityUtxo(mysql, tokenId, constants.TOKEN_MELT_MASK); + + expect(mintUtxo).toStrictEqual({ + txId: 'txId', + index: 0, + tokenId, + address: addr1, + value: 0, + authorities: constants.TOKEN_MINT_MASK, + timelock: 10000, + heightlock: null, + locked: true, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }); + expect(meltUtxo).toStrictEqual({ + txId: 'txId', + index: 1, + tokenId, + address: addr1, + value: 0, + authorities: constants.TOKEN_MELT_MASK, + timelock: 10000, + heightlock: null, + locked: true, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }); +}); + +test('getAffectedAddressTxCountFromTxList', async () => { + expect.hasAssertions(); + + const addr1 = 'addr1'; + const addr2 = 'addr2'; + const addr3 = 'addr3'; + const token1 = 'token1'; + const token2 = 'token2'; + const token3 = 'token3'; + const txId1 = 'txId1'; + const txId2 = 'txId2'; + const txId3 = 'txId3'; + const timestamp1 = 10; + const timestamp2 = 20; + + const entries: AddressTxHistoryTableEntry[] = [ + { address: addr1, txId: txId1, tokenId: token1, balance: 10, timestamp: timestamp1, voided: true }, + { address: addr1, txId: txId1, tokenId: token2, balance: 7, timestamp: timestamp1, voided: true }, + { address: addr2, txId: txId1, tokenId: token2, balance: 5, timestamp: timestamp1, voided: true }, + { address: addr3, txId: txId1, tokenId: token1, balance: 3, timestamp: timestamp1, voided: true }, + { address: addr1, txId: txId2, tokenId: token1, balance: -1, timestamp: timestamp2, voided: false }, + { address: addr1, txId: txId2, tokenId: token3, balance: 3, timestamp: timestamp2, voided: false }, + { address: addr2, txId: txId3, tokenId: token2, balance: -5, timestamp: timestamp2, voided: true }, + { address: addr3, txId: txId3, tokenId: token1, balance: 3, timestamp: timestamp2, voided: true }, + ]; + + await addToAddressTxHistoryTable(mysql, entries); + + expect(await getAffectedAddressTxCountFromTxList(mysql, [txId1, txId3])).toStrictEqual({ + [`${addr1}_${token1}`]: 1, + [`${addr1}_${token2}`]: 1, + [`${addr2}_${token2}`]: 2, + [`${addr3}_${token1}`]: 2, + }); + + // txId2 is not voided, so we should not count them on the address transaction count: + expect(await getAffectedAddressTxCountFromTxList(mysql, [txId1, txId2, txId3])).toStrictEqual({ + [`${addr1}_${token1}`]: 1, + [`${addr1}_${token2}`]: 1, + [`${addr2}_${token2}`]: 2, + [`${addr3}_${token1}`]: 2, + }); + + // We should get an empty object if no addresses have been affected: + expect(await getAffectedAddressTxCountFromTxList(mysql, [txId2])).toStrictEqual({}); +}); + +test('incrementTokensTxCount', async () => { + expect.hasAssertions(); + + const htr = new TokenInfo('00', 'Hathor', 'HTR', 5); + const token1 = new TokenInfo('token1', 'MyToken1', 'MT1', 10); + const token2 = new TokenInfo('token2', 'MyToken2', 'MT2', 15); + + await addToTokenTable(mysql, [ + { id: htr.id, name: htr.name, symbol: htr.symbol, transactions: htr.transactions }, + { id: token1.id, name: token1.name, symbol: token1.symbol, transactions: token1.transactions }, + { id: token2.id, name: token2.name, symbol: token2.symbol, transactions: token2.transactions }, + ]); + + await incrementTokensTxCount(mysql, ['token1', '00', 'token2']); + + await expect(checkTokenTable(mysql, 3, [{ + tokenId: token1.id, + tokenSymbol: token1.symbol, + tokenName: token1.name, + transactions: token1.transactions + 1, + }, { + tokenId: token2.id, + tokenSymbol: token2.symbol, + tokenName: token2.name, + transactions: token2.transactions + 1, + }, { + tokenId: htr.id, + tokenSymbol: htr.symbol, + tokenName: htr.name, + transactions: htr.transactions + 1, + }])).resolves.toBe(true); +}); + +test('existsPushDevice', async () => { + expect.hasAssertions(); + + const walletId = 'wallet1'; + const deviceId = 'device1'; + const pushProvider = 'android'; + const enablePush = true; + const enableShowAmounts = false; + + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + let existsResult = await existsPushDevice(mysql, deviceId, walletId); + + // there is no device registered to a wallet at this stage + expect(existsResult).toBe(false); + + // register the device to a wallet + await registerPushDevice(mysql, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + }); + + existsResult = await existsPushDevice(mysql, deviceId, walletId); + + // there is a device registered to a wallet + expect(existsResult).toBe(true); +}); + +test('registerPushDevice', async () => { + expect.hasAssertions(); + + const walletId = 'wallet1'; + const deviceId = 'device1'; + const pushProvider = 'android'; + const enablePush = true; + const enableShowAmounts = false; + + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + await registerPushDevice(mysql, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + }); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + })).resolves.toBe(true); +}); + +describe('updatePushDevice', () => { + it('should update pushDevice when register exists', async () => { + expect.hasAssertions(); + + const walletId = 'wallet1'; + const deviceId = 'device1'; + const pushProvider = 'android'; + const enableShowAmounts = false; + + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + await registerPushDevice(mysql, { + walletId, + deviceId, + pushProvider, + enablePush: false, + enableShowAmounts, + }); + + await updatePushDevice(mysql, { + walletId, + deviceId, + enablePush: true, + enableShowAmounts, + }); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId, + pushProvider, + enablePush: true, + enableShowAmounts, + })).resolves.toBe(true); + }); + + it('should update pushDevice when more than 1 wallet is related', async () => { + expect.hasAssertions(); + + const deviceToUpdate = 'device1'; + const deviceToKeep = 'device2'; + const walletId = 'wallet1'; + const pushProvider = 'android'; + const enableShowAmounts = false; + + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + const devicesToAdd = [deviceToUpdate, deviceToKeep]; + devicesToAdd.forEach(async (eachDevice) => { + await registerPushDevice(mysql, { + walletId, + deviceId: eachDevice, + pushProvider, + enablePush: false, + enableShowAmounts, + }); + }); + await expect(checkPushDevicesTable(mysql, devicesToAdd.length)).resolves.toBe(true); + + await updatePushDevice(mysql, { + walletId, + deviceId: deviceToUpdate, + enablePush: true, + enableShowAmounts, + }); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId: deviceToUpdate, + pushProvider, + enablePush: true, + enableShowAmounts, + })).resolves.toBe(true); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId: deviceToKeep, + pushProvider, + enablePush: false, + enableShowAmounts, + })).resolves.toBe(true); + }); + + it('should run update successfuly even when there is no device registered', async () => { + expect.hasAssertions(); + + const deviceId = 'device1'; + const walletId = 'wallet1'; + const enablePush = true; + const enableShowAmounts = false; + + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + await updatePushDevice(mysql, { + walletId, + deviceId, + enablePush, + enableShowAmounts, + }); + + await expect(checkPushDevicesTable(mysql, 0)).resolves.toBe(true); + }); +}); + +test('removeAllPushDeviceByDeviceId', async () => { + expect.hasAssertions(); + + const walletId = 'wallet1'; + const deviceIdOne = 'device_1'; + const deviceIdTwo = 'device_2'; + const pushProvider = 'android'; + const enablePush = true; + const enableShowAmounts = false; + + // NOTE: Because deviceId is a primary key in push_devices table + // it is not possible to register more than one device with the same deviceId. + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + await registerPushDevice(mysql, { + walletId, + deviceId: deviceIdOne, + pushProvider, + enablePush, + enableShowAmounts, + }); + await registerPushDevice(mysql, { + walletId, + deviceId: deviceIdTwo, + pushProvider, + enablePush, + enableShowAmounts, + }); + await expect(checkPushDevicesTable(mysql, 2)).resolves.toBe(true); + + // remove all push device registered + await removeAllPushDevicesByDeviceId(mysql, deviceIdOne); + await expect(checkPushDevicesTable(mysql, 1)).resolves.toBe(true); +}); + +test('existsWallet', async () => { + expect.hasAssertions(); + + // wallet do not exists yet + const walletId = 'wallet1'; + let exists = await existsWallet(mysql, walletId); + + expect(exists).toBe(false); + + // wallet exists + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + exists = await existsWallet(mysql, walletId); + + expect(exists).toBe(true); +}); + +describe('unregisterPushDevice', () => { + it('should unregister device', async () => { + expect.hasAssertions(); + + const walletId = 'wallet1'; + const deviceId = 'device1'; + const pushProvider = 'android'; + const enablePush = false; + const enableShowAmounts = false; + + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + await registerPushDevice(mysql, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + }); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + })).resolves.toBe(true); + + await unregisterPushDevice(mysql, deviceId, walletId); + + await expect(checkPushDevicesTable(mysql, 0)).resolves.toBe(true); + }); + + it('should unregister the right device in face of many', async () => { + expect.hasAssertions(); + + const pushProvider = 'android'; + const enablePush = false; + const enableShowAmounts = false; + const deviceToUnregister = 'device1'; + const deviceToRemain = 'device2'; + const devicesToAdd = [deviceToUnregister, deviceToRemain]; + + const walletId = 'wallet1'; + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + devicesToAdd.forEach(async (eachDevice) => { + await registerPushDevice(mysql, { + walletId, + deviceId: eachDevice, + pushProvider, + enablePush, + enableShowAmounts, + }); + }); + await expect(checkPushDevicesTable(mysql, 2)).resolves.toBe(true); + + await unregisterPushDevice(mysql, deviceToUnregister, walletId); + + await expect(checkPushDevicesTable(mysql, 1)).resolves.toBe(true); + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId: deviceToRemain, + pushProvider, + enablePush, + enableShowAmounts, + })).resolves.toBe(true); + }); + + it('should succeed even when no device exists', async () => { + expect.hasAssertions(); + + const deviceId = 'device-not-exists'; + const walletId = 'wallet-not-exist'; + + await expect(checkPushDevicesTable(mysql, 0)).resolves.toBe(true); + + await unregisterPushDevice(mysql, deviceId, walletId); + + await expect(checkPushDevicesTable(mysql, 0)).resolves.toBe(true); + }); + + it('should unregister device when provided only the device id', async () => { + expect.hasAssertions(); + + const walletId = 'wallet1'; + const deviceId = 'device1'; + const pushProvider = 'android'; + const enablePush = false; + const enableShowAmounts = false; + + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + await registerPushDevice(mysql, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + }); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + })).resolves.toBe(true); + + await unregisterPushDevice(mysql, deviceId); + + await expect(checkPushDevicesTable(mysql, 0)).resolves.toBe(true); + }); +}); + +describe('getTransactionById', () => { + it('should return a tx their tokens and balances', async () => { + expect.hasAssertions(); + + const txId1 = 'txId1'; + const walletId1 = 'wallet1'; + const addr1 = 'addr1'; + const token1 = { id: 'token1', name: 'Token 1', symbol: 'T1' }; + const token2 = { id: 'token2', name: 'Token 2', symbol: 'T2' }; + const timestamp1 = 10; + const height1 = 1; + const version1 = 3; + const weight1 = 65.4321; + + await createWallet(mysql, walletId1, XPUBKEY, AUTH_XPUBKEY, 5); + await addOrUpdateTx(mysql, txId1, height1, timestamp1, version1, weight1); + + await addToTokenTable(mysql, [ + { id: token1.id, name: token1.name, symbol: token1.symbol, transactions: 0 }, + { id: token2.id, name: token2.name, symbol: token2.symbol, transactions: 0 }, + ]); + const entries = [ + { address: addr1, txId: txId1, tokenId: token1.id, balance: 10, timestamp: timestamp1 }, + { address: addr1, txId: txId1, tokenId: token2.id, balance: 7, timestamp: timestamp1 }, + ]; + await addToAddressTxHistoryTable(mysql, entries); + await initWalletTxHistory(mysql, walletId1, [addr1]); + + const txTokens = await getTransactionById(mysql, txId1, walletId1); + + const [firstToken] = txTokens.filter((eachToken) => eachToken.tokenId === 'token1'); + const [secondToken] = txTokens.filter((eachToken) => eachToken.tokenId === 'token2'); + + expect(firstToken).toStrictEqual({ + balance: 10, + timestamp: timestamp1, + tokenId: token1.id, + tokenName: token1.name, + tokenSymbol: token1.symbol, + txId: txId1, + version: version1, + voided: false, + weight: weight1, + }); + expect(secondToken).toStrictEqual({ + balance: 7, + timestamp: timestamp1, + tokenId: token2.id, + tokenName: token2.name, + tokenSymbol: token2.symbol, + txId: txId1, + version: version1, + voided: false, + weight: weight1, + }); + }); + + it('should return empty list when there is no record', async () => { + expect.hasAssertions(); + + const txId = 'txId1'; + const walletId = 'wallet1'; + + const txTokens = await getTransactionById(mysql, txId, walletId); + + expect(txTokens).toHaveLength(0); + }); +}); + +describe('getPushDevice', () => { + it('should return PushDevice type object when device found', async () => { + expect.hasAssertions(); + + const walletId = 'wallet1'; + const deviceId = 'device1'; + const pushProvider = 'android'; + const enablePush = true; + const enableShowAmounts = false; + + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + await registerPushDevice(mysql, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + }); + + const result = await getPushDevice(mysql, deviceId); + + const expected = { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + } as PushDevice; + expect(result).toStrictEqual(expected); + }); + + it('should return null when device not found', async () => { + expect.hasAssertions(); + + const walletId = 'wallet1'; + const deviceId = 'device1'; + + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + const result = await getPushDevice(mysql, deviceId); + + expect(result).toBeNull(); + }); + + it('should return null when wallet not found', async () => { + expect.hasAssertions(); + + const deviceId = 'device1'; + + const result = await getPushDevice(mysql, deviceId); + + expect(result).toBeNull(); + }); +}); + +describe('getPushDeviceSettingsList', () => { + it('should return an empty list when no device settings are found', async () => { + expect.hasAssertions(); + + // arrange variables + const deviceCandidates = [ + { + walletId: 'wallet1', + deviceId: 'device1', + pushProvider: PushProvider.ANDROID, + enablePush: true, + enableShowAmounts: true, + }, + { + walletId: 'wallet2', + deviceId: 'device2', + pushProvider: PushProvider.ANDROID, + enablePush: false, + enableShowAmounts: true, + }, + { + walletId: 'wallet3', + deviceId: 'device3', + pushProvider: PushProvider.ANDROID, + enablePush: true, + enableShowAmounts: false, + }, + { + walletId: 'wallet4', + deviceId: 'device4', + pushProvider: PushProvider.ANDROID, + enablePush: false, + enableShowAmounts: false, + }, + ]; + + // devices to load on database + const devicesToLoad = deviceCandidates.filter((each) => each.enablePush === true); + // devices to not load on database, they will be used on query + const devicesToNotLoad = deviceCandidates.filter((each) => each.enablePush === false); + + // register wallets that will not be queried + const loadWallet = (eachDevice) => createWallet(mysql, eachDevice.walletId, XPUBKEY, AUTH_XPUBKEY, 5); + await devicesToLoad.forEach(loadWallet); + + // register devices related to the loaded wallets + const loadDevice = (eachDevice) => registerPushDevice(mysql, { + walletId: eachDevice.walletId, + deviceId: eachDevice.deviceId, + pushProvider: eachDevice.pushProvider, + enablePush: eachDevice.enablePush, + enableShowAmounts: eachDevice.enableShowAmounts, + }); + await devicesToLoad.forEach(loadDevice); + + // get settings querying only devices not loaded on database, resulting on empty list + const notRegisteredWalletIdList = devicesToNotLoad.map((each) => each.walletId); + const result = await getPushDeviceSettingsList(mysql, notRegisteredWalletIdList); + + // assert settings + expect(result).toStrictEqual([]); + }); + + it('should return a list of settings even when some wallet ids are not found', async () => { + expect.hasAssertions(); + + // arrange variables + const deviceCandidates = [ + { + walletId: 'wallet1', + deviceId: 'device1', + pushProvider: PushProvider.ANDROID, + enablePush: true, + enableShowAmounts: true, + }, + { + walletId: 'wallet2', + deviceId: 'device2', + pushProvider: PushProvider.ANDROID, + enablePush: false, + enableShowAmounts: true, + }, + { + walletId: 'wallet3', + deviceId: 'device3', + pushProvider: PushProvider.ANDROID, + enablePush: true, + enableShowAmounts: false, + }, + { + walletId: 'wallet4', + deviceId: 'device4', + pushProvider: PushProvider.ANDROID, + enablePush: false, + enableShowAmounts: false, + }, + ]; + + // devices to load on database + const devicesToLoad = deviceCandidates.filter((each) => each.enablePush === true); + // devices to not load on database + const devicesToNotLoad = deviceCandidates.filter((each) => each.enablePush === false); + + // register wallets to be used by registered devices + const loadWallet = (eachDevice) => createWallet(mysql, eachDevice.walletId, XPUBKEY, AUTH_XPUBKEY, 5); + await devicesToLoad.forEach(loadWallet); + + // register devices related to the loaded wallets + const loadDevice = (eachDevice) => registerPushDevice(mysql, { + walletId: eachDevice.walletId, + deviceId: eachDevice.deviceId, + pushProvider: eachDevice.pushProvider, + enablePush: eachDevice.enablePush, + enableShowAmounts: eachDevice.enableShowAmounts, + }); + await devicesToLoad.forEach(loadDevice); + + // get settings, query be all wallets of deviceCandidates, some are loaded on database, some are not + const walletIdList = deviceCandidates.map((each) => each.walletId); + const result = await getPushDeviceSettingsList(mysql, walletIdList); + + // assert settings, only devices with loaded wallets on database will be found + expect(result).toHaveLength(2); + + // verify devices loaded, they should yield a not empty list, equal to the loaded devices + const expectedPushDeviceSettigsList = deviceCandidates + .filter((each) => each.enablePush === true) + .map((each) => ({ + deviceId: each.deviceId, + walletId: each.walletId, + enablePush: each.enablePush, + enableShowAmounts: each.enableShowAmounts, + })); + expect(result).toStrictEqual(expectedPushDeviceSettigsList); + + // verify devices not loaded, they should yield an empty list + const walletIdListForNotRegisteredDevices = devicesToNotLoad.map((each) => each.deviceId); + const resultNotRegisteredDevices = await getPushDeviceSettingsList(mysql, walletIdListForNotRegisteredDevices); + expect(resultNotRegisteredDevices).toStrictEqual([]); + }); + + it('should return a list of settings for all the wallet ids', async () => { + expect.hasAssertions(); + + // arrange variables + const devicesToLoad = [ + { + walletId: 'wallet1', + deviceId: 'device1', + pushProvider: PushProvider.ANDROID, + enablePush: true, + enableShowAmounts: true, + }, + { + walletId: 'wallet2', + deviceId: 'device2', + pushProvider: PushProvider.ANDROID, + enablePush: false, + enableShowAmounts: true, + }, + { + walletId: 'wallet3', + deviceId: 'device3', + pushProvider: PushProvider.ANDROID, + enablePush: true, + enableShowAmounts: false, + }, + { + walletId: 'wallet4', + deviceId: 'device4', + pushProvider: PushProvider.ANDROID, + enablePush: false, + enableShowAmounts: false, + }, + ]; + + // register wallets, load all the wallets related to devicesToLoad + const loadWallet = (eachDevice) => createWallet(mysql, eachDevice.walletId, XPUBKEY, AUTH_XPUBKEY, 5); + await devicesToLoad.forEach(loadWallet); + + // register devices, register all the devices + const loadDevice = (eachDevice) => registerPushDevice(mysql, { + walletId: eachDevice.walletId, + deviceId: eachDevice.deviceId, + pushProvider: eachDevice.pushProvider, + enablePush: eachDevice.enablePush, + enableShowAmounts: eachDevice.enableShowAmounts, + }); + await devicesToLoad.forEach(loadDevice); + + // get settings, get every device registered + const walletIdList = devicesToLoad.map((each) => each.walletId); + const result = await getPushDeviceSettingsList(mysql, walletIdList); + + // assert settings + expect(result).toHaveLength(4); + + const expectedPushDeviceSettigsList = devicesToLoad.map((each) => ({ + deviceId: each.deviceId, + walletId: each.walletId, + enablePush: each.enablePush, + enableShowAmounts: each.enableShowAmounts, + })); + expect(result).toStrictEqual(expectedPushDeviceSettigsList); + }); +}); + +describe('getTokenSymbols', () => { + it('should return a map of token symbol by token id', async () => { + expect.hasAssertions(); + + const tokensToPersist = [ + new TokenInfo('token1', 'tokenName1', 'TKN1'), + new TokenInfo('token2', 'tokenName2', 'TKN2'), + new TokenInfo('token3', 'tokenName3', 'TKN3'), + new TokenInfo('token4', 'tokenName4', 'TKN4'), + new TokenInfo('token5', 'tokenName5', 'TKN5'), + ]; + + // persist tokens + for (const eachToken of tokensToPersist) { + await storeTokenInformation(mysql, eachToken.id, eachToken.name, eachToken.symbol); + } + + const tokenIdList = tokensToPersist.map((each: TokenInfo) => each.id); + const tokenSymbolMap = await getTokenSymbols(mysql, tokenIdList); + + expect(tokenSymbolMap).toStrictEqual({ + token1: 'TKN1', + token2: 'TKN2', + token3: 'TKN3', + token4: 'TKN4', + token5: 'TKN5', + }); + }); + + it('should return null when no token is found', async () => { + expect.hasAssertions(); + + const tokensToPersist = [ + new TokenInfo('token1', 'tokenName1', 'TKN1'), + new TokenInfo('token2', 'tokenName2', 'TKN2'), + new TokenInfo('token3', 'tokenName3', 'TKN3'), + new TokenInfo('token4', 'tokenName4', 'TKN4'), + new TokenInfo('token5', 'tokenName5', 'TKN5'), + ]; + + // no token persistence + + let tokenIdList = tokensToPersist.map((each: TokenInfo) => each.id); + let tokenSymbolMap = await getTokenSymbols(mysql, tokenIdList); + + expect(tokenSymbolMap).toBeNull(); + + tokenIdList = []; + tokenSymbolMap = await getTokenSymbols(mysql, tokenIdList); + + expect(tokenSymbolMap).toBeNull(); + }); +}); + +describe('countStalePushDevices', () => { + it('should return the number of stale push devices', async () => { + expect.hasAssertions(); + + /** + * Before any push device is registered, there should be no stale push devices + */ + await expect(countStalePushDevices(mysql)).resolves.toBe(0); + + const walletId = 'wallet1'; + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + const pushRegister = buildPushRegister({ + walletId: 'wallet1', + updatedAt: daysAgo(32), // it must be 32 because there are months with 31 days + }); + await insertPushDevice(mysql, pushRegister); + + await expect(countStalePushDevices(mysql)).resolves.toBe(1); + }); +}); + +describe('deleteStalePushDevices', () => { + it('should delete stale push devices', async () => { + expect.hasAssertions(); + + /** + * Before any push device is registered, deleteStalePushDevices should not fail + */ + await expect(deleteStalePushDevices(mysql)).resolves.toBeUndefined(); + + const walletId = 'wallet1'; + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 5); + + const pushRegister = buildPushRegister({ + walletId: 'wallet1', + updatedAt: daysAgo(32), // it must be 32 because there are months with 31 days + }); + await insertPushDevice(mysql, pushRegister); + + await expect(countStalePushDevices(mysql)).resolves.toBe(1); + + await deleteStalePushDevices(mysql); + + await expect(countStalePushDevices(mysql)).resolves.toBe(0); + }); +}); + +describe('Clear unsent txProposals utxos', () => { + it('should unset txProposal and txProposalId from unsent txProposals', async () => { + expect.hasAssertions(); + + const walletId = 'wallet-id'; + + const txProposalId1: string = uuidv4() as string; + const txProposalId2: string = uuidv4() as string; + const txProposalId3: string = uuidv4() as string; + + // count unsent tx proposals + await createTxProposal(mysql, txProposalId1, walletId, 1); + await createTxProposal(mysql, txProposalId2, walletId, 1); + await createTxProposal(mysql, txProposalId3, walletId, 1); + + await addToUtxoTable(mysql, [{ + txId: 'tx1', + index: 0, + tokenId: '00', + address: 'address1', + value: 5, + authorities: 0, + timelock: 0, + heightlock: 0, + locked: false, + spentBy: null, + txProposalId: txProposalId1, + txProposalIndex: 0, + }, { + txId: 'tx2', + index: 0, + tokenId: '00', + address: 'address1', + value: 5, + authorities: 0, + timelock: 0, + heightlock: 0, + locked: false, + spentBy: null, + txProposalId: txProposalId2, + txProposalIndex: 0, + }, { + txId: 'tx3', + index: 0, + tokenId: '00', + address: 'address1', + value: 5, + authorities: 0, + timelock: 0, + heightlock: 0, + locked: false, + spentBy: null, + txProposalId: txProposalId3, + txProposalIndex: 0, + }]); + + let utxo1 = await getTxOutput(mysql, 'tx1', 0, false); + let utxo2 = await getTxOutput(mysql, 'tx2', 0, false); + let utxo3 = await getTxOutput(mysql, 'tx3', 0, false); + + expect(utxo1.txProposalId).toStrictEqual(txProposalId1); + expect(utxo2.txProposalId).toStrictEqual(txProposalId2); + expect(utxo3.txProposalId).toStrictEqual(txProposalId3); + + await cleanUnsentTxProposalsUtxos(); + + utxo1 = await getTxOutput(mysql, 'tx1', 0, false); + utxo2 = await getTxOutput(mysql, 'tx2', 0, false); + utxo3 = await getTxOutput(mysql, 'tx3', 0, false); + + expect(utxo1.txProposalId).toBeNull(); + expect(utxo2.txProposalId).toBeNull(); + expect(utxo3.txProposalId).toBeNull(); + + const txProposals = await Promise.all([ + getTxProposal(mysql, txProposalId1), + getTxProposal(mysql, txProposalId2), + getTxProposal(mysql, txProposalId3), + ]); + + expect(txProposals[0].status).toStrictEqual(TxProposalStatus.CANCELLED); + expect(txProposals[1].status).toStrictEqual(TxProposalStatus.CANCELLED); + expect(txProposals[2].status).toStrictEqual(TxProposalStatus.CANCELLED); + + const spy = jest.spyOn(Db, 'releaseTxProposalUtxos'); + spy.mockImplementationOnce(() => { + throw new Error('error-releasing-tx-proposal'); + }); + + await cleanUnsentTxProposalsUtxos(); + + expect(logger.error).toHaveBeenCalledWith( + 'Failed to release unspent tx proposals: ', + expect.anything(), + expect.anything(), + ); + }); + + it('should not fail when there is nothing to clear', async () => { + expect.hasAssertions(); + + await cleanUnsentTxProposalsUtxos(); + + expect(logger.debug).toHaveBeenCalledWith('No txproposals utxos to clean.'); + }); +}); + +describe('getAddressByIndex', () => { + it('should find a wallets address from its index', async () => { + expect.hasAssertions(); + + const address = 'address'; + const walletId = 'walletId'; + const index = 0; + const transactions = 0; + + await addToAddressTable(mysql, [{ + address, + index, + walletId, + transactions, + }]); + + await expect(getAddressAtIndex(mysql, walletId, index)) + .resolves + .toStrictEqual({ + address, + index, + transactions, + }); + }); + + it('should return null if an address couldnt be found', async () => { + expect.hasAssertions(); + + const walletId = 'walletId'; + + await expect(getAddressAtIndex(mysql, walletId, 1)) + .resolves + .toBeNull(); + }); +}); diff --git a/packages/wallet-service/tests/db.utils.test.ts b/packages/wallet-service/tests/db.utils.test.ts new file mode 100644 index 00000000..9b8c6f59 --- /dev/null +++ b/packages/wallet-service/tests/db.utils.test.ts @@ -0,0 +1,64 @@ +import { + getTxsFromDBResult, + getTxFromDBResult, +} from '@src/db/utils'; + +test('getTxsFromDBResult should transform DB Result to array of Tx', async () => { + expect.hasAssertions(); + + // Simulate Row Data Packets + const dbResult = [ + { + tx_id: 'txId10', + timestamp: 6, + version: 0, + voided: 0, + height: 10, + weight: 60, + }, + { + tx_id: 'txId8', + timestamp: 4, + version: 0, + voided: 0, + height: 8, + weight: 60, + }, + { + tx_id: 'txId9', + timestamp: 5, + version: 0, + voided: 0, + height: 9, + weight: 60, + }, + ]; + + const txs = getTxsFromDBResult(dbResult); + + // txId is an attribute of Tx interface + expect(txs[0].txId).toBe('txId10'); + expect(txs[1].txId).toBe('txId8'); + expect(txs[2].txId).toBe('txId9'); +}); + +test('getTxFromDBResult should transform DB Result to Tx', async () => { + expect.hasAssertions(); + + // Simulate Row Data Packets + const dbResult = [ + { + tx_id: 'txId10', + timestamp: 6, + version: 0, + voided: 0, + height: 10, + weight: 60, + }, + ]; + + const tx = getTxFromDBResult(dbResult); + + // txId is an attribute of Tx interface + expect(tx.txId).toBe('txId10'); +}); diff --git a/packages/wallet-service/tests/fullnode.test.ts b/packages/wallet-service/tests/fullnode.test.ts new file mode 100644 index 00000000..4fc3a02c --- /dev/null +++ b/packages/wallet-service/tests/fullnode.test.ts @@ -0,0 +1,58 @@ +import fullnode from '@src/fullnode'; + +test('downloadTx', async () => { + expect.hasAssertions(); + + const mockData = { + success: true, + tx: { + hash: 'tx1', + }, + meta: {}, + }; + + const apiGetSpy = jest.spyOn(fullnode.api, 'get'); + apiGetSpy.mockImplementation(() => Promise.resolve({ + status: 200, + data: mockData, + })); + + const response = await fullnode.downloadTx('tx1'); + expect(response).toStrictEqual(mockData); +}); + +test('getConfirmationData', async () => { + expect.hasAssertions(); + + const mockData = { + success: true, + accumulated_weight: 67.45956109191802, + accumulated_bigger: true, + stop_value: 67.45416781056525, + confirmation_level: 1, + }; + + const apiGetSpy = jest.spyOn(fullnode.api, 'get'); + apiGetSpy.mockImplementation(() => Promise.resolve({ + status: 200, + data: mockData, + })); + + const response = await fullnode.getConfirmationData('tx1'); + expect(response).toStrictEqual(mockData); +}); + +test('queryGraphvizNeighbours', async () => { + expect.hasAssertions(); + + const mockData = 'diagraph {}'; + + const apiGetSpy = jest.spyOn(fullnode.api, 'get'); + apiGetSpy.mockImplementation(() => Promise.resolve({ + status: 200, + data: mockData, + })); + + const response = await fullnode.queryGraphvizNeighbours('tx1', 'test', 1); + expect(response).toStrictEqual(mockData); +}); diff --git a/packages/wallet-service/tests/integration.test.ts b/packages/wallet-service/tests/integration.test.ts new file mode 100644 index 00000000..af02e4ad --- /dev/null +++ b/packages/wallet-service/tests/integration.test.ts @@ -0,0 +1,450 @@ +import { initFirebaseAdminMock } from '@tests/utils/firebase-admin.mock'; +import eventTemplate from '@events/eventTemplate.json'; +import { loadWallet } from '@src/api/wallet'; +import { createWallet, getMinersList } from '@src/db'; +import * as txProcessor from '@src/txProcessor'; +import { Transaction, WalletStatus, TxInput } from '@src/types'; +import { closeDbConnection, getDbConnection, getUnixTimestamp, getWalletId } from '@src/utils'; +import { + ADDRESSES, + XPUBKEY, + AUTH_XPUBKEY, + cleanDatabase, + checkAddressTable, + checkAddressBalanceTable, + checkAddressTxHistoryTable, + checkUtxoTable, + checkWalletBalanceTable, + checkWalletTable, + checkWalletTxHistoryTable, + createOutput, + createInput, + addToUtxoTable, +} from '@tests/utils'; + +const mysql = getDbConnection(); + +initFirebaseAdminMock(); +const blockReward = 6400; +const htrToken = '00'; +const walletId = getWalletId(XPUBKEY); +const now = getUnixTimestamp(); +const maxGap = parseInt(process.env.MAX_ADDRESS_GAP, 10); +const OLD_ENV = process.env; + +/* + * xpubkey first addresses are: [ + * 'HBCQgVR8Xsyv1BLDjf9NJPK1Hwg4rKUh62', + * 'HPDWdurEygcubNMUUnTDUAzngrSXFaqGQc', + * 'HEYCNNZZYrimD97AtoRcgcNFzyxtkgtt9Q', + * 'HPTtSRrDd4ekU4ZQ2jnSLYayL8hiToE5D4', + * 'HTYymKpjyXnz4ssEAnywtwnXnfneZH1Dbh', + * 'HUp754aDZ7yKndw2JchXEiMvgzKuXasUmF', + * 'HLfGaQoxssGbZ4h9wbLyiCafdE8kPm6Fo4', + * 'HV3ox5B1Dai6Jp5EhV8DvUiucc1z3WJHjL', + * ] + */ + +const blockEvent = JSON.parse(JSON.stringify(eventTemplate)); +const block: Transaction = blockEvent.Records[0].body; +const txId1 = 'txId1'; +block.tx_id = txId1; +block.timestamp = now; +block.height = 1; +block.outputs = [createOutput(0, blockReward, ADDRESSES[0])]; + +// receive another block. Reward from first block should now be unlocked +const blockEvent2 = JSON.parse(JSON.stringify(eventTemplate)); +const block2: Transaction = blockEvent2.Records[0].body; +const txId2 = 'txId2'; +block2.tx_id = txId2; +block2.timestamp = block.timestamp + 30; +block2.height = block.height + 1; +block2.outputs = [createOutput(0, blockReward, ADDRESSES[0])]; + +// block3 is from another miner +const blockEvent3 = JSON.parse(JSON.stringify(eventTemplate)); +const block3: Transaction = blockEvent3.Records[0].body; +const anotherMinerTx = 'another_miner_tx'; +block3.tx_id = anotherMinerTx; +block3.timestamp = block.timestamp + 60; +block3.height = block2.height + 1; +block3.outputs = [createOutput(0, blockReward, 'HTRuXktQiHvrfrwCZCPPBXNZK5SejgPneE')]; + +// block4 is from yet another miner +const blockEvent4 = JSON.parse(JSON.stringify(eventTemplate)); +const block4: Transaction = blockEvent4.Records[0].body; +const yetAnotherMinerTx = 'yet_another_miner_tx'; +block4.tx_id = yetAnotherMinerTx; +block4.timestamp = block.timestamp + 90; +block4.height = block3.height + 1; +block4.outputs = [createOutput(0, blockReward, 'HJPcaSncHGhzasvbbWP5yfZ6XSixwLHdHu')]; + +// tx sends first block rewards to 2 addresses on the same wallet +const txEvent = JSON.parse(JSON.stringify(eventTemplate)); +const tx: Transaction = txEvent.Records[0].body; +const txId3 = 'txId3'; +tx.version = 1; +tx.tx_id = txId3; +tx.timestamp += 20; +tx.inputs = [createInput(blockReward, ADDRESSES[0], txId1, 0)]; +tx.outputs = [ + createOutput(0, blockReward - 5000, ADDRESSES[1]), + createOutput(1, 5000, ADDRESSES[2]), +]; + +// tx sends one of last tx's outputs to 2 addresses, one of which is not from this wallet. Also, output sent to this wallet is locked +const txEvent2 = JSON.parse(JSON.stringify(eventTemplate)); +const tx2: Transaction = txEvent2.Records[0].body; +const timelock = now + 90000; +tx2.version = 1; +const txId4 = 'txId4'; +tx2.tx_id = txId4; +tx2.timestamp += 20; +tx2.inputs = [ + createInput(5000, ADDRESSES[2], txId2, 1), +]; +tx2.outputs = [ + createOutput(0, 1000, ADDRESSES[6], '00', timelock), // belongs to this wallet + createOutput(1, 4000, 'HCuWC2qgNP47BtWtsTM48PokKitVdR6pch'), // other wallet +]; + +// tx2Inputs on the format addToUtxoTable expects +const tx2Inputs = tx2.inputs.map((input: TxInput) => ({ + txId: input.tx_id, + index: input.index, + tokenId: input.token, + address: input.decoded.address, + value: input.value, + authorities: null, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, +})); + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + +beforeAll(async () => { + // modify env so block reward is unlocked after 1 new block (overrides .env file) + jest.resetModules(); + process.env = { ...OLD_ENV }; + process.env.BLOCK_REWARD_LOCK = '1'; +}); + +afterAll(async () => { + await closeDbConnection(mysql); + // restore old env + process.env = OLD_ENV; +}); + +// eslint-disable-next-line jest/prefer-expect-assertions, jest/expect-expect +test('receive blocks and txs and then start wallet', async () => { + /* + * receive first block + */ + await txProcessor.onNewTxEvent(blockEvent); + await checkAfterReceivingFirstBlock(false); + + /* + * receive second block + */ + await txProcessor.onNewTxEvent(blockEvent2); + await checkAfterReceivingSecondBlock(false); + + /* + * add transaction that sends block reward to 2 different addresses on same wallet + */ + await txProcessor.onNewTxEvent(txEvent); + await checkAfterReceivingTx1(false); + + // txEvent2 uses utxos that are not from the received blocks, so we must add them to the database + await addToUtxoTable(mysql, tx2Inputs); + + /* + * add transaction that sends block reward to 2 different addresses, one of which is not in this wallet + */ + await txProcessor.onNewTxEvent(txEvent2); + await checkAfterReceivingTx2(false); + + /* + * create wallet + */ + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, maxGap); + await loadWallet({ xpubkey: XPUBKEY, maxGap }, null, null); + + await checkAfterReceivingTx2(true); +}, 60000); + +// eslint-disable-next-line jest/prefer-expect-assertions, jest/expect-expect +test('start wallet and then receive blocks and txs', async () => { + /* + * create wallet + */ + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, maxGap); + await loadWallet({ xpubkey: XPUBKEY, maxGap }, null, null); + + /* + * receive a block + */ + await txProcessor.onNewTxEvent(blockEvent); + await checkAfterReceivingFirstBlock(true); + + /* + * receive second block + */ + await txProcessor.onNewTxEvent(blockEvent2); + await checkAfterReceivingSecondBlock(true); + + /* + * add transaction that sends block reward to 2 different addresses on same wallet + */ + await txProcessor.onNewTxEvent(txEvent); + await checkAfterReceivingTx1(true); + + // txEvent2 uses utxos that are not from the received blocks, so we must add them to the database + await addToUtxoTable(mysql, tx2Inputs); + + /* + * add transaction that sends block reward to 2 different addresses, one of which is not in this wallet + */ + await txProcessor.onNewTxEvent(txEvent2); + await checkAfterReceivingTx2(true); +}, 60000); + +// eslint-disable-next-line jest/prefer-expect-assertions, jest/expect-expect +test('receive blocks, start wallet and then receive transactions', async () => { + /* + * receive a block + */ + await txProcessor.onNewTxEvent(blockEvent); + await checkAfterReceivingFirstBlock(false); + + /* + * receive second block + */ + await txProcessor.onNewTxEvent(blockEvent2); + await checkAfterReceivingSecondBlock(false); + + /* + * create wallet + */ + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, maxGap); + await loadWallet({ xpubkey: XPUBKEY, maxGap }, null, null); + + /* + * add transaction that sends block reward to 2 different addresses on same wallet + */ + await txProcessor.onNewTxEvent(txEvent); + await checkAfterReceivingTx1(true); + + // txEvent2 uses utxos that are not from the received blocks, so we must add them to the database + await addToUtxoTable(mysql, tx2Inputs); + + /* + * add transaction that sends block reward to 2 different addresses, one of which is not in this wallet + */ + await txProcessor.onNewTxEvent(txEvent2); + await checkAfterReceivingTx2(true); +}, 35000); + +// eslint-disable-next-line jest/prefer-expect-assertions, jest/expect-expect +test('receive blocks and tx1, start wallet and then receive tx2', async () => { + /* + * receive a block + */ + await txProcessor.onNewTxEvent(blockEvent); + await checkAfterReceivingFirstBlock(false); + + /* + * receive second block + */ + await txProcessor.onNewTxEvent(blockEvent2); + await checkAfterReceivingSecondBlock(false); + + /* + * add transaction that sends block reward to 2 different addresses on same wallet + */ + await txProcessor.onNewTxEvent(txEvent); + await checkAfterReceivingTx1(false); + + /* + * create wallet + */ + await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, maxGap); + await loadWallet({ xpubkey: XPUBKEY, maxGap }, null, null); + + // txEvent2 uses utxos that are not from the received blocks, so we must add them to the database + await addToUtxoTable(mysql, tx2Inputs); + + /* + * add transaction that sends block reward to 2 different addresses, one of which is not in this wallet + */ + await txProcessor.onNewTxEvent(txEvent2); + await checkAfterReceivingTx2(true); +}, 35000); + +// eslint-disable-next-line jest/prefer-expect-assertions, jest/expect-expect +test('receive blocks fom 3 different miners, check miners list', async () => { + /* + * receive a block + */ + await txProcessor.onNewTxEvent(blockEvent); + + /* + * receive second block + */ + await txProcessor.onNewTxEvent(blockEvent2); + + /* + * receive the third block + */ + await txProcessor.onNewTxEvent(blockEvent3); + + /* + * receive the fourth block + */ + await txProcessor.onNewTxEvent(blockEvent4); + + const minerList = await getMinersList(mysql); + + expect(minerList).toHaveLength(3); +}, 35000); + +/* + * After receiving the block, we only have 1 used address and block rewards are locked + */ +const checkAfterReceivingFirstBlock = async (walletStarted = false) => { + const blockRewardLock = parseInt(process.env.BLOCK_REWARD_LOCK, 10); + await expect( + checkUtxoTable(mysql, 1, txId1, 0, htrToken, ADDRESSES[0], blockReward, 0, null, block.height + blockRewardLock, true), + ).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, ADDRESSES[0], htrToken, 0, blockReward, null, 1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 1, ADDRESSES[0], txId1, htrToken, blockReward, block.timestamp)).resolves.toBe(true); + if (walletStarted) { + await expect(checkWalletTable(mysql, 1, walletId, WalletStatus.READY)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 1, walletId, htrToken, txId1, blockReward, block.timestamp)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, htrToken, 0, blockReward, null, 1)).resolves.toBe(true); + await expect(checkAddressTable(mysql, maxGap + 1, ADDRESSES[0], 0, walletId, 1)).resolves.toBe(true); + // addresses other than the used on must have been added to address table + for (let i = 1; i < maxGap + 1; i++) { + await expect(checkAddressTable(mysql, maxGap + 1, ADDRESSES[i], i, walletId, 0)).resolves.toBe(true); + } + } else { + await expect(checkWalletTable(mysql, 0)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 0)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 1, ADDRESSES[0], null, null, 1)).resolves.toBe(true); + } +}; + +/* + * After receiving second block, rewards from the first block are unlocked + */ +const checkAfterReceivingSecondBlock = async (walletStarted = false) => { + const blockRewardLock = parseInt(process.env.BLOCK_REWARD_LOCK, 10); + await expect( + checkUtxoTable(mysql, 2, txId2, 0, htrToken, ADDRESSES[0], blockReward, 0, null, block2.height + blockRewardLock, true), + ).resolves.toBe(true); + // first block utxo is unlocked + await expect( + checkUtxoTable(mysql, 2, txId1, 0, htrToken, ADDRESSES[0], blockReward, 0, null, block.height + blockRewardLock, false), + ).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, ADDRESSES[0], htrToken, blockReward, blockReward, null, 2)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 2, ADDRESSES[0], txId1, htrToken, blockReward, block.timestamp)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 2, ADDRESSES[0], txId2, htrToken, blockReward, block2.timestamp)).resolves.toBe(true); + if (walletStarted) { + await expect(checkWalletTable(mysql, 1, walletId, WalletStatus.READY)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 2, walletId, htrToken, txId1, blockReward, block.timestamp)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 2, walletId, htrToken, txId2, blockReward, block2.timestamp)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, htrToken, blockReward, blockReward, null, 2)).resolves.toBe(true); + await expect(checkAddressTable(mysql, maxGap + 1, ADDRESSES[0], 0, walletId, 2)).resolves.toBe(true); + // addresses other than the used on must have been added to address table + for (let i = 1; i < maxGap + 1; i++) { + await expect(checkAddressTable(mysql, maxGap + 1, ADDRESSES[i], i, walletId, 0)).resolves.toBe(true); + } + } else { + await expect(checkWalletTable(mysql, 0)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 0)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 1, ADDRESSES[0], null, null, 2)).resolves.toBe(true); + } +}; + +/* + * This tx sends the block output to 2 addresses on the same wallet, so we have 3 used addresses + */ +const checkAfterReceivingTx1 = async (walletStarted = false) => { + await expect(checkUtxoTable(mysql, 3, txId3, 0, htrToken, ADDRESSES[1], blockReward - 5000, 0, null, null, false)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 3, txId3, 1, htrToken, ADDRESSES[2], 5000, 0, null, null, false)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 3, ADDRESSES[0], htrToken, 0, blockReward, null, 3)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 3, ADDRESSES[1], htrToken, blockReward - 5000, 0, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 3, ADDRESSES[2], htrToken, 5000, 0, null, 1)).resolves.toBe(true); + // 3 new entries must have been address to address_tx_history + await expect(checkAddressTxHistoryTable(mysql, 5, ADDRESSES[0], txId3, htrToken, (-1) * blockReward, tx.timestamp)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 5, ADDRESSES[1], txId3, htrToken, blockReward - 5000, tx.timestamp)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 5, ADDRESSES[2], txId3, htrToken, 5000, tx.timestamp)).resolves.toBe(true); + if (walletStarted) { + await expect(checkWalletTable(mysql, 1, walletId, WalletStatus.READY)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 3, walletId, htrToken, txId1, blockReward, block.timestamp)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 3, walletId, htrToken, txId2, blockReward, block2.timestamp)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 3, walletId, htrToken, txId3, 0, tx.timestamp)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, htrToken, blockReward, blockReward, null, 3)).resolves.toBe(true); + await expect(checkAddressTable(mysql, maxGap + 3, ADDRESSES[0], 0, walletId, 3)).resolves.toBe(true); + await expect(checkAddressTable(mysql, maxGap + 3, ADDRESSES[1], 1, walletId, 1)).resolves.toBe(true); + await expect(checkAddressTable(mysql, maxGap + 3, ADDRESSES[2], 2, walletId, 1)).resolves.toBe(true); + } else { + await expect(checkWalletTable(mysql, 0)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 0)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 3, ADDRESSES[0], null, null, 3)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 3, ADDRESSES[1], null, null, 1)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 3, ADDRESSES[2], null, null, 1)).resolves.toBe(true); + } +}; + +/* + * This tx sends the 5000 HTR output to 2 addresses, one on the same wallet (1000 HTR, locked) and another that's not (4000 HTR) + */ +const checkAfterReceivingTx2 = async (walletStarted = false) => { + await expect(checkUtxoTable(mysql, 5, txId3, 0, htrToken, ADDRESSES[1], blockReward - 5000, 0, null, null, false)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 5, txId4, 0, htrToken, ADDRESSES[6], 1000, 0, timelock, null, true)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 5, txId4, 1, htrToken, 'HCuWC2qgNP47BtWtsTM48PokKitVdR6pch', 4000, 0, null, null, false)).resolves.toBe(true); + // we now have 5 addresses total + await expect(checkAddressBalanceTable(mysql, 5, ADDRESSES[0], htrToken, 0, blockReward, null, 3)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, ADDRESSES[1], htrToken, blockReward - 5000, 0, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, ADDRESSES[2], htrToken, 0, 0, null, 2)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, ADDRESSES[6], htrToken, 0, 1000, timelock, 1)).resolves.toBe(true); // locked + await expect(checkAddressBalanceTable(mysql, 5, 'HCuWC2qgNP47BtWtsTM48PokKitVdR6pch', htrToken, 4000, 0, null, 1)).resolves.toBe(true); + // 3 new entries must have been address to address_tx_history + await expect(checkAddressTxHistoryTable(mysql, 8, ADDRESSES[2], txId4, htrToken, -5000, tx2.timestamp)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, ADDRESSES[6], txId4, htrToken, 1000, tx2.timestamp)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 8, 'HCuWC2qgNP47BtWtsTM48PokKitVdR6pch', txId4, htrToken, 4000, tx2.timestamp)).resolves.toBe(true); + if (walletStarted) { + await expect(checkWalletTable(mysql, 1, walletId, WalletStatus.READY)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 4, walletId, htrToken, txId1, blockReward, block.timestamp)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 4, walletId, htrToken, txId2, blockReward, block2.timestamp)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 4, walletId, htrToken, txId3, 0, tx.timestamp)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 4, walletId, htrToken, txId4, -4000, tx2.timestamp)).resolves.toBe(true); + await expect( + checkWalletBalanceTable(mysql, 1, walletId, htrToken, blockReward - 4000 - 1000, blockReward + 1000, timelock, 4), + ).resolves.toBe(true); + // HLfGaQoxssGbZ4h9wbLyiCafdE8kPm6Fo4 has index 6, so we have 12 addresses from the wallet plus the other one + await expect(checkAddressTable(mysql, maxGap + 7 + 1, ADDRESSES[0], 0, walletId, 3)).resolves.toBe(true); + await expect(checkAddressTable(mysql, maxGap + 7 + 1, ADDRESSES[1], 1, walletId, 1)).resolves.toBe(true); + await expect(checkAddressTable(mysql, maxGap + 7 + 1, ADDRESSES[2], 2, walletId, 2)).resolves.toBe(true); + await expect(checkAddressTable(mysql, maxGap + 7 + 1, ADDRESSES[6], 6, walletId, 1)).resolves.toBe(true); + await expect(checkAddressTable(mysql, maxGap + 7 + 1, 'HCuWC2qgNP47BtWtsTM48PokKitVdR6pch', null, null, 1)).resolves.toBe(true); + } else { + await expect(checkWalletTable(mysql, 0)).resolves.toBe(true); + await expect(checkWalletTxHistoryTable(mysql, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 0)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 5, ADDRESSES[0], null, null, 3)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 5, ADDRESSES[1], null, null, 1)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 5, ADDRESSES[2], null, null, 2)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 5, ADDRESSES[6], null, null, 1)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 5, 'HCuWC2qgNP47BtWtsTM48PokKitVdR6pch', null, null, 1)).resolves.toBe(true); + } +}; diff --git a/packages/wallet-service/tests/jestSetup.ts b/packages/wallet-service/tests/jestSetup.ts new file mode 100644 index 00000000..7a477f44 --- /dev/null +++ b/packages/wallet-service/tests/jestSetup.ts @@ -0,0 +1,6 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { config } from 'dotenv'; + +Object.defineProperty(global, '_bitcore', { get() { return undefined; }, set() {} }); + +config(); diff --git a/packages/wallet-service/tests/mempool.test.ts b/packages/wallet-service/tests/mempool.test.ts new file mode 100644 index 00000000..311ac682 --- /dev/null +++ b/packages/wallet-service/tests/mempool.test.ts @@ -0,0 +1,162 @@ +import { onHandleOldVoidedTxs } from '@src/mempool'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { + addToAddressTxHistoryTable, + addToAddressBalanceTable, + addToTransactionTable, + addToUtxoTable, + checkUtxoTable, + cleanDatabase, + ADDRESSES, + TX_IDS, +} from '@tests/utils'; +import * as Utils from '@src/utils'; +import * as Db from '@src/db'; + +const mysql = getDbConnection(); +const OLD_ENV = process.env; + +beforeEach(async () => { + process.env = { ...OLD_ENV }; + await cleanDatabase(mysql); +}); + +afterAll(async () => { + await closeDbConnection(mysql); +}); + +test('onHandleOldVoidedTxs', async () => { + expect.hasAssertions(); + + const transactions = [ + [TX_IDS[0], 1, 2, false, null, 60], + [TX_IDS[1], 601, 2, false, null, 60], + [TX_IDS[2], 1000, 2, false, null, 60], + // This should be our best block: + [TX_IDS[3], 20 * 60, 0, false, 10, 60], + ]; + + const utxos = [{ + txId: TX_IDS[0], + index: 0, + tokenId: '00', + address: ADDRESSES[0], + value: 50, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[1], + index: 0, + tokenId: '00', + address: ADDRESSES[1], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 0, + tokenId: '00', + address: ADDRESSES[2], + value: 150, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 1, + tokenId: '00', + address: ADDRESSES[3], + value: 200, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + const txHistory = [ + { address: ADDRESSES[0], txId: TX_IDS[0], tokenId: '00', balance: 50, timestamp: 10 }, + { address: ADDRESSES[1], txId: TX_IDS[1], tokenId: '00', balance: 100, timestamp: 10 }, + { address: ADDRESSES[2], txId: TX_IDS[2], tokenId: '00', balance: 150, timestamp: 10 }, + { address: ADDRESSES[3], txId: TX_IDS[2], tokenId: '00', balance: 200, timestamp: 10 }, + ]; + + const addressEntries = [ + // address, tokenId, unlocked, locked, lockExpires, transactions, unlocked_authorities, locked_authorities, total_received + [ADDRESSES[0], '00', 0, 0, null, 1, 0, 0, 100], + [ADDRESSES[1], '00', 0, 0, null, 1, 0, 0, 200], + [ADDRESSES[2], '00', 0, 0, null, 1, 0, 0, 300], + [ADDRESSES[3], '00', 0, 0, null, 1, 0, 0, 400], + ]; + + await addToAddressBalanceTable(mysql, addressEntries); + await addToAddressTxHistoryTable(mysql, txHistory); + await addToTransactionTable(mysql, transactions); + await addToUtxoTable(mysql, utxos); + + const isTxVoidedSpy = jest.spyOn(Utils, 'isTxVoided'); + + // and the check on the fullnode + isTxVoidedSpy.mockReturnValue(Promise.resolve([true, {}])); + // we also need to mock the offset + process.env.VOIDED_TX_OFFSET = '10'; // query will be on timestamp < 600 + + await onHandleOldVoidedTxs(); + + await expect(checkUtxoTable(mysql, 4, TX_IDS[0], 0, '00', ADDRESSES[0], 50, 0, null, null, false, null, true)).resolves.toBe(true); +}); + +test('onHandleOldVoidedTxs should try to confirm the block by fetching the first_block', async () => { + expect.hasAssertions(); + + const transactions = [ + [TX_IDS[0], 1, 2, false, null, 60], + // This is the block tx: + [TX_IDS[3], 15 * 60, 0, false, 10, 60], + ]; + + const utxos = [{ + txId: TX_IDS[0], + index: 0, + tokenId: '00', + address: ADDRESSES[0], + value: 50, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToTransactionTable(mysql, transactions); + await addToUtxoTable(mysql, utxos); + + const isTxVoidedSpy = jest.spyOn(Utils, 'isTxVoided'); + const fetchBlockHeightSpy = jest.spyOn(Utils, 'fetchBlockHeight'); + const updateTxSpy = jest.spyOn(Db, 'updateTx'); + + // also the fetchBlockHeight that goes to the fullnode + fetchBlockHeightSpy.mockReturnValue(Promise.resolve([5, {}] as [number, any])); + // also the check on the fullnode + isTxVoidedSpy.mockReturnValue(Promise.resolve([false, { + meta: { + first_block: TX_IDS[1], + }, + }])); + // and finally, the updateTx so we can expect it to be called + const updateTxMock = updateTxSpy.mockReturnValue(Promise.resolve()); + + // we also need to mock the offset + process.env.VOIDED_TX_OFFSET = '10'; // query will be on timestamp < 5 + + await onHandleOldVoidedTxs(); + expect(updateTxMock).toHaveBeenCalledTimes(1); +}); diff --git a/packages/wallet-service/tests/pushRegister.test.ts b/packages/wallet-service/tests/pushRegister.test.ts new file mode 100644 index 00000000..c4a866a0 --- /dev/null +++ b/packages/wallet-service/tests/pushRegister.test.ts @@ -0,0 +1,257 @@ +import { + register, +} from '@src/api/pushRegister'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { + addToWalletTable, + makeGatewayEventWithAuthorizer, + cleanDatabase, + checkPushDevicesTable, +} from '@tests/utils'; +import { ApiError } from '@src/api/errors'; +import { APIGatewayProxyResult } from 'aws-lambda'; + +const mysql = getDbConnection(); + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + +afterAll(async () => { + await closeDbConnection(mysql); +}); + +test('register a device for push notification', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + deviceId: 'device1', + pushProvider: 'android', + enablePush: true, + enableShowAmounts: false, + })); + + const result = await register(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); +}); + +describe('statusCode:200', () => { + it('should have default value for enablePush', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const payloadWithoutEnablePush = JSON.stringify({ + deviceId: 'device1', + pushProvider: 'android', + enableShowAmounts: false, + }); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, payloadWithoutEnablePush); + + const result = await register(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + await expect( + checkPushDevicesTable(mysql, 1, { + walletId: 'my-wallet', + deviceId: 'device1', + pushProvider: 'android', + enablePush: false, + enableShowAmounts: false, + }), + ).resolves.toBe(true); + }); + + it('should have default value for enableShowAmounts', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const payloadWithoutEnableShowAmounts = JSON.stringify({ + deviceId: 'device1', + pushProvider: 'android', + enablePush: true, + }); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, payloadWithoutEnableShowAmounts); + + const result = await register(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + await expect( + checkPushDevicesTable(mysql, 1, { + walletId: 'my-wallet', + deviceId: 'device1', + pushProvider: 'android', + enablePush: true, + enableShowAmounts: false, + }), + ).resolves.toBe(true); + }); + + it('should register even if alredy exists (idempotency proof)', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const payload = JSON.stringify({ + deviceId: 'device1', + pushProvider: 'android', + enablePush: true, + enableShowAmounts: false, + }); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, payload); + + let result = await register(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + + result = await register(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + }); +}); + +describe('statusCode:400', () => { + it('should validate provider', async () => { + expect.hasAssertions(); + const pushProvider = 'not-supported-provider'; + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + deviceId: 'device1', + pushProvider, + enablePush: true, + enableShowAmounts: false, + })); + + const result = await register(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(400); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.error).toStrictEqual(ApiError.INVALID_PAYLOAD); + expect(returnBody.details).toMatchInlineSnapshot(` +[ + { + "message": "\"pushProvider\" with value \"not-supported-provider\" fails to match the required pattern: /^(?:ios|android)$/", + "path": [ + "pushProvider", + ], + }, +] +`); + }); + + it('should validate deviceId', async () => { + expect.hasAssertions(); + const deviceId = (new Array(257)).fill('x').join(''); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + deviceId, + pushProvider: 'android', + enablePush: true, + enableShowAmounts: false, + })); + + const result = await register(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(400); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.error).toStrictEqual(ApiError.INVALID_PAYLOAD); + expect(returnBody.details).toMatchInlineSnapshot(` +[ + { + "message": "\"deviceId\" length must be less than or equal to 256 characters long", + "path": [ + "deviceId", + ], + }, +] +`); + }); +}); + +describe('statusCode:404', () => { + it('should validate wallet existence', async () => { + expect.hasAssertions(); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + deviceId: 'device1', + pushProvider: 'android', + enablePush: true, + enableShowAmounts: false, + })); + + const result = await register(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(404); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.error).toStrictEqual(ApiError.WALLET_NOT_FOUND); + }); +}); diff --git a/packages/wallet-service/tests/pushSendNotificationToDevice.test.ts b/packages/wallet-service/tests/pushSendNotificationToDevice.test.ts new file mode 100644 index 00000000..04cc0607 --- /dev/null +++ b/packages/wallet-service/tests/pushSendNotificationToDevice.test.ts @@ -0,0 +1,289 @@ +/* eslint-disable global-require */ +import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; +import { logger } from '@tests/winston.mock'; // most be the first to import +import { initFirebaseAdminMock } from '@tests/utils/firebase-admin.mock'; + +import { + send, +} from '@src/api/pushSendNotificationToDevice'; +import { + PushNotificationUtils, + PushNotificationError, +} from '@src/utils/pushnotification.utils'; +import { + register, +} from '@src/api/pushRegister'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { + addToWalletTable, + makeGatewayEventWithAuthorizer, + cleanDatabase, + checkPushDevicesTable, +} from '@tests/utils'; +import { APIGatewayProxyResult, Context } from 'aws-lambda'; +import { Severity } from '@src/types'; + +const mysql = getDbConnection(); + +initFirebaseAdminMock(); +const spyOnSendToFcm = jest.spyOn(PushNotificationUtils, 'sendToFcm'); +const spyOnLoggerError = jest.spyOn(logger, 'error'); + +const initEnv = process.env; + +beforeEach(async () => { + process.env = { + ...initEnv, + PUSH_ALLOWED_PROVIDERS: 'android,ios', + }; + spyOnSendToFcm.mockClear(); + spyOnLoggerError.mockClear(); + jest.resetModules(); // Needed for the AWS.SQS mock, as it is getting cached + await cleanDatabase(mysql); + jest.resetModules(); +}); + +afterAll(async () => { + process.env = initEnv; + await closeDbConnection(mysql); +}); + +const buildEventPayload = (options?) => ({ + deviceId: 'device1', + metadata: { + titleLocKey: 'new_transaction_received_title', + bodyLocKey: 'new_transaction_received_description_without_tokens', + txId: '00e2597222154cf99bfef171e27374e7f35aa7448afae10c15e9f049b95a3e67', + ...options, + }, +}); + +test('send push notification to the right provider', async () => { + expect.hasAssertions(); + spyOnSendToFcm.mockImplementation(() => Promise.resolve({ + success: true, + })); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + deviceId: 'device1', + pushProvider: 'android', + enablePush: true, + enableShowAmounts: false, + })); + await register(event, null, null) as APIGatewayProxyResult; + + const validPayload = buildEventPayload(); + const sendContext = { awsRequestId: '123' } as Context; + + const result = await send(validPayload, sendContext, null) as { success: boolean, message?: string }; + + expect(result.success).toStrictEqual(true); + expect(spyOnSendToFcm).toHaveBeenCalledTimes(1); +}); + +test('should unregister device when invalid device id', async () => { + expect.hasAssertions(); + spyOnSendToFcm.mockImplementation(() => Promise.resolve({ + success: false, + errorMessage: PushNotificationError.INVALID_DEVICE_ID, + })); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + deviceId: 'device1', + pushProvider: 'android', + enablePush: true, + enableShowAmounts: false, + })); + await register(event, null, null) as APIGatewayProxyResult; + await expect( + checkPushDevicesTable(mysql, 1), + ).resolves.toBe(true); + + const validPayload = buildEventPayload(); + const sendContext = { awsRequestId: '123' } as Context; + + const result = await send(validPayload, sendContext, null) as { success: boolean, message?: string }; + + expect(result.success).toStrictEqual(false); + expect(result.message).toStrictEqual('Failed due to invalid device id.'); + expect(spyOnSendToFcm).toHaveBeenCalledTimes(1); + await expect( + checkPushDevicesTable(mysql, 0), + ).resolves.toBe(true); +}); + +describe('validation', () => { + it('should validate deviceId', async () => { + expect.hasAssertions(); + const deviceId = (new Array(257)).fill('x').join(''); + + const payloadWithInvalidDeviceId = { + deviceId, + title: 'You have a new transaction', + description: '5HTR was sent to my-wallet', + metadata: { + txId: '00e2597222154cf99bfef171e27374e7f35aa7448afae10c15e9f049b95a3e67', + }, + }; + const sendEvent = { body: payloadWithInvalidDeviceId }; + const sendContext = { awsRequestId: '123' } as Context; + + const result = await send(sendEvent, sendContext, null) as { success: boolean, message?: string }; + + expect(result.success).toStrictEqual(false); + }); + + it('should validate title', async () => { + expect.hasAssertions(); + + const payloadWithoutTitle = { + deviceId: 'device1', + description: '5HTR was sent to my-wallet', + metadata: { + txId: '00e2597222154cf99bfef171e27374e7f35aa7448afae10c15e9f049b95a3e67', + }, + }; + const sendEvent = { body: payloadWithoutTitle }; + const sendContext = { awsRequestId: '123' } as Context; + + const result = await send(sendEvent, sendContext, null) as { success: boolean, message?: string }; + + expect(result.success).toStrictEqual(false); + }); + + it('should validate description', async () => { + expect.hasAssertions(); + + const payloadWithoutDescription = { + deviceId: 'device1', + title: 'You have a new transaction', + metadata: { + txId: '00e2597222154cf99bfef171e27374e7f35aa7448afae10c15e9f049b95a3e67', + }, + }; + const sendEvent = { body: payloadWithoutDescription }; + const sendContext = { awsRequestId: '123' } as Context; + + const result = await send(sendEvent, sendContext, null) as { success: boolean, message?: string }; + + expect(result.success).toStrictEqual(false); + }); + + it('should validate metadata', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + deviceId: 'device1', + pushProvider: 'android', + enablePush: true, + enableShowAmounts: false, + })); + await register(event, null, null) as APIGatewayProxyResult; + + const payloadWithoutMetadata = { + deviceId: 'device1', + title: 'You have a new transaction', + description: '5HTR was sent to my-wallet', + }; + const sendEvent = { body: payloadWithoutMetadata }; + const sendContext = { awsRequestId: '123' } as Context; + + const result = await send(sendEvent, sendContext, null) as { success: boolean, message?: string }; + + expect(result.success).toStrictEqual(false); + }); +}); + +describe('alert', () => { + it('should alert when push device not found', async () => { + expect.hasAssertions(); + + // skip device registration + + const validPayload = buildEventPayload(); + const sendContext = { awsRequestId: '123' } as Context; + + const result = await send(validPayload, sendContext, null) as { success: boolean, message?: string }; + + expect(result.success).toStrictEqual(false); + expect(result.message).not.toBeNull(); + expect(spyOnLoggerError).toHaveBeenCalledWith('Device not found.', { deviceId: 'device1' }); + expect(mockedAddAlert).toHaveBeenCalledWith( + 'Device not found while trying to send notification', + '-', + Severity.MINOR, + { deviceId: 'device1' }, + ); + }); + + it('should alert when provider not implemented', async () => { + expect.hasAssertions(); + + // allow android and desktop, while test for ios provider + process.env.PUSH_ALLOWED_PROVIDERS = 'android,desktop'; + await import('@src/api/pushSendNotificationToDevice'); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + deviceId: 'device1', + pushProvider: 'ios', + enablePush: true, + enableShowAmounts: false, + })); + await register(event, null, null) as APIGatewayProxyResult; + + const validPayload = buildEventPayload(); + const sendContext = { awsRequestId: '123' } as Context; + + const result = await send(validPayload, sendContext, null) as { success: boolean, message?: string }; + + expect(result.success).toStrictEqual(false); + expect(result.message).not.toBeNull(); + expect(spyOnLoggerError).toHaveBeenCalledWith('Provider invalid.', { deviceId: 'device1', pushProvider: 'ios' }); + expect(mockedAddAlert).toHaveBeenCalledWith( + 'Invalid provider error while sending push notification', + '-', + Severity.MINOR, + { deviceId: 'device1', pushProvider: 'ios' }, + ); + }); +}); diff --git a/packages/wallet-service/tests/pushUnregister.test.ts b/packages/wallet-service/tests/pushUnregister.test.ts new file mode 100644 index 00000000..89f831e4 --- /dev/null +++ b/packages/wallet-service/tests/pushUnregister.test.ts @@ -0,0 +1,245 @@ +import { + unregister, +} from '@src/api/pushUnregister'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { registerPushDevice, unregisterPushDevice } from '@src/db'; +import { + addToWalletTable, + makeGatewayEventWithAuthorizer, + cleanDatabase, + checkPushDevicesTable, +} from '@tests/utils'; +import { APIGatewayProxyResult } from 'aws-lambda'; +import { ApiError } from '@src/api/errors'; + +const mysql = getDbConnection(); + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + +afterAll(async () => { + await closeDbConnection(mysql); +}); + +test('unregister push device given a wallet', async () => { + expect.hasAssertions(); + + // register a wallet + const walletId = 'wallet1'; + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + // register the device to a wallet + const deviceId = 'device1'; + const pushProvider = 'android'; + const enablePush = true; + const enableShowAmounts = false; + await registerPushDevice(mysql, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + }); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId, + pushProvider, + enablePush: true, + enableShowAmounts, + })).resolves.toBe(true); + + const event = makeGatewayEventWithAuthorizer(walletId, { + deviceId, + }, null); + + const result = await unregister(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + + await expect(checkPushDevicesTable(mysql, 0, { + walletId, + deviceId, + pushProvider, + enablePush: true, + enableShowAmounts, + })).resolves.toBe(true); +}); + +describe('statusCode:200', () => { + it('should unregister the right device in face of many', async () => { + expect.hasAssertions(); + + // register wallets + const walletId = 'wallet1'; + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + // register the device to a wallets + const pushProvider = 'android'; + const enablePush = true; + const enableShowAmounts = false; + + const deviceToUnregister = 'device1'; + const deviceToRemain = 'device2'; + const devicesToAdd = [deviceToUnregister, deviceToRemain]; + devicesToAdd.forEach(async (eachDevice) => { + await registerPushDevice(mysql, { + walletId, + deviceId: eachDevice, + pushProvider, + enablePush, + enableShowAmounts, + }); + }); + await expect(checkPushDevicesTable(mysql, 2)).resolves.toBe(true); + + const event = makeGatewayEventWithAuthorizer(walletId, { + deviceId: deviceToUnregister, + }, null); + + const result = await unregister(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId: deviceToRemain, + pushProvider, + enablePush: true, + enableShowAmounts, + })).resolves.toBe(true); + }); + + it('should succeed even when there is no device registered', async () => { + expect.hasAssertions(); + + const walletId = 'wallet1'; + const deviceId = 'device1'; + + const event = makeGatewayEventWithAuthorizer(walletId, { + deviceId, + }, null); + + const result = await unregister(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + + await expect(checkPushDevicesTable(mysql, 0)).resolves.toBe(true); + }); + + it('should succeed even when device id not exists', async () => { + expect.hasAssertions(); + + // register a wallet + const walletId = 'wallet1'; + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + // register the device to a wallet + const deviceId = 'device1'; + const pushProvider = 'android'; + const enablePush = true; + const enableShowAmounts = false; + await registerPushDevice(mysql, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + }); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId, + pushProvider, + enablePush: true, + enableShowAmounts, + })).resolves.toBe(true); + + const event = makeGatewayEventWithAuthorizer(walletId, { + deviceId: 'device-not-exists', + }, null); + + const result = await unregister(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + })).resolves.toBe(true); + }); +}); + +describe('statusCode:400', () => { + it('should validate deviceId', async () => { + expect.hasAssertions(); + const deviceId = (new Array(257)).fill('x').join(''); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const event = makeGatewayEventWithAuthorizer('my-wallet', { + deviceId, + }, null); + + const result = await unregister(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(400); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.error).toStrictEqual(ApiError.INVALID_PAYLOAD); + expect(returnBody.details).toMatchInlineSnapshot(` +[ + { + "message": "\"deviceId\" length must be less than or equal to 256 characters long", + "path": [ + "deviceId", + ], + }, +] +`); + }); +}); diff --git a/packages/wallet-service/tests/pushUpdate.test.ts b/packages/wallet-service/tests/pushUpdate.test.ts new file mode 100644 index 00000000..c427c1da --- /dev/null +++ b/packages/wallet-service/tests/pushUpdate.test.ts @@ -0,0 +1,242 @@ +import { + update, +} from '@src/api/pushUpdate'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { registerPushDevice } from '@src/db'; +import { + addToWalletTable, + makeGatewayEventWithAuthorizer, + cleanDatabase, + checkPushDevicesTable, +} from '@tests/utils'; +import { ApiError } from '@src/api/errors'; +import { APIGatewayProxyResult } from 'aws-lambda'; + +const mysql = getDbConnection(); + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + +afterAll(async () => { + await closeDbConnection(mysql); +}); + +test('update push device given a wallet', async () => { + expect.hasAssertions(); + + // register a wallet + const walletId = 'wallet1'; + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + // register the device to a wallet + const deviceId = 'device1'; + const pushProvider = 'android'; + const enablePush = false; // disabled push notification + const enableShowAmounts = false; + await registerPushDevice(mysql, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + }); + + const event = makeGatewayEventWithAuthorizer(walletId, null, JSON.stringify({ + deviceId, + enablePush: true, // enables push notification + enableShowAmounts: false, + })); + + const result = await update(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId, + pushProvider, + enablePush: true, + enableShowAmounts, + })).resolves.toBe(true); +}); + +describe('statusCode:200', () => { + it('should have default value for enablePush', async () => { + expect.hasAssertions(); + + // register a wallet + const walletId = 'wallet1'; + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + // register the device to a wallet + const deviceId = 'device1'; + const pushProvider = 'android'; + const enablePush = true; // start enabled + const enableShowAmounts = false; + await registerPushDevice(mysql, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + }); + + // enablePush should be disabled by default + const event = makeGatewayEventWithAuthorizer(walletId, null, JSON.stringify({ + deviceId, + enableShowAmounts: false, + })); + + const result = await update(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId, + pushProvider, + enablePush: false, // default value + enableShowAmounts, + })).resolves.toBe(true); + }); + + it('should have default value for enableShowAmounts', async () => { + expect.hasAssertions(); + + // register a wallet + const walletId = 'wallet1'; + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + // register the device to a wallet + const deviceId = 'device1'; + const pushProvider = 'android'; + const enablePush = false; + const enableShowAmounts = true; // start enabled + await registerPushDevice(mysql, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts, + }); + + // enableShowAmounts should be disabled by default + const event = makeGatewayEventWithAuthorizer(walletId, null, JSON.stringify({ + deviceId, + enablePush, + })); + + const result = await update(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + + await expect(checkPushDevicesTable(mysql, 1, { + walletId, + deviceId, + pushProvider, + enablePush, + enableShowAmounts: false, // default value + })).resolves.toBe(true); + }); +}); + +describe('statusCode:400', () => { + it('should validate deviceId', async () => { + expect.hasAssertions(); + const deviceId = (new Array(257)).fill('x').join(''); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + deviceId, + enablePush: false, + enableShowAmounts: false, + })); + + const result = await update(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(400); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.error).toStrictEqual(ApiError.INVALID_PAYLOAD); + expect(returnBody.details).toMatchInlineSnapshot(` +[ + { + "message": "\"deviceId\" length must be less than or equal to 256 characters long", + "path": [ + "deviceId", + ], + }, +] +`); + }); +}); + +describe('statusCode:404', () => { + it('should validate deviceId existence', async () => { + expect.hasAssertions(); + const deviceId = 'device-not-registered'; + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ + deviceId, + enablePush: false, + enableShowAmounts: false, + })); + + const result = await update(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(404); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.error).toStrictEqual(ApiError.DEVICE_NOT_FOUND); + }); +}); diff --git a/packages/wallet-service/tests/redis.test.ts b/packages/wallet-service/tests/redis.test.ts new file mode 100644 index 00000000..fb5d4b6c --- /dev/null +++ b/packages/wallet-service/tests/redis.test.ts @@ -0,0 +1,247 @@ +import { + getRedisClient, + closeRedisClient, + scanAll, + initWsConnection, + endWsConnection, + wsJoinChannel, + wsJoinWallet, + wsGetChannelConnections, + wsGetWalletConnections, + wsGetAllConnections, + wsGetConnection, +} from '@src/redis'; + +import { + redisAddKeys, + redisCleanup, +} from '@tests/utils'; + +import { promisify } from 'util'; + +const client = getRedisClient(); +const getAsync = promisify(client.get).bind(client); +const keysAsync = promisify(client.keys).bind(client); + +beforeEach(() => { + redisCleanup(client); +}); + +afterAll(async () => { + redisCleanup(client); + await closeRedisClient(client); +}); + +test('Redis func: scanAll', async () => { + expect.hasAssertions(); + const keys = { + foo0: '0', + foo1: '1', + foo2: '2', + bar0: '3', + }; + redisAddKeys(client, keys); + const keysFAll = await scanAll(client, 'foo*'); + expect(keysFAll.sort()).toStrictEqual(['foo0', 'foo1', 'foo2'].sort()); + const keysAll0 = await scanAll(client, '*0'); + expect(keysAll0.sort()).toStrictEqual(['foo0', 'bar0'].sort()); + const keysAll1 = await scanAll(client, '*1'); + expect(keysAll1.sort()).toStrictEqual(['foo1']); +}); + +test('initWsConnection', async () => { + expect.hasAssertions(); + await initWsConnection(client, { + id: 'abcd', + url: 'efgh', + }); + await getAsync('walletsvc:conn:abcd').then((val) => { + expect(val).toStrictEqual('efgh'); + }); + // client.get('walletsvc:conn:abcd', (err, reply) => { + // if (err) throw err; + // expect(reply).toStrictEqual('efgh'); + // }); +}); + +test('endWsConnection', async () => { + expect.hasAssertions(); + + const connID = 'abcd'; + const keysToDel = { + 'walletsvc:conn:abcd': '1', + 'walletsvc:chan:foo:abcd': '1', + 'walletsvc:chan:wallet-1234:abcd': '1', + }; + const otherConn = 'efgh'; + const otherKeys = { + 'walletsvc:conn:efgh': '2', + 'walletsvc:chan:foo:efgh': '2', + 'walletsvc:chan:wallet-1234:efgh': '2', + }; + + redisAddKeys(client, keysToDel); + redisAddKeys(client, otherKeys); + + await endWsConnection(client, connID); + // should delete keys of disconnecting client and keep others + await keysAsync('*').then((keys) => { + expect(keys.sort()).toStrictEqual(Object.keys(otherKeys).sort()); + }); + + redisAddKeys(client, { foo: 'bar' }); + await endWsConnection(client, otherConn); + // should NOT affect unrelated keys + await keysAsync('*').then((keys) => { + expect(keys).toStrictEqual(['foo']); + }); +}); + +test('wsJoinWallet', async () => { + expect.hasAssertions(); + + // works the same way as wsJoinChannel, but with a special channel + + const connInfo = { + id: 'abcd', + url: 'http://url.com', + }; + const connKeys = { + 'walletsvc:conn:abcd': '1', + 'walletsvc:chan:foo:abcd': '1', + 'walletsvc:chan:wallet-1234:abcd': '1', + }; + redisAddKeys(client, connKeys); + await wsJoinWallet(client, connInfo, 'bar'); + + // should have the channel bar on connection abcd, and it should equal the url + const chanKey = 'walletsvc:chan:wallet-bar:abcd'; + await getAsync(chanKey).then((url) => { + expect(url).toStrictEqual('http://url.com'); + }); + + // other keys should not be affected + await keysAsync('*').then((keys) => { + expect(keys.sort()).toStrictEqual(Object.keys(connKeys).concat([chanKey]).sort()); + }); +}); + +test('wsJoinChannel', async () => { + expect.hasAssertions(); + + const connInfo = { + id: 'abcd', + url: 'http://url.com', + }; + const connKeys = { + 'walletsvc:conn:abcd': '1', + 'walletsvc:chan:foo:abcd': '1', + 'walletsvc:chan:wallet-1234:abcd': '1', + }; + redisAddKeys(client, connKeys); + await wsJoinChannel(client, connInfo, 'bar'); + + // should have the channel bar on connection abcd, and it should equal the url + const chanKey = 'walletsvc:chan:bar:abcd'; + await getAsync(chanKey).then((url) => { + expect(url).toStrictEqual('http://url.com'); + }); + + // other keys should not be affected + await keysAsync('*').then((keys) => { + expect(keys.sort()).toStrictEqual(Object.keys(connKeys).concat([chanKey]).sort()); + }); +}); + +test('wsGetChannelConnections', async () => { + expect.hasAssertions(); + + const connKeys = { + 'walletsvc:conn:abcd': '1', + 'walletsvc:chan:foo:abcd': '1', + 'walletsvc:chan:bar:abcd': 'url', + 'walletsvc:chan:wallet-1234:abcd': '1', + }; + redisAddKeys(client, connKeys); + const connections = await wsGetChannelConnections(client, 'bar'); + expect(connections).toStrictEqual([{ id: 'abcd', url: 'url' }]); +}); + +test('wsJoinChannel + wsGetChannelConnections', async () => { + expect.hasAssertions(); + + const connInfo = { + id: 'abcd', + url: 'http://url.com', + }; + // initConn + joinChannel should include on channel connections + // maybe include initConnection as needed? + // await initWsConnection(connInfo); + await wsJoinChannel(client, connInfo, 'foo'); + const connections = await wsGetChannelConnections(client, 'foo'); + expect(connections).toStrictEqual([connInfo]); +}); + +test('wsGetWalletConnections', async () => { + expect.hasAssertions(); + + const connKeys = { + 'walletsvc:conn:abcd': '1', + 'walletsvc:chan:foo:abcd': '1', + 'walletsvc:chan:bar:abcd': '1', + 'walletsvc:chan:wallet-1234:abcd': 'url', + }; + redisAddKeys(client, connKeys); + const connections = await wsGetWalletConnections(client, '1234'); + expect(connections).toStrictEqual([{ id: 'abcd', url: 'url' }]); +}); + +test('wsJoinWallet + wsGetWalletConnections', async () => { + expect.hasAssertions(); + + const connInfo = { + id: 'abcd', + url: 'http://url.com', + }; + // initConn + joinWallet should include on wallet connections + // maybe include initConnection as needed? + // await initWsConnection(connInfo); + await wsJoinWallet(client, connInfo, '1234'); + const connections = await wsGetWalletConnections(client, '1234'); + // should we separate id and url checks? + expect(connections).toStrictEqual([connInfo]); +}); + +test('wsGetAllConnections', async () => { + expect.hasAssertions(); + + const connInfos = [ + { id: 'abcd', url: '1' }, + { id: 'efgh', url: '2' }, + ]; + const connKeys = { + 'walletsvc:conn:abcd': '1', + 'walletsvc:conn:efgh': '2', + 'foo:bar': '1', + }; + redisAddKeys(client, connKeys); + const connections = await wsGetAllConnections(client); + // compare ids + expect(connections.map((i) => i.id).sort()).toStrictEqual(connInfos.map((i) => i.id).sort()); + // compare urls + expect(connections.map((i) => i.url).sort()).toStrictEqual(connInfos.map((i) => i.url).sort()); +}); + +test('wsGetConnection', async () => { + expect.hasAssertions(); + + const connInfo = { id: 'abcd', url: '1' }; + const connKeys = { + 'walletsvc:conn:abcd': '1', + 'walletsvc:conn:efgh': '2', + 'foo:bar': '1', + }; + redisAddKeys(client, connKeys); + const connection = await wsGetConnection(client, 'abcd'); + expect(connection).toStrictEqual(connInfo.url); +}); diff --git a/packages/wallet-service/tests/txById.test.ts b/packages/wallet-service/tests/txById.test.ts new file mode 100644 index 00000000..011de406 --- /dev/null +++ b/packages/wallet-service/tests/txById.test.ts @@ -0,0 +1,137 @@ +import { + get, +} from '@src/api/txById'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { addOrUpdateTx, createWallet, initWalletTxHistory } from '@src/db'; +import { + makeGatewayEventWithAuthorizer, + cleanDatabase, + XPUBKEY, + AUTH_XPUBKEY, + addToAddressTxHistoryTable, + addToTokenTable, +} from '@tests/utils'; +import { APIGatewayProxyResult } from 'aws-lambda'; +import { ApiError } from '@src/api/errors'; + +const mysql = getDbConnection(); + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + +afterAll(async () => { + await closeDbConnection(mysql); +}); + +test('get a transaction given its ID', async () => { + expect.hasAssertions(); + const txId1 = new Array(64).fill('0').join(''); + const walletId1 = 'wallet1'; + const addr1 = 'addr1'; + const token1 = { id: 'token1', name: 'Token 1', symbol: 'T1' }; + const token2 = { id: 'token2', name: 'Token 2', symbol: 'T2' }; + const timestamp1 = 10; + const height1 = 1; + const version1 = 3; + const weight1 = 65.4321; + + await createWallet(mysql, walletId1, XPUBKEY, AUTH_XPUBKEY, 5); + await addOrUpdateTx(mysql, txId1, height1, timestamp1, version1, weight1); + + await addToTokenTable(mysql, [ + { id: token1.id, name: token1.name, symbol: token1.symbol, transactions: 0 }, + { id: token2.id, name: token2.name, symbol: token2.symbol, transactions: 0 }, + ]); + const entries = [ + { address: addr1, txId: txId1, tokenId: token1.id, balance: 10, timestamp: timestamp1 }, + { address: addr1, txId: txId1, tokenId: token2.id, balance: 7, timestamp: timestamp1 }, + ]; + await addToAddressTxHistoryTable(mysql, entries); + await initWalletTxHistory(mysql, walletId1, [addr1]); + + const event = makeGatewayEventWithAuthorizer(walletId1, { + txId: txId1, + }, null); + + const result = await get(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + expect(returnBody.txTokens).toHaveLength(2); + expect(returnBody.txTokens).toStrictEqual([ + { + balance: 10, + timestamp: 10, + tokenId: token1.id, + tokenName: token1.name, + tokenSymbol: token1.symbol, + txId: txId1, + version: 3, + voided: false, + weight: 65.4321, + }, + { + balance: 7, + timestamp: 10, + tokenId: token2.id, + tokenName: token2.name, + tokenSymbol: token2.symbol, + txId: txId1, + version: 3, + voided: false, + weight: 65.4321, + }, + ]); +}); + +describe('statusCode:400', () => { + it('should validate txId', async () => { + expect.hasAssertions(); + + const walletId = 'wallet1'; + const event = makeGatewayEventWithAuthorizer(walletId, { + txId: '', // must be string + }, null); + + const result = await get(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(400); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.error).toStrictEqual(ApiError.INVALID_PAYLOAD); + expect(returnBody.details).toMatchInlineSnapshot(` +[ + { + "message": "\"txId\" is not allowed to be empty", + "path": [ + "txId", + ], + }, +] +`); + }); +}); + +describe('statusCode:404', () => { + it('should validate tx existence', async () => { + expect.hasAssertions(); + const txIdNotRegistered = new Array(64).fill('0').join(''); + + await addOrUpdateTx(mysql, 'txId1', 1, 2, 3, 65.4321); + + const walletId = 'wallet1'; + const event = makeGatewayEventWithAuthorizer(walletId, { + txId: txIdNotRegistered, + }, null); + + const result = await get(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(404); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.error).toStrictEqual(ApiError.TX_NOT_FOUND); + expect(returnBody.details).toBeUndefined(); + }); +}); diff --git a/packages/wallet-service/tests/txOutputs.test.ts b/packages/wallet-service/tests/txOutputs.test.ts new file mode 100644 index 00000000..40936992 --- /dev/null +++ b/packages/wallet-service/tests/txOutputs.test.ts @@ -0,0 +1,890 @@ +import { + getFilteredTxOutputs, + getFilteredUtxos, +} from '@src/api/txOutputs'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { + addToUtxoTable, + addToWalletTable, + addToAddressTable, + makeGatewayEventWithAuthorizer, + cleanDatabase, + ADDRESSES, + TX_IDS, +} from '@tests/utils'; +import { ApiError } from '@src/api/errors'; +import { APIGatewayProxyResult } from 'aws-lambda'; + +const mysql = getDbConnection(); + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + +afterAll(async () => { + await closeDbConnection(mysql); +}); + +test('filter utxos api with invalid parameters', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 4, + }, { + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 4, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + + const utxos = [{ + txId: TX_IDS[0], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 50, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[1], + index: 0, + tokenId: token1, + address: ADDRESSES[1], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 0, + tokenId: token1, + address: ADDRESSES[2], + value: 150, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 1, + tokenId: token1, + address: ADDRESSES[3], + value: 200, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + + let event = makeGatewayEventWithAuthorizer('my-wallet', { + biggerThan: 'invalid-parameter', + smallerThan: 'invalid-parameter', + }, null); + + let result = await getFilteredUtxos(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(400); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.details).toHaveLength(2); + expect(returnBody.error).toStrictEqual(ApiError.INVALID_PAYLOAD); + + // Should complain about missing index + + event = makeGatewayEventWithAuthorizer('my-wallet', { + txId: TX_IDS[0], + }, null); + + result = await getFilteredUtxos(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(400); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.details[0].message).toStrictEqual('"value" contains [txId] without its required peers [index]'); + + // tx_output not found should return an empty list + + event = makeGatewayEventWithAuthorizer('my-wallet', { + txId: TX_IDS[3], + index: '0', // queryparams expects a string + }, null); + + result = await getFilteredUtxos(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + expect(returnBody.utxos).toStrictEqual([]); + + // Utxo not from user's wallet + + event = makeGatewayEventWithAuthorizer('my-wallet', { + txId: TX_IDS[2], + index: '1', + }, null); + + result = await getFilteredUtxos(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(403); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.error).toStrictEqual(ApiError.TX_OUTPUT_NOT_IN_WALLET); +}); + +test('filter tx_output api with invalid parameters', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 4, + }, { + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 4, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + + const utxos = [{ + txId: TX_IDS[0], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 50, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[1], + index: 0, + tokenId: token1, + address: ADDRESSES[1], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 0, + tokenId: token1, + address: ADDRESSES[2], + value: 150, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 1, + tokenId: token1, + address: ADDRESSES[3], + value: 200, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + + let event = makeGatewayEventWithAuthorizer('my-wallet', { + biggerThan: 'invalid-parameter', + smallerThan: 'invalid-parameter', + }, null); + + let result = await getFilteredTxOutputs(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(400); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.details).toHaveLength(2); + expect(returnBody.error).toStrictEqual(ApiError.INVALID_PAYLOAD); + + // Should complain about missing index + + event = makeGatewayEventWithAuthorizer('my-wallet', { + txId: TX_IDS[0], + }, null); + + result = await getFilteredTxOutputs(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(400); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.details[0].message).toStrictEqual('"value" contains [txId] without its required peers [index]'); + + event = makeGatewayEventWithAuthorizer('my-wallet', { + txId: TX_IDS[3], + index: '0', // queryparams expects a string + }, null); + + // tx_output not found should return an empty list + + result = await getFilteredTxOutputs(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(200); + expect(returnBody.success).toStrictEqual(true); + expect(returnBody.txOutputs).toStrictEqual([]); + + // Utxo not from user's wallet + + event = makeGatewayEventWithAuthorizer('my-wallet', { + txId: TX_IDS[2], + index: '1', + }, null); + + result = await getFilteredTxOutputs(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toStrictEqual(403); + expect(returnBody.success).toStrictEqual(false); + expect(returnBody.error).toStrictEqual(ApiError.TX_OUTPUT_NOT_IN_WALLET); +}); + +test('get utxos with wallet id', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 4, + }, { + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 4, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + + const utxos = [{ + txId: TX_IDS[0], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 50, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[1], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 0, + tokenId: token1, + address: ADDRESSES[1], + value: 150, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 1, + tokenId: token1, + address: ADDRESSES[0], + value: 200, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + + const event = makeGatewayEventWithAuthorizer('my-wallet', { + tokenId: token1, + biggerThan: '50', + smallerThan: '200', + }, null); + + const result = await getFilteredUtxos(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + const formatUtxo = (utxo, path) => ({ + txId: utxo.txId, + index: utxo.index, + tokenId: utxo.tokenId, + address: utxo.address, + value: utxo.value, + authorities: utxo.authorities, + timelock: utxo.timelock, + heightlock: utxo.heightlock, + locked: utxo.locked, + addressPath: `m/44'/280'/0'/0/${path}`, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.utxos).toHaveLength(2); + expect(returnBody.utxos[0]).toStrictEqual(formatUtxo(utxos[2], 1)); + expect(returnBody.utxos[1]).toStrictEqual(formatUtxo(utxos[1], 0)); +}); + +test('get tx outputs with wallet id', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 4, + }, { + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 4, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + + const utxos = [{ + txId: TX_IDS[0], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 50, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[1], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 0, + tokenId: token1, + address: ADDRESSES[1], + value: 150, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 1, + tokenId: token1, + address: ADDRESSES[0], + value: 200, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + + const event = makeGatewayEventWithAuthorizer('my-wallet', { + tokenId: token1, + biggerThan: '50', + smallerThan: '200', + }, null); + + const result = await getFilteredTxOutputs(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + const formatUtxo = (utxo, path) => ({ + txId: utxo.txId, + index: utxo.index, + tokenId: utxo.tokenId, + address: utxo.address, + value: utxo.value, + authorities: utxo.authorities, + timelock: utxo.timelock, + heightlock: utxo.heightlock, + locked: utxo.locked, + addressPath: `m/44'/280'/0'/0/${path}`, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.txOutputs).toHaveLength(2); + expect(returnBody.txOutputs[0]).toStrictEqual(formatUtxo(utxos[2], 1)); + expect(returnBody.txOutputs[1]).toStrictEqual(formatUtxo(utxos[1], 0)); +}); + +test('get authority utxos', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 4, + }, { + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 4, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + + const utxos = [{ + txId: TX_IDS[0], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 0, + authorities: 1, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[1], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 0, + authorities: 2, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 0, + tokenId: token1, + address: ADDRESSES[1], + value: 0, + authorities: 1, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 1, + tokenId: token1, + address: ADDRESSES[0], + value: 0, + authorities: 1, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[3], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 150, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + + const formatUtxo = (utxo, path) => ({ + txId: utxo.txId, + index: utxo.index, + tokenId: utxo.tokenId, + address: utxo.address, + value: utxo.value, + authorities: utxo.authorities, + timelock: utxo.timelock, + heightlock: utxo.heightlock, + locked: utxo.locked, + addressPath: `m/44'/280'/0'/0/${path}`, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }); + + let event = makeGatewayEventWithAuthorizer('my-wallet', { + tokenId: token1, + authority: '1', // Only mint authorities + }, null); + + let result = await getFilteredTxOutputs(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.txOutputs).toHaveLength(3); + expect(returnBody.txOutputs[0]).toStrictEqual(formatUtxo(utxos[0], 0)); + expect(returnBody.txOutputs[1]).toStrictEqual(formatUtxo(utxos[2], 1)); + expect(returnBody.txOutputs[2]).toStrictEqual(formatUtxo(utxos[3], 0)); + + event = makeGatewayEventWithAuthorizer('my-wallet', { + tokenId: token1, + authority: '3', // Mint and melt authorities + }, null); + + result = await getFilteredTxOutputs(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.txOutputs).toHaveLength(4); +}); + +test('get a specific utxo', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 4, + }, { + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 4, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + + const utxos = [{ + txId: TX_IDS[0], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 50, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[1], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 0, + tokenId: token1, + address: ADDRESSES[1], + value: 150, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 1, + tokenId: token1, + address: ADDRESSES[0], + value: 200, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + + const event = makeGatewayEventWithAuthorizer('my-wallet', { + txId: TX_IDS[0], + index: '0', + }, null); + + const result = await getFilteredTxOutputs(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + const formatUtxo = (utxo, path) => ({ + txId: utxo.txId, + index: utxo.index, + tokenId: utxo.tokenId, + address: utxo.address, + value: utxo.value, + authorities: utxo.authorities, + timelock: utxo.timelock, + heightlock: utxo.heightlock, + locked: utxo.locked, + txProposalId: null, + txProposalIndex: null, + addressPath: `m/44'/280'/0'/0/${path}`, + spentBy: null, + }); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.txOutputs).toHaveLength(1); + expect(returnBody.txOutputs[0]).toStrictEqual(formatUtxo(utxos[0], 0)); +}); + +test('get utxos from addresses that are not my own should fail with ApiError.ADDRESS_NOT_IN_WALLET', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 4, + }, { + address: ADDRESSES[1], + index: 1, + walletId: 'other-wallet', + transactions: 4, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + + const utxos = [{ + txId: TX_IDS[0], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 50, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[1], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 0, + tokenId: token1, + address: ADDRESSES[1], + value: 150, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 1, + tokenId: token1, + address: ADDRESSES[1], + value: 200, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, null, { + addresses: [ADDRESSES[1]], + }); + + const result = await getFilteredTxOutputs(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toStrictEqual(ApiError.ADDRESS_NOT_IN_WALLET); +}); + +test('get spent tx_output', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 4, + }, { + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 4, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + + const txOutputs = [{ + txId: TX_IDS[0], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 50, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[1], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + }]; + + await addToUtxoTable(mysql, txOutputs); + + const event = makeGatewayEventWithAuthorizer('my-wallet', { + tokenId: token1, + skipSpent: 'false', // should include TX_IDS[1] + }, null); + + const result = await getFilteredTxOutputs(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + const formatTxOutput = (txOutput, path) => ({ + ...txOutput, + txProposalIndex: null, + txProposalId: null, + addressPath: `m/44'/280'/0'/0/${path}`, + }); + + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.txOutputs).toHaveLength(2); + expect(returnBody.txOutputs[0]).toStrictEqual(formatTxOutput(txOutputs[1], 0)); + expect(returnBody.txOutputs[1]).toStrictEqual(formatTxOutput(txOutputs[0], 0)); +}); diff --git a/packages/wallet-service/tests/txProcessor.test.ts b/packages/wallet-service/tests/txProcessor.test.ts new file mode 100644 index 00000000..31b63bd1 --- /dev/null +++ b/packages/wallet-service/tests/txProcessor.test.ts @@ -0,0 +1,782 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import firebaseMock from '@tests/utils/firebase-admin.mock'; +import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; +import hathorLib from '@hathor/wallet-lib'; +import eventTemplate from '@events/eventTemplate.json'; +import tokenCreationTx from '@events/tokenCreationTx.json'; +import { + getLatestHeight, + getTokenInformation, + fetchTx, + getTxOutput, + getWalletTxHistory, +} from '@src/db'; +import * as Db from '@src/db'; +import * as txProcessor from '@src/txProcessor'; +import { closeDbConnection, getDbConnection, isAuthority } from '@src/utils'; +import { NftUtils } from '@src/utils/nft.utils'; +import { + XPUBKEY, + AUTH_XPUBKEY, + addToAddressTable, + addToAddressBalanceTable, + addToUtxoTable, + addToWalletTable, + addToWalletBalanceTable, + cleanDatabase, + checkUtxoTable, + checkAddressTable, + checkAddressBalanceTable, + checkAddressTxHistoryTable, + checkWalletBalanceTable, + createOutput, + createInput, + addToAddressTxHistoryTable, + addToWalletTxHistoryTable, +} from '@tests/utils'; +import { getHandlerContext, nftCreationTx } from '@events/nftCreationTx'; +import * as pushNotificationUtils from '@src/utils/pushnotification.utils'; +import * as commons from '@src/commons'; +import { Context } from 'aws-lambda'; +import { StringMap, WalletBalanceValue, Severity } from '@src/types'; +import createDefaultLogger from '@src/logger'; + +const mysql = getDbConnection(); +const blockReward = 6400; +const OLD_ENV = process.env; + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + +beforeAll(async () => { + // modify env so block reward is unlocked after 1 new block (overrides .env file) + jest.resetModules(); + process.env = { ...OLD_ENV }; + process.env.BLOCK_REWARD_LOCK = '1'; + firebaseMock.resetAllMocks(); +}); + +afterAll(async () => { + await closeDbConnection(mysql); + // restore old env + process.env = OLD_ENV; +}); + +/* + * In an unlikely scenario, we can receive a tx spending a UTXO that is still marked as locked. + */ +test('spend "locked" utxo', async () => { + expect.hasAssertions(); + + const txId1 = 'txId1'; + const txId2 = 'txId2'; + const token = 'tokenId'; + const addr = 'address'; + const walletId = 'walletId'; + const timelock = 1000; + const maxGap = parseInt(process.env.MAX_ADDRESS_GAP, 10); + + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + status: 'ready', + maxGap: 10, + createdAt: 1, + readyAt: 2, + }]); + + // we received a tx that has timelock + await addToUtxoTable(mysql, [{ + txId: txId1, + index: 0, + tokenId: token, + address: addr, + value: 2500, + authorities: 0, + timelock, + heightlock: null, + locked: true, + spentBy: null, + }]); + + await addToAddressTable(mysql, [ + { address: addr, index: 0, walletId, transactions: 1 }, + ]); + + await addToAddressBalanceTable(mysql, [ + [addr, token, 0, 2500, timelock, 1, 0, 0, 2500], + ]); + + await addToWalletBalanceTable(mysql, [{ + walletId, + tokenId: token, + unlockedBalance: 0, + lockedBalance: 2500, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: timelock, + transactions: 1, + }]); + + // let's now receive a tx that spends this utxo, while it's still marked as locked + const evt = JSON.parse(JSON.stringify(eventTemplate)); + const tx = evt.Records[0].body; + tx.version = 1; + tx.tx_id = txId2; + tx.timestamp += timelock + 1; + tx.inputs = [createInput(2500, addr, txId1, 0, token)]; + tx.outputs = [ + createOutput(0, 2000, addr, token), // one output to the same address + createOutput(1, 500, 'other', token), // and one to another address + ]; + await txProcessor.onNewTxEvent(evt); + + await expect(checkUtxoTable(mysql, 2, txId2, 0, token, addr, 2000, 0, null, null, false)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 2, txId2, 1, token, 'other', 500, 0, null, null, false)).resolves.toBe(true); + await expect(checkAddressTable(mysql, maxGap + 2, addr, 0, walletId, 2)).resolves.toBe(true); + await expect(checkAddressTable(mysql, maxGap + 2, 'other', null, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 2, addr, token, 2000, 0, null, 2)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 2, 'other', token, 500, 0, null, 1)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 2000, 0, null, 2)).resolves.toBe(true); +}); + +test('Genesis transactions should throw', async () => { + expect.hasAssertions(); + + const evt = JSON.parse(JSON.stringify(eventTemplate)); + const tx = evt.Records[0].body; + + tx.inputs = []; + tx.outputs = []; + tx.parents = []; + + process.env.NETWORK = 'mainnet'; + + tx.tx_id = txProcessor.IGNORE_TXS.mainnet[0]; + + await expect(() => txProcessor.onNewTxEvent(evt)).rejects.toThrow('Rejecting tx as it is part of the genesis transactions.'); + + tx.tx_id = txProcessor.IGNORE_TXS.mainnet[1]; + + await expect(() => txProcessor.onNewTxEvent(evt)).rejects.toThrow('Rejecting tx as it is part of the genesis transactions.'); + + tx.tx_id = txProcessor.IGNORE_TXS.mainnet[2]; + + await expect(() => txProcessor.onNewTxEvent(evt)).rejects.toThrow('Rejecting tx as it is part of the genesis transactions.'); + + process.env.NETWORK = 'testnet'; + + tx.tx_id = txProcessor.IGNORE_TXS.testnet[0]; + + await expect(() => txProcessor.onNewTxEvent(evt)).rejects.toThrow('Rejecting tx as it is part of the genesis transactions.'); + + tx.tx_id = txProcessor.IGNORE_TXS.testnet[1]; + + await expect(() => txProcessor.onNewTxEvent(evt)).rejects.toThrow('Rejecting tx as it is part of the genesis transactions.'); + + tx.tx_id = txProcessor.IGNORE_TXS.testnet[2]; + + await expect(() => txProcessor.onNewTxEvent(evt)).rejects.toThrow('Rejecting tx as it is part of the genesis transactions.'); +}); + +/* + * receive some transactions and blocks and make sure database is correct + */ +test('txProcessor', async () => { + expect.hasAssertions(); + const blockRewardLock = parseInt(process.env.BLOCK_REWARD_LOCK, 10); + + // receive a block + const evt = JSON.parse(JSON.stringify(eventTemplate)); + const block = evt.Records[0].body; + block.version = 0; + block.tx_id = 'txId1'; + block.height = 1; + block.inputs = []; + block.outputs = [createOutput(0, blockReward, 'address1')]; + await txProcessor.onNewTxEvent(evt); + // check databases + await expect(checkUtxoTable(mysql, 1, 'txId1', 0, '00', 'address1', blockReward, 0, null, block.height + blockRewardLock, true)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 1, 'address1', null, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, 'address1', '00', 0, blockReward, null, 1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 1, 'address1', 'txId1', '00', blockReward, block.timestamp)).resolves.toBe(true); + expect(await getLatestHeight(mysql)).toBe(block.height); + + // receive another block, for the same address + block.tx_id = 'txId2'; + block.timestamp += 10; + block.height += 1; + await txProcessor.onNewTxEvent(evt); + // we now have 2 blocks, still only 1 address + await expect(checkUtxoTable(mysql, 2, 'txId2', 0, '00', 'address1', blockReward, 0, null, block.height + blockRewardLock, true)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 1, 'address1', null, null, 2)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, 'address1', '00', blockReward, blockReward, null, 2)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 2, 'address1', 'txId2', '00', blockReward, block.timestamp)).resolves.toBe(true); + expect(await getLatestHeight(mysql)).toBe(block.height); + + // receive another block, for a different address + block.tx_id = 'txId3'; + block.timestamp += 10; + block.height += 1; + block.outputs = [createOutput(0, blockReward, 'address2')]; + await txProcessor.onNewTxEvent(evt); + // we now have 3 blocks and 2 addresses + await expect(checkUtxoTable(mysql, 3, 'txId3', 0, '00', 'address2', blockReward, 0, null, block.height + blockRewardLock, true)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 2, 'address2', null, null, 1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 3, 'address2', 'txId3', '00', blockReward, block.timestamp)).resolves.toBe(true); + // new block reward is locked + await expect(checkAddressBalanceTable(mysql, 2, 'address2', '00', 0, blockReward, null, 1)).resolves.toBe(true); + // address1's balance is all unlocked now + await expect(checkAddressBalanceTable(mysql, 2, 'address1', '00', 2 * blockReward, 0, null, 2)).resolves.toBe(true); + expect(await getLatestHeight(mysql)).toBe(block.height); + + // spend first block to 2 other addresses + const tx = evt.Records[0].body; + tx.version = 1; + tx.tx_id = 'txId4'; + tx.timestamp += 10; + tx.inputs = [createInput(blockReward, 'address1', 'txId1', 0)]; + tx.outputs = [ + createOutput(0, 5, 'address3'), + createOutput(1, blockReward - 5, 'address4'), + ]; + await txProcessor.onNewTxEvent(evt); + expect(await getLatestHeight(mysql)).toBe(block.height); + for (const [index, output] of tx.outputs.entries()) { + const { token, decoded, value } = output; + // we now have 4 utxos (had 3, 2 added and 1 removed) + await expect(checkUtxoTable(mysql, 4, tx.tx_id, index, token, decoded.address, value, 0, decoded.timelock, null, false)).resolves.toBe(true); + // the 2 addresses on the outputs have been added to the address table, with null walletId and index + await expect(checkAddressTable(mysql, 4, decoded.address, null, null, 1)).resolves.toBe(true); + // there are 4 different addresses with some balance + await expect(checkAddressBalanceTable(mysql, 4, decoded.address, token, value, 0, null, 1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 6, decoded.address, tx.tx_id, token, value, tx.timestamp)).resolves.toBe(true); + } + for (const input of tx.inputs) { + const { decoded, token, value } = input; + // the input will have a negative amount in the address_tx_history table + await expect(checkAddressTxHistoryTable(mysql, 6, decoded.address, tx.tx_id, token, (-1) * value, tx.timestamp)).resolves.toBe(true); + } + // address1 balance has decreased + await expect(checkAddressBalanceTable(mysql, 4, 'address1', '00', blockReward, 0, null, 3)).resolves.toBe(true); + // address2 balance is still locked + await expect(checkAddressBalanceTable(mysql, 4, 'address2', '00', 0, blockReward, null, 1)).resolves.toBe(true); +}); + +test('txProcessor should be able to re-process txs that were voided in the past', async () => { + expect.hasAssertions(); + + const walletId = 'walletId'; + const txId = 'txId1'; + const address = 'address1'; + const tokenId = '00'; + + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + status: 'ready', + maxGap: 10, + createdAt: 1, + readyAt: 2, + }]); + + await addToAddressTable(mysql, [ + { address, index: 0, walletId, transactions: 1 }, + ]); + + // receive a block + const evt = JSON.parse(JSON.stringify(eventTemplate)); + const block = evt.Records[0].body; + const blockUtxo = createOutput(0, blockReward, address); + block.version = 0; + block.tx_id = txId; + block.height = 1; + block.inputs = []; + block.outputs = [blockUtxo]; + + await txProcessor.onNewTxEvent(evt); + + const logger = createDefaultLogger(); + + // void it + const transaction = await fetchTx(mysql, txId); + await commons.handleVoided(mysql, logger, transaction); + + // call it again with the same tx + await txProcessor.onNewTxEvent(evt); + + expect(await getTxOutput(mysql, txId, 0, false)).toStrictEqual({ + txId, + index: 0, + tokenId, + address, + value: blockReward, + authorities: 0, + timelock: null, + heightlock: 2, + locked: true, + txProposalId: null, + txProposalIndex: null, + spentBy: null, + }); + + expect(await getWalletTxHistory(mysql, walletId, tokenId, 0, 10)).toStrictEqual([ + { + txId: 'txId1', + timestamp: expect.anything(), + voided: 0, + balance: 6400, + version: 0, + }, + ]); + + expect(await checkAddressTxHistoryTable( + mysql, + 1, + address, + txId, + tokenId, + blockReward, + block.timestamp, + )).toStrictEqual(true); +}); + +test('txProcessor should ignore NFT outputs', async () => { + expect.hasAssertions(); + + const txId1 = 'txId1'; + const txId2 = 'txId2'; + const addr = 'address'; + const walletId = 'walletId'; + const timelock = 1000; + + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + status: 'ready', + maxGap: 10, + createdAt: 1, + readyAt: 2, + }]); + + await addToUtxoTable(mysql, [{ + txId: txId1, + index: 0, + tokenId: '00', + address: addr, + value: 41, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]); + + await addToAddressTable(mysql, [ + { address: addr, index: 0, walletId, transactions: 1 }, + ]); + + await addToAddressBalanceTable(mysql, [ + [addr, '00', 41, 0, null, 1, 0, 0, 41], + ]); + + await addToAddressTxHistoryTable(mysql, [ + { address: addr, txId: txId1, tokenId: '00', balance: 41, timestamp: 0 }, + ]); + + await addToWalletBalanceTable(mysql, [{ + walletId, + tokenId: '00', + unlockedBalance: 41, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + const evt = JSON.parse(JSON.stringify(eventTemplate)); + const tx = evt.Records[0].body; + tx.version = 1; + tx.tx_id = txId2; + tx.timestamp += timelock + 1; + tx.inputs = [createInput(41, addr, txId1, 0, '00')]; + const invalidScriptOutput = createOutput(0, 1, addr, '00'); + tx.outputs = [ + { + ...invalidScriptOutput, + index: null, + decoded: null, + }, + createOutput(1, 39, addr, '00'), + ]; + await txProcessor.onNewTxEvent(evt); + // check databases + await expect(checkUtxoTable(mysql, 1, txId2, 1, '00', addr, 39, 0, null, null, false)).resolves.toBe(true); +}); + +describe('NFT metadata updating', () => { + const spyUpdateMetadata = jest.spyOn(NftUtils, '_updateMetadata'); + + afterEach(() => { + spyUpdateMetadata.mockReset(); + }); + + afterAll(() => { + // Clear mocks + spyUpdateMetadata.mockRestore(); + }); + + it('should reject a call for a missing mandatory parameter', async () => { + expect.hasAssertions(); + + spyUpdateMetadata.mockImplementation(async () => ({ updated: 'ok' })); + + await expect(txProcessor.onNewNftEvent( + { nftUid: '' }, + getHandlerContext(), + () => '', + )).rejects.toThrow('Missing mandatory parameter nftUid'); + expect(spyUpdateMetadata).toHaveBeenCalledTimes(0); + }); + + it('should request update with minimum NFT data', async () => { + expect.hasAssertions(); + + spyUpdateMetadata.mockImplementation(async () => ({ updated: 'ok' })); + + const result = await txProcessor.onNewNftEvent( + { nftUid: nftCreationTx.tx_id }, + getHandlerContext(), + () => '', + ); + expect(spyUpdateMetadata).toHaveBeenCalledTimes(1); + expect(spyUpdateMetadata).toHaveBeenCalledWith(nftCreationTx.tx_id, { id: nftCreationTx.tx_id, nft: true }); + expect(result).toStrictEqual({ success: true }); + }); + + it('should return a standardized message on nft validation failure', async () => { + expect.hasAssertions(); + + const spyCreateOrUpdate = jest.spyOn(NftUtils, 'createOrUpdateNftMetadata'); + spyCreateOrUpdate.mockImplementation(() => { + throw new Error('Failure on validation'); + }); + + const result = await txProcessor.onNewNftEvent( + { nftUid: nftCreationTx.tx_id }, + getHandlerContext(), + () => '', + ); + + const expectedResult = { + success: false, + message: `onNewNftEvent failed for token ${nftCreationTx.tx_id}`, + }; + expect(result).toStrictEqual(expectedResult); + expect(spyCreateOrUpdate).toHaveBeenCalledWith(nftCreationTx.tx_id); + + spyCreateOrUpdate.mockReset(); + spyCreateOrUpdate.mockRestore(); + }); +}); + +test('receive token creation tx', async () => { + expect.hasAssertions(); + + // we must already have a tx to be used for deposit + await addToUtxoTable(mysql, [{ + txId: tokenCreationTx.inputs[0].tx_id, + index: tokenCreationTx.inputs[0].index, + tokenId: tokenCreationTx.inputs[0].token, + address: tokenCreationTx.inputs[0].decoded.address, + value: tokenCreationTx.inputs[0].value, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]); + await addToAddressBalanceTable(mysql, [[tokenCreationTx.inputs[0].decoded.address, + tokenCreationTx.inputs[0].token, tokenCreationTx.inputs[0].value, 0, null, 1, 0, 0, tokenCreationTx.inputs[0].value]]); + + // receive event + const evt = JSON.parse(JSON.stringify(eventTemplate)); + evt.Records[0].body = tokenCreationTx; + await txProcessor.onNewTxEvent(evt); + + for (const [index, output] of tokenCreationTx.outputs.entries()) { + let value = output.value; + let authorities = 0; + if (isAuthority(output.token_data)) { // eslint-disable-line no-bitwise + authorities = value; + value = 0; + } + const { token } = output; + const { address, timelock } = output.decoded; + const length = tokenCreationTx.outputs.length; + const transactions = index === 0 ? 2 : 1; // this address already has the first tx received + await expect( + checkUtxoTable(mysql, length, tokenCreationTx.tx_id, index, token, address, value, authorities, timelock, null, false), + ).resolves.toBe(true); + + await expect(checkAddressBalanceTable(mysql, length, address, token, value, 0, null, transactions, authorities, 0)).resolves.toBe(true); + } + const tokenInfo = await getTokenInformation(mysql, tokenCreationTx.tx_id); + expect(tokenInfo.id).toBe(tokenCreationTx.tx_id); + expect(tokenInfo.name).toBe(tokenCreationTx.token_name); + expect(tokenInfo.symbol).toBe(tokenCreationTx.token_symbol); +}); + +test('onHandleVoidedTxRequest', async () => { + expect.hasAssertions(); + + const txId1 = 'txId1'; + const txId2 = 'txId2'; + const txId3 = 'txId3'; + const token = 'tokenId'; + const addr = 'address'; + const walletId = 'walletId'; + const timelock = 1000; + + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + status: 'ready', + maxGap: 10, + createdAt: 1, + readyAt: 2, + }]); + + await addToUtxoTable(mysql, [{ + txId: txId1, + index: 0, + tokenId: token, + address: addr, + value: 2500, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]); + + await addToAddressTable(mysql, [ + { address: addr, index: 0, walletId, transactions: 1 }, + ]); + + await addToAddressBalanceTable(mysql, [ + [addr, token, 2500, 0, null, 1, 0, 0, 2500], + ]); + + await addToAddressTxHistoryTable(mysql, [ + { address: addr, txId: txId1, tokenId: token, balance: 2500, timestamp: 0 }, + ]); + + await addToWalletBalanceTable(mysql, [{ + walletId, + tokenId: token, + unlockedBalance: 2500, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + const evt = JSON.parse(JSON.stringify(eventTemplate)); + const tx = evt.Records[0].body; + tx.version = 1; + tx.tx_id = txId2; + tx.timestamp += timelock + 1; + tx.inputs = [createInput(2500, addr, txId1, 0, token)]; + tx.outputs = [ + createOutput(0, 2000, addr, token), // one output to the same address + createOutput(1, 500, 'other', token), // and one to another address + ]; + + // Adds txId2 that spends the utxo with index 0 from txId1 + await txProcessor.onNewTxEvent(evt); + + const evt2 = JSON.parse(JSON.stringify(eventTemplate)); + const tx2 = evt2.Records[0].body; + tx2.version = 1; + tx2.tx_id = txId3; + tx2.timestamp += 1; + tx2.inputs = [createInput(2000, addr, txId2, 0, token)]; + tx2.outputs = [ + createOutput(0, 1500, addr, token), // one output to the same address + createOutput(1, 500, 'other', token), // and one to another address + ]; + + // Adds txId3 that spends the utxo with index 0 from txId2 + await txProcessor.onNewTxEvent(evt2); + + // Balance for addr should be 1500 and it should have 3 transactions (txId1, txId2 and txId3) + await expect(checkAddressBalanceTable(mysql, 2, addr, token, 1500, 0, null, 3)).resolves.toBe(true); + + // Voids the first transaction (txId2), causing txId3 to be voided as well, + // as it spends utxos from txId2 + await txProcessor.handleVoidedTx(tx); + + // both utxos should be voided + await expect(checkUtxoTable(mysql, 5, txId2, 0, token, addr, 2000, 0, null, null, false, null, true)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 5, txId2, 1, token, 'other', 500, 0, null, null, false, null, true)).resolves.toBe(true); + + // txId3 will be voided because txId2 was voided and it spends its utxo + await expect(checkUtxoTable(mysql, 5, txId3, 0, token, addr, 1500, 0, null, null, false, null, true)).resolves.toBe(true); + + // the original utxo (txId1, 0) should not be voided and should not have been spent + await expect(checkUtxoTable(mysql, 5, txId1, 0, token, addr, 2500, 0, null, null, false, null, false)).resolves.toBe(true); + + // Balance should be back to 2500 as the transactions that spent the original utxo were voided and we should + // have total of one transaction as both txId2 and txId3 were voided. + await expect(checkAddressBalanceTable(mysql, 2, addr, token, 2500, 0, null, 1)).resolves.toBe(true); +}, 20000); + +test('txProcessor should rollback the entire transaction if an error occurs on balance calculation', async () => { + expect.hasAssertions(); + const blockRewardLock = parseInt(process.env.BLOCK_REWARD_LOCK, 10); + + // receive a block + const evt = JSON.parse(JSON.stringify(eventTemplate)); + const block = evt.Records[0].body; + block.version = 0; + block.tx_id = 'txId1'; + block.height = 1; + block.inputs = []; + block.outputs = [createOutput(0, blockReward, 'address1')]; + await txProcessor.onNewTxEvent(evt); + + // check databases + await expect(checkUtxoTable(mysql, 1, 'txId1', 0, '00', 'address1', blockReward, 0, null, block.height + blockRewardLock, true)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 1, 'address1', null, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 1, 'address1', '00', 0, blockReward, null, 1)).resolves.toBe(true); + await expect(checkAddressTxHistoryTable(mysql, 1, 'address1', 'txId1', '00', blockReward, block.timestamp)).resolves.toBe(true); + expect(await getLatestHeight(mysql)).toBe(block.height); + + // receive another block, for the same address and make it fail so it will rollback the entire transaction + block.tx_id = 'txId2'; + block.timestamp += 10; + block.height = 2; + + const spy = jest.spyOn(Db, 'unlockUtxos'); + spy.mockImplementationOnce(() => { + throw new Error('unlock-utxos-error'); + }); + + await expect(() => txProcessor.onNewTxEvent(evt)).rejects.toThrow('unlock-utxos-error'); + + let latestHeight = await getLatestHeight(mysql); + + // last transaction should have been rolled back and latest height will be the first successful block's height + expect(latestHeight).toBe(block.height - 1); + + // send again should work (we are using mockImplementationOnce) + await txProcessor.onNewTxEvent(evt); + latestHeight = await getLatestHeight(mysql); + expect(latestHeight).toBe(block.height); + + // test subsequent calls + block.tx_id = 'txId3'; + block.timestamp += 10; + block.height = 3; + await txProcessor.onNewTxEvent(evt); + block.tx_id = 'txId4'; + block.timestamp += 10; + block.height = 4; + await txProcessor.onNewTxEvent(evt); + block.tx_id = 'txId5'; + block.timestamp += 10; + block.height = 5; + await txProcessor.onNewTxEvent(evt); + + latestHeight = await getLatestHeight(mysql); + expect(latestHeight).toBe(block.height); + + // Send another one that will also rollback + spy.mockImplementationOnce(() => { + throw new Error('unlock-utxos-error'); + }); + block.tx_id = 'txId6'; + block.timestamp += 10; + block.height = 6; + await expect(() => txProcessor.onNewTxEvent(evt)).rejects.toThrow('unlock-utxos-error'); + + latestHeight = await getLatestHeight(mysql); + expect(latestHeight).toBe(block.height - 1); + + // finally, test the balances + await expect(checkUtxoTable(mysql, 5, 'txId2', 0, '00', 'address1', blockReward, 0, null, 2 + blockRewardLock, false)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 5, 'txId3', 0, '00', 'address1', blockReward, 0, null, 3 + blockRewardLock, false)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 5, 'txId4', 0, '00', 'address1', blockReward, 0, null, 4 + blockRewardLock, false)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 5, 'txId5', 0, '00', 'address1', blockReward, 0, null, 5 + blockRewardLock, true)).resolves.toBe(true); + await expect(checkAddressTable(mysql, 1, 'address1', null, null, 5)).resolves.toBe(true); + // txId5 is locked, so our address balance will be 25600 + await expect(checkAddressBalanceTable(mysql, 1, 'address1', '00', blockReward * 4, blockReward, null, 5)).resolves.toBe(true); +}); + +test('txProcess onNewTxRequest with push notification', async () => { + expect.hasAssertions(); + + const fakeEvent = JSON.parse(JSON.stringify(eventTemplate)).Records[0]; + const fakeContext = { + awsRequestId: 'requestId', + } as unknown as Context; + const fakeWalletBalanceValue = { 123: { txId: 'txId' } } as unknown as StringMap; + + const addNewTxMock = jest.spyOn(txProcessor, 'addNewTx'); + const isTransactionNFTCreationMock = jest.spyOn(NftUtils, 'isTransactionNFTCreation'); + const isPushNotificationEnabledMock = jest.spyOn(pushNotificationUtils, 'isPushNotificationEnabled'); + const getWalletBalancesForTxMock = jest.spyOn(commons, 'getWalletBalancesForTx'); + const invokeOnTxPushNotificationRequestedLambdaMock = jest.spyOn(pushNotificationUtils.PushNotificationUtils, 'invokeOnTxPushNotificationRequestedLambda'); + + /** + * Push notification disabled + */ + addNewTxMock.mockImplementation(() => Promise.resolve()); + isTransactionNFTCreationMock.mockReturnValue(false); + isPushNotificationEnabledMock.mockReturnValue(false); + + await txProcessor.onNewTxRequest(fakeEvent, fakeContext, null); + + expect(invokeOnTxPushNotificationRequestedLambdaMock).toHaveBeenCalledTimes(0); + + /** + * Push notification enabled + */ + isPushNotificationEnabledMock.mockReturnValue(true); + // Get a valid wallet balance value to invoke push notification lambda + getWalletBalancesForTxMock.mockResolvedValue(fakeWalletBalanceValue); + invokeOnTxPushNotificationRequestedLambdaMock.mockResolvedValue(); + + await txProcessor.onNewTxRequest(fakeEvent, fakeContext, null); + + expect(invokeOnTxPushNotificationRequestedLambdaMock).toHaveBeenCalledTimes(1); +}); + +test('onNewTxRequest should send alert on SQS on failure', async () => { + expect.hasAssertions(); + + const addNewTxSpy = jest.spyOn(txProcessor, 'addNewTx'); + addNewTxSpy.mockImplementationOnce(() => Promise.reject(new Error('error'))); + + const fakeEvent = JSON.parse(JSON.stringify(eventTemplate)).Records[0]; + const fakeContext = { + awsRequestId: 'requestId', + } as unknown as Context; + + await txProcessor.onNewTxRequest(fakeEvent, fakeContext, null); + + expect(mockedAddAlert).toHaveBeenCalledWith( + 'Error on onNewTxRequest', + 'Erroed on onNewTxRequest lambda', + Severity.MINOR, + { TxId: null, error: 'error' }, + ); +}); diff --git a/packages/wallet-service/tests/txProposal.test.ts b/packages/wallet-service/tests/txProposal.test.ts new file mode 100644 index 00000000..c618722e --- /dev/null +++ b/packages/wallet-service/tests/txProposal.test.ts @@ -0,0 +1,1840 @@ +import { create as txProposalCreate, checkMissingUtxos } from '@src/api/txProposalCreate'; +import { send as txProposalSend } from '@src/api/txProposalSend'; +import { destroy as txProposalDestroy } from '@src/api/txProposalDestroy'; +import { + getTxProposal, + getUtxos, + updateTxProposal, + updateVersionData, +} from '@src/db'; +import { TxProposalStatus, IWalletInput, DbTxOutput } from '@src/types'; +import { closeDbConnection, getDbConnection, getUnixTimestamp } from '@src/utils'; +import { + addToWalletBalanceTable, + addToTxProposalTable, + addToAddressTable, + addToWalletTable, + addToUtxoTable, + makeGatewayEventWithAuthorizer, + cleanDatabase, + ADDRESSES, + TX_IDS, + addToVersionDataTable, +} from '@tests/utils'; +import { APIGatewayProxyResult } from 'aws-lambda'; + +import { ApiError } from '@src/api/errors'; + +import hathorLib from '@hathor/wallet-lib'; +import CreateTokenTransaction from '@hathor/wallet-lib/lib/models/create_token_transaction'; + +const defaultDerivationPath = `m/44'/${hathorLib.constants.HATHOR_BIP44_CODE}'/0'/0/`; + +const mysql = getDbConnection(); + +beforeEach(async () => { + await cleanDatabase(mysql); + const now = getUnixTimestamp(); + + const versionData = { + timestamp: now, + version: '0.38.4', + network: process.env.NETWORK, + minWeight: 8, + minTxWeight: 8, + minTxWeightCoefficient: 0, + minTxWeightK: 0, + tokenDepositPercentage: 0.01, + rewardSpendMinBlocks: 300, + maxNumberInputs: 255, + maxNumberOutputs: 255, + }; + + await addToVersionDataTable(mysql, versionData); +}); + +afterAll(async () => { + await closeDbConnection(mysql); +}); + +const _checkTxProposalTables = async (txProposalId, inputs): Promise => { + const utxos = await getUtxos(mysql, inputs); + for (const utxo of utxos) { + expect(utxo.txProposalId).toBe(txProposalId); + } + expect(await getTxProposal(mysql, txProposalId)).not.toBeNull(); +}; + +test('POST /txproposals with null as param should fail with ApiError.INVALID_PAYLOAD', async () => { + expect.hasAssertions(); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, null); + const result = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); +}); + +test('POST /txproposals with utxos that are already used on another txproposal should fail with ApiError.INPUTS_ALREADY_USED', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; + + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '0000001e39bc37fe8710c01cc1e8c0a937bf6f9337551fbbfddc222bfc28c197', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '00000060a25077e48926bcd9473d77259296e123ec6af1c1a16c1c381093ab90', + index: 0, + tokenId: token2, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token1', + unlockedBalance: 400, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 2, + }, { + walletId: 'my-wallet', + tokenId: 'token2', + unlockedBalance: 300, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 0, + }]); + + // only one output, spending the whole 300 utxo of token1 + const p2pkhAddress = new hathorLib.P2PKH(new hathorLib.Address(ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + })).createScript(); + + const outputs = [ + new hathorLib.Output( + 300, + p2pkhAddress, { + tokenData: 1, + }, + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs, { tokens: [token1] }); + + const txHex = transaction.toHex(); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const result = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(201); + expect(returnBody.success).toBe(true); + expect(returnBody.txProposalId).toHaveLength(36); + expect(returnBody.inputs).toHaveLength(1); + expect(returnBody.inputs).toContainEqual({ txId: utxos[0].txId, index: utxos[0].index, addressPath: `${defaultDerivationPath}0` }); + + // Send the same tx (same txHex) again + const usedInputsEvent = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const usedInputsResult = await txProposalCreate(usedInputsEvent, null, null) as APIGatewayProxyResult; + const usedInputsReturnBody = JSON.parse(usedInputsResult.body as string); + + expect(usedInputsReturnBody.success).toBe(false); + expect(usedInputsReturnBody.error).toBe(ApiError.INPUTS_ALREADY_USED); +}); + +test('POST /txproposals with too many outputs should fail with ApiError.TOO_MANY_OUTPUTS', async () => { + expect.hasAssertions(); + + const now = getUnixTimestamp(); + + await updateVersionData(mysql, { + timestamp: now, + version: '0.38.4', + network: process.env.NETWORK, + minWeight: 8, + minTxWeight: 8, + minTxWeightCoefficient: 0, + minTxWeightK: 0, + tokenDepositPercentage: 0.01, + rewardSpendMinBlocks: 300, + maxNumberInputs: 255, + maxNumberOutputs: 2, // mocking to force a failure + }); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; + + const utxos = [ + ['004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', 0, token1, ADDRESSES[0], 300, 0, null, null, false], + ['0000001e39bc37fe8710c01cc1e8c0a937bf6f9337551fbbfddc222bfc28c197', 0, token1, ADDRESSES[0], 100, 0, null, null, false], + ['00000060a25077e48926bcd9473d77259296e123ec6af1c1a16c1c381093ab90', 0, token2, ADDRESSES[0], 300, 0, null, null, false], + ]; + + const outputs = [...Array(10).keys()].map(() => ( + new hathorLib.Output(300, new hathorLib.P2PKH(new hathorLib.Address(ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + })).createScript(), { + tokenData: 1, + }) + )); + + const inputs = [new hathorLib.Input(utxos[0][0], utxos[0][1])]; + const transaction = new hathorLib.Transaction(inputs, outputs, { tokens: [token1] }); + + const txHex = transaction.toHex(); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const result = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.TOO_MANY_OUTPUTS); +}); + +test('POST /txproposals with a wallet that is not ready should fail with ApiError.WALLET_NOT_READY', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'not-ready-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'creating', + maxGap: 5, + createdAt: 10000, + readyAt: null, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'not-ready-wallet', + transactions: 2, + }]); + + const utxos = [{ + txId: 'txSuccess0', + index: 0, + tokenId: 'token1', + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: 'txSuccess1', + index: 0, + tokenId: 'token1', + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: 'txSuccess2', + index: 0, + tokenId: 'token2', + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token1', + unlockedBalance: 400, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 2, + }, { + walletId: 'my-wallet', + tokenId: 'token2', + unlockedBalance: 300, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 0, + }]); + + const event = makeGatewayEventWithAuthorizer('not-ready-wallet', null, JSON.stringify({ txHex: '0001000102006f1ebedd590bb5db5c71adbdeaa9b15f7f75c6257c26b11781dc1a5b20f83300006a473045022100fd6b496012c0db9f7300f2e399cfd2706e85f294e4a9195583df35174496a27d022007f3ea316c74a4f61719d2eff347dd4a88d7041fe7f7251514a38b66c0de097c2102b31636b7f35a6cbb42a2053554314a4ca808b7c4840dcc306060a5e7a3ae1b2b0000006400001976a91482965a89ed19afbc81ad0fc82861ffea3e6c591b88ac0001863b00001976a9140f101f6734e10ad87d305cf5af679e3362a659f488ac40200000218def4160dcc4660200b584c970b3597d59f3d3b8bf52c4928c6ce25604fe3488467d3f2c0f4dd6e2006f1ebedd590bb5db5c71adbdeaa9b15f7f75c6257c26b11781dc1a5b20f83300000161' })); + const result = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.WALLET_NOT_READY); +}); + +test('PUT /txproposals/{proposalId} with an empty body should fail with ApiError.INVALID_PAYLOAD', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; + + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '0000001e39bc37fe8710c01cc1e8c0a937bf6f9337551fbbfddc222bfc28c197', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '00000060a25077e48926bcd9473d77259296e123ec6af1c1a16c1c381093ab90', + index: 0, + tokenId: token2, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token1', + unlockedBalance: 400, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 2, + }, { + walletId: 'my-wallet', + tokenId: 'token2', + unlockedBalance: 300, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 0, + }]); + + // only one output, spending the whole 300 utxo of token1 + const outputs = [ + new hathorLib.Output( + 300, + new hathorLib.P2PKH(new hathorLib.Address(ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + })).createScript(), { + tokenData: 1, + }, + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs, { tokens: [token1] }); + + const txHex = transaction.toHex(); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const txCreateResult = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(txCreateResult.body as string); + + const txSendEvent = makeGatewayEventWithAuthorizer('my-wallet', { txProposalId: returnBody.txProposalId }, null); + const txSendResult = await txProposalSend(txSendEvent, null, null) as APIGatewayProxyResult; + + expect(JSON.parse(txSendResult.body as string).error).toStrictEqual(ApiError.INVALID_PAYLOAD); +}); + +test('PUT /txproposals/{proposalId} with missing params should fail with ApiError.MISSING_PARAMETER', async () => { + expect.hasAssertions(); + + const txSendEvent = makeGatewayEventWithAuthorizer('my-wallet', null, null); + const txSendResult = await txProposalSend(txSendEvent, null, null) as APIGatewayProxyResult; + + expect(JSON.parse(txSendResult.body as string).error).toBe(ApiError.MISSING_PARAMETER); + expect(JSON.parse(txSendResult.body as string).parameter).toBe('txProposalId'); +}); + +test('PUT /txproposals/{proposalId} with a missing proposalId should fail with ApiError.TX_PROPOSAL_NOT_FOUND', async () => { + expect.hasAssertions(); + + const txSendEvent = makeGatewayEventWithAuthorizer('my-wallet', { txProposalId: '8d1e2921-7bc9-41f5-9758-40b734edff0f' }, JSON.stringify({ + txHex: 'txhex', + })); + const txSendResult = await txProposalSend(txSendEvent, null, null) as APIGatewayProxyResult; + + expect(JSON.parse(txSendResult.body as string).error).toStrictEqual(ApiError.TX_PROPOSAL_NOT_FOUND); +}); + +test('PUT /txproposals/{proposalId} with a invalid proposalId should fail with ApiError.INVALID_PARAMETER', async () => { + expect.hasAssertions(); + + const txSendEvent = makeGatewayEventWithAuthorizer('my-wallet', { txProposalId: 'invalid-uuid' }, null); + const txSendResult = await txProposalSend(txSendEvent, null, null) as APIGatewayProxyResult; + + expect(JSON.parse(txSendResult.body as string).error).toStrictEqual(ApiError.INVALID_PARAMETER); + expect(JSON.parse(txSendResult.body as string).parameter).toStrictEqual('txProposalId'); +}); + +test('PUT /txproposals/{proposalId} on a proposal which status is not OPEN or SEND_ERROR should fail with ApiError.TX_PROPOSAL_NOT_OPEN', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; + + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '0000001e39bc37fe8710c01cc1e8c0a937bf6f9337551fbbfddc222bfc28c197', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '00000060a25077e48926bcd9473d77259296e123ec6af1c1a16c1c381093ab90', + index: 0, + tokenId: token2, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token1', + unlockedBalance: 400, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 2, + }, { + walletId: 'my-wallet', + tokenId: 'token2', + unlockedBalance: 300, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 0, + }]); + + // only one output, spending the whole 300 utxo of token1 + const outputs = [ + new hathorLib.Output( + 300, + new hathorLib.P2PKH( + new hathorLib.Address( + ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + }, + ), + ).createScript(), { + tokenData: 1, + }, + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs, { tokens: [token1] }); + + const txHex = transaction.toHex(); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const txCreateResult = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(txCreateResult.body as string); + + // Set tx_proposal status to CANCELLED so it will fail on txProposalSend + const now = getUnixTimestamp(); + await updateTxProposal( + mysql, + returnBody.txProposalId, + now, + TxProposalStatus.CANCELLED, + ); + + const txSendEvent = makeGatewayEventWithAuthorizer('my-wallet', { txProposalId: returnBody.txProposalId }, JSON.stringify({ + txHex, + })); + const txSendResult = await txProposalSend(txSendEvent, null, null) as APIGatewayProxyResult; + + expect(JSON.parse(txSendResult.body as string).error).toStrictEqual(ApiError.TX_PROPOSAL_NOT_OPEN); +}); + +test('PUT /txproposals/{proposalId} on a proposal which is not owned by the user\'s wallet should fail with ApiError.FORBIDDEN', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; + + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '0000001e39bc37fe8710c01cc1e8c0a937bf6f9337551fbbfddc222bfc28c197', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '00000060a25077e48926bcd9473d77259296e123ec6af1c1a16c1c381093ab90', + index: 0, + tokenId: token2, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token1', + unlockedBalance: 400, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 2, + }, { + walletId: 'my-wallet', + tokenId: 'token2', + unlockedBalance: 300, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 0, + }]); + + // only one output, spending the whole 300 utxo of token1 + const outputs = [ + new hathorLib.Output( + 300, + new hathorLib.P2PKH(new hathorLib.Address( + ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + }, + )).createScript(), { + tokenData: 1, + }, + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs, { tokens: [token1] }); + + const txHex = transaction.toHex(); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const txCreateResult = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(txCreateResult.body as string); + + // Set tx_proposal status to CANCELLED so it will fail on txProposalSend + const now = getUnixTimestamp(); + await updateTxProposal( + mysql, + returnBody.txProposalId, + now, + TxProposalStatus.CANCELLED, + ); + + const txSendEvent = makeGatewayEventWithAuthorizer('another-wallet', { txProposalId: returnBody.txProposalId }, JSON.stringify({ + txHex, + })); + const txSendResult = await txProposalSend(txSendEvent, null, null) as APIGatewayProxyResult; + + expect(JSON.parse(txSendResult.body as string).error).toStrictEqual(ApiError.FORBIDDEN); +}); + +test('PUT /txproposals/{proposalId} with an invalid txHex should fail and update tx_proposal to SEND_ERROR', async () => { + expect.hasAssertions(); + + // Create the spy to mock wallet-lib + const spy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); + spy.mockReturnValue({ + post: () => Promise.resolve({ + data: { + success: false, + message: 'invalid txhex', + }, + }), + get: () => Promise.resolve({ + data: { + success: true, + version: '0.38.0', + network: 'mainnet', + min_weight: 14, + min_tx_weight: 14, + min_tx_weight_coefficient: 1.6, + min_tx_weight_k: 100, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 255, + }, + }), + }); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; + + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '0000001e39bc37fe8710c01cc1e8c0a937bf6f9337551fbbfddc222bfc28c197', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '00000060a25077e48926bcd9473d77259296e123ec6af1c1a16c1c381093ab90', + index: 0, + tokenId: token2, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token1', + unlockedBalance: 400, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 2, + }, { + walletId: 'my-wallet', + tokenId: 'token2', + unlockedBalance: 300, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 0, + }]); + + // only one output, spending the whole 300 utxo of token1 + const outputs = [ + new hathorLib.Output( + 300, + new hathorLib.P2PKH(new hathorLib.Address( + ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + }, + )).createScript(), { + tokenData: 1, + }, + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs, { tokens: [token1] }); + + const txHex = transaction.toHex(); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const txCreateResult = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(txCreateResult.body as string); + + const txSendEvent = makeGatewayEventWithAuthorizer('my-wallet', { txProposalId: returnBody.txProposalId }, JSON.stringify({ + txHex, + })); + const txSendResult = await txProposalSend(txSendEvent, null, null) as APIGatewayProxyResult; + + expect(JSON.parse(txSendResult.body).success).toStrictEqual(false); + + const txProposal = await getTxProposal(mysql, returnBody.txProposalId); + + expect(txProposal.status).toStrictEqual(TxProposalStatus.SEND_ERROR); + + spy.mockRestore(); +}); + +test('PUT /txproposals/{proposalId} should update tx_proposal to SEND_ERROR on fail because of wallet-lib call error', async () => { + expect.hasAssertions(); + + // Create the spy to mock wallet-lib + const spy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); + spy.mockReturnValue({ + post: () => { + throw new Error('Wallet lib error'); + }, + get: () => Promise.resolve({ + data: { + success: true, + version: '0.38.0', + network: 'mainnet', + min_weight: 14, + min_tx_weight: 14, + min_tx_weight_coefficient: 1.6, + min_tx_weight_k: 100, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 255, + }, + }), + }); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; + + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '0000001e39bc37fe8710c01cc1e8c0a937bf6f9337551fbbfddc222bfc28c197', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '00000060a25077e48926bcd9473d77259296e123ec6af1c1a16c1c381093ab90', + index: 0, + tokenId: token2, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token1', + unlockedBalance: 400, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 2, + }, { + walletId: 'my-wallet', + tokenId: 'token2', + unlockedBalance: 300, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 0, + }]); + + // only one output, spending the whole 300 utxo of token1 + const outputs = [ + new hathorLib.Output( + 300, + new hathorLib.P2PKH(new hathorLib.Address( + ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + }, + )).createScript(), { + tokenData: 1, + }, + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs, { tokens: [token1] }); + + const txHex = transaction.toHex(); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const txCreateResult = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(txCreateResult.body as string); + + const txSendEvent = makeGatewayEventWithAuthorizer('my-wallet', { txProposalId: returnBody.txProposalId }, JSON.stringify({ + txHex, + })); + const txSendResult = await txProposalSend(txSendEvent, null, null) as APIGatewayProxyResult; + + expect(JSON.parse(txSendResult.body).success).toStrictEqual(false); + + const txProposal = await getTxProposal(mysql, returnBody.txProposalId); + + expect(txProposal.status).toStrictEqual(TxProposalStatus.SEND_ERROR); + + spy.mockRestore(); +}); + +test('DELETE /txproposals/{proposalId} should delete a tx_proposal and remove the utxos associated to it', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; + + const utxos = [{ + txId: '00000000000000001650cd208a2bcff09dce8af88d1b07097ef0efdba4aacbaa', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '000000000000000042fb8ae48accbc48561729e2359838751e11f837ca9a5746', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '0000000000000000cfd3dea4c689aa4c863bf6e6aea4518abcfe7d5ff6769aef', + index: 0, + tokenId: token2, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token1', + unlockedBalance: 400, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 2, + }, { + walletId: 'my-wallet', + tokenId: 'token2', + unlockedBalance: 300, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 0, + }]); + + // only one output, spending the whole 300 utxo of token1 + const outputs = [ + new hathorLib.Output( + 300, + new hathorLib.P2PKH(new hathorLib.Address( + ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + }, + )).createScript(), { + tokenData: 1, + }, + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs, { tokens: [token1] }); + + const txHex = transaction.toHex(); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const txCreateResult = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(txCreateResult.body as string); + const txProposalId = returnBody.txProposalId; + + const checkInputs: IWalletInput[] = [ + { + txId: '00000000000000001650cd208a2bcff09dce8af88d1b07097ef0efdba4aacbaa', + index: 0, + }, + ]; + const utxosAfterProposal = await getUtxos(mysql, checkInputs); + for (const u of utxosAfterProposal) { + expect(u.txProposalId).toBe(txProposalId); + } + + const txDeleteEvent = makeGatewayEventWithAuthorizer('my-wallet', { txProposalId: returnBody.txProposalId }, null); + const txDeleteResult = await txProposalDestroy(txDeleteEvent, null, null) as APIGatewayProxyResult; + + expect(JSON.parse(txDeleteResult.body).success).toStrictEqual(true); + + const txProposal = await getTxProposal(mysql, returnBody.txProposalId); + + expect(txProposal.status).toStrictEqual(TxProposalStatus.CANCELLED); + + const utxosAfterDestroyProposal = await getUtxos(mysql, checkInputs); + for (const u of utxosAfterDestroyProposal) { + expect(u.txProposalId).toBeNull(); + expect(u.txProposalIndex).toBeNull(); + } +}); + +test('DELETE /txproposals/{proposalId} with missing txProposalId should fail with ApiError.MISSING_PARAMETER', async () => { + expect.hasAssertions(); + + const txDeleteEvent = makeGatewayEventWithAuthorizer('wallet-id', null, null); + const txDeleteResult = await txProposalDestroy(txDeleteEvent, null, null) as APIGatewayProxyResult; + const txDeleteResultBody = JSON.parse(txDeleteResult.body as string); + + expect(txDeleteResultBody.success).toStrictEqual(false); + expect(txDeleteResultBody.error).toStrictEqual(ApiError.MISSING_PARAMETER); + expect(txDeleteResultBody.parameter).toStrictEqual('txProposalId'); +}); + +test('DELETE /txproposals/{proposalId} with not existing tx_proposal should fail with ApiError.TX_PROPOSAL_NOT_FOUND', async () => { + expect.hasAssertions(); + + const txDeleteEvent = makeGatewayEventWithAuthorizer('wallet-id', { txProposalId: 'invalid-tx-proposal-id' }, null); + const txDeleteResult = await txProposalDestroy(txDeleteEvent, null, null) as APIGatewayProxyResult; + const txDeleteResultBody = JSON.parse(txDeleteResult.body as string); + + expect(txDeleteResultBody.success).toStrictEqual(false); + expect(txDeleteResultBody.error).toStrictEqual(ApiError.TX_PROPOSAL_NOT_FOUND); +}); + +test('DELETE /txproposals/{proposalId} should fail with ApiError.TX_PROPOSAL_NOT_OPEN on already sent tx_proposals', async () => { + expect.hasAssertions(); + + await addToTxProposalTable(mysql, [['fe141b88-7328-4851-a608-631d1d5a5513', 'wallet-id', 'sent', 1, 1]]); + + const txDeleteEvent = makeGatewayEventWithAuthorizer('wallet-id', { txProposalId: 'fe141b88-7328-4851-a608-631d1d5a5513' }, null); + const txDeleteResult = await txProposalDestroy(txDeleteEvent, null, null) as APIGatewayProxyResult; + const txDeleteResultBody = JSON.parse(txDeleteResult.body as string); + + expect(txDeleteResultBody.success).toStrictEqual(false); + expect(txDeleteResultBody.error).toStrictEqual(ApiError.TX_PROPOSAL_NOT_OPEN); +}); + +test('POST /txproposals one output and input on txHex', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; + + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '0000001e39bc37fe8710c01cc1e8c0a937bf6f9337551fbbfddc222bfc28c197', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '00000060a25077e48926bcd9473d77259296e123ec6af1c1a16c1c381093ab90', + index: 0, + tokenId: token2, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token1', + unlockedBalance: 400, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 2, + }, { + walletId: 'my-wallet', + tokenId: 'token2', + unlockedBalance: 300, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 0, + }]); + + // only one output, spending the whole 300 utxo of token1 + const outputs = [ + new hathorLib.Output( + 300, + new hathorLib.P2PKH(new hathorLib.Address( + ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + }, + )).createScript(), { + tokenData: 1, + }, + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs, { tokens: [token1] }); + + const txHex = transaction.toHex(); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const result = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(201); + expect(returnBody.success).toBe(true); + expect(returnBody.txProposalId).toHaveLength(36); + expect(returnBody.inputs).toHaveLength(1); + expect(returnBody.inputs).toContainEqual({ txId: utxos[0].txId, index: utxos[0].index, addressPath: `${defaultDerivationPath}0` }); + + await _checkTxProposalTables(returnBody.txProposalId, returnBody.inputs); +}); + +test('POST /txproposals with denied utxos', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToWalletTable(mysql, [{ + id: 'other-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey2', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 0, + walletId: 'other-wallet', + transactions: 2, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; + + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId: token1, + address: ADDRESSES[1], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '0000001e39bc37fe8710c01cc1e8c0a937bf6f9337551fbbfddc222bfc28c197', + index: 0, + tokenId: token1, + address: ADDRESSES[1], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '00000060a25077e48926bcd9473d77259296e123ec6af1c1a16c1c381093ab90', + index: 0, + tokenId: token2, + address: ADDRESSES[1], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + + const outputs = [ + new hathorLib.Output( + 300, + new hathorLib.P2PKH(new hathorLib.Address( + ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + }, + )).createScript(), { + tokenData: 1, + }, + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs, { tokens: [token1] }); + + const txHex = transaction.toHex(); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const result = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toStrictEqual(ApiError.INPUTS_NOT_IN_WALLET); +}); + +test('POST /txproposals a tx create action on txHex', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId: '00', + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 0, + }]); + + // hathor input for deposit + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const outputs = [ + // change output 100 htr deposited: + new hathorLib.Output( + 200, + new hathorLib.P2PKH( + new hathorLib.Address( + ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + }, + ), + ).createScript(), + { tokenData: 0 }, + ), + // MINT mask + new hathorLib.Output( + hathorLib.constants.TOKEN_MINT_MASK, + new hathorLib.P2PKH( + new hathorLib.Address( + ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + }, + ), + ).createScript(), + { tokenData: 1 | hathorLib.constants.TOKEN_AUTHORITY_MASK }, // eslint-disable-line no-bitwise + ), + // MELT mask + new hathorLib.Output( + hathorLib.constants.TOKEN_MELT_MASK, + new hathorLib.P2PKH( + new hathorLib.Address( + ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + }, + ), + ).createScript(), + { tokenData: 1 | hathorLib.constants.TOKEN_AUTHORITY_MASK }, // eslint-disable-line no-bitwise + ), + // New created tokens + new hathorLib.Output( + 100 * 100, + new hathorLib.P2PKH( + new hathorLib.Address( + ADDRESSES[0], { + network: new hathorLib.Network(process.env.NETWORK), + }, + ), + ).createScript(), + { tokenData: 1 }, + ), + ]; + + const name = 'Test token'; + const symbol = 'TSTKN'; + const transaction = new CreateTokenTransaction(name, symbol, inputs, outputs, { + version: hathorLib.constants.CREATE_TOKEN_TX_VERSION, + }); + + const txHex = transaction.toHex(); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const result = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(201); + expect(returnBody.success).toBe(true); + expect(returnBody.txProposalId).toHaveLength(36); + expect(returnBody.inputs).toHaveLength(1); + expect(returnBody.inputs).toContainEqual({ txId: utxos[0].txId, index: utxos[0].index, addressPath: `${defaultDerivationPath}0` }); +}); + +test('PUT /txproposals/{proposalId} with txhex', async () => { + expect.hasAssertions(); + + // Create the spy to mock wallet-lib + const spy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); + spy.mockReturnValue({ + post: () => Promise.resolve({ + data: { success: true }, + }), + get: () => Promise.resolve({ + data: { + success: true, + version: '0.38.0', + network: 'mainnet', + min_weight: 14, + min_tx_weight: 14, + min_tx_weight_coefficient: 1.6, + min_tx_weight_k: 100, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 255, + }, + }), + }); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; + + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '0000001e39bc37fe8710c01cc1e8c0a937bf6f9337551fbbfddc222bfc28c197', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '00000060a25077e48926bcd9473d77259296e123ec6af1c1a16c1c381093ab90', + index: 0, + tokenId: token2, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token1', + unlockedBalance: 400, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 2, + }, { + walletId: 'my-wallet', + tokenId: 'token2', + unlockedBalance: 300, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 0, + }]); + + // only one output, spending the whole 300 utxo of token1 + const outputs = [ + new hathorLib.Output( + 300, + new hathorLib.P2PKH( + new hathorLib.Address(ADDRESSES[0], { network: new hathorLib.Network(process.env.NETWORK) }), + ).createScript(), + { tokenData: 1 }, + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs, { tokens: [token1] }); + + const txHex = transaction.toHex(); + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const result = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + const txSendEvent = makeGatewayEventWithAuthorizer('my-wallet', { txProposalId: returnBody.txProposalId }, JSON.stringify({ + txHex, + })); + const txSendResult = await txProposalSend(txSendEvent, null, null) as APIGatewayProxyResult; + + const sendReturnBody = JSON.parse(txSendResult.body as string); + const txProposal = await getTxProposal(mysql, sendReturnBody.txProposalId); + + expect(sendReturnBody.success).toStrictEqual(true); + expect(txProposal.status).toStrictEqual(TxProposalStatus.SENT); + + spy.mockRestore(); +}); + +test('PUT /txproposals/{proposalId} with a different txhex than the one sent in txProposalCreate', async () => { + expect.hasAssertions(); + + // Create the spy to mock wallet-lib + const spy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); + spy.mockReturnValue({ + post: () => Promise.resolve({ + data: { success: true }, + }), + get: () => Promise.resolve({ + data: { + success: true, + version: '0.38.0', + network: 'mainnet', + min_weight: 14, + min_tx_weight: 14, + min_tx_weight_coefficient: 1.6, + min_tx_weight_k: 100, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 255, + }, + }), + }); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'my-wallet', + transactions: 2, + }]); + + const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; + const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; + + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '0000001e39bc37fe8710c01cc1e8c0a937bf6f9337551fbbfddc222bfc28c197', + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: '00000060a25077e48926bcd9473d77259296e123ec6af1c1a16c1c381093ab90', + index: 0, + tokenId: token2, + address: ADDRESSES[0], + value: 300, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'my-wallet', + tokenId: 'token1', + unlockedBalance: 400, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 2, + }, { + walletId: 'my-wallet', + tokenId: 'token2', + unlockedBalance: 300, + lockedBalance: 0, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[1], + index: 1, + walletId: 'my-wallet', + transactions: 0, + }]); + + // only one output, spending the whole 300 utxo of token1 + const outputs = [ + new hathorLib.Output( + 300, + new hathorLib.P2PKH( + new hathorLib.Address(ADDRESSES[0], { network: new hathorLib.Network(process.env.NETWORK) }), + ).createScript(), + { tokenData: 1 }, + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs, { tokens: [token1] }); + const txHex = transaction.toHex(); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); + const result = await txProposalCreate(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + const differentInputs = [new hathorLib.Input(utxos[2].txId, utxos[2].index)]; + const transaction2 = new hathorLib.Transaction(differentInputs, outputs, { tokens: [token1] }); + const txHex2 = transaction2.toHex(); + + const txSendEvent = makeGatewayEventWithAuthorizer('my-wallet', { txProposalId: returnBody.txProposalId }, JSON.stringify({ + txHex: txHex2, + })); + const txSendResult = await txProposalSend(txSendEvent, null, null) as APIGatewayProxyResult; + + const sendReturnBody = JSON.parse(txSendResult.body as string); + + expect(sendReturnBody.success).toStrictEqual(false); + expect(sendReturnBody.error).toStrictEqual(ApiError.TX_PROPOSAL_NO_MATCH); + + spy.mockRestore(); +}); + +test('checkMissingUtxos', async () => { + expect.hasAssertions(); + const inputs: IWalletInput[] = [{ + txId: TX_IDS[0], + index: 0, + }, { + txId: TX_IDS[0], + index: 1, + }]; + + const utxos: DbTxOutput[] = [{ + txId: TX_IDS[0], + index: 0, + tokenId: '00', + address: ADDRESSES[0], + value: 0, + authorities: 0, + timelock: 0, + heightlock: 0, + locked: false, + spentBy: null, + txProposalId: null, + txProposalIndex: null, + }]; + + const checkMissingResult = checkMissingUtxos(inputs, utxos); + + expect(checkMissingResult).toHaveLength(1); +}); diff --git a/packages/wallet-service/tests/txPushNotificationRequested.test.ts b/packages/wallet-service/tests/txPushNotificationRequested.test.ts new file mode 100644 index 00000000..f4fdd2e1 --- /dev/null +++ b/packages/wallet-service/tests/txPushNotificationRequested.test.ts @@ -0,0 +1,640 @@ +import { logger } from '@tests/winston.mock'; +import { initFirebaseAdminMock } from '@tests/utils/firebase-admin.mock'; +import { closeDbConnection, getDbConnection } from '@src/utils'; +import { + addToWalletTable, + cleanDatabase, + buildWallet, +} from '@tests/utils'; +import { handleRequest, pushNotificationMessage } from '@src/api/txPushNotificationRequested'; +import { StringMap, WalletBalanceValue, PushProvider, SendNotificationToDevice } from '@src/types'; +import { PushNotificationUtils } from '@src/utils/pushnotification.utils'; +import { registerPushDevice, storeTokenInformation } from '@src/db'; +import { Context } from 'aws-lambda'; + +const mysql = getDbConnection(); + +initFirebaseAdminMock(); +const spyOnInvokeSendNotification = jest.spyOn(PushNotificationUtils, 'invokeSendNotificationHandlerLambda'); + +const buildEvent = (walletId, txId, walletBalanceForTx?): StringMap => ({ + [walletId]: { + walletId, + addresses: [ + 'addr2', + ], + txId, + walletBalanceForTx: walletBalanceForTx || [ + { + tokenId: 'token2', + tokenSymbol: 'T2', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + total: 10, + totalAmountSent: 10, + unlockedAmount: 10, + unlockedAuthorities: { + melt: false, + mint: false, + }, + }, + { + tokenId: 'token1', + tokenSymbol: 'T1', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + totalAmountSent: 5, + unlockedAmount: 5, + unlockedAuthorities: { + melt: false, + mint: false, + }, + total: 5, + }, + ], + }, +}); + +beforeEach(async () => { + initFirebaseAdminMock.mockReset(); + spyOnInvokeSendNotification.mockReset(); + await cleanDatabase(mysql); +}); + +afterAll(async () => { + await closeDbConnection(mysql); +}); + +describe('success', () => { + it('should alert when invoke send notification fails', async () => { + expect.hasAssertions(); + + const walletId = 'wallet1'; + await addToWalletTable(mysql, [buildWallet({ id: walletId })]); + + const deviceId = 'device1'; + const pushDevice = { + deviceId, + walletId, + pushProvider: PushProvider.ANDROID, + enablePush: true, + enableShowAmounts: false, + }; + + await storeTokenInformation(mysql, 'token1', 'token1', 'T1'); + + await registerPushDevice(mysql, pushDevice); + + const txId = 'txId1'; + + const sendEvent = buildEvent(walletId, txId, [ + { + tokenId: 'token2', + tokenSymbol: 'T2', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + total: 10, + totalAmountSent: 10, + unlockedAmount: 10, + unlockedAuthorities: { + melt: false, + mint: false, + }, + }, + ]); + const sendContext = { awsRequestId: '123' } as Context; + + spyOnInvokeSendNotification.mockRejectedValue(new Error('Error sending push notification')); + const result = await handleRequest(sendEvent, sendContext, null) as { success: boolean, message?: string, details?: unknown }; + + expect(result.success).toStrictEqual(true); + expect(spyOnInvokeSendNotification).toHaveBeenCalledTimes(1); + const lastErrorCall = logger.error.mock.calls[logger.error.mock.calls.length - 1][0]; + expect(lastErrorCall).toMatchInlineSnapshot('"Unexpected failure while calling invokeSendNotificationHandlerLambda."'); + }); + + it('should invoke send notification with generic message', async () => { + expect.hasAssertions(); + + const walletId = 'wallet1'; + await addToWalletTable(mysql, [buildWallet({ id: walletId })]); + + // device with disabled enableShowAmounts, resulting in a generic notification + const deviceId = 'device1'; + const pushDevice = { + deviceId, + walletId, + pushProvider: PushProvider.ANDROID, + enablePush: true, + enableShowAmounts: false, + }; + + await storeTokenInformation(mysql, 'token1', 'token1', 'T1'); + await storeTokenInformation(mysql, 'token2', 'token2', 'T2'); + + await registerPushDevice(mysql, pushDevice); + + const txId = 'txId1'; + + const sendEvent = buildEvent(walletId, txId, [ + { + tokenId: 'token2', + tokenSymbol: 'T2', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + total: 10, + totalAmountSent: 10, + unlockedAmount: 10, + unlockedAuthorities: { + melt: false, + mint: false, + }, + }, + { + tokenId: 'token1', + tokenSymbol: 'T1', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + totalAmountSent: 5, + unlockedAmount: 5, + unlockedAuthorities: { + melt: false, + mint: false, + }, + total: 5, + }, + ]); + const sendContext = { awsRequestId: '123' } as Context; + + const result = await handleRequest(sendEvent, sendContext, null) as { success: boolean, message?: string, details?: unknown }; + + expect(result.success).toStrictEqual(true); + expect(spyOnInvokeSendNotification).toHaveBeenCalledTimes(1); + + const expectedNotification = { + deviceId, + metadata: { + txId, + bodyLocKey: 'new_transaction_received_description_without_tokens', + titleLocKey: 'new_transaction_received_title', + }, + } as SendNotificationToDevice; + expect(spyOnInvokeSendNotification).toHaveBeenLastCalledWith(expectedNotification); + }); + + it('should succeed wihout invoke notification when device settings found has push notification disabled', async () => { + expect.hasAssertions(); + const walletId = 'wallet1'; + const deviceId = 'device1'; + const txId = 'txId1'; + + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + // device with disabled enableShowAmounts, resulting in no notification + const pushDevice = { + deviceId, + walletId, + pushProvider: PushProvider.ANDROID, + enablePush: false, + enableShowAmounts: false, + }; + await registerPushDevice(mysql, pushDevice); + + await storeTokenInformation(mysql, 'token2', 'token2', 'T2'); + + const sendEvent = buildEvent(walletId, txId, [ + { + tokenId: 'token2', + tokenSymbol: 'T2', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + total: 10, + totalAmountSent: 10, + unlockedAmount: 10, + unlockedAuthorities: { + melt: false, + mint: false, + }, + }, + ]); + const sendContext = { awsRequestId: '123' } as Context; + + const result = await handleRequest(sendEvent, sendContext, null) as { success: boolean, message?: string, details?: unknown }; + + expect(result.success).toStrictEqual(true); + expect(spyOnInvokeSendNotification).toHaveBeenCalledTimes(0); + }); + + describe('should invoke send notification with specific message', () => { + const walletId = 'wallet1'; + const deviceId = 'device1'; + const txId = 'txId1'; + + beforeEach(async () => { + await addToWalletTable(mysql, [buildWallet({ id: walletId })]); + + // device with enabled enableShowAmounts, resulting in an specific notification + const pushDevice = { + deviceId, + walletId, + pushProvider: PushProvider.ANDROID, + enablePush: true, + enableShowAmounts: true, + }; + await registerPushDevice(mysql, pushDevice); + await storeTokenInformation(mysql, 'token1', 'token1', 'T1'); + await storeTokenInformation(mysql, 'token2', 'token2', 'T2'); + await storeTokenInformation(mysql, 'token3', 'token3', 'T3'); + await storeTokenInformation(mysql, 'token4', 'token4', 'T4'); + }); + + it('token balance with 1 token', async () => { + expect.hasAssertions(); + + const sendEvent = buildEvent(walletId, txId, [ + { + tokenId: 'token2', + tokenSymbol: 'T2', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + total: 10, + totalAmountSent: 10, + unlockedAmount: 10, + unlockedAuthorities: { + melt: false, + mint: false, + }, + }, + ]); + const sendContext = { awsRequestId: '123' } as Context; + + const result = await handleRequest(sendEvent, sendContext, null) as { success: boolean, message?: string, details?: unknown }; + + expect(result.success).toStrictEqual(true); + expect(spyOnInvokeSendNotification).toHaveBeenCalledTimes(1); + + // first argument of the first call + const notificationSentOnSpy = spyOnInvokeSendNotification.mock.calls[0][0]; + expect(notificationSentOnSpy).toMatchInlineSnapshot(` +{ + "deviceId": "device1", + "metadata": { + "bodyLocArgs": "["10 T2"]", + "bodyLocKey": "new_transaction_received_description_with_tokens", + "titleLocKey": "new_transaction_received_title", + "txId": "txId1", + }, +} +`); + }); + + it('token balance with 2 token', async () => { + expect.hasAssertions(); + + const sendEvent = buildEvent(walletId, txId, [ + { + tokenId: 'token2', + tokenSymbol: 'T2', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + total: 10, + totalAmountSent: 10, + unlockedAmount: 10, + unlockedAuthorities: { + melt: false, + mint: false, + }, + }, + { + tokenId: 'token1', + tokenSymbol: 'T1', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + totalAmountSent: 5, + unlockedAmount: 5, + unlockedAuthorities: { + melt: false, + mint: false, + }, + total: 5, + }, + ]); + const sendContext = { awsRequestId: '123' } as Context; + + const result = await handleRequest(sendEvent, sendContext, null) as { success: boolean, message?: string, details?: unknown }; + + expect(result.success).toStrictEqual(true); + expect(spyOnInvokeSendNotification).toHaveBeenCalledTimes(1); + + // first argument of the first call + const notificationSentOnSpy = spyOnInvokeSendNotification.mock.calls[0][0]; + expect(notificationSentOnSpy).toMatchInlineSnapshot(` +{ + "deviceId": "device1", + "metadata": { + "bodyLocArgs": "["10 T2","5 T1"]", + "bodyLocKey": "new_transaction_received_description_with_tokens", + "titleLocKey": "new_transaction_received_title", + "txId": "txId1", + }, +} +`); + }); + + it('token balance with 3 tokens', async () => { + expect.hasAssertions(); + + const sendEvent = buildEvent(walletId, txId, [ + { + tokenId: 'token2', + tokenSymbol: 'T2', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + total: 10, + totalAmountSent: 10, + unlockedAmount: 10, + unlockedAuthorities: { + melt: false, + mint: false, + }, + }, + { + tokenId: 'token1', + tokenSymbol: 'T1', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + totalAmountSent: 5, + unlockedAmount: 5, + unlockedAuthorities: { + melt: false, + mint: false, + }, + total: 5, + }, + { + tokenId: 'token3', + tokenSymbol: 'T3', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + totalAmountSent: 1, + unlockedAmount: 1, + unlockedAuthorities: { + melt: false, + mint: false, + }, + total: 1, + }, + ]); + const sendContext = { awsRequestId: '123' } as Context; + + const result = await handleRequest(sendEvent, sendContext, null) as { success: boolean, message?: string, details?: unknown }; + + expect(result.success).toStrictEqual(true); + expect(spyOnInvokeSendNotification).toHaveBeenCalledTimes(1); + + // first argument of the first call + const notificationSentOnSpy = spyOnInvokeSendNotification.mock.calls[0][0]; + expect(notificationSentOnSpy).toMatchInlineSnapshot(` +{ + "deviceId": "device1", + "metadata": { + "bodyLocArgs": "["10 T2","5 T1","1"]", + "bodyLocKey": "new_transaction_received_description_with_tokens", + "titleLocKey": "new_transaction_received_title", + "txId": "txId1", + }, +} +`); + }); + + it('token balance with more than 3 tokens', async () => { + expect.hasAssertions(); + + const sendEvent = buildEvent(walletId, txId, [ + { + tokenId: 'token2', + tokenSymbol: 'T2', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + total: 10, + totalAmountSent: 10, + unlockedAmount: 10, + unlockedAuthorities: { + melt: false, + mint: false, + }, + }, + { + tokenId: 'token1', + tokenSymbol: 'T1', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + totalAmountSent: 5, + unlockedAmount: 5, + unlockedAuthorities: { + melt: false, + mint: false, + }, + total: 5, + }, + { + tokenId: 'token3', + tokenSymbol: 'T3', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + totalAmountSent: 1, + unlockedAmount: 1, + unlockedAuthorities: { + melt: false, + mint: false, + }, + total: 1, + }, + { + tokenId: 'token4', + tokenSymbol: 'T4', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + totalAmountSent: 1, + unlockedAmount: 1, + unlockedAuthorities: { + melt: false, + mint: false, + }, + total: 1, + }, + ]); + const sendContext = { awsRequestId: '123' } as Context; + + const result = await handleRequest(sendEvent, sendContext, null) as { success: boolean, message?: string, details?: unknown }; + + expect(result.success).toStrictEqual(true); + expect(spyOnInvokeSendNotification).toHaveBeenCalledTimes(1); + + // first argument of the first call + const notificationSentOnSpy = spyOnInvokeSendNotification.mock.calls[0][0]; + expect(notificationSentOnSpy).toMatchInlineSnapshot(` +{ + "deviceId": "device1", + "metadata": { + "bodyLocArgs": "["10 T2","5 T1","2"]", + "bodyLocKey": "new_transaction_received_description_with_tokens", + "titleLocKey": "new_transaction_received_title", + "txId": "txId1", + }, +} +`); + }); + }); +}); + +describe('failure', () => { + it('should fails when no device settings is found', async () => { + expect.hasAssertions(); + const walletId = 'wallet1'; + const txId = 'txId1'; + + const sendEvent = buildEvent(walletId, txId, [ + { + tokenId: 'token2', + tokenSymbol: 'T2', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + total: 10, + totalAmountSent: 10, + unlockedAmount: 10, + unlockedAuthorities: { + melt: false, + mint: false, + }, + }, + ]); + const sendContext = { awsRequestId: '123' } as Context; + + const result = await handleRequest(sendEvent, sendContext, null) as { success: boolean, message?: string, details?: unknown }; + + expect(result.success).toStrictEqual(false); + expect(result.message).toStrictEqual(pushNotificationMessage.deviceSettingsNotFound); + expect(spyOnInvokeSendNotification).toHaveBeenCalledTimes(0); + }); +}); + +describe('validation StringMap', () => { + it('should validate map format', async () => { + expect.hasAssertions(); + + const sendEvent = [] as unknown as StringMap; + const sendContext = { awsRequestId: '123' } as Context; + + const result = await handleRequest(sendEvent, sendContext, null) as { success: boolean, message?: string, details?: unknown }; + + expect(result.success).toStrictEqual(false); + expect(result.message).toStrictEqual(pushNotificationMessage.invalidPayload); + expect(/must be of type object/.test(result.details[0].message)).toStrictEqual(true); + expect(spyOnInvokeSendNotification).toHaveBeenCalledTimes(0); + }); + + it('should validate map key type', async () => { + expect.hasAssertions(); + + const sendEvent = {} as unknown as StringMap; + const sendContext = { awsRequestId: '123' } as Context; + + const result = await handleRequest(sendEvent, sendContext, null) as { success: boolean, message?: string, details?: unknown }; + + expect(result.success).toStrictEqual(false); + expect(result.message).toStrictEqual(pushNotificationMessage.invalidPayload); + expect(/must have at least 1 key/.test(result.details[0].message)).toStrictEqual(true); + expect(spyOnInvokeSendNotification).toHaveBeenCalledTimes(0); + }); + + it('should validate required WalletBalanceValue keys', async () => { + expect.hasAssertions(); + + const sendEvent = { wallet1: { } } as unknown as StringMap; + const sendContext = { awsRequestId: '123' } as Context; + + const result = await handleRequest(sendEvent, sendContext, null) as { success: boolean, message?: string, details?: unknown }; + + expect(result.success).toStrictEqual(false); + expect(result.message).toStrictEqual(pushNotificationMessage.invalidPayload); + expect(result.details).toHaveLength(4); + expect(spyOnInvokeSendNotification).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/wallet-service/tests/types.test.ts b/packages/wallet-service/tests/types.test.ts new file mode 100644 index 00000000..d14768f6 --- /dev/null +++ b/packages/wallet-service/tests/types.test.ts @@ -0,0 +1,154 @@ +import { Authorities, Balance, DecodedOutput, TokenBalanceMap, TxInput, TxOutput } from '@src/types'; + +test('Authorities', () => { + expect.hasAssertions(); + + const a = new Authorities(); + expect(a.array).toHaveLength(Authorities.LENGTH); + + expect(new Authorities()).toStrictEqual(new Authorities([0, 0, 0, 0, 0, 0, 0, 0])); + expect(new Authorities(0b0)).toStrictEqual(new Authorities([0])); + expect(new Authorities(0b10000000)).toStrictEqual(new Authorities([1, 0, 0, 0, 0, 0, 0, 0])); + expect(new Authorities(0b11111111)).toStrictEqual(new Authorities([1, 1, 1, 1, 1, 1, 1, 1])); + + // clone + const b = new Authorities(0b101); + expect(b.clone()).toStrictEqual(b); + expect(b.clone()).not.toBe(b); + + // toInteger + expect((new Authorities(0b0)).toInteger()).toBe(0b0); + expect((new Authorities(0b10)).toInteger()).toBe(0b10); + expect((new Authorities(0b11111111)).toInteger()).toBe(0b11111111); + + // toNegative + expect((new Authorities([0, 0, 0, 0, 1, 0, -1, 0])).toNegative().array).toStrictEqual([0, 0, 0, 0, -1, 0, 1, 0]); + + // merge + expect(Authorities.merge(new Authorities(0b0), new Authorities(0b1)).toInteger()).toBe(0b1); + expect(Authorities.merge(new Authorities(0b0), new Authorities(0b11111111)).toInteger()).toBe(0b11111111); + expect(Authorities.merge(new Authorities(0b01010101), new Authorities(0b10101010)).toInteger()).toBe(0b11111111); + expect(Authorities.merge(new Authorities(0b11111111), new Authorities(0b11111111)).toInteger()).toBe(0b11111111); + // with negative values + expect(Authorities.merge(new Authorities([0, -1, 1]), new Authorities([-1, -1, -1]))).toStrictEqual(new Authorities([-1, -1, 0])); +}); + +test('Balance merge', () => { + expect.hasAssertions(); + + const b1 = new Balance(3, 1, 2, null, new Authorities(0b01), new Authorities(0b00)); + const b2 = new Balance(7, 3, 4, null, new Authorities(0b10), new Authorities(0b11)); + expect(Balance.merge(b1, b2)).toStrictEqual(new Balance(10, 4, 6, null, new Authorities(0b11), new Authorities(0b11))); + + const b3 = new Balance(3, 1, 2, 1000); + const b4 = new Balance(7, 3, 4); + expect(Balance.merge(b3, b4)).toStrictEqual(new Balance(10, 4, 6, 1000)); + expect(Balance.merge(b4, b3)).toStrictEqual(new Balance(10, 4, 6, 1000)); + + const b5 = new Balance(30, 10, 20, 2000); + expect(Balance.merge(b3, b5)).toStrictEqual(new Balance(33, 11, 22, 1000)); + expect(Balance.merge(b5, b3)).toStrictEqual(new Balance(33, 11, 22, 1000)); +}); + +test('Balance total and authorities', () => { + expect.hasAssertions(); + const b = new Balance(3, 1, 2, null, new Authorities(0b01), new Authorities(0b10)); + expect(b.total()).toBe(3); + expect(b.authorities()).toStrictEqual(new Authorities(0b11)); +}); + +test('TokenBalanceMap basic', () => { + expect.hasAssertions(); + const t1 = new TokenBalanceMap(); + // return an empty balance + expect(t1.get('token1')).toStrictEqual(new Balance()); + // add balance for a token and fetch it again + const b1 = new Balance(14, 5, 9, 1000); + t1.set('token1', b1); + expect(t1.get('token1')).toStrictEqual(b1); + // balance for a different token should still be 0 + expect(t1.get('token2')).toStrictEqual(new Balance()); +}); + +test('TokenBalanceMap clone', () => { + expect.hasAssertions(); + const t1 = new TokenBalanceMap(); + t1.set('token1', new Balance(14, 5, 9, 1000)); + const t2 = t1.clone(); + expect(t1).toStrictEqual(t2); + expect(t1).not.toBe(t2); + // should also clone balances + expect(t1.get('token1')).not.toBe(t2.get('token1')); +}); + +test('TokenBalanceMap fromStringMap', () => { + expect.hasAssertions(); + const t1 = new TokenBalanceMap(); + t1.set('token1', new Balance(15, 0, 15)); + t1.set('token2', new Balance(5, 2, -3, 1000)); + const t2 = TokenBalanceMap.fromStringMap({ + token1: { totalSent: 15, unlocked: 0, locked: 15 }, + token2: { totalSent: 5, unlocked: 2, locked: -3, lockExpires: 1000 }, + }); + expect(t2).toStrictEqual(t1); +}); + +test('TokenBalanceMap merge', () => { + expect.hasAssertions(); + const t1 = TokenBalanceMap.fromStringMap({ + token1: { totalSent: 10, unlocked: 0, locked: 10 }, + token2: { totalSent: 12, unlocked: 5, locked: 7 }, + }); + const t2 = TokenBalanceMap.fromStringMap({ + token1: { totalSent: 10, unlocked: 2, locked: -3, lockExpires: 1000 }, + token3: { totalSent: 10, unlocked: 9, locked: 0 }, + }); + const merged = new TokenBalanceMap(); + merged.set('token1', new Balance(20, 2, 7, 1000)); + merged.set('token2', new Balance(12, 5, 7)); + merged.set('token3', new Balance(10, 9, 0)); + expect(TokenBalanceMap.merge(t1, t2)).toStrictEqual(merged); + + // with null/undefined parameter + expect(TokenBalanceMap.merge(t1, null)).toStrictEqual(t1); + expect(TokenBalanceMap.merge(undefined, t1)).toStrictEqual(t1); + + // should clone the objects + expect(TokenBalanceMap.merge(t1, null)).not.toBe(t1); + expect(TokenBalanceMap.merge(undefined, t1)).not.toBe(t1); +}); + +test('TokenBalanceMap fromTxOutput fromTxInput', () => { + expect.hasAssertions(); + const timelock = 1000; + const decoded: DecodedOutput = { + type: 'P2PKH', + address: 'HCLqWoDJvprSnwwmr6huBg3bNR7DxjwXcD', + timelock, + }; + const txOutput: TxOutput = { + value: 200, + token_data: 0, + script: 'not-used', + token: '00', + spent_by: null, + decoded, + locked: false, + }; + const txInput: TxInput = { + tx_id: '00000000000000029411240dc4aea675b672c260f1419c8a3b87cfa203398098', + index: 2, + value: 200, + token_data: 0, + script: 'not-used', + token: '00', + decoded, + }; + + expect(TokenBalanceMap.fromTxInput(txInput)).toStrictEqual(TokenBalanceMap.fromStringMap({ '00': { totalSent: 0, unlocked: -txInput.value, locked: 0 } })); + expect(TokenBalanceMap.fromTxOutput(txOutput)).toStrictEqual(TokenBalanceMap.fromStringMap({ '00': { totalSent: 200, unlocked: txOutput.value, locked: 0 } })); + + // locked + txOutput.locked = true; + expect(TokenBalanceMap.fromTxOutput(txOutput)).toStrictEqual(TokenBalanceMap.fromStringMap({ '00': { totalSent: 200, locked: txOutput.value, unlocked: 0, lockExpires: timelock } })); +}); diff --git a/packages/wallet-service/tests/types.ts b/packages/wallet-service/tests/types.ts new file mode 100644 index 00000000..2bf07dbe --- /dev/null +++ b/packages/wallet-service/tests/types.ts @@ -0,0 +1,53 @@ +/* eslint-disable max-classes-per-file */ + +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export interface WalletBalanceEntry { + walletId: string; + tokenId: string; + unlockedBalance: number; + lockedBalance: number; + unlockedAuthorities: number; + lockedAuthorities: number; + timelockExpires?: number; + transactions: number; +} + +export interface AddressTxHistoryTableEntry { + address: string; + txId: string; + tokenId: string; + balance: number; + timestamp: number; + voided?: boolean; +} + +export interface AddressTableEntry { + address: string; + index: number; + walletId?: string; + transactions: number; +} + +export interface TokenTableEntry { + id: string; + name: string; + symbol: string; + transactions: number; +} + +export interface WalletTableEntry { + id: string; + xpubkey: string; + authXpubkey: string; + status: string; + maxGap: number; + highestUsedIndex?: number; + createdAt: number; + readyAt: number; +} diff --git a/packages/wallet-service/tests/utils.test.ts b/packages/wallet-service/tests/utils.test.ts new file mode 100644 index 00000000..c3568748 --- /dev/null +++ b/packages/wallet-service/tests/utils.test.ts @@ -0,0 +1,94 @@ +import { CustomStorage, arrayShuffle, sha256d, isTxVoided } from '@src/utils'; +import hathorLib from '@hathor/wallet-lib'; +import * as Fullnode from '@src/fullnode'; +import { TEST_SEED, XPUBKEY, AUTH_XPUBKEY, ADDRESSES } from '@tests/utils'; + +test('CustomStorage', () => { + expect.hasAssertions(); + + const store = new CustomStorage(); + // Should be initialized with hathor default server and server + expect(store.getItem('wallet:defaultServer')).toBe(hathorLib.constants.DEFAULT_SERVER); + expect(store.getItem('wallet:server')).toBe(hathorLib.constants.DEFAULT_SERVER); + + store.setItem('hathor', 'hathor'); + expect(store.getItem('hathor')).toBe('hathor'); + store.removeItem('hathor'); + + expect(store.getItem('hathor')).toBeUndefined(); + + store.setItem('hathor', 'hathor2'); + store.clear(); + expect(store.getItem('hathor')).toBeUndefined(); + + store.preStart(); + expect(store.getItem('wallet:defaultServer')).toBe(hathorLib.constants.DEFAULT_SERVER); + expect(store.getItem('wallet:server')).toBe(hathorLib.constants.DEFAULT_SERVER); +}); + +test('sha256d', () => { + expect.hasAssertions(); + // sha256d(my-test-data) -> 4f1ba9a4204e97a293b16ead6caced38f6d91d95618b96e261c6332ed24f7894 + // sha256d(something-else) -> 5c690b78d489f158d8575e7ed271521d056c445e8bd3978c8295775c1743bec0 + let result = sha256d('my-test-data', 'hex'); + expect(result).toBe('4f1ba9a4204e97a293b16ead6caced38f6d91d95618b96e261c6332ed24f7894'); + result = sha256d('something-else', 'hex'); + expect(result).toBe('5c690b78d489f158d8575e7ed271521d056c445e8bd3978c8295775c1743bec0'); +}); + +test('arrayShuffle', () => { + expect.hasAssertions(); + const original = Array.from(Array(10).keys()); + + const shuffled = Array.from(Array(10).keys()); + arrayShuffle(shuffled); + + expect(original).not.toStrictEqual(shuffled); +}); + +test('isTxVoided', async () => { + expect.hasAssertions(); + + const spy = jest.spyOn(Fullnode.default, 'downloadTx'); + + const mockImplementation = jest.fn((txId) => { + if (txId === '0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7') { + return { + meta: { + voided_by: [], + }, + }; + } + + return { + meta: { + voided_by: ['0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7'], + }, + }; + }); + + spy.mockImplementation(mockImplementation); + + expect(await isTxVoided('0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7')).toStrictEqual([ + false, + { meta: { voided_by: [] } }, + ]); + expect(await isTxVoided('5c690b78d489f158d8575e7ed271521d056c445e8bd3978c8295775c1743bec0')).toStrictEqual([ + true, + { meta: { voided_by: ['0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7'] } }, + ]); +}); + +test('XPUBKEY, AUTH_XPUBKEY and ADDRESSES should be derived from TEST_SEED', async () => { + expect.hasAssertions(); + const xpubkey = hathorLib.walletUtils.getXPubKeyFromSeed(TEST_SEED); + expect(xpubkey).toStrictEqual(XPUBKEY); + + const authXpubkey = hathorLib.HathorWalletServiceWallet.getAuthXPubKeyFromSeed(TEST_SEED); + expect(authXpubkey).toStrictEqual(AUTH_XPUBKEY); + + // Generate addresses in change derivation path 0 + const derivedXpub = hathorLib.walletUtils.xpubDeriveChild(xpubkey, 0); + const addresses = Object.keys(hathorLib.walletUtils.getAddresses(derivedXpub, 0, 17)); + expect(addresses).toStrictEqual(ADDRESSES); +}); diff --git a/packages/wallet-service/tests/utils.ts b/packages/wallet-service/tests/utils.ts new file mode 100644 index 00000000..88c33602 --- /dev/null +++ b/packages/wallet-service/tests/utils.ts @@ -0,0 +1,1142 @@ +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { ServerlessMysql } from 'serverless-mysql'; +import { isEqual } from 'lodash'; +import { + DbSelectResult, + TxInput, + TxOutputWithIndex, + FullNodeVersionData, + WalletBalanceValue, + StringMap, + PushProvider, + DbTxOutput, +} from '@src/types'; +import { getWalletId } from '@src/utils'; +import { walletUtils, Network, network, HathorWalletServiceWallet } from '@hathor/wallet-lib'; +import { + AddressTxHistoryTableEntry, + AddressTableEntry, + WalletBalanceEntry, + WalletTableEntry, + TokenTableEntry, +} from '@tests/types'; +import { RedisClient } from 'redis'; +import bitcore from 'bitcore-lib'; +import Mnemonic from 'bitcore-mnemonic'; + +export const TEST_SEED = 'neither image nasty party brass oyster treat twelve olive menu invest title fan only rack draw call impact use curtain winner horn juice unlock'; +// we'll use this xpubkey and corresponding addresses in some tests +export const XPUBKEY = 'xpub6CsZPtBWMkwxVxyBTKT8AWZcYqzwZ5K2qMkqjFpibMbBZ72JAvLMz7LquJNs4svfTiNYy6GbLo8gqECWsC6hTRt7imnphUFNEMz6VuRSjww'; +export const AUTH_XPUBKEY = 'xpub6BBrYRzvafoaGsgPkrngKNcdRx2w33dL1fcyTxC9CbL8FChKfYyfTb5kLGwjgNrpb8Za9bws8UKkET1ZDJGUvooFk1UEJtssvC6qN987u1J'; + +export const TX_IDS = [ + '0000033139d08176d1051fb3a272c3610457f0c7f686afbe0afe3d37f966db85', + '000003ae3be32b9df13157a27b77cf8e5fed3c20ad309a843002a10c5430c9cc', + '000005cbcb8b29f74446a260cd7d36fab3cba1295ac9fe904795d7b064e0e53c', + '0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7', + '00000649d769de25fcca204faaa23d4974d00fcb01130ab3f736fade4013598d', + '000002e185a37162bbcb1ec43576056638f0fad43648ae070194d1e1105f339a', + '00000597288221301f856e245579e7d32cea3e257330f9cb10178bb487b343e5', +]; + +export const ADDRESSES = [ + 'HBCQgVR8Xsyv1BLDjf9NJPK1Hwg4rKUh62', + 'HPDWdurEygcubNMUUnTDUAzngrSXFaqGQc', + 'HEYCNNZZYrimD97AtoRcgcNFzyxtkgtt9Q', + 'HPTtSRrDd4ekU4ZQ2jnSLYayL8hiToE5D4', + 'HTYymKpjyXnz4ssEAnywtwnXnfneZH1Dbh', + 'HUp754aDZ7yKndw2JchXEiMvgzKuXasUmF', + 'HLfGaQoxssGbZ4h9wbLyiCafdE8kPm6Fo4', + 'HV3ox5B1Dai6Jp5EhV8DvUiucc1z3WJHjL', + 'HNWxs2bxgYtzfCpU6cJMGLgmqv7eGupTHr', + 'H9Ef7qteC4vAoVUYx5mvP9jCfmZgU9rSvL', + 'H7hxR75zsPzwfPWbrdkkFbKN2SiL2Lvyuw', + 'HVCa4QJbHB6pkqvNkmQZD2vpmwTYMNdzVo', + 'HBchgf1JLxwJzUg6epckK3YJn6Bq8XJMPV', + 'HVWf61fwoj9Dx15NvWicqXQgGMYVYedSx4', + 'H7PfxBmaqjoBisFRzpizoB9JcYSvoo8D2j', + 'HC1NXVzGcVAd84QMfFngHiKyK2K8SUiTaL', + 'HCqsSDrbs1cfqnF6QMUQkdGYXjEMyt9N3Y', +]; + +export const cleanDatabase = async (mysql: ServerlessMysql): Promise => { + const TABLES = [ + 'address', + 'address_balance', + 'address_tx_history', + 'token', + 'tx_proposal', + 'transaction', + 'tx_output', + 'version_data', + 'wallet', + 'wallet_balance', + 'wallet_tx_history', + 'miner', + 'push_devices', + ]; + await mysql.query('SET FOREIGN_KEY_CHECKS = 0'); + for (const table of TABLES) { + await mysql.query(`DELETE FROM ${table}`); + } + await mysql.query('SET FOREIGN_KEY_CHECKS = 1'); +}; + +export const createOutput = ( + index: number, + value: number, + address: string, + token = '00', + timelock: number = null, + locked = false, + tokenData = 0, + spentBy = null, +): TxOutputWithIndex => ( + { + value, + token, + locked, + index, + decoded: { + type: 'P2PKH', + address, + timelock, + }, + token_data: tokenData, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + spent_by: spentBy, + } +); + +export const createInput = ( + value: number, + address: string, + txId: string, + index: number, + token = '00', + timelock = null, + tokenData = 0, +): TxInput => ( + { + value, + token_data: tokenData, + script: 'dqkUCEboPJo9txn548FA/NLLaMLsfsSIrA==', + decoded: { + type: 'P2PKH', + address, + timelock, + }, + token, + tx_id: txId, + index, + } +); + +export const checkUtxoTable = async ( + mysql: ServerlessMysql, + totalResults: number, + txId?: string, + index?: number, + tokenId?: string, + address?: string, + value?: number, + authorities?: number, + timelock?: number | null, + heightlock?: number | null, + locked?: boolean, + spentBy?: string | null, + voided = false, +): Promise> => { + // first check the total number of rows in the table + let results: DbSelectResult = await mysql.query('SELECT * FROM `tx_output` WHERE spent_by IS NULL'); + if (results.length !== totalResults) { + return { + error: 'checkUtxoTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + if (totalResults === 0) return true; + + // now fetch the exact entry + const baseQuery = ` + SELECT * + FROM \`tx_output\` + WHERE \`tx_id\` = ? + AND \`index\` = ? + AND \`token_id\` = ? + AND \`address\` = ? + AND \`value\` = ? + AND \`authorities\` = ? + AND \`locked\` = ? + AND \`voided\` = ? + AND \`timelock\``; + results = await mysql.query( + `${baseQuery} ${timelock ? '= ?' : 'IS ?'} + AND \`heightlock\` ${heightlock ? '= ?' : 'IS ?'} + AND \`spent_by\` ${spentBy ? '= ?' : 'IS ?'} + `, + [txId, index, tokenId, address, value, authorities, locked, voided, timelock, heightlock, spentBy], + ); + if (results.length !== 1) { + return { + error: 'checkUtxoTable query', + params: { txId, index, tokenId, address, value, authorities, timelock, heightlock, locked, spentBy, voided }, + results, + }; + } + return true; +}; + +export const checkAddressTable = async ( + mysql: ServerlessMysql, + totalResults: number, + address?: string, + index?: number | null, + walletId?: string | null, + transactions?: number, +): Promise> => { + // first check the total number of rows in the table + let results: DbSelectResult = await mysql.query('SELECT * FROM `address`'); + if (results.length !== totalResults) { + return { + error: 'checkAddressTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + if (totalResults === 0) return true; + + // now fetch the exact entry + const baseQuery = ` + SELECT * + FROM \`address\` + WHERE \`address\` = ? + AND \`transactions\` = ? + AND \`index\` + `; + const query = `${baseQuery} ${index !== null ? '= ?' : 'IS ?'} AND wallet_id ${walletId ? '= ?' : 'IS ?'}`; + results = await mysql.query( + query, + [address, transactions, index, walletId], + ); + if (results.length !== 1) { + return { + error: 'checkAddressTable query', + params: { address, transactions, index, walletId }, + results, + }; + } + return true; +}; + +export const checkAddressBalanceTable = async ( + mysql: ServerlessMysql, + totalResults: number, + address: string, + tokenId: string, + unlocked: number, + locked: number, + lockExpires: number | null, + transactions: number, + unlockedAuthorities = 0, + lockedAuthorities = 0, +): Promise> => { + // first check the total number of rows in the table + let results: DbSelectResult = await mysql.query(` + SELECT * + FROM \`address_balance\` + `); + if (results.length !== totalResults) { + return { + error: 'checkAddressBalanceTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + // now fetch the exact entry + const baseQuery = ` + SELECT * + FROM \`address_balance\` + WHERE \`address\` = ? + AND \`token_id\` = ? + AND \`unlocked_balance\` = ? + AND \`locked_balance\` = ? + AND \`transactions\` = ? + AND \`unlocked_authorities\` = ? + AND \`locked_authorities\` = ?`; + + results = await mysql.query( + `${baseQuery} AND timelock_expires ${lockExpires === null ? 'IS' : '='} ?`, [ + address, + tokenId, + unlocked, + locked, + transactions, + unlockedAuthorities, + lockedAuthorities, + lockExpires, + ], + ); + + if (results.length !== 1) { + return { + error: 'checkAddressBalanceTable query', + params: { address, tokenId, unlocked, locked, lockExpires, transactions, unlockedAuthorities, lockedAuthorities }, + results, + }; + } + return true; +}; + +export const checkAddressTxHistoryTable = async ( + mysql: ServerlessMysql, + totalResults: number, + address: string, + txId: string, + tokenId: string, + balance: number, + timestamp: number, +): Promise> => { + // first check the total number of rows in the table + let results: DbSelectResult = await mysql.query('SELECT * FROM `address_tx_history`'); + expect(results).toHaveLength(totalResults); + if (results.length !== totalResults) { + return { + error: 'checkAddressTxHistoryTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + // If we expect the table to be empty, we can return now. + if (totalResults === 0) { + return true; + } + + // now fetch the exact entry + results = await mysql.query( + `SELECT * + FROM \`address_tx_history\` + WHERE \`address\` = ? + AND \`tx_id\` = ? + AND \`token_id\` = ? + AND \`balance\` = ? + AND \`timestamp\` = ?`, + [ + address, + txId, + tokenId, + balance, + timestamp, + ], + ); + if (results.length !== 1) { + return { + error: 'checkAddressTxHistoryTable query', + params: { address, txId, tokenId, balance, timestamp }, + results, + }; + } + return true; +}; + +export const checkWalletTable = async (mysql: ServerlessMysql, + totalResults: number, + id?: string, + status?: string): Promise> => { + // first check the total number of rows in the table + let results: DbSelectResult = await mysql.query('SELECT * FROM `wallet`'); + expect(results).toHaveLength(totalResults); + if (results.length !== totalResults) { + return { + error: 'checkWalletTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + if (totalResults === 0) return true; + + // now fetch the exact entry + results = await mysql.query( + `SELECT * + FROM \`wallet\` + WHERE \`id\` = ? + AND \`status\` = ?`, + [id, status], + ); + if (results.length !== 1) { + return { + error: 'checkWalletTable query', + params: { id, status }, + results, + }; + } + return true; +}; + +export const checkWalletTxHistoryTable = async (mysql: ServerlessMysql, + totalResults: number, + walletId?: string, + tokenId?: string, + txId?: string, + balance?: number, + timestamp?: number): Promise> => { + // first check the total number of rows in the table + let results: DbSelectResult = await mysql.query('SELECT * FROM `wallet_tx_history`'); + expect(results).toHaveLength(totalResults); + if (results.length !== totalResults) { + return { + error: 'checkWalletTxHistoryTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + if (totalResults === 0) return true; + + // now fetch the exact entry + results = await mysql.query( + `SELECT * + FROM \`wallet_tx_history\` + WHERE \`wallet_id\` = ? + AND \`token_id\` = ? + AND \`tx_id\` = ? + AND \`balance\` = ? + AND \`timestamp\` = ?`, + [ + walletId, + tokenId, + txId, + balance, + timestamp, + ], + ); + + if (results.length !== 1) { + return { + error: 'checkWalletTxHistoryTable query', + params: { walletId, tokenId, txId, balance, timestamp }, + results, + }; + } + return true; +}; + +export const checkWalletBalanceTable = async ( + mysql: ServerlessMysql, + totalResults: number, + walletId?: string, + tokenId?: string, + unlocked?: number, + locked?: number, + lockExpires?: number | null, + transactions?: number, + unlockedAuthorities = 0, + lockedAuthorities = 0, +): Promise> => { + // first check the total number of rows in the table + let results: DbSelectResult = await mysql.query(` + SELECT * + FROM \`wallet_balance\` + `); + expect(results).toHaveLength(totalResults); + if (results.length !== totalResults) { + return { + error: 'checkWalletBalanceTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + if (totalResults === 0) return true; + + // now fetch the exact entry + const baseQuery = ` + SELECT * + FROM \`wallet_balance\` + WHERE \`wallet_id\` = ? + AND \`token_id\` = ? + AND \`unlocked_balance\` = ? + AND \`locked_balance\` = ? + AND \`transactions\` = ? + AND \`unlocked_authorities\` = ? + AND \`locked_authorities\` = ? + `; + results = await mysql.query( + `${baseQuery} AND timelock_expires ${lockExpires === null ? 'IS' : '='} ?`, + [walletId, tokenId, unlocked, locked, transactions, unlockedAuthorities, lockedAuthorities, lockExpires], + ); + if (results.length !== 1) { + return { + error: 'checkWalletBalanceTable query', + params: { walletId, tokenId, unlocked, locked, lockExpires, transactions, unlockedAuthorities, lockedAuthorities }, + results, + }; + } + return true; +}; + +type Token = { + tokenId: string; + tokenSymbol: string; + tokenName: string; + transactions: number; +} + +export const checkTokenTable = async ( + mysql: ServerlessMysql, + totalResults: number, + entries: Token[], +): Promise> => { + // first check the total number of rows in the table + let results: DbSelectResult = await mysql.query('SELECT * FROM `token`'); + if (results.length !== totalResults) { + return { + error: 'checkTokenTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + if (totalResults === 0) return true; + + // Fetch the exact entries + const query = ` + SELECT id AS tokenId, + symbol AS tokenSymbol, + name AS tokenName, + transactions + FROM \`token\` + WHERE \`id\` IN (?) + `; + results = await mysql.query( + query, + [entries.map((token) => token.tokenId)], + ); + + const invalidResults = results.filter((token: Token) => { + const entry = entries.find(({ tokenId }) => tokenId === token.tokenId); + + if (!entry) { + return true; + } + + // token is a RowDataPacket, so just cast it to an object so we can + // compare it + if (!isEqual({ ...token }, entry)) { + return true; + } + + return false; + }); + + if (invalidResults.length > 0) { + return { + error: 'checkTokenTable query', + params: entries, + invalidResults, + }; + } + return true; +}; + +export const countTxOutputTable = async ( + mysql: ServerlessMysql, +): Promise => { + const results: DbSelectResult = await mysql.query( + `SELECT COUNT(*) AS count + FROM \`tx_output\` + WHERE \`voided\` = FALSE`, + ); + + if (results.length > 0) { + return results[0].count as number; + } + + return 0; +}; + +export const addToTransactionTable = async ( + mysql: ServerlessMysql, + entries: unknown[][], +): Promise => { + await mysql.query( + `INSERT INTO \`transaction\`(\`tx_id\`, \`timestamp\`, + \`version\`, \`voided\`, + \`height\`, \`weight\`) + VALUES ?`, + [entries], + ); +}; + +export const addToUtxoTable = async ( + mysql: ServerlessMysql, + entries: DbTxOutput[], +): Promise => { + const payload = entries.map((entry: DbTxOutput) => ([ + entry.txId, + entry.index, + entry.tokenId, + entry.address, + entry.value, + entry.authorities, + entry.timelock || null, + entry.heightlock || null, + entry.locked, + entry.spentBy || null, + entry.txProposalId || null, + entry.txProposalIndex, + entry.voided || false, + ])); + await mysql.query( + `INSERT INTO \`tx_output\`( + \`tx_id\` + , \`index\` + , \`token_id\` + , \`address\` + , \`value\` + , \`authorities\` + , \`timelock\` + , \`heightlock\` + , \`locked\` + , \`spent_by\` + , \`tx_proposal\` + , \`tx_proposal_index\` + , \`voided\`) + VALUES ?`, + [payload], + ); +}; + +export const addToWalletTable = async ( + mysql: ServerlessMysql, + entries: WalletTableEntry[], +): Promise => { + const payload = entries.map((entry) => [ + entry.id, + entry.xpubkey, + entry.highestUsedIndex || -1, + entry.authXpubkey, + entry.status, + entry.maxGap, + entry.createdAt, + entry.readyAt, + ]); + await mysql.query(` + INSERT INTO \`wallet\`(\`id\`, \`xpubkey\`, + \`last_used_address_index\`, + \`auth_xpubkey\`, + \`status\`, \`max_gap\`, + \`created_at\`, \`ready_at\`) + VALUES ?`, + [payload]); +}; + +export const addToWalletBalanceTable = async ( + mysql: ServerlessMysql, + entries: WalletBalanceEntry[], +): Promise => { + const payload = entries.map((entry) => ([ + entry.walletId, + entry.tokenId, + entry.unlockedBalance, + entry.lockedBalance, + entry.unlockedAuthorities, + entry.lockedAuthorities, + entry.timelockExpires, + entry.transactions, + ])); + + await mysql.query(` + INSERT INTO \`wallet_balance\`(\`wallet_id\`, \`token_id\`, + \`unlocked_balance\`, \`locked_balance\`, + \`unlocked_authorities\`, \`locked_authorities\`, + \`timelock_expires\`, \`transactions\`) + VALUES ?`, + [payload]); +}; + +export const addToWalletTxHistoryTable = async ( + mysql: ServerlessMysql, + entries: unknown[][], +): Promise => { + await mysql.query(` + INSERT INTO \`wallet_tx_history\`(\`wallet_id\`, \`tx_id\`, + \`token_id\`, \`balance\`, + \`timestamp\`, \`voided\`) + VALUES ?`, + [entries]); +}; + +export const addToAddressTable = async ( + mysql: ServerlessMysql, + entries: AddressTableEntry[], +): Promise => { + const payload = entries.map((entry) => ([ + entry.address, + entry.index, + entry.walletId, + entry.transactions, + ])); + + await mysql.query(` + INSERT INTO \`address\`(\`address\`, \`index\`, + \`wallet_id\`, \`transactions\`) + VALUES ?`, + [payload]); +}; + +export const addToAddressTxHistoryTable = async ( + mysql: ServerlessMysql, + entries: AddressTxHistoryTableEntry[], +): Promise => { + const payload = entries.map((entry) => ([ + entry.address, + entry.txId, + entry.tokenId, + entry.balance, + entry.timestamp, + entry.voided || false, + ])); + + await mysql.query(` + INSERT INTO \`address_tx_history\`(\`address\`, \`tx_id\`, + \`token_id\`, \`balance\`, + \`timestamp\`, \`voided\`) + VALUES ?`, + [payload]); +}; + +export const addToAddressBalanceTable = async ( + mysql: ServerlessMysql, + entries: unknown[][], +): Promise => { + await mysql.query(` + INSERT INTO \`address_balance\`(\`address\`, \`token_id\`, + \`unlocked_balance\`, \`locked_balance\`, + \`timelock_expires\`, \`transactions\`, + \`unlocked_authorities\`, \`locked_authorities\`, + \`total_received\`) + VALUES ?`, + [entries]); +}; + +export const addToTokenTable = async ( + mysql: ServerlessMysql, + entries: TokenTableEntry[], +): Promise => { + const payload = entries.map((entry) => ([ + entry.id, + entry.name, + entry.symbol, + entry.transactions, + ])); + + await mysql.query( + 'INSERT INTO `token`(`id`, `name`, `symbol`, `transactions`) VALUES ?', + [payload], + ); +}; + +export const addToTxProposalTable = async ( + mysql: ServerlessMysql, + entries: unknown[][], +): Promise => { + await mysql.query( + 'INSERT INTO tx_proposal (`id`, `wallet_id`, `status`, `created_at`, `updated_at`) VALUES ?', + [entries], + ); +}; + +export const makeGatewayEvent = ( + params: { [name: string]: string }, + body = null, + multiValueQueryStringParameters = null, +): APIGatewayProxyEvent => ({ + body, + queryStringParameters: params, + pathParameters: params, + headers: {}, + multiValueHeaders: {}, + httpMethod: '', + isBase64Encoded: false, + path: '', + multiValueQueryStringParameters, + stageVariables: null, + requestContext: null, + resource: null, +}); + +/* + * The views protected by the bearer authorizer may use the `walletIdProxyHandler` + * function that extracts the walletId from the requestContext and not from parameters. + */ +export const makeGatewayEventWithAuthorizer = ( + walletId: string, + params: { [name: string]: string }, + body = null, + multiValueQueryStringParameters: { [name: string]: string[] } = null, +): APIGatewayProxyEvent => ({ + body, + queryStringParameters: params, + pathParameters: params, + headers: { + origin: 'https://hathor.com/', // We add this origin to get the access-control-allow-origin header from middy + }, + multiValueHeaders: {}, + httpMethod: '', + isBase64Encoded: false, + path: '', + multiValueQueryStringParameters, + stageVariables: null, + requestContext: { + authorizer: { principalId: walletId }, + accountId: '', + apiId: '', + httpMethod: '', + identity: null, + path: '', + protocol: '', + requestId: '', + requestTimeEpoch: 0, + resourceId: '', + resourcePath: '', + stage: '', + }, + resource: null, +}); + +export const addToVersionDataTable = async (mysql: ServerlessMysql, versionData: FullNodeVersionData): Promise => { + const payload = [[ + 1, + versionData.timestamp, + versionData.version, + versionData.network, + versionData.minWeight, + versionData.minTxWeight, + versionData.minTxWeightCoefficient, + versionData.minTxWeightK, + versionData.tokenDepositPercentage, + versionData.rewardSpendMinBlocks, + versionData.maxNumberInputs, + versionData.maxNumberOutputs, + ]]; + + await mysql.query( + `INSERT INTO \`version_data\`(\`id\`, \`timestamp\`, + \`version\`, \`network\`, + \`min_weight\`, \`min_tx_weight\`, + \`min_tx_weight_coefficient\`, \`min_tx_weight_k\`, + \`token_deposit_percentage\`, \`reward_spend_min_blocks\`, + \`max_number_inputs\`, \`max_number_outputs\`) + VALUES ?`, + [payload], + ); +}; + +export const checkVersionDataTable = async (mysql: ServerlessMysql, versionData: FullNodeVersionData): Promise> => { + // first check the total number of rows in the table + let results: DbSelectResult = await mysql.query('SELECT * FROM `version_data`'); + + if (results.length > 1) { + return { + error: 'version_data total results', + expected: 1, + received: results.length, + results, + }; + } + + // now fetch the exact entry + const baseQuery = ` + SELECT * + FROM \`version_data\` + WHERE \`id\` = 1 + `; + + results = await mysql.query(baseQuery); + + if (results.length !== 1) { + return { + error: 'checkVersionDataTable query', + }; + } + + const dbVersionData: FullNodeVersionData = { + timestamp: results[0].timestamp as number, + version: results[0].version as string, + network: results[0].network as string, + minWeight: results[0].min_weight as number, + minTxWeight: results[0].min_tx_weight as number, + minTxWeightCoefficient: results[0].min_tx_weight_coefficient as number, + minTxWeightK: results[0].min_tx_weight_k as number, + tokenDepositPercentage: results[0].token_deposit_percentage as number, + rewardSpendMinBlocks: results[0].reward_spend_min_blocks as number, + maxNumberInputs: results[0].max_number_inputs as number, + maxNumberOutputs: results[0].max_number_outputs as number, + }; + + if (Object.entries(dbVersionData).toString() !== Object.entries(versionData).toString()) { + return { + error: 'checkVersionDataTable results don\'t match', + expected: versionData, + received: dbVersionData, + }; + } + + return true; +}; + +export const redisAddKeys = ( + client: RedisClient, + keyMapping: Record, +): void => { + const multi = client.multi(); + for (const [k, v] of Object.entries(keyMapping)) { + multi.set(k, v); + } + multi.exec(); +}; + +export const redisCleanup = ( + client: RedisClient, +): void => { + client.flushdb(); +}; + +export const getAuthData = (now: number): any => { + // get the first address + const xpubChangeDerivation = walletUtils.xpubDeriveChild(XPUBKEY, 0); + const firstAddress = walletUtils.getAddressAtIndex(xpubChangeDerivation, 0, process.env.NETWORK); + + // we need signatures for both the account path and the purpose path: + const walletId = getWalletId(XPUBKEY); + const xpriv = getXPrivKeyFromSeed(TEST_SEED, { + passphrase: '', + networkName: process.env.NETWORK, + }); + + // account path + const accountDerivationIndex = '0\''; + const derivedPrivKey = walletUtils.deriveXpriv(xpriv, accountDerivationIndex); + const address = derivedPrivKey.publicKey.toAddress(network.getNetwork()).toString(); + const message = new bitcore.Message(String(now).concat(walletId).concat(address)); + const xpubkeySignature = message.sign(derivedPrivKey.privateKey); + + // auth purpose path (m/280'/280') + const authDerivedPrivKey = HathorWalletServiceWallet.deriveAuthPrivateKey(xpriv); + const authAddress = authDerivedPrivKey.publicKey.toAddress(network.getNetwork()); + const authMessage = new bitcore.Message(String(now).concat(walletId).concat(authAddress)); + const authXpubkeySignature = authMessage.sign(authDerivedPrivKey.privateKey); + + return { + walletId, + xpubkey: XPUBKEY, + xpubkeySignature, + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature, + firstAddress, + timestamp: now, + }; +}; + +export const checkPushDevicesTable = async ( + mysql: ServerlessMysql, + totalResults: number, + filter?: { + deviceId: string, + walletId: string, + pushProvider: string, + enablePush: boolean, + enableShowAmounts: boolean, + }, +): Promise> => { + let results: DbSelectResult = await mysql.query('SELECT * FROM `push_devices`'); + if (!filter && results.length !== totalResults) { + return { + error: 'checkPushDevicesTable total results', + expected: totalResults, + received: results.length, + results, + }; + } + + if (totalResults === 0) return true; + if (!filter) return true; + + // now fetch the exact entry + const baseQuery = ` + SELECT * + FROM \`push_devices\` + WHERE \`wallet_id\` = ? + AND \`device_id\` = ? + AND \`push_provider\` = ? + AND \`enable_push\` = ? + AND \`enable_show_amounts\` = ? + `; + + results = await mysql.query(baseQuery, [ + filter.walletId, + filter.deviceId, + filter.pushProvider, + filter.enablePush, + filter.enableShowAmounts, + ]); + + if (results.length !== totalResults) { + return { + error: 'checkPushDevicesTable total results after filter', + expected: totalResults, + received: results.length, + results, + }; + } + + if (results.length !== 1) { + return { + error: 'checkPushDevicesTable query', + params: { ...filter }, + results, + }; + } + return true; +}; + +/** + * Builds a default value for StringMap. + */ +export const buildWalletBalanceValueMap = ( + override?: Record, +): StringMap => ({ + wallet1: { + walletId: 'wallet1', + addresses: ['addr1'], + txId: 'tx1', + walletBalanceForTx: [ + { + tokenId: 'token1', + tokenSymbol: 'T1', + lockExpires: null, + lockedAmount: 0, + lockedAuthorities: { + melt: false, + mint: false, + }, + total: 10, + totalAmountSent: 10, + unlockedAmount: 10, + unlockedAuthorities: { + melt: false, + mint: false, + }, + }, + ], + }, + ...override, +}); + +export const buildWallet = (overwrite?): WalletTableEntry => { + const defaultWallet = { + id: 'id', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }; + + return { + ...defaultWallet, + ...overwrite, + }; +}; + +export const buildPushRegister = (overwrite?): { + deviceId: string, + walletId: string, + pushProvider: PushProvider, + enablePush: boolean, + enableShowAmounts: boolean, + updatedAt: number, +} => { + const defaultPushRegister = { + deviceId: 'deviceId', + walletId: 'walletId', + pushProvider: PushProvider.ANDROID, + enablePush: true, + enableShowAmounts: true, + updatedAt: new Date().getTime(), + }; + + return { + ...defaultPushRegister, + ...overwrite, + }; +}; + +export const insertPushDevice = async (mysql: ServerlessMysql, pushRegister: { + deviceId: string, + walletId: string, + pushProvider: PushProvider, + enablePush: boolean, + enableShowAmounts: boolean, + updatedAt: number, +}): Promise => { + await mysql.query( + ` + INSERT + INTO \`push_devices\` ( + device_id + , wallet_id + , push_provider + , enable_push + , enable_show_amounts + , updated_at) + VALUES (?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + updated_at = CURRENT_TIMESTAMP`, + [ + pushRegister.deviceId, + pushRegister.walletId, + pushRegister.pushProvider, + pushRegister.enablePush, + pushRegister.enableShowAmounts, + pushRegister.updatedAt, + ], + ); +}; + +export const daysAgo = (days) => new Date(new Date().getTime() - days * 24 * 60 * 60 * 1000); + +bitcore.Networks.add({ + ...network.bitcoreNetwork, + networkMagic: network.bitcoreNetwork.networkMagic.readUInt32BE(), +}); + +export const getXPrivKeyFromSeed = ( + seed: string, + options: { + passphrase?: string, + networkName?: string + } = {}): bitcore.HDPrivateKey => { + const methodOptions = Object.assign({passphrase: '', networkName: 'mainnet'}, options); + const { passphrase, networkName } = methodOptions; + + const network = new Network(networkName); + const code = new Mnemonic(seed); + return code.toHDPrivateKey(passphrase, network.bitcoreNetwork); +}; diff --git a/packages/wallet-service/tests/utils/alerting.utils.mock.ts b/packages/wallet-service/tests/utils/alerting.utils.mock.ts new file mode 100644 index 00000000..263316ec --- /dev/null +++ b/packages/wallet-service/tests/utils/alerting.utils.mock.ts @@ -0,0 +1,4 @@ +export const mockedAddAlert = jest.fn(); +export default jest.mock('@src/utils/alerting.utils', () => ({ + addAlert: mockedAddAlert.mockReturnValue(Promise.resolve()), +})); diff --git a/packages/wallet-service/tests/utils/aws-sdk.mock.ts b/packages/wallet-service/tests/utils/aws-sdk.mock.ts new file mode 100644 index 00000000..89f457a1 --- /dev/null +++ b/packages/wallet-service/tests/utils/aws-sdk.mock.ts @@ -0,0 +1,19 @@ +export const promiseMock = jest.fn(); +export const invokeMock = jest.fn(); +export const sendMock = jest.fn(); +export const lambdaInvokeCommandMock = jest.fn(); +export const lambdaClientMock = jest.fn().mockReturnValue({ + send: sendMock, +}); + +jest.mock('@aws-sdk/client-lambda', () => ({ + LambdaClient: lambdaClientMock, + InvokeCommand: lambdaInvokeCommandMock, +})); +export const newLambdaMock = jest.fn().mockReturnValue({ + invoke: invokeMock.mockReturnValue({ + promise: promiseMock.mockReturnValue({ + StatusCode: 202, + }), + }), +}); \ No newline at end of file diff --git a/packages/wallet-service/tests/utils/firebase-admin.mock.ts b/packages/wallet-service/tests/utils/firebase-admin.mock.ts new file mode 100644 index 00000000..1e93f4da --- /dev/null +++ b/packages/wallet-service/tests/utils/firebase-admin.mock.ts @@ -0,0 +1,15 @@ +export const sendMulticastMock = jest.fn(); +export const messaging = jest.fn(); + +export const initFirebaseAdminMock = jest.fn(); +export default jest.mock('firebase-admin', () => ({ + credential: { + cert: jest.fn(), + }, + initializeApp: initFirebaseAdminMock, + messaging: messaging.mockImplementation(() => ({ + sendMulticast: sendMulticastMock.mockReturnValue({ + failureCount: 0, + }), + })), +})); diff --git a/packages/wallet-service/tests/utils/nft.utils.test.ts b/packages/wallet-service/tests/utils/nft.utils.test.ts new file mode 100644 index 00000000..22d3a955 --- /dev/null +++ b/packages/wallet-service/tests/utils/nft.utils.test.ts @@ -0,0 +1,288 @@ +import hathorLib from '@hathor/wallet-lib'; +import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; +import { Severity } from '@src/types'; +import { MAX_METADATA_UPDATE_RETRIES, NftUtils } from '@src/utils/nft.utils'; +import { getHandlerContext, getTransaction } from '@events/nftCreationTx'; +import { + LambdaClient as LambdaClientMock, + InvokeCommandOutput, +} from '@aws-sdk/client-lambda'; + +jest.mock('@aws-sdk/client-lambda', () => { + const mLambda = { send: jest.fn() }; + const mInvokeCommand = jest.fn(); + return { + LambdaClient: jest.fn(() => mLambda), + InvokeCommand: mInvokeCommand, + }; +}); + +describe('shouldInvokeNftHandlerForTx', () => { + it('should return false for a NFT transaction if the feature is disabled', () => { + expect.hasAssertions(); + + // Preparation + const tx = getTransaction(); + const isNftTransaction = NftUtils.isTransactionNFTCreation(tx); + expect(isNftTransaction).toStrictEqual(true); + + expect(process.env.NFT_AUTO_REVIEW_ENABLED).not.toStrictEqual('true'); + + // Execution + const result = NftUtils.shouldInvokeNftHandlerForTx(tx); + + // Assertion + expect(result).toBe(false); + }); + + it('should return true for a NFT transaction if the feature is enabled', () => { + expect.hasAssertions(); + + // Preparation + const tx = getTransaction(); + const isNftTransaction = NftUtils.isTransactionNFTCreation(tx); + expect(isNftTransaction).toStrictEqual(true); + + const oldValue = process.env.NFT_AUTO_REVIEW_ENABLED; + process.env.NFT_AUTO_REVIEW_ENABLED = 'true'; + + // Execution + const result = NftUtils.shouldInvokeNftHandlerForTx(tx); + + // Assertion + expect(result).toBe(true); + + // Tearing Down + process.env.NFT_AUTO_REVIEW_ENABLED = oldValue; + }); +}); + +describe('isTransactionNFTCreation', () => { + it('should return false on quick validations', () => { + expect.hasAssertions(); + + // Preparing mocks + const spyCreateTx = jest.spyOn(hathorLib.helpersUtils, 'createTxFromHistoryObject'); + spyCreateTx.mockImplementation(() => ({})); + let tx; + let result; + + // Incorrect version + tx = getTransaction(); + tx.version = hathorLib.constants.DEFAULT_TX_VERSION; + result = NftUtils.isTransactionNFTCreation(tx); + expect(result).toBe(false); + expect(spyCreateTx).not.toHaveBeenCalled(); + + // Missing name + tx = getTransaction(); + tx.token_name = undefined; + result = NftUtils.isTransactionNFTCreation(tx); + expect(result).toBe(false); + expect(spyCreateTx).not.toHaveBeenCalled(); + + // Missing symbol + tx = getTransaction(); + tx.token_symbol = undefined; + result = NftUtils.isTransactionNFTCreation(tx); + expect(result).toBe(false); + expect(spyCreateTx).not.toHaveBeenCalled(); + + // Reverting mocks + spyCreateTx.mockRestore(); + }); + + it('should return true when the wallet-lib validation does not fail', () => { + expect.hasAssertions(); + + // Preparing mocks + const spyNftValidation = jest.spyOn(hathorLib.CreateTokenTransaction.prototype, 'validateNft'); + spyNftValidation.mockImplementation(() => undefined); + + // Validation + const tx = getTransaction(); + const result = NftUtils.isTransactionNFTCreation(tx); + expect(result).toBe(true); + + // Reverting mocks + spyNftValidation.mockRestore(); + }); + + it('should return true when the wallet-lib validation does not fail (unmocked)', () => { + expect.hasAssertions(); + + // Validation + const tx = getTransaction(); + const result = NftUtils.isTransactionNFTCreation(tx); + expect(result).toBe(true); + }); + + it('should return false when the wallet-lib validation throws', () => { + expect.hasAssertions(); + + // Preparing mocks + const spyNftValidation = jest.spyOn(hathorLib.CreateTokenTransaction.prototype, 'validateNft'); + spyNftValidation.mockImplementation(() => { + throw new Error('not a nft'); + }); + + // Validation + const tx = getTransaction(); + const result = NftUtils.isTransactionNFTCreation(tx); + expect(result).toBe(false); + + // Reverting mocks + spyNftValidation.mockRestore(); + }); +}); + +describe('createOrUpdateNftMetadata', () => { + const spyUpdateMetadata = jest.spyOn(NftUtils, '_updateMetadata'); + + afterEach(() => { + spyUpdateMetadata.mockReset(); + }); + + afterAll(() => { + // Clear mocks + spyUpdateMetadata.mockRestore(); + }); + + it('should request the create/update metadata with minimum nft data', async () => { + expect.hasAssertions(); + const expectedUpdateRequest = { id: 'sampleUid', nft: true }; + const expectedUpdateResponse = { updated: 'ok' }; + + spyUpdateMetadata.mockImplementation(async () => expectedUpdateResponse); + const result = await NftUtils.createOrUpdateNftMetadata('sampleUid'); + + expect(spyUpdateMetadata).toHaveBeenCalledTimes(1); + + expect(spyUpdateMetadata).toHaveBeenCalledWith('sampleUid', expectedUpdateRequest); + expect(result).toBeUndefined(); // The method returns void + }); +}); + +describe('_updateMetadata', () => { + it('should return the update lambda response on success', async () => { + expect.hasAssertions(); + + // Building the mock lambda + const expectedLambdaResponse = { + StatusCode: 202, + Payload: 'sampleData', + }; + + const mLambdaClient = new LambdaClientMock({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (mLambdaClient.send as jest.Mocked).mockImplementation( + async () => Promise.resolve(expectedLambdaResponse), + ); + const oldStage = process.env.STAGE; + process.env.STAGE = 'dev'; // Testing all code branches, including the developer ones, for increased coverage + + const result = await NftUtils._updateMetadata('sampleUid', { sampleData: 'fake' }); + expect(result).toStrictEqual(expectedLambdaResponse); + process.env.STAGE = oldStage; + }); + + it('should retry calling the update lambda a set number of times', async () => { + expect.hasAssertions(); + + // Building the mock lambda + let failureCount = 0; + const expectedLambdaResponse = { + StatusCode: 202, + Payload: 'sampleData', + }; + const mLambdaClient = new LambdaClientMock({}); + (mLambdaClient.send as jest.Mocked).mockImplementation(async () => { + if (failureCount < MAX_METADATA_UPDATE_RETRIES - 1) { + ++failureCount; + return { + StatusCode: 500, + Payload: 'failurePayload', + }; + } + return expectedLambdaResponse; + }); + + const result = await NftUtils._updateMetadata('sampleUid', { sampleData: 'fake' }); + expect(result).toStrictEqual(expectedLambdaResponse); + }); + + it('should throw after reaching retry count', async () => { + expect.hasAssertions(); + + // Building the mock lambda + let failureCount = 0; + const mLambdaClient = new LambdaClientMock({}); + (mLambdaClient.send as jest.Mocked).mockImplementation(() => { + if (failureCount < MAX_METADATA_UPDATE_RETRIES) { + ++failureCount; + return { + StatusCode: 500, + Payload: 'failurePayload', + }; + } + return { + StatusCode: 202, + Payload: 'sampleData', + }; + }); + + // eslint-disable-next-line jest/valid-expect + expect(NftUtils._updateMetadata('sampleUid', { sampleData: 'fake' })) + .rejects.toThrow(new Error('Metadata update failed for tx_id: sampleUid.')); + }); +}); + +describe('invokeNftHandlerLambda', () => { + it('should return the lambda response on success', async () => { + expect.hasAssertions(); + + // Building the mock lambda + const expectedLambdaResponse: InvokeCommandOutput = { + StatusCode: 202, + $metadata: {} + }; + const mLambdaClient = new LambdaClientMock({}); + (mLambdaClient.send as jest.Mocked).mockImplementationOnce(async () => expectedLambdaResponse); + + await expect(NftUtils.invokeNftHandlerLambda('sampleUid')).resolves.toBeUndefined(); + }); + + it('should throw when payload response status is invalid', async () => { + expect.hasAssertions(); + + // Building the mock lambda + const mLambdaClient = new LambdaClientMock({}); + const expectedLambdaResponse: InvokeCommandOutput = { + StatusCode: 500, + $metadata: {} + }; + (mLambdaClient.send as jest.Mocked).mockImplementation(() => expectedLambdaResponse); + + await expect(NftUtils.invokeNftHandlerLambda('sampleUid')) + .rejects.toThrow(new Error('onNewNftEvent lambda invoke failed for tx: sampleUid')); + + expect(mockedAddAlert).toHaveBeenCalledWith( + 'Error on NFTHandler lambda', + 'Erroed on invokeNftHandlerLambda invocation', + Severity.MINOR, + { TxId: 'sampleUid' }, + ); + }); +}); + +describe('minor helpers', () => { + it('should generate an event context', () => { + expect.hasAssertions(); + + const c = getHandlerContext(); + expect(c.done()).toBeUndefined(); + expect(c.fail('fail')).toBeUndefined(); + expect(c.getRemainingTimeInMillis()).toStrictEqual(0); + expect(c.succeed('pass')).toBeUndefined(); + }); +}); diff --git a/packages/wallet-service/tests/utils/pushnotification.utils.boundary.test.ts b/packages/wallet-service/tests/utils/pushnotification.utils.boundary.test.ts new file mode 100644 index 00000000..737bc236 --- /dev/null +++ b/packages/wallet-service/tests/utils/pushnotification.utils.boundary.test.ts @@ -0,0 +1,40 @@ +import { PushNotificationUtils } from '@src/utils/pushnotification.utils'; +import { SendNotificationToDevice } from '@src/types'; + +/** + * README + * To make this test work, you need to comment the line + * `'/tests/utils/pushnotification.utils.boundary.test.ts',` in jest.config.js. + * + * You need to configure the firebase environment variables in the .env file. + * + * ATTENTION! + * - The tests in this file are not run by default because they trigger real calls to FCM. + * - Do NOT use production configuration to run the tests. + */ + +/** + * Run the following test to send a notification to your device. + * @example + * npx jest --testPathPattern=pushnotification.utils.boundary.test.ts -t=sendToFcm + */ +test('sendToFcm', async () => { + expect.hasAssertions(); + + const buildNotification = (deviceId: string, metadata?: Record) => ({ + deviceId, + metadata: { + txId: '00c30fc8a1b9a326a766ab0351faf3635297d316fd039a0eda01734d9de40185', + bodyLocKey: 'new_transaction_received_description_without_tokens', + titleLocKey: 'new_transaction_received_title', + ...metadata, + }, + } as SendNotificationToDevice); + + // Go to the wallet-mobile and log the deviceId in the push notification saga initialization. + const notification = buildNotification(''); + + const result = await PushNotificationUtils.sendToFcm(notification); + + expect(result.success).toStrictEqual(true); +}); diff --git a/packages/wallet-service/tests/utils/pushnotification.utils.test.ts b/packages/wallet-service/tests/utils/pushnotification.utils.test.ts new file mode 100644 index 00000000..4ff58d5b --- /dev/null +++ b/packages/wallet-service/tests/utils/pushnotification.utils.test.ts @@ -0,0 +1,572 @@ +/* eslint-disable no-shadow */ +/* eslint-disable @typescript-eslint/naming-convention */ +// mocks should be imported first +import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; +import { sendMulticastMock, messaging, initFirebaseAdminMock } from '@tests/utils/firebase-admin.mock'; +import { logger } from '@tests/winston.mock'; +import { PushNotificationUtils, PushNotificationError, buildFunctionName, FunctionName } from '@src/utils/pushnotification.utils'; +import * as pushnotificationUtils from '@src/utils/pushnotification.utils'; +import { SendNotificationToDevice, Severity } from '@src/types'; +import { sendMock, lambdaInvokeCommandMock } from '@tests/utils/aws-sdk.mock'; +import { LambdaClient } from '@aws-sdk/client-lambda'; +import { buildWalletBalanceValueMap } from '@tests/utils'; + +const isFirebaseInitializedMock = jest.spyOn(pushnotificationUtils, 'isFirebaseInitialized'); + +describe('PushNotificationUtils', () => { + const initEnv = process.env; + + beforeEach(() => { + process.env = { + ...initEnv, + WALLET_SERVICE_LAMBDA_ENDPOINT: 'endpoint', + STAGE: 'stage', + ON_TX_PUSH_NOTIFICATION_REQUESTED_LAMBDA_ENDPOINT: 'endpoint', + FIREBASE_PROJECT_ID: 'projectId', + FIREBASE_PRIVATE_KEY_ID: 'private-key-id', + FIREBASE_PRIVATE_KEY: 'private-key', + FIREBASE_CLIENT_EMAIL: 'client-email', + FIREBASE_CLIENT_ID: 'client-id', + FIREBASE_AUTH_URI: 'https://accounts.google.com/o/oauth2/auth', + FIREBASE_TOKEN_URI: 'https://oauth2.googleapis.com/token', + FIREBASE_AUTH_PROVIDER_X509_CERT_URL: 'https://www.googleapis.com/oauth2/v1/certs', + FIREBASE_CLIENT_X509_CERT_URL: 'https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk.iam.gserviceaccount.com', + PUSH_ALLOWED_PROVIDERS: 'android,ios', + }; + initFirebaseAdminMock.mockReset(); + isFirebaseInitializedMock.mockReset(); + mockedAddAlert.mockReset(); + jest.resetModules(); + }); + + afterEach(() => { + process.env = initEnv; + }); + + // test firebase initialization error + it('firebase initialization error', async () => { + expect.hasAssertions(); + + // load local env + process.env.PUSH_NOTIFICATION_ENABLED = 'true'; + logger.error.mockReset(); + initFirebaseAdminMock.mockImplementation(() => { + throw new Error('Failed to parse private key: Error: Invalid PEM formatted message.'); + }); + + // reload module + await import('@src/utils/pushnotification.utils'); + + const resultMessageOfLastCallToLoggerError = logger.error.mock.calls[0][0]; + expect(resultMessageOfLastCallToLoggerError).toMatchInlineSnapshot('"Error initializing Firebase Admin SDK. ErrorMessage: Failed to parse private key: Error: Invalid PEM formatted message."'); + }); + + describe('process.env', () => { + it('WALLET_SERVICE_LAMBDA_ENDPOINT', async () => { + expect.hasAssertions(); + + // load local env + process.env.WALLET_SERVICE_LAMBDA_ENDPOINT = ''; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(mockedAddAlert).toHaveBeenLastCalledWith( + 'Lambda missing env variables', + 'Env missing the following variables WALLET_SERVICE_LAMBDA_ENDPOINT', + Severity.MINOR, + ); + }); + + it('STAGE', async () => { + expect.hasAssertions(); + + // load local env + process.env.STAGE = ''; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(mockedAddAlert).toHaveBeenLastCalledWith( + 'Lambda missing env variables', + 'Env missing the following variables STAGE', + Severity.MINOR, + ); + }); + + it('FIREBASE_PROJECT_ID', async () => { + expect.hasAssertions(); + + // load local env + process.env.FIREBASE_PROJECT_ID = ''; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(mockedAddAlert).toHaveBeenLastCalledWith( + 'Lambda missing env variables', + 'Env missing the following variables FIREBASE_PROJECT_ID', + Severity.MINOR, + ); + }); + + it('FIREBASE_PRIVATE_KEY_ID', async () => { + expect.hasAssertions(); + + // load local env + process.env.FIREBASE_PRIVATE_KEY_ID = ''; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(mockedAddAlert).toHaveBeenLastCalledWith( + 'Lambda missing env variables', + 'Env missing the following variables FIREBASE_PRIVATE_KEY_ID', + Severity.MINOR, + ); + }); + + it('FIREBASE_PRIVATE_KEY', async () => { + expect.hasAssertions(); + + // load local env + process.env.FIREBASE_PRIVATE_KEY = ''; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(mockedAddAlert).toHaveBeenLastCalledWith( + 'Lambda missing env variables', + 'Env missing the following variables FIREBASE_PRIVATE_KEY', + Severity.MINOR, + ); + }); + + // generate test for every comment below + it('FIREBASE_CLIENT_EMAIL', async () => { + expect.hasAssertions(); + + // load local env + process.env.FIREBASE_CLIENT_EMAIL = ''; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(mockedAddAlert).toHaveBeenLastCalledWith( + 'Lambda missing env variables', + 'Env missing the following variables FIREBASE_CLIENT_EMAIL', + Severity.MINOR, + ); + }); + + // FIREBASE_CLIENT_ID: 'client-id', + it('FIREBASE_CLIENT_ID', async () => { + expect.hasAssertions(); + + // load local env + process.env.FIREBASE_CLIENT_ID = ''; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(mockedAddAlert).toHaveBeenLastCalledWith( + 'Lambda missing env variables', + 'Env missing the following variables FIREBASE_CLIENT_ID', + Severity.MINOR, + ); + }); + + // FIREBASE_AUTH_URI: 'https://accounts.google.com/o/oauth2/auth', + it('FIREBASE_AUTH_URI', async () => { + expect.hasAssertions(); + + // load local env + process.env.FIREBASE_AUTH_URI = ''; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(mockedAddAlert).toHaveBeenLastCalledWith( + 'Lambda missing env variables', + 'Env missing the following variables FIREBASE_AUTH_URI', + Severity.MINOR, + ); + }); + + // FIREBASE_TOKEN_URI: 'https://oauth2.googleapis.com/token', + it('FIREBASE_TOKEN_URI', async () => { + expect.hasAssertions(); + + // load local env + process.env.FIREBASE_TOKEN_URI = ''; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(mockedAddAlert).toHaveBeenLastCalledWith( + 'Lambda missing env variables', + 'Env missing the following variables FIREBASE_TOKEN_URI', + Severity.MINOR, + ); + }); + + // FIREBASE_AUTH_PROVIDER_X509_CERT_URL: 'https://www.googleapis.com/oauth2/v1/certs', + it('FIREBASE_AUTH_PROVIDER_X509_CERT_URL', async () => { + expect.hasAssertions(); + + // load local env + process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL = ''; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(mockedAddAlert).toHaveBeenLastCalledWith( + 'Lambda missing env variables', + 'Env missing the following variables FIREBASE_AUTH_PROVIDER_X509_CERT_URL', + Severity.MINOR, + ); + }); + + // FIREBASE_CLIENT_X509_CERT_URL: 'https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk.iam.gserviceaccount.com', + it('FIREBASE_CLIENT_X509_CERT_URL', async () => { + expect.hasAssertions(); + + // load local env + process.env.FIREBASE_CLIENT_X509_CERT_URL = ''; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(mockedAddAlert).toHaveBeenLastCalledWith( + 'Lambda missing env variables', + 'Env missing the following variables FIREBASE_CLIENT_X509_CERT_URL', + Severity.MINOR, + ); + }); + + it('FIREBASE_PRIVATE_KEY-IIFE', async () => { + expect.hasAssertions(); + + // load local env + // env variables are of type string, by assigning a boolean value we can test the error handling + process.env.FIREBASE_PRIVATE_KEY = true as unknown as string; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(logger.error).toHaveBeenLastCalledWith('[ALERT] Error while parsing the env.FIREBASE_PRIVATE_KEY.'); + }); + + it('PUSH_ALLOWED_PROVIDERS', async () => { + expect.hasAssertions(); + + // load local env + process.env.PUSH_ALLOWED_PROVIDERS = ''; + + // reload module + await import('@src/utils/pushnotification.utils'); + + expect(logger.error).toHaveBeenLastCalledWith('[ALERT] env.PUSH_ALLOWED_PROVIDERS is empty.'); + }); + }); + + describe('sendToFcm(notification)', () => { + beforeEach(() => { + sendMulticastMock.mockReset(); + messaging.mockImplementation(() => ({ + sendMulticast: sendMulticastMock.mockReturnValue({ + failureCount: 0, + }), + })); + }); + + it('should return success false when firebase is not initialized', async () => { + expect.hasAssertions(); + + isFirebaseInitializedMock.mockReturnValue(false); + const notification = { + deviceId: 'device1', + title: 'New transaction', + description: 'You recieved 1 HTR.', + metadata: { + txId: 'tx1', + }, + } as SendNotificationToDevice; + const result = await PushNotificationUtils.sendToFcm(notification); + + expect(result).toStrictEqual({ success: false, errorMessage: 'Firebase not initialized.' }); + }); + + it('should return success true when succeed', async () => { + expect.hasAssertions(); + + isFirebaseInitializedMock.mockReturnValue(true); + const notification = { + deviceId: 'device1', + title: 'New transaction', + description: 'You recieved 1 HTR.', + metadata: { + txId: 'tx1', + }, + } as SendNotificationToDevice; + const result = await PushNotificationUtils.sendToFcm(notification); + + expect(result).toStrictEqual({ success: true }); + }); + + it('should return success false when deviceId is invalid', async () => { + expect.hasAssertions(); + + isFirebaseInitializedMock.mockReturnValue(true); + messaging.mockImplementation(() => ({ + sendMulticast: sendMulticastMock.mockReturnValue({ + responses: [ + { + error: { + code: 'token-not-registered', + }, + }, + ], + failureCount: 1, + }), + })); + + const notification = { + deviceId: 'device1', + title: 'New transaction', + description: 'You recieved 1 HTR.', + metadata: { + txId: 'tx1', + }, + } as SendNotificationToDevice; + const result = await PushNotificationUtils.sendToFcm(notification); + + expect(result).toStrictEqual({ success: false, errorMessage: PushNotificationError.INVALID_DEVICE_ID }); + }); + + it('should return success false with unknown error when failure is not treated', async () => { + expect.hasAssertions(); + + isFirebaseInitializedMock.mockReturnValue(true); + messaging.mockImplementation(() => ({ + sendMulticast: sendMulticastMock.mockReturnValue({ + responses: [ + { + error: { + code: 'any-other-code', + }, + }, + ], + failureCount: 1, + }), + })); + + const notification = { + deviceId: 'device1', + title: 'New transaction', + description: 'You recieved 1 HTR.', + metadata: { + txId: 'tx1', + }, + } as SendNotificationToDevice; + const result = await PushNotificationUtils.sendToFcm(notification); + + expect(result).toStrictEqual({ success: false, errorMessage: PushNotificationError.UNKNOWN }); + expect(logger.error).toHaveBeenLastCalledWith('Error while calling sendMulticast(message) of Firebase Cloud Message.', { error: { code: 'any-other-code' } }); + + expect(mockedAddAlert).toHaveBeenLastCalledWith( + 'Error on PushNotificationUtils', + 'Error while calling sendMulticast(message) of Firebase Cloud Message.', + Severity.MAJOR, + { error: { code: 'any-other-code' } }, + ); + }); + }); + + describe('invokeSendNotificationHandlerLambda(notification)', () => { + beforeEach(() => { + sendMock.mockReset(); + // default mock return value + sendMock.mockReturnValue({ + StatusCode: 202, + }); + }); + + it('should call lambda with success', async () => { + expect.hasAssertions(); + + // load local env + const fakeEndpoint = 'endpoint'; + process.env.WALLET_SERVICE_LAMBDA_ENDPOINT = fakeEndpoint; + const fakeStage = 'test'; + process.env.STAGE = fakeStage; + + // reload module + const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + + const notification = { + deviceId: 'device1', + title: 'New transaction', + description: 'You recieved 1 HTR.', + metadata: { + txId: 'tx1', + }, + } as SendNotificationToDevice; + + const result = await PushNotificationUtils.invokeSendNotificationHandlerLambda(notification); + + // a void method returns undefined + expect(result).toBeUndefined(); + + // assert Lambda constructor call + expect(LambdaClient).toHaveBeenCalledTimes(1); + expect(LambdaClient).toHaveBeenCalledWith({ + endpoint: fakeEndpoint, + region: 'local', + }); + + // assert lambda invoke call + expect(sendMock).toHaveBeenCalledTimes(1); + expect(lambdaInvokeCommandMock).toHaveBeenCalledWith({ + FunctionName: `hathor-wallet-service-${fakeStage}-sendNotificationToDevice`, + InvocationType: 'Event', + Payload: JSON.stringify(notification), + }); + }); + + it('should throw error when lambda invokation fails', async () => { + expect.hasAssertions(); + + // load local env + const fakeEndpoint = 'endpoint'; + process.env.WALLET_SERVICE_LAMBDA_ENDPOINT = fakeEndpoint; + const fakeStage = 'test'; + process.env.STAGE = fakeStage; + + // reload module + const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + + const notification = { + deviceId: 'device1', + title: 'New transaction', + description: 'You recieved 1 HTR.', + metadata: { + txId: 'tx1', + }, + } as SendNotificationToDevice; + + // simulate a failing lambda invokation + sendMock.mockReturnValue({ + StatusCode: 500, + }); + + await expect(PushNotificationUtils.invokeSendNotificationHandlerLambda(notification)) + .rejects.toThrow(`hathor-wallet-service-${fakeStage}-sendNotificationToDevice lambda invoke failed for device: ${notification.deviceId}`); + }); + + it('should throw error when env variables are not set', async () => { + expect.hasAssertions(); + + // load local env + const fakeEndpoint = ''; + process.env.WALLET_SERVICE_LAMBDA_ENDPOINT = fakeEndpoint; + const fakeStage = ''; + process.env.STAGE = fakeStage; + + // reload module + const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + + const notification = { + deviceId: 'device1', + title: 'New transaction', + description: 'You recieved 1 HTR.', + metadata: { + txId: 'tx1', + }, + } as SendNotificationToDevice; + + await expect(PushNotificationUtils.invokeSendNotificationHandlerLambda(notification)) + .rejects.toThrow('Environment variables WALLET_SERVICE_LAMBDA_ENDPOINT and STAGE are not set.'); + }); + }); + + describe('invokeOnTxPushNotificationRequestedLambda(walletBalanceValueMap)', () => { + it('should succeed', async () => { + expect.hasAssertions(); + + // clear counts + jest.clearAllMocks(); + // reload module + process.env.PUSH_NOTIFICATION_ENABLED = 'true'; + sendMock.mockReturnValueOnce({ + StatusCode: 202, + }); + const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + + const walletMap = buildWalletBalanceValueMap(); + const result = await PushNotificationUtils.invokeOnTxPushNotificationRequestedLambda(walletMap); + + // void method returns undefined + expect(result).toBeUndefined(); + + // assert Lambda constructor call + expect(LambdaClient).toHaveBeenCalledTimes(1); + expect(LambdaClient).toHaveBeenCalledWith({ + endpoint: process.env.ON_TX_PUSH_NOTIFICATION_REQUESTED_LAMBDA_ENDPOINT, + region: 'local', + }); + + // assert lambda invoke call + expect(sendMock).toHaveBeenCalledTimes(1); + expect(lambdaInvokeCommandMock).toHaveBeenCalledTimes(1); + expect(lambdaInvokeCommandMock).toHaveBeenCalledWith({ + FunctionName: buildFunctionName(FunctionName.ON_TX_PUSH_NOTIFICATION_REQUESTED), + InvocationType: 'Event', + Payload: JSON.stringify(walletMap), + }); + }); + + // it should not call lambda when push notification is disabled + it('should not call lambda when push notification is disabled', async () => { + expect.hasAssertions(); + + // clear counts + jest.clearAllMocks(); + // reload module + process.env.PUSH_NOTIFICATION_ENABLED = 'false'; + const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + + const walletMap = buildWalletBalanceValueMap(); + const result = await PushNotificationUtils.invokeOnTxPushNotificationRequestedLambda(walletMap); + + // void method returns undefined + expect(result).toBeUndefined(); + + // assert Lambda constructor call + expect(LambdaClient).toHaveBeenCalledTimes(0); + + // assert lambda invoke call + expect(sendMock).toHaveBeenCalledTimes(0); + + // assert log message + expect(logger.debug).toHaveBeenCalledWith('Push notification is disabled. Skipping invocation of OnTxPushNotificationRequestedLambda lambda.'); + }); + + it('should throw an error when invoke fails', async () => { + expect.hasAssertions(); + + const not202Code = 500; + // simulate a failing lambda invokation + sendMock.mockReturnValue({ + StatusCode: not202Code, + }); + + // reload module + process.env.PUSH_NOTIFICATION_ENABLED = 'true'; + const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + + const walletMap = buildWalletBalanceValueMap(); + await expect(PushNotificationUtils.invokeOnTxPushNotificationRequestedLambda(walletMap)).rejects.toMatchInlineSnapshot('[Error: hathor-wallet-service-stage-txPushRequested lambda invoke failed for wallets: wallet1]'); + }); + }); +}); diff --git a/packages/wallet-service/tests/winston.mock.ts b/packages/wallet-service/tests/winston.mock.ts new file mode 100644 index 00000000..47344fe6 --- /dev/null +++ b/packages/wallet-service/tests/winston.mock.ts @@ -0,0 +1,21 @@ +export const logger = { + debug: jest.fn(), + log: jest.fn(), + error: jest.fn(), +}; + +// IMPORTANT First mock winston +jest.mock('winston', () => ({ + format: { + colorize: jest.fn(), + combine: jest.fn(), + label: jest.fn(), + timestamp: jest.fn(), + printf: jest.fn(), + json: jest.fn(), + }, + createLogger: jest.fn().mockReturnValue(logger), + transports: { + Console: jest.fn(), + }, +})); diff --git a/packages/wallet-service/tests/ws.utils.test.ts b/packages/wallet-service/tests/ws.utils.test.ts new file mode 100644 index 00000000..0aaeeac2 --- /dev/null +++ b/packages/wallet-service/tests/ws.utils.test.ts @@ -0,0 +1,44 @@ +import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; +import { connectionInfoFromEvent } from '@src/ws/utils'; +import { Severity } from '@src/types'; + +test('connectionInfoFromEvent', async () => { + expect.hasAssertions(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const event = { + requestContext: { + connectionId: 'abc123', + domainName: 'dom123', + stage: 'test123', + }, + }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const connInfo = connectionInfoFromEvent(event); + expect(connInfo).toStrictEqual({ id: 'abc123', url: `https://${process.env.WS_DOMAIN}` }); +}); + +test('missing WS_DOMAIN should throw', () => { + expect.hasAssertions(); + + delete process.env.WS_DOMAIN; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const event = { + requestContext: { + connectionId: 'abc123', + domainName: 'dom123', + stage: 'test123', + }, + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => connectionInfoFromEvent(event)).toThrow('Domain not on env variables'); + expect(mockedAddAlert).toHaveBeenCalledWith( + 'Erroed while fetching connection info', + 'Domain not on env variables', + Severity.MINOR, + ); +}); diff --git a/packages/wallet-service/tsconfig.json b/packages/wallet-service/tsconfig.json new file mode 100644 index 00000000..f5098fa9 --- /dev/null +++ b/packages/wallet-service/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "lib": ["es2017"], + "removeComments": true, + "moduleResolution": "node", + "module": "commonjs", + "noUnusedLocals": false, + "noUnusedParameters": false, + "sourceMap": true, + "target": "es2017", + "outDir": "./dist", + "inlineSources": true, + "esModuleInterop": true, + "sourceRoot": "/", + "baseUrl": "./", + "resolveJsonModule": true, + "skipLibCheck": true, + "paths": { + "@src/*": ["src/*"], + "@tests/*": ["tests/*"], + "@events/*": ["events/*"] + } + }, + "include": [ + "src/*.ts", + "src/**/*.ts", + "tests" + ], + "exclude": [ + "node_modules/**/*", + ".serverless/**/*", + ".webpack/**/*", + "_warmup/**/*", + ".vscode/**/*" + ] +} diff --git a/packages/wallet-service/webpack.config.js b/packages/wallet-service/webpack.config.js new file mode 100644 index 00000000..b7bf98f5 --- /dev/null +++ b/packages/wallet-service/webpack.config.js @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const path = require('path'); +const slsw = require('serverless-webpack'); +const nodeExternals = require('webpack-node-externals'); + +module.exports = { + context: __dirname, + mode: slsw.lib.webpack.isLocal ? 'development' : 'production', + entry: slsw.lib.entries, + devtool: slsw.lib.webpack.isLocal ? 'eval-cheap-module-source-map' : 'source-map', + resolve: { + extensions: ['.mjs', '.json', '.ts'], + symlinks: false, + cacheWithContext: false, + alias: { + '@src': path.resolve(__dirname, './src'), + '@tests': path.resolve(__dirname, './tests'), + '@events': path.resolve(__dirname, './events'), + }, + }, + output: { + libraryTarget: 'commonjs', + path: path.join(__dirname, '.webpack'), + filename: '[name].js', + }, + target: 'node', + externals: [nodeExternals()], + module: { + rules: [ + // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` + { + test: /\.(tsx?)$/, + loader: 'ts-loader', + exclude: [ + [ + path.resolve(__dirname, 'node_modules'), + path.resolve(__dirname, '.serverless'), + path.resolve(__dirname, '.webpack'), + ], + ], + options: { + transpileOnly: true, + experimentalWatchApi: true, + }, + }, + ], + }, + plugins: [], +}; diff --git a/scripts/build-and-push-docker.sh b/scripts/build-and-push-docker.sh deleted file mode 100644 index fee8629e..00000000 --- a/scripts/build-and-push-docker.sh +++ /dev/null @@ -1,21 +0,0 @@ -set -e -set -o pipefail - -if [ -z "$AWS_ACCOUNT_ID" ]; then - echo "Please export a AWS_ACCOUNT_ID env var before running this"; - exit 1; -fi - -if [ -z "$DOCKER_IMAGE_TAG" ]; then - commit=`git rev-parse HEAD`; - timestamp=`date +%s`; - export DOCKER_IMAGE_TAG="dev-$commit-$timestamp"; -fi; - -echo $DOCKER_IMAGE_TAG; - -aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.eu-central-1.amazonaws.com; - -docker build -t $AWS_ACCOUNT_ID.dkr.ecr.eu-central-1.amazonaws.com/hathor-wallet-service-sync-daemon:$DOCKER_IMAGE_TAG .; - -docker push $AWS_ACCOUNT_ID.dkr.ecr.eu-central-1.amazonaws.com/hathor-wallet-service-sync-daemon:$DOCKER_IMAGE_TAG; diff --git a/scripts/build-daemon.sh b/scripts/build-daemon.sh new file mode 100644 index 00000000..1d8a29e9 --- /dev/null +++ b/scripts/build-daemon.sh @@ -0,0 +1,27 @@ +set -e +set -o pipefail + +if [ -z "$ACCOUNT_ID" ]; then + echo "Please export a ACCOUNT_ID env var before running this"; + exit 1; +fi + +# Fetch the image tag from the temp file, this should be filled if the stage +# is not `dev` +DOCKER_IMAGE_TAG=$(cat /tmp/docker_image_tag 2>/dev/null || echo "") + +if [ -z "$DOCKER_IMAGE_TAG" ]; then + commit=`git rev-parse HEAD`; + timestamp=`date +%s`; + # Default to the dev image tag + DOCKER_IMAGE_TAG="dev-$commit-$timestamp"; +fi; + +echo $DOCKER_IMAGE_TAG; +# Store the updated image tag in the tmp file so the upload stage is able to use +# it +echo $DOCKER_IMAGE_TAG > /tmp/docker_image_tag; + +aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.eu-central-1.amazonaws.com; + +docker build -t $ACCOUNT_ID.dkr.ecr.eu-central-1.amazonaws.com/hathor-wallet-service-sync-daemon:$DOCKER_IMAGE_TAG .; diff --git a/scripts/push-daemon.sh b/scripts/push-daemon.sh new file mode 100644 index 00000000..b186545b --- /dev/null +++ b/scripts/push-daemon.sh @@ -0,0 +1,15 @@ +set -e +set -o pipefail + +DOCKER_IMAGE_TAG=$(cat /tmp/docker_image_tag) + +if [ -z "$DOCKER_IMAGE_TAG" ]; then + echo "No docker image tag on tmp file at /tmp/docker_image_tag"; + exit 1; +fi + +echo $DOCKER_IMAGE_TAG; + +aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.eu-central-1.amazonaws.com; + +docker push $ACCOUNT_ID.dkr.ecr.eu-central-1.amazonaws.com/hathor-wallet-service-sync-daemon:$DOCKER_IMAGE_TAG; diff --git a/src/api/fullnode.ts b/src/api/fullnode.ts deleted file mode 100644 index fdfaebfb..00000000 --- a/src/api/fullnode.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Copyright (c) Hathor Labs and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { - Block, - FullBlock, - Input, - Output, - DecodedScript, - RawInput, - RawOutput, -} from '../types'; -import axios from 'axios'; -import { globalCache } from '../utils'; -import logger from '../logger'; - -const DEFAULT_SERVER = - process.env.DEFAULT_SERVER || - 'https://node1.foxtrot.testnet.hathor.network/v1a/'; - -/** - * Returns a transaction from the fullnode - * - * @param txId - The transaction id to be downloaded - * @param noCache - Ignores cached transactions - */ -export const downloadTx = async (txId: string, noCache: boolean = false) => { - if (!noCache && globalCache.get(txId)) { - return globalCache.get(txId); - } - - const response = await axios.get(`${DEFAULT_SERVER}transaction?id=${txId}`); - - if (!noCache) { - globalCache.set(txId, response.data); - } - - return response.data; -}; - -/** - * Returns a list of transactions on the mempool - * - */ -export const downloadMempool = async () => { - const response = await axios.get(`${DEFAULT_SERVER}mempool`); - - const data = response.data; - - if (!data.success) { - logger.error(data); - throw new Error('Mempool API failure'); - } - return data; -}; - -/** - * Returns a `FullBlock` downloaded from the full_node - * - * @param height - The block's height - */ -export const downloadBlockByHeight = async ( - height: number -): Promise => { - const response = await axios.get( - `${DEFAULT_SERVER}block_at_height?height=${height}` - ); - - const data = response.data; - - if (!data.success) { - throw new Error(`Block height ${height} download failed`); - } - - const responseBlock = data.block; - - const block: FullBlock = { - txId: responseBlock.tx_id as string, - version: responseBlock.version as number, - weight: responseBlock.weight as number, - timestamp: responseBlock.timestamp as number, - nonce: responseBlock.nonce as string, - inputs: responseBlock.inputs.map((input: RawInput) => { - const typedDecodedScript: DecodedScript = { - type: input.decoded.type as string, - address: input.decoded.address as string, - timelock: input.decoded.timelock - ? (input.decoded.timelock as number) - : null, - value: input.decoded.value ? (input.decoded.value as number) : null, - tokenData: input.decoded.token_data - ? (input.decoded.token_data as number) - : null, - }; - const typedInput: Input = { - txId: input.tx_id as string, - index: input.index as number, - value: input.value as number, - tokenData: input.token_data as number, - script: input.script as string, - decoded: typedDecodedScript, - token: input.token as string, - }; - - return typedInput; - }), - outputs: responseBlock.outputs.map( - (output: RawOutput): Output => { - const typedDecodedScript: DecodedScript = { - type: output.decoded.type as string, - address: output.decoded.address as string, - timelock: output.decoded.timelock - ? (output.decoded.timelock as number) - : null, - value: output.decoded.value ? (output.decoded.value as number) : null, - tokenData: output.decoded.token_data - ? (output.decoded.token_data as number) - : null, - }; - - const typedOutput: Output = { - value: output.value as number, - tokenData: output.token_data as number, - script: output.script as string, - decoded: typedDecodedScript, - token: output.token as string, - }; - - return typedOutput; - } - ), - parents: responseBlock.parents, - height: responseBlock.height as number, - }; - - return block; -}; - -/** - * Downloads a block from the full_node using the `block_at_height` API - * - * @param txId - The block txId - * @param noCache - Prevents downloading the block from cache as a reorg may have ocurred - */ -export const getBlockByTxId = async ( - txId: string, - noCache: boolean = false -) => { - return downloadTx(txId, noCache); -}; - -/** - * Returns the best block from the full_node as a typed `Block`. - * TODO FIXME: Change this method to query the best block from the `/v1a/get_block_template` or - * a specialized API from the full_node to query its best block. - */ -export const getFullNodeBestBlock = async (): Promise => { - const response = await axios.get( - `${DEFAULT_SERVER}transaction?type=block&count=1` - ); - const { transactions } = response.data; - - const bestBlock: Block = { - txId: transactions[0].tx_id as string, - height: transactions[0].height as number, - }; - - return bestBlock; -}; diff --git a/src/api/lambda.ts b/src/api/lambda.ts deleted file mode 100644 index 4bb91157..00000000 --- a/src/api/lambda.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Copyright (c) Hathor Labs and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import AWS from 'aws-sdk'; -import logger from '../logger'; -import { PreparedTx, ApiResponse, Block, Severity } from '../types'; - -AWS.config.update({ - region: process.env.AWS_REGION, -}); - -/** - * Calls a function from the wallet-service lambda - * - * @param fnName - The lambda function name - * @param payload - The payload to be sent - */ -export const lambdaCall = (fnName: string, payload: any): Promise => - new Promise((resolve, reject) => { - const lambda = new AWS.Lambda({ - apiVersion: '2015-03-31', - endpoint: - process.env.WALLET_SERVICE_STAGE === 'local' - ? process.env.WALLET_SERVICE_LOCAL_URL || 'http://localhost:3002' - : `https://lambda.${process.env.AWS_REGION}.amazonaws.com`, - }); - - const params = { - FunctionName: `${process.env.WALLET_SERVICE_NAME}-${process.env.WALLET_SERVICE_STAGE}-${fnName}`, - Payload: JSON.stringify({ - body: payload, - }), - }; - - lambda.invoke(params, (err, data) => { - if (err) { - logger.error( - `Erroed on ${fnName} method call with payload: ${JSON.stringify(payload)}` - ); - logger.error(err); - reject(err); - } else { - if (data.StatusCode !== 200) { - reject(new Error('Request failed.')); - } - - try { - const responsePayload = JSON.parse(data.Payload as string); - const body = JSON.parse(responsePayload.body); - - resolve(body); - } catch (e) { - logger.error( - `Erroed on lambda call to ${fnName} with payload: ${JSON.stringify( - payload - )}` - ); - logger.error(e); - - return reject(e.message); - } - } - }); - }); - - -/** - * Adds a message to the SQS alerting queue - * - * @param fnName - The lambda function name - * @param payload - The payload to be sent - */ -export const addAlert = async (title: string, message: string, severity: Severity, metadata?: any): Promise => { - const preparedMessage = { - title, - message, - severity, - metadata, - environment: process.env.NETWORK, - application: process.env.APPLICATION_NAME, - }; - - const sqs = new AWS.SQS({ apiVersion: '2015-03-31' }); - - const params = { - MessageBody: JSON.stringify(preparedMessage), - QueueUrl: process.env.ALERT_QUEUE_URL as string, - MessageAttributes: { - None: { - DataType: 'String', - StringValue: '--', - }, - } - }; - - sqs.sendMessage(params, (err) => { - if (err) { - logger.error('[ALERT] Erroed while sending message to the alert sqs queue'); - logger.error(err); - } - }); -} - -/** - * Calls the onHandleReorgRequest lambda function - */ -export const invokeReorg = async (): Promise => - lambdaCall('onHandleReorgRequest', {}); - -/** - * Calls the onNewTxRequest lambda function with a PreparedTx - * - * @param tx - The prepared transaction to be sent - */ -export const sendTx = async (tx: PreparedTx): Promise => - lambdaCall('onNewTxRequest', tx); - -/** - * Calls the getLatestBlock lambda function from the wallet-service returning - * a typed `Block`. - */ -export const getWalletServiceBestBlock = async (): Promise => { - const response = await lambdaCall('getLatestBlock', {}); - const bestBlock: Block = response.block; - - return bestBlock; -}; diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 44a5b3a5..00000000 --- a/src/index.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) Hathor Labs and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { interpret } from 'xstate'; -import { SyncMachine } from './machine'; -// @ts-ignore -import { Connection } from '@hathor/wallet-lib'; -import { addAlert } from './api/lambda'; -import { Severity } from './types'; - -import logger from './logger'; - -// @ts-ignore -const machine = interpret(SyncMachine).onTransition(state => { - if (state.changed) { - logger.debug('Transitioned to state: ', state.value); - } -}); - -const handleMessage = (message: any) => { - switch (message.type) { - case 'dashboard:metrics': - break; - - /* This message is only being used as a signal that a new block may have arrived - * the sync mechanism will download all blocks from the current height until the - * full node's best block height - */ - case 'network:new_tx_accepted': - if (message.is_voided) return; - if (!message.is_block) { - // identify the tx as a mempool tx - if (message.first_block) return; - machine.send({ type: 'MEMPOOL_UPDATE' }); - return; - } - machine.send({ type: 'NEW_BLOCK' }); - break; - - case 'state_update': - /* This handles state updates from the websocket connection. - * We will trigger a re-sync (by sending the NEW_BLOCK event) to - * the machine, triggering a download if new blocks were generated. - */ - if (message.state === Connection.CONNECTED) { - logger.info('Websocket connected.'); - machine.send({ type: 'NEW_BLOCK' }); - } - if (message.state === Connection.CONNECTING) { - logger.info( - `Websocket is attempting to connect to ${process.env.DEFAULT_SERVER}` - ); - } - if (message.state === Connection.CLOSED) { - logger.error('Websocket connection was closed.'); - } - break; - } -}; - -const DEFAULT_SERVER = process.env.DEFAULT_SERVER; -const conn = new Connection({ - network: process.env.NETWORK, - servers: [DEFAULT_SERVER], -}); - -// @ts-ignore -conn.websocket.on('network', message => handleMessage(message)); -// @ts-ignore -conn.on('state', state => - handleMessage({ - type: 'state_update', - state, - }) -); -// @ts-ignore -conn.websocket.on('connection_error', evt => { - addAlert( - 'Failed to send block transaction', - `WebSocket connection error: ${evt.message}`, - Severity.MINOR, - ); - logger.error(`Websocket connection error: ${evt.message}`); -}); - -machine.start(); -conn.start(); diff --git a/src/logger.ts b/src/logger.ts deleted file mode 100644 index cbd265b7..00000000 --- a/src/logger.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) Hathor Labs and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import util from 'util'; -import * as winston from 'winston'; - -const CONSOLE_LEVEL = process.env.CONSOLE_LEVEL || 'info'; - -const myFormat = winston.format.printf( - ({ level, message, service, timestamp, ...args }) => { - let argsStr = ''; - - if (Object.keys(args).length > 0) { - // Adapted from https://github.com/winstonjs/logform/blob/master/pretty-print.js - const stripped = Object.assign({}, args); - - const levelSymbol = Symbol.for('level'); - const messageSymbol = Symbol.for('message'); - const splatSymbol = Symbol.for('splat'); - - // Typing Symbol as any is a workaround for https://github.com/microsoft/TypeScript/issues/1863 - delete stripped[levelSymbol as any]; - delete stripped[messageSymbol as any]; - delete stripped[splatSymbol as any]; - - argsStr = util.inspect(stripped, { - compact: true, - breakLength: Infinity, - }); - } - - return `${timestamp} [${service}] ${level}: ${message} ${argsStr}`; - } -); - -const transports = [ - new winston.transports.Console({ - format: winston.format.combine(winston.format.colorize(), myFormat), - level: CONSOLE_LEVEL, - }), -]; - -const logger = winston.createLogger({ - format: winston.format.combine( - winston.format.timestamp(), - winston.format.errors({ stack: true }) - ), - defaultMeta: { service: 'wallet-service-daemon' }, - transports: transports, -}); - -export default logger; diff --git a/src/machine.ts b/src/machine.ts deleted file mode 100644 index 074a5973..00000000 --- a/src/machine.ts +++ /dev/null @@ -1,274 +0,0 @@ -/** - * Copyright (c) Hathor Labs and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { Machine, assign, send } from 'xstate'; -import { syncToLatestBlock, syncLatestMempool } from './utils'; -import { - GeneratorYieldResult, - HandlerEvent, - StatusEvent, - MempoolEvent, - SyncContext, - SyncSchema, - Severity, -} from './types'; -import logger from './logger'; -import { invokeReorg, addAlert } from './api/lambda'; - -// @ts-ignore -export const syncHandler = () => (callback, onReceive) => { - logger.debug('Sync handler instantiated'); - const iterator = syncToLatestBlock(); - const asyncCall: () => void = async () => { - for (;;) { - const block: GeneratorYieldResult = await iterator.next(); - const { value, done } = block; - - if (done) { - // The generator reached its end, we should end this handler - logger.debug('Done.', value); - break; - } - - if (value && !value.success) { - if (value.type === 'reorg') { - logger.warn('A reorg happened: ', value.message); - callback('REORG'); - return; - } - - logger.error(value.message); - - callback('ERROR'); - return; - } - - if (value.type === 'finished') { - logger.info('Sync generator finished.'); - callback('DONE'); - } else if (value.type === 'block_success') { - logger.info( - `Block id: ${value.blockId} sent successfully, transactions sent: ${value.transactions.length}` - ); - } else { - logger.warn( - `Unhandled type received from sync generator: ${value.type}` - ); - } - } - - return; - }; - - /* onReceive is used for bi-directional communication between the - * machine and the invoked service (syncHandler). - * - * For now, the only message we are handling is the start event, to indicate - * that we should start the async promise dealing with the generator. - */ - onReceive((e: HandlerEvent) => { - if (e.type === 'START') { - asyncCall(); - } - }); - - return () => { - logger.debug('Stopping the iterator.'); - iterator.return('finished'); - - return; - }; -}; - -// @ts-ignore -export const mempoolHandler = () => (callback, onReceive) => { - logger.debug('Mempool handler instantiated'); - const iterator = syncLatestMempool(); - const asyncCall: () => void = async () => { - for (;;) { - const txResult: GeneratorYieldResult = await iterator.next(); - const { value, done } = txResult; - - if (done) { - // The generator reached its end, we should end this handler - logger.debug('Done.', value); - break; - } - - if (value && !value.success) { - logger.error(value.message); - callback('ERROR'); - return; - } - - if (value.type === 'finished') { - logger.info('Sync generator finished.'); - callback('DONE'); - return; - } else if (value.type === 'tx_success') { - logger.info('Mempool tx synced!'); - } else { - logger.warn( - `Unhandled type received from sync generator: ${value.type}` - ); - } - } - - return; - }; - - onReceive((e: HandlerEvent) => { - if (e.type === 'START') { - asyncCall(); - } - }); - - return () => { - logger.debug('Stopping the iterator.'); - iterator.return('finished'); - }; -}; - -/* See README for an explanation on how the machine works. - * TODO: We need to type the Event - */ -export const SyncMachine = Machine( - { - id: 'sync', - initial: 'idle', - context: { - hasMoreBlocks: false, - hasMempoolUpdate: false, - }, - states: { - idle: { - always: [ - // Conditions are tested in order, the first valid one is taken, if any are valid - // https://xstate.js.org/docs/guides/guards.html#multiple-guards - { target: 'syncing', cond: 'hasMoreBlocks' }, - { target: 'mempoolsync', cond: 'hasMempoolUpdate' }, - ], - on: { - NEW_BLOCK: 'syncing', - MEMPOOL_UPDATE: 'mempoolsync', - }, - }, - mempoolsync: { - invoke: { - id: 'syncLatestMempool', - src: 'mempoolHandler', - }, - on: { - MEMPOOL_UPDATE: { - actions: ['setMempoolUpdate'], - }, - // Stop mempool sync when a block arrives - // this means that the mempool may not be fully synced when it leaves this state - // giving priority to blocks means the mempool may change between syncs - NEW_BLOCK: { - target: 'syncing', - // When block sync finishes, go back to mempool sync - actions: ['setMempoolUpdate'], - }, - STOP: 'idle', - DONE: 'idle', - // Errors on mempool sync are "ignored" since next sync (either block or mempool) should fix it - ERROR: 'idle', - }, - entry: [ - 'resetMempoolUpdate', - send('START', { - to: 'syncLatestMempool', - }), - ], - }, - syncing: { - invoke: { - id: 'syncToLatestBlock', - src: 'syncHandler', - }, - on: { - NEW_BLOCK: { - actions: ['setMoreBlocks'], - }, - STOP: 'idle', - DONE: 'idle', - ERROR: 'failure', - REORG: 'reorg', - }, - entry: [ - 'resetMoreBlocks', - send('START', { - to: 'syncToLatestBlock', - }), - ], - }, - reorg: { - invoke: { - id: 'invokeReorg', - src: (_context, _event) => async () => { - const response = await invokeReorg(); - - if (!response.success) { - logger.error(response); - throw new Error('Reorg failed'); - } - - return; - }, - onDone: { - target: 'idle', - }, - onError: { - target: 'failure', - }, - }, - }, - failure: { - type: 'final', - entry: ['logFailure'], - }, - }, - }, - { - guards: { - hasMoreBlocks: ctx => ctx.hasMoreBlocks, - hasMempoolUpdate: ctx => ctx.hasMempoolUpdate, - }, - actions: { - // @ts-ignore - logFailure: () => { - addAlert( - `Wallet Service sync stopped on ${process.env.NETWORK}`, - 'Machine transitioned to failure state', - process.env.NETWORK === 'mainnet' ? Severity.CRITICAL : Severity.MAJOR, - ); - logger.error('Machine transitioned to failure state.'); - }, - // @ts-ignore - resetMoreBlocks: assign({ - hasMoreBlocks: () => false, - }), - // @ts-ignore - setMoreBlocks: assign({ - hasMoreBlocks: () => true, - }), - // @ts-ignore - resetMempoolUpdate: assign({ - hasMempoolUpdate: () => false, - }), - // @ts-ignore - setMempoolUpdate: assign({ - hasMempoolUpdate: () => true, - }), - }, - services: { - syncHandler, - mempoolHandler, - }, - } -); diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index cf2452c4..00000000 --- a/src/types.ts +++ /dev/null @@ -1,322 +0,0 @@ -/** - * Copyright (c) Hathor Labs and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -export interface Block { - txId: string; - height: number; -} - -export interface DecodedScript { - type?: string; - address?: string; - timelock?: number | undefined | null; - value?: number | undefined | null; - tokenData?: number | undefined | null; -} - -export interface Input { - txId: string; - value: number; - tokenData: number; - script: string; - decoded: DecodedScript; - index: number; - token?: string | undefined | null; -} - -export interface Output { - value: number; - tokenData: number; - script: string; - decoded: DecodedScript; - token?: string | undefined | null; -} - -export interface Token { - uid: string; - // Hathor will return name: null and symbol: null - name: string | null; - symbol: string | null; -} - -export interface FullTx { - txId: string; - nonce: string; - timestamp: number; - version: number; - weight: number; - voided?: boolean; - parents: string[]; - tokenName?: string | null; - tokenSymbol?: string | null; - inputs: Input[]; - outputs: Output[]; - tokens?: Token[]; - height?: number; - raw?: string; -} - -export interface FullBlock { - txId: string; - nonce?: string; - timestamp: number; - version: number; - weight: number; - parents: string[]; - tokenName?: string | null; - tokenSymbol?: string | null; - inputs: Input[]; - outputs: Output[]; - tokens?: Token[]; - height: number; - raw?: string; -} - -export interface ApiResponse { - success: boolean; - message?: string; -} - -export interface DownloadBlockApiResponse extends ApiResponse { - block: FullBlock; -} - -export interface SyncSchema { - states: { - idle: {}; - mempoolsync: {}; - syncing: {}; - failure: {}; - reorg: {}; - }; -} - -export interface SyncContext { - hasMoreBlocks: boolean; - hasMempoolUpdate: boolean; - error?: {}; -} - -/* -TODO: This is not being used in the machine, we should type all events. -export type SyncEvent = - | { type: 'NEW_BLOCK'; message: any } - | { type: 'STOP' }; -*/ - -export interface HandlerEvent { - type: string; -} - -export type StatusEvent = - | { - type: 'finished'; - success: boolean; - message?: string; - } - | { - type: 'block_success'; - success: boolean; - height?: number; - blockId: string; - message?: string; - transactions: string[]; - } - | { - type: 'transaction_failure'; - success: boolean; - message?: string; - } - | { - type: 'reorg'; - success: boolean; - message?: string; - } - | { - type: 'error'; - success: boolean; - message?: string; - error?: string; - }; - -export type MempoolEvent = - | { - type: 'finished'; - success: boolean; - message?: string; - } - | { - type: 'tx_success'; - success: boolean; - txId: string; - message?: string; - } - | { - type: 'wait'; - success: boolean; - message?: string; - } - | { - type: 'error'; - success: boolean; - message?: string; - error?: string; - }; - -/* export interface StatusEvent { - type: string; - success: boolean; - blockId?: string; - height?: number; - transactions?: string[]; - message?: string; - error?: string; -}; */ - -export interface GeneratorYieldResult { - done?: boolean; - value: StatusEvent; -} - -export interface GeneratorYieldResult { - done?: boolean; - value: StatusEvent; -} - -export interface PreparedDecodedScript { - type: string; - address: string; - timelock?: number | undefined | null; - value?: number; - token_data?: number; -} - -export interface PreparedInput { - tx_id: string; - value: number; - token_data: number; - script: string; - decoded: PreparedDecodedScript; - index: number; - token: string; -} - -export interface PreparedOutput { - value: number; - token_data: number; - script: string; - token: string; - decoded: PreparedDecodedScript; -} - -export interface PreparedTx { - tx_id: string; - inputs: PreparedInput[]; - outputs: PreparedOutput[]; - timestamp: number; - version: number; - weight: number; - parents: string[]; - nonce?: string; - height?: number; - tokens?: Token[]; - token_name?: string | null; - token_symbol?: string | null; - raw?: string; -} - -export interface RawDecodedInput { - type: string; - address: string; - timelock?: number | null; - value: number; - token_data: number; -} - -/* Everything is optional because scripts that were not able to - * be decoded will be returned as {} - */ -export interface RawDecodedOutput { - type?: string; - address?: string; - timelock?: number | null; - value?: number; - token_data?: number; -} - -export interface RawInput { - value: number; - token_data: number; - script: string; - decoded: RawDecodedInput; - tx_id: string; - index: number; - token?: string | null; - spent_by?: string | null; -} - -export interface RawOutput { - value: number; - token_data: number; - script: string; - decoded: RawDecodedOutput; - token?: string | null; - spent_by?: string | null; -} - -export interface RawTx { - hash: string; - nonce: string; - timestamp: number; - version: number; - weight: number; - parents: string[]; - inputs: RawInput[]; - outputs: RawOutput[]; - tokens: Token[]; - token_name?: string | null; - token_symbol?: string | null; - raw: string; -} - -export interface Meta { - hash: string; - spent_outputs: any; - received_by: string[]; - children: string[]; - conflict_with: string[]; - voided_by: string[]; - twins: string[]; - accumulated_weight: number; - score: number; - height: number; - validation?: string; - first_block?: string | null; - first_block_height?: number | null; -} - -export interface RawTxResponse { - tx: RawTx; - meta: Meta; - success: boolean; - message?: string; - spent_outputs?: any; -} - -export enum Severity { - CRITICAL = 'critical', - MAJOR = 'major', - MEDIUM = 'medium', - MINOR = 'minor', - WARNING = 'warning', - INFO = 'info', -} - -export interface TxSendResult { - success: boolean; - message?: string; -} diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 349df085..00000000 --- a/src/utils.ts +++ /dev/null @@ -1,694 +0,0 @@ -/** - * Copyright (c) Hathor Labs and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { - Block, - ApiResponse, - FullBlock, - Input, - Output, - DecodedScript, - FullTx, - StatusEvent, - MempoolEvent, - PreparedTx, - PreparedInput, - PreparedOutput, - PreparedDecodedScript, - RawTxResponse, - TxSendResult, - RawTx, - RawInput, - RawOutput, - Severity, -} from './types'; -import { - downloadTx, - downloadMempool, - getBlockByTxId, - getFullNodeBestBlock, - downloadBlockByHeight, -} from './api/fullnode'; -import { getWalletServiceBestBlock, sendTx, addAlert } from './api/lambda'; -import dotenv from 'dotenv'; -// @ts-ignore -import { wallet } from '@hathor/wallet-lib'; -import logger from './logger'; -import { isNumber, isEmpty } from 'lodash'; - -dotenv.config(); - -export const IGNORE_TXS: Map = new Map([ - [ - 'mainnet', - [ - '000006cb93385b8b87a545a1cbb6197e6caff600c12cc12fc54250d39c8088fc', - '0002d4d2a15def7604688e1878ab681142a7b155cbe52a6b4e031250ae96db0a', - '0002ad8d1519daaddc8e1a37b14aac0b045129c01832281fb1c02d873c7abbf9', - ], - ], - [ - 'testnet', - [ - '0000033139d08176d1051fb3a272c3610457f0c7f686afbe0afe3d37f966db85', - '00e161a6b0bee1781ea9300680913fb76fd0fac4acab527cd9626cc1514abdc9', - '00975897028ceb037307327c953f5e7ad4d3f42402d71bd3d11ecb63ac39f01a', - ], - ], -]); - -const TX_CACHE_SIZE: number = - parseInt(process.env.TX_CACHE_SIZE as string) || 200; - -function getAlertSeverityForReorgSize(reorg_size: number) { - if (reorg_size < 3) return Severity.INFO; - else if (reorg_size < 5) return Severity.WARNING; - else if (reorg_size < 10) return Severity.MINOR; - else if (reorg_size < 20) return Severity.MEDIUM; - else if (reorg_size < 30) return Severity.MAJOR; - else return Severity.CRITICAL; -} - -/** - * Download and parse a tx by it's id - * - * @param txId - the id of the tx to be downloaded - */ -export const downloadTxFromId = async ( - txId: string, - noCache: boolean = false -): Promise => { - const network: string = process.env.NETWORK || 'mainnet'; - - // Do not download genesis transactions - if (IGNORE_TXS.has(network)) { - const networkTxs: string[] = IGNORE_TXS.get(network) as string[]; - - if (networkTxs.includes(txId)) { - // Skip - return null; - } - } - - const txData: RawTxResponse = await downloadTx(txId, noCache); - const { tx } = txData; - - return parseTx(tx); -}; - -/** - * Recursively downloads all transactions confirmed directly or indirectly by a block - * - * This method will go through the parent tree and the inputs tree downloading all transactions, - * while ignoring transactions confirmed by blocks with height < blockHeight - * - * NOTE: This operation will get slower and slower as the BFS dives into the funds and the confirmation DAGs - * - * @param blockId - The blockId to download the transactions - * @param blockHeight - The block height from the block we are downloading transactions from - * @param txIds - List of transactions to download - * @param data - Downloaded transactions, used while being called recursively - */ -export const recursivelyDownloadTx = async ( - blockId: string, - blockHeight: number, - txIds: string[] = [], - data = new Map() -): Promise> => { - if (txIds.length === 0) { - return data; - } - - const txId: string = txIds.pop() as string; - const network: string = process.env.NETWORK || 'mainnet'; - - if (IGNORE_TXS.has(network)) { - const networkTxs: string[] = IGNORE_TXS.get(network) as string[]; - - if (networkTxs.includes(txId)) { - // Skip - return recursivelyDownloadTx(blockId, blockHeight, txIds, data); - } - } - - const txData: RawTxResponse = await downloadTx(txId); - const { tx, meta } = txData; - const parsedTx: FullTx = parseTx(tx); - - if (parsedTx.parents.length > 2) { - // We downloaded a block, we should ignore it - return recursivelyDownloadTx(blockId, blockHeight, txIds, data); - } - - // Transaction was already confirmed by a different block - if (meta.first_block && meta.first_block !== blockId) { - let firstBlockHeight = meta.first_block_height; - - // This should never happen - // Using == to include `undefined` on this check - if (firstBlockHeight == null) { - throw new Error('Transaction was confirmed by a block but we were unable to detect its height'); - } - - // If the transaction was confirmed by an older block, ignore it as it was - // already sent to the wallet-service - if (firstBlockHeight < blockHeight) { - return recursivelyDownloadTx(blockId, blockHeight, txIds, data); - } - } - - const inputList = parsedTx.inputs.map((input) => input.txId); - const txList = [...parsedTx.parents, ...inputList]; - const txIdsSet = new Set(txIds); - - // check if we have already downloaded any of the transactions of the new list - const newTxIds = txList.filter(transaction => { - return ( - !txIdsSet.has(transaction) && - /* Removing the current tx from the list of transactions to download: */ - transaction !== txId && - /* Data works as our return list on the recursion and also as a "seen" list on the BFS. - * We don't want to download a transaction that is already on our seen list. */ - !data.has(transaction) - ); - }); - - const newData = data.set(parsedTx.txId, { - ...parsedTx, - voided: (meta.voided_by && meta.voided_by.length && meta.voided_by.length) > 0, - }); - - return recursivelyDownloadTx(blockId, blockHeight, [...txIds, ...newTxIds], newData); -}; - -/** - * Prepares a transaction to be sent to the wallet-service `onNewTxRequest` - * - * @param tx - `FullTx` or `FullBlock` representing a typed transaction to be prepared - */ -export const prepareTx = (tx: FullTx | FullBlock): PreparedTx => { - const prepared = { - tx_id: tx.txId, - nonce: tx.nonce, - timestamp: tx.timestamp, - version: tx.version, - weight: tx.weight, - parents: tx.parents, - token_name: tx.tokenName, - token_symbol: tx.tokenSymbol, - height: tx.height, - inputs: tx.inputs.map(input => { - const baseInput: PreparedInput = { - tx_id: input.txId, - value: input.value, - token_data: input.tokenData, - script: input.script, - decoded: input.decoded as PreparedDecodedScript, - index: input.index, - token: '', - }; - - if (input.tokenData === 0) { - return { - ...baseInput, - token: '00', - }; - } - - if (!tx.tokens || tx.tokens.length <= 0) { - throw new Error( - 'Input is a token but there are no tokens in the tokens list.' - ); - } - - const { uid } = tx.tokens[ - wallet.getTokenIndex(input.decoded.tokenData) - 1 - ]; - - return { - ...baseInput, - token: uid, - }; - }), - outputs: tx.outputs.map(output => { - const baseOutput: PreparedOutput = { - value: output.value, - token_data: output.tokenData, - script: output.script, - token: '', - decoded: output.decoded as PreparedDecodedScript, - }; - - if (output.tokenData === 0) { - return { - ...baseOutput, - token: '00', - }; - } - - if (!tx.tokens || tx.tokens.length <= 0) { - throw new Error( - 'Output is a token but there are no tokens in the tokens list.' - ); - } - - if (!output.decoded || isEmpty(output.decoded) || !output.decoded.type) { - return baseOutput; - } - - const { uid } = tx.tokens[ - wallet.getTokenIndex(output.decoded.tokenData) - 1 - ]; - - return { - ...baseOutput, - token: uid, - }; - }), - tokens: tx.tokens, - raw: tx.raw, - }; - - return prepared; -}; - -/** - * Types a tx that was received from the full_node - * - * @param tx - The transaction object as received by the full_node - */ -export const parseTx = (tx: RawTx): FullTx => { - const parsedTx: FullTx = { - txId: tx.hash as string, - nonce: tx.nonce as string, - version: tx.version as number, - weight: tx.weight as number, - timestamp: tx.timestamp as number, - tokenName: tx.token_name ? (tx.token_name as string) : null, - tokenSymbol: tx.token_symbol ? (tx.token_symbol as string) : null, - inputs: tx.inputs.map((input: RawInput) => { - const typedDecodedScript: DecodedScript = { - type: input.decoded.type as string, - address: input.decoded.address as string, - timelock: isNumber(input.decoded.timelock) - ? (input.decoded.timelock as number) - : null, - value: isNumber(input.decoded.value) - ? (input.decoded.value as number) - : null, - tokenData: isNumber(input.decoded.token_data) - ? (input.decoded.token_data as number) - : null, - }; - const typedInput: Input = { - txId: input.tx_id as string, - index: input.index as number, - value: input.value as number, - tokenData: input.token_data as number, - script: input.script as string, - decoded: typedDecodedScript, - }; - - return typedInput; - }), - outputs: tx.outputs.map( - (output: RawOutput): Output => { - const typedDecodedScript: DecodedScript = { - type: output.decoded.type as string, - address: output.decoded.address as string, - timelock: isNumber(output.decoded.timelock) - ? (output.decoded.timelock as number) - : null, - value: isNumber(output.decoded.value) - ? (output.decoded.value as number) - : null, - tokenData: isNumber(output.decoded.token_data) - ? (output.decoded.token_data as number) - : null, - }; - - const typedOutput: Output = { - value: output.value as number, - tokenData: output.token_data as number, - script: output.script as string, - decoded: typedDecodedScript, - }; - - return typedOutput; - } - ), - parents: tx.parents, - tokens: tx.tokens, - raw: tx.raw as string, - }; - - return parsedTx; -}; - -/** - * Syncs the latest mempool - * - * @generator - * @yields {MempoolEvent} - */ -export async function* syncLatestMempool(): AsyncGenerator { - logger.info(`Downloading mempool.`); - let mempoolResp; - try { - mempoolResp = await downloadMempool(); - } catch (e) { - yield { - success: false, - type: 'error', - message: 'Could not download from mempool api', - }; - return; - } - - for (let i = 0; i < mempoolResp.transactions.length; i++) { - const tx = await downloadTxFromId(mempoolResp.transactions[i], true); // we don't want to cache mempool transactions - - if (tx === null) { - addAlert( - 'Failure to download tx from mempool', - `Failure to download transaction ${mempoolResp.transactions[i]} from mempool`, - Severity.WARNING, // Transaction will be downloaded again when it's confirmed by a block - { - 'MempoolLength': mempoolResp.transactions.length, - 'TxId': mempoolResp.transactions[i], - }, - ); - yield { - type: 'error', - success: false, - message: `Failure on transaction ${mempoolResp.transactions[i]} in mempool`, - }; - return; - } - - const preparedTx: PreparedTx = prepareTx(tx); - const sendTxResponse: TxSendResult = await sendTxHandler(preparedTx); - - if (!sendTxResponse.success) { - addAlert( - 'Failure to send mempool tx to the wallet service', - `Failure to send transaction ${tx.txId} from mempool to the wallet service`, - Severity.WARNING, // Transaction will be downloaded again when it's confirmed by a block - { - 'MempoolLength': mempoolResp.transactions.length, - 'TxId': mempoolResp.transactions[i], - }, - ); - - yield { - type: 'error', - success: false, - message: `Failure on transaction ${preparedTx.tx_id} in mempool: ${sendTxResponse.message}`, - }; - return; - } - - yield { - type: 'tx_success', - success: true, - txId: preparedTx.tx_id, - }; - } - - yield { - success: true, - type: 'finished', - }; -} - -/** - * Sends a transaction to the lambda and handle the response - */ -export async function sendTxHandler ( - preparedTx: PreparedTx, -): Promise { - try { - const sendTxResponse: ApiResponse = await sendTx(preparedTx); - - if (!sendTxResponse.success) { - logger.error(sendTxResponse.message); - return { - success: false, - message: sendTxResponse.message, - }; - } - - return { success: true }; - } catch(e) { - logger.error(e); - return { - success: false, - message: e.message, - }; - } -} - -/** - * Syncs to the latest block - * - * @generator - * @yields {StatusEvent} A status event indicating if a block at a height was successfully sent, \ - * if an error ocurred or if a reorg ocurred. - */ -export async function* syncToLatestBlock(): AsyncGenerator { - const ourBestBlock: Block = await getWalletServiceBestBlock(); - const fullNodeBestBlock: Block = await getFullNodeBestBlock(); - - // Check if our best block is still in the fullnode's chain - const ourBestBlockInFullNode = await getBlockByTxId(ourBestBlock.txId, true); - - if (!ourBestBlockInFullNode.success) { - logger.warn(ourBestBlockInFullNode.message); - - yield { - type: 'reorg', - success: false, - message: 'Best block not found in the full-node. Reorg?', - }; - - addAlert( - `Potential re-org on ${process.env.NETWORK}`, - `Best block not found in the full-node.`, - Severity.INFO, - { - 'Wallet Service best block': ourBestBlock.txId, - 'Fullnode best block': fullNodeBestBlock.txId, - }, - ); - - return; - } - - const { meta } = ourBestBlockInFullNode; - - if (meta.voided_by && meta.voided_by.length && meta.voided_by.length > 0) { - const reorgSize = fullNodeBestBlock.height - ourBestBlock.height; - - const severity = getAlertSeverityForReorgSize(reorgSize); - - addAlert( - `Re-org on ${process.env.NETWORK}`, - `The daemon's best block has been voided, handling re-org`, - severity, - { - 'Wallet Service best block': ourBestBlock.txId, - 'Fullnode best block': fullNodeBestBlock.txId, - 'Reorg size': reorgSize, - }, - ); - - yield { - type: 'reorg', - success: false, - message: 'Our best block was voided, we should reorg.', - }; - - return; - } - - if (ourBestBlock.height > meta.height) { - addAlert( - `Re-org on ${process.env.NETWORK}`, - `The downloaded height (${ourBestBlock.height}) is higher than the wallet service height (${meta.height})`, - Severity.INFO, - { - 'Wallet Service best block': ourBestBlock.txId, - 'Fullnode best block': fullNodeBestBlock.txId, - }, - ); - - yield { - type: 'reorg', - success: false, - message: `Our height is higher than the wallet-service\'s height, we should reorg.`, - }; - - return; - } - - logger.info( - `Downloading ${fullNodeBestBlock.height - ourBestBlock.height} blocks...` - ); - let success = true; - - blockLoop: for ( - let i = ourBestBlock.height + 1; - i <= fullNodeBestBlock.height; - i++ - ) { - const block: FullBlock = await downloadBlockByHeight(i); - const preparedBlock: PreparedTx = prepareTx(block); - - // Ignore parents[0] because it is a block - const blockTxs = [block.parents[1], block.parents[2]]; - - // Download block transactions - const txList: Map = await recursivelyDownloadTx( - block.txId, - block.height, - blockTxs, - ); - - const txs: FullTx[] = Array.from(txList.values()).sort( - (x, y) => x.timestamp - y.timestamp - ); - - // Exclude duplicates and voided transactions: - const uniqueTxs: Record = txs.reduce( - (acc: Record, tx: FullTx) => { - if (tx.voided) { - return acc; - } - - if (tx.txId in acc) { - return acc; - } - - return { - ...acc, - [tx.txId]: tx, - }; - }, - {} - ); - - for (const key of Object.keys(uniqueTxs)) { - const preparedTx: PreparedTx = prepareTx({ - ...uniqueTxs[key], - height: block.height, // this tx is confirmed by the current block on the loop, we must send its height - }); - - - const sendTxResponse: TxSendResult = await sendTxHandler(preparedTx); - - if (!sendTxResponse.success) { - addAlert( - 'Failed to send transaction', - `Failure on transaction ${preparedTx.tx_id} from block: ${preparedBlock.tx_id}`, - process.env.NETWORK === 'mainnet' ? Severity.CRITICAL : Severity.MAJOR, - ); - - yield { - type: 'transaction_failure', - success: false, - message: `Failure on transaction ${preparedTx.tx_id} from block: ${preparedBlock.tx_id}`, - }; - - success = false; - - break blockLoop; - } - } - - // We will send the block only after all transactions were sent - // to be sure that all downloads were succesfull since there is no - // ROLLBACK yet on the wallet-service. - const sendBlockResponse: TxSendResult = await sendTxHandler(preparedBlock); - - if (!sendBlockResponse.success) { - addAlert( - 'Failed to send block transaction', - `Failure on block ${preparedBlock.tx_id}`, - process.env.NETWORK === 'mainnet' ? Severity.CRITICAL : Severity.MAJOR, - ); - yield { - type: 'error', - success: false, - message: `Failure on block ${preparedBlock.tx_id}`, - }; - - success = false; - - break; - } - - yield { - type: 'block_success', - success: true, - blockId: preparedBlock.tx_id, - height: preparedBlock.height, - transactions: Object.keys(uniqueTxs), - }; - } - - yield { - success, - type: 'finished', - }; -} - -// Map remembers the insertion order, so we can use it as a FIFO queue -export class LRU { - max: number; - cache: Map; - - constructor(max: number = 10) { - this.max = max; - this.cache = new Map(); - } - - get(txId: string): any { - const transaction = this.cache.get(txId); - - if (transaction) { - this.cache.delete(txId); - // Refresh it in the Map - this.cache.set(txId, transaction); - } - - return transaction; - } - - set(txId: string, transaction: any): void { - if (this.cache.has(txId)) { - // Refresh it in the map - this.cache.delete(txId); - } - - // Remove oldest - if (this.cache.size === this.max) { - this.cache.delete(this.first()); - } - - this.cache.set(txId, transaction); - } - - first(): string { - return this.cache.keys().next().value; - } - - clear(): void { - this.cache = new Map(); - } -} - -export const globalCache = new LRU(TX_CACHE_SIZE); diff --git a/test/machine.test.ts b/test/machine.test.ts deleted file mode 100644 index b21f9f43..00000000 --- a/test/machine.test.ts +++ /dev/null @@ -1,364 +0,0 @@ -/** - * Copyright (c) Hathor Labs and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * @jest-environment node - */ - -import { interpret } from 'xstate'; -import { SyncMachine } from '../src/machine'; -import * as Lambda from '../src/api/lambda'; -import { Severity } from '../src/types'; - -beforeAll(async () => { - jest.clearAllMocks(); -}); - -test('SyncMachine should start as idle', async () => { - // @ts-ignore - const syncMachine = interpret(SyncMachine).start(); - - expect(syncMachine.state.value).toStrictEqual('idle'); -}, 100); - -test('An idle SyncMachine should transition to \'syncing\' when a NEW_BLOCK action is received', async () => { - const TestSyncMachine = SyncMachine.withConfig({ - services: { - syncHandler: () => () => { - return () => {}; - }, - }, - }); - - // @ts-ignore - const syncMachine = interpret(TestSyncMachine).start(); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); -}, 500); - -test('A SyncMachine in the syncing state should transition to \'failure\' when an ERROR event is received', async () => { - const addAlertSpy = jest.spyOn(Lambda, 'addAlert'); - const addAlertMock = addAlertSpy.mockReturnValue(Promise.resolve()); - - const TestSyncMachine = SyncMachine.withConfig({ - services: { - syncHandler: () => () => { - return () => {}; - }, - }, - }); - - // @ts-ignore - const syncMachine = interpret(TestSyncMachine).start(); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); - - syncMachine.send({ type: 'ERROR' }); - - expect(syncMachine.state.value).toStrictEqual('failure'); - - expect(addAlertMock).toHaveBeenCalledWith( - `Wallet Service sync stopped on ${process.env.NETWORK}`, - 'Machine transitioned to failure state', - process.env.NETWORK === 'mainnet' ? Severity.CRITICAL : Severity.MAJOR, - ); -}, 500); - -test('A SyncMachine in the syncing state should store hasMoreBlocks on context if a NEW_BLOCK event is received', async () => { - const TestSyncMachine = SyncMachine.withConfig({ - services: { - syncHandler: () => () => { - return () => {}; - }, - }, - }); - - // @ts-ignore - const syncMachine = interpret(TestSyncMachine).start(); - - expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(false); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(true); -}, 500); - -test('A SyncMachine should transition to \'idle\' when it is on \'syncing\' state and received \'DONE\'', async () => { - const TestSyncMachine = SyncMachine.withConfig({ - services: { - syncHandler: () => () => { - return () => {}; - }, - }, - }); - - // @ts-ignore - const syncMachine = interpret(TestSyncMachine).start(); - - expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(false); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); - - syncMachine.send({ type: 'DONE' }); - - expect(syncMachine.state.value).toStrictEqual('idle'); -}, 500); - -test('A SyncMachine should transition to \'syncing\' if hasMoreBlocks context is true on IDLE state entry', async () => { - const TestSyncMachine = SyncMachine.withConfig({ - services: { - syncHandler: () => () => { - return () => {}; - }, - }, - }); - - // @ts-ignore - const syncMachine = interpret(TestSyncMachine).start(); - - expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(false); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(true); - - syncMachine.send({ type: 'DONE' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); -}, 500); - -test('A SyncMachine should clear hasMoreBlocks from context when transitioning to \'syncing\'', async () => { - const TestSyncMachine = SyncMachine.withConfig({ - services: { - syncHandler: () => () => { - return () => {}; - }, - }, - }); - - // @ts-ignore - const syncMachine = interpret(TestSyncMachine).start(); - - expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(false); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); - - expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(false); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(true); - - syncMachine.send({ type: 'DONE' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); - - expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(false); -}, 500); - -test('A SyncMachine should call the cleanupFn on the syncHandler service when state is transitioned out of syncing', async () => { - const addAlertSpy = jest.spyOn(Lambda, 'addAlert'); - const addAlertMock = addAlertSpy.mockReturnValue(Promise.resolve()); - const mockCleanupFunction = jest.fn(); - - const TestSyncMachine = SyncMachine.withConfig({ - services: { - syncHandler: () => () => { - return mockCleanupFunction; - }, - }, - }); - - // @ts-ignore - const syncMachine = interpret(TestSyncMachine).start(); - - expect(mockCleanupFunction).toHaveBeenCalledTimes(0); - - expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(false); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); - - syncMachine.send({ type: 'DONE' }); - - expect(mockCleanupFunction).toHaveBeenCalledTimes(1); - - expect(syncMachine.state.value).toStrictEqual('idle'); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); - - syncMachine.send({ type: 'STOP' }); - - expect(mockCleanupFunction).toHaveBeenCalledTimes(2); - - expect(syncMachine.state.value).toStrictEqual('idle'); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); - - syncMachine.send({ type: 'ERROR' }); - - expect(syncMachine.state.value).toStrictEqual('failure'); - expect(addAlertMock).toHaveBeenCalledWith( - `Wallet Service sync stopped on ${process.env.NETWORK}`, - 'Machine transitioned to failure state', - process.env.NETWORK === 'mainnet' ? Severity.CRITICAL : Severity.MAJOR, - ); - - expect(mockCleanupFunction).toHaveBeenCalledTimes(3); -}, 500); - -test('The SyncMachine should transition to \'reorg\' state when a reorg is detected', async () => { - const mockCleanupFunction = jest.fn(); - const addAlertSpy = jest.spyOn(Lambda, 'addAlert'); - const invokeReorgSpy = jest.spyOn(Lambda, 'invokeReorg'); - addAlertSpy.mockReturnValue(Promise.resolve()); - invokeReorgSpy.mockReturnValue(Promise.resolve({ success: true })); - - const TestSyncMachine = SyncMachine.withConfig({ - services: { - syncHandler: () => () => { - return mockCleanupFunction; - }, - }, - }); - - // @ts-ignore - const syncMachine = interpret(TestSyncMachine).start(); - - expect(mockCleanupFunction).toHaveBeenCalledTimes(0); - - expect(syncMachine.state.context.hasMoreBlocks).toStrictEqual(false); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); - - syncMachine.send({ type: 'REORG' }); - - expect(syncMachine.state.value).toStrictEqual('reorg'); -}, 500); - -test('Mempool: transition to \'idle\' on ERROR event', async () => { - const mockCleanupFunction = jest.fn(); - - const TestSyncMachine = SyncMachine.withConfig({ - services: { - syncHandler: () => () => { - return mockCleanupFunction; - }, - }, - }); - - // @ts-ignore - const syncMachine = interpret(TestSyncMachine).start(); - - syncMachine.send({ type: 'MEMPOOL_UPDATE' }); - - expect(syncMachine.state.value).toStrictEqual('mempoolsync'); - - syncMachine.send({ type: 'ERROR' }); - - expect(syncMachine.state.value).toStrictEqual('idle'); -}, 500); - -test('Mempool: transition to \'idle\' on DONE event', async () => { - const mockCleanupFunction = jest.fn(); - - const TestSyncMachine = SyncMachine.withConfig({ - services: { - syncHandler: () => () => { - return mockCleanupFunction; - }, - }, - }); - - // @ts-ignore - const syncMachine = interpret(TestSyncMachine).start(); - - syncMachine.send({ type: 'MEMPOOL_UPDATE' }); - - expect(syncMachine.state.value).toStrictEqual('mempoolsync'); - - syncMachine.send({ type: 'DONE' }); - - expect(syncMachine.state.value).toStrictEqual('idle'); -}, 500); - -test('Mempool: transition to \'idle\' on STOP event', async () => { - const mockCleanupFunction = jest.fn(); - - const TestSyncMachine = SyncMachine.withConfig({ - services: { - syncHandler: () => () => { - return mockCleanupFunction; - }, - }, - }); - - // @ts-ignore - const syncMachine = interpret(TestSyncMachine).start(); - - syncMachine.send({ type: 'MEMPOOL_UPDATE' }); - - expect(syncMachine.state.value).toStrictEqual('mempoolsync'); - - syncMachine.send({ type: 'STOP' }); - - expect(syncMachine.state.value).toStrictEqual('idle'); -}, 500); - -test('Mempool: transition to \'syncing\' on NEW_BLOCK event and back to \'mempoolsync\' when DONE with block sync', async () => { - const mockCleanupFunction = jest.fn(); - - const TestSyncMachine = SyncMachine.withConfig({ - services: { - syncHandler: () => () => { - return mockCleanupFunction; - }, - }, - }); - - // @ts-ignore - const syncMachine = interpret(TestSyncMachine).start(); - - syncMachine.send({ type: 'MEMPOOL_UPDATE' }); - - expect(syncMachine.state.value).toStrictEqual('mempoolsync'); - - syncMachine.send({ type: 'NEW_BLOCK' }); - - expect(syncMachine.state.value).toStrictEqual('syncing'); - - expect(syncMachine.state.context.hasMempoolUpdate).toStrictEqual(true); - - syncMachine.send({ type: 'DONE' }); - - // Will go to idle and straight back to mempoolsync, setting hasMempoolUpdate to false - expect(syncMachine.state.context.hasMempoolUpdate).toStrictEqual(false); - expect(syncMachine.state.value).toStrictEqual('mempoolsync'); -}, 500); diff --git a/test/utils.test.ts b/test/utils.test.ts deleted file mode 100644 index ae089838..00000000 --- a/test/utils.test.ts +++ /dev/null @@ -1,523 +0,0 @@ -/** - * Copyright (c) Hathor Labs and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * @jest-environment node - */ -import { - OUR_BEST_BLOCK_API_RESPONSE_VOIDED, - OUR_BEST_BLOCK_API_RESPONSE, - BLOCK_BY_HEIGHT, - MOCK_TXS, - MOCK_FULL_TXS, - MOCK_NFT_TX, - MOCK_CREATE_TOKEN_TX, - generateBlock, -} from './utils'; - -import { FullTx, Severity } from '../src/types'; -import { prepareTx, parseTx } from '../src/utils'; -import * as Utils from '../src/utils'; -import * as FullNode from '../src/api/fullnode'; -import * as Lambda from '../src/api/lambda'; -import axios from 'axios'; -// @ts-ignore -import hathorLib from '@hathor/wallet-lib'; -const { globalCache, syncToLatestBlock, LRU } = Utils; -const { downloadTx } = FullNode; - -beforeEach(async () => { - jest.clearAllMocks(); - globalCache.clear(); -}); - -test('syncToLatestBlock should send transaction height for every block tx', async () => { - expect.hasAssertions(); - - const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn( - Lambda, - 'getWalletServiceBestBlock' - ); - const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); - const downloadBlockByHeightSpy = jest.spyOn( - FullNode, - 'downloadBlockByHeight' - ); - const recursivelyDownloadTxSpy = jest.spyOn(Utils, 'recursivelyDownloadTx'); - const sendTxSpy = jest.spyOn(Lambda, 'sendTx'); - - getFullNodeBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[0], 1)) - ); - getWalletServiceBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[1], 0)) - ); - getBlockByTxIdSpy.mockReturnValue( - Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE) - ); - downloadBlockByHeightSpy.mockReturnValue(Promise.resolve(BLOCK_BY_HEIGHT)); - recursivelyDownloadTxSpy.mockReturnValue( - Promise.resolve( - new Map([[MOCK_FULL_TXS[0].txId, MOCK_FULL_TXS[0]]]) - ) - ); - - const mockSendTxImplementation = jest.fn(tx => { - return Promise.resolve({ - success: true, - }); - }); - - const mockFn = sendTxSpy.mockImplementation(mockSendTxImplementation); - const iterator = syncToLatestBlock(); - - await iterator.next(); - - expect(mockFn).toHaveBeenCalledWith( - prepareTx({ - ...MOCK_FULL_TXS[0], - height: BLOCK_BY_HEIGHT.height, - }) - ); -}); - -test('syncToLatestBlockGen should yield an error when the latest block from the wallet-service is_voided', async () => { - expect.hasAssertions(); - - const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn( - Lambda, - 'getWalletServiceBestBlock' - ); - const addAlertSpy = jest.spyOn(Lambda, 'addAlert'); - const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); - const downloadBlockByHeightSpy = jest.spyOn( - FullNode, - 'downloadBlockByHeight' - ); - - getFullNodeBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[0], 1)) - ); - getWalletServiceBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[1], 0)) - ); - getBlockByTxIdSpy.mockReturnValue( - Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE_VOIDED) - ); - downloadBlockByHeightSpy.mockReturnValue(Promise.resolve(BLOCK_BY_HEIGHT)); - - const addAlertMock = addAlertSpy.mockReturnValue(Promise.resolve()); - const iterator = syncToLatestBlock(); - - const { - value: { type, success, message }, - } = await iterator.next(); - - expect(type).toStrictEqual('reorg'); - expect(success).toStrictEqual(false); - expect(message).toStrictEqual('Our best block was voided, we should reorg.'); - expect(addAlertMock).toHaveBeenCalledWith( - `Re-org on ${process.env.NETWORK}`, - 'The daemon\'s best block has been voided, handling re-org', - Severity.INFO, - { - 'Wallet Service best block': MOCK_TXS[1], - 'Fullnode best block': MOCK_TXS[0], - 'Reorg size': 1, - }, - ); -}, 500); - -test('syncToLatestBlockGen should yield an error when our best block height is higher than the fullnode\'s', async () => { - expect.hasAssertions(); - - const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn( - Lambda, - 'getWalletServiceBestBlock' - ); - const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); - const downloadBlockByHeightSpy = jest.spyOn( - FullNode, - 'downloadBlockByHeight' - ); - const addAlertSpy = jest.spyOn(Lambda, 'addAlert'); - - getWalletServiceBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[1], 3)) - ); - getFullNodeBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[0], 6)) - ); - getBlockByTxIdSpy.mockReturnValue( - Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE_VOIDED) - ); - downloadBlockByHeightSpy.mockReturnValue(Promise.resolve(BLOCK_BY_HEIGHT)); - - const addAlertMock = addAlertSpy.mockReturnValue(Promise.resolve()); - const iterator = syncToLatestBlock(); - - const { - value: { type, success, message }, - } = await iterator.next(); - - expect(type).toStrictEqual('reorg'); - expect(success).toStrictEqual(false); - expect(message).toStrictEqual('Our best block was voided, we should reorg.'); - - expect(addAlertMock).toHaveBeenCalledWith( - `Re-org on ${process.env.NETWORK}`, - 'The daemon\'s best block has been voided, handling re-org', - Severity.WARNING, - { - 'Wallet Service best block': '000001517136ab420446a80b212715160c4693deabfa72d1f2e99683fdcb845e', - 'Fullnode best block': '0000018b4b08ad8668a42af30185e4ff228b5d2afc41ce7ee5cb7a085342ffda', - 'Reorg size': 3, - }, - ); -}, 500); - -test('syncToLatestBlockGen should yield an error when it fails to send a block', async () => { - expect.hasAssertions(); - - const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn( - Lambda, - 'getWalletServiceBestBlock' - ); - const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); - const sendTxSpy = jest.spyOn(Lambda, 'sendTx'); - const addAlertSpy = jest.spyOn(Lambda, 'addAlert'); - const downloadBlockByHeightSpy = jest.spyOn( - FullNode, - 'downloadBlockByHeight' - ); - const recursivelyDownloadTxSpy = jest.spyOn(Utils, 'recursivelyDownloadTx'); - - getWalletServiceBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[1], 3)) - ); - getFullNodeBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[0], 6)) - ); - getBlockByTxIdSpy.mockReturnValue( - Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE) - ); - sendTxSpy.mockReturnValue( - Promise.resolve({ success: false, message: 'generic error message' }) - ); - downloadBlockByHeightSpy.mockReturnValue(Promise.resolve(BLOCK_BY_HEIGHT)); - recursivelyDownloadTxSpy.mockReturnValue( - Promise.resolve(new Map()) - ); - - const addAlertMock = addAlertSpy.mockReturnValue(Promise.resolve()); - const iterator = syncToLatestBlock(); - - const { - value: { type, success, message }, - } = await iterator.next(); - - expect(type).toStrictEqual('error'); - expect(success).toStrictEqual(false); - expect(message).toStrictEqual( - 'Failure on block 0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7' - ); - expect(addAlertMock).toHaveBeenCalledWith( - 'Failed to send block transaction', - 'Failure on block 0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7', - process.env.NETWORK === 'mainnet' ? Severity.CRITICAL : Severity.MAJOR, - ); -}, 500); - -test('syncToLatestBlock should handle errors properly if lambdaCall throws', async () => { - expect.hasAssertions(); - - const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn( - Lambda, - 'getWalletServiceBestBlock' - ); - const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); - const sendTxSpy = jest.spyOn(Lambda, 'sendTx'); - const addAlertSpy = jest.spyOn(Lambda, 'addAlert'); - const downloadBlockByHeightSpy = jest.spyOn( - FullNode, - 'downloadBlockByHeight' - ); - const recursivelyDownloadTxSpy = jest.spyOn(Utils, 'recursivelyDownloadTx'); - - getWalletServiceBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[1], 3)) - ); - getFullNodeBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[0], 6)) - ); - getBlockByTxIdSpy.mockReturnValue( - Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE) - ); - - sendTxSpy.mockReturnValue(Promise.reject(new Error('generic error message'))); - downloadBlockByHeightSpy.mockReturnValue(Promise.resolve(BLOCK_BY_HEIGHT)); - recursivelyDownloadTxSpy.mockReturnValue( - Promise.resolve(new Map()) - ); - - const addAlertMock = addAlertSpy.mockReturnValue(Promise.resolve()); - const iterator = syncToLatestBlock(); - - const { - value: { type, success, message }, - } = await iterator.next(); - - expect(type).toStrictEqual('error'); - expect(success).toStrictEqual(false); - expect(message).toStrictEqual( - 'Failure on block 0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7' - ); - expect(addAlertMock).toHaveBeenCalledWith( - 'Failed to send block transaction', - 'Failure on block 0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7', - process.env.NETWORK === 'mainnet' ? Severity.CRITICAL : Severity.MAJOR, - ); -}, 500); - -test('syncToLatestBlockGen should yield an error when it fails to send a transaction', async () => { - expect.hasAssertions(); - - const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn( - Lambda, - 'getWalletServiceBestBlock' - ); - const addAlertSpy = jest.spyOn(Lambda, 'addAlert'); - const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); - const sendTxSpy = jest.spyOn(Lambda, 'sendTx'); - const downloadBlockByHeightSpy = jest.spyOn( - FullNode, - 'downloadBlockByHeight' - ); - const recursivelyDownloadTxSpy = jest.spyOn(Utils, 'recursivelyDownloadTx'); - - getWalletServiceBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[1], 3)) - ); - getFullNodeBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[0], 6)) - ); - getBlockByTxIdSpy.mockReturnValue( - Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE) - ); - const addAlertMock = addAlertSpy.mockReturnValue(Promise.resolve()); - // sendTxSpy.mockReturnValue(Promise.resolve({ success: false, message: 'generic error message' })); - downloadBlockByHeightSpy.mockReturnValue(Promise.resolve(BLOCK_BY_HEIGHT)); - recursivelyDownloadTxSpy.mockReturnValue( - Promise.resolve( - new Map([ - [MOCK_FULL_TXS[0].txId as string, MOCK_FULL_TXS[0] as FullTx], - ]) - ) - ); - - const mockSendTxImplementation = jest.fn(tx => { - if (hathorLib.helpers.isBlock(tx)) { - // is block - return Promise.resolve({ - success: true, - }); - } - - // is tx - return Promise.resolve({ - success: false, - message: 'generic send tx error message', - }); - }); - - sendTxSpy.mockImplementation(mockSendTxImplementation); - - const iterator = syncToLatestBlock(); - - const { - value: { type, success, message }, - } = await iterator.next(); - - expect(type).toStrictEqual('transaction_failure'); - expect(success).toStrictEqual(false); - expect(message).toStrictEqual( - 'Failure on transaction 0000000033a3bb347e0401d85a70b38f0aa7b5e37ea4c70d7dacf8e493946e64 from block: 0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7' - ); - expect(addAlertMock).toHaveBeenCalledWith( - 'Failed to send transaction', - 'Failure on transaction 0000000033a3bb347e0401d85a70b38f0aa7b5e37ea4c70d7dacf8e493946e64 from block: 0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7', - process.env.NETWORK === 'mainnet' ? Severity.CRITICAL : Severity.MAJOR, - ); -}, 500); - -test('syncToLatestBlockGen should sync from our current height until the best block height', async () => { - expect.hasAssertions(); - - const getFullNodeBestBlockSpy = jest.spyOn(FullNode, 'getFullNodeBestBlock'); - const getWalletServiceBestBlockSpy = jest.spyOn( - Lambda, - 'getWalletServiceBestBlock' - ); - const getBlockByTxIdSpy = jest.spyOn(FullNode, 'getBlockByTxId'); - const sendTxSpy = jest.spyOn(Lambda, 'sendTx'); - const downloadBlockByHeightSpy = jest.spyOn( - FullNode, - 'downloadBlockByHeight' - ); - const recursivelyDownloadTxSpy = jest.spyOn(Utils, 'recursivelyDownloadTx'); - - getWalletServiceBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[1], 1)) - ); - getFullNodeBestBlockSpy.mockReturnValue( - Promise.resolve(generateBlock(MOCK_TXS[0], 3)) - ); - getBlockByTxIdSpy.mockReturnValue( - Promise.resolve(OUR_BEST_BLOCK_API_RESPONSE) - ); - sendTxSpy.mockReturnValue(Promise.resolve({ success: true, message: 'ok' })); - recursivelyDownloadTxSpy.mockReturnValue( - Promise.resolve(new Map()) - ); - - const mockBlockHeightImplementation = jest.fn((height: number) => { - return Promise.resolve({ - ...BLOCK_BY_HEIGHT, - height, - }); - }); - - downloadBlockByHeightSpy.mockImplementationOnce( - mockBlockHeightImplementation - ); - - const iterator = syncToLatestBlock(); - - const y1 = await iterator.next(); - expect(y1.value.success).toStrictEqual(true); - expect(y1.value.height).toStrictEqual(2); - expect(y1.value.type).toStrictEqual('block_success'); - - const y2 = await iterator.next(); - expect(y2.value.success).toStrictEqual(true); - expect(y2.value.height).toStrictEqual(3); - expect(y2.value.type).toStrictEqual('block_success'); - - const { value } = await iterator.next(); - expect(value.success).toStrictEqual(true); - expect(value.type).toStrictEqual('finished'); -}, 500); - -test('Dowload tx should cache transactions', async () => { - expect.hasAssertions(); - - const axiosGetSpy = jest.spyOn(axios, 'get'); - - const mockAxiosGetImplementation = jest.fn(url => { - const [_, txId] = url.split('='); - // is tx - return Promise.resolve({ - success: true, - data: { - tx_id: txId, - }, - }); - }); - - axiosGetSpy.mockImplementation(mockAxiosGetImplementation); - - await downloadTx('tx1'); - - const cachedTx = globalCache.get('tx1'); - - expect(cachedTx).toStrictEqual({ tx_id: 'tx1' }); -}, 500); - -test('Dowload tx should not cache transactions if noCache is set to true', async () => { - expect.hasAssertions(); - - const axiosGetSpy = jest.spyOn(axios, 'get'); - - const mockAxiosGetImplementation = jest.fn(url => { - const [_, txId] = url.split('='); - // is tx - return Promise.resolve({ - success: true, - data: { - tx_id: txId, - }, - }); - }); - - axiosGetSpy.mockImplementation(mockAxiosGetImplementation); - - await downloadTx('tx1', true); - - const cachedTx = globalCache.get('tx1'); - - expect(cachedTx).toStrictEqual(undefined); -}, 500); - -test('LRU cache', async () => { - expect.hasAssertions(); - - const cache = new LRU(3); - - cache.set('tx1', { tx_id: 'tx1' }); - cache.set('tx2', { tx_id: 'tx2' }); - cache.set('tx3', { tx_id: 'tx3' }); - - expect(cache.first()).toStrictEqual('tx1'); - - expect(cache.get('tx1')).toStrictEqual({ tx_id: 'tx1' }); - expect(cache.get('tx2')).toStrictEqual({ tx_id: 'tx2' }); - expect(cache.get('tx3')).toStrictEqual({ tx_id: 'tx3' }); - - cache.set('tx4', { tx_id: 'tx4' }); - - expect(cache.get('tx1')).toStrictEqual(undefined); - expect(cache.first()).toStrictEqual('tx2'); - - cache.set('tx5', { tx_id: 'tx5' }); - - expect(cache.get('tx2')).toStrictEqual(undefined); - expect(cache.first()).toStrictEqual('tx3'); - - cache.set('tx6', { tx_id: 'tx6' }); - - expect(cache.get('tx3')).toStrictEqual(undefined); - expect(cache.first()).toStrictEqual('tx4'); -}, 500); - -test('prepareTx on a CREATE_TOKEN tx should have token_name and token_symbol', async () => { - expect.hasAssertions(); - - const { tx } = MOCK_CREATE_TOKEN_TX; - const parsedTx = parseTx(tx); - const preparedTx = prepareTx(parsedTx); - - expect(preparedTx.token_name).toStrictEqual('XCoin'); - expect(preparedTx.token_symbol).toStrictEqual('XCN'); -}, 500); - -test('prepareTx on a NFT transaction should not throw', async () => { - expect.hasAssertions(); - - const { tx } = MOCK_NFT_TX; - const parsedTx = parseTx(tx); - const preparedTx = prepareTx(parsedTx); - - expect(preparedTx.outputs[0].decoded.type).toStrictEqual(undefined); - expect(preparedTx.outputs[0].value).toStrictEqual(1); - expect(preparedTx.outputs[0].token_data).toStrictEqual(0); -}, 500); diff --git a/test/utils.ts b/test/utils.ts deleted file mode 100644 index a6d849b5..00000000 --- a/test/utils.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { FullBlock, FullTx, Block, RawTxResponse } from '../src/types'; - -export const MOCK_TXS = [ - '0000018b4b08ad8668a42af30185e4ff228b5d2afc41ce7ee5cb7a085342ffda', - '000001517136ab420446a80b212715160c4693deabfa72d1f2e99683fdcb845e', - '0000018b4b08ad8668a42af30185e4ff228b5d2afc41ce7ee5cb7a085342ffda', - '00000154ac4fac94eaeafbecdca8d7e10e23953dd8250b0b154e5d2a31abc641', - '006358e9e1e2b22c0658f3f14a315cd8c10ef2fd5c12b6cf3be64557a90f5bd3', - '0034557890ad299a2d683459132c0b09aba219e9aac67fbd31028432594022d7', - '0000018b4b08ad8668a42af30185e4ff228b5d2afc41ce7ee5cb7a085342ffda', -]; - -export interface DecodedScript { - type: string; - address: string; - timelock?: number; -} - -export const MOCK_FULL_TXS: FullTx[] = [ - { - txId: '0000000033a3bb347e0401d85a70b38f0aa7b5e37ea4c70d7dacf8e493946e64', - nonce: '2553516830', - timestamp: 1615397872, - version: 1, - weight: 17.52710175798647, - parents: [ - '00000000d016d0d677b533b37efd958ecfa1feefa123721240e55d1dac499f1a', - '00000000911bc85c571d8d671f202ae6ac4d50043800e72672ccb65e925853a3', - ], - inputs: [ - { - value: 500, - tokenData: 1, - script: 'dqkU57viZuQ/P/Az3VqVQ9pxVi58uAmIrA==', - decoded: { - type: 'P2PKH', - address: 'HTeRZ6LksptwhxT1xxBuC8DxHWmpycHMEW', - timelock: null, - value: 500, - tokenData: 1, - }, - txId: - '000000005b7069bf187363f79df0b14763b60a9ead153a9eab51cdaf5b6283ec', - index: 1, - }, - ], - outputs: [ - { - value: 475, - tokenData: 1, - script: 'dqkUi2Jrejdrx0C6QW/osvQNIqltHFmIrA==', - decoded: { - type: 'P2PKH', - address: 'HKE8DLbXXbMAjvMAfkZRLAC16CaoCY38we', - timelock: null, - value: 475, - tokenData: 1, - }, - }, - { - value: 25, - tokenData: 1, - script: 'dqkUCXfQI6LZVe5cqOy274Aoolf6Q7SIrA==', - decoded: { - type: 'P2PKH', - address: 'H7PBzpvKSBjAhoWVwiAKJVgJr9ZKy2QhpS', - timelock: null, - value: 25, - tokenData: 1, - }, - }, - ], - tokens: [ - { - uid: '00000000f76262bb1cca969d952ac2f0e85f88ec34c31f26a13eb3c31e29d4ed', - name: 'Cathor', - symbol: 'CTHOR', - }, - ], - }, -]; - -export const generateBlock = (txId: string, height: number): Block => { - return { - txId, - height, - }; -}; - -export const OUR_BEST_BLOCK_API_RESPONSE = { - success: true, - tx: { - hash: '0000018b4b08ad8668a42af30185e4ff228b5d2afc41ce7ee5cb7a085342ffda', - nonce: '326066', - timestamp: 1617745066, - version: 0, - weight: 23.09092323788272, - parents: [ - '00000154ac4fac94eaeafbecdca8d7e10e23953dd8250b0b154e5d2a31abc641', - '006358e9e1e2b22c0658f3f14a315cd8c10ef2fd5c12b6cf3be64557a90f5bd3', - '0034557890ad299a2d683459132c0b09aba219e9aac67fbd31028432594022d7', - ], - inputs: [], - outputs: [], - tokens: [], - data: '', - height: 646026, - }, - meta: { - hash: '0000018b4b08ad8668a42af30185e4ff228b5d2afc41ce7ee5cb7a085342ffda', - spent_outputs: [], - received_by: [], - children: [ - '000000f4016a7402c71b772ad0bf91505d4083cb48723995bb3917d7cd0dd7cd', - ], - conflict_with: [], - voided_by: [], - twins: [], - accumulated_weight: 23.09092323788272, - score: 44.90233102099014, - height: 646026, - first_block: null, - validation: 'full', - }, - spent_outputs: {}, -}; - -export const OUR_BEST_BLOCK_API_RESPONSE_VOIDED = { - success: true, - tx: { - hash: '0000018b4b08ad8668a42af30185e4ff228b5d2afc41ce7ee5cb7a085342ffda', - nonce: '326066', - timestamp: 1617745066, - version: 0, - weight: 23.09092323788272, - parents: [ - '00000154ac4fac94eaeafbecdca8d7e10e23953dd8250b0b154e5d2a31abc641', - '006358e9e1e2b22c0658f3f14a315cd8c10ef2fd5c12b6cf3be64557a90f5bd3', - '0034557890ad299a2d683459132c0b09aba219e9aac67fbd31028432594022d7', - ], - inputs: [], - outputs: [], - tokens: [], - data: '', - height: 646026, - }, - meta: { - hash: '0000018b4b08ad8668a42af30185e4ff228b5d2afc41ce7ee5cb7a085342ffda', - spent_outputs: [], - received_by: [], - children: [ - '000000f4016a7402c71b772ad0bf91505d4083cb48723995bb3917d7cd0dd7cd', - ], - conflict_with: [], - voided_by: [ - '000000f4016a7402c71b772ad0bf91505d4083cb48723995bb3917d7cd0dd7cd', - ], - twins: [], - accumulated_weight: 23.09092323788272, - score: 44.90233102099014, - height: 646026, - first_block: null, - validation: 'full', - }, - spent_outputs: {}, -}; - -export const BLOCK_BY_HEIGHT: FullBlock = { - txId: '0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7', - version: 0, - weight: 21.0, - timestamp: 1596605949, - inputs: [], - outputs: [ - { - value: 6400, - tokenData: 0, - script: 'dqkUtneiAsjMwg/3ZaeJ/+i3kw0zZCWIrA==', - decoded: { - type: 'P2PKH', - address: 'WfJqB5SNHnkwXCCGLMBVPcwuVr94hq1oKH', - timelock: null, - }, - token: '00', - }, - ], - parents: [ - '000005cbcb8b29f74446a260cd7d36fab3cba1295ac9fe904795d7b064e0e53c', - '00975897028ceb037307327c953f5e7ad4d3f42402d71bd3d11ecb63ac39f01a', - '00e161a6b0bee1781ea9300680913fb76fd0fac4acab527cd9626cc1514abdc9', - ], - height: 3, -}; - -export const MOCK_CREATE_TOKEN_TX: RawTxResponse = { - success: true, - tx: { - hash: '0035db82f5993097515d5bcc9e869700d538332e017c7ff599c47f659ab63d42', - nonce: '180', - timestamp: 1620266110, - version: 2, - weight: 8.000001, - parents: [ - '00504c97802cc199e2e418aefdafd1a627fdc4cf6fc9e4198b916c2456bbb203', - '0063b3ec31f8ffe0ebcb465e6c1111e1e9700926ac4d504c74b74b1af9cc6aad', - ], - inputs: [ - { - value: 1, - token_data: 0, - script: 'dqkURCVU2U54vCcmN8UVMeIBKQ+ldayIrA==', - decoded: { - type: 'P2PKH', - address: 'WUtMYoi96nNVgf6i3Rq3GuvJkYsbkx3KDi', - timelock: null, - value: 1, - token_data: 2, - }, - tx_id: - '00504c97802cc199e2e418aefdafd1a627fdc4cf6fc9e4198b916c2456bbb203', - index: 1, - }, - ], - outputs: [ - { - value: 100, - token_data: 1, - script: 'dqkU1vXqQItRBKC9TwophPs9I5reNnOIrA==', - decoded: { - type: 'P2PKH', - address: 'WiGe5TRjhAsrYP2dxp1zsgvYZqcBjXdWmy', - timelock: null, - value: 100, - token_data: 1, - }, - }, - { - value: 1, - token_data: 129, - script: 'dqkUvKVTGtZCXV/Wmwxsdc47FUnf8f6IrA==', - decoded: { - type: 'P2PKH', - address: 'WfsVxwxZhrfKHSYCeqPubQkWaeBcWZJ1ox', - timelock: null, - value: 1, - token_data: 129, - }, - }, - { - value: 2, - token_data: 129, - script: 'dqkU6v6yo/94Z55pVSHPv+gTJWLln22IrA==', - decoded: { - type: 'P2PKH', - address: 'Wk6a7Xif6qYsprSzFmFhVXYrgQdqg7h1K6', - timelock: null, - value: 2, - token_data: 129, - }, - }, - ], - tokens: [ - { - uid: '0035db82f5993097515d5bcc9e869700d538332e017c7ff599c47f659ab63d42', - name: 'XCoin', - symbol: 'XCN', - }, - { - uid: '00', - name: null, - symbol: null, - }, - ], - token_name: 'XCoin', - token_symbol: 'XCN', - raw: '', - }, - meta: { - hash: '0035db82f5993097515d5bcc9e869700d538332e017c7ff599c47f659ab63d42', - spent_outputs: [ - [0, []], - [1, []], - [2, []], - ], - received_by: [], - children: [], - conflict_with: [], - voided_by: [], - twins: [], - accumulated_weight: 25.78875940418488, - score: 0.0, - height: 0, - first_block: - '000000bd45ecc5119963cc3fa03e894f574e69811eef266ed7c6a0d4c1e1806c', - }, - spent_outputs: {}, -}; - -export const MOCK_NFT_TX: RawTxResponse = { - success: true, - tx: { - hash: '0055c424b9038b0a8888b574ccdb1933a007fdfc15b91a4b38a48cc883b540bf', - nonce: '389', - timestamp: 1626187098, - version: 2, - weight: 8.0, - parents: [ - '0055b20066e8168ad8f05e82d66a34d19970cfb1861281735215cdd84744d842', - '00bb42880bd1183ce34df2185d1431f531a0a95af3556e368fa72e462edf7a9f', - ], - inputs: [ - { - value: 2, - token_data: 0, - script: 'dqkU8uf1ieRE8taN5bCNug5z5UHMO6eIrA==', - decoded: { - type: 'P2PKH', - address: 'WkpQH9t4ue4LbTQKAEWssiXnYHC8CyMp7J', - timelock: null, - value: 2, - token_data: 0, - }, - tx_id: - '0055b20066e8168ad8f05e82d66a34d19970cfb1861281735215cdd84744d842', - index: 1, - }, - ], - outputs: [ - { - value: 1, - token_data: 0, - script: - 'TFFodHRwczovL2lwZnMuaW8vaXBmcy9RbWJIdEZrWWlGSG5XdEV6bm01RFFHTVNOSmdwTExXeDdRNlBxdHAxb0NiQlpwL21ldGFkYXRhLmpzb26s', - decoded: {}, - }, - { - value: 2, - token_data: 129, - script: 'dqkUYpULlr3iJ6sZbP3YIfgL52fasneIrA==', - decoded: { - type: 'P2PKH', - address: 'WXfHeaEtr3fS9ex42V5chr2jY7wb5tdcWD', - timelock: null, - value: 2, - token_data: 129, - }, - }, - { - value: 1, - token_data: 1, - script: 'dqkUYpULlr3iJ6sZbP3YIfgL52fasneIrA==', - decoded: { - type: 'P2PKH', - address: 'WXfHeaEtr3fS9ex42V5chr2jY7wb5tdcWD', - timelock: null, - value: 1, - token_data: 1, - }, - }, - ], - tokens: [ - { - uid: '0055c424b9038b0a8888b574ccdb1933a007fdfc15b91a4b38a48cc883b540bf', - name: 'Furia Special Edition', - symbol: 'DPL9', - }, - ], - token_name: 'Furia Special Edition', - token_symbol: 'DPL9', - raw: - '000201030055b20066e8168ad8f05e82d66a34d19970cfb1861281735215cdd84744d8420100694630440220692c2a95bbb335729520bc1717d9b6da7361ebfc5fb500e6ac45ed4243b4912202201980930659406e06a0f0117a9eb01a29f06faaebf9e4ec58cc96941681043a2e21020377708f22ac1e829c9cfbfd891bb99a47f460bf45d71f4841db404cbefdcb93000000010000544c5168747470733a2f2f697066732e696f2f697066732f516d624874466b596946486e5774457a6e6d354451474d534e4a67704c4c577837513650717470316f4362425a702f6d657461646174612e6a736f6eac0000000281001976a91462950b96bde227ab196cfdd821f80be767dab27788ac0000000101001976a91462950b96bde227ab196cfdd821f80be767dab27788ac01154675726961205370656369616c2045646974696f6e0444504c39402000000000000060eda55a020055b20066e8168ad8f05e82d66a34d19970cfb1861281735215cdd84744d84200bb42880bd1183ce34df2185d1431f531a0a95af3556e368fa72e462edf7a9f00000185', - }, - meta: { - hash: '0055c424b9038b0a8888b574ccdb1933a007fdfc15b91a4b38a48cc883b540bf', - spent_outputs: [ - [0, []], - [1, []], - [2, []], - ], - received_by: [], - children: [], - conflict_with: [], - voided_by: [], - twins: [], - accumulated_weight: 8.0, - score: 0, - height: 0, - first_block: - '000000b17b22dd27fb1205a1f810a2c4d40de1e20af140e001529642c4b173a1', - validation: 'full', - }, - spent_outputs: {}, -}; diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 33057047..00000000 --- a/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "include": ["src", "types"], - "compilerOptions": { - "module": "esnext", - "lib": ["esnext"], - "importHelpers": true, - "declaration": true, - "sourceMap": true, - "rootDir": "./src", - "strict": true, - "moduleResolution": "node", - "jsx": "react", - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "baseUrl": "./", - "noEmit": true, - "paths": { - "@src/*": ["src/*"], - "@tests/*": ["tests/*"] - } - } -} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..5d5f8e1b --- /dev/null +++ b/yarn.lock @@ -0,0 +1,16235 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10 + +"2-thenable@npm:^1.0.0": + version: 1.0.0 + resolution: "2-thenable@npm:1.0.0" + dependencies: + d: "npm:1" + es5-ext: "npm:^0.10.47" + checksum: 10/567cda6fb2fd8884b2a5efdfbec7476da9ec9e3bf84d8bcc637dcda09254c135b4fc91321c0d81a501a9bdafd2d8939f163c2a803c0bccd2f4b6631bbe2e4958 + languageName: node + linkType: hard + +"@aashutoshrathi/word-wrap@npm:^1.2.3": + version: 1.2.6 + resolution: "@aashutoshrathi/word-wrap@npm:1.2.6" + checksum: 10/6eebd12a5cd03cee38fcb915ef9f4ea557df6a06f642dfc7fe8eb4839eb5c9ca55a382f3604d52c14200b0c214c12af5e1f23d2a6d8e23ef2d016b105a9d6c0a + languageName: node + linkType: hard + +"@ampproject/remapping@npm:^2.2.0": + version: 2.2.1 + resolution: "@ampproject/remapping@npm:2.2.1" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.0" + "@jridgewell/trace-mapping": "npm:^0.3.9" + checksum: 10/e15fecbf3b54c988c8b4fdea8ef514ab482537e8a080b2978cc4b47ccca7140577ca7b65ad3322dcce65bc73ee6e5b90cbfe0bbd8c766dad04d5c62ec9634c42 + languageName: node + linkType: hard + +"@aws-crypto/crc32@npm:3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/crc32@npm:3.0.0" + dependencies: + "@aws-crypto/util": "npm:^3.0.0" + "@aws-sdk/types": "npm:^3.222.0" + tslib: "npm:^1.11.1" + checksum: 10/672d593fd98a88709a1b488db92aabf584b6dad3e8099e04b6d2870e34a2ee668cbbe0e5406e60c0d776b9c34a91cfc427999230ad959518fed56a3db037704c + languageName: node + linkType: hard + +"@aws-crypto/ie11-detection@npm:^3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/ie11-detection@npm:3.0.0" + dependencies: + tslib: "npm:^1.11.1" + checksum: 10/f5aee4a11a113ab9640474e75d398c99538aa30775f484cd519f0de0096ae0d4a6b68d2f0c685f24bd6f2425067c565bc20592c36c0dc1f4d28c1b4751a40734 + languageName: node + linkType: hard + +"@aws-crypto/sha256-browser@npm:3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/sha256-browser@npm:3.0.0" + dependencies: + "@aws-crypto/ie11-detection": "npm:^3.0.0" + "@aws-crypto/sha256-js": "npm:^3.0.0" + "@aws-crypto/supports-web-crypto": "npm:^3.0.0" + "@aws-crypto/util": "npm:^3.0.0" + "@aws-sdk/types": "npm:^3.222.0" + "@aws-sdk/util-locate-window": "npm:^3.0.0" + "@aws-sdk/util-utf8-browser": "npm:^3.0.0" + tslib: "npm:^1.11.1" + checksum: 10/4e075906c48a46bbb8babb60db3e6b280db405a88c68b77c1496c26218292d5ea509beae3ccc19366ca6bc944c6d37fe347d0917909900dbac86f054a19c71c7 + languageName: node + linkType: hard + +"@aws-crypto/sha256-js@npm:3.0.0, @aws-crypto/sha256-js@npm:^3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/sha256-js@npm:3.0.0" + dependencies: + "@aws-crypto/util": "npm:^3.0.0" + "@aws-sdk/types": "npm:^3.222.0" + tslib: "npm:^1.11.1" + checksum: 10/f9fc2d51631950434d0f91f51c2ce17845d4e8e75971806e21604987e3186ee1e54de8a89e5349585b91cb36e56d5f058d6a45004e1bfbce1351dbb40f479152 + languageName: node + linkType: hard + +"@aws-crypto/supports-web-crypto@npm:^3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/supports-web-crypto@npm:3.0.0" + dependencies: + tslib: "npm:^1.11.1" + checksum: 10/8a48788d2866e391354f256aa79b577b2ba1474b50184cbe690467de7e64a79928afece95007ab69a1556f99da97ea129487db091d94489847e14decdc7c9a6f + languageName: node + linkType: hard + +"@aws-crypto/util@npm:^3.0.0": + version: 3.0.0 + resolution: "@aws-crypto/util@npm:3.0.0" + dependencies: + "@aws-sdk/types": "npm:^3.222.0" + "@aws-sdk/util-utf8-browser": "npm:^3.0.0" + tslib: "npm:^1.11.1" + checksum: 10/92c835b83d7a888b37b2f2a37c82e58bb8fabb617e371173c488d2a71b916c69ee566f0ea0b3f7f4e16296226c49793f95b3d59fc07a7ca00af91f8f9f29e6c4 + languageName: node + linkType: hard + +"@aws-sdk/client-apigatewaymanagementapi@npm:^3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/client-apigatewaymanagementapi@npm:3.465.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/client-sts": "npm:3.465.0" + "@aws-sdk/core": "npm:3.465.0" + "@aws-sdk/credential-provider-node": "npm:3.465.0" + "@aws-sdk/middleware-host-header": "npm:3.465.0" + "@aws-sdk/middleware-logger": "npm:3.465.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.465.0" + "@aws-sdk/middleware-signing": "npm:3.465.0" + "@aws-sdk/middleware-user-agent": "npm:3.465.0" + "@aws-sdk/region-config-resolver": "npm:3.465.0" + "@aws-sdk/types": "npm:3.465.0" + "@aws-sdk/util-endpoints": "npm:3.465.0" + "@aws-sdk/util-user-agent-browser": "npm:3.465.0" + "@aws-sdk/util-user-agent-node": "npm:3.465.0" + "@smithy/config-resolver": "npm:^2.0.18" + "@smithy/fetch-http-handler": "npm:^2.2.6" + "@smithy/hash-node": "npm:^2.0.15" + "@smithy/invalid-dependency": "npm:^2.0.13" + "@smithy/middleware-content-length": "npm:^2.0.15" + "@smithy/middleware-endpoint": "npm:^2.2.0" + "@smithy/middleware-retry": "npm:^2.0.20" + "@smithy/middleware-serde": "npm:^2.0.13" + "@smithy/middleware-stack": "npm:^2.0.7" + "@smithy/node-config-provider": "npm:^2.1.5" + "@smithy/node-http-handler": "npm:^2.1.9" + "@smithy/protocol-http": "npm:^3.0.9" + "@smithy/smithy-client": "npm:^2.1.15" + "@smithy/types": "npm:^2.5.0" + "@smithy/url-parser": "npm:^2.0.13" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-body-length-browser": "npm:^2.0.0" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.19" + "@smithy/util-defaults-mode-node": "npm:^2.0.25" + "@smithy/util-endpoints": "npm:^1.0.4" + "@smithy/util-retry": "npm:^2.0.6" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/bc065fe81fd96630721788d68e19689cfd95fa361ca77e23c70f0fd0fe2fc3b2bc8d88a7b5db4f4325b0d15733233c5537596730cbc8b9e3d4c1f987d8cd146e + languageName: node + linkType: hard + +"@aws-sdk/client-cloudformation@npm:^3.410.0": + version: 3.423.0 + resolution: "@aws-sdk/client-cloudformation@npm:3.423.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/client-sts": "npm:3.423.0" + "@aws-sdk/credential-provider-node": "npm:3.423.0" + "@aws-sdk/middleware-host-header": "npm:3.418.0" + "@aws-sdk/middleware-logger": "npm:3.418.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.418.0" + "@aws-sdk/middleware-signing": "npm:3.418.0" + "@aws-sdk/middleware-user-agent": "npm:3.418.0" + "@aws-sdk/region-config-resolver": "npm:3.418.0" + "@aws-sdk/types": "npm:3.418.0" + "@aws-sdk/util-endpoints": "npm:3.418.0" + "@aws-sdk/util-user-agent-browser": "npm:3.418.0" + "@aws-sdk/util-user-agent-node": "npm:3.418.0" + "@smithy/config-resolver": "npm:^2.0.10" + "@smithy/fetch-http-handler": "npm:^2.1.5" + "@smithy/hash-node": "npm:^2.0.9" + "@smithy/invalid-dependency": "npm:^2.0.9" + "@smithy/middleware-content-length": "npm:^2.0.11" + "@smithy/middleware-endpoint": "npm:^2.0.9" + "@smithy/middleware-retry": "npm:^2.0.12" + "@smithy/middleware-serde": "npm:^2.0.9" + "@smithy/middleware-stack": "npm:^2.0.2" + "@smithy/node-config-provider": "npm:^2.0.12" + "@smithy/node-http-handler": "npm:^2.1.5" + "@smithy/protocol-http": "npm:^3.0.5" + "@smithy/smithy-client": "npm:^2.1.6" + "@smithy/types": "npm:^2.3.3" + "@smithy/url-parser": "npm:^2.0.9" + "@smithy/util-base64": "npm:^2.0.0" + "@smithy/util-body-length-browser": "npm:^2.0.0" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.10" + "@smithy/util-defaults-mode-node": "npm:^2.0.12" + "@smithy/util-retry": "npm:^2.0.2" + "@smithy/util-utf8": "npm:^2.0.0" + "@smithy/util-waiter": "npm:^2.0.9" + fast-xml-parser: "npm:4.2.5" + tslib: "npm:^2.5.0" + uuid: "npm:^8.3.2" + checksum: 10/fcb054725918feb447db434502641da22cc434a21014904dd86a56e897322401b2e49343228a84a3200689a984cd8c5baab60a941e70cd8648c018665d1940b7 + languageName: node + linkType: hard + +"@aws-sdk/client-lambda@npm:^3.421.0": + version: 3.423.0 + resolution: "@aws-sdk/client-lambda@npm:3.423.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/client-sts": "npm:3.423.0" + "@aws-sdk/credential-provider-node": "npm:3.423.0" + "@aws-sdk/middleware-host-header": "npm:3.418.0" + "@aws-sdk/middleware-logger": "npm:3.418.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.418.0" + "@aws-sdk/middleware-signing": "npm:3.418.0" + "@aws-sdk/middleware-user-agent": "npm:3.418.0" + "@aws-sdk/region-config-resolver": "npm:3.418.0" + "@aws-sdk/types": "npm:3.418.0" + "@aws-sdk/util-endpoints": "npm:3.418.0" + "@aws-sdk/util-user-agent-browser": "npm:3.418.0" + "@aws-sdk/util-user-agent-node": "npm:3.418.0" + "@smithy/config-resolver": "npm:^2.0.10" + "@smithy/eventstream-serde-browser": "npm:^2.0.9" + "@smithy/eventstream-serde-config-resolver": "npm:^2.0.9" + "@smithy/eventstream-serde-node": "npm:^2.0.9" + "@smithy/fetch-http-handler": "npm:^2.1.5" + "@smithy/hash-node": "npm:^2.0.9" + "@smithy/invalid-dependency": "npm:^2.0.9" + "@smithy/middleware-content-length": "npm:^2.0.11" + "@smithy/middleware-endpoint": "npm:^2.0.9" + "@smithy/middleware-retry": "npm:^2.0.12" + "@smithy/middleware-serde": "npm:^2.0.9" + "@smithy/middleware-stack": "npm:^2.0.2" + "@smithy/node-config-provider": "npm:^2.0.12" + "@smithy/node-http-handler": "npm:^2.1.5" + "@smithy/protocol-http": "npm:^3.0.5" + "@smithy/smithy-client": "npm:^2.1.6" + "@smithy/types": "npm:^2.3.3" + "@smithy/url-parser": "npm:^2.0.9" + "@smithy/util-base64": "npm:^2.0.0" + "@smithy/util-body-length-browser": "npm:^2.0.0" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.10" + "@smithy/util-defaults-mode-node": "npm:^2.0.12" + "@smithy/util-retry": "npm:^2.0.2" + "@smithy/util-stream": "npm:^2.0.12" + "@smithy/util-utf8": "npm:^2.0.0" + "@smithy/util-waiter": "npm:^2.0.9" + tslib: "npm:^2.5.0" + checksum: 10/796c3c36bacbaec44b9d66cb3b20cc73e391fa9b80775705679b8c24ea241c3dd1ddaf6a54dbddf2aa31d2b2a93b805693de26a40a52931dff6a03aaad33d3ab + languageName: node + linkType: hard + +"@aws-sdk/client-lambda@npm:^3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/client-lambda@npm:3.465.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/client-sts": "npm:3.465.0" + "@aws-sdk/core": "npm:3.465.0" + "@aws-sdk/credential-provider-node": "npm:3.465.0" + "@aws-sdk/middleware-host-header": "npm:3.465.0" + "@aws-sdk/middleware-logger": "npm:3.465.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.465.0" + "@aws-sdk/middleware-signing": "npm:3.465.0" + "@aws-sdk/middleware-user-agent": "npm:3.465.0" + "@aws-sdk/region-config-resolver": "npm:3.465.0" + "@aws-sdk/types": "npm:3.465.0" + "@aws-sdk/util-endpoints": "npm:3.465.0" + "@aws-sdk/util-user-agent-browser": "npm:3.465.0" + "@aws-sdk/util-user-agent-node": "npm:3.465.0" + "@smithy/config-resolver": "npm:^2.0.18" + "@smithy/eventstream-serde-browser": "npm:^2.0.13" + "@smithy/eventstream-serde-config-resolver": "npm:^2.0.13" + "@smithy/eventstream-serde-node": "npm:^2.0.13" + "@smithy/fetch-http-handler": "npm:^2.2.6" + "@smithy/hash-node": "npm:^2.0.15" + "@smithy/invalid-dependency": "npm:^2.0.13" + "@smithy/middleware-content-length": "npm:^2.0.15" + "@smithy/middleware-endpoint": "npm:^2.2.0" + "@smithy/middleware-retry": "npm:^2.0.20" + "@smithy/middleware-serde": "npm:^2.0.13" + "@smithy/middleware-stack": "npm:^2.0.7" + "@smithy/node-config-provider": "npm:^2.1.5" + "@smithy/node-http-handler": "npm:^2.1.9" + "@smithy/protocol-http": "npm:^3.0.9" + "@smithy/smithy-client": "npm:^2.1.15" + "@smithy/types": "npm:^2.5.0" + "@smithy/url-parser": "npm:^2.0.13" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-body-length-browser": "npm:^2.0.0" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.19" + "@smithy/util-defaults-mode-node": "npm:^2.0.25" + "@smithy/util-endpoints": "npm:^1.0.4" + "@smithy/util-retry": "npm:^2.0.6" + "@smithy/util-stream": "npm:^2.0.20" + "@smithy/util-utf8": "npm:^2.0.2" + "@smithy/util-waiter": "npm:^2.0.13" + tslib: "npm:^2.5.0" + checksum: 10/b8fd3f4cca15f69d33f1589d461b1266f7d96526d0174f2b4cc1baacac8e43d6e55750c814bc51f3c3446fe40f52fd441b4d99487814c2051b41f41cd6baa612 + languageName: node + linkType: hard + +"@aws-sdk/client-lambda@npm:^3.474.0": + version: 3.474.0 + resolution: "@aws-sdk/client-lambda@npm:3.474.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/client-sts": "npm:3.474.0" + "@aws-sdk/core": "npm:3.474.0" + "@aws-sdk/credential-provider-node": "npm:3.474.0" + "@aws-sdk/middleware-host-header": "npm:3.468.0" + "@aws-sdk/middleware-logger": "npm:3.468.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.468.0" + "@aws-sdk/middleware-signing": "npm:3.468.0" + "@aws-sdk/middleware-user-agent": "npm:3.470.0" + "@aws-sdk/region-config-resolver": "npm:3.470.0" + "@aws-sdk/types": "npm:3.468.0" + "@aws-sdk/util-endpoints": "npm:3.470.0" + "@aws-sdk/util-user-agent-browser": "npm:3.468.0" + "@aws-sdk/util-user-agent-node": "npm:3.470.0" + "@smithy/config-resolver": "npm:^2.0.21" + "@smithy/eventstream-serde-browser": "npm:^2.0.15" + "@smithy/eventstream-serde-config-resolver": "npm:^2.0.15" + "@smithy/eventstream-serde-node": "npm:^2.0.15" + "@smithy/fetch-http-handler": "npm:^2.3.1" + "@smithy/hash-node": "npm:^2.0.17" + "@smithy/invalid-dependency": "npm:^2.0.15" + "@smithy/middleware-content-length": "npm:^2.0.17" + "@smithy/middleware-endpoint": "npm:^2.2.3" + "@smithy/middleware-retry": "npm:^2.0.24" + "@smithy/middleware-serde": "npm:^2.0.15" + "@smithy/middleware-stack": "npm:^2.0.9" + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/node-http-handler": "npm:^2.2.1" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/smithy-client": "npm:^2.1.18" + "@smithy/types": "npm:^2.7.0" + "@smithy/url-parser": "npm:^2.0.15" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-body-length-browser": "npm:^2.0.1" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.22" + "@smithy/util-defaults-mode-node": "npm:^2.0.29" + "@smithy/util-endpoints": "npm:^1.0.7" + "@smithy/util-retry": "npm:^2.0.8" + "@smithy/util-stream": "npm:^2.0.23" + "@smithy/util-utf8": "npm:^2.0.2" + "@smithy/util-waiter": "npm:^2.0.15" + tslib: "npm:^2.5.0" + checksum: 10/a0b440441cc4ac5e0af5c6ec3bbf39a7c3935dc457a9cc5276161e9955c7d102a61ba93d3c3d359cc6f57f2afbe3684521c5c9b33da68f97a4e4413bbb2b0639 + languageName: node + linkType: hard + +"@aws-sdk/client-sqs@npm:^3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/client-sqs@npm:3.465.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/client-sts": "npm:3.465.0" + "@aws-sdk/core": "npm:3.465.0" + "@aws-sdk/credential-provider-node": "npm:3.465.0" + "@aws-sdk/middleware-host-header": "npm:3.465.0" + "@aws-sdk/middleware-logger": "npm:3.465.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.465.0" + "@aws-sdk/middleware-sdk-sqs": "npm:3.465.0" + "@aws-sdk/middleware-signing": "npm:3.465.0" + "@aws-sdk/middleware-user-agent": "npm:3.465.0" + "@aws-sdk/region-config-resolver": "npm:3.465.0" + "@aws-sdk/types": "npm:3.465.0" + "@aws-sdk/util-endpoints": "npm:3.465.0" + "@aws-sdk/util-user-agent-browser": "npm:3.465.0" + "@aws-sdk/util-user-agent-node": "npm:3.465.0" + "@smithy/config-resolver": "npm:^2.0.18" + "@smithy/fetch-http-handler": "npm:^2.2.6" + "@smithy/hash-node": "npm:^2.0.15" + "@smithy/invalid-dependency": "npm:^2.0.13" + "@smithy/md5-js": "npm:^2.0.15" + "@smithy/middleware-content-length": "npm:^2.0.15" + "@smithy/middleware-endpoint": "npm:^2.2.0" + "@smithy/middleware-retry": "npm:^2.0.20" + "@smithy/middleware-serde": "npm:^2.0.13" + "@smithy/middleware-stack": "npm:^2.0.7" + "@smithy/node-config-provider": "npm:^2.1.5" + "@smithy/node-http-handler": "npm:^2.1.9" + "@smithy/protocol-http": "npm:^3.0.9" + "@smithy/smithy-client": "npm:^2.1.15" + "@smithy/types": "npm:^2.5.0" + "@smithy/url-parser": "npm:^2.0.13" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-body-length-browser": "npm:^2.0.0" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.19" + "@smithy/util-defaults-mode-node": "npm:^2.0.25" + "@smithy/util-endpoints": "npm:^1.0.4" + "@smithy/util-retry": "npm:^2.0.6" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/ee8f583b63d54c513cc2c11d5d606d6bfdb378546845a0816ca144bdb23aace4768c0824924acc7c94481868ccbd8a393487a83a4a8dd8adf2957dbc1b37e884 + languageName: node + linkType: hard + +"@aws-sdk/client-sqs@npm:^3.474.0": + version: 3.474.0 + resolution: "@aws-sdk/client-sqs@npm:3.474.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/client-sts": "npm:3.474.0" + "@aws-sdk/core": "npm:3.474.0" + "@aws-sdk/credential-provider-node": "npm:3.474.0" + "@aws-sdk/middleware-host-header": "npm:3.468.0" + "@aws-sdk/middleware-logger": "npm:3.468.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.468.0" + "@aws-sdk/middleware-sdk-sqs": "npm:3.468.0" + "@aws-sdk/middleware-signing": "npm:3.468.0" + "@aws-sdk/middleware-user-agent": "npm:3.470.0" + "@aws-sdk/region-config-resolver": "npm:3.470.0" + "@aws-sdk/types": "npm:3.468.0" + "@aws-sdk/util-endpoints": "npm:3.470.0" + "@aws-sdk/util-user-agent-browser": "npm:3.468.0" + "@aws-sdk/util-user-agent-node": "npm:3.470.0" + "@smithy/config-resolver": "npm:^2.0.21" + "@smithy/fetch-http-handler": "npm:^2.3.1" + "@smithy/hash-node": "npm:^2.0.17" + "@smithy/invalid-dependency": "npm:^2.0.15" + "@smithy/md5-js": "npm:^2.0.17" + "@smithy/middleware-content-length": "npm:^2.0.17" + "@smithy/middleware-endpoint": "npm:^2.2.3" + "@smithy/middleware-retry": "npm:^2.0.24" + "@smithy/middleware-serde": "npm:^2.0.15" + "@smithy/middleware-stack": "npm:^2.0.9" + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/node-http-handler": "npm:^2.2.1" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/smithy-client": "npm:^2.1.18" + "@smithy/types": "npm:^2.7.0" + "@smithy/url-parser": "npm:^2.0.15" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-body-length-browser": "npm:^2.0.1" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.22" + "@smithy/util-defaults-mode-node": "npm:^2.0.29" + "@smithy/util-endpoints": "npm:^1.0.7" + "@smithy/util-retry": "npm:^2.0.8" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/46ded5a040ed30940706ed98926fb51963a986465c311187f360df102d1823db388d641453c50e9c6c0ca2148c6bcdddeb0c41ded6617535b50e174c33910946 + languageName: node + linkType: hard + +"@aws-sdk/client-sso@npm:3.423.0": + version: 3.423.0 + resolution: "@aws-sdk/client-sso@npm:3.423.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/middleware-host-header": "npm:3.418.0" + "@aws-sdk/middleware-logger": "npm:3.418.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.418.0" + "@aws-sdk/middleware-user-agent": "npm:3.418.0" + "@aws-sdk/region-config-resolver": "npm:3.418.0" + "@aws-sdk/types": "npm:3.418.0" + "@aws-sdk/util-endpoints": "npm:3.418.0" + "@aws-sdk/util-user-agent-browser": "npm:3.418.0" + "@aws-sdk/util-user-agent-node": "npm:3.418.0" + "@smithy/config-resolver": "npm:^2.0.10" + "@smithy/fetch-http-handler": "npm:^2.1.5" + "@smithy/hash-node": "npm:^2.0.9" + "@smithy/invalid-dependency": "npm:^2.0.9" + "@smithy/middleware-content-length": "npm:^2.0.11" + "@smithy/middleware-endpoint": "npm:^2.0.9" + "@smithy/middleware-retry": "npm:^2.0.12" + "@smithy/middleware-serde": "npm:^2.0.9" + "@smithy/middleware-stack": "npm:^2.0.2" + "@smithy/node-config-provider": "npm:^2.0.12" + "@smithy/node-http-handler": "npm:^2.1.5" + "@smithy/protocol-http": "npm:^3.0.5" + "@smithy/smithy-client": "npm:^2.1.6" + "@smithy/types": "npm:^2.3.3" + "@smithy/url-parser": "npm:^2.0.9" + "@smithy/util-base64": "npm:^2.0.0" + "@smithy/util-body-length-browser": "npm:^2.0.0" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.10" + "@smithy/util-defaults-mode-node": "npm:^2.0.12" + "@smithy/util-retry": "npm:^2.0.2" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/1012d661052b04749643c32bc5d2dbfcd0d3a803cafc5c4b195f5a27851a16d9800a76447b15f885d93a93c120d8796d3feb3dc9859ace4ea4b6f36b65cda934 + languageName: node + linkType: hard + +"@aws-sdk/client-sso@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/client-sso@npm:3.465.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/core": "npm:3.465.0" + "@aws-sdk/middleware-host-header": "npm:3.465.0" + "@aws-sdk/middleware-logger": "npm:3.465.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.465.0" + "@aws-sdk/middleware-user-agent": "npm:3.465.0" + "@aws-sdk/region-config-resolver": "npm:3.465.0" + "@aws-sdk/types": "npm:3.465.0" + "@aws-sdk/util-endpoints": "npm:3.465.0" + "@aws-sdk/util-user-agent-browser": "npm:3.465.0" + "@aws-sdk/util-user-agent-node": "npm:3.465.0" + "@smithy/config-resolver": "npm:^2.0.18" + "@smithy/fetch-http-handler": "npm:^2.2.6" + "@smithy/hash-node": "npm:^2.0.15" + "@smithy/invalid-dependency": "npm:^2.0.13" + "@smithy/middleware-content-length": "npm:^2.0.15" + "@smithy/middleware-endpoint": "npm:^2.2.0" + "@smithy/middleware-retry": "npm:^2.0.20" + "@smithy/middleware-serde": "npm:^2.0.13" + "@smithy/middleware-stack": "npm:^2.0.7" + "@smithy/node-config-provider": "npm:^2.1.5" + "@smithy/node-http-handler": "npm:^2.1.9" + "@smithy/protocol-http": "npm:^3.0.9" + "@smithy/smithy-client": "npm:^2.1.15" + "@smithy/types": "npm:^2.5.0" + "@smithy/url-parser": "npm:^2.0.13" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-body-length-browser": "npm:^2.0.0" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.19" + "@smithy/util-defaults-mode-node": "npm:^2.0.25" + "@smithy/util-endpoints": "npm:^1.0.4" + "@smithy/util-retry": "npm:^2.0.6" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/0f3c7ee4f3d0b321973ab2c75980a7d3e01fd23bf69b55444324fd7e6d17da3bd8a9dc10b3f6901cab79e8269f943362070ae249341c2fa8ea7caf7ab525d76d + languageName: node + linkType: hard + +"@aws-sdk/client-sso@npm:3.474.0": + version: 3.474.0 + resolution: "@aws-sdk/client-sso@npm:3.474.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/core": "npm:3.474.0" + "@aws-sdk/middleware-host-header": "npm:3.468.0" + "@aws-sdk/middleware-logger": "npm:3.468.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.468.0" + "@aws-sdk/middleware-user-agent": "npm:3.470.0" + "@aws-sdk/region-config-resolver": "npm:3.470.0" + "@aws-sdk/types": "npm:3.468.0" + "@aws-sdk/util-endpoints": "npm:3.470.0" + "@aws-sdk/util-user-agent-browser": "npm:3.468.0" + "@aws-sdk/util-user-agent-node": "npm:3.470.0" + "@smithy/config-resolver": "npm:^2.0.21" + "@smithy/fetch-http-handler": "npm:^2.3.1" + "@smithy/hash-node": "npm:^2.0.17" + "@smithy/invalid-dependency": "npm:^2.0.15" + "@smithy/middleware-content-length": "npm:^2.0.17" + "@smithy/middleware-endpoint": "npm:^2.2.3" + "@smithy/middleware-retry": "npm:^2.0.24" + "@smithy/middleware-serde": "npm:^2.0.15" + "@smithy/middleware-stack": "npm:^2.0.9" + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/node-http-handler": "npm:^2.2.1" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/smithy-client": "npm:^2.1.18" + "@smithy/types": "npm:^2.7.0" + "@smithy/url-parser": "npm:^2.0.15" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-body-length-browser": "npm:^2.0.1" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.22" + "@smithy/util-defaults-mode-node": "npm:^2.0.29" + "@smithy/util-endpoints": "npm:^1.0.7" + "@smithy/util-retry": "npm:^2.0.8" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/2f5b630564c39fc8792b91fdd774d0b61ed22086d92b5c0a2c8f37c1ac46b64c94f4ac6c4fe3259a2ab5b339b4740ef75768c48cca1083464705022fba73a5c0 + languageName: node + linkType: hard + +"@aws-sdk/client-sts@npm:3.423.0, @aws-sdk/client-sts@npm:^3.410.0": + version: 3.423.0 + resolution: "@aws-sdk/client-sts@npm:3.423.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/credential-provider-node": "npm:3.423.0" + "@aws-sdk/middleware-host-header": "npm:3.418.0" + "@aws-sdk/middleware-logger": "npm:3.418.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.418.0" + "@aws-sdk/middleware-sdk-sts": "npm:3.418.0" + "@aws-sdk/middleware-signing": "npm:3.418.0" + "@aws-sdk/middleware-user-agent": "npm:3.418.0" + "@aws-sdk/region-config-resolver": "npm:3.418.0" + "@aws-sdk/types": "npm:3.418.0" + "@aws-sdk/util-endpoints": "npm:3.418.0" + "@aws-sdk/util-user-agent-browser": "npm:3.418.0" + "@aws-sdk/util-user-agent-node": "npm:3.418.0" + "@smithy/config-resolver": "npm:^2.0.10" + "@smithy/fetch-http-handler": "npm:^2.1.5" + "@smithy/hash-node": "npm:^2.0.9" + "@smithy/invalid-dependency": "npm:^2.0.9" + "@smithy/middleware-content-length": "npm:^2.0.11" + "@smithy/middleware-endpoint": "npm:^2.0.9" + "@smithy/middleware-retry": "npm:^2.0.12" + "@smithy/middleware-serde": "npm:^2.0.9" + "@smithy/middleware-stack": "npm:^2.0.2" + "@smithy/node-config-provider": "npm:^2.0.12" + "@smithy/node-http-handler": "npm:^2.1.5" + "@smithy/protocol-http": "npm:^3.0.5" + "@smithy/smithy-client": "npm:^2.1.6" + "@smithy/types": "npm:^2.3.3" + "@smithy/url-parser": "npm:^2.0.9" + "@smithy/util-base64": "npm:^2.0.0" + "@smithy/util-body-length-browser": "npm:^2.0.0" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.10" + "@smithy/util-defaults-mode-node": "npm:^2.0.12" + "@smithy/util-retry": "npm:^2.0.2" + "@smithy/util-utf8": "npm:^2.0.0" + fast-xml-parser: "npm:4.2.5" + tslib: "npm:^2.5.0" + checksum: 10/48a71cc9e82947e1a720b05e3a3261210fcb5526bdb30c11da933f287d4bb1221ec0a3a75a7b025a6458569c8ae43a8238939b07d00f566428bd9217105418ac + languageName: node + linkType: hard + +"@aws-sdk/client-sts@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/client-sts@npm:3.465.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/core": "npm:3.465.0" + "@aws-sdk/credential-provider-node": "npm:3.465.0" + "@aws-sdk/middleware-host-header": "npm:3.465.0" + "@aws-sdk/middleware-logger": "npm:3.465.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.465.0" + "@aws-sdk/middleware-sdk-sts": "npm:3.465.0" + "@aws-sdk/middleware-signing": "npm:3.465.0" + "@aws-sdk/middleware-user-agent": "npm:3.465.0" + "@aws-sdk/region-config-resolver": "npm:3.465.0" + "@aws-sdk/types": "npm:3.465.0" + "@aws-sdk/util-endpoints": "npm:3.465.0" + "@aws-sdk/util-user-agent-browser": "npm:3.465.0" + "@aws-sdk/util-user-agent-node": "npm:3.465.0" + "@smithy/config-resolver": "npm:^2.0.18" + "@smithy/fetch-http-handler": "npm:^2.2.6" + "@smithy/hash-node": "npm:^2.0.15" + "@smithy/invalid-dependency": "npm:^2.0.13" + "@smithy/middleware-content-length": "npm:^2.0.15" + "@smithy/middleware-endpoint": "npm:^2.2.0" + "@smithy/middleware-retry": "npm:^2.0.20" + "@smithy/middleware-serde": "npm:^2.0.13" + "@smithy/middleware-stack": "npm:^2.0.7" + "@smithy/node-config-provider": "npm:^2.1.5" + "@smithy/node-http-handler": "npm:^2.1.9" + "@smithy/protocol-http": "npm:^3.0.9" + "@smithy/smithy-client": "npm:^2.1.15" + "@smithy/types": "npm:^2.5.0" + "@smithy/url-parser": "npm:^2.0.13" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-body-length-browser": "npm:^2.0.0" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.19" + "@smithy/util-defaults-mode-node": "npm:^2.0.25" + "@smithy/util-endpoints": "npm:^1.0.4" + "@smithy/util-retry": "npm:^2.0.6" + "@smithy/util-utf8": "npm:^2.0.2" + fast-xml-parser: "npm:4.2.5" + tslib: "npm:^2.5.0" + checksum: 10/5239c4396306e55e584fa3961cbac98f00fc3848564d2ad88af607b44bd864c4e5d85e14c73d2bf278c6381ebd3a2fa8ccc326a88c15afe97ee46d75f21f9c66 + languageName: node + linkType: hard + +"@aws-sdk/client-sts@npm:3.474.0": + version: 3.474.0 + resolution: "@aws-sdk/client-sts@npm:3.474.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/core": "npm:3.474.0" + "@aws-sdk/credential-provider-node": "npm:3.474.0" + "@aws-sdk/middleware-host-header": "npm:3.468.0" + "@aws-sdk/middleware-logger": "npm:3.468.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.468.0" + "@aws-sdk/middleware-user-agent": "npm:3.470.0" + "@aws-sdk/region-config-resolver": "npm:3.470.0" + "@aws-sdk/types": "npm:3.468.0" + "@aws-sdk/util-endpoints": "npm:3.470.0" + "@aws-sdk/util-user-agent-browser": "npm:3.468.0" + "@aws-sdk/util-user-agent-node": "npm:3.470.0" + "@smithy/config-resolver": "npm:^2.0.21" + "@smithy/core": "npm:^1.1.0" + "@smithy/fetch-http-handler": "npm:^2.3.1" + "@smithy/hash-node": "npm:^2.0.17" + "@smithy/invalid-dependency": "npm:^2.0.15" + "@smithy/middleware-content-length": "npm:^2.0.17" + "@smithy/middleware-endpoint": "npm:^2.2.3" + "@smithy/middleware-retry": "npm:^2.0.24" + "@smithy/middleware-serde": "npm:^2.0.15" + "@smithy/middleware-stack": "npm:^2.0.9" + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/node-http-handler": "npm:^2.2.1" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/smithy-client": "npm:^2.1.18" + "@smithy/types": "npm:^2.7.0" + "@smithy/url-parser": "npm:^2.0.15" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-body-length-browser": "npm:^2.0.1" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.22" + "@smithy/util-defaults-mode-node": "npm:^2.0.29" + "@smithy/util-endpoints": "npm:^1.0.7" + "@smithy/util-middleware": "npm:^2.0.8" + "@smithy/util-retry": "npm:^2.0.8" + "@smithy/util-utf8": "npm:^2.0.2" + fast-xml-parser: "npm:4.2.5" + tslib: "npm:^2.5.0" + checksum: 10/acd452293d763715016ce886203099303c6e1db968b9ac6ee20a9275151eb16340a98f277a3670fa3b24dcf7f5031d854601be9cafc8ed9964327f0d17ba91a0 + languageName: node + linkType: hard + +"@aws-sdk/core@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/core@npm:3.465.0" + dependencies: + "@smithy/smithy-client": "npm:^2.1.15" + tslib: "npm:^2.5.0" + checksum: 10/d6b1c37ef46ff5e9de5b7b2f86182c0813c367c48765ba94f82bca0b8b2154d8364c5ba2873cf149afddab262734ad554dca2e2023a29defdaf5c4c36ff37fac + languageName: node + linkType: hard + +"@aws-sdk/core@npm:3.474.0": + version: 3.474.0 + resolution: "@aws-sdk/core@npm:3.474.0" + dependencies: + "@smithy/core": "npm:^1.1.0" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/signature-v4": "npm:^2.0.0" + "@smithy/smithy-client": "npm:^2.1.18" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/9da2048ed33fe197b0a790528cde25d72d8e50bbe5ed63f5d3ced9aae117c85d5470825de642c301fc780c51bd12a2c9b468112a47af07ba0155df484e48f8aa + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-env@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.418.0" + dependencies: + "@aws-sdk/types": "npm:3.418.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + checksum: 10/84e0a2395d8d551f2c3bf2e79db8fa75f019ddd27af9de6b99e5e705e46b37a3f27542957910c097adc55727d4419ee0c8f0a92324e37a0a6417f996080f8529 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-env@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.465.0" + dependencies: + "@aws-sdk/types": "npm:3.465.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + checksum: 10/c12a7a1f021e3410afd001a14075d27c2f3d0a72b7f9b62728f5e7127940aff4d94eb65c1dec792a440da1a8d165636ca88a172a14375eec41f9492c4214014c + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-env@npm:3.468.0": + version: 3.468.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.468.0" + dependencies: + "@aws-sdk/types": "npm:3.468.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/5e8fe5c7a94534d0570e5767a224dfd92f3b039336263ad3121c74aef4ecdac111f24b17be7c78d8a04ed09b2b3d89b65a3bc020180d23d6c5bb4fe98d85f89f + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-ini@npm:3.423.0": + version: 3.423.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.423.0" + dependencies: + "@aws-sdk/credential-provider-env": "npm:3.418.0" + "@aws-sdk/credential-provider-process": "npm:3.418.0" + "@aws-sdk/credential-provider-sso": "npm:3.423.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.418.0" + "@aws-sdk/types": "npm:3.418.0" + "@smithy/credential-provider-imds": "npm:^2.0.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + checksum: 10/2a46df73c139cd574e53291380bcf03331b3a1fca0a6cf01b9f5ed398dada79e277f1580438388da7646bfd94537e3d06e507dfb23683525f16a140624f1fd19 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-ini@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.465.0" + dependencies: + "@aws-sdk/credential-provider-env": "npm:3.465.0" + "@aws-sdk/credential-provider-process": "npm:3.465.0" + "@aws-sdk/credential-provider-sso": "npm:3.465.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.465.0" + "@aws-sdk/types": "npm:3.465.0" + "@smithy/credential-provider-imds": "npm:^2.0.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + checksum: 10/4c592f7d5592ac2c7013a07fdfcdf66a58de57cddbc71645f1b8595a2ced008143bc03842274fe6ae1cb20ab115b6dc33f7e7f327afe65f401ebb5d101e4beb7 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-ini@npm:3.474.0": + version: 3.474.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.474.0" + dependencies: + "@aws-sdk/credential-provider-env": "npm:3.468.0" + "@aws-sdk/credential-provider-process": "npm:3.468.0" + "@aws-sdk/credential-provider-sso": "npm:3.474.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.468.0" + "@aws-sdk/types": "npm:3.468.0" + "@smithy/credential-provider-imds": "npm:^2.0.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/4784eabcf0dd39717d5b9eb33deac1602112cfe4ebd57917847813bc41a354b97c5d1c34fcf7b035ad76740dbf2dfb0f080e2f5f77302ceefe4ecc7096db918a + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-node@npm:3.423.0": + version: 3.423.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.423.0" + dependencies: + "@aws-sdk/credential-provider-env": "npm:3.418.0" + "@aws-sdk/credential-provider-ini": "npm:3.423.0" + "@aws-sdk/credential-provider-process": "npm:3.418.0" + "@aws-sdk/credential-provider-sso": "npm:3.423.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.418.0" + "@aws-sdk/types": "npm:3.418.0" + "@smithy/credential-provider-imds": "npm:^2.0.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + checksum: 10/dbe003a4a03c16bab9a9308329778040c17e36ce137c9343fa64a9e5545c184e492581773401754de30d185a7ea5523008ea69944fc1883699fae601bc042f1a + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-node@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.465.0" + dependencies: + "@aws-sdk/credential-provider-env": "npm:3.465.0" + "@aws-sdk/credential-provider-ini": "npm:3.465.0" + "@aws-sdk/credential-provider-process": "npm:3.465.0" + "@aws-sdk/credential-provider-sso": "npm:3.465.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.465.0" + "@aws-sdk/types": "npm:3.465.0" + "@smithy/credential-provider-imds": "npm:^2.0.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + checksum: 10/194c88b21a686364983393e5e1373b6664ef6a93a26f34a362517c5ee6a537dad11e68e791a98cf7d39c94e5c6ae4767e5dd867250957de66a4b298e127bdc0b + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-node@npm:3.474.0": + version: 3.474.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.474.0" + dependencies: + "@aws-sdk/credential-provider-env": "npm:3.468.0" + "@aws-sdk/credential-provider-ini": "npm:3.474.0" + "@aws-sdk/credential-provider-process": "npm:3.468.0" + "@aws-sdk/credential-provider-sso": "npm:3.474.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.468.0" + "@aws-sdk/types": "npm:3.468.0" + "@smithy/credential-provider-imds": "npm:^2.0.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/2e1ab98b9ffa32918234e4bc3387d23127c227c886212c7dbb0b307719d59984785f5a6ec17bb9849e2925994e9a4cdaed0b78d7095ad9aaa6f614bc18e897a4 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-process@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.418.0" + dependencies: + "@aws-sdk/types": "npm:3.418.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + checksum: 10/d9aa1d88650072f5ef6501acf3fc4505f10747084f8d47db6976dd243b4ac9c8b67423dc012b0528006a064e2e824ff9995a6dd7485b6d2c683f27cc14b6a194 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-process@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.465.0" + dependencies: + "@aws-sdk/types": "npm:3.465.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + checksum: 10/678a09af9ec7978e3f0de0060ddeab1c251a8ce4203e84dc00f569c4ba29eda350861bd30b24bb0b4e854451f098b8ed0b0dbcb85876a3a03b19d004185427a3 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-process@npm:3.468.0": + version: 3.468.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.468.0" + dependencies: + "@aws-sdk/types": "npm:3.468.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/7a345716ac618d8c36db7dafd168d7d12807fd3e181392f61da116eb061d3b8eef135f06c30031104831289caa5c56f7e70448e56c04a892a793cc51ab137013 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-sso@npm:3.423.0": + version: 3.423.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.423.0" + dependencies: + "@aws-sdk/client-sso": "npm:3.423.0" + "@aws-sdk/token-providers": "npm:3.418.0" + "@aws-sdk/types": "npm:3.418.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + checksum: 10/3fe1c8433206cde932f5cddfa96395bd4d4438c39955d42e701a086be0fa9ffd0107ff7dc5f1b5624cdfe6473f265859aa4f30ae512cf6fc177252e6cf7bb122 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-sso@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.465.0" + dependencies: + "@aws-sdk/client-sso": "npm:3.465.0" + "@aws-sdk/token-providers": "npm:3.465.0" + "@aws-sdk/types": "npm:3.465.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + checksum: 10/68ee03c12ce4bfbb4287b1655a0e595cf8e423f0107a87c6b0d4d108e420f3bd554e32427198f74bf26ca9d3bb859fd24a3826ccbd1727a77690c0766aff078b + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-sso@npm:3.474.0": + version: 3.474.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.474.0" + dependencies: + "@aws-sdk/client-sso": "npm:3.474.0" + "@aws-sdk/token-providers": "npm:3.470.0" + "@aws-sdk/types": "npm:3.468.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/fa35b0eba0fa500a6c35cd3b10831b9599606ddf54ee328264ec044c22296320eff391fc15a9506150b587891325a64cdb29ba1fb616ed924c3f8bba2d495773 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-web-identity@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.418.0" + dependencies: + "@aws-sdk/types": "npm:3.418.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + checksum: 10/cc53d6c2dab188d8d378282aa60a7e2f97bfb8b9913d06770339ac9bf449ce524895b577ba225ba61f97cb789b81ebaa2d6f1c507a43c5436a1bf1e9c9b5cfe9 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-web-identity@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.465.0" + dependencies: + "@aws-sdk/types": "npm:3.465.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + checksum: 10/5c7a7cded65f05b9beeec57e293e9b9b1870693bebb3016219d02dc0bb134baf031efbe11a4115cfbbd18d0f2d3e4b16263bc0359d31187659062782b260a57b + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-web-identity@npm:3.468.0": + version: 3.468.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.468.0" + dependencies: + "@aws-sdk/types": "npm:3.468.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/a5fac595637703e6d270c26a2121d574f6f18f96df45d81d7eb1e4e9230c2cf4710f1655e6cd3602dda93e4d447d34bfa707868aaa7083492522d6ab5443a716 + languageName: node + linkType: hard + +"@aws-sdk/middleware-host-header@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.418.0" + dependencies: + "@aws-sdk/types": "npm:3.418.0" + "@smithy/protocol-http": "npm:^3.0.5" + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + checksum: 10/5cdfa1d7254362bec676053f143a6b24b152275de855be9e788e91e2cf45b2c6b0628bdd98abc935e67f20e29776d2702698cfde08fdee4a29e4fe539155e8af + languageName: node + linkType: hard + +"@aws-sdk/middleware-host-header@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.465.0" + dependencies: + "@aws-sdk/types": "npm:3.465.0" + "@smithy/protocol-http": "npm:^3.0.9" + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + checksum: 10/d315aad512145cbf442100145d74b95a194e658fd989466510fec1925032a18fa72e1ec98ace843b0d3045e4ebee02eb8637aa39dcbdc8e864adb634ac4305f8 + languageName: node + linkType: hard + +"@aws-sdk/middleware-host-header@npm:3.468.0": + version: 3.468.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.468.0" + dependencies: + "@aws-sdk/types": "npm:3.468.0" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/d511dea932f68c02f4c683d2b31345c6f9b9d63c7e5be6b4ebf829da056519c63fdc215b19e16628f37b0d57be95d6c107593de452e26fb71f713368da26cbf4 + languageName: node + linkType: hard + +"@aws-sdk/middleware-logger@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/middleware-logger@npm:3.418.0" + dependencies: + "@aws-sdk/types": "npm:3.418.0" + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + checksum: 10/5733dc9b960456b677fab926b85857249eed6af1b9358bb3fd8cf22a15250c8285d1a208cef1a79e2ca96798ae53968ab7a7b41139d244d93972131e418a5380 + languageName: node + linkType: hard + +"@aws-sdk/middleware-logger@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/middleware-logger@npm:3.465.0" + dependencies: + "@aws-sdk/types": "npm:3.465.0" + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + checksum: 10/351984f7541a4201ea04d1b954b072c1d521bcc15a58bd46b6778be36ee1712934d3546f3be9f11bf32cee2498e2a8854d24168f4546bd0636d2fd9a0a2e259c + languageName: node + linkType: hard + +"@aws-sdk/middleware-logger@npm:3.468.0": + version: 3.468.0 + resolution: "@aws-sdk/middleware-logger@npm:3.468.0" + dependencies: + "@aws-sdk/types": "npm:3.468.0" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/75dba345d91451e0a0d2d95c15f12934a9b29be0a271f1904552bfe81cf3a5daf2b0c027fe03a7b475c8a256ea8158c8c87d641a473c943add8bfcb6e40c341d + languageName: node + linkType: hard + +"@aws-sdk/middleware-recursion-detection@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.418.0" + dependencies: + "@aws-sdk/types": "npm:3.418.0" + "@smithy/protocol-http": "npm:^3.0.5" + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + checksum: 10/cb497658121607e84b1cb6732c67d6147406a3e30dc2d1a1b20668fbab75c198c7677eb4e098de93b1cabc7e4d2347cde4b45efb5fcfa4a08e38f52abadfb682 + languageName: node + linkType: hard + +"@aws-sdk/middleware-recursion-detection@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.465.0" + dependencies: + "@aws-sdk/types": "npm:3.465.0" + "@smithy/protocol-http": "npm:^3.0.9" + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + checksum: 10/ee8f806f9400f04de760b576800266df6674d423032aaa211c6c0609ca565c2d9009d12673624dacad5d2d32128d638a24da35aa0db88c56bc3720cea3c7c6c4 + languageName: node + linkType: hard + +"@aws-sdk/middleware-recursion-detection@npm:3.468.0": + version: 3.468.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.468.0" + dependencies: + "@aws-sdk/types": "npm:3.468.0" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/490855cfde0abd0e769dfdb63355ba645dfe4bc6b82a90b05ab648ae098f2af7ec6f76dab315825bfe02c2b37f307b1d895728547c5a9c592377f309199ba4ab + languageName: node + linkType: hard + +"@aws-sdk/middleware-sdk-sqs@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.465.0" + dependencies: + "@aws-sdk/types": "npm:3.465.0" + "@smithy/types": "npm:^2.5.0" + "@smithy/util-hex-encoding": "npm:^2.0.0" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/09bad59f85f411f8f3fa30c22c505c4682bd5cc6f859bd4a53feb715ee4d86668b82fb739c1240f02e0abcdb9cae6de1ee7948f5aff53aaa5b4772c829e2efd9 + languageName: node + linkType: hard + +"@aws-sdk/middleware-sdk-sqs@npm:3.468.0": + version: 3.468.0 + resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.468.0" + dependencies: + "@aws-sdk/types": "npm:3.468.0" + "@smithy/types": "npm:^2.7.0" + "@smithy/util-hex-encoding": "npm:^2.0.0" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/becbe86421896455c73bba3189a98ef5800ae4b0393cb942939db119c8910435cff94f75a3afdb33761a6e12dfcd8c801cd9e1f4c1ab24442f7c9792ccbeeaa4 + languageName: node + linkType: hard + +"@aws-sdk/middleware-sdk-sts@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/middleware-sdk-sts@npm:3.418.0" + dependencies: + "@aws-sdk/middleware-signing": "npm:3.418.0" + "@aws-sdk/types": "npm:3.418.0" + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + checksum: 10/6b571248202a50440e3422be7b3c5151547aef526c8d9a1250313212e3574a9c7e01e7f255fac408b1f6641dfddbdd62f78819bca7a3029fd851e1b3b1a2cf20 + languageName: node + linkType: hard + +"@aws-sdk/middleware-sdk-sts@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/middleware-sdk-sts@npm:3.465.0" + dependencies: + "@aws-sdk/middleware-signing": "npm:3.465.0" + "@aws-sdk/types": "npm:3.465.0" + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + checksum: 10/1a741493769fefb6aa172a597d1bc0c9cb446400b93a783d75a270a0cede5731e7a072a8ecdd5fe4f4ed6de04caa98c8427af9e7cefe624ae4fe03a8cb987eaf + languageName: node + linkType: hard + +"@aws-sdk/middleware-signing@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/middleware-signing@npm:3.418.0" + dependencies: + "@aws-sdk/types": "npm:3.418.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/protocol-http": "npm:^3.0.5" + "@smithy/signature-v4": "npm:^2.0.0" + "@smithy/types": "npm:^2.3.3" + "@smithy/util-middleware": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/bbfcac0f93388ed07d3cc3c5156a11716622790f347b1f0178c7acf6d44dfd43b59b0e13287e334b8d92a8856304684bbe04c23cbdcaa1dbf4a606890d6af8fd + languageName: node + linkType: hard + +"@aws-sdk/middleware-signing@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/middleware-signing@npm:3.465.0" + dependencies: + "@aws-sdk/types": "npm:3.465.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/protocol-http": "npm:^3.0.9" + "@smithy/signature-v4": "npm:^2.0.0" + "@smithy/types": "npm:^2.5.0" + "@smithy/util-middleware": "npm:^2.0.6" + tslib: "npm:^2.5.0" + checksum: 10/6740ac0a45a976327d63ca518667c49addfcd9d702f76a1459b875b6724d391d8af8038b6ea4c95955ecd0d314bfe6f17f7dbac84562c61ec91ed1aa92e655b4 + languageName: node + linkType: hard + +"@aws-sdk/middleware-signing@npm:3.468.0": + version: 3.468.0 + resolution: "@aws-sdk/middleware-signing@npm:3.468.0" + dependencies: + "@aws-sdk/types": "npm:3.468.0" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/signature-v4": "npm:^2.0.0" + "@smithy/types": "npm:^2.7.0" + "@smithy/util-middleware": "npm:^2.0.8" + tslib: "npm:^2.5.0" + checksum: 10/e14d0ace15d8e67700955dda58de3017b8d012ff363737266063224d481df0d1efe86f6b292b847ab2b13ffc2115e9d95f622291512266a82eebc9959ad7b4c5 + languageName: node + linkType: hard + +"@aws-sdk/middleware-user-agent@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.418.0" + dependencies: + "@aws-sdk/types": "npm:3.418.0" + "@aws-sdk/util-endpoints": "npm:3.418.0" + "@smithy/protocol-http": "npm:^3.0.5" + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + checksum: 10/992229cfb793b06df3872e42be6a6ad3ac520b8989af7187a5844978761799e7b91b9d61bd211b346f5c7af0c079f982222285a85f8f4c0bf353e952c0a411f5 + languageName: node + linkType: hard + +"@aws-sdk/middleware-user-agent@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.465.0" + dependencies: + "@aws-sdk/types": "npm:3.465.0" + "@aws-sdk/util-endpoints": "npm:3.465.0" + "@smithy/protocol-http": "npm:^3.0.9" + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + checksum: 10/fd82408a1e34623a06bd23a80e274732f57ef939896e38e39bcaa4b88beaab42239811cd5237af7df779b24387b25542a0b004079e6ec43e05287e7889e688f0 + languageName: node + linkType: hard + +"@aws-sdk/middleware-user-agent@npm:3.470.0": + version: 3.470.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.470.0" + dependencies: + "@aws-sdk/types": "npm:3.468.0" + "@aws-sdk/util-endpoints": "npm:3.470.0" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/e5597ff614e7efe804483357b1e10bae62e24aef6484a6038d085c6483e947ac30df5e928cc03b4311d6fca4549fab696131b3a282355222f60a8c73025d3498 + languageName: node + linkType: hard + +"@aws-sdk/region-config-resolver@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.418.0" + dependencies: + "@smithy/node-config-provider": "npm:^2.0.12" + "@smithy/types": "npm:^2.3.3" + "@smithy/util-config-provider": "npm:^2.0.0" + "@smithy/util-middleware": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/48e033415845ce817171de8fe22ff29c750ee29206b2f9aa07a135d6858289eb69673d3c33283967174f49ed3f5cb89f5d09319858ad8d5b4a494af62a9d6d67 + languageName: node + linkType: hard + +"@aws-sdk/region-config-resolver@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.465.0" + dependencies: + "@smithy/node-config-provider": "npm:^2.1.5" + "@smithy/types": "npm:^2.5.0" + "@smithy/util-config-provider": "npm:^2.0.0" + "@smithy/util-middleware": "npm:^2.0.6" + tslib: "npm:^2.5.0" + checksum: 10/97c92a1a1f3ffb8489649156b5c61052d0af0802fecaa97da0c5859ff0a83ff45b8ecf773909f63ca430c3fa4b3ea9c0b68e07b192dbb9b88fe4fdaf162bfc93 + languageName: node + linkType: hard + +"@aws-sdk/region-config-resolver@npm:3.470.0": + version: 3.470.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.470.0" + dependencies: + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/types": "npm:^2.7.0" + "@smithy/util-config-provider": "npm:^2.0.0" + "@smithy/util-middleware": "npm:^2.0.8" + tslib: "npm:^2.5.0" + checksum: 10/cef036e44b9af913f83e6f2782c13eaa7c048954904bf2d5728639bc08c4328236b55860d85acdca7d951d773d8f932a90745b507955c3004cb15b28c8f1f0ab + languageName: node + linkType: hard + +"@aws-sdk/token-providers@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/token-providers@npm:3.418.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/middleware-host-header": "npm:3.418.0" + "@aws-sdk/middleware-logger": "npm:3.418.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.418.0" + "@aws-sdk/middleware-user-agent": "npm:3.418.0" + "@aws-sdk/types": "npm:3.418.0" + "@aws-sdk/util-endpoints": "npm:3.418.0" + "@aws-sdk/util-user-agent-browser": "npm:3.418.0" + "@aws-sdk/util-user-agent-node": "npm:3.418.0" + "@smithy/config-resolver": "npm:^2.0.10" + "@smithy/fetch-http-handler": "npm:^2.1.5" + "@smithy/hash-node": "npm:^2.0.9" + "@smithy/invalid-dependency": "npm:^2.0.9" + "@smithy/middleware-content-length": "npm:^2.0.11" + "@smithy/middleware-endpoint": "npm:^2.0.9" + "@smithy/middleware-retry": "npm:^2.0.12" + "@smithy/middleware-serde": "npm:^2.0.9" + "@smithy/middleware-stack": "npm:^2.0.2" + "@smithy/node-config-provider": "npm:^2.0.12" + "@smithy/node-http-handler": "npm:^2.1.5" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/protocol-http": "npm:^3.0.5" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/smithy-client": "npm:^2.1.6" + "@smithy/types": "npm:^2.3.3" + "@smithy/url-parser": "npm:^2.0.9" + "@smithy/util-base64": "npm:^2.0.0" + "@smithy/util-body-length-browser": "npm:^2.0.0" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.10" + "@smithy/util-defaults-mode-node": "npm:^2.0.12" + "@smithy/util-retry": "npm:^2.0.2" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/08a33eae8c3d4e253c19c58fec5c54fc8f6212011270627c7c1380d7642a73e4875d23553619d3857be1bcbaa3795892f9a812de73dcc68b20a624c54338bead + languageName: node + linkType: hard + +"@aws-sdk/token-providers@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/token-providers@npm:3.465.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/middleware-host-header": "npm:3.465.0" + "@aws-sdk/middleware-logger": "npm:3.465.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.465.0" + "@aws-sdk/middleware-user-agent": "npm:3.465.0" + "@aws-sdk/region-config-resolver": "npm:3.465.0" + "@aws-sdk/types": "npm:3.465.0" + "@aws-sdk/util-endpoints": "npm:3.465.0" + "@aws-sdk/util-user-agent-browser": "npm:3.465.0" + "@aws-sdk/util-user-agent-node": "npm:3.465.0" + "@smithy/config-resolver": "npm:^2.0.18" + "@smithy/fetch-http-handler": "npm:^2.2.6" + "@smithy/hash-node": "npm:^2.0.15" + "@smithy/invalid-dependency": "npm:^2.0.13" + "@smithy/middleware-content-length": "npm:^2.0.15" + "@smithy/middleware-endpoint": "npm:^2.2.0" + "@smithy/middleware-retry": "npm:^2.0.20" + "@smithy/middleware-serde": "npm:^2.0.13" + "@smithy/middleware-stack": "npm:^2.0.7" + "@smithy/node-config-provider": "npm:^2.1.5" + "@smithy/node-http-handler": "npm:^2.1.9" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/protocol-http": "npm:^3.0.9" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/smithy-client": "npm:^2.1.15" + "@smithy/types": "npm:^2.5.0" + "@smithy/url-parser": "npm:^2.0.13" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-body-length-browser": "npm:^2.0.0" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.19" + "@smithy/util-defaults-mode-node": "npm:^2.0.25" + "@smithy/util-endpoints": "npm:^1.0.4" + "@smithy/util-retry": "npm:^2.0.6" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/30cff56d0f4e07d708e02a40e67fdaad713ba5ed62274b06b29e93a60815c52b3f271d9e183e8846728d6a65cfdceb10e4369d8af67dbfd6a6363865e45790ad + languageName: node + linkType: hard + +"@aws-sdk/token-providers@npm:3.470.0": + version: 3.470.0 + resolution: "@aws-sdk/token-providers@npm:3.470.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-crypto/sha256-js": "npm:3.0.0" + "@aws-sdk/middleware-host-header": "npm:3.468.0" + "@aws-sdk/middleware-logger": "npm:3.468.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.468.0" + "@aws-sdk/middleware-user-agent": "npm:3.470.0" + "@aws-sdk/region-config-resolver": "npm:3.470.0" + "@aws-sdk/types": "npm:3.468.0" + "@aws-sdk/util-endpoints": "npm:3.470.0" + "@aws-sdk/util-user-agent-browser": "npm:3.468.0" + "@aws-sdk/util-user-agent-node": "npm:3.470.0" + "@smithy/config-resolver": "npm:^2.0.21" + "@smithy/fetch-http-handler": "npm:^2.3.1" + "@smithy/hash-node": "npm:^2.0.17" + "@smithy/invalid-dependency": "npm:^2.0.15" + "@smithy/middleware-content-length": "npm:^2.0.17" + "@smithy/middleware-endpoint": "npm:^2.2.3" + "@smithy/middleware-retry": "npm:^2.0.24" + "@smithy/middleware-serde": "npm:^2.0.15" + "@smithy/middleware-stack": "npm:^2.0.9" + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/node-http-handler": "npm:^2.2.1" + "@smithy/property-provider": "npm:^2.0.0" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/shared-ini-file-loader": "npm:^2.0.6" + "@smithy/smithy-client": "npm:^2.1.18" + "@smithy/types": "npm:^2.7.0" + "@smithy/url-parser": "npm:^2.0.15" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-body-length-browser": "npm:^2.0.1" + "@smithy/util-body-length-node": "npm:^2.1.0" + "@smithy/util-defaults-mode-browser": "npm:^2.0.22" + "@smithy/util-defaults-mode-node": "npm:^2.0.29" + "@smithy/util-endpoints": "npm:^1.0.7" + "@smithy/util-retry": "npm:^2.0.8" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/c12043d08fa5cc12bf2c64f33552f60d12fafc180892ca5f4d47f77222d546992edc7c6fb530d8af7d6b77c72d0e2abfa4702f6c9fd2fbfa366decf4dc367014 + languageName: node + linkType: hard + +"@aws-sdk/types@npm:3.418.0, @aws-sdk/types@npm:^3.222.0": + version: 3.418.0 + resolution: "@aws-sdk/types@npm:3.418.0" + dependencies: + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + checksum: 10/627955c2c92f7dd80ab5ac0fd23b6f5d5ff7a8cbc3dcc6f8b86b702f73b844219c3192990dc7048bbca9b36e2e46cdb48d21a8dc3eaf36861623348c1c1427a1 + languageName: node + linkType: hard + +"@aws-sdk/types@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/types@npm:3.465.0" + dependencies: + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + checksum: 10/09bbdf1789bad734b35f370edd0d6b3bfea4654c1dd6d959828b24a3daf8438fce6dfb1f94aa7b66e7989825e31b405c20b731efa7ec73342240e52437461fe9 + languageName: node + linkType: hard + +"@aws-sdk/types@npm:3.468.0": + version: 3.468.0 + resolution: "@aws-sdk/types@npm:3.468.0" + dependencies: + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/d2599c6e73e932925ecebdb4f71bfa25895423ddf6ea981ea815dcf7a307c989d5e53bc9d2a95fed14fd0f6223bcf561dcff64113cf5a77b3d5b263664323b03 + languageName: node + linkType: hard + +"@aws-sdk/util-endpoints@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/util-endpoints@npm:3.418.0" + dependencies: + "@aws-sdk/types": "npm:3.418.0" + tslib: "npm:^2.5.0" + checksum: 10/8ed0c67b5650c0ad4454fd67c0e94e35872ef3da867e7f33411676d95d53402561b615f849083c00d4180158308908e8511d9be2557fdf1c21cf8f4318ef0b86 + languageName: node + linkType: hard + +"@aws-sdk/util-endpoints@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/util-endpoints@npm:3.465.0" + dependencies: + "@aws-sdk/types": "npm:3.465.0" + "@smithy/util-endpoints": "npm:^1.0.4" + tslib: "npm:^2.5.0" + checksum: 10/0d23fb4961db4cfdccef61d6767d1b208c139e6dac01f0f2fcc37e3f8ca67125e07fece1e6e95ada71ab65f7da4514f4f835e8fe1d8530138e70f12d5f1540a9 + languageName: node + linkType: hard + +"@aws-sdk/util-endpoints@npm:3.470.0": + version: 3.470.0 + resolution: "@aws-sdk/util-endpoints@npm:3.470.0" + dependencies: + "@aws-sdk/types": "npm:3.468.0" + "@smithy/util-endpoints": "npm:^1.0.7" + tslib: "npm:^2.5.0" + checksum: 10/6e14724c5951f9b9b91c75b3553b25d5219412914a9897e62aebbafb6f0f366c8f69861074c36f81380e6b5469f371b96a9d9fd9b2b7f86ead84fa29068aefdc + languageName: node + linkType: hard + +"@aws-sdk/util-locate-window@npm:^3.0.0": + version: 3.310.0 + resolution: "@aws-sdk/util-locate-window@npm:3.310.0" + dependencies: + tslib: "npm:^2.5.0" + checksum: 10/163f27aad377c3f798b814bea57bfe1388fbc8a8411407e4c0c23328e32d171645645ac3f4c72e14bf2430a4794b5a5966d9b40c675256b23fa6299a2eb976aa + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-browser@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.418.0" + dependencies: + "@aws-sdk/types": "npm:3.418.0" + "@smithy/types": "npm:^2.3.3" + bowser: "npm:^2.11.0" + tslib: "npm:^2.5.0" + checksum: 10/a2e53033a067dee2a95c5709eb0170cff3e668ff82cef99d047da9d2acbe58fff666956f086008ca6bec1a6c6b1e939c9c56bb251380f6ce49eb9058bba2b40b + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-browser@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.465.0" + dependencies: + "@aws-sdk/types": "npm:3.465.0" + "@smithy/types": "npm:^2.5.0" + bowser: "npm:^2.11.0" + tslib: "npm:^2.5.0" + checksum: 10/942b30c0c98069d2e11a871297a61ba1cf32743e150f9dcf318f45060d8206ee5f797ee38257186f6ea708bb2b337b299efd1a4fcd7be7942b3781c6aadd455f + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-browser@npm:3.468.0": + version: 3.468.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.468.0" + dependencies: + "@aws-sdk/types": "npm:3.468.0" + "@smithy/types": "npm:^2.7.0" + bowser: "npm:^2.11.0" + tslib: "npm:^2.5.0" + checksum: 10/b2d78fa8565f29219192d1f70b834d4d982fe3ec757a493bd0c2edffb20d606b9bec50fca955fd00787e939935eb71498ca637f5fddd1476255b01f460396737 + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-node@npm:3.418.0": + version: 3.418.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.418.0" + dependencies: + "@aws-sdk/types": "npm:3.418.0" + "@smithy/node-config-provider": "npm:^2.0.12" + "@smithy/types": "npm:^2.3.3" + tslib: "npm:^2.5.0" + peerDependencies: + aws-crt: ">=1.0.0" + peerDependenciesMeta: + aws-crt: + optional: true + checksum: 10/c950f87158d905ca98a2c4047b13cfeb282a9f895d8a39553257afe6f1a9b04eb1a559c1144909a7805503b524dc62e60ea0cd9fdaf54c9bc156424831ec6064 + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-node@npm:3.465.0": + version: 3.465.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.465.0" + dependencies: + "@aws-sdk/types": "npm:3.465.0" + "@smithy/node-config-provider": "npm:^2.1.5" + "@smithy/types": "npm:^2.5.0" + tslib: "npm:^2.5.0" + peerDependencies: + aws-crt: ">=1.0.0" + peerDependenciesMeta: + aws-crt: + optional: true + checksum: 10/cdbfb4a01197f91337630fa83fe5fcf4cfe5e0d1535d98db186e7ef05d93c966cbef7348bd7a630ff9e9ea07dfb25c952355d51e9d0e58814694b181e7120d79 + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-node@npm:3.470.0": + version: 3.470.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.470.0" + dependencies: + "@aws-sdk/types": "npm:3.468.0" + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + peerDependencies: + aws-crt: ">=1.0.0" + peerDependenciesMeta: + aws-crt: + optional: true + checksum: 10/05571ba83dcbb91273fe3b9c1c69ced301489e76f78fe299ba74c125c775912110b8721d19ebc1a9270b115797124cd97a4b9a3fbe8355eadec1138a06cbc82f + languageName: node + linkType: hard + +"@aws-sdk/util-utf8-browser@npm:^3.0.0": + version: 3.259.0 + resolution: "@aws-sdk/util-utf8-browser@npm:3.259.0" + dependencies: + tslib: "npm:^2.3.1" + checksum: 10/bdcf29a92a9a1010b44bf8bade3f1224cb6577a6550b39df97cc053d353f2868d355c25589d61e1da54691d65350d8578a496840ad770ed916a6c3af0971f657 + languageName: node + linkType: hard + +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.22.13": + version: 7.22.13 + resolution: "@babel/code-frame@npm:7.22.13" + dependencies: + "@babel/highlight": "npm:^7.22.13" + chalk: "npm:^2.4.2" + checksum: 10/bf6ae6ba3a510adfda6a211b4a89b0f1c98ca1352b745c077d113f3b568141e0d44ce750b9ac2a80143ba5c8c4080c50fcfc1aa11d86e194ea6785f62520eb5a + languageName: node + linkType: hard + +"@babel/compat-data@npm:^7.22.9": + version: 7.22.20 + resolution: "@babel/compat-data@npm:7.22.20" + checksum: 10/b93ff936b1b913116349341bde45709971a3cde98f47668162741ea75ddc80b0b1815bbe26233159b77c5f88c7cfa71fbbb9a5074edcf0a88b66d3936d9241f9 + languageName: node + linkType: hard + +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3": + version: 7.23.0 + resolution: "@babel/core@npm:7.23.0" + dependencies: + "@ampproject/remapping": "npm:^2.2.0" + "@babel/code-frame": "npm:^7.22.13" + "@babel/generator": "npm:^7.23.0" + "@babel/helper-compilation-targets": "npm:^7.22.15" + "@babel/helper-module-transforms": "npm:^7.23.0" + "@babel/helpers": "npm:^7.23.0" + "@babel/parser": "npm:^7.23.0" + "@babel/template": "npm:^7.22.15" + "@babel/traverse": "npm:^7.23.0" + "@babel/types": "npm:^7.23.0" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 10/dd8f988e9ea82b449aaeb3f0c510e39839d9af61ca99391c7d7d06cd1005f21b93cc8d18ee1f3b929a2a37fbda1ee4b0d9304574f02cc365dc327edc6d0348ef + languageName: node + linkType: hard + +"@babel/generator@npm:^7.23.0, @babel/generator@npm:^7.7.2": + version: 7.23.0 + resolution: "@babel/generator@npm:7.23.0" + dependencies: + "@babel/types": "npm:^7.23.0" + "@jridgewell/gen-mapping": "npm:^0.3.2" + "@jridgewell/trace-mapping": "npm:^0.3.17" + jsesc: "npm:^2.5.1" + checksum: 10/bd1598bd356756065d90ce26968dd464ac2b915c67623f6f071fb487da5f9eb454031a380e20e7c9a7ce5c4a49d23be6cb9efde404952b0b3f3c0c3a9b73d68a + languageName: node + linkType: hard + +"@babel/helper-compilation-targets@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/helper-compilation-targets@npm:7.22.15" + dependencies: + "@babel/compat-data": "npm:^7.22.9" + "@babel/helper-validator-option": "npm:^7.22.15" + browserslist: "npm:^4.21.9" + lru-cache: "npm:^5.1.1" + semver: "npm:^6.3.1" + checksum: 10/9706decaa1591cf44511b6f3447eb9653b50ca3538215fe2e5387a8598c258c062f4622da5b95e61f0415706534deee619bbf53a2889f9bd967949b8f6024e0e + languageName: node + linkType: hard + +"@babel/helper-environment-visitor@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-environment-visitor@npm:7.22.20" + checksum: 10/d80ee98ff66f41e233f36ca1921774c37e88a803b2f7dca3db7c057a5fea0473804db9fb6729e5dbfd07f4bed722d60f7852035c2c739382e84c335661590b69 + languageName: node + linkType: hard + +"@babel/helper-function-name@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-function-name@npm:7.23.0" + dependencies: + "@babel/template": "npm:^7.22.15" + "@babel/types": "npm:^7.23.0" + checksum: 10/7b2ae024cd7a09f19817daf99e0153b3bf2bc4ab344e197e8d13623d5e36117ed0b110914bc248faa64e8ccd3e97971ec7b41cc6fd6163a2b980220c58dcdf6d + languageName: node + linkType: hard + +"@babel/helper-hoist-variables@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-hoist-variables@npm:7.22.5" + dependencies: + "@babel/types": "npm:^7.22.5" + checksum: 10/394ca191b4ac908a76e7c50ab52102669efe3a1c277033e49467913c7ed6f7c64d7eacbeabf3bed39ea1f41731e22993f763b1edce0f74ff8563fd1f380d92cc + languageName: node + linkType: hard + +"@babel/helper-module-imports@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/helper-module-imports@npm:7.22.15" + dependencies: + "@babel/types": "npm:^7.22.15" + checksum: 10/5ecf9345a73b80c28677cfbe674b9f567bb0d079e37dcba9055e36cb337db24ae71992a58e1affa9d14a60d3c69907d30fe1f80aea105184501750a58d15c81c + languageName: node + linkType: hard + +"@babel/helper-module-transforms@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-module-transforms@npm:7.23.0" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-module-imports": "npm:^7.22.15" + "@babel/helper-simple-access": "npm:^7.22.5" + "@babel/helper-split-export-declaration": "npm:^7.22.6" + "@babel/helper-validator-identifier": "npm:^7.22.20" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/d72fe444f7b6c5aadaac8f393298d603eedd48e5dead67273a48e5c83a677cbccbd8a12a06c5bf5d97924666083279158a4bd0e799d28b86cbbfacba9e41f598 + languageName: node + linkType: hard + +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.8.0": + version: 7.22.5 + resolution: "@babel/helper-plugin-utils@npm:7.22.5" + checksum: 10/ab220db218089a2aadd0582f5833fd17fa300245999f5f8784b10f5a75267c4e808592284a29438a0da365e702f05acb369f99e1c915c02f9f9210ec60eab8ea + languageName: node + linkType: hard + +"@babel/helper-simple-access@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-simple-access@npm:7.22.5" + dependencies: + "@babel/types": "npm:^7.22.5" + checksum: 10/7d5430eecf880937c27d1aed14245003bd1c7383ae07d652b3932f450f60bfcf8f2c1270c593ab063add185108d26198c69d1aca0e6fb7c6fdada4bcf72ab5b7 + languageName: node + linkType: hard + +"@babel/helper-split-export-declaration@npm:^7.22.6": + version: 7.22.6 + resolution: "@babel/helper-split-export-declaration@npm:7.22.6" + dependencies: + "@babel/types": "npm:^7.22.5" + checksum: 10/e141cace583b19d9195f9c2b8e17a3ae913b7ee9b8120246d0f9ca349ca6f03cb2c001fd5ec57488c544347c0bb584afec66c936511e447fd20a360e591ac921 + languageName: node + linkType: hard + +"@babel/helper-string-parser@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-string-parser@npm:7.22.5" + checksum: 10/7f275a7f1a9504da06afc33441e219796352a4a3d0288a961bc14d1e30e06833a71621b33c3e60ee3ac1ff3c502d55e392bcbc0665f6f9d2629809696fab7cdd + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-validator-identifier@npm:7.22.20" + checksum: 10/df882d2675101df2d507b95b195ca2f86a3ef28cb711c84f37e79ca23178e13b9f0d8b522774211f51e40168bf5142be4c1c9776a150cddb61a0d5bf3e95750b + languageName: node + linkType: hard + +"@babel/helper-validator-option@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/helper-validator-option@npm:7.22.15" + checksum: 10/68da52b1e10002a543161494c4bc0f4d0398c8fdf361d5f7f4272e95c45d5b32d974896d44f6a0ea7378c9204988879d73613ca683e13bd1304e46d25ff67a8d + languageName: node + linkType: hard + +"@babel/helpers@npm:^7.23.0": + version: 7.23.1 + resolution: "@babel/helpers@npm:7.23.1" + dependencies: + "@babel/template": "npm:^7.22.15" + "@babel/traverse": "npm:^7.23.0" + "@babel/types": "npm:^7.23.0" + checksum: 10/f0802d1bd88fe752c32e3f6f54c3873b926ab8ada22cf1df23ec0829f4836a65ad3625d4a29cefb59786060439c538de6be6a690e069a05c00c3802de8e52fea + languageName: node + linkType: hard + +"@babel/highlight@npm:^7.22.13": + version: 7.22.20 + resolution: "@babel/highlight@npm:7.22.20" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.22.20" + chalk: "npm:^2.4.2" + js-tokens: "npm:^4.0.0" + checksum: 10/1aabc95b2cb7f67adc26c7049554306f1435bfedb76b9731c36ff3d7cdfcb32bd65a6dd06985644124eb2100bd911721d9e5c4f5ac40b7f0da2995a61bf8da92 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.15, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/parser@npm:7.23.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10/201641e068f8cca1ff12b141fcba32d7ccbabc586961bd1b85ae89d9695867f84d57fc2e1176dc4981fd28e5e97ca0e7c32cd688bd5eabb641a302abc0cb5040 + languageName: node + linkType: hard + +"@babel/plugin-syntax-async-generators@npm:^7.8.4": + version: 7.8.4 + resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/7ed1c1d9b9e5b64ef028ea5e755c0be2d4e5e4e3d6cf7df757b9a8c4cfa4193d268176d0f1f7fbecdda6fe722885c7fda681f480f3741d8a2d26854736f05367 + languageName: node + linkType: hard + +"@babel/plugin-syntax-bigint@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-bigint@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/3a10849d83e47aec50f367a9e56a6b22d662ddce643334b087f9828f4c3dd73bdc5909aaeabe123fed78515767f9ca43498a0e621c438d1cd2802d7fae3c9648 + languageName: node + linkType: hard + +"@babel/plugin-syntax-class-properties@npm:^7.8.3": + version: 7.12.13 + resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.12.13" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/24f34b196d6342f28d4bad303612d7ff566ab0a013ce89e775d98d6f832969462e7235f3e7eaf17678a533d4be0ba45d3ae34ab4e5a9dcbda5d98d49e5efa2fc + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-meta@npm:^7.8.3": + version: 7.10.4 + resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/166ac1125d10b9c0c430e4156249a13858c0366d38844883d75d27389621ebe651115cb2ceb6dc011534d5055719fa1727b59f39e1ab3ca97820eef3dcab5b9b + languageName: node + linkType: hard + +"@babel/plugin-syntax-json-strings@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-json-strings@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/bf5aea1f3188c9a507e16efe030efb996853ca3cadd6512c51db7233cc58f3ac89ff8c6bdfb01d30843b161cfe7d321e1bf28da82f7ab8d7e6bc5464666f354a + languageName: node + linkType: hard + +"@babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.22.5 + resolution: "@babel/plugin-syntax-jsx@npm:7.22.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/8829d30c2617ab31393d99cec2978e41f014f4ac6f01a1cecf4c4dd8320c3ec12fdc3ce121126b2d8d32f6887e99ca1a0bad53dedb1e6ad165640b92b24980ce + languageName: node + linkType: hard + +"@babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": + version: 7.10.4 + resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/aff33577037e34e515911255cdbb1fd39efee33658aa00b8a5fd3a4b903585112d037cce1cc9e4632f0487dc554486106b79ccd5ea63a2e00df4363f6d4ff886 + languageName: node + linkType: hard + +"@babel/plugin-syntax-nullish-coalescing-operator@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-nullish-coalescing-operator@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/87aca4918916020d1fedba54c0e232de408df2644a425d153be368313fdde40d96088feed6c4e5ab72aac89be5d07fef2ddf329a15109c5eb65df006bf2580d1 + languageName: node + linkType: hard + +"@babel/plugin-syntax-numeric-separator@npm:^7.8.3": + version: 7.10.4 + resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/01ec5547bd0497f76cc903ff4d6b02abc8c05f301c88d2622b6d834e33a5651aa7c7a3d80d8d57656a4588f7276eba357f6b7e006482f5b564b7a6488de493a1 + languageName: node + linkType: hard + +"@babel/plugin-syntax-object-rest-spread@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-object-rest-spread@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/fddcf581a57f77e80eb6b981b10658421bc321ba5f0a5b754118c6a92a5448f12a0c336f77b8abf734841e102e5126d69110a306eadb03ca3e1547cab31f5cbf + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-catch-binding@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-catch-binding@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/910d90e72bc90ea1ce698e89c1027fed8845212d5ab588e35ef91f13b93143845f94e2539d831dc8d8ededc14ec02f04f7bd6a8179edd43a326c784e7ed7f0b9 + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-chaining@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-chaining@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/eef94d53a1453361553c1f98b68d17782861a04a392840341bc91780838dd4e695209c783631cf0de14c635758beafb6a3a65399846ffa4386bff90639347f30 + languageName: node + linkType: hard + +"@babel/plugin-syntax-top-level-await@npm:^7.8.3": + version: 7.14.5 + resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/bbd1a56b095be7820029b209677b194db9b1d26691fe999856462e66b25b281f031f3dfd91b1619e9dcf95bebe336211833b854d0fb8780d618e35667c2d0d7e + languageName: node + linkType: hard + +"@babel/plugin-syntax-typescript@npm:^7.7.2": + version: 7.22.5 + resolution: "@babel/plugin-syntax-typescript@npm:7.22.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/8ab7718fbb026d64da93681a57797d60326097fd7cb930380c8bffd9eb101689e90142c760a14b51e8e69c88a73ba3da956cb4520a3b0c65743aee5c71ef360a + languageName: node + linkType: hard + +"@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3": + version: 7.22.15 + resolution: "@babel/template@npm:7.22.15" + dependencies: + "@babel/code-frame": "npm:^7.22.13" + "@babel/parser": "npm:^7.22.15" + "@babel/types": "npm:^7.22.15" + checksum: 10/21e768e4eed4d1da2ce5d30aa51db0f4d6d8700bc1821fec6292587df7bba2fe1a96451230de8c64b989740731888ebf1141138bfffb14cacccf4d05c66ad93f + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/traverse@npm:7.23.0" + dependencies: + "@babel/code-frame": "npm:^7.22.13" + "@babel/generator": "npm:^7.23.0" + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-function-name": "npm:^7.23.0" + "@babel/helper-hoist-variables": "npm:^7.22.5" + "@babel/helper-split-export-declaration": "npm:^7.22.6" + "@babel/parser": "npm:^7.23.0" + "@babel/types": "npm:^7.23.0" + debug: "npm:^4.1.0" + globals: "npm:^11.1.0" + checksum: 10/dfa970f2e3dfc2d443f092f5a80752d44c6f38705162d1b5b69ebd8a6ff657351ff269a888556be5d921b3392c6c031c33d2bc52e2fba442f602a5a21d769ed4 + languageName: node + linkType: hard + +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3": + version: 7.23.0 + resolution: "@babel/types@npm:7.23.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.22.5" + "@babel/helper-validator-identifier": "npm:^7.22.20" + to-fast-properties: "npm:^2.0.0" + checksum: 10/ca5b896a26c91c5672254725c4c892a35567d2122afc47bd5331d1611a7f9230c19fc9ef591a5a6f80bf0d80737e104a9ac205c96447c74bee01d4319db58001 + languageName: node + linkType: hard + +"@bcoe/v8-coverage@npm:^0.2.3": + version: 0.2.3 + resolution: "@bcoe/v8-coverage@npm:0.2.3" + checksum: 10/1a1f0e356a3bb30b5f1ced6f79c413e6ebacf130421f15fac5fcd8be5ddf98aedb4404d7f5624e3285b700e041f9ef938321f3ca4d359d5b716f96afa120d88d + languageName: node + linkType: hard + +"@colors/colors@npm:1.5.0": + version: 1.5.0 + resolution: "@colors/colors@npm:1.5.0" + checksum: 10/9d226461c1e91e95f067be2bdc5e6f99cfe55a721f45afb44122e23e4b8602eeac4ff7325af6b5a369f36396ee1514d3809af3f57769066d80d83790d8e53339 + languageName: node + linkType: hard + +"@cspotcode/source-map-support@npm:^0.8.0": + version: 0.8.1 + resolution: "@cspotcode/source-map-support@npm:0.8.1" + dependencies: + "@jridgewell/trace-mapping": "npm:0.3.9" + checksum: 10/b6e38a1712fab242c86a241c229cf562195aad985d0564bd352ac404be583029e89e93028ffd2c251d2c407ecac5fb0cbdca94a2d5c10f29ac806ede0508b3ff + languageName: node + linkType: hard + +"@dabh/diagnostics@npm:^2.0.2": + version: 2.0.3 + resolution: "@dabh/diagnostics@npm:2.0.3" + dependencies: + colorspace: "npm:1.1.x" + enabled: "npm:2.0.x" + kuler: "npm:^2.0.0" + checksum: 10/14e449a7f42f063f959b472f6ce02d16457a756e852a1910aaa831b63fc21d86f6c32b2a1aa98a4835b856548c926643b51062d241fb6e9b2b7117996053e6b9 + languageName: node + linkType: hard + +"@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": + version: 4.4.0 + resolution: "@eslint-community/eslint-utils@npm:4.4.0" + dependencies: + eslint-visitor-keys: "npm:^3.3.0" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 10/8d70bcdcd8cd279049183aca747d6c2ed7092a5cf0cf5916faac1ef37ffa74f0c245c2a3a3d3b9979d9dfdd4ca59257b4c5621db699d637b847a2c5e02f491c2 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.5.1, @eslint-community/regexpp@npm:^4.6.1": + version: 4.9.1 + resolution: "@eslint-community/regexpp@npm:4.9.1" + checksum: 10/8f1ba51fa5dedd93f01623382d006c838a436aaea85561c7e540b15600988350843bf746a60e2aaefa79ee4904c9dc0a2f3f00e025b162112c76520ffb34805d + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^2.1.2": + version: 2.1.2 + resolution: "@eslint/eslintrc@npm:2.1.2" + dependencies: + ajv: "npm:^6.12.4" + debug: "npm:^4.3.2" + espree: "npm:^9.6.0" + globals: "npm:^13.19.0" + ignore: "npm:^5.2.0" + import-fresh: "npm:^3.2.1" + js-yaml: "npm:^4.1.0" + minimatch: "npm:^3.1.2" + strip-json-comments: "npm:^3.1.1" + checksum: 10/fa25638f2666cac6810f98ee7d0f4b912f191806467c1b40d72bac759fffef0b3357f12a1869817286837b258e4de3517e0c7408520e156ca860fc53a1fbaed9 + languageName: node + linkType: hard + +"@eslint/js@npm:8.50.0": + version: 8.50.0 + resolution: "@eslint/js@npm:8.50.0" + checksum: 10/1600a84ea1635cb46ae9f9cbc7c4cb054e54b8032707531b3b812d6096e46c54c449e8ecec7eb99725c3aa6da1ebbd4a60ca4fda925200395d5839ded09a0da8 + languageName: node + linkType: hard + +"@fastify/busboy@npm:^1.2.1": + version: 1.2.1 + resolution: "@fastify/busboy@npm:1.2.1" + dependencies: + text-decoding: "npm:^1.0.0" + checksum: 10/1d1963c64992c5f4cd26aceb399dbddfcf2824e5259a7b92c77d2137e67d55e6bc416efe430bf59e897e3a57dad66fec8e51297f4858b855c3b38c7c17818b43 + languageName: node + linkType: hard + +"@firebase/app-types@npm:0.9.0": + version: 0.9.0 + resolution: "@firebase/app-types@npm:0.9.0" + checksum: 10/e6fff0ea48bcd346d10279346fe24e1b7dd9dfbb15923bdebf22b207d916e60f704c3aa534cdf5ac136b0fb6b01669cdeaed71f02761b48b434a7ca9862e9bf9 + languageName: node + linkType: hard + +"@firebase/auth-interop-types@npm:0.2.1": + version: 0.2.1 + resolution: "@firebase/auth-interop-types@npm:0.2.1" + checksum: 10/ffb11ad045db50dfae57433c19cc2d6d9619c648c3f3153c1834d76704a80ca78e0ac41356548a10cd104452ab24d393703d13e66d7822581b8b025dd4dab6ea + languageName: node + linkType: hard + +"@firebase/component@npm:0.6.4": + version: 0.6.4 + resolution: "@firebase/component@npm:0.6.4" + dependencies: + "@firebase/util": "npm:1.9.3" + tslib: "npm:^2.1.0" + checksum: 10/aee5f9d85463190f91f715b5ec8ca3153c3ac3c99f567bdff0f77df2c948eaede527a0fdee72d423241fb135278a713220eeddfd99f0de225b2b9dd85e77a263 + languageName: node + linkType: hard + +"@firebase/database-compat@npm:^0.3.4": + version: 0.3.4 + resolution: "@firebase/database-compat@npm:0.3.4" + dependencies: + "@firebase/component": "npm:0.6.4" + "@firebase/database": "npm:0.14.4" + "@firebase/database-types": "npm:0.10.4" + "@firebase/logger": "npm:0.4.0" + "@firebase/util": "npm:1.9.3" + tslib: "npm:^2.1.0" + checksum: 10/c90d8d970c5dbfd7884cfbdb769c636a455556b5da8b11f95262ddd5c489520d3eb3e305c66e2cc74e5b0da141da024b545f2402c74f011d824d252d2f21c11b + languageName: node + linkType: hard + +"@firebase/database-types@npm:0.10.4, @firebase/database-types@npm:^0.10.4": + version: 0.10.4 + resolution: "@firebase/database-types@npm:0.10.4" + dependencies: + "@firebase/app-types": "npm:0.9.0" + "@firebase/util": "npm:1.9.3" + checksum: 10/0fc46f8e2883e15ddac65860626cbdd3a3d98e698c7c8aba7a9478a070b102001052d7dd2bcd71fae4ccaa7f6e11fd34e53e5c72d4da1029be35d6aae956e339 + languageName: node + linkType: hard + +"@firebase/database@npm:0.14.4": + version: 0.14.4 + resolution: "@firebase/database@npm:0.14.4" + dependencies: + "@firebase/auth-interop-types": "npm:0.2.1" + "@firebase/component": "npm:0.6.4" + "@firebase/logger": "npm:0.4.0" + "@firebase/util": "npm:1.9.3" + faye-websocket: "npm:0.11.4" + tslib: "npm:^2.1.0" + checksum: 10/3c34c3ae48fa87d23aad59ff045a1d2bddedc5594af1379e042007990133f0096589aa7b2dbe396047b1c0e5e33c5aeeaba92500325c2c93b4ada3ec04361fb6 + languageName: node + linkType: hard + +"@firebase/logger@npm:0.4.0": + version: 0.4.0 + resolution: "@firebase/logger@npm:0.4.0" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10/1ac02b142ad15c047573bdf52611c19a3548f4aa7c67264c18e0a857756137d2a45892505d3c916edf83f77dc32a0f1e34525fb6ab3dbd020f24643c43c18860 + languageName: node + linkType: hard + +"@firebase/util@npm:1.9.3": + version: 1.9.3 + resolution: "@firebase/util@npm:1.9.3" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10/77f2cc342e6dd826dfd8c7fd705bb1f8b569493ea172a0c7f80603785b27d37cdf0b29035d3526de15bdf2d634007a4270e3e2c3254fa7f93332114b3e866c99 + languageName: node + linkType: hard + +"@gar/promisify@npm:^1.0.1": + version: 1.1.3 + resolution: "@gar/promisify@npm:1.1.3" + checksum: 10/052dd232140fa60e81588000cbe729a40146579b361f1070bce63e2a761388a22a16d00beeffc504bd3601cb8e055c57b21a185448b3ed550cf50716f4fd442e + languageName: node + linkType: hard + +"@google-cloud/firestore@npm:^6.6.0": + version: 6.8.0 + resolution: "@google-cloud/firestore@npm:6.8.0" + dependencies: + fast-deep-equal: "npm:^3.1.1" + functional-red-black-tree: "npm:^1.0.1" + google-gax: "npm:^3.5.7" + protobufjs: "npm:^7.2.5" + checksum: 10/7b01c1aa49a8428671a6c26ebb0d18071c53acbb6f9624748c765e078ad938fdfc2e1624fe6c54f0b78a287025b09a11a5cfe6de1f173f34c8ab7fc917551484 + languageName: node + linkType: hard + +"@google-cloud/paginator@npm:^3.0.7": + version: 3.0.7 + resolution: "@google-cloud/paginator@npm:3.0.7" + dependencies: + arrify: "npm:^2.0.0" + extend: "npm:^3.0.2" + checksum: 10/b4d61df447d1bb35515cb4335f35a42b7ded9157ccc814ebc5753366ab091c1baced8b1067d876a3e2eb336ca628b6c4f25effe62cd84c7130f24388d711e485 + languageName: node + linkType: hard + +"@google-cloud/projectify@npm:^3.0.0": + version: 3.0.0 + resolution: "@google-cloud/projectify@npm:3.0.0" + checksum: 10/84da9bec8d39b2293a3fc5764417b62338178438e4b3a27e158a3073e199c802fa38b80c25b46e26b8b04e9463cf2857fefcb36d2745ea90d4323602d0ca38d8 + languageName: node + linkType: hard + +"@google-cloud/promisify@npm:^3.0.0": + version: 3.0.1 + resolution: "@google-cloud/promisify@npm:3.0.1" + checksum: 10/36e732cf88b66292402f762ccb1bb13841c2c2680ddc21d80afc940c30b5f81469e1aa6eeb52ecdfa4ddcc1255d9020c9c2306b657ee0338c310086e4f79b832 + languageName: node + linkType: hard + +"@google-cloud/storage@npm:^6.9.5": + version: 6.12.0 + resolution: "@google-cloud/storage@npm:6.12.0" + dependencies: + "@google-cloud/paginator": "npm:^3.0.7" + "@google-cloud/projectify": "npm:^3.0.0" + "@google-cloud/promisify": "npm:^3.0.0" + abort-controller: "npm:^3.0.0" + async-retry: "npm:^1.3.3" + compressible: "npm:^2.0.12" + duplexify: "npm:^4.0.0" + ent: "npm:^2.2.0" + extend: "npm:^3.0.2" + fast-xml-parser: "npm:^4.2.2" + gaxios: "npm:^5.0.0" + google-auth-library: "npm:^8.0.1" + mime: "npm:^3.0.0" + mime-types: "npm:^2.0.8" + p-limit: "npm:^3.0.1" + retry-request: "npm:^5.0.0" + teeny-request: "npm:^8.0.0" + uuid: "npm:^8.0.0" + checksum: 10/b19bebb1c1c51a6ad1d5d056bcc22d2f671ad28ce5d6faf35bc08b35eac800987ae39a1da70ccd20b8e3271dbbcf79df474ae3f9265f27e0f5ef6bc993b9f312 + languageName: node + linkType: hard + +"@grpc/grpc-js@npm:~1.8.0": + version: 1.8.21 + resolution: "@grpc/grpc-js@npm:1.8.21" + dependencies: + "@grpc/proto-loader": "npm:^0.7.0" + "@types/node": "npm:>=12.12.47" + checksum: 10/8c2674b435efd7d8cf54d63c3ef810efc5f7f2f479b77d7cb4baa0ba1ad21734ac7e0f8c068bfb004445d8c89b0b3773b7995dc0d43e163a3d45b7e1d0f55537 + languageName: node + linkType: hard + +"@grpc/proto-loader@npm:^0.7.0": + version: 0.7.10 + resolution: "@grpc/proto-loader@npm:0.7.10" + dependencies: + lodash.camelcase: "npm:^4.3.0" + long: "npm:^5.0.0" + protobufjs: "npm:^7.2.4" + yargs: "npm:^17.7.2" + bin: + proto-loader-gen-types: build/bin/proto-loader-gen-types.js + checksum: 10/1fdc0b10480614cecc4bf52578756cbf59ec75f1bea37452947125eff81cd3ceabba04606247ed8361f97bcd00d147ca4118abc22b046cc0541cb749671b97d9 + languageName: node + linkType: hard + +"@hapi/accept@npm:^6.0.1": + version: 6.0.2 + resolution: "@hapi/accept@npm:6.0.2" + dependencies: + "@hapi/boom": "npm:^10.0.1" + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/5511abf491f08a75863527c8eefe88b9508e608926a42b5d622309ab8c3937de857df1ade43fe0054a324bd539e3677ad20e2c28f0a688087b9d38f7f30d5096 + languageName: node + linkType: hard + +"@hapi/ammo@npm:^6.0.1": + version: 6.0.1 + resolution: "@hapi/ammo@npm:6.0.1" + dependencies: + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/42ade652cd5811f9ea307269b9297fbcb9fcd86996c99e5ca4124b166dc98b1a81ea4eee8dfe018a347e4706f4a6a5309e8dc3792b00f44f00e1e66617b762ec + languageName: node + linkType: hard + +"@hapi/b64@npm:^6.0.1": + version: 6.0.1 + resolution: "@hapi/b64@npm:6.0.1" + dependencies: + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/95f929fb140164d0b9420c013d573a3ca6829c8d1be9526997c1b26a8e74d0055c270bc721d1aedaf927336ac6bece59b923e29aac0ed08d905b023032f6f0ba + languageName: node + linkType: hard + +"@hapi/boom@npm:^10.0.0, @hapi/boom@npm:^10.0.1": + version: 10.0.1 + resolution: "@hapi/boom@npm:10.0.1" + dependencies: + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/99415b0e2f6aeefae91475e9215620d6cb0cc9f16b836e052c006cffb59cf39c45f724e545c2c50ba036d786c6c7935bffc97ae1cb504c65d2540d200bd40fff + languageName: node + linkType: hard + +"@hapi/bounce@npm:^3.0.1": + version: 3.0.1 + resolution: "@hapi/bounce@npm:3.0.1" + dependencies: + "@hapi/boom": "npm:^10.0.1" + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/366e17907ee58ca610d16dbdb5ddf4cf888c811b42a1eeb1d3a0be05c78017d9a44085bf1ce5e20f008c31be7f068a7744f963a6714714f2520c863a0659c914 + languageName: node + linkType: hard + +"@hapi/bourne@npm:^3.0.0": + version: 3.0.0 + resolution: "@hapi/bourne@npm:3.0.0" + checksum: 10/b3b5d7bdf511fe27b7b8b01b9457f125646665bef72a78848c69170efdea19c2b72522246a87ede6cd811e51e7a556ceff194e46fb1393c6c8c796431c1810b6 + languageName: node + linkType: hard + +"@hapi/call@npm:^9.0.1": + version: 9.0.1 + resolution: "@hapi/call@npm:9.0.1" + dependencies: + "@hapi/boom": "npm:^10.0.1" + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/083ad34770e3314eb311a6d69e088cc9814dd0ae65b860be2b97cd5ee84a44efd6fe83d6aa1c330a72a533dde323d2dc6a4e1e65e76e2c1c347cb87ec25ff190 + languageName: node + linkType: hard + +"@hapi/catbox-memory@npm:^6.0.1": + version: 6.0.1 + resolution: "@hapi/catbox-memory@npm:6.0.1" + dependencies: + "@hapi/boom": "npm:^10.0.1" + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/e1876b066dcf3f0a1fc779490ef97e98b71f829cf70d263fe1b7958264e5f4b253ab10504b7f819d43a3cf83ff8bc26ea205f42b5f0b006af2bfd6a636cd40f9 + languageName: node + linkType: hard + +"@hapi/catbox@npm:^12.1.1": + version: 12.1.1 + resolution: "@hapi/catbox@npm:12.1.1" + dependencies: + "@hapi/boom": "npm:^10.0.1" + "@hapi/hoek": "npm:^11.0.2" + "@hapi/podium": "npm:^5.0.0" + "@hapi/validate": "npm:^2.0.1" + checksum: 10/e59231074a918a938f7d887e03eec05b5375714000cd9e60a6a032b3054a4cb60278f40e117b3ead3a91bcdffe3fdc99bbd180c238d0e85ce69560d4936caf99 + languageName: node + linkType: hard + +"@hapi/content@npm:^6.0.0": + version: 6.0.0 + resolution: "@hapi/content@npm:6.0.0" + dependencies: + "@hapi/boom": "npm:^10.0.0" + checksum: 10/51a62c805e505f90e928d50554a7dcb5a56db7e3508cf258b148f668fa293e814161009340a8f586107edae2d2fdf5bc31056c3c70ec1cf85c325eaae226ce5b + languageName: node + linkType: hard + +"@hapi/cryptiles@npm:^6.0.1": + version: 6.0.1 + resolution: "@hapi/cryptiles@npm:6.0.1" + dependencies: + "@hapi/boom": "npm:^10.0.1" + checksum: 10/eee3887d5474d887aa23c389a41d61e55f08b4325751f7cf35bac0563b4f75890af4a6e1cf5b1b8573656db8d33690804e9e6ca191be20e80a540914c3c7eb19 + languageName: node + linkType: hard + +"@hapi/file@npm:^3.0.0": + version: 3.0.0 + resolution: "@hapi/file@npm:3.0.0" + checksum: 10/3a7d8850e8e395f7a8460878a9993e18694c381a2ce5badb15088d4aef2eb002e08fd32cc255451d0d9fdae229338ea5d5589a01da27be5c0792084311ed3df3 + languageName: node + linkType: hard + +"@hapi/h2o2@npm:^10.0.4": + version: 10.0.4 + resolution: "@hapi/h2o2@npm:10.0.4" + dependencies: + "@hapi/boom": "npm:^10.0.1" + "@hapi/hoek": "npm:^11.0.2" + "@hapi/validate": "npm:^2.0.1" + "@hapi/wreck": "npm:^18.0.1" + checksum: 10/38c1a63dbd6f43c31b3fdeafd6b1f7ea09f75ab6f16c9fd65e98114f62e9a1d5675ff1deb6004bc95cf6ec50d30868de67d320a27ece61e28d4896f135028315 + languageName: node + linkType: hard + +"@hapi/hapi@npm:^21.3.2": + version: 21.3.2 + resolution: "@hapi/hapi@npm:21.3.2" + dependencies: + "@hapi/accept": "npm:^6.0.1" + "@hapi/ammo": "npm:^6.0.1" + "@hapi/boom": "npm:^10.0.1" + "@hapi/bounce": "npm:^3.0.1" + "@hapi/call": "npm:^9.0.1" + "@hapi/catbox": "npm:^12.1.1" + "@hapi/catbox-memory": "npm:^6.0.1" + "@hapi/heavy": "npm:^8.0.1" + "@hapi/hoek": "npm:^11.0.2" + "@hapi/mimos": "npm:^7.0.1" + "@hapi/podium": "npm:^5.0.1" + "@hapi/shot": "npm:^6.0.1" + "@hapi/somever": "npm:^4.1.1" + "@hapi/statehood": "npm:^8.1.1" + "@hapi/subtext": "npm:^8.1.0" + "@hapi/teamwork": "npm:^6.0.0" + "@hapi/topo": "npm:^6.0.1" + "@hapi/validate": "npm:^2.0.1" + checksum: 10/202dca65873835eb15fc24b5afe3d8174be8ca673a6c9365f2d46d33ec09883cc7d37c98ff7bedcd381e8c761f35ddf4320127e384d44a4a82232c31d63e4330 + languageName: node + linkType: hard + +"@hapi/heavy@npm:^8.0.1": + version: 8.0.1 + resolution: "@hapi/heavy@npm:8.0.1" + dependencies: + "@hapi/boom": "npm:^10.0.1" + "@hapi/hoek": "npm:^11.0.2" + "@hapi/validate": "npm:^2.0.1" + checksum: 10/7aee6d0dad3e7b7d875c68cbc39eea4338c70fb1b96de9c5e93e52f649fc7345b7fb1fa7521afc73ef333462e17e36b7d1fb774105de563d37322f2b66de1804 + languageName: node + linkType: hard + +"@hapi/hoek@npm:^11.0.2": + version: 11.0.2 + resolution: "@hapi/hoek@npm:11.0.2" + checksum: 10/11fcca5370c675de6db584201bf7c13972af519ee2853fa7ded929c725f050ce8b889959e971cef3727f4d8772dc24009c472c3aac5c64dbdf0cc2681dbca10c + languageName: node + linkType: hard + +"@hapi/hoek@npm:^9.0.0": + version: 9.3.0 + resolution: "@hapi/hoek@npm:9.3.0" + checksum: 10/ad83a223787749f3873bce42bd32a9a19673765bf3edece0a427e138859ff729469e68d5fdf9ff6bbee6fb0c8e21bab61415afa4584f527cfc40b59ea1957e70 + languageName: node + linkType: hard + +"@hapi/iron@npm:^7.0.1": + version: 7.0.1 + resolution: "@hapi/iron@npm:7.0.1" + dependencies: + "@hapi/b64": "npm:^6.0.1" + "@hapi/boom": "npm:^10.0.1" + "@hapi/bourne": "npm:^3.0.0" + "@hapi/cryptiles": "npm:^6.0.1" + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/e86f8b73cca392dd51888e994b86c44839acc3b5697249a921b03ed66b65e3876d9ca539aaa7072c049398a15a8c0b40c4b72a9a0ec33821a7a718f375d9cf68 + languageName: node + linkType: hard + +"@hapi/mimos@npm:^7.0.1": + version: 7.0.1 + resolution: "@hapi/mimos@npm:7.0.1" + dependencies: + "@hapi/hoek": "npm:^11.0.2" + mime-db: "npm:^1.52.0" + checksum: 10/821a94c757d172291047d0ad588947164c97a32884f01fe41bc547ace9ca4a950a73d14634fd6c21dae91a213ce5ef2cbdb90fc544830f3cde3576082c5e965c + languageName: node + linkType: hard + +"@hapi/nigel@npm:^5.0.1": + version: 5.0.1 + resolution: "@hapi/nigel@npm:5.0.1" + dependencies: + "@hapi/hoek": "npm:^11.0.2" + "@hapi/vise": "npm:^5.0.1" + checksum: 10/50c97ec45a7cc816dc16db0d79ae19633e6e0ecc8d52ba5ef738aaee1cb94a181990ac52df71cb1b01b866ad32b4fb643ae6ff64af7926de4ec5844324664478 + languageName: node + linkType: hard + +"@hapi/pez@npm:^6.1.0": + version: 6.1.0 + resolution: "@hapi/pez@npm:6.1.0" + dependencies: + "@hapi/b64": "npm:^6.0.1" + "@hapi/boom": "npm:^10.0.1" + "@hapi/content": "npm:^6.0.0" + "@hapi/hoek": "npm:^11.0.2" + "@hapi/nigel": "npm:^5.0.1" + checksum: 10/f704d0f5ef9fa65a09559773fbd3e49f938b84e209bae9d1b184a31b2a6f9c670e641a63d7d80ef6ef3be18239a8dc8efb773b05a497268caf4628f6fc756dfe + languageName: node + linkType: hard + +"@hapi/podium@npm:^5.0.0, @hapi/podium@npm:^5.0.1": + version: 5.0.1 + resolution: "@hapi/podium@npm:5.0.1" + dependencies: + "@hapi/hoek": "npm:^11.0.2" + "@hapi/teamwork": "npm:^6.0.0" + "@hapi/validate": "npm:^2.0.1" + checksum: 10/2646c6284c1ffe91512e7a17eb0048a148ac25ef29be389182e99a81e9dda437c261b5c3bf8c4a20757a491e68b53ee1b62c9396f3861ffb4752b782a85d571d + languageName: node + linkType: hard + +"@hapi/shot@npm:^6.0.1": + version: 6.0.1 + resolution: "@hapi/shot@npm:6.0.1" + dependencies: + "@hapi/hoek": "npm:^11.0.2" + "@hapi/validate": "npm:^2.0.1" + checksum: 10/6eb387f9c676922c504b042671139aefa943e0460534179501e793e3658741f45be7fc0a45a4972dd2907ba05157a5a3f9b04c19b0f8de71239e2719744d5a43 + languageName: node + linkType: hard + +"@hapi/somever@npm:^4.1.1": + version: 4.1.1 + resolution: "@hapi/somever@npm:4.1.1" + dependencies: + "@hapi/bounce": "npm:^3.0.1" + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/1e1f5e12743239867574d9020decf834271a515d7fbe39d9005d7706cd6899391d17946d8349bfd87fe841c29c61939fde81e980b27f432d3c9166644ec85115 + languageName: node + linkType: hard + +"@hapi/statehood@npm:^8.1.1": + version: 8.1.1 + resolution: "@hapi/statehood@npm:8.1.1" + dependencies: + "@hapi/boom": "npm:^10.0.1" + "@hapi/bounce": "npm:^3.0.1" + "@hapi/bourne": "npm:^3.0.0" + "@hapi/cryptiles": "npm:^6.0.1" + "@hapi/hoek": "npm:^11.0.2" + "@hapi/iron": "npm:^7.0.1" + "@hapi/validate": "npm:^2.0.1" + checksum: 10/b8259b5470d88064da0f803d39d2ccd244894cd9c20c26f65299398a528eb3a9450c32cde250cb3499d3c80a2e1d9523c609c05658616fcc5be3a8d9f05bbbe1 + languageName: node + linkType: hard + +"@hapi/subtext@npm:^8.1.0": + version: 8.1.0 + resolution: "@hapi/subtext@npm:8.1.0" + dependencies: + "@hapi/boom": "npm:^10.0.1" + "@hapi/bourne": "npm:^3.0.0" + "@hapi/content": "npm:^6.0.0" + "@hapi/file": "npm:^3.0.0" + "@hapi/hoek": "npm:^11.0.2" + "@hapi/pez": "npm:^6.1.0" + "@hapi/wreck": "npm:^18.0.1" + checksum: 10/3f7bf0c689d67307fa4fd454ce491e210c610823386def3155422c0c45c7e0512429a14d8091f72e10b289ae4e6bf1d449f926945148b9e216238412e6aa6901 + languageName: node + linkType: hard + +"@hapi/teamwork@npm:^6.0.0": + version: 6.0.0 + resolution: "@hapi/teamwork@npm:6.0.0" + checksum: 10/e79c8e590e5325ed5a8967cf09ae6a635aac08885953887fc2559e8200ba8575fa12d6637c88ae138ccb157f66c823786e0b53c7f98baf50e3a93207a3f5485c + languageName: node + linkType: hard + +"@hapi/topo@npm:^5.0.0": + version: 5.1.0 + resolution: "@hapi/topo@npm:5.1.0" + dependencies: + "@hapi/hoek": "npm:^9.0.0" + checksum: 10/084bfa647015f4fd3fdd51fadb2747d09ef2f5e1443d6cbada2988b0c88494f85edf257ec606c790db146ac4e34ff57f3fcb22e3299b8e06ed5c87ba7583495c + languageName: node + linkType: hard + +"@hapi/topo@npm:^6.0.1": + version: 6.0.2 + resolution: "@hapi/topo@npm:6.0.2" + dependencies: + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/ef959d3796638e11e5f7a9f2a64295ded7c638d6a21fa37bf1265b14c3a6636da593c9976138826c5b8972830a706d18a82b489e3d902e77026f7f0bec908704 + languageName: node + linkType: hard + +"@hapi/validate@npm:^2.0.1": + version: 2.0.1 + resolution: "@hapi/validate@npm:2.0.1" + dependencies: + "@hapi/hoek": "npm:^11.0.2" + "@hapi/topo": "npm:^6.0.1" + checksum: 10/0fcf0b1b192240080861cfd7d312175cdd9639f43a411847dc54619d28e8bed54956c893a2c10b0d543122a11c5eec16cfca2182936c00a58ddbd98608651782 + languageName: node + linkType: hard + +"@hapi/vise@npm:^5.0.1": + version: 5.0.1 + resolution: "@hapi/vise@npm:5.0.1" + dependencies: + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/093aefdc91024bed1058d8d12cfc691f4ad785a9acbf42bca75791d9f982acb7342922c986bef190d6d660a3e4686c24f6d980e55db58a69ab0299128168052b + languageName: node + linkType: hard + +"@hapi/wreck@npm:^18.0.1": + version: 18.0.1 + resolution: "@hapi/wreck@npm:18.0.1" + dependencies: + "@hapi/boom": "npm:^10.0.1" + "@hapi/bourne": "npm:^3.0.0" + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/456b9d056f71bef9cb4449e1d32bbfbdf4d0f010281b1138225050c3eac3e5194b5d57ba3dd7a18d538199b5cc2284343ccd254079a8ac8cf6776b46f1a0addb + languageName: node + linkType: hard + +"@hathor/healthcheck-lib@npm:^0.1.0": + version: 0.1.0 + resolution: "@hathor/healthcheck-lib@npm:0.1.0" + checksum: 10/f2ffabff2948f009acb9b98787c4fbf6b2a4d00c178f3f843999c387084d4cd052ee51ba8b16c0a7d98a5b3d11d5a5d51eea9d36a723bc97d2c955a329a25574 + languageName: node + linkType: hard + +"@hathor/wallet-lib@npm:^0.39.0": + version: 0.39.0 + resolution: "@hathor/wallet-lib@npm:0.39.0" + dependencies: + axios: "npm:^0.18.0" + bitcore-lib: "npm:^8.25.10" + bitcore-mnemonic: "npm:^8.25.10" + crypto-js: "npm:^3.1.9-1" + isomorphic-ws: "npm:^4.0.1" + lodash: "npm:^4.17.11" + long: "npm:^4.0.0" + ws: "npm:^7.2.1" + checksum: 10/1a49bb3f335b4d9f2005df4459f11687a2ccf4595afa45282b43f9127e2b4360ee37d2df6fe551007d83dc7bcbc518f0228f36247a1b147d702f9bd1cae66705 + languageName: node + linkType: hard + +"@humanwhocodes/config-array@npm:^0.11.11": + version: 0.11.11 + resolution: "@humanwhocodes/config-array@npm:0.11.11" + dependencies: + "@humanwhocodes/object-schema": "npm:^1.2.1" + debug: "npm:^4.1.1" + minimatch: "npm:^3.0.5" + checksum: 10/4aad64bc4c68ec99a72c91ad9a8a9070e8da47e8fc4f51eefa2eaf56f4b0cae17dfc3ff82eb9268298f687b5bb3b68669ff542203c77bcd400dc27924d56cad6 + languageName: node + linkType: hard + +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 10/e993950e346331e5a32eefb27948ecdee2a2c4ab3f072b8f566cd213ef485dd50a3ca497050608db91006f5479e43f91a439aef68d2a313bd3ded06909c7c5b3 + languageName: node + linkType: hard + +"@humanwhocodes/object-schema@npm:^1.2.1": + version: 1.2.1 + resolution: "@humanwhocodes/object-schema@npm:1.2.1" + checksum: 10/b48a8f87fcd5fdc4ac60a31a8bf710d19cc64556050575e6a35a4a48a8543cf8cde1598a65640ff2cdfbfd165b38f9db4fa3782bea7848eb585cc3db824002e6 + languageName: node + linkType: hard + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 10/e9ed5fd27c3aec1095e3a16e0c0cf148d1fee55a38665c35f7b3f86a9b5d00d042ddaabc98e8a1cb7463b9378c15f22a94eb35e99469c201453eb8375191f243 + languageName: node + linkType: hard + +"@istanbuljs/load-nyc-config@npm:^1.0.0": + version: 1.1.0 + resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" + dependencies: + camelcase: "npm:^5.3.1" + find-up: "npm:^4.1.0" + get-package-type: "npm:^0.1.0" + js-yaml: "npm:^3.13.1" + resolve-from: "npm:^5.0.0" + checksum: 10/b000a5acd8d4fe6e34e25c399c8bdbb5d3a202b4e10416e17bfc25e12bab90bb56d33db6089ae30569b52686f4b35ff28ef26e88e21e69821d2b85884bd055b8 + languageName: node + linkType: hard + +"@istanbuljs/schema@npm:^0.1.2": + version: 0.1.3 + resolution: "@istanbuljs/schema@npm:0.1.3" + checksum: 10/a9b1e49acdf5efc2f5b2359f2df7f90c5c725f2656f16099e8b2cd3a000619ecca9fc48cf693ba789cf0fd989f6e0df6a22bc05574be4223ecdbb7997d04384b + languageName: node + linkType: hard + +"@jest/console@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/console@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + slash: "npm:^3.0.0" + checksum: 10/4a80c750e8a31f344233cb9951dee9b77bf6b89377cb131f8b3cde07ff218f504370133a5963f6a786af4d2ce7f85642db206ff7a15f99fe58df4c38ac04899e + languageName: node + linkType: hard + +"@jest/core@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/core@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/reporters": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-changed-files: "npm:^29.7.0" + jest-config: "npm:^29.7.0" + jest-haste-map: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-resolve-dependencies: "npm:^29.7.0" + jest-runner: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + jest-watcher: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-ansi: "npm:^6.0.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10/ab6ac2e562d083faac7d8152ec1cc4eccc80f62e9579b69ed40aedf7211a6b2d57024a6cd53c4e35fd051c39a236e86257d1d99ebdb122291969a0a04563b51e + languageName: node + linkType: hard + +"@jest/environment@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/environment@npm:29.7.0" + dependencies: + "@jest/fake-timers": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-mock: "npm:^29.7.0" + checksum: 10/90b5844a9a9d8097f2cf107b1b5e57007c552f64315da8c1f51217eeb0a9664889d3f145cdf8acf23a84f4d8309a6675e27d5b059659a004db0ea9546d1c81a8 + languageName: node + linkType: hard + +"@jest/expect-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect-utils@npm:29.7.0" + dependencies: + jest-get-type: "npm:^29.6.3" + checksum: 10/ef8d379778ef574a17bde2801a6f4469f8022a46a5f9e385191dc73bb1fc318996beaed4513fbd7055c2847227a1bed2469977821866534593a6e52a281499ee + languageName: node + linkType: hard + +"@jest/expect@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect@npm:29.7.0" + dependencies: + expect: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + checksum: 10/fea6c3317a8da5c840429d90bfe49d928e89c9e89fceee2149b93a11b7e9c73d2f6e4d7cdf647163da938fc4e2169e4490be6bae64952902bc7a701033fd4880 + languageName: node + linkType: hard + +"@jest/fake-timers@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/fake-timers@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@sinonjs/fake-timers": "npm:^10.0.2" + "@types/node": "npm:*" + jest-message-util: "npm:^29.7.0" + jest-mock: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10/9b394e04ffc46f91725ecfdff34c4e043eb7a16e1d78964094c9db3fde0b1c8803e45943a980e8c740d0a3d45661906de1416ca5891a538b0660481a3a828c27 + languageName: node + linkType: hard + +"@jest/globals@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/globals@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/expect": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + jest-mock: "npm:^29.7.0" + checksum: 10/97dbb9459135693ad3a422e65ca1c250f03d82b2a77f6207e7fa0edd2c9d2015fbe4346f3dc9ebff1678b9d8da74754d4d440b7837497f8927059c0642a22123 + languageName: node + linkType: hard + +"@jest/reporters@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/reporters@npm:29.7.0" + dependencies: + "@bcoe/v8-coverage": "npm:^0.2.3" + "@jest/console": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + collect-v8-coverage: "npm:^1.0.0" + exit: "npm:^0.1.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + istanbul-lib-coverage: "npm:^3.0.0" + istanbul-lib-instrument: "npm:^6.0.0" + istanbul-lib-report: "npm:^3.0.0" + istanbul-lib-source-maps: "npm:^4.0.0" + istanbul-reports: "npm:^3.1.3" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + slash: "npm:^3.0.0" + string-length: "npm:^4.0.1" + strip-ansi: "npm:^6.0.0" + v8-to-istanbul: "npm:^9.0.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10/a17d1644b26dea14445cedd45567f4ba7834f980be2ef74447204e14238f121b50d8b858fde648083d2cd8f305f81ba434ba49e37a5f4237a6f2a61180cc73dc + languageName: node + linkType: hard + +"@jest/schemas@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/schemas@npm:29.6.3" + dependencies: + "@sinclair/typebox": "npm:^0.27.8" + checksum: 10/910040425f0fc93cd13e68c750b7885590b8839066dfa0cd78e7def07bbb708ad869381f725945d66f2284de5663bbecf63e8fdd856e2ae6e261ba30b1687e93 + languageName: node + linkType: hard + +"@jest/source-map@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/source-map@npm:29.6.3" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.18" + callsites: "npm:^3.0.0" + graceful-fs: "npm:^4.2.9" + checksum: 10/bcc5a8697d471396c0003b0bfa09722c3cd879ad697eb9c431e6164e2ea7008238a01a07193dfe3cbb48b1d258eb7251f6efcea36f64e1ebc464ea3c03ae2deb + languageName: node + linkType: hard + +"@jest/test-result@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-result@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/istanbul-lib-coverage": "npm:^2.0.0" + collect-v8-coverage: "npm:^1.0.0" + checksum: 10/c073ab7dfe3c562bff2b8fee6cc724ccc20aa96bcd8ab48ccb2aa309b4c0c1923a9e703cea386bd6ae9b71133e92810475bb9c7c22328fc63f797ad3324ed189 + languageName: node + linkType: hard + +"@jest/test-sequencer@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-sequencer@npm:29.7.0" + dependencies: + "@jest/test-result": "npm:^29.7.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + slash: "npm:^3.0.0" + checksum: 10/4420c26a0baa7035c5419b0892ff8ffe9a41b1583ec54a10db3037cd46a7e29dd3d7202f8aa9d376e9e53be5f8b1bc0d16e1de6880a6d319b033b01dc4c8f639 + languageName: node + linkType: hard + +"@jest/transform@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/transform@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + babel-plugin-istanbul: "npm:^6.1.1" + chalk: "npm:^4.0.0" + convert-source-map: "npm:^2.0.0" + fast-json-stable-stringify: "npm:^2.1.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + pirates: "npm:^4.0.4" + slash: "npm:^3.0.0" + write-file-atomic: "npm:^4.0.2" + checksum: 10/30f42293545ab037d5799c81d3e12515790bb58513d37f788ce32d53326d0d72ebf5b40f989e6896739aa50a5f77be44686e510966370d58511d5ad2637c68c1 + languageName: node + linkType: hard + +"@jest/types@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/types@npm:29.6.3" + dependencies: + "@jest/schemas": "npm:^29.6.3" + "@types/istanbul-lib-coverage": "npm:^2.0.0" + "@types/istanbul-reports": "npm:^3.0.0" + "@types/node": "npm:*" + "@types/yargs": "npm:^17.0.8" + chalk: "npm:^4.0.0" + checksum: 10/f74bf512fd09bbe2433a2ad460b04668b7075235eea9a0c77d6a42222c10a79b9747dc2b2a623f140ed40d6865a2ed8f538f3cbb75169120ea863f29a7ed76cd + languageName: node + linkType: hard + +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2": + version: 0.3.3 + resolution: "@jridgewell/gen-mapping@npm:0.3.3" + dependencies: + "@jridgewell/set-array": "npm:^1.0.1" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@jridgewell/trace-mapping": "npm:^0.3.9" + checksum: 10/072ace159c39ab85944bdabe017c3de15c5e046a4a4a772045b00ff05e2ebdcfa3840b88ae27e897d473eb4d4845b37be3c78e28910c779f5aeeeae2fb7f0cc2 + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": + version: 3.1.1 + resolution: "@jridgewell/resolve-uri@npm:3.1.1" + checksum: 10/64d59df8ae1a4e74315eb1b61e012f1c7bc8aac47a3a1e683f6fe7008eab07bc512a742b7aa7c0405685d1421206de58c9c2e6adbfe23832f8bd69408ffc183e + languageName: node + linkType: hard + +"@jridgewell/set-array@npm:^1.0.1": + version: 1.1.2 + resolution: "@jridgewell/set-array@npm:1.1.2" + checksum: 10/69a84d5980385f396ff60a175f7177af0b8da4ddb81824cb7016a9ef914eee9806c72b6b65942003c63f7983d4f39a5c6c27185bbca88eb4690b62075602e28e + languageName: node + linkType: hard + +"@jridgewell/source-map@npm:^0.3.3": + version: 0.3.5 + resolution: "@jridgewell/source-map@npm:0.3.5" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.0" + "@jridgewell/trace-mapping": "npm:^0.3.9" + checksum: 10/73838ac43235edecff5efc850c0d759704008937a56b1711b28c261e270fe4bf2dc06d0b08663aeb1ab304f81f6de4f5fb844344403cf53ba7096967a9953cae + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14": + version: 1.4.15 + resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" + checksum: 10/89960ac087781b961ad918978975bcdf2051cd1741880469783c42de64239703eab9db5230d776d8e6a09d73bb5e4cb964e07d93ee6e2e7aea5a7d726e865c09 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.0.3" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + checksum: 10/83deafb8e7a5ca98993c2c6eeaa93c270f6f647a4c0dc00deb38c9cf9b2d3b7bf15e8839540155247ef034a052c0ec4466f980bf0c9e2ab63b97d16c0cedd3ff + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.9": + version: 0.3.19 + resolution: "@jridgewell/trace-mapping@npm:0.3.19" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10/06a2a4e26e3cc369c41144fad7cbee29ba9ea6aca85acc565ec8f2110e298fdbf93986e17da815afae94539dcc03115cdbdbb575d3bea356e167da6987531e4d + languageName: node + linkType: hard + +"@jsdoc/salty@npm:^0.2.1": + version: 0.2.5 + resolution: "@jsdoc/salty@npm:0.2.5" + dependencies: + lodash: "npm:^4.17.21" + checksum: 10/b3f868457af175852403bc668422d8e5e83794e750717ddf91a9470f591c48a1079a48796a0deaedad8eda7b448b6f3a02700c05aff4aba299d2d578ee58fdf5 + languageName: node + linkType: hard + +"@kwsites/file-exists@npm:^1.1.1": + version: 1.1.1 + resolution: "@kwsites/file-exists@npm:1.1.1" + dependencies: + debug: "npm:^4.1.1" + checksum: 10/4ff945de7293285133aeae759caddc71e73c4a44a12fac710fdd4f574cce2671a3f89d8165fdb03d383cfc97f3f96f677d8de3c95133da3d0e12a123a23109fe + languageName: node + linkType: hard + +"@kwsites/promise-deferred@npm:^1.1.1": + version: 1.1.1 + resolution: "@kwsites/promise-deferred@npm:1.1.1" + checksum: 10/07455477a0123d9a38afb503739eeff2c5424afa8d3dbdcc7f9502f13604488a4b1d9742fc7288832a52a6422cf1e1c0a1d51f69a39052f14d27c9a0420b6629 + languageName: node + linkType: hard + +"@mapbox/node-pre-gyp@npm:^1.0.0": + version: 1.0.11 + resolution: "@mapbox/node-pre-gyp@npm:1.0.11" + dependencies: + detect-libc: "npm:^2.0.0" + https-proxy-agent: "npm:^5.0.0" + make-dir: "npm:^3.1.0" + node-fetch: "npm:^2.6.7" + nopt: "npm:^5.0.0" + npmlog: "npm:^5.0.1" + rimraf: "npm:^3.0.2" + semver: "npm:^7.3.5" + tar: "npm:^6.1.11" + bin: + node-pre-gyp: bin/node-pre-gyp + checksum: 10/59529a2444e44fddb63057152452b00705aa58059079191126c79ac1388ae4565625afa84ed4dd1bf017d1111ab6e47907f7c5192e06d83c9496f2f3e708680a + languageName: node + linkType: hard + +"@middy/core@npm:^2.5.7": + version: 2.5.7 + resolution: "@middy/core@npm:2.5.7" + checksum: 10/1ab47fcf1e8e28fc31d666d10ee4a08f853bad743301622bf6046199340bbcd765cbdc929a1d6c49c529028aab770ba4415cc9c061b463aa641175674b901c71 + languageName: node + linkType: hard + +"@middy/http-cors@npm:^2.5.7": + version: 2.5.7 + resolution: "@middy/http-cors@npm:2.5.7" + dependencies: + "@middy/util": "npm:^2.5.7" + checksum: 10/2fdb402bff9866c073459e147dd3891e78145ab6d3544b58a5f6666d4a06972ce6ea2bf16f75e62b8a7da0da7ced8a8487fa22126cbf81304a0bb114ea687bcc + languageName: node + linkType: hard + +"@middy/util@npm:^2.5.7": + version: 2.5.7 + resolution: "@middy/util@npm:2.5.7" + checksum: 10/89713c2a3a9fe0a5720a2216853c122676365e45f5627f4337a23f6833db7d819bdc5930e70243b206569207d707e93ec25d2199e76689e9489dda4d05031ecb + languageName: node + linkType: hard + +"@noble/hashes@npm:^1.2.0": + version: 1.3.2 + resolution: "@noble/hashes@npm:1.3.2" + checksum: 10/685f59d2d44d88e738114b71011d343a9f7dce9dfb0a121f1489132f9247baa60bc985e5ec6f3213d114fbd1e1168e7294644e46cbd0ce2eba37994f28eeb51b + languageName: node + linkType: hard + +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": "npm:2.0.5" + run-parallel: "npm:^1.1.9" + checksum: 10/6ab2a9b8a1d67b067922c36f259e3b3dfd6b97b219c540877a4944549a4d49ea5ceba5663905ab5289682f1f3c15ff441d02f0447f620a42e1cb5e1937174d4b + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 10/012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": "npm:2.1.5" + fastq: "npm:^1.6.0" + checksum: 10/40033e33e96e97d77fba5a238e4bba4487b8284678906a9f616b5579ddaf868a18874c0054a75402c9fbaaa033a25ceae093af58c9c30278e35c23c9479e79b0 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^1.0.0": + version: 1.1.1 + resolution: "@npmcli/fs@npm:1.1.1" + dependencies: + "@gar/promisify": "npm:^1.0.1" + semver: "npm:^7.3.5" + checksum: 10/8b5e6d75759b9f1a8b7885913df274c8cbbb1221176872615f2aecedf47b2c36e5dfbf4046ff1a905c9f3592fbd32051b3050b8a897bf03514a1a404b39af074 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.0 + resolution: "@npmcli/fs@npm:3.1.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10/f3a7ab3a31de65e42aeb6ed03ed035ef123d2de7af4deb9d4a003d27acc8618b57d9fb9d259fe6c28ca538032a028f37337264388ba27d26d37fff7dde22476e + languageName: node + linkType: hard + +"@npmcli/move-file@npm:^1.0.1": + version: 1.1.2 + resolution: "@npmcli/move-file@npm:1.1.2" + dependencies: + mkdirp: "npm:^1.0.4" + rimraf: "npm:^3.0.2" + checksum: 10/c96381d4a37448ea280951e46233f7e541058cf57a57d4094dd4bdcaae43fa5872b5f2eb6bfb004591a68e29c5877abe3cdc210cb3588cbf20ab2877f31a7de7 + languageName: node + linkType: hard + +"@one-ini/wasm@npm:0.1.1": + version: 0.1.1 + resolution: "@one-ini/wasm@npm:0.1.1" + checksum: 10/673c11518dba2e582e42415cbefe928513616f3af25e12f6e4e6b1b98b52b3e6c14bc251a361654af63cd64f208f22a1f7556fa49da2bf7efcf28cb14f16f807 + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 10/115e8ceeec6bc69dff2048b35c0ab4f8bbee12d8bb6c1f4af758604586d802b6e669dcb02dda61d078de42c2b4ddce41b3d9e726d7daa6b4b850f4adbf7333ff + languageName: node + linkType: hard + +"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/aspromise@npm:1.1.2" + checksum: 10/8a938d84fe4889411296db66b29287bd61ea3c14c2d23e7a8325f46a2b8ce899857c5f038d65d7641805e6c1d06b495525c7faf00c44f85a7ee6476649034969 + languageName: node + linkType: hard + +"@protobufjs/base64@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/base64@npm:1.1.2" + checksum: 10/c71b100daeb3c9bdccab5cbc29495b906ba0ae22ceedc200e1ba49717d9c4ab15a6256839cebb6f9c6acae4ed7c25c67e0a95e734f612b258261d1a3098fe342 + languageName: node + linkType: hard + +"@protobufjs/codegen@npm:^2.0.4": + version: 2.0.4 + resolution: "@protobufjs/codegen@npm:2.0.4" + checksum: 10/c6ee5fa172a8464f5253174d3c2353ea520c2573ad7b6476983d9b1346f4d8f2b44aa29feb17a949b83c1816bc35286a5ea265ed9d8fdd2865acfa09668c0447 + languageName: node + linkType: hard + +"@protobufjs/eventemitter@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/eventemitter@npm:1.1.0" + checksum: 10/03af3e99f17ad421283d054c88a06a30a615922a817741b43ca1b13e7c6b37820a37f6eba9980fb5150c54dba6e26cb6f7b64a6f7d8afa83596fafb3afa218c3 + languageName: node + linkType: hard + +"@protobufjs/fetch@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/fetch@npm:1.1.0" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.1" + "@protobufjs/inquire": "npm:^1.1.0" + checksum: 10/67ae40572ad536e4ef94269199f252c024b66e3059850906bdaee161ca1d75c73d04d35cd56f147a8a5a079f5808e342b99e61942c1dae15604ff0600b09a958 + languageName: node + linkType: hard + +"@protobufjs/float@npm:^1.0.2": + version: 1.0.2 + resolution: "@protobufjs/float@npm:1.0.2" + checksum: 10/634c2c989da0ef2f4f19373d64187e2a79f598c5fb7991afb689d29a2ea17c14b796b29725945fa34b9493c17fb799e08ac0a7ccaae460ee1757d3083ed35187 + languageName: node + linkType: hard + +"@protobufjs/inquire@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/inquire@npm:1.1.0" + checksum: 10/c09efa34a5465cb120775e1a482136f2340a58b4abce7e93d72b8b5a9324a0e879275016ef9fcd73d72a4731639c54f2bb755bb82f916e4a78892d1d840bb3d2 + languageName: node + linkType: hard + +"@protobufjs/path@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/path@npm:1.1.2" + checksum: 10/bb709567935fd385a86ad1f575aea98131bbd719c743fb9b6edd6b47ede429ff71a801cecbd64fc72deebf4e08b8f1bd8062793178cdaed3713b8d15771f9b83 + languageName: node + linkType: hard + +"@protobufjs/pool@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/pool@npm:1.1.0" + checksum: 10/b9c7047647f6af28e92aac54f6f7c1f7ff31b201b4bfcc7a415b2861528854fce3ec666d7e7e10fd744da905f7d4aef2205bbcc8944ca0ca7a82e18134d00c46 + languageName: node + linkType: hard + +"@protobufjs/utf8@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/utf8@npm:1.1.0" + checksum: 10/131e289c57534c1d73a0e55782d6751dd821db1583cb2f7f7e017c9d6747addaebe79f28120b2e0185395d990aad347fb14ffa73ef4096fa38508d61a0e64602 + languageName: node + linkType: hard + +"@serverless/dashboard-plugin@npm:^7.0.2": + version: 7.0.5 + resolution: "@serverless/dashboard-plugin@npm:7.0.5" + dependencies: + "@aws-sdk/client-cloudformation": "npm:^3.410.0" + "@aws-sdk/client-sts": "npm:^3.410.0" + "@serverless/event-mocks": "npm:^1.1.1" + "@serverless/platform-client": "npm:^4.4.0" + "@serverless/utils": "npm:^6.14.0" + child-process-ext: "npm:^3.0.1" + chokidar: "npm:^3.5.3" + flat: "npm:^5.0.2" + fs-extra: "npm:^9.1.0" + js-yaml: "npm:^4.1.0" + jszip: "npm:^3.10.1" + lodash: "npm:^4.17.21" + memoizee: "npm:^0.4.15" + ncjsm: "npm:^4.3.2" + node-dir: "npm:^0.1.17" + node-fetch: "npm:^2.6.8" + open: "npm:^7.4.2" + semver: "npm:^7.3.8" + simple-git: "npm:^3.16.0" + timers-ext: "npm:^0.1.7" + type: "npm:^2.7.2" + uuid: "npm:^8.3.2" + yamljs: "npm:^0.3.0" + checksum: 10/f6d3c75ab09936254c23db697fcefa07cc5724b79b544a85c1f15ae1a8975f2e0d03e579239aa66f09107238833ab2ed1a587ee9d9dcd2644071a88f3b8e7eb1 + languageName: node + linkType: hard + +"@serverless/event-mocks@npm:^1.1.1": + version: 1.1.1 + resolution: "@serverless/event-mocks@npm:1.1.1" + dependencies: + "@types/lodash": "npm:^4.14.123" + lodash: "npm:^4.17.11" + checksum: 10/27d345f69909fec7717a23bfa58fd8cfefe13997e5cc917bcd8d249cfd57e03e44a3278d6107f9024b6e959dd505bb27d95c50a71678da106ff57d0d468f0c0d + languageName: node + linkType: hard + +"@serverless/platform-client@npm:^4.4.0": + version: 4.4.0 + resolution: "@serverless/platform-client@npm:4.4.0" + dependencies: + adm-zip: "npm:^0.5.5" + archiver: "npm:^5.3.0" + axios: "npm:^0.21.1" + fast-glob: "npm:^3.2.7" + https-proxy-agent: "npm:^5.0.0" + ignore: "npm:^5.1.8" + isomorphic-ws: "npm:^4.0.1" + js-yaml: "npm:^3.14.1" + jwt-decode: "npm:^2.2.0" + minimatch: "npm:^3.0.4" + querystring: "npm:^0.2.1" + run-parallel-limit: "npm:^1.1.0" + throat: "npm:^5.0.0" + traverse: "npm:^0.6.6" + ws: "npm:^7.5.3" + checksum: 10/1df46d03318f31ed7e48c67b7a45baa38ae786578cf365aa33ad38bea9ff61c20ed719c9e569e8cbf09c71da1d363bb9e203448474f8bd416898a1438c5472a5 + languageName: node + linkType: hard + +"@serverless/utils@npm:^6.13.1, @serverless/utils@npm:^6.14.0, @serverless/utils@npm:^6.15.0": + version: 6.15.0 + resolution: "@serverless/utils@npm:6.15.0" + dependencies: + archive-type: "npm:^4.0.0" + chalk: "npm:^4.1.2" + ci-info: "npm:^3.8.0" + cli-progress-footer: "npm:^2.3.2" + content-disposition: "npm:^0.5.4" + d: "npm:^1.0.1" + decompress: "npm:^4.2.1" + event-emitter: "npm:^0.3.5" + ext: "npm:^1.7.0" + ext-name: "npm:^5.0.0" + file-type: "npm:^16.5.4" + filenamify: "npm:^4.3.0" + get-stream: "npm:^6.0.1" + got: "npm:^11.8.6" + inquirer: "npm:^8.2.5" + js-yaml: "npm:^4.1.0" + jwt-decode: "npm:^3.1.2" + lodash: "npm:^4.17.21" + log: "npm:^6.3.1" + log-node: "npm:^8.0.3" + make-dir: "npm:^4.0.0" + memoizee: "npm:^0.4.15" + ms: "npm:^2.1.3" + ncjsm: "npm:^4.3.2" + node-fetch: "npm:^2.6.11" + open: "npm:^8.4.2" + p-event: "npm:^4.2.0" + supports-color: "npm:^8.1.1" + timers-ext: "npm:^0.1.7" + type: "npm:^2.7.2" + uni-global: "npm:^1.0.0" + uuid: "npm:^8.3.2" + write-file-atomic: "npm:^4.0.2" + checksum: 10/061e43fac8497fa0cf15349309171c113dbb47e5e4098106dc115c2f1f6538b6655353917e76dd2b33a565d73d47fa767e74489af799a17a52e31c1000a3c0c4 + languageName: node + linkType: hard + +"@sideway/address@npm:^4.1.3": + version: 4.1.4 + resolution: "@sideway/address@npm:4.1.4" + dependencies: + "@hapi/hoek": "npm:^9.0.0" + checksum: 10/48c422bd2d1d1c7bff7e834f395b870a66862125e9f2302f50c781a33e9f4b2b004b4db0003b232899e71c5f649d39f34aa6702a55947145708d7689ae323cc5 + languageName: node + linkType: hard + +"@sideway/formula@npm:^3.0.1": + version: 3.0.1 + resolution: "@sideway/formula@npm:3.0.1" + checksum: 10/8d3ee7f80df4e5204b2cbe92a2a711ca89684965a5c9eb3b316b7051212d3522e332a65a0bb2a07cc708fcd1d0b27fcb30f43ff0bcd5089d7006c7160a89eefe + languageName: node + linkType: hard + +"@sideway/pinpoint@npm:^2.0.0": + version: 2.0.0 + resolution: "@sideway/pinpoint@npm:2.0.0" + checksum: 10/1ed21800128b2b23280ba4c9db26c8ff6142b97a8683f17639fd7f2128aa09046461574800b30fb407afc5b663c2331795ccf3b654d4b38fa096e41a5c786bf8 + languageName: node + linkType: hard + +"@sinclair/typebox@npm:^0.27.8": + version: 0.27.8 + resolution: "@sinclair/typebox@npm:0.27.8" + checksum: 10/297f95ff77c82c54de8c9907f186076e715ff2621c5222ba50b8d40a170661c0c5242c763cba2a4791f0f91cb1d8ffa53ea1d7294570cf8cd4694c0e383e484d + languageName: node + linkType: hard + +"@sindresorhus/is@npm:^4.0.0": + version: 4.6.0 + resolution: "@sindresorhus/is@npm:4.6.0" + checksum: 10/e7f36ed72abfcd5e0355f7423a72918b9748bb1ef370a59f3e5ad8d40b728b85d63b272f65f63eec1faf417cda89dcb0aeebe94015647b6054659c1442fe5ce0 + languageName: node + linkType: hard + +"@sinonjs/commons@npm:^3.0.0": + version: 3.0.0 + resolution: "@sinonjs/commons@npm:3.0.0" + dependencies: + type-detect: "npm:4.0.8" + checksum: 10/086720ae0bc370829322df32612205141cdd44e592a8a9ca97197571f8f970352ea39d3bda75b347c43789013ddab36b34b59e40380a49bdae1c2df3aa85fe4f + languageName: node + linkType: hard + +"@sinonjs/fake-timers@npm:^10.0.2": + version: 10.3.0 + resolution: "@sinonjs/fake-timers@npm:10.3.0" + dependencies: + "@sinonjs/commons": "npm:^3.0.0" + checksum: 10/78155c7bd866a85df85e22028e046b8d46cf3e840f72260954f5e3ed5bd97d66c595524305a6841ffb3f681a08f6e5cef572a2cce5442a8a232dc29fb409b83e + languageName: node + linkType: hard + +"@smithy/abort-controller@npm:^2.0.10": + version: 2.0.10 + resolution: "@smithy/abort-controller@npm:2.0.10" + dependencies: + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/cae4813cfe48bf07d1acde5c67b58c92333887809323ba7c587ed3aed4d6f228a330cb5f5c1ef1a65f4765db63cd81588f3d533ba4301976b57fe74ead50352b + languageName: node + linkType: hard + +"@smithy/abort-controller@npm:^2.0.14": + version: 2.0.14 + resolution: "@smithy/abort-controller@npm:2.0.14" + dependencies: + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/ec0334438bcbcdbeee0c1005b95ca10f79f8e03f145ac854183cba1963cba368380d3dfd44eca208a7c6cd627597edea1dafbc99e269e29201a61dec08aa6987 + languageName: node + linkType: hard + +"@smithy/abort-controller@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/abort-controller@npm:2.0.15" + dependencies: + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/c2ee2d57cfe58515f8a228f72ad2aa033db294fa13295078b836de9839f512e681245720f6130f66c13b314b3dff0f8b0886758e5117250511d66c41af07125f + languageName: node + linkType: hard + +"@smithy/config-resolver@npm:^2.0.10, @smithy/config-resolver@npm:^2.0.11": + version: 2.0.11 + resolution: "@smithy/config-resolver@npm:2.0.11" + dependencies: + "@smithy/node-config-provider": "npm:^2.0.13" + "@smithy/types": "npm:^2.3.4" + "@smithy/util-config-provider": "npm:^2.0.0" + "@smithy/util-middleware": "npm:^2.0.3" + tslib: "npm:^2.5.0" + checksum: 10/3a1ff763457a51692efd775b6997313d248fcc862a8ab5798231b95a31a0d5179e3c5e882d6508a7992c99ca3a06895df652d03e4a1a83bd3019418372dcab01 + languageName: node + linkType: hard + +"@smithy/config-resolver@npm:^2.0.18, @smithy/config-resolver@npm:^2.0.19": + version: 2.0.19 + resolution: "@smithy/config-resolver@npm:2.0.19" + dependencies: + "@smithy/node-config-provider": "npm:^2.1.6" + "@smithy/types": "npm:^2.6.0" + "@smithy/util-config-provider": "npm:^2.0.0" + "@smithy/util-middleware": "npm:^2.0.7" + tslib: "npm:^2.5.0" + checksum: 10/c2d7dc945df3a3d8e4e14e371bdb4653b75d3c481e680cc559ae15ef3464d7c44a35de936a982726c4cc04a87d918e5af5ef9efe10115f9d3fff112aee604222 + languageName: node + linkType: hard + +"@smithy/config-resolver@npm:^2.0.21": + version: 2.0.21 + resolution: "@smithy/config-resolver@npm:2.0.21" + dependencies: + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/types": "npm:^2.7.0" + "@smithy/util-config-provider": "npm:^2.0.0" + "@smithy/util-middleware": "npm:^2.0.8" + tslib: "npm:^2.5.0" + checksum: 10/21c1a8eb4ee5d22f98713a41c16bdfe40e5f9c7cd0a5eb4442ee18f87afb405a4b845ee76fd794ce61060e993fa24522b46554dfc225a287dfa44b3dc7374f0c + languageName: node + linkType: hard + +"@smithy/core@npm:^1.1.0": + version: 1.1.0 + resolution: "@smithy/core@npm:1.1.0" + dependencies: + "@smithy/middleware-endpoint": "npm:^2.2.3" + "@smithy/middleware-retry": "npm:^2.0.24" + "@smithy/middleware-serde": "npm:^2.0.15" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/smithy-client": "npm:^2.1.18" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/293aa1fc614a677e3de3eceb29dafff2ee469efe698f939c37d0b8067ec85c112f49be6a5e305617d572852d50a2daced933c0e42be1c9f327823d6fd1470c32 + languageName: node + linkType: hard + +"@smithy/credential-provider-imds@npm:^2.0.0, @smithy/credential-provider-imds@npm:^2.0.13": + version: 2.0.13 + resolution: "@smithy/credential-provider-imds@npm:2.0.13" + dependencies: + "@smithy/node-config-provider": "npm:^2.0.13" + "@smithy/property-provider": "npm:^2.0.11" + "@smithy/types": "npm:^2.3.4" + "@smithy/url-parser": "npm:^2.0.10" + tslib: "npm:^2.5.0" + checksum: 10/a6362273bee9fb6e3536a6ffff8d1e6634a86540aca77d2804a6c86ab204666af1b8cff8a57849ecd611faa046b1de1f969bc3776a37f1cddced846ffe1867dc + languageName: node + linkType: hard + +"@smithy/credential-provider-imds@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/credential-provider-imds@npm:2.1.2" + dependencies: + "@smithy/node-config-provider": "npm:^2.1.6" + "@smithy/property-provider": "npm:^2.0.15" + "@smithy/types": "npm:^2.6.0" + "@smithy/url-parser": "npm:^2.0.14" + tslib: "npm:^2.5.0" + checksum: 10/632d023515bb436e80d6d82268dadf1cb86721e086d027bcf33fa7cfefa2f4bcae8aa7171f6ad07bab7e0476f74e05c5b381bccaf759da25c9ec8406802d06cb + languageName: node + linkType: hard + +"@smithy/credential-provider-imds@npm:^2.1.4": + version: 2.1.4 + resolution: "@smithy/credential-provider-imds@npm:2.1.4" + dependencies: + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/property-provider": "npm:^2.0.16" + "@smithy/types": "npm:^2.7.0" + "@smithy/url-parser": "npm:^2.0.15" + tslib: "npm:^2.5.0" + checksum: 10/4311eae8ba86563b7ba7c1f45a7c75f94b82c8b527a336af9631391c383666329273d5c9e0c9e2b62eeb10253b048003b293dc88ae038f878995b5a85c2e2c69 + languageName: node + linkType: hard + +"@smithy/eventstream-codec@npm:^2.0.10": + version: 2.0.10 + resolution: "@smithy/eventstream-codec@npm:2.0.10" + dependencies: + "@aws-crypto/crc32": "npm:3.0.0" + "@smithy/types": "npm:^2.3.4" + "@smithy/util-hex-encoding": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/429e4cf491926e72938249ff7230b84d15803a3ec706a6ad58960642e8d28dce54a44f1eaca5c8b8ed88fa1ee5d6e36dd5e0fb67010b820dba86efceaeca6a7e + languageName: node + linkType: hard + +"@smithy/eventstream-codec@npm:^2.0.14": + version: 2.0.14 + resolution: "@smithy/eventstream-codec@npm:2.0.14" + dependencies: + "@aws-crypto/crc32": "npm:3.0.0" + "@smithy/types": "npm:^2.6.0" + "@smithy/util-hex-encoding": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/a124898d3138ac43bdd65af5fef5eba4e7270e9d1d93602ea4101e3648b6d3f56ed348e759772c007f0b253c542a01e5161cdbe3d4414d82abef5daf4fe5bed3 + languageName: node + linkType: hard + +"@smithy/eventstream-codec@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/eventstream-codec@npm:2.0.15" + dependencies: + "@aws-crypto/crc32": "npm:3.0.0" + "@smithy/types": "npm:^2.7.0" + "@smithy/util-hex-encoding": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/feed4eeb80d636d0d9654eefa269e51e10b1afcc696ad81e144acc7dd30d3123793d05a36949c9454d64fd43414a6e1a720b689c917d2ec0c8e19d417ae78ede + languageName: node + linkType: hard + +"@smithy/eventstream-serde-browser@npm:^2.0.13": + version: 2.0.14 + resolution: "@smithy/eventstream-serde-browser@npm:2.0.14" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^2.0.14" + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/e7caaf6ad57c646329a0b9f061ae0a623f09f3818154bf495b3bf8be5c375e5c93e2a0a1b4f8bb421212ffe28a63c20f2bc13d35e5700146f6c81a8b4f9b30f1 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-browser@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/eventstream-serde-browser@npm:2.0.15" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^2.0.15" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/8bae1d59247ffb1b20a308a944e34f95b7725eedc45d8d4f5cce6d3e732cd94a889933057f4ceb7b0e9bd64afd3384febe71a6a9d25452aa9eaa6858b0072a13 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-browser@npm:^2.0.9": + version: 2.0.10 + resolution: "@smithy/eventstream-serde-browser@npm:2.0.10" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^2.0.10" + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/b66bd20abb727dba1e4aa45f08222ce88501b8fb55a4eae8d2e8f1d5133979a2d73395a5439908bbe250e2c4201063fbb077eb1c18518ecf3cf01037965fae86 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-config-resolver@npm:^2.0.13": + version: 2.0.14 + resolution: "@smithy/eventstream-serde-config-resolver@npm:2.0.14" + dependencies: + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/47540c64f5d847736419e086eee9ead42ea42d262e8f6565b859e7d5bd7e1416cef1bc7c489cd6d8ee781017d5cd0d66c6a42b54521f3846e26fa1374ebec5ca + languageName: node + linkType: hard + +"@smithy/eventstream-serde-config-resolver@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/eventstream-serde-config-resolver@npm:2.0.15" + dependencies: + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/d8124edd4aefac89e2c5286d6c88d997214cc20c0739c0c0167a8122cabdd72ba80c052c3f4893e10f2ec7a517cc97c2e77885f2caba09e5bb711a0bd03630ba + languageName: node + linkType: hard + +"@smithy/eventstream-serde-config-resolver@npm:^2.0.9": + version: 2.0.10 + resolution: "@smithy/eventstream-serde-config-resolver@npm:2.0.10" + dependencies: + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/bd8a16aa1bf68c9544dc5d74ebbdb3ef450b1e2d46994d047ae54e24b4b8f0de3c014858ea7a4f3afd2b281f13a36882b0fac88e8ab5d3496dc9584b969b989f + languageName: node + linkType: hard + +"@smithy/eventstream-serde-node@npm:^2.0.13": + version: 2.0.14 + resolution: "@smithy/eventstream-serde-node@npm:2.0.14" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^2.0.14" + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/cdd3d44296377422a4e61a54a795fde5d7675f068c00b2199c2a28245ae89ec39b9171419873427549a423f1ba20139f7572d2945a93d18dac14743bdbb15dea + languageName: node + linkType: hard + +"@smithy/eventstream-serde-node@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/eventstream-serde-node@npm:2.0.15" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^2.0.15" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/7f1bec1d952089f9629cd73e3161c24bbd4f8a781001ae40e10b557152cb944a898ad3f46228b41366f1e46a190eb8d9fea8ea9cb1fb93e19a182b374f9f1839 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-node@npm:^2.0.9": + version: 2.0.10 + resolution: "@smithy/eventstream-serde-node@npm:2.0.10" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^2.0.10" + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/b990642ad9f7c652edf4758e96d800ff998a15edd33de7266c5429669c60dd6d3e45e079a98a1a2f3574089bc9037dc10a02b97f75b359bb5750c45aae26be9c + languageName: node + linkType: hard + +"@smithy/eventstream-serde-universal@npm:^2.0.10": + version: 2.0.10 + resolution: "@smithy/eventstream-serde-universal@npm:2.0.10" + dependencies: + "@smithy/eventstream-codec": "npm:^2.0.10" + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/3ed6a6deb16b5b41e79146f49599fb464392c5d501d2555f259534543508db3479fa6b47d7e435156e6e39303fb87cec27091808efd71d264e27504ca657c2fa + languageName: node + linkType: hard + +"@smithy/eventstream-serde-universal@npm:^2.0.14": + version: 2.0.14 + resolution: "@smithy/eventstream-serde-universal@npm:2.0.14" + dependencies: + "@smithy/eventstream-codec": "npm:^2.0.14" + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/e4f90a7caf6604e62955a6524082eb25e94d9c1514f0cea9474cf9b33f252ec4d3d48a50cfeac75399a99a492b21f671b24ae5abb4935b0aa2c849a789139031 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-universal@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/eventstream-serde-universal@npm:2.0.15" + dependencies: + "@smithy/eventstream-codec": "npm:^2.0.15" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/40f330bbdc3a5e4b372324bbe97c75e4d7e6cfcf530b6a022015da659c95881260d6e871cbdd19e1d5a80af62ef4ef509cba5f75f18e3e8b941d7e2e431ace3b + languageName: node + linkType: hard + +"@smithy/fetch-http-handler@npm:^2.1.5, @smithy/fetch-http-handler@npm:^2.2.1": + version: 2.2.1 + resolution: "@smithy/fetch-http-handler@npm:2.2.1" + dependencies: + "@smithy/protocol-http": "npm:^3.0.6" + "@smithy/querystring-builder": "npm:^2.0.10" + "@smithy/types": "npm:^2.3.4" + "@smithy/util-base64": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/a9d878e63dbac0329f08c40628afda58c4c0758fa33f2a11b9cded8447eb600d61ac244f174affe3a4e2ac7250f600197ffb13136a52ae6ca71c04b6ce5a5f70 + languageName: node + linkType: hard + +"@smithy/fetch-http-handler@npm:^2.2.6, @smithy/fetch-http-handler@npm:^2.2.7": + version: 2.2.7 + resolution: "@smithy/fetch-http-handler@npm:2.2.7" + dependencies: + "@smithy/protocol-http": "npm:^3.0.10" + "@smithy/querystring-builder": "npm:^2.0.14" + "@smithy/types": "npm:^2.6.0" + "@smithy/util-base64": "npm:^2.0.1" + tslib: "npm:^2.5.0" + checksum: 10/73f868d456d7b5aa7a116f35d13e45bf93f0936ec10dac48cce04d866130f3335cf545eb0d16a4c248aa48d6f5b7a1ba5666ba912d6a8f0295c2cd37d1ec3196 + languageName: node + linkType: hard + +"@smithy/fetch-http-handler@npm:^2.3.1": + version: 2.3.1 + resolution: "@smithy/fetch-http-handler@npm:2.3.1" + dependencies: + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/querystring-builder": "npm:^2.0.15" + "@smithy/types": "npm:^2.7.0" + "@smithy/util-base64": "npm:^2.0.1" + tslib: "npm:^2.5.0" + checksum: 10/a0b50b2f4ed03018d132b8c3d57493c9ff46711d961fc2c05b66274a1749e4ddc15a2d589515aa7019c67f33dbcc388113c2860e818dea827342750c6dc7d70d + languageName: node + linkType: hard + +"@smithy/hash-node@npm:^2.0.15": + version: 2.0.16 + resolution: "@smithy/hash-node@npm:2.0.16" + dependencies: + "@smithy/types": "npm:^2.6.0" + "@smithy/util-buffer-from": "npm:^2.0.0" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/740e0794d20a9553095c705a307bfe8fa384519b98e2df515b5b0873752913e33845620a541ba299a9cdd7fd9fad588a6573f801aa86a4644408fd086da7cc07 + languageName: node + linkType: hard + +"@smithy/hash-node@npm:^2.0.17": + version: 2.0.17 + resolution: "@smithy/hash-node@npm:2.0.17" + dependencies: + "@smithy/types": "npm:^2.7.0" + "@smithy/util-buffer-from": "npm:^2.0.0" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/7b6923a2f2b7eb461facc5112568f6645265ddc8503be7c49586fb44c283d08f27c362f3f828c1d0ec052953ce9d20149f5b0ce399cbb4da7d09dc7c55a2f2dd + languageName: node + linkType: hard + +"@smithy/hash-node@npm:^2.0.9": + version: 2.0.10 + resolution: "@smithy/hash-node@npm:2.0.10" + dependencies: + "@smithy/types": "npm:^2.3.4" + "@smithy/util-buffer-from": "npm:^2.0.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/02dd05f865521ee86ffce16bfcd93a5baeae19e8574771a4c9121e91422de2016fd6f7f98c6ba599367afffc04a0d53621eb84de0a1846ec2e322bf7c4a0b066 + languageName: node + linkType: hard + +"@smithy/invalid-dependency@npm:^2.0.13": + version: 2.0.14 + resolution: "@smithy/invalid-dependency@npm:2.0.14" + dependencies: + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/cdb7f4de939ef7bb5a666fa47fb6d65bb4684855a4d97056a0457697e0caf276b735f6409df90b96d9b51560aca7ba45bf08cc3288fb23619179c4ab3ba7c1b0 + languageName: node + linkType: hard + +"@smithy/invalid-dependency@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/invalid-dependency@npm:2.0.15" + dependencies: + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/3bfc6a221b7dfd58b2acb93a40928fb7e240379406fea3d12b34fb924b7158b18849028c32bafa7df04405a2d4be65179f63d31cf0003058a55f136b5e2860e8 + languageName: node + linkType: hard + +"@smithy/invalid-dependency@npm:^2.0.9": + version: 2.0.10 + resolution: "@smithy/invalid-dependency@npm:2.0.10" + dependencies: + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/948d6877adf2d0b4a4631e294a94425e2a99c422b0c46bc9b83a5286109a1723ab97e869f99724bd0f126d6038ac9aa620caebd00ee6be7423002d2ba83fee2a + languageName: node + linkType: hard + +"@smithy/is-array-buffer@npm:^2.0.0": + version: 2.0.0 + resolution: "@smithy/is-array-buffer@npm:2.0.0" + dependencies: + tslib: "npm:^2.5.0" + checksum: 10/30f8e51403c52f27b5a6777e565128f2c8523d6e9a99f2005cdcaa31b7401376de77fa4a245de4a397d605af1cead8bea3189f3e7450386888e1656fe728030d + languageName: node + linkType: hard + +"@smithy/md5-js@npm:^2.0.15": + version: 2.0.16 + resolution: "@smithy/md5-js@npm:2.0.16" + dependencies: + "@smithy/types": "npm:^2.6.0" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/fb37d9dc48b486660f48059e745c74d8f3a9e400e3520e7cc78ffb3f46b517227157455a008ea09a60319d5ea7072133ebd2e570bb020f0361f1a190887b82c8 + languageName: node + linkType: hard + +"@smithy/md5-js@npm:^2.0.17": + version: 2.0.17 + resolution: "@smithy/md5-js@npm:2.0.17" + dependencies: + "@smithy/types": "npm:^2.7.0" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/289d0bee75233145a4c713a9e6bf70c2259c81e7abb5cf48815b4db8205120376c6fb12d3776d970b1813f55dc2e099a8a670cf2522bf21018071bb731edd81d + languageName: node + linkType: hard + +"@smithy/middleware-content-length@npm:^2.0.11": + version: 2.0.12 + resolution: "@smithy/middleware-content-length@npm:2.0.12" + dependencies: + "@smithy/protocol-http": "npm:^3.0.6" + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/f77c75e6b2d206087faadcdc4aa4e4c4de41eb4de142e2089ceb21bad8b78de8e1be2fce0544b8fc05e716b4605af085e15ead03aa84813c61643f5e57de4705 + languageName: node + linkType: hard + +"@smithy/middleware-content-length@npm:^2.0.15": + version: 2.0.16 + resolution: "@smithy/middleware-content-length@npm:2.0.16" + dependencies: + "@smithy/protocol-http": "npm:^3.0.10" + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/32db634c119907f4ed3b27b4ad26cde1affb20d5d7dd09af450c82419c23b652c248222aab5de3dbc5ecd10dda1fc27844dba88f77ff7d5be75287d69fdcd3f7 + languageName: node + linkType: hard + +"@smithy/middleware-content-length@npm:^2.0.17": + version: 2.0.17 + resolution: "@smithy/middleware-content-length@npm:2.0.17" + dependencies: + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/77f6b93299ca3e484a5e18f835ed6b00d8a898facf8576c7566743218b3ce0e197297912156ae2b8ba87ba9bfabdd8c5b66eaa66f8fb2ead4045e39309159a6e + languageName: node + linkType: hard + +"@smithy/middleware-endpoint@npm:^2.0.9": + version: 2.0.10 + resolution: "@smithy/middleware-endpoint@npm:2.0.10" + dependencies: + "@smithy/middleware-serde": "npm:^2.0.10" + "@smithy/types": "npm:^2.3.4" + "@smithy/url-parser": "npm:^2.0.10" + "@smithy/util-middleware": "npm:^2.0.3" + tslib: "npm:^2.5.0" + checksum: 10/ecd6e336965d09b03ed62421f0b2d65bd70f964e3ac56b4c856aad62ca7b3cf87c03f6031f7128d7d7c7b904d0a0964bef3711e5c819ef98d16f6ccf01f79125 + languageName: node + linkType: hard + +"@smithy/middleware-endpoint@npm:^2.2.0": + version: 2.2.1 + resolution: "@smithy/middleware-endpoint@npm:2.2.1" + dependencies: + "@smithy/middleware-serde": "npm:^2.0.14" + "@smithy/node-config-provider": "npm:^2.1.6" + "@smithy/shared-ini-file-loader": "npm:^2.2.5" + "@smithy/types": "npm:^2.6.0" + "@smithy/url-parser": "npm:^2.0.14" + "@smithy/util-middleware": "npm:^2.0.7" + tslib: "npm:^2.5.0" + checksum: 10/2ed4d12be8c7c846e7f68f8421bb74daf43632d1276ca09d5215d8bf9033c54df7b59cfd0390a9c3e630fac9ddb456baa28f531a197eb753cad54e6b7795b5ca + languageName: node + linkType: hard + +"@smithy/middleware-endpoint@npm:^2.2.3": + version: 2.2.3 + resolution: "@smithy/middleware-endpoint@npm:2.2.3" + dependencies: + "@smithy/middleware-serde": "npm:^2.0.15" + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/shared-ini-file-loader": "npm:^2.2.7" + "@smithy/types": "npm:^2.7.0" + "@smithy/url-parser": "npm:^2.0.15" + "@smithy/util-middleware": "npm:^2.0.8" + tslib: "npm:^2.5.0" + checksum: 10/392204943143e5d692e1dc28b81371f26acc781fa34c96e0380703fe760724cfac4b2370394c377e40d56aa9e74965a48653ec8814527f73cb442e3e82ed2fd6 + languageName: node + linkType: hard + +"@smithy/middleware-retry@npm:^2.0.12": + version: 2.0.13 + resolution: "@smithy/middleware-retry@npm:2.0.13" + dependencies: + "@smithy/node-config-provider": "npm:^2.0.13" + "@smithy/protocol-http": "npm:^3.0.6" + "@smithy/service-error-classification": "npm:^2.0.3" + "@smithy/types": "npm:^2.3.4" + "@smithy/util-middleware": "npm:^2.0.3" + "@smithy/util-retry": "npm:^2.0.3" + tslib: "npm:^2.5.0" + uuid: "npm:^8.3.2" + checksum: 10/8c7e766cf6ec36fb26d2854ef09ef62c61567a5b9949da05880d314d55f747d42370e980096a991f524ccaf03966f81f9445c2da64d48676a5d4806f04d48c76 + languageName: node + linkType: hard + +"@smithy/middleware-retry@npm:^2.0.20": + version: 2.0.21 + resolution: "@smithy/middleware-retry@npm:2.0.21" + dependencies: + "@smithy/node-config-provider": "npm:^2.1.6" + "@smithy/protocol-http": "npm:^3.0.10" + "@smithy/service-error-classification": "npm:^2.0.7" + "@smithy/types": "npm:^2.6.0" + "@smithy/util-middleware": "npm:^2.0.7" + "@smithy/util-retry": "npm:^2.0.7" + tslib: "npm:^2.5.0" + uuid: "npm:^8.3.2" + checksum: 10/61de5f151315c26919f117d019f1a971f78365ee7d3de1c0b32425b4962f04199521df771037790e4026c550aceed77041430cc247ec0e05e9c14bb24ae4d4ea + languageName: node + linkType: hard + +"@smithy/middleware-retry@npm:^2.0.24": + version: 2.0.24 + resolution: "@smithy/middleware-retry@npm:2.0.24" + dependencies: + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/service-error-classification": "npm:^2.0.8" + "@smithy/smithy-client": "npm:^2.1.18" + "@smithy/types": "npm:^2.7.0" + "@smithy/util-middleware": "npm:^2.0.8" + "@smithy/util-retry": "npm:^2.0.8" + tslib: "npm:^2.5.0" + uuid: "npm:^8.3.2" + checksum: 10/7627caec01d37169892f4d13dbc4dcfd8b6a9e7d49c5d9bb4b770ebd9c90c7a71faf12b6461c6a9b154a54dba22656e3cf150710c5c58d54602de2bb14f9d3a8 + languageName: node + linkType: hard + +"@smithy/middleware-serde@npm:^2.0.10, @smithy/middleware-serde@npm:^2.0.9": + version: 2.0.10 + resolution: "@smithy/middleware-serde@npm:2.0.10" + dependencies: + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/7a15c2fb69337a354af94d8317b82e5da64acdf8fd3dea5f42c2c149ac444bdc6e9b93380c5be6e2a57ae4b0189d2fad92a9a6d10f3340acadeba5edacf1150e + languageName: node + linkType: hard + +"@smithy/middleware-serde@npm:^2.0.13, @smithy/middleware-serde@npm:^2.0.14": + version: 2.0.14 + resolution: "@smithy/middleware-serde@npm:2.0.14" + dependencies: + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/6343405b1844aaa01ebb254bdddfec37b617d28bcac09dfaf80940410f767cd4a79784609e4522e459e2e1e5db2c52a2e5b0547f7d7b2831b63324db2f519586 + languageName: node + linkType: hard + +"@smithy/middleware-serde@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/middleware-serde@npm:2.0.15" + dependencies: + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/7c1a4a027422fde89c2a55463a44266020efe72e278414ff50281715db8ff7a0229ead1c1c75634a5b722a0b40468d489acb59c3ff19004591b74b6b21d8792b + languageName: node + linkType: hard + +"@smithy/middleware-stack@npm:^2.0.2, @smithy/middleware-stack@npm:^2.0.4": + version: 2.0.4 + resolution: "@smithy/middleware-stack@npm:2.0.4" + dependencies: + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/e2f4cdbe2a4b91215a132b2c7e84a4532e3a2cd18aa40d9b8e1ce4d176715696f5bcefa1aaf64ac25c2b353d215f56aa65535b678dcd1c35a65f69bc971bf9c9 + languageName: node + linkType: hard + +"@smithy/middleware-stack@npm:^2.0.7, @smithy/middleware-stack@npm:^2.0.8": + version: 2.0.8 + resolution: "@smithy/middleware-stack@npm:2.0.8" + dependencies: + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/55ad4d0513eb635a8983b3ae3fdd75dee527ac9975b1bb9cca2276f52f8f3ffcac723dcf0a4373ed4938879581ccb0df769ea9210708374e73b0797d3904f480 + languageName: node + linkType: hard + +"@smithy/middleware-stack@npm:^2.0.9": + version: 2.0.9 + resolution: "@smithy/middleware-stack@npm:2.0.9" + dependencies: + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/f61cc4ba71760424a63528f84f57d8ee71314a945d72f6fbe685308f08da817e8023b98c06c352b747684205668a73afe4d03bcb12013f5a254399850f88e01c + languageName: node + linkType: hard + +"@smithy/node-config-provider@npm:^2.0.12, @smithy/node-config-provider@npm:^2.0.13": + version: 2.0.13 + resolution: "@smithy/node-config-provider@npm:2.0.13" + dependencies: + "@smithy/property-provider": "npm:^2.0.11" + "@smithy/shared-ini-file-loader": "npm:^2.0.12" + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/9cdee5599b01ef2c9f8c3aa9e0728be7be18ec40c9f17bcb557319bb9c93248425e512841709e0bb1fd12c56b5671f5903dec612712a06ad7bbd3422f9f34856 + languageName: node + linkType: hard + +"@smithy/node-config-provider@npm:^2.1.5, @smithy/node-config-provider@npm:^2.1.6": + version: 2.1.6 + resolution: "@smithy/node-config-provider@npm:2.1.6" + dependencies: + "@smithy/property-provider": "npm:^2.0.15" + "@smithy/shared-ini-file-loader": "npm:^2.2.5" + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/01d69eba3f1ce86cc1e9951fe344da43546612c8e1c981ee0f42b551b30a0b7ff435d9653d74dde42be331fba3f7a9f5afedbb62f800a32725151377f6957b7d + languageName: node + linkType: hard + +"@smithy/node-config-provider@npm:^2.1.8": + version: 2.1.8 + resolution: "@smithy/node-config-provider@npm:2.1.8" + dependencies: + "@smithy/property-provider": "npm:^2.0.16" + "@smithy/shared-ini-file-loader": "npm:^2.2.7" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/938203fb0b3166c8334c4e74bc9153f707b392f1cf93a8397f4ba07736b5cb339d05115aa3efa6afc7bb015cbd0e76bd7ea7a13d35dd3cc643ac969e9ef4d893 + languageName: node + linkType: hard + +"@smithy/node-http-handler@npm:^2.1.10, @smithy/node-http-handler@npm:^2.1.9": + version: 2.1.10 + resolution: "@smithy/node-http-handler@npm:2.1.10" + dependencies: + "@smithy/abort-controller": "npm:^2.0.14" + "@smithy/protocol-http": "npm:^3.0.10" + "@smithy/querystring-builder": "npm:^2.0.14" + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/22af345a37cdba4973d496654bd32ab01f5ec176d312b50e0ae44a27c4857b18729f3acc2517ecc78925f28592b05ae104963d963bb1517bb4bcec30bd0e0d4e + languageName: node + linkType: hard + +"@smithy/node-http-handler@npm:^2.1.5, @smithy/node-http-handler@npm:^2.1.6": + version: 2.1.6 + resolution: "@smithy/node-http-handler@npm:2.1.6" + dependencies: + "@smithy/abort-controller": "npm:^2.0.10" + "@smithy/protocol-http": "npm:^3.0.6" + "@smithy/querystring-builder": "npm:^2.0.10" + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/c7720b9b23f4fd5e88eed011292134219eeacf02bb8cfe758d461b685baa30fe46484a6cda3797d58c0b32fc76fd5046656c2b4e3464dd21757c4f05519e69f1 + languageName: node + linkType: hard + +"@smithy/node-http-handler@npm:^2.2.1": + version: 2.2.1 + resolution: "@smithy/node-http-handler@npm:2.2.1" + dependencies: + "@smithy/abort-controller": "npm:^2.0.15" + "@smithy/protocol-http": "npm:^3.0.11" + "@smithy/querystring-builder": "npm:^2.0.15" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/1df37d998e9ca5b5e99a6ee20185eb1e55b104acf2f96ab1bd1007906e049e13dc8ae8cef36a6379d9d296440c7110349dce7fef11a341eb2704f0c24524c5d6 + languageName: node + linkType: hard + +"@smithy/property-provider@npm:^2.0.0, @smithy/property-provider@npm:^2.0.11": + version: 2.0.11 + resolution: "@smithy/property-provider@npm:2.0.11" + dependencies: + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/1b15aeb9c7a28d8587d3607ad7046c85fab3b3ed51d7113dda942e9bd9c02b717a8ca03d7fd7f6edfaec3aa5e08305103d572a9ef4346cee49d2adc7bd52cf95 + languageName: node + linkType: hard + +"@smithy/property-provider@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/property-provider@npm:2.0.15" + dependencies: + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/672e7730ca541a95d74e1a698790aea7c5c64994eff941e7b932f6dd60a66aa8fa8e594f00710df94d9f8b4f34882f2ddaf93e349ef01d6bb30fe39d7ccfb38a + languageName: node + linkType: hard + +"@smithy/property-provider@npm:^2.0.16": + version: 2.0.16 + resolution: "@smithy/property-provider@npm:2.0.16" + dependencies: + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/62984f913b7ba77d41fa4ffec354c45d2bb3eb9df209edfddf9b4ea6aec29c745ab79418960ae2a3f109291ffb1279746a05929692b76df23c1fc5569a5837e4 + languageName: node + linkType: hard + +"@smithy/protocol-http@npm:^3.0.10, @smithy/protocol-http@npm:^3.0.9": + version: 3.0.10 + resolution: "@smithy/protocol-http@npm:3.0.10" + dependencies: + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/8efbdad96105fd0c29abfd2396f0b1e9e08747b1275a8e147e0bbcdffdd95b6deb06ac8354bca9ba9c0b82a0bbb5b98b16331e0c5f87d069c515b04126c5c12f + languageName: node + linkType: hard + +"@smithy/protocol-http@npm:^3.0.11": + version: 3.0.11 + resolution: "@smithy/protocol-http@npm:3.0.11" + dependencies: + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/7d56eaaf9f712e3af0d2607b9e24bf36a4e8fd369737b6401c329278319e5e91394170bfcf82ec77c3fb672f6dec943cba377ebdaf085fd3d3e0c1ae6cc54d08 + languageName: node + linkType: hard + +"@smithy/protocol-http@npm:^3.0.5, @smithy/protocol-http@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/protocol-http@npm:3.0.6" + dependencies: + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/5c807dbfd42691bcdba3e7bd49e509a03ab7a77b78806089146ed7332bcbef6e85a04282c707b1fb2b25ec906e4cf0516620900a71054a25b10262224c864bfb + languageName: node + linkType: hard + +"@smithy/querystring-builder@npm:^2.0.10": + version: 2.0.10 + resolution: "@smithy/querystring-builder@npm:2.0.10" + dependencies: + "@smithy/types": "npm:^2.3.4" + "@smithy/util-uri-escape": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/dbdd8b5d62313bd3125a942a1706cc0c45c519ceca74e1baa2e4026af5117fa2429dfdd25d2b35e3f30d0824d96ba244ba3b18016f53deed6b35b9de2f48168a + languageName: node + linkType: hard + +"@smithy/querystring-builder@npm:^2.0.14": + version: 2.0.14 + resolution: "@smithy/querystring-builder@npm:2.0.14" + dependencies: + "@smithy/types": "npm:^2.6.0" + "@smithy/util-uri-escape": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/7ee2ac4ea48a75a3e63af90bd3b8b3f508bae3b257a0037ba6e767e19b60536558cc0ee5a54761b413ada64b0c970fc01b063b8c2d22275a85a4572498a88798 + languageName: node + linkType: hard + +"@smithy/querystring-builder@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/querystring-builder@npm:2.0.15" + dependencies: + "@smithy/types": "npm:^2.7.0" + "@smithy/util-uri-escape": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/63cd0e29a4ed536b47954f7f641bc08f62266173f1385806c142bec80237e4727ce4bfbba6a5d48302d2f705c4e639fe4503da380989fc701181b7035623c82e + languageName: node + linkType: hard + +"@smithy/querystring-parser@npm:^2.0.10": + version: 2.0.10 + resolution: "@smithy/querystring-parser@npm:2.0.10" + dependencies: + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/727b82a6785699b7af434f57c8de64279ea2259871e1bc12dbe2124a6eafbd0c6be0fbda6c79267a35fa3b1e639e2b0210bb6c6ec4efd81d6e06c84be36cab9c + languageName: node + linkType: hard + +"@smithy/querystring-parser@npm:^2.0.14": + version: 2.0.14 + resolution: "@smithy/querystring-parser@npm:2.0.14" + dependencies: + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/19c3633ebc852b7ebfe28bfae4438b7f1d3e6bc998fd2c08ff99662f3127e5784905240395833202ed59051bf80505c78d93f34a3945f382d30847dee55cb449 + languageName: node + linkType: hard + +"@smithy/querystring-parser@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/querystring-parser@npm:2.0.15" + dependencies: + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/846eee7e7abf366d41bacaf948a38d57ebf641e47bfb8aa15c37fbec6c187d376b0d83d3ec29510b714ec490b76f7c4974eeb5ce5790adb88b5ad4dc107fea53 + languageName: node + linkType: hard + +"@smithy/service-error-classification@npm:^2.0.3": + version: 2.0.3 + resolution: "@smithy/service-error-classification@npm:2.0.3" + dependencies: + "@smithy/types": "npm:^2.3.4" + checksum: 10/d1285b00754b0cfb4f2013af9f299e6edb5c464b10d68b395e9e78a72afe9ccdf540b7e59a552b15d51db325f81281622c169ca8d6677104f74181cc882a8f1a + languageName: node + linkType: hard + +"@smithy/service-error-classification@npm:^2.0.7": + version: 2.0.7 + resolution: "@smithy/service-error-classification@npm:2.0.7" + dependencies: + "@smithy/types": "npm:^2.6.0" + checksum: 10/930c63fc88c6cc97a28dd13ae2d4a4bac41b2d6d61a84b99ab9005cccff665b126c264912d0a0250e3f3d9e152061b34df3323159f0bad7b47055dffd476bc06 + languageName: node + linkType: hard + +"@smithy/service-error-classification@npm:^2.0.8": + version: 2.0.8 + resolution: "@smithy/service-error-classification@npm:2.0.8" + dependencies: + "@smithy/types": "npm:^2.7.0" + checksum: 10/5193c8e820446793b339b885b43a3fd9e7a4ba5d2cb6ff6f4ae62a997519b669ba9b9983f146532a72fbdfb741be34754678f5fd68a4534c83741d3a069bd00a + languageName: node + linkType: hard + +"@smithy/shared-ini-file-loader@npm:^2.0.12, @smithy/shared-ini-file-loader@npm:^2.0.6": + version: 2.0.12 + resolution: "@smithy/shared-ini-file-loader@npm:2.0.12" + dependencies: + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/f893be76937cc9a7ca4218a5392c489b1160e53ed964f355207c3c9d2d7997d1547471d23b9ab0f6e3fe76b9756d4b9f49ab63d1317afff3d9e949abb1b7ea1f + languageName: node + linkType: hard + +"@smithy/shared-ini-file-loader@npm:^2.2.5": + version: 2.2.5 + resolution: "@smithy/shared-ini-file-loader@npm:2.2.5" + dependencies: + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/6dfc2d7146da7be5570c08709e4065d428573068d5863b7ddd481b6574c7e18e19ecfad8a0e01780c84bb1bdff38a1de56d7eff68b7a8c9797702c405aedceb9 + languageName: node + linkType: hard + +"@smithy/shared-ini-file-loader@npm:^2.2.7": + version: 2.2.7 + resolution: "@smithy/shared-ini-file-loader@npm:2.2.7" + dependencies: + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/014dd77366168f225488b78e85fb0a5ee73d3a5f0996fad96f4be88b8b87642be0370964634f1098a3393305f9aa84cfc6ae4946c26076d56ecc279373fbe636 + languageName: node + linkType: hard + +"@smithy/signature-v4@npm:^2.0.0": + version: 2.0.10 + resolution: "@smithy/signature-v4@npm:2.0.10" + dependencies: + "@smithy/eventstream-codec": "npm:^2.0.10" + "@smithy/is-array-buffer": "npm:^2.0.0" + "@smithy/types": "npm:^2.3.4" + "@smithy/util-hex-encoding": "npm:^2.0.0" + "@smithy/util-middleware": "npm:^2.0.3" + "@smithy/util-uri-escape": "npm:^2.0.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/6bad2825d8c0caf33d6ed21edf9aa905b1b7a6e19f866b75b86ee40d0be3b5ac35abd0f87745fc835cc2777a71742664fdcb543fea238aa2fd64d8ad7829dc78 + languageName: node + linkType: hard + +"@smithy/smithy-client@npm:^2.1.15, @smithy/smithy-client@npm:^2.1.16": + version: 2.1.16 + resolution: "@smithy/smithy-client@npm:2.1.16" + dependencies: + "@smithy/middleware-stack": "npm:^2.0.8" + "@smithy/types": "npm:^2.6.0" + "@smithy/util-stream": "npm:^2.0.21" + tslib: "npm:^2.5.0" + checksum: 10/daca467424bb742d64e077cb33cb9874c59aa11fa66d0e502aa6a453c85d7b1104056e388891fd4e954f832ff2bb14b267307e168ee974c92e1290fced49dcff + languageName: node + linkType: hard + +"@smithy/smithy-client@npm:^2.1.18": + version: 2.1.18 + resolution: "@smithy/smithy-client@npm:2.1.18" + dependencies: + "@smithy/middleware-stack": "npm:^2.0.9" + "@smithy/types": "npm:^2.7.0" + "@smithy/util-stream": "npm:^2.0.23" + tslib: "npm:^2.5.0" + checksum: 10/b34182367401e586bc0cb0f17aaba6d16955eb0e58abdbe578b5df0f4539bd0ffe17adef7cec52ec6e43cd4dce237daa77930b30deaa48b5ac298e767a412113 + languageName: node + linkType: hard + +"@smithy/smithy-client@npm:^2.1.6, @smithy/smithy-client@npm:^2.1.9": + version: 2.1.9 + resolution: "@smithy/smithy-client@npm:2.1.9" + dependencies: + "@smithy/middleware-stack": "npm:^2.0.4" + "@smithy/types": "npm:^2.3.4" + "@smithy/util-stream": "npm:^2.0.14" + tslib: "npm:^2.5.0" + checksum: 10/c0a6d991a90fbb4d7fa9399b362d3e4841c8566ef2bbc6f282d6d250237a18291571f08d522004d4838aa6712aa7b046d6df432a82e048b0d57ac76cca6e81c6 + languageName: node + linkType: hard + +"@smithy/types@npm:^2.3.3, @smithy/types@npm:^2.3.4": + version: 2.3.4 + resolution: "@smithy/types@npm:2.3.4" + dependencies: + tslib: "npm:^2.5.0" + checksum: 10/8a5ad3b47e6318215786bc61787e1ff7a11b002c9d27b4af2d307edbfa522d21097b2a6bd7f83657736f6c646a61e03cd2d1be3c3f8f7353860e976e64323584 + languageName: node + linkType: hard + +"@smithy/types@npm:^2.5.0, @smithy/types@npm:^2.6.0": + version: 2.6.0 + resolution: "@smithy/types@npm:2.6.0" + dependencies: + tslib: "npm:^2.5.0" + checksum: 10/15e147838ab1997ef1a795b844f67e307c66fd8337d5ef9e17787a58b6a04ec0bd064b91f3fba5406f525e4205ca23ceb6c19aa7673777abcb3f6263b4e39b29 + languageName: node + linkType: hard + +"@smithy/types@npm:^2.7.0": + version: 2.7.0 + resolution: "@smithy/types@npm:2.7.0" + dependencies: + tslib: "npm:^2.5.0" + checksum: 10/f2428a072b77240ebd44e3394ce723a1559e90c13ed9518b025e7c0ad589c836ab613e0f725419bfd5636d5950aaa04f9acf35f908295e1b3a7068501aae8a91 + languageName: node + linkType: hard + +"@smithy/url-parser@npm:^2.0.10, @smithy/url-parser@npm:^2.0.9": + version: 2.0.10 + resolution: "@smithy/url-parser@npm:2.0.10" + dependencies: + "@smithy/querystring-parser": "npm:^2.0.10" + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/70528ef1d821eacbdfe1b8bb5b914839f955b240a2d65952dcaf6ab92e7cedaabde55385f8b2ab594005a4b41e42faf24516d9ed4990bd6efd2218f6c081f88d + languageName: node + linkType: hard + +"@smithy/url-parser@npm:^2.0.13, @smithy/url-parser@npm:^2.0.14": + version: 2.0.14 + resolution: "@smithy/url-parser@npm:2.0.14" + dependencies: + "@smithy/querystring-parser": "npm:^2.0.14" + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/d379bfc899dc0130f46c20a1c6c75041d4d27bebbfd0f29a4d2978b524bb21fa4471133da283bff7002f8c41a7a26d385f4f264b602b7363cdba6a8308c5bbae + languageName: node + linkType: hard + +"@smithy/url-parser@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/url-parser@npm:2.0.15" + dependencies: + "@smithy/querystring-parser": "npm:^2.0.15" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/b064650b900ecb2f0426b95df56e59d590e03fc399d0c87d4368e813fcef3a13f597c9aec03661ad31a88e27d0dc4c576e6fd7b88d503f371ef857b8eb48f0b2 + languageName: node + linkType: hard + +"@smithy/util-base64@npm:^2.0.0": + version: 2.0.0 + resolution: "@smithy/util-base64@npm:2.0.0" + dependencies: + "@smithy/util-buffer-from": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/1e99afde11eea39c5400e89ae51e940bc4295d8823b4d362223f26c825bdb78b7f96df1834518f6484a272c6c44ac82ec49cb3fd5cf40108940133a208e6eedf + languageName: node + linkType: hard + +"@smithy/util-base64@npm:^2.0.1": + version: 2.0.1 + resolution: "@smithy/util-base64@npm:2.0.1" + dependencies: + "@smithy/util-buffer-from": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/6c71765396e7c36229f78b3ab7404d86390b4191350955b3af3ca6e3e42f67428801722706153f5593571be51f3b418843c49326d894cd4445eb9ed9a04844a7 + languageName: node + linkType: hard + +"@smithy/util-body-length-browser@npm:^2.0.0": + version: 2.0.0 + resolution: "@smithy/util-body-length-browser@npm:2.0.0" + dependencies: + tslib: "npm:^2.5.0" + checksum: 10/59ccbe316fe31ca08cbcad3154e6dfec960dc54ca13b1c0b73f7135054ccc7f35bf938ba306ed34dc6931bc8c444222145c8eed0d57198784dc03344e40f4100 + languageName: node + linkType: hard + +"@smithy/util-body-length-browser@npm:^2.0.1": + version: 2.0.1 + resolution: "@smithy/util-body-length-browser@npm:2.0.1" + dependencies: + tslib: "npm:^2.5.0" + checksum: 10/fdeea18772d7d4542d0192a5cf9b145f7626b8ab76be57bd7453cb73d84480bb12f83b982467b7e4dc015434e16c9e3f7ffdffa0e4ba1c4f6e570c0425bee3d1 + languageName: node + linkType: hard + +"@smithy/util-body-length-node@npm:^2.1.0": + version: 2.1.0 + resolution: "@smithy/util-body-length-node@npm:2.1.0" + dependencies: + tslib: "npm:^2.5.0" + checksum: 10/1b2e3a99811b623d68e800a4c400a0a55eb9ce12f5cfa5b8509a0fdd805a279a931759ff55472983b37dcbcc58221a3bbfef86e5e4304af973a1e2c5f8651078 + languageName: node + linkType: hard + +"@smithy/util-buffer-from@npm:^2.0.0": + version: 2.0.0 + resolution: "@smithy/util-buffer-from@npm:2.0.0" + dependencies: + "@smithy/is-array-buffer": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/15326acdb8666ff8c342bfa23ace07ea6a1b7e849b118f5b28f0b93cd775e83c77fa53ab5b04b8f795798d316991042296c3c5522fb68c91df9e921d4c83e398 + languageName: node + linkType: hard + +"@smithy/util-config-provider@npm:^2.0.0": + version: 2.0.0 + resolution: "@smithy/util-config-provider@npm:2.0.0" + dependencies: + tslib: "npm:^2.5.0" + checksum: 10/13910f0643c6bf71184049e58ec6fa5544c1ed94f6b90080fc53d32fffaacb8e4bb5bd80e55d3536af2e9684cae95842ff3e2a07c50c18f00c7f1fe35c34fd8a + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-browser@npm:^2.0.10": + version: 2.0.13 + resolution: "@smithy/util-defaults-mode-browser@npm:2.0.13" + dependencies: + "@smithy/property-provider": "npm:^2.0.11" + "@smithy/smithy-client": "npm:^2.1.9" + "@smithy/types": "npm:^2.3.4" + bowser: "npm:^2.11.0" + tslib: "npm:^2.5.0" + checksum: 10/bc92f76a9ba3046c087bc63bf90354a55832c0637c9b3e69e40a38669c7d2e6996782a23487a2f39bf2b25fcc1261dc301e5dd345241f25188bd1239ea569d1f + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-browser@npm:^2.0.19": + version: 2.0.20 + resolution: "@smithy/util-defaults-mode-browser@npm:2.0.20" + dependencies: + "@smithy/property-provider": "npm:^2.0.15" + "@smithy/smithy-client": "npm:^2.1.16" + "@smithy/types": "npm:^2.6.0" + bowser: "npm:^2.11.0" + tslib: "npm:^2.5.0" + checksum: 10/43f4f7a186f1a8fb7aeb0c6dbcde4d84c00edcc5ca9700500f003da9a02a89a913bd5ef6759a9eac9a7f8ce4400cf4827ffdba957f033051e989cca2306e7ee6 + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-browser@npm:^2.0.22": + version: 2.0.22 + resolution: "@smithy/util-defaults-mode-browser@npm:2.0.22" + dependencies: + "@smithy/property-provider": "npm:^2.0.16" + "@smithy/smithy-client": "npm:^2.1.18" + "@smithy/types": "npm:^2.7.0" + bowser: "npm:^2.11.0" + tslib: "npm:^2.5.0" + checksum: 10/69bb381e49f4f5ef22788d9367d0ea2a62d1a2411d8666ad2170e1d13dc45c0fa55116f1bb1a12f45abbac9c20997fe1f234afc718ae6ea584cb4ee4afded547 + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-node@npm:^2.0.12": + version: 2.0.15 + resolution: "@smithy/util-defaults-mode-node@npm:2.0.15" + dependencies: + "@smithy/config-resolver": "npm:^2.0.11" + "@smithy/credential-provider-imds": "npm:^2.0.13" + "@smithy/node-config-provider": "npm:^2.0.13" + "@smithy/property-provider": "npm:^2.0.11" + "@smithy/smithy-client": "npm:^2.1.9" + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/46f50108f26731729cd4f1bbf4b396d787e27bab4f614a5df6791d81482e8a28182cfd3d899af6d7343214b8a5d29af4fd9742ac39bb12fd07909bec00661432 + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-node@npm:^2.0.25": + version: 2.0.26 + resolution: "@smithy/util-defaults-mode-node@npm:2.0.26" + dependencies: + "@smithy/config-resolver": "npm:^2.0.19" + "@smithy/credential-provider-imds": "npm:^2.1.2" + "@smithy/node-config-provider": "npm:^2.1.6" + "@smithy/property-provider": "npm:^2.0.15" + "@smithy/smithy-client": "npm:^2.1.16" + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/5ef44082a7ddfe9994e3ecbba169bbfbf9ba7340b766edd1c7d31ad63a5adcbcabe9d22b3e53fe4238ce6527bf6fdeb44cc9fcef7812f8e8fbacde077a078086 + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-node@npm:^2.0.29": + version: 2.0.29 + resolution: "@smithy/util-defaults-mode-node@npm:2.0.29" + dependencies: + "@smithy/config-resolver": "npm:^2.0.21" + "@smithy/credential-provider-imds": "npm:^2.1.4" + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/property-provider": "npm:^2.0.16" + "@smithy/smithy-client": "npm:^2.1.18" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/fd55c71222a861d2737e347783acb712ede8af80ae538a5ca74ff75855128e7349f42843b7991de261862a1999a55df3877cc9d7bc5c258bd57f0e3443219c46 + languageName: node + linkType: hard + +"@smithy/util-endpoints@npm:^1.0.4": + version: 1.0.5 + resolution: "@smithy/util-endpoints@npm:1.0.5" + dependencies: + "@smithy/node-config-provider": "npm:^2.1.6" + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/65e97429d2e9e15465043a9227378555579e05a4be0d4835f82bed5a3ce795e3f51201f4f55ed3c89fa9bde250f36e858fb3cf62004294fddee54d01fe5647d8 + languageName: node + linkType: hard + +"@smithy/util-endpoints@npm:^1.0.7": + version: 1.0.7 + resolution: "@smithy/util-endpoints@npm:1.0.7" + dependencies: + "@smithy/node-config-provider": "npm:^2.1.8" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/4ede4e47a6d1f73894825e799c9d1061f20e344ff7a36971312ed3941ab820ac9803b7133fe6540a0fa85c739bc5725f095bcf2be881df444c613e468946812d + languageName: node + linkType: hard + +"@smithy/util-hex-encoding@npm:^2.0.0": + version: 2.0.0 + resolution: "@smithy/util-hex-encoding@npm:2.0.0" + dependencies: + tslib: "npm:^2.5.0" + checksum: 10/196b594d5e4a31fbc6a6ada8e1af307e0af55721685df70e20415733f46d6d2d6f7c52f9d2bf4512f0033cc1adb74f115c68025d9b7d7023342ef6f0514cee2a + languageName: node + linkType: hard + +"@smithy/util-middleware@npm:^2.0.2, @smithy/util-middleware@npm:^2.0.3": + version: 2.0.3 + resolution: "@smithy/util-middleware@npm:2.0.3" + dependencies: + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/9622d24ec5fe3ffceeeb911d3f0cffcac3f872a7960540e96a91404b354595e6b092c53c27cc2dfe65a259ae8e3f48e69673ab76df2d521bb808c13eae2d848e + languageName: node + linkType: hard + +"@smithy/util-middleware@npm:^2.0.6, @smithy/util-middleware@npm:^2.0.7": + version: 2.0.7 + resolution: "@smithy/util-middleware@npm:2.0.7" + dependencies: + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/053ee434d72d57c5629076adc42aad4357da7aab480f70fddda2b852205c4371465da450025d9719019c8e5900ff613b82332b6b050ea841d5f49dd060e135c6 + languageName: node + linkType: hard + +"@smithy/util-middleware@npm:^2.0.8": + version: 2.0.8 + resolution: "@smithy/util-middleware@npm:2.0.8" + dependencies: + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/b28342e36c301a5b2c2d7110528845e219569137c4f947614680f4fb67a5606681fd26a4c56171b814340c8d2b9b17807f34df2d60fd660c803c4d602dfe5a47 + languageName: node + linkType: hard + +"@smithy/util-retry@npm:^2.0.2, @smithy/util-retry@npm:^2.0.3": + version: 2.0.3 + resolution: "@smithy/util-retry@npm:2.0.3" + dependencies: + "@smithy/service-error-classification": "npm:^2.0.3" + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/27540886c39805a059582cd90f4559a27f1287abc2d5840983305832ca8e72ef9c517fb58ee4b0d5a0edbf4ccbdce440533b58de89091021db4357a2e84f96a4 + languageName: node + linkType: hard + +"@smithy/util-retry@npm:^2.0.6, @smithy/util-retry@npm:^2.0.7": + version: 2.0.7 + resolution: "@smithy/util-retry@npm:2.0.7" + dependencies: + "@smithy/service-error-classification": "npm:^2.0.7" + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/6ee41e84d4b87f4bdbf7ee45666387b13723230b3a1c3b86f51988e0ca878fa89c068f6c12640d52e85a8c825565ebf658620ba9a158d61fb4a2d698ecb0c2d8 + languageName: node + linkType: hard + +"@smithy/util-retry@npm:^2.0.8": + version: 2.0.8 + resolution: "@smithy/util-retry@npm:2.0.8" + dependencies: + "@smithy/service-error-classification": "npm:^2.0.8" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/fdcfc20d8a9290adfd6ebd30a6d0dcda849ba9fad9f53dcca34564ae7e871dcd97da66b739571809835ee375946a4bcc0d42cde2e98762e4829bde077d529e28 + languageName: node + linkType: hard + +"@smithy/util-stream@npm:^2.0.12, @smithy/util-stream@npm:^2.0.14": + version: 2.0.14 + resolution: "@smithy/util-stream@npm:2.0.14" + dependencies: + "@smithy/fetch-http-handler": "npm:^2.2.1" + "@smithy/node-http-handler": "npm:^2.1.6" + "@smithy/types": "npm:^2.3.4" + "@smithy/util-base64": "npm:^2.0.0" + "@smithy/util-buffer-from": "npm:^2.0.0" + "@smithy/util-hex-encoding": "npm:^2.0.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/c34a1e24036d845e0d1bc3b3d6912289ebd0721cea1e175dd6c3c67e14eedcd5753fd05a327b5c7f2a580b1de394486ab758e76be9a9ffa1a6d7c57f42b5cffb + languageName: node + linkType: hard + +"@smithy/util-stream@npm:^2.0.20, @smithy/util-stream@npm:^2.0.21": + version: 2.0.21 + resolution: "@smithy/util-stream@npm:2.0.21" + dependencies: + "@smithy/fetch-http-handler": "npm:^2.2.7" + "@smithy/node-http-handler": "npm:^2.1.10" + "@smithy/types": "npm:^2.6.0" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-buffer-from": "npm:^2.0.0" + "@smithy/util-hex-encoding": "npm:^2.0.0" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/69fe2403f1d32fd7aa9a5a71f0638b31e5aed870c5fa0b15dbf6fabb11e068e9a6c5bc85629a40b5822e521355de57e76ebee022db947120670ea96f65990cee + languageName: node + linkType: hard + +"@smithy/util-stream@npm:^2.0.23": + version: 2.0.23 + resolution: "@smithy/util-stream@npm:2.0.23" + dependencies: + "@smithy/fetch-http-handler": "npm:^2.3.1" + "@smithy/node-http-handler": "npm:^2.2.1" + "@smithy/types": "npm:^2.7.0" + "@smithy/util-base64": "npm:^2.0.1" + "@smithy/util-buffer-from": "npm:^2.0.0" + "@smithy/util-hex-encoding": "npm:^2.0.0" + "@smithy/util-utf8": "npm:^2.0.2" + tslib: "npm:^2.5.0" + checksum: 10/4dacaded6d5834fda89cc5f5348e97988fb38724461e3fa70b44b7fbfa44eb91f015914bcb98c60174b4a82666851ca8df14fe0e6bcf5c63af1e65074144e5fe + languageName: node + linkType: hard + +"@smithy/util-uri-escape@npm:^2.0.0": + version: 2.0.0 + resolution: "@smithy/util-uri-escape@npm:2.0.0" + dependencies: + tslib: "npm:^2.5.0" + checksum: 10/2f121d1fce9878e22fc5eaa0f8f4e47e967fce6d727b4283902d842842c7835b47de08e16b2c6fef389457a6edf2523274019fe511ede98ce0f38a11aea63bc2 + languageName: node + linkType: hard + +"@smithy/util-utf8@npm:^2.0.0": + version: 2.0.0 + resolution: "@smithy/util-utf8@npm:2.0.0" + dependencies: + "@smithy/util-buffer-from": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/43c924be7883287937d91a1f042196b1e7f9400e9114759c2ac5b4fedb6756063faf2e684b153a96573b0039b745c196968ce53ae9f38a2aeb690ad0c3c27ea8 + languageName: node + linkType: hard + +"@smithy/util-utf8@npm:^2.0.2": + version: 2.0.2 + resolution: "@smithy/util-utf8@npm:2.0.2" + dependencies: + "@smithy/util-buffer-from": "npm:^2.0.0" + tslib: "npm:^2.5.0" + checksum: 10/9356200ac7ccef414cd924b4fd2bfeb1d0a2e7992b4c924f0328205ab9bb8c688bc4b5c271c237db90ea75fb448f32c1f76c6e8883c2f088ea0559737ea99d9d + languageName: node + linkType: hard + +"@smithy/util-waiter@npm:^2.0.13": + version: 2.0.14 + resolution: "@smithy/util-waiter@npm:2.0.14" + dependencies: + "@smithy/abort-controller": "npm:^2.0.14" + "@smithy/types": "npm:^2.6.0" + tslib: "npm:^2.5.0" + checksum: 10/782143eb2c622787bea4ef485b872fc4726d3aee83150607bb726a717de920833645ae5ecc58edd8d7101f6c6a5632e23272d5892eca9a93d53dcb9a72b1dccd + languageName: node + linkType: hard + +"@smithy/util-waiter@npm:^2.0.15": + version: 2.0.15 + resolution: "@smithy/util-waiter@npm:2.0.15" + dependencies: + "@smithy/abort-controller": "npm:^2.0.15" + "@smithy/types": "npm:^2.7.0" + tslib: "npm:^2.5.0" + checksum: 10/87d0b08720461e651be0cd35784292c3bf5cfd88a64acd78185b1d2c5dcf66d5c6ea068d1416c33e276c9e4aef14fb93441f80843987ca5901716a5bd35cf271 + languageName: node + linkType: hard + +"@smithy/util-waiter@npm:^2.0.9": + version: 2.0.10 + resolution: "@smithy/util-waiter@npm:2.0.10" + dependencies: + "@smithy/abort-controller": "npm:^2.0.10" + "@smithy/types": "npm:^2.3.4" + tslib: "npm:^2.5.0" + checksum: 10/0cc2a97e506d1cdce5a70dfce25e8b88d451c2529b67b73c7f36d746354a0376b9cf0b91f62499a1c5db88b253bfabdbfb49e41bae300423327744c21d4a91c4 + languageName: node + linkType: hard + +"@szmarczak/http-timer@npm:^4.0.5": + version: 4.0.6 + resolution: "@szmarczak/http-timer@npm:4.0.6" + dependencies: + defer-to-connect: "npm:^2.0.0" + checksum: 10/c29df3bcec6fc3bdec2b17981d89d9c9fc9bd7d0c9bcfe92821dc533f4440bc890ccde79971838b4ceed1921d456973c4180d7175ee1d0023ad0562240a58d95 + languageName: node + linkType: hard + +"@tokenizer/token@npm:^0.3.0": + version: 0.3.0 + resolution: "@tokenizer/token@npm:0.3.0" + checksum: 10/889c1f1e63ac7c92c0ea22d4a2861142f1b43c3d92eb70ec42aa9e9851fab2e9952211d50f541b287781280df2f979bf5600a9c1f91fbc61b7fcf9994e9376a5 + languageName: node + linkType: hard + +"@tootallnate/once@npm:1": + version: 1.1.2 + resolution: "@tootallnate/once@npm:1.1.2" + checksum: 10/e1fb1bbbc12089a0cb9433dc290f97bddd062deadb6178ce9bcb93bb7c1aecde5e60184bc7065aec42fe1663622a213493c48bbd4972d931aae48315f18e1be9 + languageName: node + linkType: hard + +"@tootallnate/once@npm:2": + version: 2.0.0 + resolution: "@tootallnate/once@npm:2.0.0" + checksum: 10/ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8 + languageName: node + linkType: hard + +"@tsconfig/node10@npm:^1.0.7": + version: 1.0.9 + resolution: "@tsconfig/node10@npm:1.0.9" + checksum: 10/a33ae4dc2a621c0678ac8ac4bceb8e512ae75dac65417a2ad9b022d9b5411e863c4c198b6ba9ef659e14b9fb609bbec680841a2e84c1172df7a5ffcf076539df + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node12@npm:1.0.11" + checksum: 10/5ce29a41b13e7897a58b8e2df11269c5395999e588b9a467386f99d1d26f6c77d1af2719e407621412520ea30517d718d5192a32403b8dfcc163bf33e40a338a + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.3 + resolution: "@tsconfig/node14@npm:1.0.3" + checksum: 10/19275fe80c4c8d0ad0abed6a96dbf00642e88b220b090418609c4376e1cef81bf16237bf170ad1b341452feddb8115d8dd2e5acdfdea1b27422071163dc9ba9d + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.2": + version: 1.0.4 + resolution: "@tsconfig/node16@npm:1.0.4" + checksum: 10/202319785901f942a6e1e476b872d421baec20cf09f4b266a1854060efbf78cde16a4d256e8bc949d31e6cd9a90f1e8ef8fb06af96a65e98338a2b6b0de0a0ff + languageName: node + linkType: hard + +"@types/aws-lambda@npm:^8.10.95": + version: 8.10.123 + resolution: "@types/aws-lambda@npm:8.10.123" + checksum: 10/de782a12ac1fcd1c758f36b4d894766c31eda4b2b74aee01fc850cb2e3fb301dd629abb7e34dbe5d345faaf18fb4924bb03153bb2787446c98c665a9932d76c5 + languageName: node + linkType: hard + +"@types/babel__core@npm:^7.1.14": + version: 7.20.2 + resolution: "@types/babel__core@npm:7.20.2" + dependencies: + "@babel/parser": "npm:^7.20.7" + "@babel/types": "npm:^7.20.7" + "@types/babel__generator": "npm:*" + "@types/babel__template": "npm:*" + "@types/babel__traverse": "npm:*" + checksum: 10/78aede009117ff6c95ef36db19e27ad15ecdcb5cfc9ad57d43caa5d2f44127105691a3e6e8d1806fd305484db8a74fdec5640e88da452c511f6351353f7ac0c8 + languageName: node + linkType: hard + +"@types/babel__generator@npm:*": + version: 7.6.5 + resolution: "@types/babel__generator@npm:7.6.5" + dependencies: + "@babel/types": "npm:^7.0.0" + checksum: 10/168bbfab7662353c472e03b06c4c10d3d4134756d2b15129bed987ebaaccd52d17f0c53a9bc6522cdc50babb41ed1c8e219953acbe4c27382ccffd6cb9d8a0c2 + languageName: node + linkType: hard + +"@types/babel__template@npm:*": + version: 7.4.2 + resolution: "@types/babel__template@npm:7.4.2" + dependencies: + "@babel/parser": "npm:^7.1.0" + "@babel/types": "npm:^7.0.0" + checksum: 10/0fe977b45a3269336c77f3ae4641a6c48abf0fa35ab1a23fb571690786af02d6cec08255a43499b0b25c5633800f7ae882ace450cce905e3060fa9e6995047ae + languageName: node + linkType: hard + +"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6": + version: 7.20.2 + resolution: "@types/babel__traverse@npm:7.20.2" + dependencies: + "@babel/types": "npm:^7.20.7" + checksum: 10/4f950a5d66ff266e70e01ae0c5277efb543221da2087dc3e86b1e0c8e74431364110d1c765ab875d06d02a357962a7419270a3115a7d23421d5ad788f41d92d0 + languageName: node + linkType: hard + +"@types/body-parser@npm:*": + version: 1.19.3 + resolution: "@types/body-parser@npm:1.19.3" + dependencies: + "@types/connect": "npm:*" + "@types/node": "npm:*" + checksum: 10/932fa71437c275023799123680ef26ffd90efd37f51a1abe405e6ae6e5b4ad9511b7a3a8f5a12877ed1444a02b6286c0a137a98e914b3c61932390c83643cc2c + languageName: node + linkType: hard + +"@types/cacheable-request@npm:^6.0.1": + version: 6.0.3 + resolution: "@types/cacheable-request@npm:6.0.3" + dependencies: + "@types/http-cache-semantics": "npm:*" + "@types/keyv": "npm:^3.1.4" + "@types/node": "npm:*" + "@types/responselike": "npm:^1.0.0" + checksum: 10/159f9fdb2a1b7175eef453ae2ced5ea04c0d2b9610cc9ccd9f9abb066d36dacb1f37acd879ace10ad7cbb649490723feb396fb7307004c9670be29636304b988 + languageName: node + linkType: hard + +"@types/connect@npm:*": + version: 3.4.36 + resolution: "@types/connect@npm:3.4.36" + dependencies: + "@types/node": "npm:*" + checksum: 10/4dee3d966fb527b98f0cbbdcf6977c9193fc3204ed539b7522fe5e64dfa45f9017bdda4ffb1f760062262fce7701a0ee1c2f6ce2e50af36c74d4e37052303172 + languageName: node + linkType: hard + +"@types/debug@npm:^4.1.8": + version: 4.1.9 + resolution: "@types/debug@npm:4.1.9" + dependencies: + "@types/ms": "npm:*" + checksum: 10/e88ee8b19d106f33eb0d3bc58bacff9702e98d821fd1ebd1de8942e6b97419e19a1ccf39370f1764a1dc66f79fd4619f3412e1be6eeb9f0b76412f5ffe4ead93 + languageName: node + linkType: hard + +"@types/eslint-scope@npm:^3.7.3": + version: 3.7.5 + resolution: "@types/eslint-scope@npm:3.7.5" + dependencies: + "@types/eslint": "npm:*" + "@types/estree": "npm:*" + checksum: 10/e91ce335c3791c2cf6084caa0073f90d5b7ae3fcf27785ade8422b7d896159fa14a5a3f1efd31ef03e9ebc1ff04983288280dfe8c9a5579a958539f59df8cc9f + languageName: node + linkType: hard + +"@types/eslint-visitor-keys@npm:^1.0.0": + version: 1.0.0 + resolution: "@types/eslint-visitor-keys@npm:1.0.0" + checksum: 10/90cd39c84dab2e72d2911b141f56da021ffc781cae75512a3b15f2dcbd82e03bcec553d98bb147cb96ea02043cc3a1a05ebf20880f9ad15a995b8cf605390518 + languageName: node + linkType: hard + +"@types/eslint@npm:*": + version: 8.44.3 + resolution: "@types/eslint@npm:8.44.3" + dependencies: + "@types/estree": "npm:*" + "@types/json-schema": "npm:*" + checksum: 10/53796ff6009512775490403647577946ff924dbef5339898e361e1b29527492e5738cbd67d94202d6ebd6d45e7ac5c5da1c95fe710f16476e2dda3316d1970f6 + languageName: node + linkType: hard + +"@types/estree@npm:*, @types/estree@npm:^1.0.0": + version: 1.0.2 + resolution: "@types/estree@npm:1.0.2" + checksum: 10/01e5bf0f827b93f8d0156d38b98b7db2fa4db169d437389cd0286f913db97dd4b1cd16d01d4a4150f4e411680ddb6be4de9fa1a8c34ceb15b82c38b485ddc115 + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:^4.17.33": + version: 4.17.37 + resolution: "@types/express-serve-static-core@npm:4.17.37" + dependencies: + "@types/node": "npm:*" + "@types/qs": "npm:*" + "@types/range-parser": "npm:*" + "@types/send": "npm:*" + checksum: 10/bb88921d147dd38bfcc286271378384fbbdde1fdd452b092518a8dd425057f0a6368b615320f300d7011a02ec5d925ab55da1c1b3997710dec3869a67506a611 + languageName: node + linkType: hard + +"@types/express@npm:^4.17.14": + version: 4.17.18 + resolution: "@types/express@npm:4.17.18" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^4.17.33" + "@types/qs": "npm:*" + "@types/serve-static": "npm:*" + checksum: 10/b344988a35d3cae7b29e984f010ac9124de5e61fe2104c0fc541db6ba8fc4433c69d505537429d157400572c258b47afca4bd668b58de101ae879c868b81bcb1 + languageName: node + linkType: hard + +"@types/glob@npm:*": + version: 8.1.0 + resolution: "@types/glob@npm:8.1.0" + dependencies: + "@types/minimatch": "npm:^5.1.2" + "@types/node": "npm:*" + checksum: 10/9101f3a9061e40137190f70626aa0e202369b5ec4012c3fabe6f5d229cce04772db9a94fa5a0eb39655e2e4ad105c38afbb4af56a56c0996a8c7d4fc72350e3d + languageName: node + linkType: hard + +"@types/graceful-fs@npm:^4.1.3": + version: 4.1.7 + resolution: "@types/graceful-fs@npm:4.1.7" + dependencies: + "@types/node": "npm:*" + checksum: 10/8b97e208f85c9efd02a6003a582c77646dd87be0af13aec9419a720771560a8a87a979eaca73ae193d7c73127f34d0a958403a9b5d6246e450289fd8c79adf09 + languageName: node + linkType: hard + +"@types/http-cache-semantics@npm:*": + version: 4.0.2 + resolution: "@types/http-cache-semantics@npm:4.0.2" + checksum: 10/6cf83a583a559ecaa95bae6d122d854028c0b0e0e3ad70fb46c0bcb1f447235fcf2e9516993b45bbb41e4dd5b54719cb1614b2e0057278a86b689a75cb732561 + languageName: node + linkType: hard + +"@types/http-errors@npm:*": + version: 2.0.2 + resolution: "@types/http-errors@npm:2.0.2" + checksum: 10/d7f14045240ac4b563725130942b8e5c8080bfabc724c8ff3f166ea928ff7ae02c5194763bc8f6aaf21897e8a44049b0492493b9de3e058247e58fdfe0f86692 + languageName: node + linkType: hard + +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": + version: 2.0.4 + resolution: "@types/istanbul-lib-coverage@npm:2.0.4" + checksum: 10/a25d7589ee65c94d31464c16b72a9dc81dfa0bea9d3e105ae03882d616e2a0712a9c101a599ec482d297c3591e16336962878cb3eb1a0a62d5b76d277a890ce7 + languageName: node + linkType: hard + +"@types/istanbul-lib-report@npm:*": + version: 3.0.1 + resolution: "@types/istanbul-lib-report@npm:3.0.1" + dependencies: + "@types/istanbul-lib-coverage": "npm:*" + checksum: 10/d50e7271901b1366b2a9965fc425a8e4c2b749cc913f34d4257395fe390553852df33b5e9f54a0f8522eafded7d898cbf96bcba2f6a75c731476fd578c08041d + languageName: node + linkType: hard + +"@types/istanbul-reports@npm:^3.0.0": + version: 3.0.2 + resolution: "@types/istanbul-reports@npm:3.0.2" + dependencies: + "@types/istanbul-lib-report": "npm:*" + checksum: 10/f52028d6fe4d28f0085dd7ed66ccfa6af632579e9a4091b90928ffef93d4dbec0bacd49e9caf1b939d05df9eafc5ac1f5939413cdf8ac59fbe4b29602d4d0939 + languageName: node + linkType: hard + +"@types/jest@npm:^27.0.24": + version: 27.5.2 + resolution: "@types/jest@npm:27.5.2" + dependencies: + jest-matcher-utils: "npm:^27.0.0" + pretty-format: "npm:^27.0.0" + checksum: 10/8608696fbdea81bc9a600d1c5aeb290063357eaa55c0174e7db15087c4f483113b35f8b4c4ae364d2632cfed15a4dd674786254826b946c896de5612c8cb1a26 + languageName: node + linkType: hard + +"@types/jest@npm:^29.5.4": + version: 29.5.5 + resolution: "@types/jest@npm:29.5.5" + dependencies: + expect: "npm:^29.0.0" + pretty-format: "npm:^29.0.0" + checksum: 10/85bf86fd31ed9b76c26abc6bf771d09a9a8ff9362c81be353b8cf8ba102e09741b7f6951dca09aaa56d5fb410291e1eb5650b508da2fb3d36a0f035a91552a0d + languageName: node + linkType: hard + +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.3, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": + version: 7.0.13 + resolution: "@types/json-schema@npm:7.0.13" + checksum: 10/24000f93d34b3848053b8eb36bbbcfb6b465f691d61186ddac9596b6f1fb105ae84a8be63c0c0f3b6d8f7eb6f891f6cdf3c34910aefc756a1971164c4262de1a + languageName: node + linkType: hard + +"@types/json5@npm:^0.0.29": + version: 0.0.29 + resolution: "@types/json5@npm:0.0.29" + checksum: 10/4e5aed58cabb2bbf6f725da13421aa50a49abb6bc17bfab6c31b8774b073fa7b50d557c61f961a09a85f6056151190f8ac95f13f5b48136ba5841f7d4484ec56 + languageName: node + linkType: hard + +"@types/jsonwebtoken@npm:^9.0.0": + version: 9.0.3 + resolution: "@types/jsonwebtoken@npm:9.0.3" + dependencies: + "@types/node": "npm:*" + checksum: 10/62599dea2c16e3043135620780e88785e81f9cebe5e4fd155ab30030eaefba4b04b0ea5e49ab08feab6838021b2e9a289f7e733966ce288e2d70813631c228bb + languageName: node + linkType: hard + +"@types/keyv@npm:^3.1.4": + version: 3.1.4 + resolution: "@types/keyv@npm:3.1.4" + dependencies: + "@types/node": "npm:*" + checksum: 10/e009a2bfb50e90ca9b7c6e8f648f8464067271fd99116f881073fa6fa76dc8d0133181dd65e6614d5fb1220d671d67b0124aef7d97dc02d7e342ab143a47779d + languageName: node + linkType: hard + +"@types/linkify-it@npm:*": + version: 3.0.3 + resolution: "@types/linkify-it@npm:3.0.3" + checksum: 10/a734becc4e7476833b0e6951ec133c006a34809639c722d3e28b7cf88f5f6ccbb433f195788be5e56209b1e9e6e0778879291dd2db401acee3bb585c44dcc329 + languageName: node + linkType: hard + +"@types/lodash@npm:^4.14.123": + version: 4.14.199 + resolution: "@types/lodash@npm:4.14.199" + checksum: 10/340aabe9b023553d64e47f2af7f2010814c1178ce3a2b256e8dd54c444578d5e6e937d70c7117ee1fac5c0fc429b592ab9f6d69a966f0a1222ebcbbe6d516c4a + languageName: node + linkType: hard + +"@types/lodash@npm:^4.14.199": + version: 4.14.202 + resolution: "@types/lodash@npm:4.14.202" + checksum: 10/1bb9760a5b1dda120132c4b987330d67979c95dbc22612678682cd61b00302e190f4207228f3728580059cdab5582362262e3819aea59960c1017bd2b9fb26f6 + languageName: node + linkType: hard + +"@types/long@npm:^4.0.0": + version: 4.0.2 + resolution: "@types/long@npm:4.0.2" + checksum: 10/68afa05fb20949d88345876148a76f6ccff5433310e720db51ac5ca21cb8cc6714286dbe04713840ddbd25a8b56b7a23aa87d08472fabf06463a6f2ed4967707 + languageName: node + linkType: hard + +"@types/markdown-it@npm:^12.2.3": + version: 12.2.3 + resolution: "@types/markdown-it@npm:12.2.3" + dependencies: + "@types/linkify-it": "npm:*" + "@types/mdurl": "npm:*" + checksum: 10/8838017dd0a0a9bd596114b959d287135393a18e3ddc6a46e9770bdd35c824b88d8ba4b60540ee75ae6c79dc0ccc72ff5d7745083c27900c98925c9b5ae058e6 + languageName: node + linkType: hard + +"@types/mdurl@npm:*": + version: 1.0.3 + resolution: "@types/mdurl@npm:1.0.3" + checksum: 10/5bbed4f0eb9f60040fa26be77aa2158ca468b6423876cec0d2043e7f8298e83b8e5b95fb66056327b02d747c4d376aed16c11ff3fdc4cb3dca327a6931a71f18 + languageName: node + linkType: hard + +"@types/mime@npm:*": + version: 3.0.2 + resolution: "@types/mime@npm:3.0.2" + checksum: 10/09cf74f6377d1b27f4a24512cb689ad30af59880ac473ed6f7bc5285ecde88bbe8fe500789340ad57810da9d6fe1704f86e8bfe147b9ea76d58925204a60b906 + languageName: node + linkType: hard + +"@types/mime@npm:^1": + version: 1.3.3 + resolution: "@types/mime@npm:1.3.3" + checksum: 10/7e27dede6517c1d604821a8a5412d6b7131decc8397ad4bac9216fc90dea26c9571426623ebeea2a9b89dbfb89ad98f7370a3c62cd2be8896c6e897333b117c9 + languageName: node + linkType: hard + +"@types/minimatch@npm:^5.1.2": + version: 5.1.2 + resolution: "@types/minimatch@npm:5.1.2" + checksum: 10/94db5060d20df2b80d77b74dd384df3115f01889b5b6c40fa2dfa27cfc03a68fb0ff7c1f2a0366070263eb2e9d6bfd8c87111d4bc3ae93c3f291297c1bf56c85 + languageName: node + linkType: hard + +"@types/ms@npm:*": + version: 0.7.32 + resolution: "@types/ms@npm:0.7.32" + checksum: 10/610744605c5924aa2657c8a62d307052af4f0e38e2aa015f154ef03391fabb4fd903f9c9baacb41f6e5798b8697e898463c351e5faf638738603ed29137b5254 + languageName: node + linkType: hard + +"@types/mysql@npm:^2.15.21, @types/mysql@npm:^2.15.6": + version: 2.15.22 + resolution: "@types/mysql@npm:2.15.22" + dependencies: + "@types/node": "npm:*" + checksum: 10/6be0aac58fe5c0f20ebf149d2ab228c620f751569a24fda33df457e0520b3c2f071bda06973ad54815ef54b0e0fa2176e56aba96b65b5990054930f4e2b7bb4e + languageName: node + linkType: hard + +"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": + version: 20.8.2 + resolution: "@types/node@npm:20.8.2" + checksum: 10/61bd39870625d8afcbb4f21d6a0c3a9681f6d508dc6b06f2497e9ad3ec942092a120bcfdbc1757a8e4017308449bc2a9b9865b2b9840b158878a4e8cc0804a3c + languageName: node + linkType: hard + +"@types/node@npm:^17.0.45": + version: 17.0.45 + resolution: "@types/node@npm:17.0.45" + checksum: 10/b45fff7270b5e81be19ef91a66b764a8b21473a97a8d211218a52e3426b79ad48f371819ab9153370756b33ba284e5c875463de4d2cf48a472e9098d7f09e8a2 + languageName: node + linkType: hard + +"@types/node@npm:^18.0.4": + version: 18.18.3 + resolution: "@types/node@npm:18.18.3" + checksum: 10/b76d157967ee0dd72686983b4e49f1745646ee6c10f96bfabcf42a174746b976ca9bc921dd81f69e7f3b839d6ded6cc153b14efeca69af23511b2df796e9bac1 + languageName: node + linkType: hard + +"@types/parse-json@npm:^4.0.0": + version: 4.0.0 + resolution: "@types/parse-json@npm:4.0.0" + checksum: 10/4df9de98150d2978afc2161482a3a8e6617883effba3223324f079de97ba7eabd7d84b90ced11c3f82b0c08d4a8383f678c9f73e9c41258f769b3fa234a2bb4f + languageName: node + linkType: hard + +"@types/qs@npm:*": + version: 6.9.8 + resolution: "@types/qs@npm:6.9.8" + checksum: 10/c28e07d00d07970e5134c6eed184a0189b8a4649e28fdf36d9117fe671c067a44820890de6bdecef18217647a95e9c6aebdaaae69f5fe4b0bec9345db885f77e + languageName: node + linkType: hard + +"@types/range-parser@npm:*": + version: 1.2.5 + resolution: "@types/range-parser@npm:1.2.5" + checksum: 10/db9aaa04a02d019395a9a4346475669a2864a32a6477ad0fc457bd2ef39a167cabe742f55a8a3fa8bc90abac795b716c22b37348bc3e19313ebe6c9310815233 + languageName: node + linkType: hard + +"@types/redis@npm:^2.8.28": + version: 2.8.32 + resolution: "@types/redis@npm:2.8.32" + dependencies: + "@types/node": "npm:*" + checksum: 10/3e384297625ff410a51ae1e74531022f37e812ef5a9d17b593b4c964ec974c9af1d572f26932ae5ace032808134caece73d056edb57bff1d2538698e3f4d028e + languageName: node + linkType: hard + +"@types/responselike@npm:^1.0.0": + version: 1.0.1 + resolution: "@types/responselike@npm:1.0.1" + dependencies: + "@types/node": "npm:*" + checksum: 10/ae8c36c9354aaedfa462dab655aa17613529d545a418acc54ba0214145fc1d0454be2ae107031a1b2c24768f19f2af7e4096a85d1e604010becd0bec2355cb0e + languageName: node + linkType: hard + +"@types/retry@npm:0.12.2": + version: 0.12.2 + resolution: "@types/retry@npm:0.12.2" + checksum: 10/e5675035717b39ce4f42f339657cae9637cf0c0051cf54314a6a2c44d38d91f6544be9ddc0280587789b6afd056be5d99dbe3e9f4df68c286c36321579b1bf4a + languageName: node + linkType: hard + +"@types/rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "@types/rimraf@npm:3.0.2" + dependencies: + "@types/glob": "npm:*" + "@types/node": "npm:*" + checksum: 10/b47fa302f46434cba704d20465861ad250df79467d3d289f9d6490d3aeeb41e8cb32dd80bd1a8fd833d1e185ac719fbf9be12e05ad9ce9be094d8ee8f1405347 + languageName: node + linkType: hard + +"@types/semver@npm:^7.3.12, @types/semver@npm:^7.5.0": + version: 7.5.3 + resolution: "@types/semver@npm:7.5.3" + checksum: 10/452c2f37b16358805efcae2d9888a2cfe696b7fb9962451eb0fb46b0fa0bbd68924977cfd28afca91507eb6e3fc19909855a4f7fe4b1f1221d5aeed780e800ae + languageName: node + linkType: hard + +"@types/send@npm:*": + version: 0.17.2 + resolution: "@types/send@npm:0.17.2" + dependencies: + "@types/mime": "npm:^1" + "@types/node": "npm:*" + checksum: 10/2e7c21870da7684a9ac1db401f44d05533463cd374014bf5dcaf173e5a1074ee00d6085e7e0aba9cab345d95cb3aaaaf09a07b25e4a67344e1edc4d94eef2d17 + languageName: node + linkType: hard + +"@types/serve-static@npm:*": + version: 1.15.3 + resolution: "@types/serve-static@npm:1.15.3" + dependencies: + "@types/http-errors": "npm:*" + "@types/mime": "npm:*" + "@types/node": "npm:*" + checksum: 10/9b759cf03e0896e9434df6d2d528c3e0016a2cef54e66775fb67f8fa6f9e78eed693adc95b3d6a18aacf629d8d596ad71c1d3eee5ecf95402b766cb012061ca2 + languageName: node + linkType: hard + +"@types/stack-utils@npm:^2.0.0": + version: 2.0.1 + resolution: "@types/stack-utils@npm:2.0.1" + checksum: 10/205fdbe3326b7046d7eaf5e494d8084f2659086a266f3f9cf00bccc549c8e36e407f88168ad4383c8b07099957ad669f75f2532ed4bc70be2b037330f7bae019 + languageName: node + linkType: hard + +"@types/triple-beam@npm:^1.3.2": + version: 1.3.3 + resolution: "@types/triple-beam@npm:1.3.3" + checksum: 10/e2d54d27536a7a7cd1e4c6e9f3799a894aa5b2dc00b8dba656be7c038c3c1dedd6236551afa9c9c6ce32b0d691e1468bc124f899be0d832bc6ddea4e830107d6 + languageName: node + linkType: hard + +"@types/validator@npm:^13.7.17": + version: 13.11.2 + resolution: "@types/validator@npm:13.11.2" + checksum: 10/b16a76d573d7ddffbb0357e38d24a8b0268d64519a5c8cd8b9dac632f68251cfa4e6ada598fcacc704af672cf889fcc356f0ba6d33e99924c0b736e39c0224f9 + languageName: node + linkType: hard + +"@types/ws@npm:^8.5.5": + version: 8.5.6 + resolution: "@types/ws@npm:8.5.6" + dependencies: + "@types/node": "npm:*" + checksum: 10/1c3ce7fe65569dc85314981622bdbccd8bab671cff74367f9bfba8390ddd0f241f29cdcf67b14163a2c832a0a08bfc0e8f60912b9c143648e25a5a05e016b892 + languageName: node + linkType: hard + +"@types/yargs-parser@npm:*": + version: 21.0.1 + resolution: "@types/yargs-parser@npm:21.0.1" + checksum: 10/b9e1a5758af6adbefcc04677d6387e48e5f35977fa83d8487aea9c1fe562876e3f266f60c5853e9ae55f91559528354494693c24993495ae74a18e9cee98edaa + languageName: node + linkType: hard + +"@types/yargs@npm:^17.0.8": + version: 17.0.26 + resolution: "@types/yargs@npm:17.0.26" + dependencies: + "@types/yargs-parser": "npm:*" + checksum: 10/e5e9b654cc4f68e74b6fba93bccc35a7240b80169d1297999120dc33a3c6e3c7628d8b2f756a34074acd4d268e0b7f111ff70ba176f5baf1b1b57990a0cacf32 + languageName: node + linkType: hard + +"@typescript-eslint/eslint-plugin@npm:^6.7.3, @typescript-eslint/eslint-plugin@npm:^6.7.4": + version: 6.7.4 + resolution: "@typescript-eslint/eslint-plugin@npm:6.7.4" + dependencies: + "@eslint-community/regexpp": "npm:^4.5.1" + "@typescript-eslint/scope-manager": "npm:6.7.4" + "@typescript-eslint/type-utils": "npm:6.7.4" + "@typescript-eslint/utils": "npm:6.7.4" + "@typescript-eslint/visitor-keys": "npm:6.7.4" + debug: "npm:^4.3.4" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.2.4" + natural-compare: "npm:^1.4.0" + semver: "npm:^7.5.4" + ts-api-utils: "npm:^1.0.1" + peerDependencies: + "@typescript-eslint/parser": ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/d888cef041d0d4f804c6f37831afd5cfc93579894313de34bda1197a688f007580c6f48aa791243af59fec476d9f605e9f21e87452ade7b50df9b5d53a0160a3 + languageName: node + linkType: hard + +"@typescript-eslint/experimental-utils@npm:3.10.1": + version: 3.10.1 + resolution: "@typescript-eslint/experimental-utils@npm:3.10.1" + dependencies: + "@types/json-schema": "npm:^7.0.3" + "@typescript-eslint/types": "npm:3.10.1" + "@typescript-eslint/typescript-estree": "npm:3.10.1" + eslint-scope: "npm:^5.0.0" + eslint-utils: "npm:^2.0.0" + peerDependencies: + eslint: "*" + checksum: 10/3055eb8588d2ced3972341f5746ca5d1c27b56e5f1cf93c2e32dfb5ecfaaa7616e68050c7d16be2293e7b688be398c00cbe0b78ee0e40bb17aec47f8614d121a + languageName: node + linkType: hard + +"@typescript-eslint/experimental-utils@npm:^2.5.0": + version: 2.34.0 + resolution: "@typescript-eslint/experimental-utils@npm:2.34.0" + dependencies: + "@types/json-schema": "npm:^7.0.3" + "@typescript-eslint/typescript-estree": "npm:2.34.0" + eslint-scope: "npm:^5.0.0" + eslint-utils: "npm:^2.0.0" + peerDependencies: + eslint: "*" + checksum: 10/7f2b116bfac51ae75223f433c5d14e6f7813b0ccabc1ef826a3b8daad14d0dc51c2d5a68fb476110fe9b7e0ee81c087a5299f5445e75e45e0bad89a55a3af391 + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:^3.3.0": + version: 3.10.1 + resolution: "@typescript-eslint/parser@npm:3.10.1" + dependencies: + "@types/eslint-visitor-keys": "npm:^1.0.0" + "@typescript-eslint/experimental-utils": "npm:3.10.1" + "@typescript-eslint/types": "npm:3.10.1" + "@typescript-eslint/typescript-estree": "npm:3.10.1" + eslint-visitor-keys: "npm:^1.1.0" + peerDependencies: + eslint: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/3488d0a7c06c38893e6fc13d8eaaa408598302b1506f7cca9a4c9ebf1fdb1f30a3dbe8c9f0a8b81a7d5423a45681ddb4eee8a9686b3f811d371fddcc7da136f9 + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:^6.7.3": + version: 6.7.4 + resolution: "@typescript-eslint/parser@npm:6.7.4" + dependencies: + "@typescript-eslint/scope-manager": "npm:6.7.4" + "@typescript-eslint/types": "npm:6.7.4" + "@typescript-eslint/typescript-estree": "npm:6.7.4" + "@typescript-eslint/visitor-keys": "npm:6.7.4" + debug: "npm:^4.3.4" + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/78284615694b4bcb54dec5079bab7e36a04f58cf7cbd005a5eaa05a1544769e41d7a47c2be651312b2f764f5612b3c11ed63799f3bb5b628d2b7720252f3572c + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/scope-manager@npm:5.62.0" + dependencies: + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/visitor-keys": "npm:5.62.0" + checksum: 10/e827770baa202223bc0387e2fd24f630690809e460435b7dc9af336c77322290a770d62bd5284260fa881c86074d6a9fd6c97b07382520b115f6786b8ed499da + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:6.7.4": + version: 6.7.4 + resolution: "@typescript-eslint/scope-manager@npm:6.7.4" + dependencies: + "@typescript-eslint/types": "npm:6.7.4" + "@typescript-eslint/visitor-keys": "npm:6.7.4" + checksum: 10/eabf3f0d18389c9c799c9f9648c9fcd1b098468979459d86267f51403ab2bb005d16b6d1278c6d54794956b4c699d41e7d8bb84e73db2b448f73797701907e9d + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:6.7.4": + version: 6.7.4 + resolution: "@typescript-eslint/type-utils@npm:6.7.4" + dependencies: + "@typescript-eslint/typescript-estree": "npm:6.7.4" + "@typescript-eslint/utils": "npm:6.7.4" + debug: "npm:^4.3.4" + ts-api-utils: "npm:^1.0.1" + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/88778c47d912c4846645ed817e33a7c05433353954b670f6d257748361f5cf88ec6dd089e109298ec9c58a8a2cb96509ca9187b7d46bf810c1b0d61e3f475746 + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:3.10.1": + version: 3.10.1 + resolution: "@typescript-eslint/types@npm:3.10.1" + checksum: 10/c5c696a0c3b9dbc32b0b9f7ec8d06ab4ccb3422c4147032f001726997faef587d42d7d5c06cc3e4db52c05210b51e8052061630f56258f01523382488e9282ed + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/types@npm:5.62.0" + checksum: 10/24e8443177be84823242d6729d56af2c4b47bfc664dd411a1d730506abf2150d6c31bdefbbc6d97c8f91043e3a50e0c698239dcb145b79bb6b0c34469aaf6c45 + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:6.7.4": + version: 6.7.4 + resolution: "@typescript-eslint/types@npm:6.7.4" + checksum: 10/14aa41aefee32efe8ad469d301c2acc522e411663b912d143c327e1161242e568b8d446a72faec491b86ae44517af3ecb988823aed7b1b1bc6693ff950be4809 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:2.34.0": + version: 2.34.0 + resolution: "@typescript-eslint/typescript-estree@npm:2.34.0" + dependencies: + debug: "npm:^4.1.1" + eslint-visitor-keys: "npm:^1.1.0" + glob: "npm:^7.1.6" + is-glob: "npm:^4.0.1" + lodash: "npm:^4.17.15" + semver: "npm:^7.3.2" + tsutils: "npm:^3.17.1" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/6016d1a21a344db48e128ed5e4cf0b2c7ea07bd833240b9d1048b2c24595151ced9ec83c91d0e8bac6483d746d5cea6a414382664ef73b86705eb781fd3cd3d8 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:3.10.1": + version: 3.10.1 + resolution: "@typescript-eslint/typescript-estree@npm:3.10.1" + dependencies: + "@typescript-eslint/types": "npm:3.10.1" + "@typescript-eslint/visitor-keys": "npm:3.10.1" + debug: "npm:^4.1.1" + glob: "npm:^7.1.6" + is-glob: "npm:^4.0.1" + lodash: "npm:^4.17.15" + semver: "npm:^7.3.2" + tsutils: "npm:^3.17.1" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/ed4eedd04d1bcc651fe03925570b6199e76dd27878cde74dd3f06cf4a5b8911244746475ac9b2496d8cb9e20c70027f9f91ef688604167105a40164e4d408258 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" + dependencies: + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/visitor-keys": "npm:5.62.0" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + semver: "npm:^7.3.7" + tsutils: "npm:^3.21.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/06c975eb5f44b43bd19fadc2e1023c50cf87038fe4c0dd989d4331c67b3ff509b17fa60a3251896668ab4d7322bdc56162a9926971218d2e1a1874d2bef9a52e + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:6.7.4": + version: 6.7.4 + resolution: "@typescript-eslint/typescript-estree@npm:6.7.4" + dependencies: + "@typescript-eslint/types": "npm:6.7.4" + "@typescript-eslint/visitor-keys": "npm:6.7.4" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + semver: "npm:^7.5.4" + ts-api-utils: "npm:^1.0.1" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/3336fc8bcd141c124ab50e26a707c1ca928fa6bcb93cef4754167acbeec7022f0660e7772fc4ffa79cff139711275422449ffc9fd03c6472cf8f77e92405f82c + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:6.7.4": + version: 6.7.4 + resolution: "@typescript-eslint/utils@npm:6.7.4" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@types/json-schema": "npm:^7.0.12" + "@types/semver": "npm:^7.5.0" + "@typescript-eslint/scope-manager": "npm:6.7.4" + "@typescript-eslint/types": "npm:6.7.4" + "@typescript-eslint/typescript-estree": "npm:6.7.4" + semver: "npm:^7.5.4" + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + checksum: 10/a2b9b4307385599c1b8d073e319a49a79d9b9315d642d56380385524f333dbe98325128ff6f113566ff10f3447be98b79450567060c37b013fae0b5a83c6b9cc + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:^5.10.0": + version: 5.62.0 + resolution: "@typescript-eslint/utils@npm:5.62.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@types/json-schema": "npm:^7.0.9" + "@types/semver": "npm:^7.3.12" + "@typescript-eslint/scope-manager": "npm:5.62.0" + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/typescript-estree": "npm:5.62.0" + eslint-scope: "npm:^5.1.1" + semver: "npm:^7.3.7" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10/15ef13e43998a082b15f85db979f8d3ceb1f9ce4467b8016c267b1738d5e7cdb12aa90faf4b4e6dd6486c236cf9d33c463200465cf25ff997dbc0f12358550a1 + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:3.10.1": + version: 3.10.1 + resolution: "@typescript-eslint/visitor-keys@npm:3.10.1" + dependencies: + eslint-visitor-keys: "npm:^1.1.0" + checksum: 10/13e8059db306e626de3c55c6d3094af57c189b3f6d08ffda14eb13f6b6b9711b20625beeda6d234aed6c9d8aa9092c465aab62759dc09a03aa10dee28214a3fd + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" + dependencies: + "@typescript-eslint/types": "npm:5.62.0" + eslint-visitor-keys: "npm:^3.3.0" + checksum: 10/dc613ab7569df9bbe0b2ca677635eb91839dfb2ca2c6fa47870a5da4f160db0b436f7ec0764362e756d4164e9445d49d5eb1ff0b87f4c058946ae9d8c92eb388 + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:6.7.4": + version: 6.7.4 + resolution: "@typescript-eslint/visitor-keys@npm:6.7.4" + dependencies: + "@typescript-eslint/types": "npm:6.7.4" + eslint-visitor-keys: "npm:^3.4.1" + checksum: 10/b9e086c04689ea2180f7cacf63fcea7e6a25bb699ae7ac78b9ed4b23633711d93e08341275e38cda8eba09697d5dcb36da493dfe0e8d4cd6aadc629dc6d4b113 + languageName: node + linkType: hard + +"@webassemblyjs/ast@npm:1.11.6, @webassemblyjs/ast@npm:^1.11.5": + version: 1.11.6 + resolution: "@webassemblyjs/ast@npm:1.11.6" + dependencies: + "@webassemblyjs/helper-numbers": "npm:1.11.6" + "@webassemblyjs/helper-wasm-bytecode": "npm:1.11.6" + checksum: 10/4c1303971ccd5188731c9b01073d9738333f37b946a48c4e049f7b788706cdc66f473cd6f3e791423a94c52a3b2230d070007930d29bccbce238b23835839f3c + languageName: node + linkType: hard + +"@webassemblyjs/floating-point-hex-parser@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/floating-point-hex-parser@npm:1.11.6" + checksum: 10/29b08758841fd8b299c7152eda36b9eb4921e9c584eb4594437b5cd90ed6b920523606eae7316175f89c20628da14326801090167cc7fbffc77af448ac84b7e2 + languageName: node + linkType: hard + +"@webassemblyjs/helper-api-error@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-api-error@npm:1.11.6" + checksum: 10/e8563df85161096343008f9161adb138a6e8f3c2cc338d6a36011aa55eabb32f2fd138ffe63bc278d009ada001cc41d263dadd1c0be01be6c2ed99076103689f + languageName: node + linkType: hard + +"@webassemblyjs/helper-buffer@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-buffer@npm:1.11.6" + checksum: 10/b14d0573bf680d22b2522e8a341ec451fddd645d1f9c6bd9012ccb7e587a2973b86ab7b89fe91e1c79939ba96095f503af04369a3b356c8023c13a5893221644 + languageName: node + linkType: hard + +"@webassemblyjs/helper-numbers@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-numbers@npm:1.11.6" + dependencies: + "@webassemblyjs/floating-point-hex-parser": "npm:1.11.6" + "@webassemblyjs/helper-api-error": "npm:1.11.6" + "@xtuc/long": "npm:4.2.2" + checksum: 10/9ffd258ad809402688a490fdef1fd02222f20cdfe191c895ac215a331343292164e5033dbc0347f0f76f2447865c0b5c2d2e3304ee948d44f7aa27857028fd08 + languageName: node + linkType: hard + +"@webassemblyjs/helper-wasm-bytecode@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-wasm-bytecode@npm:1.11.6" + checksum: 10/4ebf03e9c1941288c10e94e0f813f413f972bfaa1f09be2cc2e5577f300430906b61aa24d52f5ef2f894e8e24e61c6f7c39871d7e3d98bc69460e1b8e00bb20b + languageName: node + linkType: hard + +"@webassemblyjs/helper-wasm-section@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-wasm-section@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": "npm:1.11.6" + "@webassemblyjs/helper-buffer": "npm:1.11.6" + "@webassemblyjs/helper-wasm-bytecode": "npm:1.11.6" + "@webassemblyjs/wasm-gen": "npm:1.11.6" + checksum: 10/38a615ab3d55f953daaf78b69f145e2cc1ff5288ab71715d1a164408b735c643a87acd7e7ba3e9633c5dd965439a45bb580266b05a06b22ff678d6c013514108 + languageName: node + linkType: hard + +"@webassemblyjs/ieee754@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/ieee754@npm:1.11.6" + dependencies: + "@xtuc/ieee754": "npm:^1.2.0" + checksum: 10/13574b8e41f6ca39b700e292d7edf102577db5650fe8add7066a320aa4b7a7c09a5056feccac7a74eb68c10dea9546d4461412af351f13f6b24b5f32379b49de + languageName: node + linkType: hard + +"@webassemblyjs/leb128@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/leb128@npm:1.11.6" + dependencies: + "@xtuc/long": "npm:4.2.2" + checksum: 10/ec3b72db0e7ce7908fe08ec24395bfc97db486063824c0edc580f0973a4cfbadf30529569d9c7db663a56513e45b94299cca03be9e1992ea3308bb0744164f3d + languageName: node + linkType: hard + +"@webassemblyjs/utf8@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/utf8@npm:1.11.6" + checksum: 10/361a537bd604101b320a5604c3c96d1038d83166f1b9fb86cedadc7e81bae54c3785ae5d90bf5b1842f7da08194ccaf0f44a64fcca0cbbd6afe1a166196986d6 + languageName: node + linkType: hard + +"@webassemblyjs/wasm-edit@npm:^1.11.5": + version: 1.11.6 + resolution: "@webassemblyjs/wasm-edit@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": "npm:1.11.6" + "@webassemblyjs/helper-buffer": "npm:1.11.6" + "@webassemblyjs/helper-wasm-bytecode": "npm:1.11.6" + "@webassemblyjs/helper-wasm-section": "npm:1.11.6" + "@webassemblyjs/wasm-gen": "npm:1.11.6" + "@webassemblyjs/wasm-opt": "npm:1.11.6" + "@webassemblyjs/wasm-parser": "npm:1.11.6" + "@webassemblyjs/wast-printer": "npm:1.11.6" + checksum: 10/c168bfc6d0cdd371345f36f95a4766d098a96ccc1257e6a6e3a74d987a5c4f2ddd2244a6aecfa5d032a47d74ed2c3b579e00a314d31e4a0b76ad35b31cdfa162 + languageName: node + linkType: hard + +"@webassemblyjs/wasm-gen@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/wasm-gen@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": "npm:1.11.6" + "@webassemblyjs/helper-wasm-bytecode": "npm:1.11.6" + "@webassemblyjs/ieee754": "npm:1.11.6" + "@webassemblyjs/leb128": "npm:1.11.6" + "@webassemblyjs/utf8": "npm:1.11.6" + checksum: 10/f91903506ce50763592863df5d80ffee80f71a1994a882a64cdb83b5e44002c715f1ef1727d8ccb0692d066af34d3d4f5e59e8f7a4e2eeb2b7c32692ac44e363 + languageName: node + linkType: hard + +"@webassemblyjs/wasm-opt@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/wasm-opt@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": "npm:1.11.6" + "@webassemblyjs/helper-buffer": "npm:1.11.6" + "@webassemblyjs/wasm-gen": "npm:1.11.6" + "@webassemblyjs/wasm-parser": "npm:1.11.6" + checksum: 10/e0cfeea381ecbbd0ca1616e9a08974acfe7fc81f8a16f9f2d39f565dc51784dd7043710b6e972f9968692d273e32486b9a8a82ca178d4bd520b2d5e2cf28234d + languageName: node + linkType: hard + +"@webassemblyjs/wasm-parser@npm:1.11.6, @webassemblyjs/wasm-parser@npm:^1.11.5": + version: 1.11.6 + resolution: "@webassemblyjs/wasm-parser@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": "npm:1.11.6" + "@webassemblyjs/helper-api-error": "npm:1.11.6" + "@webassemblyjs/helper-wasm-bytecode": "npm:1.11.6" + "@webassemblyjs/ieee754": "npm:1.11.6" + "@webassemblyjs/leb128": "npm:1.11.6" + "@webassemblyjs/utf8": "npm:1.11.6" + checksum: 10/6995e0b7b8ebc52b381459c6a555f87763dcd3975c4a112407682551e1c73308db7af23385972a253dceb5af94e76f9c97cb861e8239b5ed1c3e79b95d8e2097 + languageName: node + linkType: hard + +"@webassemblyjs/wast-printer@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/wast-printer@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": "npm:1.11.6" + "@xtuc/long": "npm:4.2.2" + checksum: 10/fd45fd0d693141d678cc2f6ff2d3a0d7a8884acb1c92fb0c63cf43b7978e9560be04118b12792638a39dd185640453510229e736f3049037d0c361f6435f2d5f + languageName: node + linkType: hard + +"@xtuc/ieee754@npm:^1.2.0": + version: 1.2.0 + resolution: "@xtuc/ieee754@npm:1.2.0" + checksum: 10/ab033b032927d77e2f9fa67accdf31b1ca7440974c21c9cfabc8349e10ca2817646171c4f23be98d0e31896d6c2c3462a074fe37752e523abc3e45c79254259c + languageName: node + linkType: hard + +"@xtuc/long@npm:4.2.2": + version: 4.2.2 + resolution: "@xtuc/long@npm:4.2.2" + checksum: 10/7217bae9fe240e0d804969e7b2af11cb04ec608837c78b56ca88831991b287e232a0b7fce8d548beaff42aaf0197ffa471d81be6ac4c4e53b0148025a2c076ec + languageName: node + linkType: hard + +"abbrev@npm:1, abbrev@npm:^1.0.0": + version: 1.1.1 + resolution: "abbrev@npm:1.1.1" + checksum: 10/2d882941183c66aa665118bafdab82b7a177e9add5eb2776c33e960a4f3c89cff88a1b38aba13a456de01d0dd9d66a8bea7c903268b21ea91dd1097e1e2e8243 + languageName: node + linkType: hard + +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: "npm:^5.0.0" + checksum: 10/ed84af329f1828327798229578b4fe03a4dd2596ba304083ebd2252666bdc1d7647d66d0b18704477e1f8aa315f055944aa6e859afebd341f12d0a53c37b4b40 + languageName: node + linkType: hard + +"acorn-import-assertions@npm:^1.9.0": + version: 1.9.0 + resolution: "acorn-import-assertions@npm:1.9.0" + peerDependencies: + acorn: ^8 + checksum: 10/af8dd58f6b0c6a43e85849744534b99f2133835c6fcdabda9eea27d0a0da625a0d323c4793ba7cb25cf4507609d0f747c210ccc2fc9b5866de04b0e59c9c5617 + languageName: node + linkType: hard + +"acorn-jsx@npm:^5.3.2": + version: 5.3.2 + resolution: "acorn-jsx@npm:5.3.2" + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10/d4371eaef7995530b5b5ca4183ff6f062ca17901a6d3f673c9ac011b01ede37e7a1f7f61f8f5cfe709e88054757bb8f3277dc4061087cdf4f2a1f90ccbcdb977 + languageName: node + linkType: hard + +"acorn-walk@npm:^8.1.1": + version: 8.2.0 + resolution: "acorn-walk@npm:8.2.0" + checksum: 10/e69f7234f2adfeb16db3671429a7c80894105bd7534cb2032acf01bb26e6a847952d11a062d071420b43f8d82e33d2e57f26fe87d9cce0853e8143d8910ff1de + languageName: node + linkType: hard + +"acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": + version: 8.10.0 + resolution: "acorn@npm:8.10.0" + bin: + acorn: bin/acorn + checksum: 10/522310c20fdc3c271caed3caf0f06c51d61cb42267279566edd1d58e83dbc12eebdafaab666a0f0be1b7ad04af9c6bc2a6f478690a9e6391c3c8b165ada917dd + languageName: node + linkType: hard + +"adm-zip@npm:^0.5.5": + version: 0.5.10 + resolution: "adm-zip@npm:0.5.10" + checksum: 10/c5ab79b77114d8277f0cbfd6cca830198d6c7ee4971f6960f48e08cd2375953b11dc71729b7f396abd51d2d6cce8c862fad185ea90cb2c84ab5161c37ed1b099 + languageName: node + linkType: hard + +"agent-base@npm:6, agent-base@npm:^6.0.2": + version: 6.0.2 + resolution: "agent-base@npm:6.0.2" + dependencies: + debug: "npm:4" + checksum: 10/21fb903e0917e5cb16591b4d0ef6a028a54b83ac30cd1fca58dece3d4e0990512a8723f9f83130d88a41e2af8b1f7be1386fda3ea2d181bb1a62155e75e95e23 + languageName: node + linkType: hard + +"agentkeepalive@npm:^4.1.3, agentkeepalive@npm:^4.2.1": + version: 4.5.0 + resolution: "agentkeepalive@npm:4.5.0" + dependencies: + humanize-ms: "npm:^1.2.1" + checksum: 10/dd210ba2a2e2482028f027b1156789744aadbfd773a6c9dd8e4e8001930d5af82382abe19a69240307b1d8003222ce6b0542935038313434b900e351914fc15f + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: "npm:^2.0.0" + indent-string: "npm:^4.0.0" + checksum: 10/1101a33f21baa27a2fa8e04b698271e64616b886795fd43c31068c07533c7b3facfcaf4e9e0cab3624bd88f729a592f1c901a1a229c9e490eafce411a8644b79 + languageName: node + linkType: hard + +"ajv-formats@npm:^2.1.1": + version: 2.1.1 + resolution: "ajv-formats@npm:2.1.1" + dependencies: + ajv: "npm:^8.0.0" + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 10/70c263ded219bf277ffd9127f793b625f10a46113b2e901e150da41931fcfd7f5592da6d66862f4449bb157ffe65867c3294a7df1d661cc232c4163d5a1718ed + languageName: node + linkType: hard + +"ajv-keywords@npm:^3.5.2": + version: 3.5.2 + resolution: "ajv-keywords@npm:3.5.2" + peerDependencies: + ajv: ^6.9.1 + checksum: 10/d57c9d5bf8849bddcbd801b79bc3d2ddc736c2adb6b93a6a365429589dd7993ddbd5d37c6025ed6a7f89c27506b80131d5345c5b1fa6a97e40cd10a96bcd228c + languageName: node + linkType: hard + +"ajv@npm:^6.12.4, ajv@npm:^6.12.5": + version: 6.12.6 + resolution: "ajv@npm:6.12.6" + dependencies: + fast-deep-equal: "npm:^3.1.1" + fast-json-stable-stringify: "npm:^2.0.0" + json-schema-traverse: "npm:^0.4.1" + uri-js: "npm:^4.2.2" + checksum: 10/48d6ad21138d12eb4d16d878d630079a2bda25a04e745c07846a4ad768319533031e28872a9b3c5790fa1ec41aabdf2abed30a56e5a03ebc2cf92184b8ee306c + languageName: node + linkType: hard + +"ajv@npm:^8.0.0, ajv@npm:^8.12.0": + version: 8.12.0 + resolution: "ajv@npm:8.12.0" + dependencies: + fast-deep-equal: "npm:^3.1.1" + json-schema-traverse: "npm:^1.0.0" + require-from-string: "npm:^2.0.2" + uri-js: "npm:^4.2.2" + checksum: 10/b406f3b79b5756ac53bfe2c20852471b08e122bc1ee4cde08ae4d6a800574d9cd78d60c81c69c63ff81e4da7cd0b638fafbb2303ae580d49cf1600b9059efb85 + languageName: node + linkType: hard + +"ansi-align@npm:^3.0.1": + version: 3.0.1 + resolution: "ansi-align@npm:3.0.1" + dependencies: + string-width: "npm:^4.1.0" + checksum: 10/4c7e8b6a10eaf18874ecee964b5db62ac86d0b9266ad4987b3a1efcb5d11a9e12c881ee40d14951833135a8966f10a3efe43f9c78286a6e632f53d85ad28b9c0 + languageName: node + linkType: hard + +"ansi-escapes@npm:^4.2.1": + version: 4.3.2 + resolution: "ansi-escapes@npm:4.3.2" + dependencies: + type-fest: "npm:^0.21.3" + checksum: 10/8661034456193ffeda0c15c8c564a9636b0c04094b7f78bd01517929c17c504090a60f7a75f949f5af91289c264d3e1001d91492c1bd58efc8e100500ce04de2 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 10/2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: 10/1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 + languageName: node + linkType: hard + +"ansi-styles@npm:^3.2.1": + version: 3.2.1 + resolution: "ansi-styles@npm:3.2.1" + dependencies: + color-convert: "npm:^1.9.0" + checksum: 10/d85ade01c10e5dd77b6c89f34ed7531da5830d2cb5882c645f330079975b716438cd7ebb81d0d6e6b4f9c577f19ae41ab55f07f19786b02f9dfd9e0377395665 + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 10/b4494dfbfc7e4591b4711a396bd27e540f8153914123dccb4cdbbcb514015ada63a3809f362b9d8d4f6b17a706f1d7bea3c6f974b15fa5ae76b5b502070889ff + languageName: node + linkType: hard + +"ansi-styles@npm:^5.0.0": + version: 5.2.0 + resolution: "ansi-styles@npm:5.2.0" + checksum: 10/d7f4e97ce0623aea6bc0d90dcd28881ee04cba06c570b97fd3391bd7a268eedfd9d5e2dd4fdcbdd82b8105df5faf6f24aaedc08eaf3da898e702db5948f63469 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: 10/70fdf883b704d17a5dfc9cde206e698c16bcd74e7f196ab821511651aee4f9f76c9514bdfa6ca3a27b5e49138b89cb222a28caf3afe4567570139577f991df32 + languageName: node + linkType: hard + +"anymatch@npm:^3.0.3, anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10/3e044fd6d1d26545f235a9fe4d7a534e2029d8e59fa7fd9f2a6eb21230f6b5380ea1eaf55136e60cbf8e613544b3b766e7a6fa2102e2a3a117505466e3025dc2 + languageName: node + linkType: hard + +"aproba@npm:^1.0.3 || ^2.0.0": + version: 2.0.0 + resolution: "aproba@npm:2.0.0" + checksum: 10/c2b9a631298e8d6f3797547e866db642f68493808f5b37cd61da778d5f6ada890d16f668285f7d60bd4fc3b03889bd590ffe62cf81b700e9bb353431238a0a7b + languageName: node + linkType: hard + +"archive-type@npm:^4.0.0": + version: 4.0.0 + resolution: "archive-type@npm:4.0.0" + dependencies: + file-type: "npm:^4.2.0" + checksum: 10/271f0d118294dd0305831f0700b635e8a9475f97693212d548eee48017f917e14349a25ad578f8e13486ba4b7cde1972d53e613d980e8738cfccea5fc626c76f + languageName: node + linkType: hard + +"archiver-utils@npm:^2.1.0": + version: 2.1.0 + resolution: "archiver-utils@npm:2.1.0" + dependencies: + glob: "npm:^7.1.4" + graceful-fs: "npm:^4.2.0" + lazystream: "npm:^1.0.0" + lodash.defaults: "npm:^4.2.0" + lodash.difference: "npm:^4.5.0" + lodash.flatten: "npm:^4.4.0" + lodash.isplainobject: "npm:^4.0.6" + lodash.union: "npm:^4.6.0" + normalize-path: "npm:^3.0.0" + readable-stream: "npm:^2.0.0" + checksum: 10/4df493c0e6a3a544119b08b350308923500e2c6efee6a283cba4c3202293ce3acb70897e54e24f735e3a38ff43e5a65f66e2e5225fdfc955bf2335491377be2e + languageName: node + linkType: hard + +"archiver-utils@npm:^3.0.4": + version: 3.0.4 + resolution: "archiver-utils@npm:3.0.4" + dependencies: + glob: "npm:^7.2.3" + graceful-fs: "npm:^4.2.0" + lazystream: "npm:^1.0.0" + lodash.defaults: "npm:^4.2.0" + lodash.difference: "npm:^4.5.0" + lodash.flatten: "npm:^4.4.0" + lodash.isplainobject: "npm:^4.0.6" + lodash.union: "npm:^4.6.0" + normalize-path: "npm:^3.0.0" + readable-stream: "npm:^3.6.0" + checksum: 10/a838c325a1e1d6798c07e6a3af08f480fdce57cba2964bff8761126715aa1b71e9a119442eac19b7ec6313f5298e54a180dc6612ae548825fbc9be6836e50487 + languageName: node + linkType: hard + +"archiver@npm:^5.3.0, archiver@npm:^5.3.1": + version: 5.3.2 + resolution: "archiver@npm:5.3.2" + dependencies: + archiver-utils: "npm:^2.1.0" + async: "npm:^3.2.4" + buffer-crc32: "npm:^0.2.1" + readable-stream: "npm:^3.6.0" + readdir-glob: "npm:^1.1.2" + tar-stream: "npm:^2.2.0" + zip-stream: "npm:^4.1.0" + checksum: 10/9384b3b20d330f95140c2b7a9b51140d14e9bc7b133be6cf573067ed8fc67a6e9618cfbfe60b1ba78b8034857001fd02c8900f2fba4864514670a2274d36dc9e + languageName: node + linkType: hard + +"are-we-there-yet@npm:^2.0.0": + version: 2.0.0 + resolution: "are-we-there-yet@npm:2.0.0" + dependencies: + delegates: "npm:^1.0.0" + readable-stream: "npm:^3.6.0" + checksum: 10/ea6f47d14fc33ae9cbea3e686eeca021d9d7b9db83a306010dd04ad5f2c8b7675291b127d3fcbfcbd8fec26e47b3324ad5b469a6cc3733a582f2fe4e12fc6756 + languageName: node + linkType: hard + +"are-we-there-yet@npm:^3.0.0": + version: 3.0.1 + resolution: "are-we-there-yet@npm:3.0.1" + dependencies: + delegates: "npm:^1.0.0" + readable-stream: "npm:^3.6.0" + checksum: 10/390731720e1bf9ed5d0efc635ea7df8cbc4c90308b0645a932f06e8495a0bf1ecc7987d3b97e805f62a17d6c4b634074b25200aa4d149be2a7b17250b9744bc4 + languageName: node + linkType: hard + +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3" + checksum: 10/969b491082f20cad166649fa4d2073ea9e974a4e5ac36247ca23d2e5a8b3cb12d60e9ff70a8acfe26d76566c71fd351ee5e6a9a6595157eb36f92b1fd64e1599 + languageName: node + linkType: hard + +"argparse@npm:^1.0.7": + version: 1.0.10 + resolution: "argparse@npm:1.0.10" + dependencies: + sprintf-js: "npm:~1.0.2" + checksum: 10/c6a621343a553ff3779390bb5ee9c2263d6643ebcd7843227bdde6cc7adbed796eb5540ca98db19e3fd7b4714e1faa51551f8849b268bb62df27ddb15cbcd91e + languageName: node + linkType: hard + +"argparse@npm:^2.0.1": + version: 2.0.1 + resolution: "argparse@npm:2.0.1" + checksum: 10/18640244e641a417ec75a9bd38b0b2b6b95af5199aa241b131d4b2fb206f334d7ecc600bd194861610a5579084978bfcbb02baa399dbe442d56d0ae5e60dbaef + languageName: node + linkType: hard + +"array-buffer-byte-length@npm:^1.0.0": + version: 1.0.0 + resolution: "array-buffer-byte-length@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.2" + is-array-buffer: "npm:^3.0.1" + checksum: 10/044e101ce150f4804ad19c51d6c4d4cfa505c5b2577bd179256e4aa3f3f6a0a5e9874c78cd428ee566ac574c8a04d7ce21af9fe52e844abfdccb82b33035a7c3 + languageName: node + linkType: hard + +"array-includes@npm:^3.1.6": + version: 3.1.7 + resolution: "array-includes@npm:3.1.7" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + get-intrinsic: "npm:^1.2.1" + is-string: "npm:^1.0.7" + checksum: 10/856a8be5d118967665936ad33ff3b07adfc50b06753e596e91fb80c3da9b8c022e92e3cc6781156d6ad95db7109b9f603682c7df2d6a529ed01f7f6b39a4a360 + languageName: node + linkType: hard + +"array-unflat-js@npm:^0.1.3": + version: 0.1.3 + resolution: "array-unflat-js@npm:0.1.3" + checksum: 10/9384e65fb007fccfbedb95decfa962ff1f0672ce9255a82f0877478848d79f42c456f5e43f2bfbdf246f93c33786398fcbd12242458cac3c83e5366474530afc + languageName: node + linkType: hard + +"array-union@npm:^2.1.0": + version: 2.1.0 + resolution: "array-union@npm:2.1.0" + checksum: 10/5bee12395cba82da674931df6d0fea23c4aa4660cb3b338ced9f828782a65caa232573e6bf3968f23e0c5eb301764a382cef2f128b170a9dc59de0e36c39f98d + languageName: node + linkType: hard + +"array.prototype.findlastindex@npm:^1.2.2": + version: 1.2.3 + resolution: "array.prototype.findlastindex@npm:1.2.3" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + es-shim-unscopables: "npm:^1.0.0" + get-intrinsic: "npm:^1.2.1" + checksum: 10/063cbab8eeac3aa01f3e980eecb9a8c5d87723032b49f7f814ecc6d75c33c03c17e3f43a458127a62e16303cab412f95d6ad9dc7e0ae6d9dc27a9bb76c24df7a + languageName: node + linkType: hard + +"array.prototype.flat@npm:^1.3.1": + version: 1.3.2 + resolution: "array.prototype.flat@npm:1.3.2" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + es-shim-unscopables: "npm:^1.0.0" + checksum: 10/d9d2f6f27584de92ec7995bc931103e6de722cd2498bdbfc4cba814fc3e52f056050a93be883018811f7c0a35875f5056584a0e940603a5e5934f0279896aebe + languageName: node + linkType: hard + +"array.prototype.flatmap@npm:^1.3.1": + version: 1.3.2 + resolution: "array.prototype.flatmap@npm:1.3.2" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + es-shim-unscopables: "npm:^1.0.0" + checksum: 10/33f20006686e0cbe844fde7fd290971e8366c6c5e3380681c2df15738b1df766dd02c7784034aeeb3b037f65c496ee54de665388288edb323a2008bb550f77ea + languageName: node + linkType: hard + +"arraybuffer.prototype.slice@npm:^1.0.2": + version: 1.0.2 + resolution: "arraybuffer.prototype.slice@npm:1.0.2" + dependencies: + array-buffer-byte-length: "npm:^1.0.0" + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + get-intrinsic: "npm:^1.2.1" + is-array-buffer: "npm:^3.0.2" + is-shared-array-buffer: "npm:^1.0.2" + checksum: 10/c200faf437786f5b2c80d4564ff5481c886a16dee642ef02abdc7306c7edd523d1f01d1dd12b769c7eb42ac9bc53874510db19a92a2c035c0f6696172aafa5d3 + languageName: node + linkType: hard + +"arrify@npm:^2.0.0": + version: 2.0.1 + resolution: "arrify@npm:2.0.1" + checksum: 10/067c4c1afd182806a82e4c1cb8acee16ab8b5284fbca1ce29408e6e91281c36bb5b612f6ddfbd40a0f7a7e0c75bf2696eb94c027f6e328d6e9c52465c98e4209 + languageName: node + linkType: hard + +"asap@npm:^2.0.0": + version: 2.0.6 + resolution: "asap@npm:2.0.6" + checksum: 10/b244c0458c571945e4b3be0b14eb001bea5596f9868cc50cc711dc03d58a7e953517d3f0dad81ccde3ff37d1f074701fa76a6f07d41aaa992d7204a37b915dda + languageName: node + linkType: hard + +"assert@npm:^2.1.0": + version: 2.1.0 + resolution: "assert@npm:2.1.0" + dependencies: + call-bind: "npm:^1.0.2" + is-nan: "npm:^1.3.2" + object-is: "npm:^1.1.5" + object.assign: "npm:^4.1.4" + util: "npm:^0.12.5" + checksum: 10/6b9d813c8eef1c0ac13feac5553972e4bd180ae16000d4eb5c0ded2489188737c75a5aacefc97a985008b37502f62fe1bad34da1a7481a54bbfabec3964c8aa7 + languageName: node + linkType: hard + +"async-retry@npm:^1.3.3": + version: 1.3.3 + resolution: "async-retry@npm:1.3.3" + dependencies: + retry: "npm:0.13.1" + checksum: 10/38a7152ff7265a9321ea214b9c69e8224ab1febbdec98efbbde6e562f17ff68405569b796b1c5271f354aef8783665d29953f051f68c1fc45306e61aec82fdc4 + languageName: node + linkType: hard + +"async@npm:^3.2.3, async@npm:^3.2.4": + version: 3.2.4 + resolution: "async@npm:3.2.4" + checksum: 10/bebb5dc2258c45b83fa1d3be179ae0eb468e1646a62d443c8d60a45e84041b28fccebe1e2d1f234bfc3dcad44e73dcdbf4ba63d98327c9f6556e3dbd47c2ae8b + languageName: node + linkType: hard + +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 10/3ce727cbc78f69d6a4722517a58ee926c8c21083633b1d3fdf66fd688f6c127a53a592141bd4866f9b63240a86e9d8e974b13919450bd17fa33c2d22c4558ad8 + languageName: node + linkType: hard + +"at-least-node@npm:^1.0.0": + version: 1.0.0 + resolution: "at-least-node@npm:1.0.0" + checksum: 10/463e2f8e43384f1afb54bc68485c436d7622acec08b6fad269b421cb1d29cebb5af751426793d0961ed243146fe4dc983402f6d5a51b720b277818dbf6f2e49e + languageName: node + linkType: hard + +"available-typed-arrays@npm:^1.0.5": + version: 1.0.5 + resolution: "available-typed-arrays@npm:1.0.5" + checksum: 10/4d4d5e86ea0425696f40717882f66a570647b94ac8d273ddc7549a9b61e5da099e149bf431530ccbd776bd74e02039eb8b5edf426e3e2211ee61af16698a9064 + languageName: node + linkType: hard + +"aws-lambda@npm:^1.0.7": + version: 1.0.7 + resolution: "aws-lambda@npm:1.0.7" + dependencies: + aws-sdk: "npm:^2.814.0" + commander: "npm:^3.0.2" + js-yaml: "npm:^3.14.1" + watchpack: "npm:^2.0.0-beta.10" + bin: + lambda: bin/lambda + checksum: 10/71fd8d4e57efb93dab5252e38cdeb2a90ccbe95346c5d7b63bbee7a3123cebe3873a4a930a7cd39afcbfaabd70ebb00e66fb0d5aca55cd9363f850d024c0980b + languageName: node + linkType: hard + +"aws-sdk@npm:^2.1404.0, aws-sdk@npm:^2.1454.0, aws-sdk@npm:^2.814.0": + version: 2.1469.0 + resolution: "aws-sdk@npm:2.1469.0" + dependencies: + buffer: "npm:4.9.2" + events: "npm:1.1.1" + ieee754: "npm:1.1.13" + jmespath: "npm:0.16.0" + querystring: "npm:0.2.0" + sax: "npm:1.2.1" + url: "npm:0.10.3" + util: "npm:^0.12.4" + uuid: "npm:8.0.0" + xml2js: "npm:0.5.0" + checksum: 10/f7bda3b226cd493145783ce51a72a553b4ce4d9411e2d4572c87dc38d3d78f518c68ba3e26526150d773add234ba101934a04305704432bd4886b73846554749 + languageName: node + linkType: hard + +"axios@npm:^0.18.0": + version: 0.18.1 + resolution: "axios@npm:0.18.1" + dependencies: + follow-redirects: "npm:1.5.10" + is-buffer: "npm:^2.0.2" + checksum: 10/e89e662c4998a2617bd7c34b9444a5d0b2029e3df952e5aa8756c5882e3f1d5a7143d10b5d6435d9276a71a199a3494672d7f1cc72cc8e598224feaf99793ef7 + languageName: node + linkType: hard + +"axios@npm:^0.21.1": + version: 0.21.4 + resolution: "axios@npm:0.21.4" + dependencies: + follow-redirects: "npm:^1.14.0" + checksum: 10/da644592cb6f8f9f8c64fdabd7e1396d6769d7a4c1ea5f8ae8beb5c2eb90a823e3a574352b0b934ac62edc762c0f52647753dc54f7d07279127a7e5c4cd20272 + languageName: node + linkType: hard + +"axios@npm:^1.6.2": + version: 1.6.2 + resolution: "axios@npm:1.6.2" + dependencies: + follow-redirects: "npm:^1.15.0" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 10/612bc93f8f738a518e7c5f9de9cc782bcd36aac6bae279160ef6a10260378e21c1786520eab3336898e3d66e0839ebdf739f327fb6d0431baa4d3235703a7652 + languageName: node + linkType: hard + +"babel-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "babel-jest@npm:29.7.0" + dependencies: + "@jest/transform": "npm:^29.7.0" + "@types/babel__core": "npm:^7.1.14" + babel-plugin-istanbul: "npm:^6.1.1" + babel-preset-jest: "npm:^29.6.3" + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + slash: "npm:^3.0.0" + peerDependencies: + "@babel/core": ^7.8.0 + checksum: 10/8a0953bd813b3a8926008f7351611055548869e9a53dd36d6e7e96679001f71e65fd7dbfe253265c3ba6a4e630dc7c845cf3e78b17d758ef1880313ce8fba258 + languageName: node + linkType: hard + +"babel-plugin-istanbul@npm:^6.1.1": + version: 6.1.1 + resolution: "babel-plugin-istanbul@npm:6.1.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.0.0" + "@istanbuljs/load-nyc-config": "npm:^1.0.0" + "@istanbuljs/schema": "npm:^0.1.2" + istanbul-lib-instrument: "npm:^5.0.4" + test-exclude: "npm:^6.0.0" + checksum: 10/ffd436bb2a77bbe1942a33245d770506ab2262d9c1b3c1f1da7f0592f78ee7445a95bc2efafe619dd9c1b6ee52c10033d6c7d29ddefe6f5383568e60f31dfe8d + languageName: node + linkType: hard + +"babel-plugin-jest-hoist@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-plugin-jest-hoist@npm:29.6.3" + dependencies: + "@babel/template": "npm:^7.3.3" + "@babel/types": "npm:^7.3.3" + "@types/babel__core": "npm:^7.1.14" + "@types/babel__traverse": "npm:^7.0.6" + checksum: 10/9bfa86ec4170bd805ab8ca5001ae50d8afcb30554d236ba4a7ffc156c1a92452e220e4acbd98daefc12bf0216fccd092d0a2efed49e7e384ec59e0597a926d65 + languageName: node + linkType: hard + +"babel-preset-current-node-syntax@npm:^1.0.0": + version: 1.0.1 + resolution: "babel-preset-current-node-syntax@npm:1.0.1" + dependencies: + "@babel/plugin-syntax-async-generators": "npm:^7.8.4" + "@babel/plugin-syntax-bigint": "npm:^7.8.3" + "@babel/plugin-syntax-class-properties": "npm:^7.8.3" + "@babel/plugin-syntax-import-meta": "npm:^7.8.3" + "@babel/plugin-syntax-json-strings": "npm:^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" + "@babel/plugin-syntax-numeric-separator": "npm:^7.8.3" + "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" + "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" + "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" + "@babel/plugin-syntax-top-level-await": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/94561959cb12bfa80867c9eeeace7c3d48d61707d33e55b4c3fdbe82fc745913eb2dbfafca62aef297421b38aadcb58550e5943f50fbcebbeefd70ce2bed4b74 + languageName: node + linkType: hard + +"babel-preset-jest@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-preset-jest@npm:29.6.3" + dependencies: + babel-plugin-jest-hoist: "npm:^29.6.3" + babel-preset-current-node-syntax: "npm:^1.0.0" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/aa4ff2a8a728d9d698ed521e3461a109a1e66202b13d3494e41eea30729a5e7cc03b3a2d56c594423a135429c37bf63a9fa8b0b9ce275298be3095a88c69f6fb + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 10/9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 + languageName: node + linkType: hard + +"base-x@npm:^3.0.2": + version: 3.0.9 + resolution: "base-x@npm:3.0.9" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10/957101d6fd09e1903e846fd8f69fd7e5e3e50254383e61ab667c725866bec54e5ece5ba49ce385128ae48f9ec93a26567d1d5ebb91f4d56ef4a9cc0d5a5481e8 + languageName: node + linkType: hard + +"base-x@npm:^4.0.0": + version: 4.0.0 + resolution: "base-x@npm:4.0.0" + checksum: 10/b25db9e07eb1998472a20557c7f00c797dc0595f79df95155ab74274e7fa98b9f2659b3ee547ac8773666b7f69540656793aeb97ad2b1ceccdb6fa5faaf69ac0 + languageName: node + linkType: hard + +"base64-js@npm:^1.0.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 10/669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 + languageName: node + linkType: hard + +"bech32@npm:=1.1.3": + version: 1.1.3 + resolution: "bech32@npm:1.1.3" + checksum: 10/054d582709996a18518db518c1fdb95a8cd29fceb76f747212776e0b3711f101fe1a4354d2f39ddc681eb37640b3212476596665e41d7c7a8f1068e9d5148220 + languageName: node + linkType: hard + +"bech32@npm:=2.0.0, bech32@npm:^2.0.0": + version: 2.0.0 + resolution: "bech32@npm:2.0.0" + checksum: 10/fa15acb270b59aa496734a01f9155677b478987b773bf701f465858bf1606c6a970085babd43d71ce61895f1baa594cb41a2cd1394bd2c6698f03cc2d811300e + languageName: node + linkType: hard + +"bech32@npm:^1.1.3": + version: 1.1.4 + resolution: "bech32@npm:1.1.4" + checksum: 10/63ff37c0ce43be914c685ce89700bba1589c319af0dac1ea04f51b33d0e5ecfd40d14c24f527350b94f0a4e236385373bb9122ec276410f354ddcdbf29ca13f4 + languageName: node + linkType: hard + +"bigi@npm:^1.1.0, bigi@npm:^1.4.2": + version: 1.4.2 + resolution: "bigi@npm:1.4.2" + checksum: 10/e8165beb2ad113add286f81a066653295737a3edc6287effae7b72c2d7695be19d36774069e80a977df7187857c9e2c4a9517f6ec966bb887e097ea8d36cc2e0 + languageName: node + linkType: hard + +"bignumber.js@npm:9.0.0": + version: 9.0.0 + resolution: "bignumber.js@npm:9.0.0" + checksum: 10/7406d0d11dfdd2183e19be745f0d5913e3773ded5fbca2a310221e719f15fd8ec6b8d7991031a6081a6276a8e12e27d58ead60f73dcbb9d697ebe9e2dd0ad7e0 + languageName: node + linkType: hard + +"bignumber.js@npm:^9.0.0": + version: 9.1.2 + resolution: "bignumber.js@npm:9.1.2" + checksum: 10/d89b8800a987225d2c00dcbf8a69dc08e92aa0880157c851c287b307d31ceb2fc2acb0c62c3e3a3d42b6c5fcae9b004035f13eb4386e56d529d7edac18d5c9d8 + languageName: node + linkType: hard + +"binary-extensions@npm:^2.0.0": + version: 2.2.0 + resolution: "binary-extensions@npm:2.2.0" + checksum: 10/ccd267956c58d2315f5d3ea6757cf09863c5fc703e50fbeb13a7dc849b812ef76e3cf9ca8f35a0c48498776a7478d7b4a0418e1e2b8cb9cb9731f2922aaad7f8 + languageName: node + linkType: hard + +"bindings@npm:^1.5.0": + version: 1.5.0 + resolution: "bindings@npm:1.5.0" + dependencies: + file-uri-to-path: "npm:1.0.0" + checksum: 10/593d5ae975ffba15fbbb4788fe5abd1e125afbab849ab967ab43691d27d6483751805d98cb92f7ac24a2439a8a8678cd0131c535d5d63de84e383b0ce2786133 + languageName: node + linkType: hard + +"bintrees@npm:1.0.2": + version: 1.0.2 + resolution: "bintrees@npm:1.0.2" + checksum: 10/071896cea5ea5413316c8436e95799444c208630d5c539edd8a7089fc272fc5d3634aa4a2e4847b28350dda1796162e14a34a0eda53108cc5b3c2ff6a036c1fa + languageName: node + linkType: hard + +"bip-schnorr@npm:=0.6.4": + version: 0.6.4 + resolution: "bip-schnorr@npm:0.6.4" + dependencies: + bigi: "npm:^1.4.2" + ecurve: "npm:^1.0.6" + js-sha256: "npm:^0.9.0" + randombytes: "npm:^2.1.0" + safe-buffer: "npm:^5.2.1" + checksum: 10/913859c57264e51505b57706616949fae23a9c0546f5d414dfd72b62b87fb8b9fcfe7b2006ae3dd52f9a9f78cfb1dc30b0c2d5414dcced266fc074aaea01c146 + languageName: node + linkType: hard + +"bip174@npm:^2.1.1": + version: 2.1.1 + resolution: "bip174@npm:2.1.1" + checksum: 10/b90da3f9fe5b076a3d7b125a9dd39345a8cd8ece2dcc328a19c92948a60d7242886235bc94712935c71a9c5bc445b98a3570dca8881d19d421fc88a30b150b46 + languageName: node + linkType: hard + +"bip32@npm:^3.0.1": + version: 3.1.0 + resolution: "bip32@npm:3.1.0" + dependencies: + bs58check: "npm:^2.1.1" + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + ripemd160: "npm:^2.0.2" + typeforce: "npm:^1.11.5" + wif: "npm:^2.0.6" + checksum: 10/6cdad901d25959e21835fe7b43066fc2498548962aa07ce438860b7420a7a93ceec3a9b34ddee68bbc568a68b9d96029338dbea1eadb548651b06d800a14fc3d + languageName: node + linkType: hard + +"bip66@npm:^1.1.5": + version: 1.1.5 + resolution: "bip66@npm:1.1.5" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10/6257e90ff2149aa08740ff4009730c1bceb1a3456571d3006a36b39f30044f2973e05f043ea6977046d6ab66e4a8d6f5c9785094f8317f4ff546a325baece1ab + languageName: node + linkType: hard + +"bitcoinjs-lib@npm:^6.0.1": + version: 6.1.5 + resolution: "bitcoinjs-lib@npm:6.1.5" + dependencies: + "@noble/hashes": "npm:^1.2.0" + bech32: "npm:^2.0.0" + bip174: "npm:^2.1.1" + bs58check: "npm:^3.0.1" + typeforce: "npm:^1.11.3" + varuint-bitcoin: "npm:^1.1.2" + checksum: 10/9a4dc588cb989173e8631180078c141c7ca0456865490c2c1f052e86e87b92954556a10f516371a9d96bd294433c245961dba2ff8b1d70327c22a555dd201767 + languageName: node + linkType: hard + +"bitcoinjs-message@npm:^2.2.0": + version: 2.2.0 + resolution: "bitcoinjs-message@npm:2.2.0" + dependencies: + bech32: "npm:^1.1.3" + bs58check: "npm:^2.1.2" + buffer-equals: "npm:^1.0.3" + create-hash: "npm:^1.1.2" + secp256k1: "npm:^3.0.1" + varuint-bitcoin: "npm:^1.0.1" + checksum: 10/715ee436857f74455750700210060a775047bffb894276387faf07aafaac76a8446b750352c456126069042d1ce1324e0d45b1e3d9683fb810c4f1866402a6d5 + languageName: node + linkType: hard + +"bitcore-lib@npm:8.25.10": + version: 8.25.10 + resolution: "bitcore-lib@npm:8.25.10" + dependencies: + bech32: "npm:=1.1.3" + bn.js: "npm:=4.11.8" + bs58: "npm:^4.0.1" + buffer-compare: "npm:=1.1.1" + elliptic: "npm:^6.5.3" + inherits: "npm:=2.0.1" + lodash: "npm:^4.17.20" + checksum: 10/6b51cb528a1f9b48dfa41b2bfde760b97b48213339edb5485d7e787e8f49ebc46b1d9867079cf3a78aa5af66c1adba28d5505112c15ae1aabcd020ee1b862652 + languageName: node + linkType: hard + +"bitcore-lib@npm:^8.25.10, bitcore-lib@npm:^8.25.47": + version: 8.25.47 + resolution: "bitcore-lib@npm:8.25.47" + dependencies: + bech32: "npm:=2.0.0" + bip-schnorr: "npm:=0.6.4" + bn.js: "npm:=4.11.8" + bs58: "npm:^4.0.1" + buffer-compare: "npm:=1.1.1" + elliptic: "npm:^6.5.3" + inherits: "npm:=2.0.1" + lodash: "npm:^4.17.20" + checksum: 10/7682832aaa6aa77c159dc8302e7c74be1391f4198b8cb09d3be6415b2f184a06c5c91a2058dc479a2684e95544ec932e68578ed8b7e06be8a136db30181b4998 + languageName: node + linkType: hard + +"bitcore-mnemonic@npm:8.25.10": + version: 8.25.10 + resolution: "bitcore-mnemonic@npm:8.25.10" + dependencies: + bitcore-lib: "npm:^8.25.10" + unorm: "npm:^1.4.1" + peerDependencies: + bitcore-lib: ^8.20.1 + checksum: 10/813a1694f035764f7c4782d07eabe5913771a40abebc83cf1c58647ded3c2cf22ea5c80986723b5589fc689bea0dcb335e932993d5df59ce00b523204c5475fb + languageName: node + linkType: hard + +"bitcore-mnemonic@npm:^8.25.10": + version: 8.25.47 + resolution: "bitcore-mnemonic@npm:8.25.47" + dependencies: + bitcore-lib: "npm:^8.25.47" + unorm: "npm:^1.4.1" + peerDependencies: + bitcore-lib: ^8.20.1 + checksum: 10/fca2d1471b5b4ff504111331d2a350aa2c2439a06ae56242deb9c6fdf4dcab7f1b6017b855c1c48bd889d5e48aae8fabb0dea1c6401880abbaa3d23bb0ced2d7 + languageName: node + linkType: hard + +"bl@npm:^1.0.0": + version: 1.2.3 + resolution: "bl@npm:1.2.3" + dependencies: + readable-stream: "npm:^2.3.5" + safe-buffer: "npm:^5.1.1" + checksum: 10/11d775b09ebd7d8c0df1ed7efd03cc8a2b1283c804a55153c81a0b586728a085fa24240647cac9a60163eb6f36a28cf8c45b80bf460a46336d4c84c40205faff + languageName: node + linkType: hard + +"bl@npm:^4.0.3, bl@npm:^4.1.0": + version: 4.1.0 + resolution: "bl@npm:4.1.0" + dependencies: + buffer: "npm:^5.5.0" + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.4.0" + checksum: 10/b7904e66ed0bdfc813c06ea6c3e35eafecb104369dbf5356d0f416af90c1546de3b74e5b63506f0629acf5e16a6f87c3798f16233dcff086e9129383aa02ab55 + languageName: node + linkType: hard + +"bluebird@npm:^3.7.2": + version: 3.7.2 + resolution: "bluebird@npm:3.7.2" + checksum: 10/007c7bad22c5d799c8dd49c85b47d012a1fe3045be57447721e6afbd1d5be43237af1db62e26cb9b0d9ba812d2e4ca3bac82f6d7e016b6b88de06ee25ceb96e7 + languageName: node + linkType: hard + +"bn.js@npm:=4.11.8": + version: 4.11.8 + resolution: "bn.js@npm:4.11.8" + checksum: 10/b498be9c0914dfe8e9892c1bb924a920ef8b5d4eeec3c9b670c92f55d6f20ba6342df0f6dc127da8a9917676f86edf1963c6048daa567aae53fde0356e1fabbf + languageName: node + linkType: hard + +"bn.js@npm:^4.11.8, bn.js@npm:^4.11.9": + version: 4.12.0 + resolution: "bn.js@npm:4.12.0" + checksum: 10/10f8db196d3da5adfc3207d35d0a42aa29033eb33685f20ba2c36cadfe2de63dad05df0a20ab5aae01b418d1c4b3d4d205273085262fa020d17e93ff32b67527 + languageName: node + linkType: hard + +"bowser@npm:^2.11.0": + version: 2.11.0 + resolution: "bowser@npm:2.11.0" + checksum: 10/ef46500eafe35072455e7c3ae771244e97827e0626686a9a3601c436d16eb272dad7ccbd49e2130b599b617ca9daa67027de827ffc4c220e02f63c84b69a8751 + languageName: node + linkType: hard + +"boxen@npm:^7.1.1": + version: 7.1.1 + resolution: "boxen@npm:7.1.1" + dependencies: + ansi-align: "npm:^3.0.1" + camelcase: "npm:^7.0.1" + chalk: "npm:^5.2.0" + cli-boxes: "npm:^3.0.0" + string-width: "npm:^5.1.2" + type-fest: "npm:^2.13.0" + widest-line: "npm:^4.0.1" + wrap-ansi: "npm:^8.1.0" + checksum: 10/a21d514435ccdd51f11088ad42e6298e3ff6be1bc2801699dcc1d3d79a2c5b005b5384dd03742e91a1ce2d9aedf99996efb36ed5fc7c5c392e19de2404bcfa37 + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.11 + resolution: "brace-expansion@npm:1.1.11" + dependencies: + balanced-match: "npm:^1.0.0" + concat-map: "npm:0.0.1" + checksum: 10/faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07 + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: 10/a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 + languageName: node + linkType: hard + +"braces@npm:^3.0.2, braces@npm:~3.0.2": + version: 3.0.2 + resolution: "braces@npm:3.0.2" + dependencies: + fill-range: "npm:^7.0.1" + checksum: 10/966b1fb48d193b9d155f810e5efd1790962f2c4e0829f8440b8ad236ba009222c501f70185ef732fef17a4c490bb33a03b90dab0631feafbdf447da91e8165b1 + languageName: node + linkType: hard + +"brorand@npm:^1.1.0": + version: 1.1.0 + resolution: "brorand@npm:1.1.0" + checksum: 10/8a05c9f3c4b46572dec6ef71012b1946db6cae8c7bb60ccd4b7dd5a84655db49fe043ecc6272e7ef1f69dc53d6730b9e2a3a03a8310509a3d797a618cbee52be + languageName: node + linkType: hard + +"browserify-aes@npm:^1.0.6": + version: 1.2.0 + resolution: "browserify-aes@npm:1.2.0" + dependencies: + buffer-xor: "npm:^1.0.3" + cipher-base: "npm:^1.0.0" + create-hash: "npm:^1.1.0" + evp_bytestokey: "npm:^1.0.3" + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + checksum: 10/2813058f74e083a00450b11ea9d5d1f072de7bf0133f5d122d4ff7b849bece56d52b9c51ad0db0fad21c0bc4e8272fd5196114bbe7b94a9b7feb0f9fbb33a3bf + languageName: node + linkType: hard + +"browserslist@npm:^4.14.5, browserslist@npm:^4.21.9": + version: 4.22.1 + resolution: "browserslist@npm:4.22.1" + dependencies: + caniuse-lite: "npm:^1.0.30001541" + electron-to-chromium: "npm:^1.4.535" + node-releases: "npm:^2.0.13" + update-browserslist-db: "npm:^1.0.13" + bin: + browserslist: cli.js + checksum: 10/4a515168e0589c7b1ccbf13a93116ce0418cc5e65d228ec036022cf0e08773fdfb732e2abbf1e1188b96d19ecd4dd707504e75b6d393cba2782fc7d6a7fdefe8 + languageName: node + linkType: hard + +"bs-logger@npm:0.x": + version: 0.2.6 + resolution: "bs-logger@npm:0.2.6" + dependencies: + fast-json-stable-stringify: "npm:2.x" + checksum: 10/e6d3ff82698bb3f20ce64fb85355c5716a3cf267f3977abe93bf9c32a2e46186b253f48a028ae5b96ab42bacd2c826766d9ae8cf6892f9b944656be9113cf212 + languageName: node + linkType: hard + +"bs58@npm:^4.0.0, bs58@npm:^4.0.1": + version: 4.0.1 + resolution: "bs58@npm:4.0.1" + dependencies: + base-x: "npm:^3.0.2" + checksum: 10/b3c5365bb9e0c561e1a82f1a2d809a1a692059fae016be233a6127ad2f50a6b986467c3a50669ce4c18929dcccb297c5909314dd347a25a68c21b68eb3e95ac2 + languageName: node + linkType: hard + +"bs58@npm:^5.0.0": + version: 5.0.0 + resolution: "bs58@npm:5.0.0" + dependencies: + base-x: "npm:^4.0.0" + checksum: 10/2475cb0684e07077521aac718e604a13e0f891d58cff923d437a2f7e9e28703ab39fce9f84c7c703ab369815a675f11e3bd394d38643bfe8969fbe42e6833d45 + languageName: node + linkType: hard + +"bs58check@npm:<3.0.0, bs58check@npm:^2.1.1, bs58check@npm:^2.1.2": + version: 2.1.2 + resolution: "bs58check@npm:2.1.2" + dependencies: + bs58: "npm:^4.0.0" + create-hash: "npm:^1.1.0" + safe-buffer: "npm:^5.1.2" + checksum: 10/43bdf08a5dd04581b78f040bc4169480e17008da482ffe2a6507327bbc4fc5c28de0501f7faf22901cfe57fbca79cbb202ca529003fedb4cb8dccd265b38e54d + languageName: node + linkType: hard + +"bs58check@npm:^3.0.1": + version: 3.0.1 + resolution: "bs58check@npm:3.0.1" + dependencies: + "@noble/hashes": "npm:^1.2.0" + bs58: "npm:^5.0.0" + checksum: 10/dbbecc7a09f3836e821149266c864c4bbd545539cea43c35f23f4c3c46b54c86c52b65d224b9ea2e916fa6d93bd2ce9fac5b6c6bfcf19621a9c209a5602f71c8 + languageName: node + linkType: hard + +"bser@npm:2.1.1": + version: 2.1.1 + resolution: "bser@npm:2.1.1" + dependencies: + node-int64: "npm:^0.4.0" + checksum: 10/edba1b65bae682450be4117b695997972bd9a3c4dfee029cab5bcb72ae5393a79a8f909b8bc77957eb0deec1c7168670f18f4d5c556f46cdd3bca5f3b3a8d020 + languageName: node + linkType: hard + +"buffer-alloc-unsafe@npm:^1.1.0": + version: 1.1.0 + resolution: "buffer-alloc-unsafe@npm:1.1.0" + checksum: 10/c5e18bf51f67754ec843c9af3d4c005051aac5008a3992938dda1344e5cfec77c4b02b4ca303644d1e9a6e281765155ce6356d85c6f5ccc5cd21afc868def396 + languageName: node + linkType: hard + +"buffer-alloc@npm:^1.2.0": + version: 1.2.0 + resolution: "buffer-alloc@npm:1.2.0" + dependencies: + buffer-alloc-unsafe: "npm:^1.1.0" + buffer-fill: "npm:^1.0.0" + checksum: 10/560cd27f3cbe73c614867da373407d4506309c62fe18de45a1ce191f3785ec6ca2488d802ff82065798542422980ca25f903db078c57822218182c37c3576df5 + languageName: node + linkType: hard + +"buffer-compare@npm:=1.1.1": + version: 1.1.1 + resolution: "buffer-compare@npm:1.1.1" + checksum: 10/dc5adc7406a25059ae9d2088d455bfaffeabad2c1540ad7d6d432ec257efa06ff8efc3e87d887ed46a042ac448fb1dba37e559e602753b63e097fdeac732369e + languageName: node + linkType: hard + +"buffer-crc32@npm:^0.2.1, buffer-crc32@npm:^0.2.13, buffer-crc32@npm:~0.2.3": + version: 0.2.13 + resolution: "buffer-crc32@npm:0.2.13" + checksum: 10/06252347ae6daca3453b94e4b2f1d3754a3b146a111d81c68924c22d91889a40623264e95e67955b1cb4a68cbedf317abeabb5140a9766ed248973096db5ce1c + languageName: node + linkType: hard + +"buffer-equal-constant-time@npm:1.0.1": + version: 1.0.1 + resolution: "buffer-equal-constant-time@npm:1.0.1" + checksum: 10/80bb945f5d782a56f374b292770901065bad21420e34936ecbe949e57724b4a13874f735850dd1cc61f078773c4fb5493a41391e7bda40d1fa388d6bd80daaab + languageName: node + linkType: hard + +"buffer-equals@npm:^1.0.3": + version: 1.0.4 + resolution: "buffer-equals@npm:1.0.4" + checksum: 10/392a2f82acdaad46392aec7ce54a8ff0b2a650b5802ccb0c77072050bbc7fd4e101f38460c7e88cdc7e130421882977f595d5c1a3d3343611562ecf7c684a70f + languageName: node + linkType: hard + +"buffer-fill@npm:^1.0.0": + version: 1.0.0 + resolution: "buffer-fill@npm:1.0.0" + checksum: 10/c29b4723ddeab01e74b5d3b982a0c6828f2ded49cef049ddca3dac661c874ecdbcecb5dd8380cf0f4adbeb8cff90a7de724126750a1f1e5ebd4eb6c59a1315b1 + languageName: node + linkType: hard + +"buffer-from@npm:^1.0.0": + version: 1.1.2 + resolution: "buffer-from@npm:1.1.2" + checksum: 10/0448524a562b37d4d7ed9efd91685a5b77a50672c556ea254ac9a6d30e3403a517d8981f10e565db24e8339413b43c97ca2951f10e399c6125a0d8911f5679bb + languageName: node + linkType: hard + +"buffer-xor@npm:^1.0.3": + version: 1.0.3 + resolution: "buffer-xor@npm:1.0.3" + checksum: 10/4a63d48b5117c7eda896d81cd3582d9707329b07c97a14b0ece2edc6e64220ea7ea17c94b295e8c2cb7b9f8291e2b079f9096be8ac14be238420a43e06ec66e2 + languageName: node + linkType: hard + +"buffer@npm:4.9.2": + version: 4.9.2 + resolution: "buffer@npm:4.9.2" + dependencies: + base64-js: "npm:^1.0.2" + ieee754: "npm:^1.1.4" + isarray: "npm:^1.0.0" + checksum: 10/4852a455e167bc8ca580c3c585176bbe0931c9929aeb68f3e0b49adadcb4e513fd0922a43efdf67ddb2e8785bbe8254ae17f4b69038dd06329ee9e3283c8508f + languageName: node + linkType: hard + +"buffer@npm:^5.2.1, buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.1.13" + checksum: 10/997434d3c6e3b39e0be479a80288875f71cd1c07d75a3855e6f08ef848a3c966023f79534e22e415ff3a5112708ce06127277ab20e527146d55c84566405c7c6 + languageName: node + linkType: hard + +"bufferutil@npm:^4.0.1": + version: 4.0.7 + resolution: "bufferutil@npm:4.0.7" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10/01e2144e88a6cb1cd8e4e0bb1ec622c6e400646fb451a672d20e7d40cdc7d4a82a64dbcda6f5f92b36eeca0d1e5290baf7af707994f7b7c87e911d51a265bf07 + languageName: node + linkType: hard + +"builtin-modules@npm:^3.3.0": + version: 3.3.0 + resolution: "builtin-modules@npm:3.3.0" + checksum: 10/62e063ab40c0c1efccbfa9ffa31873e4f9d57408cb396a2649981a0ecbce56aabc93c28feaccbc5658c95aab2703ad1d11980e62ec2e5e72637404e1eb60f39e + languageName: node + linkType: hard + +"builtins@npm:^1.0.3": + version: 1.0.3 + resolution: "builtins@npm:1.0.3" + checksum: 10/8f756616bd3d92611bcb5bcc3008308e7cdaadbc4603a5ce6fe709193198bc115351d138524d79e5269339ef7ba5ba73185da541c7b4bc076b00dd0124f938f6 + languageName: node + linkType: hard + +"cacache@npm:^15.2.0": + version: 15.3.0 + resolution: "cacache@npm:15.3.0" + dependencies: + "@npmcli/fs": "npm:^1.0.0" + "@npmcli/move-file": "npm:^1.0.1" + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + glob: "npm:^7.1.4" + infer-owner: "npm:^1.0.4" + lru-cache: "npm:^6.0.0" + minipass: "npm:^3.1.1" + minipass-collect: "npm:^1.0.2" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.2" + mkdirp: "npm:^1.0.3" + p-map: "npm:^4.0.0" + promise-inflight: "npm:^1.0.1" + rimraf: "npm:^3.0.2" + ssri: "npm:^8.0.1" + tar: "npm:^6.0.2" + unique-filename: "npm:^1.1.1" + checksum: 10/1432d84f3f4b31421cf47c15e6956e5e736a93c65126b0fd69ae5f70643d29be8996f33d4995204f578850de5d556268540911c04ecc1c026375b18600534f08 + languageName: node + linkType: hard + +"cacache@npm:^17.0.0": + version: 17.1.4 + resolution: "cacache@npm:17.1.4" + dependencies: + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^7.7.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^1.0.2" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: 10/6e26c788bc6a18ff42f4d4f97db30d5c60a5dfac8e7c10a03b0307a92cf1b647570547cf3cd96463976c051eb9c7258629863f156e224c82018862c1a8ad0e70 + languageName: node + linkType: hard + +"cacheable-lookup@npm:^5.0.3": + version: 5.0.4 + resolution: "cacheable-lookup@npm:5.0.4" + checksum: 10/618a8b3eea314060e74cb3285a6154e8343c244a34235acf91cfe626ee0705c24e3cd11e4b1a7b3900bd749ee203ae65afe13adf610c8ab173e99d4a208faf75 + languageName: node + linkType: hard + +"cacheable-request@npm:^7.0.2": + version: 7.0.4 + resolution: "cacheable-request@npm:7.0.4" + dependencies: + clone-response: "npm:^1.0.2" + get-stream: "npm:^5.1.0" + http-cache-semantics: "npm:^4.0.0" + keyv: "npm:^4.0.0" + lowercase-keys: "npm:^2.0.0" + normalize-url: "npm:^6.0.1" + responselike: "npm:^2.0.0" + checksum: 10/0f4f2001260ecca78b9f64fc8245e6b5a5dcde24ea53006daab71f5e0e1338095aa1512ec099c4f9895a9e5acfac9da423cb7c079e131485891e9214aca46c41 + languageName: node + linkType: hard + +"cachedir@npm:^2.3.0": + version: 2.4.0 + resolution: "cachedir@npm:2.4.0" + checksum: 10/43198514eaa61f65b5535ed29ad651f22836fba3868ed58a6a87731f05462f317d39098fa3ac778801c25455483c9b7f32a2fcad1f690a978947431f12a0f4d0 + languageName: node + linkType: hard + +"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind@npm:1.0.2" + dependencies: + function-bind: "npm:^1.1.1" + get-intrinsic: "npm:^1.0.2" + checksum: 10/ca787179c1cbe09e1697b56ad499fd05dc0ae6febe5081d728176ade699ea6b1589240cb1ff1fe11fcf9f61538c1af60ad37e8eb2ceb4ef21cd6085dfd3ccedd + languageName: node + linkType: hard + +"callsites@npm:^3.0.0": + version: 3.1.0 + resolution: "callsites@npm:3.1.0" + checksum: 10/072d17b6abb459c2ba96598918b55868af677154bec7e73d222ef95a8fdb9bbf7dae96a8421085cdad8cd190d86653b5b6dc55a4484f2e5b2e27d5e0c3fc15b3 + languageName: node + linkType: hard + +"camelcase@npm:^5.3.1": + version: 5.3.1 + resolution: "camelcase@npm:5.3.1" + checksum: 10/e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b + languageName: node + linkType: hard + +"camelcase@npm:^6.2.0": + version: 6.3.0 + resolution: "camelcase@npm:6.3.0" + checksum: 10/8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d + languageName: node + linkType: hard + +"camelcase@npm:^7.0.1": + version: 7.0.1 + resolution: "camelcase@npm:7.0.1" + checksum: 10/86ab8f3ebf08bcdbe605a211a242f00ed30d8bfb77dab4ebb744dd36efbc84432d1c4adb28975ba87a1b8be40a80fbd1e60e2f06565315918fa7350011a26d3d + languageName: node + linkType: hard + +"caniuse-lite@npm:^1.0.30001541": + version: 1.0.30001543 + resolution: "caniuse-lite@npm:1.0.30001543" + checksum: 10/745ee3a3fc6b89d2f7cbfddaf2f2eb2edf0f2b7943147c3ea99d68ed3a5dd335826ec9415179bd29e471ae5e399094201df2bd476cfc700ef0281e845fac17f9 + languageName: node + linkType: hard + +"catharsis@npm:^0.9.0": + version: 0.9.0 + resolution: "catharsis@npm:0.9.0" + dependencies: + lodash: "npm:^4.17.15" + checksum: 10/a4f54d5982c0bc4342b1b27b89aa7fb359994ecd1d41431d8bd30432171188179fce1d11a0240863adb05edd157d5af1ded4c72b4972b0098f266e28f9c67164 + languageName: node + linkType: hard + +"chalk@npm:^2.4.1, chalk@npm:^2.4.2": + version: 2.4.2 + resolution: "chalk@npm:2.4.2" + dependencies: + ansi-styles: "npm:^3.2.1" + escape-string-regexp: "npm:^1.0.5" + supports-color: "npm:^5.3.0" + checksum: 10/3d1d103433166f6bfe82ac75724951b33769675252d8417317363ef9d54699b7c3b2d46671b772b893a8e50c3ece70c4b933c73c01e81bc60ea4df9b55afa303 + languageName: node + linkType: hard + +"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: 10/cb3f3e594913d63b1814d7ca7c9bafbf895f75fbf93b92991980610dfd7b48500af4e3a5d4e3a8f337990a96b168d7eb84ee55efdce965e2ee8efc20f8c8f139 + languageName: node + linkType: hard + +"chalk@npm:^5.2.0, chalk@npm:^5.3.0": + version: 5.3.0 + resolution: "chalk@npm:5.3.0" + checksum: 10/6373caaab21bd64c405bfc4bd9672b145647fc9482657b5ea1d549b3b2765054e9d3d928870cdf764fb4aad67555f5061538ff247b8310f110c5c888d92397ea + languageName: node + linkType: hard + +"char-regex@npm:^1.0.2": + version: 1.0.2 + resolution: "char-regex@npm:1.0.2" + checksum: 10/1ec5c2906adb9f84e7f6732a40baef05d7c85401b82ffcbc44b85fbd0f7a2b0c2a96f2eb9cf55cae3235dc12d4023003b88f09bcae8be9ae894f52ed746f4d48 + languageName: node + linkType: hard + +"chardet@npm:^0.7.0": + version: 0.7.0 + resolution: "chardet@npm:0.7.0" + checksum: 10/b0ec668fba5eeec575ed2559a0917ba41a6481f49063c8445400e476754e0957ee09e44dc032310f526182b8f1bf25e9d4ed371f74050af7be1383e06bc44952 + languageName: node + linkType: hard + +"child-process-ext@npm:^2.1.1": + version: 2.1.1 + resolution: "child-process-ext@npm:2.1.1" + dependencies: + cross-spawn: "npm:^6.0.5" + es5-ext: "npm:^0.10.53" + log: "npm:^6.0.0" + split2: "npm:^3.1.1" + stream-promise: "npm:^3.2.0" + checksum: 10/a7cc593d833a9fedccf1b24179bdbd014c8bdfad4ce5b598c21b85ef58e359603bb1a2827746c93c4f6690fbfda3ca3a27557496d67ec4f7ce0e32308120acba + languageName: node + linkType: hard + +"child-process-ext@npm:^3.0.1": + version: 3.0.2 + resolution: "child-process-ext@npm:3.0.2" + dependencies: + cross-spawn: "npm:^7.0.3" + es5-ext: "npm:^0.10.62" + log: "npm:^6.3.1" + split2: "npm:^3.2.2" + stream-promise: "npm:^3.2.0" + checksum: 10/3b21cc38c3bded8ad6fb71d700348f5ff35fdbc717496fbb06f7cf4be069ddaa8e967b6038f54c8fcbbb8e20495b190ae560b90f8b7db60569956cb94883d238 + languageName: node + linkType: hard + +"chokidar@npm:^3.5.3": + version: 3.5.3 + resolution: "chokidar@npm:3.5.3" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10/863e3ff78ee7a4a24513d2a416856e84c8e4f5e60efbe03e8ab791af1a183f569b62fc6f6b8044e2804966cb81277ddbbc1dc374fba3265bd609ea8efd62f5b3 + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: 10/c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f + languageName: node + linkType: hard + +"chrome-trace-event@npm:^1.0.2": + version: 1.0.3 + resolution: "chrome-trace-event@npm:1.0.3" + checksum: 10/b5fbdae5bf00c96fa3213de919f2b2617a942bfcb891cdf735fbad2a6f4f3c25d42e3f2b1703328619d352c718b46b9e18999fd3af7ef86c26c91db6fae1f0da + languageName: node + linkType: hard + +"ci-info@npm:^3.2.0, ci-info@npm:^3.8.0": + version: 3.9.0 + resolution: "ci-info@npm:3.9.0" + checksum: 10/75bc67902b4d1c7b435497adeb91598f6d52a3389398e44294f6601b20cfef32cf2176f7be0eb961d9e085bb333a8a5cae121cb22f81cf238ae7f58eb80e9397 + languageName: node + linkType: hard + +"cipher-base@npm:^1.0.0, cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3": + version: 1.0.4 + resolution: "cipher-base@npm:1.0.4" + dependencies: + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + checksum: 10/3d5d6652ca499c3f7c5d7fdc2932a357ec1e5aa84f2ad766d850efd42e89753c97b795c3a104a8e7ae35b4e293f5363926913de3bf8181af37067d9d541ca0db + languageName: node + linkType: hard + +"cjs-module-lexer@npm:^1.0.0": + version: 1.2.3 + resolution: "cjs-module-lexer@npm:1.2.3" + checksum: 10/f96a5118b0a012627a2b1c13bd2fcb92509778422aaa825c5da72300d6dcadfb47134dd2e9d97dfa31acd674891dd91642742772d19a09a8adc3e56bd2f5928c + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 10/2ac8cd2b2f5ec986a3c743935ec85b07bc174d5421a5efc8017e1f146a1cf5f781ae962618f416352103b32c9cd7e203276e8c28241bbe946160cab16149fb68 + languageName: node + linkType: hard + +"cli-boxes@npm:^3.0.0": + version: 3.0.0 + resolution: "cli-boxes@npm:3.0.0" + checksum: 10/637d84419d293a9eac40a1c8c96a2859e7d98b24a1a317788e13c8f441be052fc899480c6acab3acc82eaf1bccda6b7542d7cdcf5c9c3cc39227175dc098d5b2 + languageName: node + linkType: hard + +"cli-color@npm:^2.0.1, cli-color@npm:^2.0.2, cli-color@npm:^2.0.3": + version: 2.0.3 + resolution: "cli-color@npm:2.0.3" + dependencies: + d: "npm:^1.0.1" + es5-ext: "npm:^0.10.61" + es6-iterator: "npm:^2.0.3" + memoizee: "npm:^0.4.15" + timers-ext: "npm:^0.1.7" + checksum: 10/35244ba10cd7e5e38df02fbe54128dd11362f0114fdcaf44ee5a59c6af8b7680258fee4954de114cc3f824ed5bf7337270098b15e05bde6ae3877a4f67558b41 + languageName: node + linkType: hard + +"cli-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "cli-cursor@npm:3.1.0" + dependencies: + restore-cursor: "npm:^3.1.0" + checksum: 10/2692784c6cd2fd85cfdbd11f53aea73a463a6d64a77c3e098b2b4697a20443f430c220629e1ca3b195ea5ac4a97a74c2ee411f3807abf6df2b66211fec0c0a29 + languageName: node + linkType: hard + +"cli-progress-footer@npm:^2.3.2": + version: 2.3.2 + resolution: "cli-progress-footer@npm:2.3.2" + dependencies: + cli-color: "npm:^2.0.2" + d: "npm:^1.0.1" + es5-ext: "npm:^0.10.61" + mute-stream: "npm:0.0.8" + process-utils: "npm:^4.0.0" + timers-ext: "npm:^0.1.7" + type: "npm:^2.6.0" + checksum: 10/a5b5e7e352468b6e70e129203fe7de57be71e26d5982754a58ed5da7b9e3cabfd90e9349806a5c6b392200d6b3284e740376700c0433ec130a96ba39bfae3464 + languageName: node + linkType: hard + +"cli-spinners@npm:^2.5.0": + version: 2.9.1 + resolution: "cli-spinners@npm:2.9.1" + checksum: 10/80b7b21f2e713729041b26afd02cd881a05ba83d0973c60d332e6010261a732a42d039bdf401dec32645cba41a69324880bbbd999c8876b1eb9888451137df01 + languageName: node + linkType: hard + +"cli-sprintf-format@npm:^1.1.1": + version: 1.1.1 + resolution: "cli-sprintf-format@npm:1.1.1" + dependencies: + cli-color: "npm:^2.0.1" + es5-ext: "npm:^0.10.53" + sprintf-kit: "npm:^2.0.1" + supports-color: "npm:^6.1.0" + checksum: 10/3dad8362e16f553cbabaec36a76350c76ccf988e56205352d9392c9311449cfa50a5c677e780411237e3712fb77928cd642406b23a5c3af1716ab0dde170815d + languageName: node + linkType: hard + +"cli-width@npm:^3.0.0": + version: 3.0.0 + resolution: "cli-width@npm:3.0.0" + checksum: 10/8730848b04fb189666ab037a35888d191c8f05b630b1d770b0b0e4c920b47bb5cc14bddf6b8ffe5bfc66cee97c8211d4d18e756c1ffcc75d7dbe7e1186cd7826 + languageName: node + linkType: hard + +"cliui@npm:^7.0.2": + version: 7.0.4 + resolution: "cliui@npm:7.0.4" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.0" + wrap-ansi: "npm:^7.0.0" + checksum: 10/db858c49af9d59a32d603987e6fddaca2ce716cd4602ba5a2bb3a5af1351eebe82aba8dff3ef3e1b331f7fa9d40ca66e67bdf8e7c327ce0ea959747ead65c0ef + languageName: node + linkType: hard + +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^7.0.0" + checksum: 10/eaa5561aeb3135c2cddf7a3b3f562fc4238ff3b3fc666869ef2adf264be0f372136702f16add9299087fb1907c2e4ec5dbfe83bd24bce815c70a80c6c1a2e950 + languageName: node + linkType: hard + +"clone-response@npm:^1.0.2": + version: 1.0.3 + resolution: "clone-response@npm:1.0.3" + dependencies: + mimic-response: "npm:^1.0.0" + checksum: 10/4e671cac39b11c60aa8ba0a450657194a5d6504df51bca3fac5b3bd0145c4f8e8464898f87c8406b83232e3bc5cca555f51c1f9c8ac023969ebfbf7f6bdabb2e + languageName: node + linkType: hard + +"clone@npm:^1.0.2": + version: 1.0.4 + resolution: "clone@npm:1.0.4" + checksum: 10/d06418b7335897209e77bdd430d04f882189582e67bd1f75a04565f3f07f5b3f119a9d670c943b6697d0afb100f03b866b3b8a1f91d4d02d72c4ecf2bb64b5dd + languageName: node + linkType: hard + +"co@npm:^4.6.0": + version: 4.6.0 + resolution: "co@npm:4.6.0" + checksum: 10/a5d9f37091c70398a269e625cedff5622f200ed0aa0cff22ee7b55ed74a123834b58711776eb0f1dc58eb6ebbc1185aa7567b57bd5979a948c6e4f85073e2c05 + languageName: node + linkType: hard + +"collect-v8-coverage@npm:^1.0.0": + version: 1.0.2 + resolution: "collect-v8-coverage@npm:1.0.2" + checksum: 10/30ea7d5c9ee51f2fdba4901d4186c5b7114a088ef98fd53eda3979da77eed96758a2cae81cc6d97e239aaea6065868cf908b24980663f7b7e96aa291b3e12fa4 + languageName: node + linkType: hard + +"color-convert@npm:^1.9.0, color-convert@npm:^1.9.3": + version: 1.9.3 + resolution: "color-convert@npm:1.9.3" + dependencies: + color-name: "npm:1.1.3" + checksum: 10/ffa319025045f2973919d155f25e7c00d08836b6b33ea2d205418c59bd63a665d713c52d9737a9e0fe467fb194b40fbef1d849bae80d674568ee220a31ef3d10 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 10/fa00c91b4332b294de06b443923246bccebe9fab1b253f7fe1772d37b06a2269b4039a85e309abe1fe11b267b11c08d1d0473fda3badd6167f57313af2887a64 + languageName: node + linkType: hard + +"color-name@npm:1.1.3": + version: 1.1.3 + resolution: "color-name@npm:1.1.3" + checksum: 10/09c5d3e33d2105850153b14466501f2bfb30324a2f76568a408763a3b7433b0e50e5b4ab1947868e65cb101bb7cb75029553f2c333b6d4b8138a73fcc133d69d + languageName: node + linkType: hard + +"color-name@npm:^1.0.0, color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: 10/b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 + languageName: node + linkType: hard + +"color-string@npm:^1.6.0": + version: 1.9.1 + resolution: "color-string@npm:1.9.1" + dependencies: + color-name: "npm:^1.0.0" + simple-swizzle: "npm:^0.2.2" + checksum: 10/72aa0b81ee71b3f4fb1ac9cd839cdbd7a011a7d318ef58e6cb13b3708dca75c7e45029697260488709f1b1c7ac4e35489a87e528156c1e365917d1c4ccb9b9cd + languageName: node + linkType: hard + +"color-support@npm:^1.1.2, color-support@npm:^1.1.3": + version: 1.1.3 + resolution: "color-support@npm:1.1.3" + bin: + color-support: bin.js + checksum: 10/4bcfe30eea1498fe1cabc852bbda6c9770f230ea0e4faf4611c5858b1b9e4dde3730ac485e65f54ca182f4c50b626c1bea7c8441ceda47367a54a818c248aa7a + languageName: node + linkType: hard + +"color@npm:^3.1.3": + version: 3.2.1 + resolution: "color@npm:3.2.1" + dependencies: + color-convert: "npm:^1.9.3" + color-string: "npm:^1.6.0" + checksum: 10/bf70438e0192f4f62f4bfbb303e7231289e8cc0d15ff6b6cbdb722d51f680049f38d4fdfc057a99cb641895cf5e350478c61d98586400b060043afc44285e7ae + languageName: node + linkType: hard + +"colorspace@npm:1.1.x": + version: 1.1.4 + resolution: "colorspace@npm:1.1.4" + dependencies: + color: "npm:^3.1.3" + text-hex: "npm:1.0.x" + checksum: 10/bb3934ef3c417e961e6d03d7ca60ea6e175947029bfadfcdb65109b01881a1c0ecf9c2b0b59abcd0ee4a0d7c1eae93beed01b0e65848936472270a0b341ebce8 + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 10/2e969e637d05d09fa50b02d74c83a1186f6914aae89e6653b62595cc75a221464f884f55f231b8f4df7a49537fba60bdc0427acd2bf324c09a1dbb84837e36e4 + languageName: node + linkType: hard + +"commander@npm:^10.0.0": + version: 10.0.1 + resolution: "commander@npm:10.0.1" + checksum: 10/8799faa84a30da985802e661cc9856adfaee324d4b138413013ef7f087e8d7924b144c30a1f1405475f0909f467665cd9e1ce13270a2f41b141dab0b7a58f3fb + languageName: node + linkType: hard + +"commander@npm:^2.20.0, commander@npm:^2.8.1": + version: 2.20.3 + resolution: "commander@npm:2.20.3" + checksum: 10/90c5b6898610cd075984c58c4f88418a4fb44af08c1b1415e9854c03171bec31b336b7f3e4cefe33de994b3f12b03c5e2d638da4316df83593b9e82554e7e95b + languageName: node + linkType: hard + +"commander@npm:^3.0.2": + version: 3.0.2 + resolution: "commander@npm:3.0.2" + checksum: 10/f42053569f5954498246783465b39139917a51284bf3361574c9f731fea27a4bd6452dbb1755cc2d923c7b47dfea67930037c7b7e862288f2c397cec9a74da87 + languageName: node + linkType: hard + +"commander@npm:~4.1.1": + version: 4.1.1 + resolution: "commander@npm:4.1.1" + checksum: 10/3b2dc4125f387dab73b3294dbcb0ab2a862f9c0ad748ee2b27e3544d25325b7a8cdfbcc228d103a98a716960b14478114a5206b5415bd48cdafa38797891562c + languageName: node + linkType: hard + +"component-emitter@npm:^1.3.0": + version: 1.3.0 + resolution: "component-emitter@npm:1.3.0" + checksum: 10/dfc1ec2e7aa2486346c068f8d764e3eefe2e1ca0b24f57506cd93b2ae3d67829a7ebd7cc16e2bf51368fac2f45f78fcff231718e40b1975647e4a86be65e1d05 + languageName: node + linkType: hard + +"compress-commons@npm:^4.1.2": + version: 4.1.2 + resolution: "compress-commons@npm:4.1.2" + dependencies: + buffer-crc32: "npm:^0.2.13" + crc32-stream: "npm:^4.0.2" + normalize-path: "npm:^3.0.0" + readable-stream: "npm:^3.6.0" + checksum: 10/76fa281412e4a95f89893dc1e3399e797de20253365cf53102ac4738fa004d3540abb12c26e3a54156f8fb4e4392ef9a9c5eecbe752f3a7d30e28c808b671e1b + languageName: node + linkType: hard + +"compressible@npm:^2.0.12": + version: 2.0.18 + resolution: "compressible@npm:2.0.18" + dependencies: + mime-db: "npm:>= 1.43.0 < 2" + checksum: 10/58321a85b375d39230405654721353f709d0c1442129e9a17081771b816302a012471a9b8f4864c7dbe02eef7f2aaac3c614795197092262e94b409c9be108f0 + languageName: node + linkType: hard + +"concat-map@npm:0.0.1": + version: 0.0.1 + resolution: "concat-map@npm:0.0.1" + checksum: 10/9680699c8e2b3af0ae22592cb764acaf973f292a7b71b8a06720233011853a58e256c89216a10cbe889727532fd77f8bcd49a760cedfde271b8e006c20e079f2 + languageName: node + linkType: hard + +"config-chain@npm:^1.1.13": + version: 1.1.13 + resolution: "config-chain@npm:1.1.13" + dependencies: + ini: "npm:^1.3.4" + proto-list: "npm:~1.2.1" + checksum: 10/83d22cabf709e7669f6870021c4d552e4fc02e9682702b726be94295f42ce76cfed00f70b2910ce3d6c9465d9758e191e28ad2e72ff4e3331768a90da6c1ef03 + languageName: node + linkType: hard + +"confusing-browser-globals@npm:^1.0.10": + version: 1.0.11 + resolution: "confusing-browser-globals@npm:1.0.11" + checksum: 10/3afc635abd37e566477f610e7978b15753f0e84025c25d49236f1f14d480117185516bdd40d2a2167e6bed8048641a9854964b9c067e3dcdfa6b5d0ad3c3a5ef + languageName: node + linkType: hard + +"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0": + version: 1.1.0 + resolution: "console-control-strings@npm:1.1.0" + checksum: 10/27b5fa302bc8e9ae9e98c03c66d76ca289ad0c61ce2fe20ab288d288bee875d217512d2edb2363fc83165e88f1c405180cf3f5413a46e51b4fe1a004840c6cdb + languageName: node + linkType: hard + +"content-disposition@npm:^0.5.4": + version: 0.5.4 + resolution: "content-disposition@npm:0.5.4" + dependencies: + safe-buffer: "npm:5.2.1" + checksum: 10/b7f4ce176e324f19324be69b05bf6f6e411160ac94bc523b782248129eb1ef3be006f6cff431aaea5e337fe5d176ce8830b8c2a1b721626ead8933f0cbe78720 + languageName: node + linkType: hard + +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 10/c987be3ec061348cdb3c2bfb924bec86dea1eacad10550a85ca23edb0fe3556c3a61c7399114f3331ccb3499d7fd0285ab24566e5745929412983494c3926e15 + languageName: node + linkType: hard + +"cookiejar@npm:^2.1.3": + version: 2.1.4 + resolution: "cookiejar@npm:2.1.4" + checksum: 10/4a184f5a0591df8b07d22a43ea5d020eacb4572c383e853a33361a99710437eaa0971716c688684075bbf695b484f5872e9e3f562382e46858716cb7fc8ce3f4 + languageName: node + linkType: hard + +"core-util-is@npm:~1.0.0": + version: 1.0.3 + resolution: "core-util-is@npm:1.0.3" + checksum: 10/9de8597363a8e9b9952491ebe18167e3b36e7707569eed0ebf14f8bba773611376466ae34575bca8cfe3c767890c859c74056084738f09d4e4a6f902b2ad7d99 + languageName: node + linkType: hard + +"cosmiconfig@npm:^7.0.1": + version: 7.1.0 + resolution: "cosmiconfig@npm:7.1.0" + dependencies: + "@types/parse-json": "npm:^4.0.0" + import-fresh: "npm:^3.2.1" + parse-json: "npm:^5.0.0" + path-type: "npm:^4.0.0" + yaml: "npm:^1.10.0" + checksum: 10/03600bb3870c80ed151b7b706b99a1f6d78df8f4bdad9c95485072ea13358ef294b13dd99f9e7bf4cc0b43bcd3599d40df7e648750d21c2f6817ca2cd687e071 + languageName: node + linkType: hard + +"crc-32@npm:^1.2.0": + version: 1.2.2 + resolution: "crc-32@npm:1.2.2" + bin: + crc32: bin/crc32.njs + checksum: 10/824f696a5baaf617809aa9cd033313c8f94f12d15ebffa69f10202480396be44aef9831d900ab291638a8022ed91c360696dd5b1ba691eb3f34e60be8835b7c3 + languageName: node + linkType: hard + +"crc32-stream@npm:^4.0.2": + version: 4.0.3 + resolution: "crc32-stream@npm:4.0.3" + dependencies: + crc-32: "npm:^1.2.0" + readable-stream: "npm:^3.4.0" + checksum: 10/d44d0ec6f04d8a1bed899ac3e4fbb82111ed567ea6d506be39147362af45c747887fce1032f4beca1646b4824e5a9614cd3332bfa94bbc5577ca5445e7f75ddd + languageName: node + linkType: hard + +"create-hash@npm:^1.1.0, create-hash@npm:^1.1.2, create-hash@npm:^1.2.0": + version: 1.2.0 + resolution: "create-hash@npm:1.2.0" + dependencies: + cipher-base: "npm:^1.0.1" + inherits: "npm:^2.0.1" + md5.js: "npm:^1.3.4" + ripemd160: "npm:^2.0.1" + sha.js: "npm:^2.4.0" + checksum: 10/3cfef32043b47a8999602af9bcd74966db6971dd3eb828d1a479f3a44d7f58e38c1caf34aa21a01941cc8d9e1a841738a732f200f00ea155f8a8835133d2e7bc + languageName: node + linkType: hard + +"create-hmac@npm:^1.1.4, create-hmac@npm:^1.1.7": + version: 1.1.7 + resolution: "create-hmac@npm:1.1.7" + dependencies: + cipher-base: "npm:^1.0.3" + create-hash: "npm:^1.1.0" + inherits: "npm:^2.0.1" + ripemd160: "npm:^2.0.0" + safe-buffer: "npm:^5.0.1" + sha.js: "npm:^2.4.8" + checksum: 10/2b26769f87e99ef72150bf99d1439d69272b2e510e23a2b8daf4e93e2412f4842504237d726044fa797cb20ee0ec8bee78d414b11f2d7ca93299185c93df0dae + languageName: node + linkType: hard + +"create-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "create-jest@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-config: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + prompts: "npm:^2.0.1" + bin: + create-jest: bin/create-jest.js + checksum: 10/847b4764451672b4174be4d5c6d7d63442ec3aa5f3de52af924e4d996d87d7801c18e125504f25232fc75840f6625b3ac85860fac6ce799b5efae7bdcaf4a2b7 + languageName: node + linkType: hard + +"create-require@npm:^1.1.0": + version: 1.1.1 + resolution: "create-require@npm:1.1.1" + checksum: 10/a9a1503d4390d8b59ad86f4607de7870b39cad43d929813599a23714831e81c520bddf61bcdd1f8e30f05fd3a2b71ae8538e946eb2786dc65c2bbc520f692eff + languageName: node + linkType: hard + +"cron-parser@npm:^4.2.0": + version: 4.9.0 + resolution: "cron-parser@npm:4.9.0" + dependencies: + luxon: "npm:^3.2.1" + checksum: 10/ffca5e532a5ee0923412ee6e4c7f9bbceacc6ddf8810c16d3e9fb4fe5ec7e2de1b6896d7956f304bb6bc96b0ce37ad7e3935304179d52951c18d84107184faa7 + languageName: node + linkType: hard + +"cross-spawn@npm:^6.0.5": + version: 6.0.5 + resolution: "cross-spawn@npm:6.0.5" + dependencies: + nice-try: "npm:^1.0.4" + path-key: "npm:^2.0.1" + semver: "npm:^5.5.0" + shebang-command: "npm:^1.2.0" + which: "npm:^1.2.9" + checksum: 10/f07e643b4875f26adffcd7f13bc68d9dff20cf395f8ed6f43a23f3ee24fc3a80a870a32b246fd074e514c8fd7da5f978ac6a7668346eec57aa87bac89c1ed3a1 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10/e1a13869d2f57d974de0d9ef7acbf69dc6937db20b918525a01dacb5032129bd552d290d886d981e99f1b624cb03657084cc87bd40f115c07ecf376821c729ce + languageName: node + linkType: hard + +"crypto-js@npm:^3.1.9-1": + version: 3.3.0 + resolution: "crypto-js@npm:3.3.0" + checksum: 10/d7e11f3a387fb143be834e1a25ecf57ead6f5765e90fbf3aed9cead680cc38b1d241718768b7bfec448a843f569374ea5b5870ac7a8165e4bfa1915f0b00c89c + languageName: node + linkType: hard + +"d@npm:1, d@npm:^1.0.1": + version: 1.0.1 + resolution: "d@npm:1.0.1" + dependencies: + es5-ext: "npm:^0.10.50" + type: "npm:^1.0.1" + checksum: 10/1296e3f92e646895681c1cb564abd0eb23c29db7d62c5120a279e84e98915499a477808e9580760f09e3744c0ed7ac8f7cff98d096ba9770754f6ef0f1c97983 + languageName: node + linkType: hard + +"dayjs@npm:^1.11.8": + version: 1.11.10 + resolution: "dayjs@npm:1.11.10" + checksum: 10/27e8f5bc01c0a76f36c656e62ab7f08c2e7b040b09e613cd4844abf03fb258e0350f0a83b02c887b84d771c1f11e092deda0beef8c6df2a1afbc3f6c1fade279 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/0073c3bcbd9cb7d71dd5f6b55be8701af42df3e56e911186dfa46fac3a5b9eb7ce7f377dd1d3be6db8977221f8eb333d945216f645cf56f6b688cd484837d255 + languageName: node + linkType: hard + +"debug@npm:=3.1.0": + version: 3.1.0 + resolution: "debug@npm:3.1.0" + dependencies: + ms: "npm:2.0.0" + checksum: 10/f5fd4b1390dd3b03a78aa30133a4b4db62acc3e6cd86af49f114bf7f7bd57c41a5c5c2eced2ad2c8190d70c60309f2dd5782feeaa0704dbaa5697890e3c5ad07 + languageName: node + linkType: hard + +"debug@npm:^2.2.0": + version: 2.6.9 + resolution: "debug@npm:2.6.9" + dependencies: + ms: "npm:2.0.0" + checksum: 10/e07005f2b40e04f1bd14a3dd20520e9c4f25f60224cb006ce9d6781732c917964e9ec029fc7f1a151083cd929025ad5133814d4dc624a9aaf020effe4914ed14 + languageName: node + linkType: hard + +"debug@npm:^3.2.7": + version: 3.2.7 + resolution: "debug@npm:3.2.7" + dependencies: + ms: "npm:^2.1.1" + checksum: 10/d86fd7be2b85462297ea16f1934dc219335e802f629ca9a69b63ed8ed041dda492389bb2ee039217c02e5b54792b1c51aa96ae954cf28634d363a2360c7a1639 + languageName: node + linkType: hard + +"decompress-response@npm:^6.0.0": + version: 6.0.0 + resolution: "decompress-response@npm:6.0.0" + dependencies: + mimic-response: "npm:^3.1.0" + checksum: 10/d377cf47e02d805e283866c3f50d3d21578b779731e8c5072d6ce8c13cc31493db1c2f6784da9d1d5250822120cefa44f1deab112d5981015f2e17444b763812 + languageName: node + linkType: hard + +"decompress-tar@npm:^4.0.0, decompress-tar@npm:^4.1.0, decompress-tar@npm:^4.1.1": + version: 4.1.1 + resolution: "decompress-tar@npm:4.1.1" + dependencies: + file-type: "npm:^5.2.0" + is-stream: "npm:^1.1.0" + tar-stream: "npm:^1.5.2" + checksum: 10/820c645dfa9a0722c4c817363431d07687374338e2af337cc20c9a44b285fdd89296837a1d1281ee9fa85c6f03d7c0f50670aec9abbd4eb198a714bb179ea0bd + languageName: node + linkType: hard + +"decompress-tarbz2@npm:^4.0.0": + version: 4.1.1 + resolution: "decompress-tarbz2@npm:4.1.1" + dependencies: + decompress-tar: "npm:^4.1.0" + file-type: "npm:^6.1.0" + is-stream: "npm:^1.1.0" + seek-bzip: "npm:^1.0.5" + unbzip2-stream: "npm:^1.0.9" + checksum: 10/519c81337730159a1f2d7072a6ee8523ffd76df48d34f14c27cb0a27f89b4e2acf75dad2f761838e5bc63230cea1ac154b092ecb7504be4e93f7d0e32ddd6aff + languageName: node + linkType: hard + +"decompress-targz@npm:^4.0.0": + version: 4.1.1 + resolution: "decompress-targz@npm:4.1.1" + dependencies: + decompress-tar: "npm:^4.1.1" + file-type: "npm:^5.2.0" + is-stream: "npm:^1.1.0" + checksum: 10/22738f58eb034568dc50d370c03b346c428bfe8292fe56165847376b5af17d3c028fefca82db642d79cb094df4c0a599d40a8f294b02aad1d3ddec82f3fd45d4 + languageName: node + linkType: hard + +"decompress-unzip@npm:^4.0.1": + version: 4.0.1 + resolution: "decompress-unzip@npm:4.0.1" + dependencies: + file-type: "npm:^3.8.0" + get-stream: "npm:^2.2.0" + pify: "npm:^2.3.0" + yauzl: "npm:^2.4.2" + checksum: 10/ba9f3204ab2415bedb18d796244928a18148ef40dbb15174d0d01e5991b39536b03d02800a8a389515a1523f8fb13efc7cd44697df758cd06c674879caefd62b + languageName: node + linkType: hard + +"decompress@npm:^4.2.1": + version: 4.2.1 + resolution: "decompress@npm:4.2.1" + dependencies: + decompress-tar: "npm:^4.0.0" + decompress-tarbz2: "npm:^4.0.0" + decompress-targz: "npm:^4.0.0" + decompress-unzip: "npm:^4.0.1" + graceful-fs: "npm:^4.1.10" + make-dir: "npm:^1.0.0" + pify: "npm:^2.3.0" + strip-dirs: "npm:^2.0.0" + checksum: 10/8247a31c6db7178413715fdfb35a482f019c81dfcd6e8e623d9f0382c9889ce797ce0144de016b256ed03298907a620ce81387cca0e69067a933470081436cb8 + languageName: node + linkType: hard + +"dedent@npm:^1.0.0": + version: 1.5.1 + resolution: "dedent@npm:1.5.1" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: 10/fc00a8bc3dfb7c413a778dc40ee8151b6c6ff35159d641f36ecd839c1df5c6e0ec5f4992e658c82624a1a62aaecaffc23b9c965ceb0bbf4d698bfc16469ac27d + languageName: node + linkType: hard + +"deep-is@npm:^0.1.3, deep-is@npm:~0.1.3": + version: 0.1.4 + resolution: "deep-is@npm:0.1.4" + checksum: 10/ec12d074aef5ae5e81fa470b9317c313142c9e8e2afe3f8efa124db309720db96d1d222b82b84c834e5f87e7a614b44a4684b6683583118b87c833b3be40d4d8 + languageName: node + linkType: hard + +"deepmerge@npm:^4.2.2": + version: 4.3.1 + resolution: "deepmerge@npm:4.3.1" + checksum: 10/058d9e1b0ff1a154468bf3837aea436abcfea1ba1d165ddaaf48ca93765fdd01a30d33c36173da8fbbed951dd0a267602bc782fe288b0fc4b7e1e7091afc4529 + languageName: node + linkType: hard + +"defaults@npm:^1.0.3": + version: 1.0.4 + resolution: "defaults@npm:1.0.4" + dependencies: + clone: "npm:^1.0.2" + checksum: 10/3a88b7a587fc076b84e60affad8b85245c01f60f38fc1d259e7ac1d89eb9ce6abb19e27215de46b98568dd5bc48471730b327637e6f20b0f1bc85cf00440c80a + languageName: node + linkType: hard + +"defer-to-connect@npm:^2.0.0": + version: 2.0.1 + resolution: "defer-to-connect@npm:2.0.1" + checksum: 10/8a9b50d2f25446c0bfefb55a48e90afd58f85b21bcf78e9207cd7b804354f6409032a1705c2491686e202e64fc05f147aa5aa45f9aa82627563f045937f5791b + languageName: node + linkType: hard + +"deferred@npm:^0.7.11": + version: 0.7.11 + resolution: "deferred@npm:0.7.11" + dependencies: + d: "npm:^1.0.1" + es5-ext: "npm:^0.10.50" + event-emitter: "npm:^0.3.5" + next-tick: "npm:^1.0.0" + timers-ext: "npm:^0.1.7" + checksum: 10/be852df96582030f5fa3fa8ab85f75ef1f83f92f0894bf7653e949fd1c0ef529fc419e47066a1234378079e7d70144217ea8a19f5cee02a72e33e2496d974210 + languageName: node + linkType: hard + +"define-data-property@npm:^1.0.1": + version: 1.1.0 + resolution: "define-data-property@npm:1.1.0" + dependencies: + get-intrinsic: "npm:^1.2.1" + gopd: "npm:^1.0.1" + has-property-descriptors: "npm:^1.0.0" + checksum: 10/6b6ec9e0981fde641b043dcc153748aa9610d0b53f30e818b522220ce8aff47026c61466a73d9c5c6452ad4d9a694337125fc95aa84c2fb3cd1f6cd5af019a1b + languageName: node + linkType: hard + +"define-lazy-prop@npm:^2.0.0": + version: 2.0.0 + resolution: "define-lazy-prop@npm:2.0.0" + checksum: 10/0115fdb065e0490918ba271d7339c42453d209d4cb619dfe635870d906731eff3e1ade8028bb461ea27ce8264ec5e22c6980612d332895977e89c1bbc80fcee2 + languageName: node + linkType: hard + +"define-properties@npm:^1.1.3, define-properties@npm:^1.1.4, define-properties@npm:^1.2.0": + version: 1.2.1 + resolution: "define-properties@npm:1.2.1" + dependencies: + define-data-property: "npm:^1.0.1" + has-property-descriptors: "npm:^1.0.0" + object-keys: "npm:^1.1.1" + checksum: 10/b4ccd00597dd46cb2d4a379398f5b19fca84a16f3374e2249201992f36b30f6835949a9429669ee6b41b6e837205a163eadd745e472069e70dfc10f03e5fcc12 + languageName: node + linkType: hard + +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 10/46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020 + languageName: node + linkType: hard + +"delegates@npm:^1.0.0": + version: 1.0.0 + resolution: "delegates@npm:1.0.0" + checksum: 10/a51744d9b53c164ba9c0492471a1a2ffa0b6727451bdc89e31627fdf4adda9d51277cfcbfb20f0a6f08ccb3c436f341df3e92631a3440226d93a8971724771fd + languageName: node + linkType: hard + +"denque@npm:^1.5.0": + version: 1.5.1 + resolution: "denque@npm:1.5.1" + checksum: 10/dbde01a987d95205f7563c67411e0964073a6b38e4cf2ff190cf91f71e2ce3f51c40bacd31f2a5497e0ff82366bcfd8231d3659cb03f987279130058d512aa29 + languageName: node + linkType: hard + +"denque@npm:^2.0.1, denque@npm:^2.1.0": + version: 2.1.0 + resolution: "denque@npm:2.1.0" + checksum: 10/8ea05321576624b90acfc1ee9208b8d1d04b425cf7573b9b4fa40a2c3ed4d4b0af5190567858f532f677ed2003d4d2b73c8130b34e3c7b8d5e88cdcfbfaa1fe7 + languageName: node + linkType: hard + +"desm@npm:^1.3.0": + version: 1.3.0 + resolution: "desm@npm:1.3.0" + checksum: 10/0954cb5492f787331714dafadd484e8ee4abc7b4ada410b2835408a566ee8f4160eb6b2cd692de68250c3b65db8a1961ac45878137455a01111f18e5b62c5a2c + languageName: node + linkType: hard + +"detect-libc@npm:^2.0.0": + version: 2.0.2 + resolution: "detect-libc@npm:2.0.2" + checksum: 10/6118f30c0c425b1e56b9d2609f29bec50d35a6af0b762b6ad127271478f3bbfda7319ce869230cf1a351f2b219f39332cde290858553336d652c77b970f15de8 + languageName: node + linkType: hard + +"detect-newline@npm:^3.0.0": + version: 3.1.0 + resolution: "detect-newline@npm:3.1.0" + checksum: 10/ae6cd429c41ad01b164c59ea36f264a2c479598e61cba7c99da24175a7ab80ddf066420f2bec9a1c57a6bead411b4655ff15ad7d281c000a89791f48cbe939e7 + languageName: node + linkType: hard + +"dezalgo@npm:^1.0.4": + version: 1.0.4 + resolution: "dezalgo@npm:1.0.4" + dependencies: + asap: "npm:^2.0.0" + wrappy: "npm:1" + checksum: 10/895389c6aead740d2ab5da4d3466d20fa30f738010a4d3f4dcccc9fc645ca31c9d10b7e1804ae489b1eb02c7986f9f1f34ba132d409b043082a86d9a4e745624 + languageName: node + linkType: hard + +"diff-sequences@npm:^27.5.1": + version: 27.5.1 + resolution: "diff-sequences@npm:27.5.1" + checksum: 10/34d852a13eb82735c39944a050613f952038614ce324256e1c3544948fa090f1ca7f329a4f1f57c31fe7ac982c17068d8915b633e300f040b97708c81ceb26cd + languageName: node + linkType: hard + +"diff-sequences@npm:^29.6.3": + version: 29.6.3 + resolution: "diff-sequences@npm:29.6.3" + checksum: 10/179daf9d2f9af5c57ad66d97cb902a538bcf8ed64963fa7aa0c329b3de3665ce2eb6ffdc2f69f29d445fa4af2517e5e55e5b6e00c00a9ae4f43645f97f7078cb + languageName: node + linkType: hard + +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2" + checksum: 10/ec09ec2101934ca5966355a229d77afcad5911c92e2a77413efda5455636c4cf2ce84057e2d7715227a2eeeda04255b849bd3ae3a4dd22eb22e86e76456df069 + languageName: node + linkType: hard + +"dir-glob@npm:^3.0.1": + version: 3.0.1 + resolution: "dir-glob@npm:3.0.1" + dependencies: + path-type: "npm:^4.0.0" + checksum: 10/fa05e18324510d7283f55862f3161c6759a3f2f8dbce491a2fc14c8324c498286c54282c1f0e933cb930da8419b30679389499b919122952a4f8592362ef4615 + languageName: node + linkType: hard + +"doctrine@npm:^2.1.0": + version: 2.1.0 + resolution: "doctrine@npm:2.1.0" + dependencies: + esutils: "npm:^2.0.2" + checksum: 10/555684f77e791b17173ea86e2eea45ef26c22219cb64670669c4f4bebd26dbc95cd90ec1f4159e9349a6bb9eb892ce4dde8cd0139e77bedd8bf4518238618474 + languageName: node + linkType: hard + +"doctrine@npm:^3.0.0": + version: 3.0.0 + resolution: "doctrine@npm:3.0.0" + dependencies: + esutils: "npm:^2.0.2" + checksum: 10/b4b28f1df5c563f7d876e7461254a4597b8cabe915abe94d7c5d1633fed263fcf9a85e8d3836591fc2d040108e822b0d32758e5ec1fe31c590dc7e08086e3e48 + languageName: node + linkType: hard + +"dotenv-expand@npm:^10.0.0": + version: 10.0.0 + resolution: "dotenv-expand@npm:10.0.0" + checksum: 10/b41eb278bc96b92cbf3037ca5f3d21e8845bf165dc06b6f9a0a03d278c2bd5a01c0cfbb3528ae3a60301ba1a8a9cace30e748c54b460753bc00d4c014b675597 + languageName: node + linkType: hard + +"dotenv@npm:^10.0.0": + version: 10.0.0 + resolution: "dotenv@npm:10.0.0" + checksum: 10/55f701ae213e3afe3f4232fae5edfb6e0c49f061a363ff9f1c5a0c2bf3fb990a6e49aeada11b2a116efb5fdc3bc3f1ef55ab330be43033410b267f7c0809a9dc + languageName: node + linkType: hard + +"dotenv@npm:^16.3.1": + version: 16.3.1 + resolution: "dotenv@npm:16.3.1" + checksum: 10/dbb778237ef8750e9e3cd1473d3c8eaa9cc3600e33a75c0e36415d0fa0848197f56c3800f77924c70e7828f0b03896818cd52f785b07b9ad4d88dba73fbba83f + languageName: node + linkType: hard + +"dotenv@npm:^8.2.0": + version: 8.6.0 + resolution: "dotenv@npm:8.6.0" + checksum: 10/31d7b5c010cebb80046ba6853d703f9573369b00b15129536494f04b0af4ea0060ce8646e3af58b455af2f6f1237879dd261a5831656410ec92561ae1ea44508 + languageName: node + linkType: hard + +"dottie@npm:^2.0.6": + version: 2.0.6 + resolution: "dottie@npm:2.0.6" + checksum: 10/698731cfa2c1b530ba3491fa864dc572678a2a6de801f25912e2e4d7d4669ae013b696711786016bf41c7b6f98057c678503f14550bb171b3f70cdadffb9218f + languageName: node + linkType: hard + +"drbg.js@npm:^1.0.1": + version: 1.0.1 + resolution: "drbg.js@npm:1.0.1" + dependencies: + browserify-aes: "npm:^1.0.6" + create-hash: "npm:^1.1.2" + create-hmac: "npm:^1.1.4" + checksum: 10/a50e770cf641ec364f6b8de8e955c63e0db59f0af6525cc0306f392f4361427e37bf5c74373b31589b24e98d523acc7bbab4c8ee421bc35a2a8a82fe6e06ce95 + languageName: node + linkType: hard + +"duplexify@npm:^4.0.0": + version: 4.1.2 + resolution: "duplexify@npm:4.1.2" + dependencies: + end-of-stream: "npm:^1.4.1" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + stream-shift: "npm:^1.0.0" + checksum: 10/eeb4f362defa4da0b2474d853bc4edfa446faeb1bde76819a68035632c118de91f6a58e6fe05c84f6e6de2548f8323ec8473aa9fe37332c99e4d77539747193e + languageName: node + linkType: hard + +"duration@npm:^0.2.2": + version: 0.2.2 + resolution: "duration@npm:0.2.2" + dependencies: + d: "npm:1" + es5-ext: "npm:~0.10.46" + checksum: 10/823c7d1d06c5126346147e5271ddce1c1bd186c110beaac3d18f22a83a1725d4a66c29bbefe1a550cd49a093727978a01708628c2f0d479eb2abd9e0afb77554 + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 10/9b1d3e1baefeaf7d70799db8774149cef33b97183a6addceeba0cf6b85ba23ee2686f302f14482006df32df75d32b17c509c143a3689627929e4a8efaf483952 + languageName: node + linkType: hard + +"ecdsa-sig-formatter@npm:1.0.11, ecdsa-sig-formatter@npm:^1.0.11": + version: 1.0.11 + resolution: "ecdsa-sig-formatter@npm:1.0.11" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10/878e1aab8a42773320bc04c6de420bee21aebd71810e40b1799880a8a1c4594bcd6adc3d4213a0fb8147d4c3f529d8f9a618d7f59ad5a9a41b142058aceda23f + languageName: node + linkType: hard + +"ecurve@npm:^1.0.6": + version: 1.0.6 + resolution: "ecurve@npm:1.0.6" + dependencies: + bigi: "npm:^1.1.0" + safe-buffer: "npm:^5.0.1" + checksum: 10/5f738e564ad956acbcd743239b0b9e144d1fca999b6018370d86084545bf03ac5b63229918ab0b2ff87f676e85c23dedabc0f1ad6ea2eef160da70cb3119924b + languageName: node + linkType: hard + +"editorconfig@npm:^1.0.3": + version: 1.0.4 + resolution: "editorconfig@npm:1.0.4" + dependencies: + "@one-ini/wasm": "npm:0.1.1" + commander: "npm:^10.0.0" + minimatch: "npm:9.0.1" + semver: "npm:^7.5.3" + bin: + editorconfig: bin/editorconfig + checksum: 10/bd0a7236f31a7f54801cb6f3222508d4f872a24e440bef30ee29f4ba667c0741724e52e0ad521abe3409b12cdafd8384bb751de9b2a2ee5f845c740edd2e742f + languageName: node + linkType: hard + +"electron-to-chromium@npm:^1.4.535": + version: 1.4.540 + resolution: "electron-to-chromium@npm:1.4.540" + checksum: 10/7ee5cf8625dba3056a96dabbfb896cc262257b2b95734f85f5b5f12e03d50069b155ab29b05a1be29a1e27ff952fe1668dac2352f9c1104591dda19889886abc + languageName: node + linkType: hard + +"elliptic@npm:^6.5.2, elliptic@npm:^6.5.3": + version: 6.5.4 + resolution: "elliptic@npm:6.5.4" + dependencies: + bn.js: "npm:^4.11.9" + brorand: "npm:^1.1.0" + hash.js: "npm:^1.0.0" + hmac-drbg: "npm:^1.0.1" + inherits: "npm:^2.0.4" + minimalistic-assert: "npm:^1.0.1" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 10/2cd7ff4b69720dbb2ca1ca650b2cf889d1df60c96d4a99d331931e4fe21e45a7f3b8074e86618ca7e56366c4b6258007f234f9d61d9b0c87bbbc8ea990b99e94 + languageName: node + linkType: hard + +"emittery@npm:^0.13.1": + version: 0.13.1 + resolution: "emittery@npm:0.13.1" + checksum: 10/fbe214171d878b924eedf1757badf58a5dce071cd1fa7f620fa841a0901a80d6da47ff05929d53163105e621ce11a71b9d8acb1148ffe1745e045145f6e69521 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: 10/c72d67a6821be15ec11997877c437491c313d924306b8da5d87d2a2bcc2cec9903cb5b04ee1a088460501d8e5b44f10df82fdc93c444101a7610b80c8b6938e1 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 10/915acf859cea7131dac1b2b5c9c8e35c4849e325a1d114c30adb8cd615970f6dca0e27f64f3a4949d7d6ed86ecd79a1c5c63f02e697513cddd7b5835c90948b8 + languageName: node + linkType: hard + +"enabled@npm:2.0.x": + version: 2.0.0 + resolution: "enabled@npm:2.0.0" + checksum: 10/9d256d89f4e8a46ff988c6a79b22fa814b4ffd82826c4fdacd9b42e9b9465709d3b748866d0ab4d442dfc6002d81de7f7b384146ccd1681f6a7f868d2acca063 + languageName: node + linkType: hard + +"encoding@npm:^0.1.12, encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 10/bb98632f8ffa823996e508ce6a58ffcf5856330fde839ae42c9e1f436cc3b5cc651d4aeae72222916545428e54fd0f6aa8862fd8d25bdbcc4589f1e3f3715e7f + languageName: node + linkType: hard + +"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": + version: 1.4.4 + resolution: "end-of-stream@npm:1.4.4" + dependencies: + once: "npm:^1.4.0" + checksum: 10/530a5a5a1e517e962854a31693dbb5c0b2fc40b46dad2a56a2deec656ca040631124f4795823acc68238147805f8b021abbe221f4afed5ef3c8e8efc2024908b + languageName: node + linkType: hard + +"enhanced-resolve@npm:^5.0.0, enhanced-resolve@npm:^5.15.0": + version: 5.15.0 + resolution: "enhanced-resolve@npm:5.15.0" + dependencies: + graceful-fs: "npm:^4.2.4" + tapable: "npm:^2.2.0" + checksum: 10/180c3f2706f9117bf4dc7982e1df811dad83a8db075723f299245ef4488e0cad7e96859c5f0e410682d28a4ecd4da021ec7d06265f7e4eb6eed30c69ca5f7d3e + languageName: node + linkType: hard + +"ent@npm:^2.2.0": + version: 2.2.0 + resolution: "ent@npm:2.2.0" + checksum: 10/818a2b5f5039ea02c9e232ba4c7496ced8512341b2524ae7c6c808d2e2b357d8087e715e0e3950cec9895c20c9b3443e0b56a2e26879984d97bb511c5fbb5299 + languageName: node + linkType: hard + +"entities@npm:~2.1.0": + version: 2.1.0 + resolution: "entities@npm:2.1.0" + checksum: 10/fe71642e42e108540b0324dea03e00f3dbad93617c601bfcf292c3f852c236af3e58469219c4653f6f05df781a446f3b82105b8d26b936d0fa246b0103f2f951 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10/65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 10/1d20d825cdcce8d811bfbe86340f4755c02655a7feb2f13f8c880566d9d72a3f6c92c192a6867632e490d6da67b678271f46e01044996a6443e870331100dfdd + languageName: node + linkType: hard + +"error-ex@npm:^1.3.1": + version: 1.3.2 + resolution: "error-ex@npm:1.3.2" + dependencies: + is-arrayish: "npm:^0.2.1" + checksum: 10/d547740aa29c34e753fb6fed2c5de81802438529c12b3673bd37b6bb1fe49b9b7abdc3c11e6062fe625d8a296b3cf769a80f878865e25e685f787763eede3ffb + languageName: node + linkType: hard + +"es-abstract@npm:^1.22.1": + version: 1.22.2 + resolution: "es-abstract@npm:1.22.2" + dependencies: + array-buffer-byte-length: "npm:^1.0.0" + arraybuffer.prototype.slice: "npm:^1.0.2" + available-typed-arrays: "npm:^1.0.5" + call-bind: "npm:^1.0.2" + es-set-tostringtag: "npm:^2.0.1" + es-to-primitive: "npm:^1.2.1" + function.prototype.name: "npm:^1.1.6" + get-intrinsic: "npm:^1.2.1" + get-symbol-description: "npm:^1.0.0" + globalthis: "npm:^1.0.3" + gopd: "npm:^1.0.1" + has: "npm:^1.0.3" + has-property-descriptors: "npm:^1.0.0" + has-proto: "npm:^1.0.1" + has-symbols: "npm:^1.0.3" + internal-slot: "npm:^1.0.5" + is-array-buffer: "npm:^3.0.2" + is-callable: "npm:^1.2.7" + is-negative-zero: "npm:^2.0.2" + is-regex: "npm:^1.1.4" + is-shared-array-buffer: "npm:^1.0.2" + is-string: "npm:^1.0.7" + is-typed-array: "npm:^1.1.12" + is-weakref: "npm:^1.0.2" + object-inspect: "npm:^1.12.3" + object-keys: "npm:^1.1.1" + object.assign: "npm:^4.1.4" + regexp.prototype.flags: "npm:^1.5.1" + safe-array-concat: "npm:^1.0.1" + safe-regex-test: "npm:^1.0.0" + string.prototype.trim: "npm:^1.2.8" + string.prototype.trimend: "npm:^1.0.7" + string.prototype.trimstart: "npm:^1.0.7" + typed-array-buffer: "npm:^1.0.0" + typed-array-byte-length: "npm:^1.0.0" + typed-array-byte-offset: "npm:^1.0.0" + typed-array-length: "npm:^1.0.4" + unbox-primitive: "npm:^1.0.2" + which-typed-array: "npm:^1.1.11" + checksum: 10/fe09bf3bf707d5a781b9e4f9ef8e835a890600b7e1e65567328da12b173e99ffd9d5b86f5d0a69a5aa308a925b59c631814ada46fca55e9db10857a352289adb + languageName: node + linkType: hard + +"es-module-lexer@npm:^1.2.1": + version: 1.3.1 + resolution: "es-module-lexer@npm:1.3.1" + checksum: 10/c6aa137c5f5865fe1d12b4edbe027ff618d3836684cda9e52ae4dec48bfc2599b25db4f1265a12228d4663e21fd0126addfb79f761d513f1a6708c37989137e3 + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.0.1": + version: 2.0.1 + resolution: "es-set-tostringtag@npm:2.0.1" + dependencies: + get-intrinsic: "npm:^1.1.3" + has: "npm:^1.0.3" + has-tostringtag: "npm:^1.0.0" + checksum: 10/ec416a12948cefb4b2a5932e62093a7cf36ddc3efd58d6c58ca7ae7064475ace556434b869b0bbeb0c365f1032a8ccd577211101234b69837ad83ad204fff884 + languageName: node + linkType: hard + +"es-shim-unscopables@npm:^1.0.0": + version: 1.0.0 + resolution: "es-shim-unscopables@npm:1.0.0" + dependencies: + has: "npm:^1.0.3" + checksum: 10/ac2db2c70d253cf83bebcdc974d185239e205ca18af743efd3b656bac00cabfee2358a050b18b63b46972dab5cfa10ef3f2597eb3a8d4d6d9417689793665da6 + languageName: node + linkType: hard + +"es-to-primitive@npm:^1.2.1": + version: 1.2.1 + resolution: "es-to-primitive@npm:1.2.1" + dependencies: + is-callable: "npm:^1.1.4" + is-date-object: "npm:^1.0.1" + is-symbol: "npm:^1.0.2" + checksum: 10/74aeeefe2714cf99bb40cab7ce3012d74e1e2c1bd60d0a913b467b269edde6e176ca644b5ba03a5b865fb044a29bca05671cd445c85ca2cdc2de155d7fc8fe9b + languageName: node + linkType: hard + +"es5-ext@npm:^0.10.12, es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.46, es5-ext@npm:^0.10.47, es5-ext@npm:^0.10.49, es5-ext@npm:^0.10.50, es5-ext@npm:^0.10.53, es5-ext@npm:^0.10.61, es5-ext@npm:^0.10.62, es5-ext@npm:~0.10.14, es5-ext@npm:~0.10.2, es5-ext@npm:~0.10.46": + version: 0.10.62 + resolution: "es5-ext@npm:0.10.62" + dependencies: + es6-iterator: "npm:^2.0.3" + es6-symbol: "npm:^3.1.3" + next-tick: "npm:^1.1.0" + checksum: 10/3f6a3bcdb7ff82aaf65265799729828023c687a2645da04005b8f1dc6676a0c41fd06571b2517f89dcf143e0268d3d9ef0fdfd536ab74580083204c688d6fb45 + languageName: node + linkType: hard + +"es6-iterator@npm:^2.0.3, es6-iterator@npm:~2.0.3": + version: 2.0.3 + resolution: "es6-iterator@npm:2.0.3" + dependencies: + d: "npm:1" + es5-ext: "npm:^0.10.35" + es6-symbol: "npm:^3.1.1" + checksum: 10/dbadecf3d0e467692815c2b438dfa99e5a97cbbecf4a58720adcb467a04220e0e36282399ba297911fd472c50ae4158fffba7ed0b7d4273fe322b69d03f9e3a5 + languageName: node + linkType: hard + +"es6-set@npm:^0.1.6": + version: 0.1.6 + resolution: "es6-set@npm:0.1.6" + dependencies: + d: "npm:^1.0.1" + es5-ext: "npm:^0.10.62" + es6-iterator: "npm:~2.0.3" + es6-symbol: "npm:^3.1.3" + event-emitter: "npm:^0.3.5" + type: "npm:^2.7.2" + checksum: 10/45e8a4432edf71be7e0b7415ec434f62e294a6cb790646a5475b01ac13fda820141eab9fa7d18e91f4e5977bdf8d27d944123fafd15740a1c7f832a2caf45ba4 + languageName: node + linkType: hard + +"es6-symbol@npm:^3.1.1, es6-symbol@npm:^3.1.3": + version: 3.1.3 + resolution: "es6-symbol@npm:3.1.3" + dependencies: + d: "npm:^1.0.1" + ext: "npm:^1.1.2" + checksum: 10/b404e5ecae1a076058aa2ba2568d87e2cb4490cb1130784b84e7b4c09c570b487d4f58ed685a08db8d350bd4916500dd3d623b26e6b3520841d30d2ebb152f8d + languageName: node + linkType: hard + +"es6-weak-map@npm:^2.0.3": + version: 2.0.3 + resolution: "es6-weak-map@npm:2.0.3" + dependencies: + d: "npm:1" + es5-ext: "npm:^0.10.46" + es6-iterator: "npm:^2.0.3" + es6-symbol: "npm:^3.1.1" + checksum: 10/5958a321cf8dfadc82b79eeaa57dc855893a4afd062b4ef5c9ded0010d3932099311272965c3d3fdd3c85df1d7236013a570e704fa6c1f159bbf979c203dd3a3 + languageName: node + linkType: hard + +"escalade@npm:^3.1.1": + version: 3.1.1 + resolution: "escalade@npm:3.1.1" + checksum: 10/afa618e73362576b63f6ca83c975456621095a1ed42ff068174e3f5cea48afc422814dda548c96e6ebb5333e7265140c7292abcc81bbd6ccb1757d50d3a4e182 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": + version: 1.0.5 + resolution: "escape-string-regexp@npm:1.0.5" + checksum: 10/6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^2.0.0": + version: 2.0.0 + resolution: "escape-string-regexp@npm:2.0.0" + checksum: 10/9f8a2d5743677c16e85c810e3024d54f0c8dea6424fad3c79ef6666e81dd0846f7437f5e729dfcdac8981bc9e5294c39b4580814d114076b8d36318f46ae4395 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 10/98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 + languageName: node + linkType: hard + +"escodegen@npm:^1.13.0": + version: 1.14.3 + resolution: "escodegen@npm:1.14.3" + dependencies: + esprima: "npm:^4.0.1" + estraverse: "npm:^4.2.0" + esutils: "npm:^2.0.2" + optionator: "npm:^0.8.1" + source-map: "npm:~0.6.1" + dependenciesMeta: + source-map: + optional: true + bin: + escodegen: bin/escodegen.js + esgenerate: bin/esgenerate.js + checksum: 10/70f095ca9393535f9f1c145ef99dc0b3ff14cca6bc4a79d90ff3352f90c3f2e07f75af6d6c05174ea67c45271f75e80dd440dd7d04ed2cf44c9452c3042fa84a + languageName: node + linkType: hard + +"eslint-config-airbnb-base@npm:^14.2.1": + version: 14.2.1 + resolution: "eslint-config-airbnb-base@npm:14.2.1" + dependencies: + confusing-browser-globals: "npm:^1.0.10" + object.assign: "npm:^4.1.2" + object.entries: "npm:^1.1.2" + peerDependencies: + eslint: ^5.16.0 || ^6.8.0 || ^7.2.0 + eslint-plugin-import: ^2.22.1 + checksum: 10/0d679b6fe8030e18be9d5876bdf4d112988f9a1bc23fbb87a835447d448877041191caae6f9f656238bf5b883da8ea80199d6769075fe3493018c5e74d5fa0dd + languageName: node + linkType: hard + +"eslint-config-airbnb-base@npm:^15.0.0": + version: 15.0.0 + resolution: "eslint-config-airbnb-base@npm:15.0.0" + dependencies: + confusing-browser-globals: "npm:^1.0.10" + object.assign: "npm:^4.1.2" + object.entries: "npm:^1.1.5" + semver: "npm:^6.3.0" + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 + checksum: 10/daa68a1dcb7bff338747a952723b5fa9d159980ec3554c395a4b52a7f7d4f00a45e7b465420eb6d4d87a82cef6361e4cfd6dbb38c2f3f52f2140b6cf13654803 + languageName: node + linkType: hard + +"eslint-import-resolver-alias@npm:^1.1.2": + version: 1.1.2 + resolution: "eslint-import-resolver-alias@npm:1.1.2" + peerDependencies: + eslint-plugin-import: ">=1.4.0" + checksum: 10/3fbb9aeda98335060bb438ed8446a060d282f80a365838a82edb1f8743b1d54c89303009c7717e3c915d5d722e57148082c5ada4455e811acdc8ed3a65059fa1 + languageName: node + linkType: hard + +"eslint-import-resolver-node@npm:^0.3.7": + version: 0.3.9 + resolution: "eslint-import-resolver-node@npm:0.3.9" + dependencies: + debug: "npm:^3.2.7" + is-core-module: "npm:^2.13.0" + resolve: "npm:^1.22.4" + checksum: 10/d52e08e1d96cf630957272e4f2644dcfb531e49dcfd1edd2e07e43369eb2ec7a7d4423d417beee613201206ff2efa4eb9a582b5825ee28802fc7c71fcd53ca83 + languageName: node + linkType: hard + +"eslint-module-utils@npm:^2.8.0": + version: 2.8.0 + resolution: "eslint-module-utils@npm:2.8.0" + dependencies: + debug: "npm:^3.2.7" + peerDependenciesMeta: + eslint: + optional: true + checksum: 10/a9a7ed93eb858092e3cdc797357d4ead2b3ea06959b0eada31ab13862d46a59eb064b9cb82302214232e547980ce33618c2992f6821138a4934e65710ed9cc29 + languageName: node + linkType: hard + +"eslint-plugin-import@npm:^2.23.3": + version: 2.28.1 + resolution: "eslint-plugin-import@npm:2.28.1" + dependencies: + array-includes: "npm:^3.1.6" + array.prototype.findlastindex: "npm:^1.2.2" + array.prototype.flat: "npm:^1.3.1" + array.prototype.flatmap: "npm:^1.3.1" + debug: "npm:^3.2.7" + doctrine: "npm:^2.1.0" + eslint-import-resolver-node: "npm:^0.3.7" + eslint-module-utils: "npm:^2.8.0" + has: "npm:^1.0.3" + is-core-module: "npm:^2.13.0" + is-glob: "npm:^4.0.3" + minimatch: "npm:^3.1.2" + object.fromentries: "npm:^2.0.6" + object.groupby: "npm:^1.0.0" + object.values: "npm:^1.1.6" + semver: "npm:^6.3.1" + tsconfig-paths: "npm:^3.14.2" + peerDependencies: + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + checksum: 10/707dc97f06b12b0f3f91d5248dcea91bcd6a72c1168249a3ba177dd1ab6f31de9d5db829705236207a6ae79ad99a7a03efdfddb4a703da3a85530f9cc7401b2f + languageName: node + linkType: hard + +"eslint-plugin-jest@npm:^23.13.2": + version: 23.20.0 + resolution: "eslint-plugin-jest@npm:23.20.0" + dependencies: + "@typescript-eslint/experimental-utils": "npm:^2.5.0" + peerDependencies: + eslint: ">=5" + checksum: 10/e20d29cdf811bd67ed21716cc52dc5613c894a935a713636ce96cbbf069435c98946ab6d7be69d6dc4004f21ea63e1b335018df8869cb3d9d3b848861bde4f61 + languageName: node + linkType: hard + +"eslint-plugin-jest@npm:^27.4.0": + version: 27.4.2 + resolution: "eslint-plugin-jest@npm:27.4.2" + dependencies: + "@typescript-eslint/utils": "npm:^5.10.0" + peerDependencies: + "@typescript-eslint/eslint-plugin": ^5.0.0 || ^6.0.0 + eslint: ^7.0.0 || ^8.0.0 + jest: "*" + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + jest: + optional: true + checksum: 10/fee5d3f345fd54d5176af90285e634ae10160cddb35c4e88c6883cef43c8f63cf262661689c8e979f51daf107c3b4a81dbb019fe76e1e561d56d6a1f1f09554f + languageName: node + linkType: hard + +"eslint-plugin-module-resolver@npm:^0.16.0": + version: 0.16.0 + resolution: "eslint-plugin-module-resolver@npm:0.16.0" + dependencies: + find-babel-config: "npm:^1.2.0" + checksum: 10/c34c0bcfd5dd0a6f03737cb1fd10f73a82a828f946b1b13bcfc0cf5fee04d2773c99a552f95fe3d7098a2924b26a0b6d000e718bc7935cece85a6e9af9217ed2 + languageName: node + linkType: hard + +"eslint-scope@npm:5.1.1, eslint-scope@npm:^5.0.0, eslint-scope@npm:^5.1.1": + version: 5.1.1 + resolution: "eslint-scope@npm:5.1.1" + dependencies: + esrecurse: "npm:^4.3.0" + estraverse: "npm:^4.1.1" + checksum: 10/c541ef384c92eb5c999b7d3443d80195fcafb3da335500946f6db76539b87d5826c8f2e1d23bf6afc3154ba8cd7c8e566f8dc00f1eea25fdf3afc8fb9c87b238 + languageName: node + linkType: hard + +"eslint-scope@npm:^7.2.2": + version: 7.2.2 + resolution: "eslint-scope@npm:7.2.2" + dependencies: + esrecurse: "npm:^4.3.0" + estraverse: "npm:^5.2.0" + checksum: 10/5c660fb905d5883ad018a6fea2b49f3cb5b1cbf2cd4bd08e98646e9864f9bc2c74c0839bed2d292e90a4a328833accc197c8f0baed89cbe8d605d6f918465491 + languageName: node + linkType: hard + +"eslint-utils@npm:^2.0.0": + version: 2.1.0 + resolution: "eslint-utils@npm:2.1.0" + dependencies: + eslint-visitor-keys: "npm:^1.1.0" + checksum: 10/a7e43a5154a16a90c021cabeb160c3668cccbcf6474ccb2a7d7762698582398f3b938c5330909b858ef7c21182edfc9786dbf89ed7b294f51b7659a378bf7cec + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^1.1.0": + version: 1.3.0 + resolution: "eslint-visitor-keys@npm:1.3.0" + checksum: 10/595ab230e0fcb52f86ba0986a9a473b9fcae120f3729b43f1157f88f27f8addb1e545c4e3d444185f2980e281ca15be5ada6f65b4599eec227cf30e41233b762 + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": + version: 3.4.3 + resolution: "eslint-visitor-keys@npm:3.4.3" + checksum: 10/3f357c554a9ea794b094a09bd4187e5eacd1bc0d0653c3adeb87962c548e6a1ab8f982b86963ae1337f5d976004146536dcee5d0e2806665b193fbfbf1a9231b + languageName: node + linkType: hard + +"eslint@npm:^8.50.0": + version: 8.50.0 + resolution: "eslint@npm:8.50.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@eslint-community/regexpp": "npm:^4.6.1" + "@eslint/eslintrc": "npm:^2.1.2" + "@eslint/js": "npm:8.50.0" + "@humanwhocodes/config-array": "npm:^0.11.11" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@nodelib/fs.walk": "npm:^1.2.8" + ajv: "npm:^6.12.4" + chalk: "npm:^4.0.0" + cross-spawn: "npm:^7.0.2" + debug: "npm:^4.3.2" + doctrine: "npm:^3.0.0" + escape-string-regexp: "npm:^4.0.0" + eslint-scope: "npm:^7.2.2" + eslint-visitor-keys: "npm:^3.4.3" + espree: "npm:^9.6.1" + esquery: "npm:^1.4.2" + esutils: "npm:^2.0.2" + fast-deep-equal: "npm:^3.1.3" + file-entry-cache: "npm:^6.0.1" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + globals: "npm:^13.19.0" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.2.0" + imurmurhash: "npm:^0.1.4" + is-glob: "npm:^4.0.0" + is-path-inside: "npm:^3.0.3" + js-yaml: "npm:^4.1.0" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + levn: "npm:^0.4.1" + lodash.merge: "npm:^4.6.2" + minimatch: "npm:^3.1.2" + natural-compare: "npm:^1.4.0" + optionator: "npm:^0.9.3" + strip-ansi: "npm:^6.0.1" + text-table: "npm:^0.2.0" + bin: + eslint: bin/eslint.js + checksum: 10/181f26677a80f21431e68a469470485467a5c847d14d8822c1041efc52905772816546ca4e3fc87b963b7b267d8faf960322df16a30a57044161a32199b0dcfa + languageName: node + linkType: hard + +"esniff@npm:^1.1.0": + version: 1.1.0 + resolution: "esniff@npm:1.1.0" + dependencies: + d: "npm:1" + es5-ext: "npm:^0.10.12" + checksum: 10/7e4248c622c193047ff6eaf207267ae51b544de572068db4b061ea5cf6ce561be65492fe061b6dbc4ce879a3bc19eb8d19a7ab59032051876ad1eda51f37b103 + languageName: node + linkType: hard + +"espree@npm:^9.0.0, espree@npm:^9.6.0, espree@npm:^9.6.1": + version: 9.6.1 + resolution: "espree@npm:9.6.1" + dependencies: + acorn: "npm:^8.9.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^3.4.1" + checksum: 10/255ab260f0d711a54096bdeda93adff0eadf02a6f9b92f02b323e83a2b7fc258797919437ad331efec3930475feb0142c5ecaaf3cdab4befebd336d47d3f3134 + languageName: node + linkType: hard + +"esprima@npm:^4.0.0, esprima@npm:^4.0.1": + version: 4.0.1 + resolution: "esprima@npm:4.0.1" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: 10/f1d3c622ad992421362294f7acf866aa9409fbad4eb2e8fa230bd33944ce371d32279667b242d8b8907ec2b6ad7353a717f3c0e60e748873a34a7905174bc0eb + languageName: node + linkType: hard + +"esquery@npm:^1.4.2": + version: 1.5.0 + resolution: "esquery@npm:1.5.0" + dependencies: + estraverse: "npm:^5.1.0" + checksum: 10/e65fcdfc1e0ff5effbf50fb4f31ea20143ae5df92bb2e4953653d8d40aa4bc148e0d06117a592ce4ea53eeab1dafdfded7ea7e22a5be87e82d73757329a1b01d + languageName: node + linkType: hard + +"esrecurse@npm:^4.3.0": + version: 4.3.0 + resolution: "esrecurse@npm:4.3.0" + dependencies: + estraverse: "npm:^5.2.0" + checksum: 10/44ffcd89e714ea6b30143e7f119b104fc4d75e77ee913f34d59076b40ef2d21967f84e019f84e1fd0465b42cdbf725db449f232b5e47f29df29ed76194db8e16 + languageName: node + linkType: hard + +"essentials@npm:^1.2.0": + version: 1.2.0 + resolution: "essentials@npm:1.2.0" + dependencies: + uni-global: "npm:^1.0.0" + checksum: 10/a283d3150feb4258deaab3b3f3c638979f92944564753658ae2e77f130f1c98d69856c5b12056936c1aacba8e02581db9cef0cbb2ceb2394067733cea28b5119 + languageName: node + linkType: hard + +"estraverse@npm:^4.1.1, estraverse@npm:^4.2.0": + version: 4.3.0 + resolution: "estraverse@npm:4.3.0" + checksum: 10/3f67ad02b6dbfaddd9ea459cf2b6ef4ecff9a6082a7af9d22e445b9abc082ad9ca47e1825557b293fcdae477f4714e561123e30bb6a5b2f184fb2bad4a9497eb + languageName: node + linkType: hard + +"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0": + version: 5.3.0 + resolution: "estraverse@npm:5.3.0" + checksum: 10/37cbe6e9a68014d34dbdc039f90d0baf72436809d02edffcc06ba3c2a12eb298048f877511353b130153e532aac8d68ba78430c0dd2f44806ebc7c014b01585e + languageName: node + linkType: hard + +"esutils@npm:^2.0.2": + version: 2.0.3 + resolution: "esutils@npm:2.0.3" + checksum: 10/b23acd24791db11d8f65be5ea58fd9a6ce2df5120ae2da65c16cfc5331ff59d5ac4ef50af66cd4bde238881503ec839928a0135b99a036a9cdfa22d17fd56cdb + languageName: node + linkType: hard + +"event-emitter@npm:^0.3.5": + version: 0.3.5 + resolution: "event-emitter@npm:0.3.5" + dependencies: + d: "npm:1" + es5-ext: "npm:~0.10.14" + checksum: 10/a7f5ea80029193f4869782d34ef7eb43baa49cd397013add1953491b24588468efbe7e3cc9eb87d53f33397e7aab690fd74c079ec440bf8b12856f6bdb6e9396 + languageName: node + linkType: hard + +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 10/49ff46c3a7facbad3decb31f597063e761785d7fdb3920d4989d7b08c97a61c2f51183e2f3a03130c9088df88d4b489b1b79ab632219901f184f85158508f4c8 + languageName: node + linkType: hard + +"events@npm:1.1.1": + version: 1.1.1 + resolution: "events@npm:1.1.1" + checksum: 10/524355c4364b4851d53ccf4fdab9570e3953e1f64ebca15554f33e50bebb4e71ab947ac0dee6f4ed5a567ff2eda54b0489b278b4fb7c8ec1f4982150079dfd40 + languageName: node + linkType: hard + +"events@npm:^3.2.0": + version: 3.3.0 + resolution: "events@npm:3.3.0" + checksum: 10/a3d47e285e28d324d7180f1e493961a2bbb4cad6412090e4dec114f4db1f5b560c7696ee8e758f55e23913ede856e3689cd3aa9ae13c56b5d8314cd3b3ddd1be + languageName: node + linkType: hard + +"evp_bytestokey@npm:^1.0.3": + version: 1.0.3 + resolution: "evp_bytestokey@npm:1.0.3" + dependencies: + md5.js: "npm:^1.3.4" + node-gyp: "npm:latest" + safe-buffer: "npm:^5.1.1" + checksum: 10/ad4e1577f1a6b721c7800dcc7c733fe01f6c310732bb5bf2240245c2a5b45a38518b91d8be2c610611623160b9d1c0e91f1ce96d639f8b53e8894625cf20fa45 + languageName: node + linkType: hard + +"execa@npm:^5.0.0": + version: 5.1.1 + resolution: "execa@npm:5.1.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^6.0.0" + human-signals: "npm:^2.1.0" + is-stream: "npm:^2.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^4.0.1" + onetime: "npm:^5.1.2" + signal-exit: "npm:^3.0.3" + strip-final-newline: "npm:^2.0.0" + checksum: 10/8ada91f2d70f7dff702c861c2c64f21dfdc1525628f3c0454fd6f02fce65f7b958616cbd2b99ca7fa4d474e461a3d363824e91b3eb881705231abbf387470597 + languageName: node + linkType: hard + +"execa@npm:^8.0.1": + version: 8.0.1 + resolution: "execa@npm:8.0.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^8.0.1" + human-signals: "npm:^5.0.0" + is-stream: "npm:^3.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^5.1.0" + onetime: "npm:^6.0.0" + signal-exit: "npm:^4.1.0" + strip-final-newline: "npm:^3.0.0" + checksum: 10/d2ab5fe1e2bb92b9788864d0713f1fce9a07c4594e272c0c97bc18c90569897ab262e4ea58d27a694d288227a2e24f16f5e2575b44224ad9983b799dc7f1098d + languageName: node + linkType: hard + +"exit@npm:^0.1.2": + version: 0.1.2 + resolution: "exit@npm:0.1.2" + checksum: 10/387555050c5b3c10e7a9e8df5f43194e95d7737c74532c409910e585d5554eaff34960c166643f5e23d042196529daad059c292dcf1fb61b8ca878d3677f4b87 + languageName: node + linkType: hard + +"expect@npm:^29.0.0, expect@npm:^29.7.0": + version: 29.7.0 + resolution: "expect@npm:29.7.0" + dependencies: + "@jest/expect-utils": "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10/63f97bc51f56a491950fb525f9ad94f1916e8a014947f8d8445d3847a665b5471b768522d659f5e865db20b6c2033d2ac10f35fcbd881a4d26407a4f6f18451a + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 10/2d9bbb6473de7051f96790d5f9a678f32e60ed0aa70741dc7fdc96fec8d631124ec3374ac144387604f05afff9500f31a1d45bd9eee4cdc2e4f9ad2d9b9d5dbd + languageName: node + linkType: hard + +"ext-list@npm:^2.0.0": + version: 2.2.2 + resolution: "ext-list@npm:2.2.2" + dependencies: + mime-db: "npm:^1.28.0" + checksum: 10/fe69fedbef044e14d4ce9e84c6afceb696ba71500c15b8d0ce0a1e280237e17c95031b3d62d5e597652fea0065b9bf957346b3900d989dff59128222231ac859 + languageName: node + linkType: hard + +"ext-name@npm:^5.0.0": + version: 5.0.0 + resolution: "ext-name@npm:5.0.0" + dependencies: + ext-list: "npm:^2.0.0" + sort-keys-length: "npm:^1.0.0" + checksum: 10/f598269bd5de4295540ea7d6f8f6a01d82a7508f148b7700a05628ef6121648d26e6e5e942049e953b3051863df6b54bd8fe951e7877f185e34ace5d44370b33 + languageName: node + linkType: hard + +"ext@npm:^1.1.2, ext@npm:^1.4.0, ext@npm:^1.6.0, ext@npm:^1.7.0": + version: 1.7.0 + resolution: "ext@npm:1.7.0" + dependencies: + type: "npm:^2.7.2" + checksum: 10/666a135980b002df0e75c8ac6c389140cdc59ac953db62770479ee2856d58ce69d2f845e5f2586716350b725400f6945e51e9159573158c39f369984c72dcd84 + languageName: node + linkType: hard + +"extend@npm:^3.0.2": + version: 3.0.2 + resolution: "extend@npm:3.0.2" + checksum: 10/59e89e2dc798ec0f54b36d82f32a27d5f6472c53974f61ca098db5d4648430b725387b53449a34df38fd0392045434426b012f302b3cc049a6500ccf82877e4e + languageName: node + linkType: hard + +"external-editor@npm:^3.0.3": + version: 3.1.0 + resolution: "external-editor@npm:3.1.0" + dependencies: + chardet: "npm:^0.7.0" + iconv-lite: "npm:^0.4.24" + tmp: "npm:^0.0.33" + checksum: 10/776dff1d64a1d28f77ff93e9e75421a81c062983fd1544279d0a32f563c0b18c52abbb211f31262e2827e48edef5c9dc8f960d06dd2d42d1654443b88568056b + languageName: node + linkType: hard + +"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": + version: 3.1.3 + resolution: "fast-deep-equal@npm:3.1.3" + checksum: 10/e21a9d8d84f53493b6aa15efc9cfd53dd5b714a1f23f67fb5dc8f574af80df889b3bce25dc081887c6d25457cce704e636395333abad896ccdec03abaf1f3f9d + languageName: node + linkType: hard + +"fast-glob@npm:^3.2.7, fast-glob@npm:^3.2.9": + version: 3.3.1 + resolution: "fast-glob@npm:3.3.1" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.4" + checksum: 10/51bcd15472879dfe51d4b01c5b70bbc7652724d39cdd082ba11276dbd7d84db0f6b33757e1938af8b2768a4bf485d9be0c89153beae24ee8331d6dcc7550379f + languageName: node + linkType: hard + +"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": + version: 2.1.0 + resolution: "fast-json-stable-stringify@npm:2.1.0" + checksum: 10/2c20055c1fa43c922428f16ca8bb29f2807de63e5c851f665f7ac9790176c01c3b40335257736b299764a8d383388dabc73c8083b8e1bc3d99f0a941444ec60e + languageName: node + linkType: hard + +"fast-levenshtein@npm:^2.0.6, fast-levenshtein@npm:~2.0.6": + version: 2.0.6 + resolution: "fast-levenshtein@npm:2.0.6" + checksum: 10/eb7e220ecf2bab5159d157350b81d01f75726a4382f5a9266f42b9150c4523b9795f7f5d9fbbbeaeac09a441b2369f05ee02db48ea938584205530fe5693cfe1 + languageName: node + linkType: hard + +"fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 + languageName: node + linkType: hard + +"fast-text-encoding@npm:^1.0.0, fast-text-encoding@npm:^1.0.3": + version: 1.0.6 + resolution: "fast-text-encoding@npm:1.0.6" + checksum: 10/f7b9e2e7a21e4ae5f4b8d3729850be83fb45052b28c9c38c09b8366463a291d6dc5448359238bdaf87f6a9e907d5895a94319a2c5e0e9f0786859ad6312d1d06 + languageName: node + linkType: hard + +"fast-xml-parser@npm:4.2.5": + version: 4.2.5 + resolution: "fast-xml-parser@npm:4.2.5" + dependencies: + strnum: "npm:^1.0.5" + bin: + fxparser: src/cli/cli.js + checksum: 10/4be7ebe24d6a9a60c278e1423cd86a7da9a77ec64c95563e2c552363caf7a777e0c87c9de1255c2f4e8dea9bce8905dc2bdc58a34e9f2b73c4693654456ad284 + languageName: node + linkType: hard + +"fast-xml-parser@npm:^4.2.2": + version: 4.3.2 + resolution: "fast-xml-parser@npm:4.3.2" + dependencies: + strnum: "npm:^1.0.5" + bin: + fxparser: src/cli/cli.js + checksum: 10/cb3d9ad7d5508e7ec1e6ee4b4753f659c7b7c93c3eb76439cb03072532d07521d53a7e35f243b490dce3fcc16519415bf1f99c6a1004a6de1dccd3d3647c336f + languageName: node + linkType: hard + +"fastest-levenshtein@npm:^1.0.16": + version: 1.0.16 + resolution: "fastest-levenshtein@npm:1.0.16" + checksum: 10/ee85d33b5cef592033f70e1c13ae8624055950b4eb832435099cd56aa313d7f251b873bedbc06a517adfaff7b31756d139535991e2406967438e03a1bf1b008e + languageName: node + linkType: hard + +"fastq@npm:^1.6.0": + version: 1.15.0 + resolution: "fastq@npm:1.15.0" + dependencies: + reusify: "npm:^1.0.4" + checksum: 10/67c01b1c972e2d5b6fea197a1a39d5d582982aea69ff4c504badac71080d8396d4843b165a9686e907c233048f15a86bbccb0e7f83ba771f6fa24bcde059d0c3 + languageName: node + linkType: hard + +"faye-websocket@npm:0.11.4": + version: 0.11.4 + resolution: "faye-websocket@npm:0.11.4" + dependencies: + websocket-driver: "npm:>=0.5.1" + checksum: 10/22433c14c60925e424332d2794463a8da1c04848539b5f8db5fced62a7a7c71a25335a4a8b37334e3a32318835e2b87b1733d008561964121c4a0bd55f0878c3 + languageName: node + linkType: hard + +"fb-watchman@npm:^2.0.0": + version: 2.0.2 + resolution: "fb-watchman@npm:2.0.2" + dependencies: + bser: "npm:2.1.1" + checksum: 10/4f95d336fb805786759e383fd7fff342ceb7680f53efcc0ef82f502eb479ce35b98e8b207b6dfdfeea0eba845862107dc73813775fc6b56b3098c6e90a2dad77 + languageName: node + linkType: hard + +"fd-slicer@npm:~1.1.0": + version: 1.1.0 + resolution: "fd-slicer@npm:1.1.0" + dependencies: + pend: "npm:~1.2.0" + checksum: 10/db3e34fa483b5873b73f248e818f8a8b59a6427fd8b1436cd439c195fdf11e8659419404826059a642b57d18075c856d06d6a50a1413b714f12f833a9341ead3 + languageName: node + linkType: hard + +"fecha@npm:^4.2.0": + version: 4.2.3 + resolution: "fecha@npm:4.2.3" + checksum: 10/534ce630c8f63c116292145607fc18c0f06bfa2fd74094357bf65daacc5d3f4f2b285bf8eb112c3bbf98c5caa6d386cced797f44b9b1b33da0c0a81020444826 + languageName: node + linkType: hard + +"figures@npm:^3.0.0": + version: 3.2.0 + resolution: "figures@npm:3.2.0" + dependencies: + escape-string-regexp: "npm:^1.0.5" + checksum: 10/a3bf94e001be51d3770500789157f067218d4bc681a65e1f69d482de15120bcac822dceb1a7b3803f32e4e3a61a46df44f7f2c8ba95d6375e7491502e0dd3d97 + languageName: node + linkType: hard + +"file-entry-cache@npm:^6.0.1": + version: 6.0.1 + resolution: "file-entry-cache@npm:6.0.1" + dependencies: + flat-cache: "npm:^3.0.4" + checksum: 10/099bb9d4ab332cb93c48b14807a6918a1da87c45dce91d4b61fd40e6505d56d0697da060cb901c729c90487067d93c9243f5da3dc9c41f0358483bfdebca736b + languageName: node + linkType: hard + +"file-type@npm:^16.5.4": + version: 16.5.4 + resolution: "file-type@npm:16.5.4" + dependencies: + readable-web-to-node-stream: "npm:^3.0.0" + strtok3: "npm:^6.2.4" + token-types: "npm:^4.1.1" + checksum: 10/46ced46bb925ab547e0a6d43108a26d043619d234cb0588d7abce7b578dafac142bcfd2e23a6adb0a4faa4b951bd1b14b355134a193362e07cd352f9bf0dc349 + languageName: node + linkType: hard + +"file-type@npm:^3.8.0": + version: 3.9.0 + resolution: "file-type@npm:3.9.0" + checksum: 10/1c8bc99bbb9cfcf13d3489e0c0250188dde622658b5a990f2ba09e6c784f183556b37b7de22104b4b0fd87f478ce12f8dc199b988616ce7cdcb41248dc0a79f9 + languageName: node + linkType: hard + +"file-type@npm:^4.2.0": + version: 4.4.0 + resolution: "file-type@npm:4.4.0" + checksum: 10/92b417a5c736ee972ba34e6a67413a6e7a3b652a624861beb5c6ace748eb684904b59712a250ac79f807d9928ba5980188bff1d8e853a72e43fb27ad340e19b2 + languageName: node + linkType: hard + +"file-type@npm:^5.2.0": + version: 5.2.0 + resolution: "file-type@npm:5.2.0" + checksum: 10/73b44eaba7a3e0684d35f24bb3f98ea8a943bf897e103768371b747b0714618301411e66ceff717c866db780af6f5bb1a3da15b744c2e04fa83d605a0682b72b + languageName: node + linkType: hard + +"file-type@npm:^6.1.0": + version: 6.2.0 + resolution: "file-type@npm:6.2.0" + checksum: 10/c7214c3cf6c72a4ed02b473a792841b4bf626a8e95bb010bd8679016b86e5bf52117264c3133735a8424bfde378c3a39b90e1f4902f5f294c41de4e81ec85fdc + languageName: node + linkType: hard + +"file-uri-to-path@npm:1.0.0": + version: 1.0.0 + resolution: "file-uri-to-path@npm:1.0.0" + checksum: 10/b648580bdd893a008c92c7ecc96c3ee57a5e7b6c4c18a9a09b44fb5d36d79146f8e442578bc0e173dc027adf3987e254ba1dfd6e3ec998b7c282873010502144 + languageName: node + linkType: hard + +"filename-reserved-regex@npm:^2.0.0": + version: 2.0.0 + resolution: "filename-reserved-regex@npm:2.0.0" + checksum: 10/9322b45726b86c45d0b4fe91be5c51e62b2e7e63db02c4a6ff3fd499bbc134d12fbf3c8b91979440ef45b3be834698ab9c3e66cb63b79fea4817e33da237d32a + languageName: node + linkType: hard + +"filenamify@npm:^4.3.0": + version: 4.3.0 + resolution: "filenamify@npm:4.3.0" + dependencies: + filename-reserved-regex: "npm:^2.0.0" + strip-outer: "npm:^1.0.1" + trim-repeated: "npm:^1.0.0" + checksum: 10/5b71a7ff8e958c8621957e6fbf7872024126d3b5da50f59b1634af3343ba1a69d4cc15cfe4ca4bbfa7c959ad4d98614ee51e6f1d9fa7326eef8ceda2da8cd74e + languageName: node + linkType: hard + +"filesize@npm:^10.0.7": + version: 10.1.0 + resolution: "filesize@npm:10.1.0" + checksum: 10/e8096a9dd639788d623e4b1c46d6710379d72f5646dbe71dfe0d423c0aa70b549b3b4d8cdd6d18a04d7068f4c257be819e6b1b83ac783d394ba4a3829e611959 + languageName: node + linkType: hard + +"fill-range@npm:^7.0.1": + version: 7.0.1 + resolution: "fill-range@npm:7.0.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: 10/e260f7592fd196b4421504d3597cc76f4a1ca7a9488260d533b611fc3cefd61e9a9be1417cb82d3b01ad9f9c0ff2dbf258e1026d2445e26b0cf5148ff4250429 + languageName: node + linkType: hard + +"find-babel-config@npm:^1.2.0": + version: 1.2.0 + resolution: "find-babel-config@npm:1.2.0" + dependencies: + json5: "npm:^0.5.1" + path-exists: "npm:^3.0.0" + checksum: 10/0dfbb7b2e4fbf90ee1fb275a2454b5f054bf192edb9c9813a769ead8fa1c89fa6d39025bc75c1e2616a438cca07f9b1351fa211a1539fd1dc8edd8c511e64fba + languageName: node + linkType: hard + +"find-requires@npm:^1.0.0": + version: 1.0.0 + resolution: "find-requires@npm:1.0.0" + dependencies: + es5-ext: "npm:^0.10.49" + esniff: "npm:^1.1.0" + bin: + find-requires: ./bin/find-requires.js + checksum: 10/02c2a35da7cfbdc38ea6e3e130aa5d11c2fdd82f83fe13ecc5f906305d05da4af066545e89b4ed9b773a6632e7a83a238ba8bdd89f2825442751329dee5d5e24 + languageName: node + linkType: hard + +"find-up@npm:^4.0.0, find-up@npm:^4.1.0": + version: 4.1.0 + resolution: "find-up@npm:4.1.0" + dependencies: + locate-path: "npm:^5.0.0" + path-exists: "npm:^4.0.0" + checksum: 10/4c172680e8f8c1f78839486e14a43ef82e9decd0e74145f40707cc42e7420506d5ec92d9a11c22bd2c48fb0c384ea05dd30e10dd152fefeec6f2f75282a8b844 + languageName: node + linkType: hard + +"find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" + dependencies: + locate-path: "npm:^6.0.0" + path-exists: "npm:^4.0.0" + checksum: 10/07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 + languageName: node + linkType: hard + +"find-yarn-workspace-root@npm:^2.0.0": + version: 2.0.0 + resolution: "find-yarn-workspace-root@npm:2.0.0" + dependencies: + micromatch: "npm:^4.0.2" + checksum: 10/7fa7942849eef4d5385ee96a0a9a5a9afe885836fd72ed6a4280312a38690afea275e7d09b343fe97daf0412d833f8ac4b78c17fc756386d9ebebf0759d707a7 + languageName: node + linkType: hard + +"firebase-admin@npm:^11.3.0": + version: 11.11.0 + resolution: "firebase-admin@npm:11.11.0" + dependencies: + "@fastify/busboy": "npm:^1.2.1" + "@firebase/database-compat": "npm:^0.3.4" + "@firebase/database-types": "npm:^0.10.4" + "@google-cloud/firestore": "npm:^6.6.0" + "@google-cloud/storage": "npm:^6.9.5" + "@types/node": "npm:>=12.12.47" + jsonwebtoken: "npm:^9.0.0" + jwks-rsa: "npm:^3.0.1" + node-forge: "npm:^1.3.1" + uuid: "npm:^9.0.0" + dependenciesMeta: + "@google-cloud/firestore": + optional: true + "@google-cloud/storage": + optional: true + checksum: 10/74f8592589c9d31b5ce14c117911df812123924c7ff06f1e0e744c85a78f54448aaefd9fd2642cf22d1b928b3161c724b1a1041e2deb5502dea16feb7f32c4e7 + languageName: node + linkType: hard + +"flat-cache@npm:^3.0.4": + version: 3.1.0 + resolution: "flat-cache@npm:3.1.0" + dependencies: + flatted: "npm:^3.2.7" + keyv: "npm:^4.5.3" + rimraf: "npm:^3.0.2" + checksum: 10/0367e6dbe0684e4b723d9aeb603d3dd225776638ed64fba6d089dc9b107aa03fb9248f1b9a128f32299a0067d6b8c7640219063b34f84c5318d06211e863a83a + languageName: node + linkType: hard + +"flat@npm:^5.0.2": + version: 5.0.2 + resolution: "flat@npm:5.0.2" + bin: + flat: cli.js + checksum: 10/72479e651c15eab53e25ce04c31bab18cfaac0556505cac19221dbbe85bbb9686bc76e4d397e89e5bf516ce667dcf818f8b07e585568edba55abc2bf1f698fb5 + languageName: node + linkType: hard + +"flatted@npm:^3.2.7": + version: 3.2.9 + resolution: "flatted@npm:3.2.9" + checksum: 10/dc2b89e46a2ebde487199de5a4fcb79e8c46f984043fea5c41dbf4661eb881fefac1c939b5bdcd8a09d7f960ec364f516970c7ec44e58ff451239c07fd3d419b + languageName: node + linkType: hard + +"fn.name@npm:1.x.x": + version: 1.1.0 + resolution: "fn.name@npm:1.1.0" + checksum: 10/000198af190ae02f0138ac5fa4310da733224c628e0230c81e3fff7c4e094af7e0e8bb9f4357cabd21db601759d89f3445da744afbae20623cfa41edf3888397 + languageName: node + linkType: hard + +"follow-redirects@npm:1.5.10": + version: 1.5.10 + resolution: "follow-redirects@npm:1.5.10" + dependencies: + debug: "npm:=3.1.0" + checksum: 10/7cf3bb10dbffabce317d1de3e3f85ea8c47fea388c4bc3da1052fd6bbb7b036772f48eed2f154d63ac39c49ab5fe4df0dda94b8e0c54eda8ca2edb001b2550b7 + languageName: node + linkType: hard + +"follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.15.0": + version: 1.15.3 + resolution: "follow-redirects@npm:1.15.3" + peerDependenciesMeta: + debug: + optional: true + checksum: 10/60d98693f4976892f8c654b16ef6d1803887a951898857ab0cdc009570b1c06314ad499505b7a040ac5b98144939f8597766e5e6a6859c0945d157b473aa6f5f + languageName: node + linkType: hard + +"for-each@npm:^0.3.3": + version: 0.3.3 + resolution: "for-each@npm:0.3.3" + dependencies: + is-callable: "npm:^1.1.3" + checksum: 10/fdac0cde1be35610bd635ae958422e8ce0cc1313e8d32ea6d34cfda7b60850940c1fd07c36456ad76bd9c24aef6ff5e03b02beb58c83af5ef6c968a64eada676 + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^4.0.1" + checksum: 10/087edd44857d258c4f73ad84cb8df980826569656f2550c341b27adf5335354393eec24ea2fabd43a253233fb27cee177ebe46bd0b7ea129c77e87cb1e9936fb + languageName: node + linkType: hard + +"fork-ts-checker-webpack-plugin@npm:^9.0.0": + version: 9.0.0 + resolution: "fork-ts-checker-webpack-plugin@npm:9.0.0" + dependencies: + "@babel/code-frame": "npm:^7.16.7" + chalk: "npm:^4.1.2" + chokidar: "npm:^3.5.3" + cosmiconfig: "npm:^7.0.1" + deepmerge: "npm:^4.2.2" + fs-extra: "npm:^10.0.0" + memfs: "npm:^3.4.1" + minimatch: "npm:^3.0.4" + node-abort-controller: "npm:^3.0.1" + schema-utils: "npm:^3.1.1" + semver: "npm:^7.3.5" + tapable: "npm:^2.2.1" + peerDependencies: + typescript: ">3.6.0" + webpack: ^5.11.0 + checksum: 10/f76b232cebc009b3087cd971d81697383578af3d293f70dde3148e67d212f4dfd5f1205b843686d7d1977340361b8e2bb7999b69ed1d82b7e16688656a3fca00 + languageName: node + linkType: hard + +"form-data@npm:^4.0.0": + version: 4.0.0 + resolution: "form-data@npm:4.0.0" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + mime-types: "npm:^2.1.12" + checksum: 10/7264aa760a8cf09482816d8300f1b6e2423de1b02bba612a136857413fdc96d7178298ced106817655facc6b89036c6e12ae31c9eb5bdc16aabf502ae8a5d805 + languageName: node + linkType: hard + +"formidable@npm:^2.0.1": + version: 2.1.2 + resolution: "formidable@npm:2.1.2" + dependencies: + dezalgo: "npm:^1.0.4" + hexoid: "npm:^1.0.0" + once: "npm:^1.4.0" + qs: "npm:^6.11.0" + checksum: 10/d385180e0461f65e6f7b70452859fe1c32aa97a290c2ca33f00cdc33145ef44fa68bbc9b93af2c3af73ae726e42c3477c6619c49f3c34b49934e9481275b7b4c + languageName: node + linkType: hard + +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 10/18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d + languageName: node + linkType: hard + +"fs-extra@npm:^10.0.0, fs-extra@npm:^10.1.0": + version: 10.1.0 + resolution: "fs-extra@npm:10.1.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/05ce2c3b59049bcb7b52001acd000e44b3c4af4ec1f8839f383ef41ec0048e3cfa7fd8a637b1bddfefad319145db89be91f4b7c1db2908205d38bf91e7d1d3b7 + languageName: node + linkType: hard + +"fs-extra@npm:^11.1.1": + version: 11.1.1 + resolution: "fs-extra@npm:11.1.1" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/c4e9fabf9762a70d1403316b7faa899f3d3303c8afa765b891c2210fdeba368461e04ae1203920b64ef6a7d066a39ab8cef2160b5ce8d1011bb4368688cd9bb7 + languageName: node + linkType: hard + +"fs-extra@npm:^9.0.1, fs-extra@npm:^9.1.0": + version: 9.1.0 + resolution: "fs-extra@npm:9.1.0" + dependencies: + at-least-node: "npm:^1.0.0" + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/08600da1b49552ed23dfac598c8fc909c66776dd130fea54fbcad22e330f7fcc13488bb995f6bc9ce5651aa35b65702faf616fe76370ee56f1aade55da982dca + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10/03191781e94bc9a54bd376d3146f90fe8e082627c502185dbf7b9b3032f66b0b142c1115f3b2cc5936575fc1b44845ce903dd4c21bec2a8d69f3bd56f9cee9ec + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10/af143246cf6884fe26fa281621d45cfe111d34b30535a475bfa38dafe343dadb466c047a924ffc7d6b7b18265df4110224ce3803806dbb07173bf2087b648d7f + languageName: node + linkType: hard + +"fs-monkey@npm:^1.0.4": + version: 1.0.5 + resolution: "fs-monkey@npm:1.0.5" + checksum: 10/7fcdf9267006800d61f1722cf9fa92ed8be8b3ed86614f6d43ab6f87a30f13bc784020465e20728ca4ea65ea7377bfcdbde52b54bf8c3cc2f43a6d62270ebf64 + languageName: node + linkType: hard + +"fs.realpath@npm:^1.0.0": + version: 1.0.0 + resolution: "fs.realpath@npm:1.0.0" + checksum: 10/e703107c28e362d8d7b910bbcbfd371e640a3bb45ae157a362b5952c0030c0b6d4981140ec319b347bce7adc025dd7813da1ff908a945ac214d64f5402a51b96 + languageName: node + linkType: hard + +"fs2@npm:^0.3.9": + version: 0.3.9 + resolution: "fs2@npm:0.3.9" + dependencies: + d: "npm:^1.0.1" + deferred: "npm:^0.7.11" + es5-ext: "npm:^0.10.53" + event-emitter: "npm:^0.3.5" + ignore: "npm:^5.1.8" + memoizee: "npm:^0.4.14" + type: "npm:^2.1.0" + checksum: 10/d9b7bd8433f5fc7ccf709d9636996eea80fa4df85a673428f1f7e9fd62ce0a362b29062fbc188c977a0d3cfb18389bb191be8aa9f3476ad1a3b6c6ae88060cc7 + languageName: node + linkType: hard + +"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10/4c1ade961ded57cdbfbb5cac5106ec17bc8bccd62e16343c569a0ceeca83b9dfef87550b4dc5cbb89642da412b20c5071f304c8c464b80415446e8e155a038c0 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"function-bind@npm:^1.1.1": + version: 1.1.1 + resolution: "function-bind@npm:1.1.1" + checksum: 10/d83f2968030678f0b8c3f2183d63dcd969344eb8b55b4eb826a94ccac6de8b87c95bebffda37a6386c74f152284eb02956ff2c496897f35d32bdc2628ac68ac5 + languageName: node + linkType: hard + +"function.prototype.name@npm:^1.1.6": + version: 1.1.6 + resolution: "function.prototype.name@npm:1.1.6" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + functions-have-names: "npm:^1.2.3" + checksum: 10/4d40be44d4609942e4e90c4fff77a811fa936f4985d92d2abfcf44f673ba344e2962bf223a33101f79c1a056465f36f09b072b9c289d7660ca554a12491cd5a2 + languageName: node + linkType: hard + +"functional-red-black-tree@npm:^1.0.1": + version: 1.0.1 + resolution: "functional-red-black-tree@npm:1.0.1" + checksum: 10/debe73e92204341d1fa5f89614e44284d3add26dee660722978d8c50829170f87d1c74768f68c251d215ae461c11db7bac13101c77f4146ff051da75466f7a12 + languageName: node + linkType: hard + +"functions-have-names@npm:^1.2.3": + version: 1.2.3 + resolution: "functions-have-names@npm:1.2.3" + checksum: 10/0ddfd3ed1066a55984aaecebf5419fbd9344a5c38dd120ffb0739fac4496758dcf371297440528b115e4367fc46e3abc86a2cc0ff44612181b175ae967a11a05 + languageName: node + linkType: hard + +"gauge@npm:^3.0.0": + version: 3.0.2 + resolution: "gauge@npm:3.0.2" + dependencies: + aproba: "npm:^1.0.3 || ^2.0.0" + color-support: "npm:^1.1.2" + console-control-strings: "npm:^1.0.0" + has-unicode: "npm:^2.0.1" + object-assign: "npm:^4.1.1" + signal-exit: "npm:^3.0.0" + string-width: "npm:^4.2.3" + strip-ansi: "npm:^6.0.1" + wide-align: "npm:^1.1.2" + checksum: 10/46df086451672a5fecd58f7ec86da74542c795f8e00153fbef2884286ce0e86653c3eb23be2d0abb0c4a82b9b2a9dec3b09b6a1cf31c28085fa0376599a26589 + languageName: node + linkType: hard + +"gauge@npm:^4.0.3": + version: 4.0.4 + resolution: "gauge@npm:4.0.4" + dependencies: + aproba: "npm:^1.0.3 || ^2.0.0" + color-support: "npm:^1.1.3" + console-control-strings: "npm:^1.1.0" + has-unicode: "npm:^2.0.1" + signal-exit: "npm:^3.0.7" + string-width: "npm:^4.2.3" + strip-ansi: "npm:^6.0.1" + wide-align: "npm:^1.1.5" + checksum: 10/09535dd53b5ced6a34482b1fa9f3929efdeac02f9858569cde73cef3ed95050e0f3d095706c1689614059898924b7a74aa14042f51381a1ccc4ee5c29d2389c4 + languageName: node + linkType: hard + +"gaxios@npm:^5.0.0, gaxios@npm:^5.0.1": + version: 5.1.3 + resolution: "gaxios@npm:5.1.3" + dependencies: + extend: "npm:^3.0.2" + https-proxy-agent: "npm:^5.0.0" + is-stream: "npm:^2.0.0" + node-fetch: "npm:^2.6.9" + checksum: 10/62d1e1901042b25b50e28e229c220e8908a0af3a7d5ff48ab6e3dd6ec4bfce6efad4f363e3668a873b1d2f06d15c69ef6ee2b6a286d558ffc2d96c32ce3cf333 + languageName: node + linkType: hard + +"gcp-metadata@npm:^5.3.0": + version: 5.3.0 + resolution: "gcp-metadata@npm:5.3.0" + dependencies: + gaxios: "npm:^5.0.0" + json-bigint: "npm:^1.0.0" + checksum: 10/ec2c32bd74ef6bfab9533612ef5ece328cb4a550e7523a20245e5eae6a9a74a68ab38ab37d1c1218d893fc39a08f590cef29c98d9a9e622db4fee39d3e1d396b + languageName: node + linkType: hard + +"generate-function@npm:^2.3.1": + version: 2.3.1 + resolution: "generate-function@npm:2.3.1" + dependencies: + is-property: "npm:^1.0.2" + checksum: 10/318f85af87c3258d86df4ebbb56b63a2ae52e71bd6cde8d0a79de09450de7422a7047fb1f8d52ccc135564a36cb986d73c63149eed96b7ac57e38acba44f29e2 + languageName: node + linkType: hard + +"gensync@npm:^1.0.0-beta.2": + version: 1.0.0-beta.2 + resolution: "gensync@npm:1.0.0-beta.2" + checksum: 10/17d8333460204fbf1f9160d067e1e77f908a5447febb49424b8ab043026049835c9ef3974445c57dbd39161f4d2b04356d7de12b2eecaa27a7a7ea7d871cbedd + languageName: node + linkType: hard + +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10/b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1": + version: 1.2.1 + resolution: "get-intrinsic@npm:1.2.1" + dependencies: + function-bind: "npm:^1.1.1" + has: "npm:^1.0.3" + has-proto: "npm:^1.0.1" + has-symbols: "npm:^1.0.3" + checksum: 10/aee631852063f8ad0d4a374970694b5c17c2fb5c92bd1929476d7eb8798ce7aebafbf9a34022c05fd1adaa2ce846d5877a627ce1986f81fc65adf3b81824bd54 + languageName: node + linkType: hard + +"get-package-type@npm:^0.1.0": + version: 0.1.0 + resolution: "get-package-type@npm:0.1.0" + checksum: 10/bba0811116d11e56d702682ddef7c73ba3481f114590e705fc549f4d868972263896af313c57a25c076e3c0d567e11d919a64ba1b30c879be985fc9d44f96148 + languageName: node + linkType: hard + +"get-stdin@npm:^8.0.0": + version: 8.0.0 + resolution: "get-stdin@npm:8.0.0" + checksum: 10/40128b6cd25781ddbd233344f1a1e4006d4284906191ed0a7d55ec2c1a3e44d650f280b2c9eeab79c03ac3037da80257476c0e4e5af38ddfb902d6ff06282d77 + languageName: node + linkType: hard + +"get-stream@npm:^2.2.0": + version: 2.3.1 + resolution: "get-stream@npm:2.3.1" + dependencies: + object-assign: "npm:^4.0.1" + pinkie-promise: "npm:^2.0.0" + checksum: 10/712738e6a39b06da774aea5d35efa16a8f067a0d93b1b564e8d0e733fafddcf021e03098895735bc45d6594d3094369d700daa0d33891f980595cf6495e33294 + languageName: node + linkType: hard + +"get-stream@npm:^5.1.0": + version: 5.2.0 + resolution: "get-stream@npm:5.2.0" + dependencies: + pump: "npm:^3.0.0" + checksum: 10/13a73148dca795e41421013da6e3ebff8ccb7fba4d2f023fd0c6da2c166ec4e789bec9774a73a7b49c08daf2cae552f8a3e914042ac23b5f59dd278cc8f9cbfb + languageName: node + linkType: hard + +"get-stream@npm:^6.0.0, get-stream@npm:^6.0.1": + version: 6.0.1 + resolution: "get-stream@npm:6.0.1" + checksum: 10/781266d29725f35c59f1d214aedc92b0ae855800a980800e2923b3fbc4e56b3cb6e462c42e09a1cf1a00c64e056a78fa407cbe06c7c92b7e5cd49b4b85c2a497 + languageName: node + linkType: hard + +"get-stream@npm:^8.0.1": + version: 8.0.1 + resolution: "get-stream@npm:8.0.1" + checksum: 10/dde5511e2e65a48e9af80fea64aff11b4921b14b6e874c6f8294c50975095af08f41bfb0b680c887f28b566dd6ec2cb2f960f9d36a323359be324ce98b766e9e + languageName: node + linkType: hard + +"get-symbol-description@npm:^1.0.0": + version: 1.0.0 + resolution: "get-symbol-description@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.1.1" + checksum: 10/7e5f298afe0f0872747dce4a949ce490ebc5d6dd6aefbbe5044543711c9b19a4dfaebdbc627aee99e1299d58a435b2fbfa083458c1d58be6dc03a3bada24d359 + languageName: node + linkType: hard + +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: "npm:^4.0.1" + checksum: 10/32cd106ce8c0d83731966d31517adb766d02c3812de49c30cfe0675c7c0ae6630c11214c54a5ae67aca882cf738d27fd7768f21aa19118b9245950554be07247 + languageName: node + linkType: hard + +"glob-parent@npm:^6.0.2": + version: 6.0.2 + resolution: "glob-parent@npm:6.0.2" + dependencies: + is-glob: "npm:^4.0.3" + checksum: 10/c13ee97978bef4f55106b71e66428eb1512e71a7466ba49025fc2aec59a5bfb0954d5abd58fc5ee6c9b076eef4e1f6d3375c2e964b88466ca390da4419a786a8 + languageName: node + linkType: hard + +"glob-to-regexp@npm:^0.4.1": + version: 0.4.1 + resolution: "glob-to-regexp@npm:0.4.1" + checksum: 10/9009529195a955c40d7b9690794aeff5ba665cc38f1519e111c58bb54366fd0c106bde80acf97ba4e533208eb53422c83b136611a54c5fefb1edd8dc267cb62e + languageName: node + linkType: hard + +"glob@npm:^10.2.2": + version: 10.3.10 + resolution: "glob@npm:10.3.10" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^2.3.5" + minimatch: "npm:^9.0.1" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry: "npm:^1.10.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10/38bdb2c9ce75eb5ed168f309d4ed05b0798f640b637034800a6bf306f39d35409bf278b0eaaffaec07591085d3acb7184a201eae791468f0f617771c2486a6a8 + languageName: node + linkType: hard + +"glob@npm:^7.0.5, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.2.3": + version: 7.2.3 + resolution: "glob@npm:7.2.3" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^3.1.1" + once: "npm:^1.3.0" + path-is-absolute: "npm:^1.0.0" + checksum: 10/59452a9202c81d4508a43b8af7082ca5c76452b9fcc4a9ab17655822e6ce9b21d4f8fbadabe4fe3faef448294cec249af305e2cd824b7e9aaf689240e5e96a7b + languageName: node + linkType: hard + +"glob@npm:^8.0.0, glob@npm:^8.1.0": + version: 8.1.0 + resolution: "glob@npm:8.1.0" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^5.0.1" + once: "npm:^1.3.0" + checksum: 10/9aab1c75eb087c35dbc41d1f742e51d0507aa2b14c910d96fb8287107a10a22f4bbdce26fc0a3da4c69a20f7b26d62f1640b346a4f6e6becfff47f335bb1dc5e + languageName: node + linkType: hard + +"globals@npm:^11.1.0": + version: 11.12.0 + resolution: "globals@npm:11.12.0" + checksum: 10/9f054fa38ff8de8fa356502eb9d2dae0c928217b8b5c8de1f09f5c9b6c8a96d8b9bd3afc49acbcd384a98a81fea713c859e1b09e214c60509517bb8fc2bc13c2 + languageName: node + linkType: hard + +"globals@npm:^13.19.0": + version: 13.22.0 + resolution: "globals@npm:13.22.0" + dependencies: + type-fest: "npm:^0.20.2" + checksum: 10/2f05c268a544b9e55a7f76f27248923116e50bcd046371fe6fa0920d9fce8432af8f92f47311986f48a1393f61f009c9345de9ed82b3a902d89245c73d0a4047 + languageName: node + linkType: hard + +"globalthis@npm:^1.0.3": + version: 1.0.3 + resolution: "globalthis@npm:1.0.3" + dependencies: + define-properties: "npm:^1.1.3" + checksum: 10/45ae2f3b40a186600d0368f2a880ae257e8278b4c7704f0417d6024105ad7f7a393661c5c2fa1334669cd485ea44bc883a08fdd4516df2428aec40c99f52aa89 + languageName: node + linkType: hard + +"globby@npm:^11.1.0": + version: 11.1.0 + resolution: "globby@npm:11.1.0" + dependencies: + array-union: "npm:^2.1.0" + dir-glob: "npm:^3.0.1" + fast-glob: "npm:^3.2.9" + ignore: "npm:^5.2.0" + merge2: "npm:^1.4.1" + slash: "npm:^3.0.0" + checksum: 10/288e95e310227bbe037076ea81b7c2598ccbc3122d87abc6dab39e1eec309aa14f0e366a98cdc45237ffcfcbad3db597778c0068217dcb1950fef6249104e1b1 + languageName: node + linkType: hard + +"google-auth-library@npm:^8.0.1, google-auth-library@npm:^8.0.2": + version: 8.9.0 + resolution: "google-auth-library@npm:8.9.0" + dependencies: + arrify: "npm:^2.0.0" + base64-js: "npm:^1.3.0" + ecdsa-sig-formatter: "npm:^1.0.11" + fast-text-encoding: "npm:^1.0.0" + gaxios: "npm:^5.0.0" + gcp-metadata: "npm:^5.3.0" + gtoken: "npm:^6.1.0" + jws: "npm:^4.0.0" + lru-cache: "npm:^6.0.0" + checksum: 10/64882b178e2b77f370a3503978ab2e31c0db1053b0eb9141c3a7a2fdfb3dd8b808b00846b518f444a8926266e400ec8df30372a719806ad03a258eadae177213 + languageName: node + linkType: hard + +"google-gax@npm:^3.5.7": + version: 3.6.1 + resolution: "google-gax@npm:3.6.1" + dependencies: + "@grpc/grpc-js": "npm:~1.8.0" + "@grpc/proto-loader": "npm:^0.7.0" + "@types/long": "npm:^4.0.0" + "@types/rimraf": "npm:^3.0.2" + abort-controller: "npm:^3.0.0" + duplexify: "npm:^4.0.0" + fast-text-encoding: "npm:^1.0.3" + google-auth-library: "npm:^8.0.2" + is-stream-ended: "npm:^0.1.4" + node-fetch: "npm:^2.6.1" + object-hash: "npm:^3.0.0" + proto3-json-serializer: "npm:^1.0.0" + protobufjs: "npm:7.2.4" + protobufjs-cli: "npm:1.1.1" + retry-request: "npm:^5.0.0" + bin: + compileProtos: build/tools/compileProtos.js + minifyProtoJson: build/tools/minify.js + checksum: 10/aa6bef74bd23cc05f8d1958c3ca45aa5eaa48d16b25f92eaa254bb6f30c94228801b91bcdba528744b5c3b368428b7298c9fb4c893a916bb4369102de664acdf + languageName: node + linkType: hard + +"google-p12-pem@npm:^4.0.0": + version: 4.0.1 + resolution: "google-p12-pem@npm:4.0.1" + dependencies: + node-forge: "npm:^1.3.1" + bin: + gp12-pem: build/src/bin/gp12-pem.js + checksum: 10/27937440d7c3f8022c6fe9068da6fd74457fea80513b3c34a7b45b29205003bfd31042f7008cd1b970b35ff7147409f0d9a24e554a327d4cae5255c4b60693db + languageName: node + linkType: hard + +"gopd@npm:^1.0.1": + version: 1.0.1 + resolution: "gopd@npm:1.0.1" + dependencies: + get-intrinsic: "npm:^1.1.3" + checksum: 10/5fbc7ad57b368ae4cd2f41214bd947b045c1a4be2f194a7be1778d71f8af9dbf4004221f3b6f23e30820eb0d052b4f819fe6ebe8221e2a3c6f0ee4ef173421ca + languageName: node + linkType: hard + +"got@npm:^11.8.6": + version: 11.8.6 + resolution: "got@npm:11.8.6" + dependencies: + "@sindresorhus/is": "npm:^4.0.0" + "@szmarczak/http-timer": "npm:^4.0.5" + "@types/cacheable-request": "npm:^6.0.1" + "@types/responselike": "npm:^1.0.0" + cacheable-lookup: "npm:^5.0.3" + cacheable-request: "npm:^7.0.2" + decompress-response: "npm:^6.0.0" + http2-wrapper: "npm:^1.0.0-beta.5.2" + lowercase-keys: "npm:^2.0.0" + p-cancelable: "npm:^2.0.0" + responselike: "npm:^2.0.0" + checksum: 10/a30c74029d81bd5fe50dea1a0c970595d792c568e188ff8be254b5bc11e6158d1b014570772d4a30d0a97723e7dd34e7c8cc1a2f23018f60aece3070a7a5c2a5 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 + languageName: node + linkType: hard + +"graphemer@npm:^1.4.0": + version: 1.4.0 + resolution: "graphemer@npm:1.4.0" + checksum: 10/6dd60dba97007b21e3a829fab3f771803cc1292977fe610e240ea72afd67e5690ac9eeaafc4a99710e78962e5936ab5a460787c2a1180f1cb0ccfac37d29f897 + languageName: node + linkType: hard + +"graphlib@npm:^2.1.8": + version: 2.1.8 + resolution: "graphlib@npm:2.1.8" + dependencies: + lodash: "npm:^4.17.15" + checksum: 10/37cbd851d3c1fb99f3174750ccaa22305d23d11746e5df81a38ba3bf25c0ba29cd9658ba69a0159ea81d56c28e8e875033eeaaa7167d838419fae08d9cd2c62c + languageName: node + linkType: hard + +"gtoken@npm:^6.1.0": + version: 6.1.2 + resolution: "gtoken@npm:6.1.2" + dependencies: + gaxios: "npm:^5.0.1" + google-p12-pem: "npm:^4.0.0" + jws: "npm:^4.0.0" + checksum: 10/c5599456205671c5c9321284266f13a0ed275eb190c0a553c4f528ca1c8cf25ec9bb0a2f916e24492915ad209f2f4001230ffdc4d8a270f4e62d4ab9c866a7b2 + languageName: node + linkType: hard + +"has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": + version: 1.0.2 + resolution: "has-bigints@npm:1.0.2" + checksum: 10/4e0426c900af034d12db14abfece02ce7dbf53f2022d28af1a97913ff4c07adb8799476d57dc44fbca0e07d1dbda2a042c2928b1f33d3f09c15de0640a7fb81b + languageName: node + linkType: hard + +"has-flag@npm:^3.0.0": + version: 3.0.0 + resolution: "has-flag@npm:3.0.0" + checksum: 10/4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b + languageName: node + linkType: hard + +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 10/261a1357037ead75e338156b1f9452c016a37dcd3283a972a30d9e4a87441ba372c8b81f818cd0fbcd9c0354b4ae7e18b9e1afa1971164aef6d18c2b6095a8ad + languageName: node + linkType: hard + +"has-property-descriptors@npm:^1.0.0": + version: 1.0.0 + resolution: "has-property-descriptors@npm:1.0.0" + dependencies: + get-intrinsic: "npm:^1.1.1" + checksum: 10/a6d3f0a266d0294d972e354782e872e2fe1b6495b321e6ef678c9b7a06a40408a6891817350c62e752adced73a94ac903c54734fee05bf65b1905ee1368194bb + languageName: node + linkType: hard + +"has-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "has-proto@npm:1.0.1" + checksum: 10/eab2ab0ed1eae6d058b9bbc4c1d99d2751b29717be80d02fd03ead8b62675488de0c7359bc1fdd4b87ef6fd11e796a9631ad4d7452d9324fdada70158c2e5be7 + languageName: node + linkType: hard + +"has-symbols@npm:^1.0.2, has-symbols@npm:^1.0.3": + version: 1.0.3 + resolution: "has-symbols@npm:1.0.3" + checksum: 10/464f97a8202a7690dadd026e6d73b1ceeddd60fe6acfd06151106f050303eaa75855aaa94969df8015c11ff7c505f196114d22f7386b4a471038da5874cf5e9b + languageName: node + linkType: hard + +"has-tostringtag@npm:^1.0.0": + version: 1.0.0 + resolution: "has-tostringtag@npm:1.0.0" + dependencies: + has-symbols: "npm:^1.0.2" + checksum: 10/95546e7132efc895a9ae64a8a7cf52588601fc3d52e0304ed228f336992cdf0baaba6f3519d2655e560467db35a1ed79f6420c286cc91a13aa0647a31ed92570 + languageName: node + linkType: hard + +"has-unicode@npm:^2.0.1": + version: 2.0.1 + resolution: "has-unicode@npm:2.0.1" + checksum: 10/041b4293ad6bf391e21c5d85ed03f412506d6623786b801c4ab39e4e6ca54993f13201bceb544d92963f9e0024e6e7fbf0cb1d84c9d6b31cb9c79c8c990d13d8 + languageName: node + linkType: hard + +"has@npm:^1.0.3": + version: 1.0.4 + resolution: "has@npm:1.0.4" + checksum: 10/c245f332fe78c7b6b8753857240ac12b3286f995f656a33c77e0f5baab7d0157e6ddb1c34940ffd2bffc51f75ede50cd8b29ff65c13e336376aca8cf3df58043 + languageName: node + linkType: hard + +"hash-base@npm:^3.0.0": + version: 3.1.0 + resolution: "hash-base@npm:3.1.0" + dependencies: + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.6.0" + safe-buffer: "npm:^5.2.0" + checksum: 10/26b7e97ac3de13cb23fc3145e7e3450b0530274a9562144fc2bf5c1e2983afd0e09ed7cc3b20974ba66039fad316db463da80eb452e7373e780cbee9a0d2f2dc + languageName: node + linkType: hard + +"hash.js@npm:^1.0.0, hash.js@npm:^1.0.3": + version: 1.1.7 + resolution: "hash.js@npm:1.1.7" + dependencies: + inherits: "npm:^2.0.3" + minimalistic-assert: "npm:^1.0.1" + checksum: 10/0c89ee4006606a40f92df5cc3c263342e7fea68110f3e9ef032bd2083650430505db01b6b7926953489517d4027535e4fdc7f970412893d3031c361d3ec8f4b3 + languageName: node + linkType: hard + +"hathor-wallet-service@workspace:.": + version: 0.0.0-use.local + resolution: "hathor-wallet-service@workspace:." + dependencies: + dotenv: "npm:^16.3.1" + mysql2: "npm:^3.6.1" + sequelize: "npm:^6.33.0" + sequelize-cli: "npm:^6.6.1" + languageName: unknown + linkType: soft + +"hexoid@npm:^1.0.0": + version: 1.0.0 + resolution: "hexoid@npm:1.0.0" + checksum: 10/f2271b8b6b0e13fb5a1eccf740f53ce8bae689c80b9498b854c447f9dc94f75f44e0de064c0e4660ecdbfa8942bb2b69973fdcb080187b45bbb409a3c71f19d4 + languageName: node + linkType: hard + +"hmac-drbg@npm:^1.0.1": + version: 1.0.1 + resolution: "hmac-drbg@npm:1.0.1" + dependencies: + hash.js: "npm:^1.0.3" + minimalistic-assert: "npm:^1.0.0" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 10/0298a1445b8029a69b713d918ecaa84a1d9f614f5857e0c6e1ca517abfa1357216987b2ee08cc6cc73ba82a6c6ddf2ff11b9717a653530ef03be599d4699b836 + languageName: node + linkType: hard + +"hosted-git-info@npm:^2.1.4": + version: 2.8.9 + resolution: "hosted-git-info@npm:2.8.9" + checksum: 10/96da7d412303704af41c3819207a09ea2cab2de97951db4cf336bb8bce8d8e36b9a6821036ad2e55e67d3be0af8f967a7b57981203fbfb88bc05cd803407b8c3 + languageName: node + linkType: hard + +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: 10/034d74029dcca544a34fb6135e98d427acd73019796ffc17383eaa3ec2fe1c0471dcbbc8f8ed39e46e86d43ccd753a160631615e4048285e313569609b66d5b7 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 10/362d5ed66b12ceb9c0a328fb31200b590ab1b02f4a254a697dc796850cc4385603e75f53ec59f768b2dad3bfa1464bd229f7de278d2899a0e3beffc634b6683f + languageName: node + linkType: hard + +"http-parser-js@npm:>=0.5.1": + version: 0.5.8 + resolution: "http-parser-js@npm:0.5.8" + checksum: 10/2a78a567ee6366dae0129d819b799dce1f95ec9732c5ab164a78ee69804ffb984abfa0660274e94e890fc54af93546eb9f12b6d10edbaed017e2d41c29b7cf29 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^4.0.1": + version: 4.0.1 + resolution: "http-proxy-agent@npm:4.0.1" + dependencies: + "@tootallnate/once": "npm:1" + agent-base: "npm:6" + debug: "npm:4" + checksum: 10/2e17f5519f2f2740b236d1d14911ea4be170c67419dc15b05ea9a860a22c5d9c6ff4da270972117067cc2cefeba9df5f7cd5e7818fdc6ae52b6acf2a533e5fdd + languageName: node + linkType: hard + +"http-proxy-agent@npm:^5.0.0": + version: 5.0.0 + resolution: "http-proxy-agent@npm:5.0.0" + dependencies: + "@tootallnate/once": "npm:2" + agent-base: "npm:6" + debug: "npm:4" + checksum: 10/5ee19423bc3e0fd5f23ce991b0755699ad2a46a440ce9cec99e8126bb98448ad3479d2c0ea54be5519db5b19a4ffaa69616bac01540db18506dd4dac3dc418f0 + languageName: node + linkType: hard + +"http2-wrapper@npm:^1.0.0-beta.5.2": + version: 1.0.3 + resolution: "http2-wrapper@npm:1.0.3" + dependencies: + quick-lru: "npm:^5.1.1" + resolve-alpn: "npm:^1.0.0" + checksum: 10/8097ee2699440c2e64bda52124990cc5b0fb347401c7797b1a0c1efd5a0f79a4ebaa68e8a6ac3e2dde5f09460c1602764da6da2412bad628ed0a3b0ae35e72d4 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^5.0.0, https-proxy-agent@npm:^5.0.1": + version: 5.0.1 + resolution: "https-proxy-agent@npm:5.0.1" + dependencies: + agent-base: "npm:6" + debug: "npm:4" + checksum: 10/f0dce7bdcac5e8eaa0be3c7368bb8836ed010fb5b6349ffb412b172a203efe8f807d9a6681319105ea1b6901e1972c7b5ea899672a7b9aad58309f766dcbe0df + languageName: node + linkType: hard + +"human-signals@npm:^2.1.0": + version: 2.1.0 + resolution: "human-signals@npm:2.1.0" + checksum: 10/df59be9e0af479036798a881d1f136c4a29e0b518d4abb863afbd11bf30efa3eeb1d0425fc65942dcc05ab3bf40205ea436b0ff389f2cd20b75b8643d539bf86 + languageName: node + linkType: hard + +"human-signals@npm:^5.0.0": + version: 5.0.0 + resolution: "human-signals@npm:5.0.0" + checksum: 10/30f8870d831cdcd2d6ec0486a7d35d49384996742052cee792854273fa9dd9e7d5db06bb7985d4953e337e10714e994e0302e90dc6848069171b05ec836d65b0 + languageName: node + linkType: hard + +"humanize-ms@npm:^1.2.1": + version: 1.2.1 + resolution: "humanize-ms@npm:1.2.1" + dependencies: + ms: "npm:^2.0.0" + checksum: 10/9c7a74a2827f9294c009266c82031030eae811ca87b0da3dceb8d6071b9bde22c9f3daef0469c3c533cc67a97d8a167cd9fc0389350e5f415f61a79b171ded16 + languageName: node + linkType: hard + +"iconv-lite@npm:^0.4.24": + version: 0.4.24 + resolution: "iconv-lite@npm:0.4.24" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3" + checksum: 10/6d3a2dac6e5d1fb126d25645c25c3a1209f70cceecc68b8ef51ae0da3cdc078c151fade7524a30b12a3094926336831fca09c666ef55b37e2c69638b5d6bd2e3 + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10/24e3292dd3dadaa81d065c6f8c41b274a47098150d444b96e5f53b4638a9a71482921ea6a91a1f59bb71d9796de25e04afd05919fa64c360347ba65d3766f10f + languageName: node + linkType: hard + +"ieee754@npm:1.1.13": + version: 1.1.13 + resolution: "ieee754@npm:1.1.13" + checksum: 10/5c2f365168e629b164f6b8863c399af03e4515cafb690fe143039c9bd76b8f670af6539a43859bbfbe7df707eac755478515319a357a29f8c5f17ec2daa24a4c + languageName: node + linkType: hard + +"ieee754@npm:^1.1.13, ieee754@npm:^1.1.4, ieee754@npm:^1.2.1": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 10/d9f2557a59036f16c282aaeb107832dc957a93d73397d89bbad4eb1130560560eb695060145e8e6b3b498b15ab95510226649a0b8f52ae06583575419fe10fc4 + languageName: node + linkType: hard + +"ignore@npm:^5.1.8, ignore@npm:^5.2.0, ignore@npm:^5.2.4": + version: 5.2.4 + resolution: "ignore@npm:5.2.4" + checksum: 10/4f7caf5d2005da21a382d4bd1d2aa741a3bed51de185c8562dd7f899a81a620ac4fd0619b06f7029a38ae79e4e4c134399db3bd0192c703c3ef54bb82df3086c + languageName: node + linkType: hard + +"immediate@npm:~3.0.5": + version: 3.0.6 + resolution: "immediate@npm:3.0.6" + checksum: 10/f9b3486477555997657f70318cc8d3416159f208bec4cca3ff3442fd266bc23f50f0c9bd8547e1371a6b5e82b821ec9a7044a4f7b944798b25aa3cc6d5e63e62 + languageName: node + linkType: hard + +"import-fresh@npm:^3.2.1": + version: 3.3.0 + resolution: "import-fresh@npm:3.3.0" + dependencies: + parent-module: "npm:^1.0.0" + resolve-from: "npm:^4.0.0" + checksum: 10/2cacfad06e652b1edc50be650f7ec3be08c5e5a6f6d12d035c440a42a8cc028e60a5b99ca08a77ab4d6b1346da7d971915828f33cdab730d3d42f08242d09baa + languageName: node + linkType: hard + +"import-local@npm:^3.0.2": + version: 3.1.0 + resolution: "import-local@npm:3.1.0" + dependencies: + pkg-dir: "npm:^4.2.0" + resolve-cwd: "npm:^3.0.0" + bin: + import-local-fixture: fixtures/cli.js + checksum: 10/bfcdb63b5e3c0e245e347f3107564035b128a414c4da1172a20dc67db2504e05ede4ac2eee1252359f78b0bfd7b19ef180aec427c2fce6493ae782d73a04cddd + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 10/2d30b157a91fe1c1d7c6f653cbf263f039be6c5bfa959245a16d4ee191fc0f2af86c08545b6e6beeb041c56b574d2d5b9f95343d378ab49c0f37394d541e7fc8 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 10/cd3f5cbc9ca2d624c6a1f53f12e6b341659aba0e2d3254ae2b4464aaea8b4294cdb09616abbc59458f980531f2429784ed6a420d48d245bcad0811980c9efae9 + languageName: node + linkType: hard + +"infer-owner@npm:^1.0.4": + version: 1.0.4 + resolution: "infer-owner@npm:1.0.4" + checksum: 10/181e732764e4a0611576466b4b87dac338972b839920b2a8cde43642e4ed6bd54dc1fb0b40874728f2a2df9a1b097b8ff83b56d5f8f8e3927f837fdcb47d8a89 + languageName: node + linkType: hard + +"inflection@npm:^1.13.4": + version: 1.13.4 + resolution: "inflection@npm:1.13.4" + checksum: 10/a0cc1b105ccbda9607b5d1610b5c7aa35456ca06b7f3573a47c677e1f829052859cacc36601c3c07de89cb756616a440814ef2d190a6ae70398e6aa6efc2a547 + languageName: node + linkType: hard + +"inflight@npm:^1.0.4": + version: 1.0.6 + resolution: "inflight@npm:1.0.6" + dependencies: + once: "npm:^1.3.0" + wrappy: "npm:1" + checksum: 10/d2ebd65441a38c8336c223d1b80b921b9fa737e37ea466fd7e253cb000c64ae1f17fa59e68130ef5bda92cfd8d36b83d37dab0eb0a4558bcfec8e8cdfd2dcb67 + languageName: node + linkType: hard + +"inherits@npm:2, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 + languageName: node + linkType: hard + +"inherits@npm:=2.0.1": + version: 2.0.1 + resolution: "inherits@npm:2.0.1" + checksum: 10/37165f42e53627edc18d815654a79e7407e356adf480aab77db3a12c978e597f3af632cf0459472dd5a088bc21ee911020f544c0d3c23b45bcfd1cd92fe9e404 + languageName: node + linkType: hard + +"ini@npm:^1.3.4": + version: 1.3.8 + resolution: "ini@npm:1.3.8" + checksum: 10/314ae176e8d4deb3def56106da8002b462221c174ddb7ce0c49ee72c8cd1f9044f7b10cc555a7d8850982c3b9ca96fc212122749f5234bc2b6fb05fb942ed566 + languageName: node + linkType: hard + +"inquirer@npm:^8.2.5": + version: 8.2.6 + resolution: "inquirer@npm:8.2.6" + dependencies: + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.1.1" + cli-cursor: "npm:^3.1.0" + cli-width: "npm:^3.0.0" + external-editor: "npm:^3.0.3" + figures: "npm:^3.0.0" + lodash: "npm:^4.17.21" + mute-stream: "npm:0.0.8" + ora: "npm:^5.4.1" + run-async: "npm:^2.4.0" + rxjs: "npm:^7.5.5" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + through: "npm:^2.3.6" + wrap-ansi: "npm:^6.0.1" + checksum: 10/f642b9e5a94faaba54f277bdda2af0e0a6b592bd7f88c60e1614b5795b19336c7025e0c2923915d5f494f600a02fe8517413779a794415bb79a9563b061d68ab + languageName: node + linkType: hard + +"internal-slot@npm:^1.0.5": + version: 1.0.5 + resolution: "internal-slot@npm:1.0.5" + dependencies: + get-intrinsic: "npm:^1.2.0" + has: "npm:^1.0.3" + side-channel: "npm:^1.0.4" + checksum: 10/e2eb5b348e427957dd4092cb57b9374a2cbcabbf61e5e5b4d99cb68eeaae29394e8efd79f23dc2b1831253346f3c16b82010737b84841225e934d80d04d68643 + languageName: node + linkType: hard + +"ip@npm:^2.0.0": + version: 2.0.0 + resolution: "ip@npm:2.0.0" + checksum: 10/1270b11e534a466fb4cf4426cbcc3a907c429389f7f4e4e3b288b42823562e88d6a509ceda8141a507de147ca506141f745005c0aa144569d94cf24a54eb52bc + languageName: node + linkType: hard + +"is-arguments@npm:^1.0.4": + version: 1.1.1 + resolution: "is-arguments@npm:1.1.1" + dependencies: + call-bind: "npm:^1.0.2" + has-tostringtag: "npm:^1.0.0" + checksum: 10/a170c7e26082e10de9be6e96d32ae3db4d5906194051b792e85fae3393b53cf2cb5b3557863e5c8ccbab55e2fd8f2f75aa643d437613f72052cf0356615c34be + languageName: node + linkType: hard + +"is-array-buffer@npm:^3.0.1, is-array-buffer@npm:^3.0.2": + version: 3.0.2 + resolution: "is-array-buffer@npm:3.0.2" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.2.0" + is-typed-array: "npm:^1.1.10" + checksum: 10/dcac9dda66ff17df9cabdc58214172bf41082f956eab30bb0d86bc0fab1e44b690fc8e1f855cf2481245caf4e8a5a006a982a71ddccec84032ed41f9d8da8c14 + languageName: node + linkType: hard + +"is-arrayish@npm:^0.2.1": + version: 0.2.1 + resolution: "is-arrayish@npm:0.2.1" + checksum: 10/73ced84fa35e59e2c57da2d01e12cd01479f381d7f122ce41dcbb713f09dbfc651315832cd2bf8accba7681a69e4d6f1e03941d94dd10040d415086360e7005e + languageName: node + linkType: hard + +"is-arrayish@npm:^0.3.1": + version: 0.3.2 + resolution: "is-arrayish@npm:0.3.2" + checksum: 10/81a78d518ebd8b834523e25d102684ee0f7e98637136d3bdc93fd09636350fa06f1d8ca997ea28143d4d13cb1b69c0824f082db0ac13e1ab3311c10ffea60ade + languageName: node + linkType: hard + +"is-bigint@npm:^1.0.1": + version: 1.0.4 + resolution: "is-bigint@npm:1.0.4" + dependencies: + has-bigints: "npm:^1.0.1" + checksum: 10/cc981cf0564c503aaccc1e5f39e994ae16ae2d1a8fcd14721f14ad431809071f39ec568cfceef901cff408045f1a6d6bac90d1b43eeb0b8e3bc34c8eb1bdb4c4 + languageName: node + linkType: hard + +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: 10/078e51b4f956c2c5fd2b26bb2672c3ccf7e1faff38e0ebdba45612265f4e3d9fc3127a1fa8370bbf09eab61339203c3d3b7af5662cbf8be4030f8fac37745b0e + languageName: node + linkType: hard + +"is-boolean-object@npm:^1.1.0": + version: 1.1.2 + resolution: "is-boolean-object@npm:1.1.2" + dependencies: + call-bind: "npm:^1.0.2" + has-tostringtag: "npm:^1.0.0" + checksum: 10/ba794223b56a49a9f185e945eeeb6b7833b8ea52a335cec087d08196cf27b538940001615d3bb976511287cefe94e5907d55f00bb49580533f9ca9b4515fcc2e + languageName: node + linkType: hard + +"is-buffer@npm:^2.0.2": + version: 2.0.5 + resolution: "is-buffer@npm:2.0.5" + checksum: 10/3261a8b858edcc6c9566ba1694bf829e126faa88911d1c0a747ea658c5d81b14b6955e3a702d59dabadd58fdd440c01f321aa71d6547105fd21d03f94d0597e7 + languageName: node + linkType: hard + +"is-builtin-module@npm:^3.2.1": + version: 3.2.1 + resolution: "is-builtin-module@npm:3.2.1" + dependencies: + builtin-modules: "npm:^3.3.0" + checksum: 10/e8f0ffc19a98240bda9c7ada84d846486365af88d14616e737d280d378695c8c448a621dcafc8332dbf0fcd0a17b0763b845400709963fa9151ddffece90ae88 + languageName: node + linkType: hard + +"is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": + version: 1.2.7 + resolution: "is-callable@npm:1.2.7" + checksum: 10/48a9297fb92c99e9df48706241a189da362bff3003354aea4048bd5f7b2eb0d823cd16d0a383cece3d76166ba16d85d9659165ac6fcce1ac12e6c649d66dbdb9 + languageName: node + linkType: hard + +"is-core-module@npm:^2.13.0": + version: 2.13.0 + resolution: "is-core-module@npm:2.13.0" + dependencies: + has: "npm:^1.0.3" + checksum: 10/55ccb5ccd208a1e088027065ee6438a99367e4c31c366b52fbaeac8fa23111cd17852111836d904da604801b3286d38d3d1ffa6cd7400231af8587f021099dc6 + languageName: node + linkType: hard + +"is-date-object@npm:^1.0.1": + version: 1.0.5 + resolution: "is-date-object@npm:1.0.5" + dependencies: + has-tostringtag: "npm:^1.0.0" + checksum: 10/cc80b3a4b42238fa0d358b9a6230dae40548b349e64a477cb7c5eff9b176ba194c11f8321daaf6dd157e44073e9b7fd01f87db1f14952a88d5657acdcd3a56e2 + languageName: node + linkType: hard + +"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1, is-docker@npm:^2.2.1": + version: 2.2.1 + resolution: "is-docker@npm:2.2.1" + bin: + is-docker: cli.js + checksum: 10/3fef7ddbf0be25958e8991ad941901bf5922ab2753c46980b60b05c1bf9c9c2402d35e6dc32e4380b980ef5e1970a5d9d5e5aa2e02d77727c3b6b5e918474c56 + languageName: node + linkType: hard + +"is-docker@npm:^3.0.0": + version: 3.0.0 + resolution: "is-docker@npm:3.0.0" + bin: + is-docker: cli.js + checksum: 10/b698118f04feb7eaf3338922bd79cba064ea54a1c3db6ec8c0c8d8ee7613e7e5854d802d3ef646812a8a3ace81182a085dfa0a71cc68b06f3fa794b9783b3c90 + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: 10/df033653d06d0eb567461e58a7a8c9f940bd8c22274b94bf7671ab36df5719791aae15eef6d83bbb5e23283967f2f984b8914559d4449efda578c775c4be6f85 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 10/44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 + languageName: node + linkType: hard + +"is-generator-fn@npm:^2.0.0": + version: 2.1.0 + resolution: "is-generator-fn@npm:2.1.0" + checksum: 10/a6ad5492cf9d1746f73b6744e0c43c0020510b59d56ddcb78a91cbc173f09b5e6beff53d75c9c5a29feb618bfef2bf458e025ecf3a57ad2268e2fb2569f56215 + languageName: node + linkType: hard + +"is-generator-function@npm:^1.0.7": + version: 1.0.10 + resolution: "is-generator-function@npm:1.0.10" + dependencies: + has-tostringtag: "npm:^1.0.0" + checksum: 10/499a3ce6361064c3bd27fbff5c8000212d48506ebe1977842bbd7b3e708832d0deb1f4cc69186ece3640770e8c4f1287b24d99588a0b8058b2dbdd344bc1f47f + languageName: node + linkType: hard + +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 10/3ed74f2b0cdf4f401f38edb0442ddfde3092d79d7d35c9919c86641efdbcbb32e45aa3c0f70ce5eecc946896cd5a0f26e4188b9f2b881876f7cb6c505b82da11 + languageName: node + linkType: hard + +"is-inside-container@npm:^1.0.0": + version: 1.0.0 + resolution: "is-inside-container@npm:1.0.0" + dependencies: + is-docker: "npm:^3.0.0" + bin: + is-inside-container: cli.js + checksum: 10/c50b75a2ab66ab3e8b92b3bc534e1ea72ca25766832c0623ac22d134116a98bcf012197d1caabe1d1c4bd5f84363d4aa5c36bb4b585fbcaf57be172cd10a1a03 + languageName: node + linkType: hard + +"is-interactive@npm:^1.0.0": + version: 1.0.0 + resolution: "is-interactive@npm:1.0.0" + checksum: 10/824808776e2d468b2916cdd6c16acacebce060d844c35ca6d82267da692e92c3a16fdba624c50b54a63f38bdc4016055b6f443ce57d7147240de4f8cdabaf6f9 + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 10/93a32f01940220532e5948538699ad610d5924ac86093fcee83022252b363eb0cc99ba53ab084a04e4fb62bf7b5731f55496257a4c38adf87af9c4d352c71c35 + languageName: node + linkType: hard + +"is-nan@npm:^1.3.2": + version: 1.3.2 + resolution: "is-nan@npm:1.3.2" + dependencies: + call-bind: "npm:^1.0.0" + define-properties: "npm:^1.1.3" + checksum: 10/1f784d3472c09bc2e47acba7ffd4f6c93b0394479aa613311dc1d70f1bfa72eb0846c81350967722c959ba65811bae222204d6c65856fdce68f31986140c7b0e + languageName: node + linkType: hard + +"is-natural-number@npm:^4.0.1": + version: 4.0.1 + resolution: "is-natural-number@npm:4.0.1" + checksum: 10/3e5e3d52e0dfa4fea923b5d2b8a5cdbd9bf110c4598d30304b98528b02f40c9058a2abf1bae10bcbaf2bac18ace41cff7bc9673aff339f8c8297fae74ae0e75d + languageName: node + linkType: hard + +"is-negative-zero@npm:^2.0.2": + version: 2.0.2 + resolution: "is-negative-zero@npm:2.0.2" + checksum: 10/edbec1a9e6454d68bf595a114c3a72343d2d0be7761d8173dae46c0b73d05bb8fe9398c85d121e7794a66467d2f40b4a610b0be84cd804262d234fc634c86131 + languageName: node + linkType: hard + +"is-network-error@npm:^1.0.0": + version: 1.0.0 + resolution: "is-network-error@npm:1.0.0" + checksum: 10/2ca2b4b2d420015e0237abe28ebf316fcd26a82304b07432abf155759a3bee6895609ac91e692a72ad61b7fc902c3283b2dece61e1ddb05a6257777a8573e468 + languageName: node + linkType: hard + +"is-number-object@npm:^1.0.4": + version: 1.0.7 + resolution: "is-number-object@npm:1.0.7" + dependencies: + has-tostringtag: "npm:^1.0.0" + checksum: 10/8700dcf7f602e0a9625830541345b8615d04953655acbf5c6d379c58eb1af1465e71227e95d501343346e1d49b6f2d53cbc166b1fc686a7ec19151272df582f9 + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 10/6a6c3383f68afa1e05b286af866017c78f1226d43ac8cb064e115ff9ed85eb33f5c4f7216c96a71e4dfea289ef52c5da3aef5bbfade8ffe47a0465d70c0c8e86 + languageName: node + linkType: hard + +"is-path-inside@npm:^3.0.3": + version: 3.0.3 + resolution: "is-path-inside@npm:3.0.3" + checksum: 10/abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 + languageName: node + linkType: hard + +"is-plain-obj@npm:^1.0.0": + version: 1.1.0 + resolution: "is-plain-obj@npm:1.1.0" + checksum: 10/0ee04807797aad50859652a7467481816cbb57e5cc97d813a7dcd8915da8195dc68c436010bf39d195226cde6a2d352f4b815f16f26b7bf486a5754290629931 + languageName: node + linkType: hard + +"is-promise@npm:^2.2.2": + version: 2.2.2 + resolution: "is-promise@npm:2.2.2" + checksum: 10/18bf7d1c59953e0ad82a1ed963fb3dc0d135c8f299a14f89a17af312fc918373136e56028e8831700e1933519630cc2fd4179a777030330fde20d34e96f40c78 + languageName: node + linkType: hard + +"is-property@npm:^1.0.2": + version: 1.0.2 + resolution: "is-property@npm:1.0.2" + checksum: 10/2f66eacb3d7237ba5c725496672edec656a20b12c80790921988578e6b11c258a062ce1e602f3cd2e3c2e05dd8b6e24e1d59254375207f157424a02ef0abb3d7 + languageName: node + linkType: hard + +"is-regex@npm:^1.1.4": + version: 1.1.4 + resolution: "is-regex@npm:1.1.4" + dependencies: + call-bind: "npm:^1.0.2" + has-tostringtag: "npm:^1.0.0" + checksum: 10/36d9174d16d520b489a5e9001d7d8d8624103b387be300c50f860d9414556d0485d74a612fdafc6ebbd5c89213d947dcc6b6bff6b2312093f71ea03cbb19e564 + languageName: node + linkType: hard + +"is-shared-array-buffer@npm:^1.0.2": + version: 1.0.2 + resolution: "is-shared-array-buffer@npm:1.0.2" + dependencies: + call-bind: "npm:^1.0.2" + checksum: 10/23d82259d6cd6dbb7c4ff3e4efeff0c30dbc6b7f88698498c17f9821cb3278d17d2b6303a5341cbd638ab925a28f3f086a6c79b3df70ac986cc526c725d43b4f + languageName: node + linkType: hard + +"is-stream-ended@npm:^0.1.4": + version: 0.1.4 + resolution: "is-stream-ended@npm:0.1.4" + checksum: 10/56cbc9cfa0a77877777a3df9e186abb5b0ca73dcbcaf0fd87ed573fb8f8e61283abec0fc072c9e3412336edc04449439b8a128d2bcc6c2797158de5465cfaf85 + languageName: node + linkType: hard + +"is-stream@npm:^1.1.0": + version: 1.1.0 + resolution: "is-stream@npm:1.1.0" + checksum: 10/351aa77c543323c4e111204482808cfad68d2e940515949e31ccd0b010fc13d5fba4b9c230e4887fd24284713040f43e542332fbf172f6b9944b7d62e389c0ec + languageName: node + linkType: hard + +"is-stream@npm:^2.0.0": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: 10/b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 + languageName: node + linkType: hard + +"is-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "is-stream@npm:3.0.0" + checksum: 10/172093fe99119ffd07611ab6d1bcccfe8bc4aa80d864b15f43e63e54b7abc71e779acd69afdb854c4e2a67fdc16ae710e370eda40088d1cfc956a50ed82d8f16 + languageName: node + linkType: hard + +"is-string@npm:^1.0.5, is-string@npm:^1.0.7": + version: 1.0.7 + resolution: "is-string@npm:1.0.7" + dependencies: + has-tostringtag: "npm:^1.0.0" + checksum: 10/2bc292fe927493fb6dfc3338c099c3efdc41f635727c6ebccf704aeb2a27bca7acb9ce6fd34d103db78692b10b22111a8891de26e12bfa1c5e11e263c99d1fef + languageName: node + linkType: hard + +"is-symbol@npm:^1.0.2, is-symbol@npm:^1.0.3": + version: 1.0.4 + resolution: "is-symbol@npm:1.0.4" + dependencies: + has-symbols: "npm:^1.0.2" + checksum: 10/a47dd899a84322528b71318a89db25c7ecdec73197182dad291df15ffea501e17e3c92c8de0bfb50e63402747399981a687b31c519971b1fa1a27413612be929 + languageName: node + linkType: hard + +"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.12, is-typed-array@npm:^1.1.3, is-typed-array@npm:^1.1.9": + version: 1.1.12 + resolution: "is-typed-array@npm:1.1.12" + dependencies: + which-typed-array: "npm:^1.1.11" + checksum: 10/d953adfd3c41618d5e01b2a10f21817e4cdc9572772fa17211100aebb3811b6e3c2e308a0558cc87d218a30504cb90154b833013437776551bfb70606fb088ca + languageName: node + linkType: hard + +"is-typedarray@npm:^1.0.0": + version: 1.0.0 + resolution: "is-typedarray@npm:1.0.0" + checksum: 10/4b433bfb0f9026f079f4eb3fbaa4ed2de17c9995c3a0b5c800bec40799b4b2a8b4e051b1ada77749deb9ded4ae52fe2096973f3a93ff83df1a5a7184a669478c + languageName: node + linkType: hard + +"is-unicode-supported@npm:^0.1.0": + version: 0.1.0 + resolution: "is-unicode-supported@npm:0.1.0" + checksum: 10/a2aab86ee7712f5c2f999180daaba5f361bdad1efadc9610ff5b8ab5495b86e4f627839d085c6530363c6d6d4ecbde340fb8e54bdb83da4ba8e0865ed5513c52 + languageName: node + linkType: hard + +"is-weakref@npm:^1.0.2": + version: 1.0.2 + resolution: "is-weakref@npm:1.0.2" + dependencies: + call-bind: "npm:^1.0.2" + checksum: 10/0023fd0e4bdf9c338438ffbe1eed7ebbbff7e7e18fb7cdc227caaf9d4bd024a2dcdf6a8c9f40c92192022eac8391243bb9e66cccebecbf6fe1d8a366108f8513 + languageName: node + linkType: hard + +"is-wsl@npm:^2.1.1, is-wsl@npm:^2.2.0": + version: 2.2.0 + resolution: "is-wsl@npm:2.2.0" + dependencies: + is-docker: "npm:^2.0.0" + checksum: 10/20849846ae414997d290b75e16868e5261e86ff5047f104027026fd61d8b5a9b0b3ade16239f35e1a067b3c7cc02f70183cb661010ed16f4b6c7c93dad1b19d8 + languageName: node + linkType: hard + +"is-wsl@npm:^3.1.0": + version: 3.1.0 + resolution: "is-wsl@npm:3.1.0" + dependencies: + is-inside-container: "npm:^1.0.0" + checksum: 10/f9734c81f2f9cf9877c5db8356bfe1ff61680f1f4c1011e91278a9c0564b395ae796addb4bf33956871041476ec82c3e5260ed57b22ac91794d4ae70a1d2f0a9 + languageName: node + linkType: hard + +"isarray@npm:^1.0.0, isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: 10/f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab + languageName: node + linkType: hard + +"isarray@npm:^2.0.5": + version: 2.0.5 + resolution: "isarray@npm:2.0.5" + checksum: 10/1d8bc7911e13bb9f105b1b3e0b396c787a9e63046af0b8fe0ab1414488ab06b2b099b87a2d8a9e31d21c9a6fad773c7fc8b257c4880f2d957274479d28ca3414 + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10/7c9f715c03aff08f35e98b1fadae1b9267b38f0615d501824f9743f3aab99ef10e303ce7db3f186763a0b70a19de5791ebfc854ff884d5a8c4d92211f642ec92 + languageName: node + linkType: hard + +"isomorphic-ws@npm:^4.0.1": + version: 4.0.1 + resolution: "isomorphic-ws@npm:4.0.1" + peerDependencies: + ws: "*" + checksum: 10/d7190eadefdc28bdb93d67b5f0c603385aaf87724fa2974abb382ac1ec9756ed2cfb27065cbe76122879c2d452e2982bc4314317f3d6c737ddda6c047328771a + languageName: node + linkType: hard + +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": + version: 3.2.0 + resolution: "istanbul-lib-coverage@npm:3.2.0" + checksum: 10/31621b84ad29339242b63d454243f558a7958ee0b5177749bacf1f74be7d95d3fd93853738ef7eebcddfaf3eab014716e51392a8dbd5aa1bdc1b15c2ebc53c24 + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^5.0.4": + version: 5.2.1 + resolution: "istanbul-lib-instrument@npm:5.2.1" + dependencies: + "@babel/core": "npm:^7.12.3" + "@babel/parser": "npm:^7.14.7" + "@istanbuljs/schema": "npm:^0.1.2" + istanbul-lib-coverage: "npm:^3.2.0" + semver: "npm:^6.3.0" + checksum: 10/bbc4496c2f304d799f8ec22202ab38c010ac265c441947f075c0f7d46bd440b45c00e46017cf9053453d42182d768b1d6ed0e70a142c95ab00df9843aa5ab80e + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^6.0.0": + version: 6.0.1 + resolution: "istanbul-lib-instrument@npm:6.0.1" + dependencies: + "@babel/core": "npm:^7.12.3" + "@babel/parser": "npm:^7.14.7" + "@istanbuljs/schema": "npm:^0.1.2" + istanbul-lib-coverage: "npm:^3.2.0" + semver: "npm:^7.5.4" + checksum: 10/95fd8c66e586840989cb3c7819c6da66c4742a6fedbf16b51a5c7f1898941ad07b79ddff020f479d3a1d76743ecdbf255d93c35221875687477d4b118026e7e7 + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: "npm:^3.0.0" + make-dir: "npm:^4.0.0" + supports-color: "npm:^7.1.0" + checksum: 10/86a83421ca1cf2109a9f6d193c06c31ef04a45e72a74579b11060b1e7bb9b6337a4e6f04abfb8857e2d569c271273c65e855ee429376a0d7c91ad91db42accd1 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^4.0.0": + version: 4.0.1 + resolution: "istanbul-lib-source-maps@npm:4.0.1" + dependencies: + debug: "npm:^4.1.1" + istanbul-lib-coverage: "npm:^3.0.0" + source-map: "npm:^0.6.1" + checksum: 10/5526983462799aced011d776af166e350191b816821ea7bcf71cab3e5272657b062c47dc30697a22a43656e3ced78893a42de677f9ccf276a28c913190953b82 + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.3": + version: 3.1.6 + resolution: "istanbul-reports@npm:3.1.6" + dependencies: + html-escaper: "npm:^2.0.0" + istanbul-lib-report: "npm:^3.0.0" + checksum: 10/135c178e509b21af5c446a6951fc01c331331bb0fdb1ed1dd7f68a8c875603c2e2ee5c82801db5feb868e5cc35e9babe2d972d322afc50f6de6cce6431b9b2ff + languageName: node + linkType: hard + +"jackspeak@npm:^2.3.5": + version: 2.3.6 + resolution: "jackspeak@npm:2.3.6" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 10/6e6490d676af8c94a7b5b29b8fd5629f21346911ebe2e32931c2a54210134408171c24cee1a109df2ec19894ad04a429402a8438cbf5cc2794585d35428ace76 + languageName: node + linkType: hard + +"java-invoke-local@npm:0.0.6": + version: 0.0.6 + resolution: "java-invoke-local@npm:0.0.6" + bin: + java-invoke-local: lib/cli.js + checksum: 10/e433098a3d6e3d32acdff483d43e8f9d2a5ef2c08e18ca79c79bd9cb475e912ceec9d2a5e4923ff877e38eaa06f29dfac2c0b4e800eefc9d6e1df674cbf797c1 + languageName: node + linkType: hard + +"jest-changed-files@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-changed-files@npm:29.7.0" + dependencies: + execa: "npm:^5.0.0" + jest-util: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + checksum: 10/3d93742e56b1a73a145d55b66e96711fbf87ef89b96c2fab7cfdfba8ec06612591a982111ca2b712bb853dbc16831ec8b43585a2a96b83862d6767de59cbf83d + languageName: node + linkType: hard + +"jest-circus@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-circus@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/expect": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + co: "npm:^4.6.0" + dedent: "npm:^1.0.0" + is-generator-fn: "npm:^2.0.0" + jest-each: "npm:^29.7.0" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + pretty-format: "npm:^29.7.0" + pure-rand: "npm:^6.0.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10/716a8e3f40572fd0213bcfc1da90274bf30d856e5133af58089a6ce45089b63f4d679bd44e6be9d320e8390483ebc3ae9921981993986d21639d9019b523123d + languageName: node + linkType: hard + +"jest-cli@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-cli@npm:29.7.0" + dependencies: + "@jest/core": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + create-jest: "npm:^29.7.0" + exit: "npm:^0.1.2" + import-local: "npm:^3.0.2" + jest-config: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + yargs: "npm:^17.3.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10/6cc62b34d002c034203065a31e5e9a19e7c76d9e8ef447a6f70f759c0714cb212c6245f75e270ba458620f9c7b26063cd8cf6cd1f7e3afd659a7cc08add17307 + languageName: node + linkType: hard + +"jest-config@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-config@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/test-sequencer": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + babel-jest: "npm:^29.7.0" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + deepmerge: "npm:^4.2.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-circus: "npm:^29.7.0" + jest-environment-node: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-runner: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + parse-json: "npm:^5.2.0" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-json-comments: "npm:^3.1.1" + peerDependencies: + "@types/node": "*" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + ts-node: + optional: true + checksum: 10/6bdf570e9592e7d7dd5124fc0e21f5fe92bd15033513632431b211797e3ab57eaa312f83cc6481b3094b72324e369e876f163579d60016677c117ec4853cf02b + languageName: node + linkType: hard + +"jest-diff@npm:^27.5.1": + version: 27.5.1 + resolution: "jest-diff@npm:27.5.1" + dependencies: + chalk: "npm:^4.0.0" + diff-sequences: "npm:^27.5.1" + jest-get-type: "npm:^27.5.1" + pretty-format: "npm:^27.5.1" + checksum: 10/af454f30f33af625832bdb02614e188a41e33ce79086b43f95dbcc515274dd36bf8443b8d0299e22c2416e7591da4321e6bc7f2b0aef56471d1133c6b6833221 + languageName: node + linkType: hard + +"jest-diff@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-diff@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + diff-sequences: "npm:^29.6.3" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10/6f3a7eb9cd9de5ea9e5aa94aed535631fa6f80221832952839b3cb59dd419b91c20b73887deb0b62230d06d02d6b6cf34ebb810b88d904bb4fe1e2e4f0905c98 + languageName: node + linkType: hard + +"jest-docblock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-docblock@npm:29.7.0" + dependencies: + detect-newline: "npm:^3.0.0" + checksum: 10/8d48818055bc96c9e4ec2e217a5a375623c0d0bfae8d22c26e011074940c202aa2534a3362294c81d981046885c05d304376afba9f2874143025981148f3e96d + languageName: node + linkType: hard + +"jest-each@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-each@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + jest-get-type: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + pretty-format: "npm:^29.7.0" + checksum: 10/bd1a077654bdaa013b590deb5f7e7ade68f2e3289180a8c8f53bc8a49f3b40740c0ec2d3a3c1aee906f682775be2bebbac37491d80b634d15276b0aa0f2e3fda + languageName: node + linkType: hard + +"jest-environment-node@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-environment-node@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/fake-timers": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-mock: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10/9cf7045adf2307cc93aed2f8488942e39388bff47ec1df149a997c6f714bfc66b2056768973770d3f8b1bf47396c19aa564877eb10ec978b952c6018ed1bd637 + languageName: node + linkType: hard + +"jest-get-type@npm:^27.5.1": + version: 27.5.1 + resolution: "jest-get-type@npm:27.5.1" + checksum: 10/63064ab70195c21007d897c1157bf88ff94a790824a10f8c890392e7d17eda9c3900513cb291ca1c8d5722cad79169764e9a1279f7c8a9c4cd6e9109ff04bbc0 + languageName: node + linkType: hard + +"jest-get-type@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-get-type@npm:29.6.3" + checksum: 10/88ac9102d4679d768accae29f1e75f592b760b44277df288ad76ce5bf038c3f5ce3719dea8aa0f035dac30e9eb034b848ce716b9183ad7cc222d029f03e92205 + languageName: node + linkType: hard + +"jest-haste-map@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-haste-map@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/graceful-fs": "npm:^4.1.3" + "@types/node": "npm:*" + anymatch: "npm:^3.0.3" + fb-watchman: "npm:^2.0.0" + fsevents: "npm:^2.3.2" + graceful-fs: "npm:^4.2.9" + jest-regex-util: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + walker: "npm:^1.0.8" + dependenciesMeta: + fsevents: + optional: true + checksum: 10/8531b42003581cb18a69a2774e68c456fb5a5c3280b1b9b77475af9e346b6a457250f9d756bfeeae2fe6cbc9ef28434c205edab9390ee970a919baddfa08bb85 + languageName: node + linkType: hard + +"jest-leak-detector@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-leak-detector@npm:29.7.0" + dependencies: + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10/e3950e3ddd71e1d0c22924c51a300a1c2db6cf69ec1e51f95ccf424bcc070f78664813bef7aed4b16b96dfbdeea53fe358f8aeaaea84346ae15c3735758f1605 + languageName: node + linkType: hard + +"jest-matcher-utils@npm:^27.0.0": + version: 27.5.1 + resolution: "jest-matcher-utils@npm:27.5.1" + dependencies: + chalk: "npm:^4.0.0" + jest-diff: "npm:^27.5.1" + jest-get-type: "npm:^27.5.1" + pretty-format: "npm:^27.5.1" + checksum: 10/037f99878a0515581d7728ed3aed03707810f4da5a1c7ffb9d68a2c6c3180851a6ec40b559af37fbe891dde3ba12552b19e47b8188a27b6c5a53376be6907f32 + languageName: node + linkType: hard + +"jest-matcher-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-matcher-utils@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10/981904a494299cf1e3baed352f8a3bd8b50a8c13a662c509b6a53c31461f94ea3bfeffa9d5efcfeb248e384e318c87de7e3baa6af0f79674e987482aa189af40 + languageName: node + linkType: hard + +"jest-message-util@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-message-util@npm:29.7.0" + dependencies: + "@babel/code-frame": "npm:^7.12.13" + "@jest/types": "npm:^29.6.3" + "@types/stack-utils": "npm:^2.0.0" + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10/31d53c6ed22095d86bab9d14c0fa70c4a92c749ea6ceece82cf30c22c9c0e26407acdfbdb0231435dc85a98d6d65ca0d9cbcd25cd1abb377fe945e843fb770b9 + languageName: node + linkType: hard + +"jest-mock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-mock@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-util: "npm:^29.7.0" + checksum: 10/ae51d1b4f898724be5e0e52b2268a68fcd876d9b20633c864a6dd6b1994cbc48d62402b0f40f3a1b669b30ebd648821f086c26c08ffde192ced951ff4670d51c + languageName: node + linkType: hard + +"jest-pnp-resolver@npm:^1.2.2": + version: 1.2.3 + resolution: "jest-pnp-resolver@npm:1.2.3" + peerDependencies: + jest-resolve: "*" + peerDependenciesMeta: + jest-resolve: + optional: true + checksum: 10/db1a8ab2cb97ca19c01b1cfa9a9c8c69a143fde833c14df1fab0766f411b1148ff0df878adea09007ac6a2085ec116ba9a996a6ad104b1e58c20adbf88eed9b2 + languageName: node + linkType: hard + +"jest-regex-util@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-regex-util@npm:29.6.3" + checksum: 10/0518beeb9bf1228261695e54f0feaad3606df26a19764bc19541e0fc6e2a3737191904607fb72f3f2ce85d9c16b28df79b7b1ec9443aa08c3ef0e9efda6f8f2a + languageName: node + linkType: hard + +"jest-resolve-dependencies@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve-dependencies@npm:29.7.0" + dependencies: + jest-regex-util: "npm:^29.6.3" + jest-snapshot: "npm:^29.7.0" + checksum: 10/1e206f94a660d81e977bcfb1baae6450cb4a81c92e06fad376cc5ea16b8e8c6ea78c383f39e95591a9eb7f925b6a1021086c38941aa7c1b8a6a813c2f6e93675 + languageName: node + linkType: hard + +"jest-resolve@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-pnp-resolver: "npm:^1.2.2" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + resolve: "npm:^1.20.0" + resolve.exports: "npm:^2.0.0" + slash: "npm:^3.0.0" + checksum: 10/faa466fd9bc69ea6c37a545a7c6e808e073c66f46ab7d3d8a6ef084f8708f201b85d5fe1799789578b8b47fa1de47b9ee47b414d1863bc117a49e032ba77b7c7 + languageName: node + linkType: hard + +"jest-runner@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runner@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/environment": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + graceful-fs: "npm:^4.2.9" + jest-docblock: "npm:^29.7.0" + jest-environment-node: "npm:^29.7.0" + jest-haste-map: "npm:^29.7.0" + jest-leak-detector: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-resolve: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-watcher: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + source-map-support: "npm:0.5.13" + checksum: 10/9d8748a494bd90f5c82acea99be9e99f21358263ce6feae44d3f1b0cd90991b5df5d18d607e73c07be95861ee86d1cbab2a3fc6ca4b21805f07ac29d47c1da1e + languageName: node + linkType: hard + +"jest-runtime@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runtime@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/fake-timers": "npm:^29.7.0" + "@jest/globals": "npm:^29.7.0" + "@jest/source-map": "npm:^29.6.3" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + cjs-module-lexer: "npm:^1.0.0" + collect-v8-coverage: "npm:^1.0.0" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-mock: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-bom: "npm:^4.0.0" + checksum: 10/59eb58eb7e150e0834a2d0c0d94f2a0b963ae7182cfa6c63f2b49b9c6ef794e5193ef1634e01db41420c36a94cefc512cdd67a055cd3e6fa2f41eaf0f82f5a20 + languageName: node + linkType: hard + +"jest-snapshot@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-snapshot@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@babel/generator": "npm:^7.7.2" + "@babel/plugin-syntax-jsx": "npm:^7.7.2" + "@babel/plugin-syntax-typescript": "npm:^7.7.2" + "@babel/types": "npm:^7.3.3" + "@jest/expect-utils": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + babel-preset-current-node-syntax: "npm:^1.0.0" + chalk: "npm:^4.0.0" + expect: "npm:^29.7.0" + graceful-fs: "npm:^4.2.9" + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + natural-compare: "npm:^1.4.0" + pretty-format: "npm:^29.7.0" + semver: "npm:^7.5.3" + checksum: 10/cb19a3948256de5f922d52f251821f99657339969bf86843bd26cf3332eae94883e8260e3d2fba46129a27c3971c1aa522490e460e16c7fad516e82d10bbf9f8 + languageName: node + linkType: hard + +"jest-util@npm:^29.0.0, jest-util@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-util@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + graceful-fs: "npm:^4.2.9" + picomatch: "npm:^2.2.3" + checksum: 10/30d58af6967e7d42bd903ccc098f3b4d3859ed46238fbc88d4add6a3f10bea00c226b93660285f058bc7a65f6f9529cf4eb80f8d4707f79f9e3a23686b4ab8f3 + languageName: node + linkType: hard + +"jest-validate@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-validate@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + camelcase: "npm:^6.2.0" + chalk: "npm:^4.0.0" + jest-get-type: "npm:^29.6.3" + leven: "npm:^3.1.0" + pretty-format: "npm:^29.7.0" + checksum: 10/8ee1163666d8eaa16d90a989edba2b4a3c8ab0ffaa95ad91b08ca42b015bfb70e164b247a5b17f9de32d096987cada63ed8491ab82761bfb9a28bc34b27ae161 + languageName: node + linkType: hard + +"jest-watcher@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-watcher@npm:29.7.0" + dependencies: + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + jest-util: "npm:^29.7.0" + string-length: "npm:^4.0.1" + checksum: 10/4f616e0345676631a7034b1d94971aaa719f0cd4a6041be2aa299be437ea047afd4fe05c48873b7963f5687a2f6c7cbf51244be8b14e313b97bfe32b1e127e55 + languageName: node + linkType: hard + +"jest-worker@npm:^27.4.5": + version: 27.5.1 + resolution: "jest-worker@npm:27.5.1" + dependencies: + "@types/node": "npm:*" + merge-stream: "npm:^2.0.0" + supports-color: "npm:^8.0.0" + checksum: 10/06c6e2a84591d9ede704d5022fc13791e8876e83397c89d481b0063332abbb64c0f01ef4ca7de520b35c7a1058556078d6bdc3631376f4e9ffb42316c1a8488e + languageName: node + linkType: hard + +"jest-worker@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-worker@npm:29.7.0" + dependencies: + "@types/node": "npm:*" + jest-util: "npm:^29.7.0" + merge-stream: "npm:^2.0.0" + supports-color: "npm:^8.0.0" + checksum: 10/364cbaef00d8a2729fc760227ad34b5e60829e0869bd84976bdfbd8c0d0f9c2f22677b3e6dd8afa76ed174765351cd12bae3d4530c62eefb3791055127ca9745 + languageName: node + linkType: hard + +"jest@npm:^29.6.4, jest@npm:^29.7.0": + version: 29.7.0 + resolution: "jest@npm:29.7.0" + dependencies: + "@jest/core": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + import-local: "npm:^3.0.2" + jest-cli: "npm:^29.7.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10/97023d78446098c586faaa467fbf2c6b07ff06e2c85a19e3926adb5b0effe9ac60c4913ae03e2719f9c01ae8ffd8d92f6b262cedb9555ceeb5d19263d8c6362a + languageName: node + linkType: hard + +"jmespath@npm:0.16.0": + version: 0.16.0 + resolution: "jmespath@npm:0.16.0" + checksum: 10/cc8b4a5cd2a22a79fc2695d66e5a43bc0020ec1ebdbe648440e796764751af2f495771ce877dea45ee6545530f0a1528450c3c3026bc0e9d976a93447af9fb74 + languageName: node + linkType: hard + +"joi@npm:^17.4.0": + version: 17.10.2 + resolution: "joi@npm:17.10.2" + dependencies: + "@hapi/hoek": "npm:^9.0.0" + "@hapi/topo": "npm:^5.0.0" + "@sideway/address": "npm:^4.1.3" + "@sideway/formula": "npm:^3.0.1" + "@sideway/pinpoint": "npm:^2.0.0" + checksum: 10/d0d882162e57f25f43f9c59269a530c7ee24b3a275474dc7729447bc37617d5c4dba091ecf0cd62aed669a1c4832fff3be582f413c7bd1add9b798f6a506c7d8 + languageName: node + linkType: hard + +"jose@npm:^4.10.4, jose@npm:^4.14.6": + version: 4.15.1 + resolution: "jose@npm:4.15.1" + checksum: 10/ae2ae10c5ac50bf7d0504eba82272339af0712b25abe99465448339df512decf0e5b2e4b8e7f6d23dc3b615bb0a66ad0a033cdf1a66132f29284cafd22f4fdb9 + languageName: node + linkType: hard + +"js-beautify@npm:^1.14.5": + version: 1.14.9 + resolution: "js-beautify@npm:1.14.9" + dependencies: + config-chain: "npm:^1.1.13" + editorconfig: "npm:^1.0.3" + glob: "npm:^8.1.0" + nopt: "npm:^6.0.0" + bin: + css-beautify: js/bin/css-beautify.js + html-beautify: js/bin/html-beautify.js + js-beautify: js/bin/js-beautify.js + checksum: 10/a7f57bb468bf812bdb7bb0dd56500d489d14ade97db8b7da0bbeae6ee00f2e3c6bd81f31b1e12849f5f8f053840da3599a178d83e9279e3b4c9783b7d94c84bb + languageName: node + linkType: hard + +"js-sha256@npm:^0.9.0": + version: 0.9.0 + resolution: "js-sha256@npm:0.9.0" + checksum: 10/4dc16be74bf4e60d8ee2a482cc822c4d4f5cd060d9f92a060fe3ab1f143cd0946edda552cd274459251c279073df15872a5df47fc4bff054bbc3812e396e990b + languageName: node + linkType: hard + +"js-string-escape@npm:^1.0.1": + version: 1.0.1 + resolution: "js-string-escape@npm:1.0.1" + checksum: 10/f11e0991bf57e0c183b55c547acec85bd2445f043efc9ea5aa68b41bd2a3e7d3ce94636cb233ae0d84064ba4c1a505d32e969813c5b13f81e7d4be12c59256fe + languageName: node + linkType: hard + +"js-tokens@npm:^4.0.0": + version: 4.0.0 + resolution: "js-tokens@npm:4.0.0" + checksum: 10/af37d0d913fb56aec6dc0074c163cc71cd23c0b8aad5c2350747b6721d37ba118af35abdd8b33c47ec2800de07dedb16a527ca9c530ee004093e04958bd0cbf2 + languageName: node + linkType: hard + +"js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.1": + version: 3.14.1 + resolution: "js-yaml@npm:3.14.1" + dependencies: + argparse: "npm:^1.0.7" + esprima: "npm:^4.0.0" + bin: + js-yaml: bin/js-yaml.js + checksum: 10/9e22d80b4d0105b9899135365f746d47466ed53ef4223c529b3c0f7a39907743fdbd3c4379f94f1106f02755b5e90b2faaf84801a891135544e1ea475d1a1379 + languageName: node + linkType: hard + +"js-yaml@npm:^4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140 + languageName: node + linkType: hard + +"js2xmlparser@npm:^4.0.2": + version: 4.0.2 + resolution: "js2xmlparser@npm:4.0.2" + dependencies: + xmlcreate: "npm:^2.0.4" + checksum: 10/42ccb1372844b6e1d9166254b01fe31d485a0e398fba4f2b095bcca081a2c2f4414b0bd4a32263cd20e01cee681684608255778034fec050e3f5929fd776936c + languageName: node + linkType: hard + +"jsdoc@npm:^4.0.0": + version: 4.0.2 + resolution: "jsdoc@npm:4.0.2" + dependencies: + "@babel/parser": "npm:^7.20.15" + "@jsdoc/salty": "npm:^0.2.1" + "@types/markdown-it": "npm:^12.2.3" + bluebird: "npm:^3.7.2" + catharsis: "npm:^0.9.0" + escape-string-regexp: "npm:^2.0.0" + js2xmlparser: "npm:^4.0.2" + klaw: "npm:^3.0.0" + markdown-it: "npm:^12.3.2" + markdown-it-anchor: "npm:^8.4.1" + marked: "npm:^4.0.10" + mkdirp: "npm:^1.0.4" + requizzle: "npm:^0.2.3" + strip-json-comments: "npm:^3.1.0" + underscore: "npm:~1.13.2" + bin: + jsdoc: jsdoc.js + checksum: 10/1cd7e871f1d9c2af5dd8d3bb1d01b9905807fcb7df7d59e0e4d2ab424224963a145944b975f70947909dd9e246ad52d5e685cd1e7b0381a2195d44d7b7c43163 + languageName: node + linkType: hard + +"jsesc@npm:^2.5.1": + version: 2.5.2 + resolution: "jsesc@npm:2.5.2" + bin: + jsesc: bin/jsesc + checksum: 10/d2096abdcdec56969764b40ffc91d4a23408aa2f351b4d1c13f736f25476643238c43fdbaf38a191c26b1b78fd856d965f5d4d0dde7b89459cd94025190cdf13 + languageName: node + linkType: hard + +"json-bigint@npm:^1.0.0": + version: 1.0.0 + resolution: "json-bigint@npm:1.0.0" + dependencies: + bignumber.js: "npm:^9.0.0" + checksum: 10/cd3973b88e5706f8f89d2a9c9431f206ef385bd5c584db1b258891a5e6642507c32316b82745239088c697f5ddfe967351e1731f5789ba7855aed56ad5f70e1f + languageName: node + linkType: hard + +"json-buffer@npm:3.0.1": + version: 3.0.1 + resolution: "json-buffer@npm:3.0.1" + checksum: 10/82876154521b7b68ba71c4f969b91572d1beabadd87bd3a6b236f85fbc7dc4695089191ed60bb59f9340993c51b33d479f45b6ba9f3548beb519705281c32c3c + languageName: node + linkType: hard + +"json-colorizer@npm:^2.2.2": + version: 2.2.2 + resolution: "json-colorizer@npm:2.2.2" + dependencies: + chalk: "npm:^2.4.1" + lodash.get: "npm:^4.4.2" + checksum: 10/9e2015d43a6dbafc095f11dfbd83ac64017ab3382f44596c13e6366eb1be3be8d6f0f925ae0f0dfb9f8ab8a74e2d2feea10a612c3d4f2925d8c850e7211c31c8 + languageName: node + linkType: hard + +"json-cycle@npm:^1.5.0": + version: 1.5.0 + resolution: "json-cycle@npm:1.5.0" + checksum: 10/4ce7594eb8f42e820c708ceaed12759168c4d29f91e0f8e213909331f7fd12b765a3b9c4a5e8f0e72bc25d5ed2a380211ff3ec95c3ba1cbb2cb5c68ea396ae9f + languageName: node + linkType: hard + +"json-parse-better-errors@npm:^1.0.1": + version: 1.0.2 + resolution: "json-parse-better-errors@npm:1.0.2" + checksum: 10/5553232045359b767b0f2039a6777fede1a8d7dca1a0ffb1f9ef73a7519489ae7f566b2e040f2b4c38edb8e35e37ae07af7f0a52420902f869ee0dbf5dc6c784 + languageName: node + linkType: hard + +"json-parse-even-better-errors@npm:^2.3.0, json-parse-even-better-errors@npm:^2.3.1": + version: 2.3.1 + resolution: "json-parse-even-better-errors@npm:2.3.1" + checksum: 10/5f3a99009ed5f2a5a67d06e2f298cc97bc86d462034173308156f15b43a6e850be8511dc204b9b94566305da2947f7d90289657237d210351a39059ff9d666cf + languageName: node + linkType: hard + +"json-refs@npm:^3.0.15": + version: 3.0.15 + resolution: "json-refs@npm:3.0.15" + dependencies: + commander: "npm:~4.1.1" + graphlib: "npm:^2.1.8" + js-yaml: "npm:^3.13.1" + lodash: "npm:^4.17.15" + native-promise-only: "npm:^0.8.1" + path-loader: "npm:^1.0.10" + slash: "npm:^3.0.0" + uri-js: "npm:^4.2.2" + bin: + json-refs: ./bin/json-refs + checksum: 10/381a5bc91fc57a10c9df036ea1c077e60e433fe5e690a441c30384b2e5ebe82e9b746c1fb1b30dcb67d4e03ee819b625057df80c5738dc13b5056fdfc5c1432b + languageName: node + linkType: hard + +"json-schema-traverse@npm:^0.4.1": + version: 0.4.1 + resolution: "json-schema-traverse@npm:0.4.1" + checksum: 10/7486074d3ba247769fda17d5181b345c9fb7d12e0da98b22d1d71a5db9698d8b4bd900a3ec1a4ffdd60846fc2556274a5c894d0c48795f14cb03aeae7b55260b + languageName: node + linkType: hard + +"json-schema-traverse@npm:^1.0.0": + version: 1.0.0 + resolution: "json-schema-traverse@npm:1.0.0" + checksum: 10/02f2f466cdb0362558b2f1fd5e15cce82ef55d60cd7f8fa828cf35ba74330f8d767fcae5c5c2adb7851fa811766c694b9405810879bc4e1ddd78a7c0e03658ad + languageName: node + linkType: hard + +"json-stable-stringify-without-jsonify@npm:^1.0.1": + version: 1.0.1 + resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" + checksum: 10/12786c2e2f22c27439e6db0532ba321f1d0617c27ad8cb1c352a0e9249a50182fd1ba8b52a18899291604b0c32eafa8afd09e51203f19109a0537f68db2b652d + languageName: node + linkType: hard + +"json5@npm:^0.5.1": + version: 0.5.1 + resolution: "json5@npm:0.5.1" + bin: + json5: lib/cli.js + checksum: 10/1d95c1cb98d884b4620321b5361062ed0febcef78576687beec014382e51ee07a8c8118421bd327e55080e8ccc4c394f4940ee5d8aedc050b8df7b7a261c9add + languageName: node + linkType: hard + +"json5@npm:^1.0.2": + version: 1.0.2 + resolution: "json5@npm:1.0.2" + dependencies: + minimist: "npm:^1.2.0" + bin: + json5: lib/cli.js + checksum: 10/a78d812dbbd5642c4f637dd130954acfd231b074965871c3e28a5bbd571f099d623ecf9161f1960c4ddf68e0cc98dee8bebfdb94a71ad4551f85a1afc94b63f6 + languageName: node + linkType: hard + +"json5@npm:^2.2.3": + version: 2.2.3 + resolution: "json5@npm:2.2.3" + bin: + json5: lib/cli.js + checksum: 10/1db67b853ff0de3534085d630691d3247de53a2ed1390ba0ddff681ea43e9b3e30ecbdb65c5e9aab49435e44059c23dbd6fee8ee619419ba37465bb0dd7135da + languageName: node + linkType: hard + +"jsonfile@npm:^6.0.1": + version: 6.1.0 + resolution: "jsonfile@npm:6.1.0" + dependencies: + graceful-fs: "npm:^4.1.6" + universalify: "npm:^2.0.0" + dependenciesMeta: + graceful-fs: + optional: true + checksum: 10/03014769e7dc77d4cf05fa0b534907270b60890085dd5e4d60a382ff09328580651da0b8b4cdf44d91e4c8ae64d91791d965f05707beff000ed494a38b6fec85 + languageName: node + linkType: hard + +"jsonpath-plus@npm:^7.2.0": + version: 7.2.0 + resolution: "jsonpath-plus@npm:7.2.0" + checksum: 10/f602445b1aa2d55abc2875859fd948f942980ef6400ca2a0362c7a6aa6f912467865262f4d092e04a16889fa74f0dbf6fd67b9dc9583485a5059be6e0a62c6c2 + languageName: node + linkType: hard + +"jsonschema@npm:^1.4.1": + version: 1.4.1 + resolution: "jsonschema@npm:1.4.1" + checksum: 10/d7a188da7a3100a2caa362b80e98666d46607b7a7153aac405b8e758132961911c6df02d444d4700691330874e21a62639f550e856b21ddd28423690751ca9c6 + languageName: node + linkType: hard + +"jsonwebtoken@npm:^8.5.1": + version: 8.5.1 + resolution: "jsonwebtoken@npm:8.5.1" + dependencies: + jws: "npm:^3.2.2" + lodash.includes: "npm:^4.3.0" + lodash.isboolean: "npm:^3.0.3" + lodash.isinteger: "npm:^4.0.4" + lodash.isnumber: "npm:^3.0.3" + lodash.isplainobject: "npm:^4.0.6" + lodash.isstring: "npm:^4.0.1" + lodash.once: "npm:^4.0.0" + ms: "npm:^2.1.1" + semver: "npm:^5.6.0" + checksum: 10/a7b52ea570f70bea183ceca970c003f223d9d3425d72498002e9775485c7584bfa3751d1c7291dbb59738074cba288effe73591b87bec5d467622ab3a156fdb6 + languageName: node + linkType: hard + +"jsonwebtoken@npm:^9.0.0": + version: 9.0.2 + resolution: "jsonwebtoken@npm:9.0.2" + dependencies: + jws: "npm:^3.2.2" + lodash.includes: "npm:^4.3.0" + lodash.isboolean: "npm:^3.0.3" + lodash.isinteger: "npm:^4.0.4" + lodash.isnumber: "npm:^3.0.3" + lodash.isplainobject: "npm:^4.0.6" + lodash.isstring: "npm:^4.0.1" + lodash.once: "npm:^4.0.0" + ms: "npm:^2.1.1" + semver: "npm:^7.5.4" + checksum: 10/6e9b6d879cec2b27f2f3a88a0c0973edc7ba956a5d9356b2626c4fddfda969e34a3832deaf79c3e1c6c9a525bc2c4f2c2447fa477f8ac660f0017c31a59ae96b + languageName: node + linkType: hard + +"jszip@npm:^3.10.1": + version: 3.10.1 + resolution: "jszip@npm:3.10.1" + dependencies: + lie: "npm:~3.3.0" + pako: "npm:~1.0.2" + readable-stream: "npm:~2.3.6" + setimmediate: "npm:^1.0.5" + checksum: 10/bfbfbb9b0a27121330ac46ab9cdb3b4812433faa9ba4a54742c87ca441e31a6194ff70ae12acefa5fe25406c432290e68003900541d948a169b23d30c34dd984 + languageName: node + linkType: hard + +"jwa@npm:^1.4.1": + version: 1.4.1 + resolution: "jwa@npm:1.4.1" + dependencies: + buffer-equal-constant-time: "npm:1.0.1" + ecdsa-sig-formatter: "npm:1.0.11" + safe-buffer: "npm:^5.0.1" + checksum: 10/0bc002b71dd70480fedc7d442a4d2b9185a9947352a027dcb4935864ad2323c57b5d391adf968a3622b61e940cef4f3484d5813b95864539272d41cac145d6f3 + languageName: node + linkType: hard + +"jwa@npm:^2.0.0": + version: 2.0.0 + resolution: "jwa@npm:2.0.0" + dependencies: + buffer-equal-constant-time: "npm:1.0.1" + ecdsa-sig-formatter: "npm:1.0.11" + safe-buffer: "npm:^5.0.1" + checksum: 10/ab983f6685d99d13ddfbffef9b1c66309a536362a8412d49ba6e687d834a1240ce39290f30ac7dbe241e0ab6c76fee7ff795776ce534e11d148158c9b7193498 + languageName: node + linkType: hard + +"jwks-rsa@npm:^3.0.1": + version: 3.0.1 + resolution: "jwks-rsa@npm:3.0.1" + dependencies: + "@types/express": "npm:^4.17.14" + "@types/jsonwebtoken": "npm:^9.0.0" + debug: "npm:^4.3.4" + jose: "npm:^4.10.4" + limiter: "npm:^1.1.5" + lru-memoizer: "npm:^2.1.4" + checksum: 10/50c9d8f36f59133ab63d48f0b7dd3d552bffc94559fd9a2f291bdd23c353cabea8297185922104f59600ac35d0e85840fe08e1f93e37ff5c3c25606a35a7ab9b + languageName: node + linkType: hard + +"jws@npm:^3.2.2": + version: 3.2.2 + resolution: "jws@npm:3.2.2" + dependencies: + jwa: "npm:^1.4.1" + safe-buffer: "npm:^5.0.1" + checksum: 10/70b016974af8a76d25030c80a0097b24ed5b17a9cf10f43b163c11cb4eb248d5d04a3fe48c0d724d2884c32879d878ccad7be0663720f46b464f662f7ed778fe + languageName: node + linkType: hard + +"jws@npm:^4.0.0": + version: 4.0.0 + resolution: "jws@npm:4.0.0" + dependencies: + jwa: "npm:^2.0.0" + safe-buffer: "npm:^5.0.1" + checksum: 10/1d15f4cdea376c6bd6a81002bd2cb0bf3d51d83da8f0727947b5ba3e10cf366721b8c0d099bf8c1eb99eb036e2c55e5fd5efd378ccff75a2b4e0bd10002348b9 + languageName: node + linkType: hard + +"jwt-decode@npm:^2.2.0": + version: 2.2.0 + resolution: "jwt-decode@npm:2.2.0" + checksum: 10/2d368aeb1d355b58af73a422e10c44d1adfd444f77f3a95bdc124696bde9cfe8423a9e43cdbce8761eb667252e25a59b5e36d30ebf4b6d2a8f8b107c2f25f358 + languageName: node + linkType: hard + +"jwt-decode@npm:^3.1.2": + version: 3.1.2 + resolution: "jwt-decode@npm:3.1.2" + checksum: 10/20a4b072d44ce3479f42d0d2c8d3dabeb353081ba4982e40b83a779f2459a70be26441be6c160bfc8c3c6eadf9f6380a036fbb06ac5406b5674e35d8c4205eeb + languageName: node + linkType: hard + +"keyv@npm:^4.0.0, keyv@npm:^4.5.3": + version: 4.5.3 + resolution: "keyv@npm:4.5.3" + dependencies: + json-buffer: "npm:3.0.1" + checksum: 10/2c96e345ecee2c7bf8876b368190b0067308b8da080c1462486fbe71a5b863242c350f1507ddad8f373c5d886b302c42f491de4d3be725071c6743a2f1188ff2 + languageName: node + linkType: hard + +"klaw@npm:^3.0.0": + version: 3.0.0 + resolution: "klaw@npm:3.0.0" + dependencies: + graceful-fs: "npm:^4.1.9" + checksum: 10/b55bb6c5dad4f5f2431914fd4b2d0312cdd3581fc1ef75ca0b83899c76dfc4c78b6a22508a33cb8c1ebe0bbdc13aa028a05eda23ef11625a935c30687fd8be14 + languageName: node + linkType: hard + +"kleur@npm:^3.0.3": + version: 3.0.3 + resolution: "kleur@npm:3.0.3" + checksum: 10/0c0ecaf00a5c6173d25059c7db2113850b5457016dfa1d0e3ef26da4704fbb186b4938d7611246d86f0ddf1bccf26828daa5877b1f232a65e7373d0122a83e7f + languageName: node + linkType: hard + +"kuler@npm:^2.0.0": + version: 2.0.0 + resolution: "kuler@npm:2.0.0" + checksum: 10/9e10b5a1659f9ed8761d38df3c35effabffbd19fc6107324095238e4ef0ff044392cae9ac64a1c2dda26e532426485342226b93806bd97504b174b0dcf04ed81 + languageName: node + linkType: hard + +"lazystream@npm:^1.0.0": + version: 1.0.1 + resolution: "lazystream@npm:1.0.1" + dependencies: + readable-stream: "npm:^2.0.5" + checksum: 10/35f8cf8b5799c76570b211b079d4d706a20cbf13a4936d44cc7dbdacab1de6b346ab339ed3e3805f4693155ee5bbebbda4050fa2b666d61956e89a573089e3d4 + languageName: node + linkType: hard + +"leven@npm:^3.1.0": + version: 3.1.0 + resolution: "leven@npm:3.1.0" + checksum: 10/638401d534585261b6003db9d99afd244dfe82d75ddb6db5c0df412842d5ab30b2ef18de471aaec70fe69a46f17b4ae3c7f01d8a4e6580ef7adb9f4273ad1e55 + languageName: node + linkType: hard + +"levn@npm:^0.4.1": + version: 0.4.1 + resolution: "levn@npm:0.4.1" + dependencies: + prelude-ls: "npm:^1.2.1" + type-check: "npm:~0.4.0" + checksum: 10/2e4720ff79f21ae08d42374b0a5c2f664c5be8b6c8f565bb4e1315c96ed3a8acaa9de788ffed82d7f2378cf36958573de07ef92336cb5255ed74d08b8318c9ee + languageName: node + linkType: hard + +"levn@npm:~0.3.0": + version: 0.3.0 + resolution: "levn@npm:0.3.0" + dependencies: + prelude-ls: "npm:~1.1.2" + type-check: "npm:~0.3.2" + checksum: 10/e1c3e75b5c430d9aa4c32c83c8a611e4ca53608ca78e3ea3bf6bbd9d017e4776d05d86e27df7901baebd3afa732abede9f26f715b8c1be19e95505c7a3a7b589 + languageName: node + linkType: hard + +"lie@npm:~3.3.0": + version: 3.3.0 + resolution: "lie@npm:3.3.0" + dependencies: + immediate: "npm:~3.0.5" + checksum: 10/f335ce67fe221af496185d7ce39c8321304adb701e122942c495f4f72dcee8803f9315ee572f5f8e8b08b9e8d7195da91b9fad776e8864746ba8b5e910adf76e + languageName: node + linkType: hard + +"limiter@npm:^1.1.5": + version: 1.1.5 + resolution: "limiter@npm:1.1.5" + checksum: 10/fa96e9912cf33ec36387e41a09694ccac7aaa8b86e1121333c30a3dfdf6265c849c980abd5f1689021bbab9aadca9d6df58d8db6ce5b999c26dd8cefe94168a9 + languageName: node + linkType: hard + +"lines-and-columns@npm:^1.1.6": + version: 1.2.4 + resolution: "lines-and-columns@npm:1.2.4" + checksum: 10/0c37f9f7fa212b38912b7145e1cd16a5f3cd34d782441c3e6ca653485d326f58b3caccda66efce1c5812bde4961bbde3374fae4b0d11bf1226152337f3894aa5 + languageName: node + linkType: hard + +"linkify-it@npm:^3.0.1": + version: 3.0.3 + resolution: "linkify-it@npm:3.0.3" + dependencies: + uc.micro: "npm:^1.0.1" + checksum: 10/1ed466b02ad361bb5e5b94a81232fc126890751038bf3e61f648f4ccb01e5e096bba66c3eff3d21ed5e3da738de0dc29783afedf0255733669889aa09d49e47e + languageName: node + linkType: hard + +"load-json-file@npm:^4.0.0": + version: 4.0.0 + resolution: "load-json-file@npm:4.0.0" + dependencies: + graceful-fs: "npm:^4.1.2" + parse-json: "npm:^4.0.0" + pify: "npm:^3.0.0" + strip-bom: "npm:^3.0.0" + checksum: 10/8f5d6d93ba64a9620445ee9bde4d98b1eac32cf6c8c2d20d44abfa41a6945e7969456ab5f1ca2fb06ee32e206c9769a20eec7002fe290de462e8c884b6b8b356 + languageName: node + linkType: hard + +"loader-runner@npm:^4.2.0": + version: 4.3.0 + resolution: "loader-runner@npm:4.3.0" + checksum: 10/555ae002869c1e8942a0efd29a99b50a0ce6c3296efea95caf48f00d7f6f7f659203ed6613688b6181aa81dc76de3e65ece43094c6dffef3127fe1a84d973cd3 + languageName: node + linkType: hard + +"locate-path@npm:^5.0.0": + version: 5.0.0 + resolution: "locate-path@npm:5.0.0" + dependencies: + p-locate: "npm:^4.1.0" + checksum: 10/83e51725e67517287d73e1ded92b28602e3ae5580b301fe54bfb76c0c723e3f285b19252e375712316774cf52006cb236aed5704692c32db0d5d089b69696e30 + languageName: node + linkType: hard + +"locate-path@npm:^6.0.0": + version: 6.0.0 + resolution: "locate-path@npm:6.0.0" + dependencies: + p-locate: "npm:^5.0.0" + checksum: 10/72eb661788a0368c099a184c59d2fee760b3831c9c1c33955e8a19ae4a21b4116e53fa736dc086cdeb9fce9f7cc508f2f92d2d3aae516f133e16a2bb59a39f5a + languageName: node + linkType: hard + +"lodash.camelcase@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.camelcase@npm:4.3.0" + checksum: 10/c301cc379310441dc73cd6cebeb91fb254bea74e6ad3027f9346fc43b4174385153df420ffa521654e502fd34c40ef69ca4e7d40ee7129a99e06f306032bfc65 + languageName: node + linkType: hard + +"lodash.clonedeep@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.clonedeep@npm:4.5.0" + checksum: 10/957ed243f84ba6791d4992d5c222ffffca339a3b79dbe81d2eaf0c90504160b500641c5a0f56e27630030b18b8e971ea10b44f928a977d5ced3c8948841b555f + languageName: node + linkType: hard + +"lodash.defaults@npm:^4.2.0": + version: 4.2.0 + resolution: "lodash.defaults@npm:4.2.0" + checksum: 10/6a2a9ea5ad7585aff8d76836c9e1db4528e5f5fa50fc4ad81183152ba8717d83aef8aec4fa88bf3417ed946fd4b4358f145ee08fbc77fb82736788714d3e12db + languageName: node + linkType: hard + +"lodash.difference@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.difference@npm:4.5.0" + checksum: 10/b22adb1be9c60e5997b8b483f8bab19878cb40eda65437907958e5d27990214716e1b00ebe312a97f47e63d8b891e4ae30947d08e1f0861ccdb9462f56ab9d77 + languageName: node + linkType: hard + +"lodash.flatten@npm:^4.4.0": + version: 4.4.0 + resolution: "lodash.flatten@npm:4.4.0" + checksum: 10/a2b192f220b0b6c78a6c0175e96bad888b9e0f2a887a8e8c1d0c29d03231fbf110bbb9be0d9de5f936537d143eeb9d5b4f44c4a44f5592c195bf2fae6a6b1e3a + languageName: node + linkType: hard + +"lodash.get@npm:^4.4.2": + version: 4.4.2 + resolution: "lodash.get@npm:4.4.2" + checksum: 10/2a4925f6e89bc2c010a77a802d1ba357e17ed1ea03c2ddf6a146429f2856a216663e694a6aa3549a318cbbba3fd8b7decb392db457e6ac0b83dc745ed0a17380 + languageName: node + linkType: hard + +"lodash.includes@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.includes@npm:4.3.0" + checksum: 10/45e0a7c7838c931732cbfede6327da321b2b10482d5063ed21c020fa72b09ca3a4aa3bda4073906ab3f436cf36eb85a52ea3f08b7bab1e0baca8235b0e08fe51 + languageName: node + linkType: hard + +"lodash.isboolean@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isboolean@npm:3.0.3" + checksum: 10/b70068b4a8b8837912b54052557b21fc4774174e3512ed3c5b94621e5aff5eb6c68089d0a386b7e801d679cd105d2e35417978a5e99071750aa2ed90bffd0250 + languageName: node + linkType: hard + +"lodash.isempty@npm:^4.4.0": + version: 4.4.0 + resolution: "lodash.isempty@npm:4.4.0" + checksum: 10/b69de4e08038f3d802fa2f510fd97f6b1785a359a648382ba30fb59e17ce0bcdad9bef2cdb9f9501abb9064c74c6edbb8db86a6d827e0d380a50a6738e051ec3 + languageName: node + linkType: hard + +"lodash.isinteger@npm:^4.0.4": + version: 4.0.4 + resolution: "lodash.isinteger@npm:4.0.4" + checksum: 10/c971f5a2d67384f429892715550c67bac9f285604a0dd79275fd19fef7717aec7f2a6a33d60769686e436ceb9771fd95fe7fcb68ad030fc907d568d5a3b65f70 + languageName: node + linkType: hard + +"lodash.isnumber@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isnumber@npm:3.0.3" + checksum: 10/913784275b565346255e6ae6a6e30b760a0da70abc29f3e1f409081585875105138cda4a429ff02577e1bc0a7ae2a90e0a3079a37f3a04c3d6c5aaa532f4cab2 + languageName: node + linkType: hard + +"lodash.isobject@npm:^3.0.2": + version: 3.0.2 + resolution: "lodash.isobject@npm:3.0.2" + checksum: 10/6c1667cbc4494d0a13a3617a4b23278d6d02dac520311f2bbb43f16f2cf71d2e6eb9dec8057315b77459df4890c756a256a087d3f4baa44a79ab5d6c968b060e + languageName: node + linkType: hard + +"lodash.isplainobject@npm:^4.0.6": + version: 4.0.6 + resolution: "lodash.isplainobject@npm:4.0.6" + checksum: 10/29c6351f281e0d9a1d58f1a4c8f4400924b4c79f18dfc4613624d7d54784df07efaff97c1ff2659f3e085ecf4fff493300adc4837553104cef2634110b0d5337 + languageName: node + linkType: hard + +"lodash.isstring@npm:^4.0.1": + version: 4.0.1 + resolution: "lodash.isstring@npm:4.0.1" + checksum: 10/eaac87ae9636848af08021083d796e2eea3d02e80082ab8a9955309569cb3a463ce97fd281d7dc119e402b2e7d8c54a23914b15d2fc7fff56461511dc8937ba0 + languageName: node + linkType: hard + +"lodash.memoize@npm:4.x": + version: 4.1.2 + resolution: "lodash.memoize@npm:4.1.2" + checksum: 10/192b2168f310c86f303580b53acf81ab029761b9bd9caa9506a019ffea5f3363ea98d7e39e7e11e6b9917066c9d36a09a11f6fe16f812326390d8f3a54a1a6da + languageName: node + linkType: hard + +"lodash.merge@npm:^4.6.2": + version: 4.6.2 + resolution: "lodash.merge@npm:4.6.2" + checksum: 10/d0ea2dd0097e6201be083865d50c3fb54fbfbdb247d9cc5950e086c991f448b7ab0cdab0d57eacccb43473d3f2acd21e134db39f22dac2d6c9ba6bf26978e3d6 + languageName: node + linkType: hard + +"lodash.once@npm:^4.0.0": + version: 4.1.1 + resolution: "lodash.once@npm:4.1.1" + checksum: 10/202f2c8c3d45e401b148a96de228e50ea6951ee5a9315ca5e15733d5a07a6b1a02d9da1e7fdf6950679e17e8ca8f7190ec33cae47beb249b0c50019d753f38f3 + languageName: node + linkType: hard + +"lodash.union@npm:^4.6.0": + version: 4.6.0 + resolution: "lodash.union@npm:4.6.0" + checksum: 10/175f5786efc527238c1350ce561c28e5ba527b5957605f9e5b8a804fce78801d09ced7b72de0302325e5b14c711f94690b1a733c13ad3674cc1a76e1172db1f8 + languageName: node + linkType: hard + +"lodash.upperfirst@npm:^4.3.1": + version: 4.3.1 + resolution: "lodash.upperfirst@npm:4.3.1" + checksum: 10/3e849d4eb4dbf26faee6435edda8e707b65a5dbd2f10f8def5a16a57bbbf38d3b7506950f0dd455e9c46ba73af35f1de75df4ef83952106949413d64eed59333 + languageName: node + linkType: hard + +"lodash@npm:^4.17.11, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 + languageName: node + linkType: hard + +"log-node@npm:^8.0.3": + version: 8.0.3 + resolution: "log-node@npm:8.0.3" + dependencies: + ansi-regex: "npm:^5.0.1" + cli-color: "npm:^2.0.1" + cli-sprintf-format: "npm:^1.1.1" + d: "npm:^1.0.1" + es5-ext: "npm:^0.10.53" + sprintf-kit: "npm:^2.0.1" + supports-color: "npm:^8.1.1" + type: "npm:^2.5.0" + peerDependencies: + log: ^6.0.0 + checksum: 10/32a085b7f16a32b110536f1c6c2768235c471343a7a988285af21ce2beab3dadf21d0c2e923ce81f60e2efc9047fe48d8d0940e76ce0c1188324ce8bf62ea4c1 + languageName: node + linkType: hard + +"log-symbols@npm:^4.1.0": + version: 4.1.0 + resolution: "log-symbols@npm:4.1.0" + dependencies: + chalk: "npm:^4.1.0" + is-unicode-supported: "npm:^0.1.0" + checksum: 10/fce1497b3135a0198803f9f07464165e9eb83ed02ceb2273930a6f8a508951178d8cf4f0378e9d28300a2ed2bc49050995d2bd5f53ab716bb15ac84d58c6ef74 + languageName: node + linkType: hard + +"log@npm:^6.0.0, log@npm:^6.3.1": + version: 6.3.1 + resolution: "log@npm:6.3.1" + dependencies: + d: "npm:^1.0.1" + duration: "npm:^0.2.2" + es5-ext: "npm:^0.10.53" + event-emitter: "npm:^0.3.5" + sprintf-kit: "npm:^2.0.1" + type: "npm:^2.5.0" + uni-global: "npm:^1.0.0" + checksum: 10/d452894862ba9188ee48a192e0aad4a67e94b0e8b6310dd78c7619110465905ac2187b7ad0ae41e1207670fdfee664f5ae98b224bc0c254234cfee222f50aa06 + languageName: node + linkType: hard + +"logform@npm:^2.3.2, logform@npm:^2.4.0": + version: 2.5.1 + resolution: "logform@npm:2.5.1" + dependencies: + "@colors/colors": "npm:1.5.0" + "@types/triple-beam": "npm:^1.3.2" + fecha: "npm:^4.2.0" + ms: "npm:^2.1.1" + safe-stable-stringify: "npm:^2.3.1" + triple-beam: "npm:^1.3.0" + checksum: 10/8f8add6f6a9b1cd03b7d093bf4a7577a45803c771d37ac04833d4507f79523f26e4ce70638828e7693e2fc8cd52d89a7a8e3738ed0e9762d8b3737c6ec04da39 + languageName: node + linkType: hard + +"long-timeout@npm:0.1.1": + version: 0.1.1 + resolution: "long-timeout@npm:0.1.1" + checksum: 10/48668e5362cb74c4b77a6b833d59f149b9bb9e99c5a5097609807e2597cd0920613b2a42b89bd0870848298be3691064d95599a04ae010023d07dba39932afa7 + languageName: node + linkType: hard + +"long@npm:^4.0.0": + version: 4.0.0 + resolution: "long@npm:4.0.0" + checksum: 10/8296e2ba7bab30f9cfabb81ebccff89c819af6a7a78b4bb5a70ea411aa764ee0532f7441381549dfa6a1a98d72abe9138bfcf99f4fa41238629849bc035b845b + languageName: node + linkType: hard + +"long@npm:^5.0.0, long@npm:^5.2.1": + version: 5.2.3 + resolution: "long@npm:5.2.3" + checksum: 10/9167ec6947a825b827c30da169a7384eec6c0c9ec2f0b9c74da2e93d81159bbe39fb09c3f13dae9721d4b807ccfa09797a7dd1012f5d478e3e33ca3c78b608e6 + languageName: node + linkType: hard + +"lowercase-keys@npm:^2.0.0": + version: 2.0.0 + resolution: "lowercase-keys@npm:2.0.0" + checksum: 10/1c233d2da35056e8c49fae8097ee061b8c799b2f02e33c2bf32f9913c7de8fb481ab04dab7df35e94156c800f5f34e99acbf32b21781d87c3aa43ef7b748b79e + languageName: node + linkType: hard + +"lru-cache@npm:^5.1.1": + version: 5.1.1 + resolution: "lru-cache@npm:5.1.1" + dependencies: + yallist: "npm:^3.0.2" + checksum: 10/951d2673dcc64a7fb888bf3d13bc2fdf923faca97d89cdb405ba3dfff77e2b26e5798d405e78fcd7094c9e7b8b4dab2ddc5a4f8a11928af24a207b7c738ca3f8 + languageName: node + linkType: hard + +"lru-cache@npm:^6.0.0": + version: 6.0.0 + resolution: "lru-cache@npm:6.0.0" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10/fc1fe2ee205f7c8855fa0f34c1ab0bcf14b6229e35579ec1fd1079f31d6fc8ef8eb6fd17f2f4d99788d7e339f50e047555551ebd5e434dda503696e7c6591825 + languageName: node + linkType: hard + +"lru-cache@npm:^7.14.1, lru-cache@npm:^7.7.1": + version: 7.18.3 + resolution: "lru-cache@npm:7.18.3" + checksum: 10/6029ca5aba3aacb554e919d7ef804fffd4adfc4c83db00fac8248c7c78811fb6d4b6f70f7fd9d55032b3823446546a007edaa66ad1f2377ae833bd983fac5d98 + languageName: node + linkType: hard + +"lru-cache@npm:^8.0.0": + version: 8.0.5 + resolution: "lru-cache@npm:8.0.5" + checksum: 10/74153ab136d0c2d735003b8b1c0fa8213c94c2520701dfe8bb31d957f975b3d3665b1ef27ac9a5b9f92c8f581c79008834c0f9bd60c5adf368476f9a95e8fa82 + languageName: node + linkType: hard + +"lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.0.1 + resolution: "lru-cache@npm:10.0.1" + checksum: 10/5bb91a97a342a41fd049c3494b44d9e21a7d4843f9284d0a0b26f00bb0e436f1f627d0641c78f88be16b86b4231546c5ee4f284733fb530c7960f0bcd7579026 + languageName: node + linkType: hard + +"lru-cache@npm:~4.0.0": + version: 4.0.2 + resolution: "lru-cache@npm:4.0.2" + dependencies: + pseudomap: "npm:^1.0.1" + yallist: "npm:^2.0.0" + checksum: 10/2ff07a37d71dd8936a29328a0b7263f1f9eb02e4e05b7313dd2b159d8c1a79da144562b23b95bbf61c985b6a110451d415fd269fb4171ccdf539378c2e6b3d7b + languageName: node + linkType: hard + +"lru-memoizer@npm:^2.1.4": + version: 2.2.0 + resolution: "lru-memoizer@npm:2.2.0" + dependencies: + lodash.clonedeep: "npm:^4.5.0" + lru-cache: "npm:~4.0.0" + checksum: 10/a13361a11c64bc5af1a7cadba4c24b2afbe11396533e222ed092723a6928e27e14f56c1402535667d4e801d8ee49a9c0fd73a20bd6806f5f92d2f4ba102026ec + languageName: node + linkType: hard + +"lru-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "lru-queue@npm:0.1.0" + dependencies: + es5-ext: "npm:~0.10.2" + checksum: 10/55b08ee3a7dbefb7d8ee2d14e0a97c69a887f78bddd9e28a687a1944b57e09513d4b401db515279e8829d52331df12a767f3ed27ca67c3322c723cc25c06403f + languageName: node + linkType: hard + +"luxon@npm:^3.2.0, luxon@npm:^3.2.1": + version: 3.4.3 + resolution: "luxon@npm:3.4.3" + checksum: 10/b155c9961cf45dadae763b0ec2f5a38d81a2197714154c1dece3ed3a553f1984a34138c1856f248863c998cb623796b27de96b7f7286acdeae68220451e24540 + languageName: node + linkType: hard + +"make-dir@npm:^1.0.0": + version: 1.3.0 + resolution: "make-dir@npm:1.3.0" + dependencies: + pify: "npm:^3.0.0" + checksum: 10/c564f6e7bb5ace1c02ad56b3a5f5e07d074af0c0b693c55c7b2c2b148882827c8c2afc7b57e43338a9f90c125b58d604e8cf3e6990a48bf949dfea8c79668c0b + languageName: node + linkType: hard + +"make-dir@npm:^3.1.0": + version: 3.1.0 + resolution: "make-dir@npm:3.1.0" + dependencies: + semver: "npm:^6.0.0" + checksum: 10/484200020ab5a1fdf12f393fe5f385fc8e4378824c940fba1729dcd198ae4ff24867bc7a5646331e50cead8abff5d9270c456314386e629acec6dff4b8016b78 + languageName: node + linkType: hard + +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 10/bf0731a2dd3aab4db6f3de1585cea0b746bb73eb5a02e3d8d72757e376e64e6ada190b1eddcde5b2f24a81b688a9897efd5018737d05e02e2a671dda9cff8a8a + languageName: node + linkType: hard + +"make-error@npm:1.x, make-error@npm:^1.1.1": + version: 1.3.6 + resolution: "make-error@npm:1.3.6" + checksum: 10/b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^11.0.3": + version: 11.1.1 + resolution: "make-fetch-happen@npm:11.1.1" + dependencies: + agentkeepalive: "npm:^4.2.1" + cacache: "npm:^17.0.0" + http-cache-semantics: "npm:^4.1.1" + http-proxy-agent: "npm:^5.0.0" + https-proxy-agent: "npm:^5.0.0" + is-lambda: "npm:^1.0.1" + lru-cache: "npm:^7.7.1" + minipass: "npm:^5.0.0" + minipass-fetch: "npm:^3.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + promise-retry: "npm:^2.0.1" + socks-proxy-agent: "npm:^7.0.0" + ssri: "npm:^10.0.0" + checksum: 10/b4b442cfaaec81db159f752a5f2e3ee3d7aa682782868fa399200824ec6298502e01bdc456e443dc219bcd5546c8e4471644d54109c8599841dc961d17a805fa + languageName: node + linkType: hard + +"make-fetch-happen@npm:^9.1.0": + version: 9.1.0 + resolution: "make-fetch-happen@npm:9.1.0" + dependencies: + agentkeepalive: "npm:^4.1.3" + cacache: "npm:^15.2.0" + http-cache-semantics: "npm:^4.1.0" + http-proxy-agent: "npm:^4.0.1" + https-proxy-agent: "npm:^5.0.0" + is-lambda: "npm:^1.0.1" + lru-cache: "npm:^6.0.0" + minipass: "npm:^3.1.3" + minipass-collect: "npm:^1.0.2" + minipass-fetch: "npm:^1.3.2" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.2" + promise-retry: "npm:^2.0.1" + socks-proxy-agent: "npm:^6.0.0" + ssri: "npm:^8.0.0" + checksum: 10/a868e74fc223a78afb7a1f8115133befdffae84f07a5f5dd9317cbf9f784a8373f28829a73ae3f31060e1b0cb4944e73257733c3b10c314354060fab412b6028 + languageName: node + linkType: hard + +"makeerror@npm:1.0.12": + version: 1.0.12 + resolution: "makeerror@npm:1.0.12" + dependencies: + tmpl: "npm:1.0.5" + checksum: 10/4c66ddfc654537333da952c084f507fa4c30c707b1635344eb35be894d797ba44c901a9cebe914aa29a7f61357543ba09b09dddbd7f65b4aee756b450f169f40 + languageName: node + linkType: hard + +"markdown-it-anchor@npm:^8.4.1": + version: 8.6.7 + resolution: "markdown-it-anchor@npm:8.6.7" + peerDependencies: + "@types/markdown-it": "*" + markdown-it: "*" + checksum: 10/1b061e9c8fb093dab6040725f9f3cedae7da1160a14ee8f29d144534be7ee5c788f02a4de4019f55eb8514cae5f12d350baaa7d08732c26a62abc60e5e66c7f7 + languageName: node + linkType: hard + +"markdown-it@npm:^12.3.2": + version: 12.3.2 + resolution: "markdown-it@npm:12.3.2" + dependencies: + argparse: "npm:^2.0.1" + entities: "npm:~2.1.0" + linkify-it: "npm:^3.0.1" + mdurl: "npm:^1.0.1" + uc.micro: "npm:^1.0.5" + bin: + markdown-it: bin/markdown-it.js + checksum: 10/d83d794bfb9f5e05750b25db401d9c1f9b97c6bbabb6cfd78988bb98652c62c24417435487238e2b91fd4e495547ae8c9429fb4c69e9f5bf49bd0dd292d53f24 + languageName: node + linkType: hard + +"marked@npm:^4.0.10": + version: 4.3.0 + resolution: "marked@npm:4.3.0" + bin: + marked: bin/marked.js + checksum: 10/c830bb4cb3705b754ca342b656e8a582d7428706b2678c898b856f6030c134ce2d1e19136efa3e6a1841f7330efbd24963d6bdeddc57d2938e906250f99895d0 + languageName: node + linkType: hard + +"md5.js@npm:^1.3.4": + version: 1.3.5 + resolution: "md5.js@npm:1.3.5" + dependencies: + hash-base: "npm:^3.0.0" + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.1.2" + checksum: 10/098494d885684bcc4f92294b18ba61b7bd353c23147fbc4688c75b45cb8590f5a95fd4584d742415dcc52487f7a1ef6ea611cfa1543b0dc4492fe026357f3f0c + languageName: node + linkType: hard + +"mdurl@npm:^1.0.1": + version: 1.0.1 + resolution: "mdurl@npm:1.0.1" + checksum: 10/ada367d01c9e81d07328101f187d5bd8641b71f33eab075df4caed935a24fa679e625f07108801d8250a5e4a99e5cd4be7679957a11424a3aa3e740d2bb2d5cb + languageName: node + linkType: hard + +"memfs@npm:^3.4.1": + version: 3.5.3 + resolution: "memfs@npm:3.5.3" + dependencies: + fs-monkey: "npm:^1.0.4" + checksum: 10/7c9cdb453a6b06e87f11e2dbe6c518fd3c1c1581b370ffa24f42f3fd5b1db8c2203f596e43321a0032963f3e9b66400f2c3cf043904ac496d6ae33eafd0878fe + languageName: node + linkType: hard + +"memoizee@npm:^0.4.14, memoizee@npm:^0.4.15": + version: 0.4.15 + resolution: "memoizee@npm:0.4.15" + dependencies: + d: "npm:^1.0.1" + es5-ext: "npm:^0.10.53" + es6-weak-map: "npm:^2.0.3" + event-emitter: "npm:^0.3.5" + is-promise: "npm:^2.2.2" + lru-queue: "npm:^0.1.0" + next-tick: "npm:^1.1.0" + timers-ext: "npm:^0.1.7" + checksum: 10/3c72cc59ae721e40980b604479e11e7d702f4167943f40f1e5c5d5da95e4b2664eec49ae533b2d41ffc938f642f145b48389ee4099e0945996fcf297e3dcb221 + languageName: node + linkType: hard + +"memorystream@npm:^0.3.1": + version: 0.3.1 + resolution: "memorystream@npm:0.3.1" + checksum: 10/2e34a1e35e6eb2e342f788f75f96c16f115b81ff6dd39e6c2f48c78b464dbf5b1a4c6ebfae4c573bd0f8dbe8c57d72bb357c60523be184655260d25855c03902 + languageName: node + linkType: hard + +"merge-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-stream@npm:2.0.0" + checksum: 10/6fa4dcc8d86629705cea944a4b88ef4cb0e07656ebf223fa287443256414283dd25d91c1cd84c77987f2aec5927af1a9db6085757cb43d90eb170ebf4b47f4f4 + languageName: node + linkType: hard + +"merge2@npm:^1.3.0, merge2@npm:^1.4.1": + version: 1.4.1 + resolution: "merge2@npm:1.4.1" + checksum: 10/7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 + languageName: node + linkType: hard + +"methods@npm:^1.1.2": + version: 1.1.2 + resolution: "methods@npm:1.1.2" + checksum: 10/a385dd974faa34b5dd021b2bbf78c722881bf6f003bfe6d391d7da3ea1ed625d1ff10ddd13c57531f628b3e785be38d3eed10ad03cebd90b76932413df9a1820 + languageName: node + linkType: hard + +"micromatch@npm:^4.0.0, micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": + version: 4.0.5 + resolution: "micromatch@npm:4.0.5" + dependencies: + braces: "npm:^3.0.2" + picomatch: "npm:^2.3.1" + checksum: 10/a749888789fc15cac0e03273844dbd749f9f8e8d64e70c564bcf06a033129554c789bb9e30d7566d7ff6596611a08e58ac12cf2a05f6e3c9c47c50c4c7e12fa2 + languageName: node + linkType: hard + +"mime-db@npm:1.52.0, mime-db@npm:>= 1.43.0 < 2, mime-db@npm:^1.28.0, mime-db@npm:^1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 10/54bb60bf39e6f8689f6622784e668a3d7f8bed6b0d886f5c3c446cb3284be28b30bf707ed05d0fe44a036f8469976b2629bbea182684977b084de9da274694d7 + languageName: node + linkType: hard + +"mime-types@npm:^2.0.8, mime-types@npm:^2.1.12, mime-types@npm:^2.1.27": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: "npm:1.52.0" + checksum: 10/89aa9651b67644035de2784a6e665fc685d79aba61857e02b9c8758da874a754aed4a9aced9265f5ed1171fd934331e5516b84a7f0218031b6fa0270eca1e51a + languageName: node + linkType: hard + +"mime@npm:2.6.0": + version: 2.6.0 + resolution: "mime@npm:2.6.0" + bin: + mime: cli.js + checksum: 10/7da117808b5cd0203bb1b5e33445c330fe213f4d8ee2402a84d62adbde9716ca4fb90dd6d9ab4e77a4128c6c5c24a9c4c9f6a4d720b095b1b342132d02dba58d + languageName: node + linkType: hard + +"mime@npm:^3.0.0": + version: 3.0.0 + resolution: "mime@npm:3.0.0" + bin: + mime: cli.js + checksum: 10/b2d31580deb58be89adaa1877cbbf152b7604b980fd7ef8f08b9e96bfedf7d605d9c23a8ba62aa12c8580b910cd7c1d27b7331d0f40f7a14e17d5a0bbec3b49f + languageName: node + linkType: hard + +"mimic-fn@npm:^2.1.0": + version: 2.1.0 + resolution: "mimic-fn@npm:2.1.0" + checksum: 10/d2421a3444848ce7f84bd49115ddacff29c15745db73f54041edc906c14b131a38d05298dae3081667627a59b2eb1ca4b436ff2e1b80f69679522410418b478a + languageName: node + linkType: hard + +"mimic-fn@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-fn@npm:4.0.0" + checksum: 10/995dcece15ee29aa16e188de6633d43a3db4611bcf93620e7e62109ec41c79c0f34277165b8ce5e361205049766e371851264c21ac64ca35499acb5421c2ba56 + languageName: node + linkType: hard + +"mimic-response@npm:^1.0.0": + version: 1.0.1 + resolution: "mimic-response@npm:1.0.1" + checksum: 10/034c78753b0e622bc03c983663b1cdf66d03861050e0c8606563d149bc2b02d63f62ce4d32be4ab50d0553ae0ffe647fc34d1f5281184c6e1e8cf4d85e8d9823 + languageName: node + linkType: hard + +"mimic-response@npm:^3.1.0": + version: 3.1.0 + resolution: "mimic-response@npm:3.1.0" + checksum: 10/7e719047612411fe071332a7498cf0448bbe43c485c0d780046c76633a771b223ff49bd00267be122cedebb897037fdb527df72335d0d0f74724604ca70b37ad + languageName: node + linkType: hard + +"minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-assert@npm:1.0.1" + checksum: 10/cc7974a9268fbf130fb055aff76700d7e2d8be5f761fb5c60318d0ed010d839ab3661a533ad29a5d37653133385204c503bfac995aaa4236f4e847461ea32ba7 + languageName: node + linkType: hard + +"minimalistic-crypto-utils@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-crypto-utils@npm:1.0.1" + checksum: 10/6e8a0422b30039406efd4c440829ea8f988845db02a3299f372fceba56ffa94994a9c0f2fd70c17f9969eedfbd72f34b5070ead9656a34d3f71c0bd72583a0ed + languageName: node + linkType: hard + +"minimatch@npm:9.0.1": + version: 9.0.1 + resolution: "minimatch@npm:9.0.1" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10/b4e98f4dc740dcf33999a99af23ae6e5e1c47632f296dc95cb649a282150f92378d41434bf64af4ea2e5975255a757d031c3bf014bad9214544ac57d97f3ba63 + languageName: node + linkType: hard + +"minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10/e0b25b04cd4ec6732830344e5739b13f8690f8a012d73445a4a19fbc623f5dd481ef7a5827fde25954cd6026fede7574cc54dc4643c99d6c6b653d6203f94634 + languageName: node + linkType: hard + +"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10/126b36485b821daf96d33b5c821dac600cc1ab36c87e7a532594f9b1652b1fa89a1eebcaad4dff17c764dce1a7ac1531327f190fed5f97d8f6e5f889c116c429 + languageName: node + linkType: hard + +"minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10/c81b47d28153e77521877649f4bab48348d10938df9e8147a58111fe00ef89559a2938de9f6632910c4f7bf7bb5cd81191a546167e58d357f0cfb1e18cecc1c5 + languageName: node + linkType: hard + +"minimist@npm:^1.2.0, minimist@npm:^1.2.6": + version: 1.2.8 + resolution: "minimist@npm:1.2.8" + checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f + languageName: node + linkType: hard + +"minipass-collect@npm:^1.0.2": + version: 1.0.2 + resolution: "minipass-collect@npm:1.0.2" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10/14df761028f3e47293aee72888f2657695ec66bd7d09cae7ad558da30415fdc4752bbfee66287dcc6fd5e6a2fa3466d6c484dc1cbd986525d9393b9523d97f10 + languageName: node + linkType: hard + +"minipass-fetch@npm:^1.3.2": + version: 1.4.1 + resolution: "minipass-fetch@npm:1.4.1" + dependencies: + encoding: "npm:^0.1.12" + minipass: "npm:^3.1.0" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.0.0" + dependenciesMeta: + encoding: + optional: true + checksum: 10/4c6f678d2c976c275ba35735aa18e341401d1fb94bbf38a36bb2c2d01835ac699f15b7ab1adaf4ee40a751361527d312a18853feaf9c0121f4904f811656575a + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.4 + resolution: "minipass-fetch@npm:3.0.4" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: 10/3edf72b900e30598567eafe96c30374432a8709e61bb06b87198fa3192d466777e2ec21c52985a0999044fa6567bd6f04651585983a1cbb27e2c1770a07ed2a2 + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10/56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.2, minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10/b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10/40982d8d836a52b0f37049a0a7e5d0f089637298e6d9b45df9c115d4f0520682a78258905e5c8b180fb41b593b0a82cc1361d2c74b45f7ada66334f84d1ecfdd + languageName: node + linkType: hard + +"minipass@npm:^3.0.0, minipass@npm:^3.1.0, minipass@npm:^3.1.1, minipass@npm:^3.1.3": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10/a5c6ef069f70d9a524d3428af39f2b117ff8cd84172e19b754e7264a33df460873e6eb3d6e55758531580970de50ae950c496256bb4ad3691a2974cddff189f0 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 10/61682162d29f45d3152b78b08bab7fb32ca10899bc5991ffe98afc18c9e9543bd1e3be94f8b8373ba6262497db63607079dc242ea62e43e7b2270837b7347c93 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.3": + version: 7.0.4 + resolution: "minipass@npm:7.0.4" + checksum: 10/e864bd02ceb5e0707696d58f7ce3a0b89233f0d686ef0d447a66db705c0846a8dc6f34865cd85256c1472ff623665f616b90b8ff58058b2ad996c5de747d2d18 + languageName: node + linkType: hard + +"minizlib@npm:^2.0.0, minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: 10/ae0f45436fb51344dcb87938446a32fbebb540d0e191d63b35e1c773d47512e17307bf54aa88326cc6d176594d00e4423563a091f7266c2f9a6872cdc1e234d1 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: 10/d71b8dcd4b5af2fe13ecf3bd24070263489404fe216488c5ba7e38ece1f54daf219e72a833a3a2dc404331e870e9f44963a33399589490956bff003a3404d3b2 + languageName: node + linkType: hard + +"moment-timezone@npm:^0.5.43": + version: 0.5.43 + resolution: "moment-timezone@npm:0.5.43" + dependencies: + moment: "npm:^2.29.4" + checksum: 10/f8b66f8562960d6c2ec90ea7e2ca8c10bd5f5cf5ced2eaaac83deb1011b145d0154e8d77018cf5e913d489898a343122a3d815768809653ab039306dce1db1eb + languageName: node + linkType: hard + +"moment@npm:^2.29.4": + version: 2.29.4 + resolution: "moment@npm:2.29.4" + checksum: 10/157c5af5a0ba8196e577bc67feb583303191d21ba1f7f2af30b3b40d4c63a64d505ba402be2a1454832082fac6be69db1e0d186c3279dae191e6634b0c33705c + languageName: node + linkType: hard + +"ms@npm:2.0.0": + version: 2.0.0 + resolution: "ms@npm:2.0.0" + checksum: 10/0e6a22b8b746d2e0b65a430519934fefd41b6db0682e3477c10f60c76e947c4c0ad06f63ffdf1d78d335f83edee8c0aa928aa66a36c7cd95b69b26f468d527f4 + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: 10/673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f + languageName: node + linkType: hard + +"ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d + languageName: node + linkType: hard + +"mute-stream@npm:0.0.8": + version: 0.0.8 + resolution: "mute-stream@npm:0.0.8" + checksum: 10/a2d2e79dde87e3424ffc8c334472c7f3d17b072137734ca46e6f221131f1b014201cc593b69a38062e974fb2394d3d1cb4349f80f012bbf8b8ac1b28033e515f + languageName: node + linkType: hard + +"mysql2@npm:^2.2.5": + version: 2.3.3 + resolution: "mysql2@npm:2.3.3" + dependencies: + denque: "npm:^2.0.1" + generate-function: "npm:^2.3.1" + iconv-lite: "npm:^0.6.3" + long: "npm:^4.0.0" + lru-cache: "npm:^6.0.0" + named-placeholders: "npm:^1.1.2" + seq-queue: "npm:^0.0.5" + sqlstring: "npm:^2.3.2" + checksum: 10/af101e46c3f342d80f85dce6053f771c9ca7fd2cdfef993226eee11edefbb01a4740f1e9674014dcd8c948c53711553c5a523d3105364f40a120318e87dc8049 + languageName: node + linkType: hard + +"mysql2@npm:^3.5.2, mysql2@npm:^3.6.1": + version: 3.6.1 + resolution: "mysql2@npm:3.6.1" + dependencies: + denque: "npm:^2.1.0" + generate-function: "npm:^2.3.1" + iconv-lite: "npm:^0.6.3" + long: "npm:^5.2.1" + lru-cache: "npm:^8.0.0" + named-placeholders: "npm:^1.1.3" + seq-queue: "npm:^0.0.5" + sqlstring: "npm:^2.3.2" + checksum: 10/903b44bbc5a59ed50ddc84489d5037d7e41e7de4fed9ccee87f850a6826714148ec944c56b186ddf6976bb3dea28d2d5d6bdb581e1855ce538c0155183ada3be + languageName: node + linkType: hard + +"mysql@npm:^2.18.1": + version: 2.18.1 + resolution: "mysql@npm:2.18.1" + dependencies: + bignumber.js: "npm:9.0.0" + readable-stream: "npm:2.3.7" + safe-buffer: "npm:5.1.2" + sqlstring: "npm:2.3.1" + checksum: 10/87d80e374717d7767d3e609f7f5e09987fa4dee208ba346ff269fffd2500719dcf2f65ac86c8e77649c3d52b86811a88e33cfd06e7e4a48cec53ecd4ac85c08d + languageName: node + linkType: hard + +"named-placeholders@npm:^1.1.2, named-placeholders@npm:^1.1.3": + version: 1.1.3 + resolution: "named-placeholders@npm:1.1.3" + dependencies: + lru-cache: "npm:^7.14.1" + checksum: 10/7834adc91e92ae1b9c4413384e3ccd297de5168bb44017ff0536705ddc4db421723bd964607849265feb3f6ded390f84cf138e5925f22f7c13324f87a803dc73 + languageName: node + linkType: hard + +"nan@npm:^2.14.0": + version: 2.18.0 + resolution: "nan@npm:2.18.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10/5520e22c64e2b5b495b1d765d6334c989b848bbe1502fec89c5857cabcc7f9f0474563377259e7574bff1c8a041d3b90e9ffa1f5e15502ffddee7b2550cc26a0 + languageName: node + linkType: hard + +"native-promise-only@npm:^0.8.1": + version: 0.8.1 + resolution: "native-promise-only@npm:0.8.1" + checksum: 10/fbc99d8dc2863658260a519557b0634c45583d9412de85fd706fa4fa9b11bfe4660bdac53f29171c4b2ad01a4b201c658a56f82431d19a5c155cc092c5127170 + languageName: node + linkType: hard + +"natural-compare@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare@npm:1.4.0" + checksum: 10/23ad088b08f898fc9b53011d7bb78ec48e79de7627e01ab5518e806033861bef68d5b0cd0e2205c2f36690ac9571ff6bcb05eb777ced2eeda8d4ac5b44592c3d + languageName: node + linkType: hard + +"ncjsm@npm:^4.3.2": + version: 4.3.2 + resolution: "ncjsm@npm:4.3.2" + dependencies: + builtin-modules: "npm:^3.3.0" + deferred: "npm:^0.7.11" + es5-ext: "npm:^0.10.62" + es6-set: "npm:^0.1.6" + ext: "npm:^1.7.0" + find-requires: "npm:^1.0.0" + fs2: "npm:^0.3.9" + type: "npm:^2.7.2" + checksum: 10/cb06d4c4926106c7b82f2a5c9753e4e28b3870bb08bb81889957579aa281ce4ee403ebc10ca0852d0f5895295b1ddc3996485e7d572b7bc6d3c81a92dc6de31f + languageName: node + linkType: hard + +"negotiator@npm:^0.6.2, negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: 10/2723fb822a17ad55c93a588a4bc44d53b22855bf4be5499916ca0cab1e7165409d0b288ba2577d7b029f10ce18cf2ed8e703e5af31c984e1e2304277ef979837 + languageName: node + linkType: hard + +"neo-async@npm:^2.6.2": + version: 2.6.2 + resolution: "neo-async@npm:2.6.2" + checksum: 10/1a7948fea86f2b33ec766bc899c88796a51ba76a4afc9026764aedc6e7cde692a09067031e4a1bf6db4f978ccd99e7f5b6c03fe47ad9865c3d4f99050d67e002 + languageName: node + linkType: hard + +"next-tick@npm:1, next-tick@npm:^1.0.0, next-tick@npm:^1.1.0": + version: 1.1.0 + resolution: "next-tick@npm:1.1.0" + checksum: 10/83b5cf36027a53ee6d8b7f9c0782f2ba87f4858d977342bfc3c20c21629290a2111f8374d13a81221179603ffc4364f38374b5655d17b6a8f8a8c77bdea4fe8b + languageName: node + linkType: hard + +"nice-try@npm:^1.0.4": + version: 1.0.5 + resolution: "nice-try@npm:1.0.5" + checksum: 10/0b4af3b5bb5d86c289f7a026303d192a7eb4417231fe47245c460baeabae7277bcd8fd9c728fb6bd62c30b3e15cd6620373e2cf33353b095d8b403d3e8a15aff + languageName: node + linkType: hard + +"node-abort-controller@npm:^3.0.1": + version: 3.1.1 + resolution: "node-abort-controller@npm:3.1.1" + checksum: 10/0a2cdb7ec0aeaf3cb31e1ca0e192f5add48f1c5c9c9ed822129f9dddbd9432f69b7425982f94ce803c56a2104884530aa67cd57696e5774b2e5b8ec2f58de042 + languageName: node + linkType: hard + +"node-addon-api@npm:^4.2.0": + version: 4.3.0 + resolution: "node-addon-api@npm:4.3.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10/d3b38d16cb9ad0714d965331d0e38cef1c27750c2c3343cd3464a9ed8158501a2910ccbf2fd9fdc476e806a19dbc9e0524ff9d66a7c779d42a9752a63ba30b80 + languageName: node + linkType: hard + +"node-dir@npm:^0.1.17": + version: 0.1.17 + resolution: "node-dir@npm:0.1.17" + dependencies: + minimatch: "npm:^3.0.2" + checksum: 10/281fdea12d9c080a7250e5b5afefa3ab39426d40753ec8126a2d1e67f189b8824723abfed74f5d8549c5d78352d8c489fe08d0b067d7684c87c07283d38374a5 + languageName: node + linkType: hard + +"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.8, node-fetch@npm:^2.6.9": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 10/b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676 + languageName: node + linkType: hard + +"node-forge@npm:^1.3.1": + version: 1.3.1 + resolution: "node-forge@npm:1.3.1" + checksum: 10/05bab6868633bf9ad4c3b1dd50ec501c22ffd69f556cdf169a00998ca1d03e8107a6032ba013852f202035372021b845603aeccd7dfcb58cdb7430013b3daa8d + languageName: node + linkType: hard + +"node-gyp-build@npm:^4.3.0": + version: 4.6.1 + resolution: "node-gyp-build@npm:4.6.1" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 10/79b948377492ae8e1aa1c18071661e6020c11f8847d5ce822abd67ec02bee5b21715b1b4861041d2b40d16633824476735bc9a60e81c82c49e715d55ee29b206 + languageName: node + linkType: hard + +"node-gyp@npm:8.x": + version: 8.4.1 + resolution: "node-gyp@npm:8.4.1" + dependencies: + env-paths: "npm:^2.2.0" + glob: "npm:^7.1.4" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^9.1.0" + nopt: "npm:^5.0.0" + npmlog: "npm:^6.0.0" + rimraf: "npm:^3.0.2" + semver: "npm:^7.3.5" + tar: "npm:^6.1.2" + which: "npm:^2.0.2" + bin: + node-gyp: bin/node-gyp.js + checksum: 10/5ac19a7f6212c787f33bb72f889fafb1ce9d80b7ecb87b3785aebb0ff94a70cd5dbb3ecb435a308eaeb26d037c6edaf173951a9edacaadf0f4c3ae189f1e5077 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 9.4.0 + resolution: "node-gyp@npm:9.4.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^7.1.4" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^11.0.3" + nopt: "npm:^6.0.0" + npmlog: "npm:^6.0.0" + rimraf: "npm:^3.0.2" + semver: "npm:^7.3.5" + tar: "npm:^6.1.2" + which: "npm:^2.0.2" + bin: + node-gyp: bin/node-gyp.js + checksum: 10/458317127c63877365f227b18ef2362b013b7f8440b35ae722935e61b31e6b84ec0e3625ab07f90679e2f41a1d5a7df6c4049fdf8e7b3c81fcf22775147b47ac + languageName: node + linkType: hard + +"node-int64@npm:^0.4.0": + version: 0.4.0 + resolution: "node-int64@npm:0.4.0" + checksum: 10/b7afc2b65e56f7035b1a2eec57ae0fbdee7d742b1cdcd0f4387562b6527a011ab1cbe9f64cc8b3cca61e3297c9637c8bf61cec2e6b8d3a711d4b5267dfafbe02 + languageName: node + linkType: hard + +"node-releases@npm:^2.0.13": + version: 2.0.13 + resolution: "node-releases@npm:2.0.13" + checksum: 10/c9bb813aab2717ff8b3015ecd4c7c5670a5546e9577699a7c84e8d69230cd3b1ce8f863f8e9b50f18b19a5ffa4b9c1a706bbbfe4c378de955fedbab04488a338 + languageName: node + linkType: hard + +"node-schedule@npm:^2.1.1": + version: 2.1.1 + resolution: "node-schedule@npm:2.1.1" + dependencies: + cron-parser: "npm:^4.2.0" + long-timeout: "npm:0.1.1" + sorted-array-functions: "npm:^1.3.0" + checksum: 10/0b0449f8a1f784cd599a8d79b1fa404ed9e3e4e2b1a48f027c97fd0632cd86e48ad762d366d6b6f9d48a940cad5b7afbdb1b833649ee870407591a6cf1297749 + languageName: node + linkType: hard + +"nopt@npm:^5.0.0": + version: 5.0.0 + resolution: "nopt@npm:5.0.0" + dependencies: + abbrev: "npm:1" + bin: + nopt: bin/nopt.js + checksum: 10/00f9bb2d16449469ba8ffcf9b8f0eae6bae285ec74b135fec533e5883563d2400c0cd70902d0a7759e47ac031ccf206ace4e86556da08ed3f1c66dda206e9ccd + languageName: node + linkType: hard + +"nopt@npm:^6.0.0": + version: 6.0.0 + resolution: "nopt@npm:6.0.0" + dependencies: + abbrev: "npm:^1.0.0" + bin: + nopt: bin/nopt.js + checksum: 10/3c1128e07cd0241ae66d6e6a472170baa9f3e84dd4203950ba8df5bafac4efa2166ce917a57ef02b01ba7c40d18b2cc64b29b225fd3640791fe07b24f0b33a32 + languageName: node + linkType: hard + +"normalize-package-data@npm:^2.3.2": + version: 2.5.0 + resolution: "normalize-package-data@npm:2.5.0" + dependencies: + hosted-git-info: "npm:^2.1.4" + resolve: "npm:^1.10.0" + semver: "npm:2 || 3 || 4 || 5" + validate-npm-package-license: "npm:^3.0.1" + checksum: 10/644f830a8bb9b7cc9bf2f6150618727659ee27cdd0840d1c1f97e8e6cab0803a098a2c19f31c6247ad9d3a0792e61521a13a6e8cd87cc6bb676e3150612c03d4 + languageName: node + linkType: hard + +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: 10/88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20 + languageName: node + linkType: hard + +"normalize-url@npm:^6.0.1": + version: 6.1.0 + resolution: "normalize-url@npm:6.1.0" + checksum: 10/5ae699402c9d5ffa330adc348fcd6fc6e6a155ab7c811b96e30b7ecab60ceef821d8f86443869671dda71bbc47f4b9625739c82ad247e883e9aefe875bfb8659 + languageName: node + linkType: hard + +"npm-registry-utilities@npm:^1.0.0": + version: 1.0.0 + resolution: "npm-registry-utilities@npm:1.0.0" + dependencies: + ext: "npm:^1.6.0" + fs2: "npm:^0.3.9" + memoizee: "npm:^0.4.15" + node-fetch: "npm:^2.6.7" + semver: "npm:^7.3.5" + type: "npm:^2.6.0" + validate-npm-package-name: "npm:^3.0.0" + checksum: 10/bc49d51c90ed997fd7b5dc9b1018c39160fac8fd53d5fd7140c7ad1e9de2e515f89530b8c65a08b6f4d8c5d4d59a4cd35503f9df274b19d6553261f6f8d6be75 + languageName: node + linkType: hard + +"npm-run-all@npm:^4.1.5": + version: 4.1.5 + resolution: "npm-run-all@npm:4.1.5" + dependencies: + ansi-styles: "npm:^3.2.1" + chalk: "npm:^2.4.1" + cross-spawn: "npm:^6.0.5" + memorystream: "npm:^0.3.1" + minimatch: "npm:^3.0.4" + pidtree: "npm:^0.3.0" + read-pkg: "npm:^3.0.0" + shell-quote: "npm:^1.6.1" + string.prototype.padend: "npm:^3.0.0" + bin: + npm-run-all: bin/npm-run-all/index.js + run-p: bin/run-p/index.js + run-s: bin/run-s/index.js + checksum: 10/46020e92813223d015f4178cce5a2338164be5f25b0c391e256c0e84ac082544986c220013f1be7f002dcac07b81c7ee0cb5c5c30b84fd6ebb6de96a8d713745 + languageName: node + linkType: hard + +"npm-run-path@npm:^4.0.1": + version: 4.0.1 + resolution: "npm-run-path@npm:4.0.1" + dependencies: + path-key: "npm:^3.0.0" + checksum: 10/5374c0cea4b0bbfdfae62da7bbdf1e1558d338335f4cacf2515c282ff358ff27b2ecb91ffa5330a8b14390ac66a1e146e10700440c1ab868208430f56b5f4d23 + languageName: node + linkType: hard + +"npm-run-path@npm:^5.1.0": + version: 5.1.0 + resolution: "npm-run-path@npm:5.1.0" + dependencies: + path-key: "npm:^4.0.0" + checksum: 10/dc184eb5ec239d6a2b990b43236845332ef12f4e0beaa9701de724aa797fe40b6bbd0157fb7639d24d3ab13f5d5cf22d223a19c6300846b8126f335f788bee66 + languageName: node + linkType: hard + +"npmlog@npm:^5.0.1": + version: 5.0.1 + resolution: "npmlog@npm:5.0.1" + dependencies: + are-we-there-yet: "npm:^2.0.0" + console-control-strings: "npm:^1.1.0" + gauge: "npm:^3.0.0" + set-blocking: "npm:^2.0.0" + checksum: 10/f42c7b9584cdd26a13c41a21930b6f5912896b6419ab15be88cc5721fc792f1c3dd30eb602b26ae08575694628ba70afdcf3675d86e4f450fc544757e52726ec + languageName: node + linkType: hard + +"npmlog@npm:^6.0.0": + version: 6.0.2 + resolution: "npmlog@npm:6.0.2" + dependencies: + are-we-there-yet: "npm:^3.0.0" + console-control-strings: "npm:^1.1.0" + gauge: "npm:^4.0.3" + set-blocking: "npm:^2.0.0" + checksum: 10/82b123677e62deb9e7472e27b92386c09e6e254ee6c8bcd720b3011013e4168bc7088e984f4fbd53cb6e12f8b4690e23e4fa6132689313e0d0dc4feea45489bb + languageName: node + linkType: hard + +"object-assign@npm:^4.0.1, object-assign@npm:^4.1.1": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f + languageName: node + linkType: hard + +"object-hash@npm:^3.0.0": + version: 3.0.0 + resolution: "object-hash@npm:3.0.0" + checksum: 10/f498d456a20512ba7be500cef4cf7b3c183cc72c65372a549c9a0e6dd78ce26f375e9b1315c07592d3fde8f10d5019986eba35970570d477ed9a2a702514432a + languageName: node + linkType: hard + +"object-inspect@npm:^1.12.3, object-inspect@npm:^1.9.0": + version: 1.12.3 + resolution: "object-inspect@npm:1.12.3" + checksum: 10/532b0036f0472f561180fac0d04fe328ee01f57637624c83fb054f81b5bfe966cdf4200612a499ed391a7ca3c46b20a0bc3a55fc8241d944abe687c556a32b39 + languageName: node + linkType: hard + +"object-is@npm:^1.1.5": + version: 1.1.5 + resolution: "object-is@npm:1.1.5" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.1.3" + checksum: 10/75365aff5da4bebad5d20efd9f9a7a13597e603f5eb03d89da8f578c3f3937fe01c6cb5fce86c0611c48795c0841401fd37c943821db0de703c7b30a290576ad + languageName: node + linkType: hard + +"object-keys@npm:^1.1.1": + version: 1.1.1 + resolution: "object-keys@npm:1.1.1" + checksum: 10/3d81d02674115973df0b7117628ea4110d56042e5326413e4b4313f0bcdf7dd78d4a3acef2c831463fa3796a66762c49daef306f4a0ea1af44877d7086d73bde + languageName: node + linkType: hard + +"object.assign@npm:^4.1.2, object.assign@npm:^4.1.4": + version: 4.1.4 + resolution: "object.assign@npm:4.1.4" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.1.4" + has-symbols: "npm:^1.0.3" + object-keys: "npm:^1.1.1" + checksum: 10/fd82d45289df0a952d772817622ecbaeb4ec933d3abb53267aede083ee38f6a395af8fadfbc569ee575115b0b7c9b286e7cfb2b7a2557b1055f7acbce513bc29 + languageName: node + linkType: hard + +"object.entries@npm:^1.1.2, object.entries@npm:^1.1.5": + version: 1.1.7 + resolution: "object.entries@npm:1.1.7" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 10/03f0bd0f23a8626c94429d15abf26ccda7723f08cd26be2c09c72d436765f8c7468605b5476ca58d4a7cec1ec7eca5be496dbd938fd4236b77ed6d05a8680048 + languageName: node + linkType: hard + +"object.fromentries@npm:^2.0.6": + version: 2.0.7 + resolution: "object.fromentries@npm:2.0.7" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 10/1bfbe42a51f8d84e417d193fae78e4b8eebb134514cdd44406480f8e8a0e075071e0717635d8e3eccd50fec08c1d555fe505c38804cbac0808397187653edd59 + languageName: node + linkType: hard + +"object.groupby@npm:^1.0.0": + version: 1.0.1 + resolution: "object.groupby@npm:1.0.1" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + get-intrinsic: "npm:^1.2.1" + checksum: 10/b7123d91403f95d63978513b23a6079c30f503311f64035fafc863c291c787f287b58df3b21ef002ce1d0b820958c9009dd5a8ab696e0eca325639d345e41524 + languageName: node + linkType: hard + +"object.values@npm:^1.1.6": + version: 1.1.7 + resolution: "object.values@npm:1.1.7" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 10/20ab42c0bbf984405c80e060114b18cf5d629a40a132c7eac4fb79c5d06deb97496311c19297dcf9c61f45c2539cd4c7f7c5d6230e51db360ff297bbc9910162 + languageName: node + linkType: hard + +"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: 10/cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 + languageName: node + linkType: hard + +"one-time@npm:^1.0.0": + version: 1.0.0 + resolution: "one-time@npm:1.0.0" + dependencies: + fn.name: "npm:1.x.x" + checksum: 10/64d0160480eeae4e3b2a6fc0a02f452e05bb0cc8373a4ed56a4fc08c3939dcb91bc20075003ed499655bd16919feb63ca56f86eee7932c5251f7d629b55dfc90 + languageName: node + linkType: hard + +"onetime@npm:^5.1.0, onetime@npm:^5.1.2": + version: 5.1.2 + resolution: "onetime@npm:5.1.2" + dependencies: + mimic-fn: "npm:^2.1.0" + checksum: 10/e9fd0695a01cf226652f0385bf16b7a24153dbbb2039f764c8ba6d2306a8506b0e4ce570de6ad99c7a6eb49520743afdb66edd95ee979c1a342554ed49a9aadd + languageName: node + linkType: hard + +"onetime@npm:^6.0.0": + version: 6.0.0 + resolution: "onetime@npm:6.0.0" + dependencies: + mimic-fn: "npm:^4.0.0" + checksum: 10/0846ce78e440841335d4e9182ef69d5762e9f38aa7499b19f42ea1c4cd40f0b4446094c455c713f9adac3f4ae86f613bb5e30c99e52652764d06a89f709b3788 + languageName: node + linkType: hard + +"open@npm:^7.4.2": + version: 7.4.2 + resolution: "open@npm:7.4.2" + dependencies: + is-docker: "npm:^2.0.0" + is-wsl: "npm:^2.1.1" + checksum: 10/4fc02ed3368dcd5d7247ad3566433ea2695b0713b041ebc0eeb2f0f9e5d4e29fc2068f5cdd500976b3464e77fe8b61662b1b059c73233ccc601fe8b16d6c1cd6 + languageName: node + linkType: hard + +"open@npm:^8.4.2": + version: 8.4.2 + resolution: "open@npm:8.4.2" + dependencies: + define-lazy-prop: "npm:^2.0.0" + is-docker: "npm:^2.1.1" + is-wsl: "npm:^2.2.0" + checksum: 10/acd81a1d19879c818acb3af2d2e8e9d81d17b5367561e623248133deb7dd3aefaed527531df2677d3e6aaf0199f84df57b6b2262babff8bf46ea0029aac536c9 + languageName: node + linkType: hard + +"optionator@npm:^0.8.1": + version: 0.8.3 + resolution: "optionator@npm:0.8.3" + dependencies: + deep-is: "npm:~0.1.3" + fast-levenshtein: "npm:~2.0.6" + levn: "npm:~0.3.0" + prelude-ls: "npm:~1.1.2" + type-check: "npm:~0.3.2" + word-wrap: "npm:~1.2.3" + checksum: 10/6fa3c841b520f10aec45563962922215180e8cfbc59fde3ecd4ba2644ad66ca96bd19ad0e853f22fefcb7fc10e7612a5215b412cc66c5588f9a3138b38f6b5ff + languageName: node + linkType: hard + +"optionator@npm:^0.9.3": + version: 0.9.3 + resolution: "optionator@npm:0.9.3" + dependencies: + "@aashutoshrathi/word-wrap": "npm:^1.2.3" + deep-is: "npm:^0.1.3" + fast-levenshtein: "npm:^2.0.6" + levn: "npm:^0.4.1" + prelude-ls: "npm:^1.2.1" + type-check: "npm:^0.4.0" + checksum: 10/fa28d3016395974f7fc087d6bbf0ac7f58ac3489f4f202a377e9c194969f329a7b88c75f8152b33fb08794a30dcd5c079db6bb465c28151357f113d80bbf67da + languageName: node + linkType: hard + +"ora@npm:^5.4.1": + version: 5.4.1 + resolution: "ora@npm:5.4.1" + dependencies: + bl: "npm:^4.1.0" + chalk: "npm:^4.1.0" + cli-cursor: "npm:^3.1.0" + cli-spinners: "npm:^2.5.0" + is-interactive: "npm:^1.0.0" + is-unicode-supported: "npm:^0.1.0" + log-symbols: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + wcwidth: "npm:^1.0.1" + checksum: 10/8d071828f40090a8e1c6e8f350c6eb065808e9ab2b3e57fa37e0d5ae78cb46dac00117c8f12c3c8b8da2923454afbd8265e08c10b69881170c5b269f451e7fef + languageName: node + linkType: hard + +"os-tmpdir@npm:~1.0.2": + version: 1.0.2 + resolution: "os-tmpdir@npm:1.0.2" + checksum: 10/5666560f7b9f10182548bf7013883265be33620b1c1b4a4d405c25be2636f970c5488ff3e6c48de75b55d02bde037249fe5dbfbb4c0fb7714953d56aed062e6d + languageName: node + linkType: hard + +"p-cancelable@npm:^2.0.0": + version: 2.1.1 + resolution: "p-cancelable@npm:2.1.1" + checksum: 10/7f1b64db17fc54acf359167d62898115dcf2a64bf6b3b038e4faf36fc059e5ed762fb9624df8ed04b25bee8de3ab8d72dea9879a2a960cd12e23c420a4aca6ed + languageName: node + linkType: hard + +"p-event@npm:^4.2.0": + version: 4.2.0 + resolution: "p-event@npm:4.2.0" + dependencies: + p-timeout: "npm:^3.1.0" + checksum: 10/d03238ff31f5694f11bd7dcc0eae16c35b1ffb8cad4e5263d5422ba0bd6736dbfdb33b72745ecb6b06b98494db80f49f12c14f5e8da1212bf6a424609ad8d885 + languageName: node + linkType: hard + +"p-finally@npm:^1.0.0": + version: 1.0.0 + resolution: "p-finally@npm:1.0.0" + checksum: 10/93a654c53dc805dd5b5891bab16eb0ea46db8f66c4bfd99336ae929323b1af2b70a8b0654f8f1eae924b2b73d037031366d645f1fd18b3d30cbd15950cc4b1d4 + languageName: node + linkType: hard + +"p-limit@npm:^2.2.0": + version: 2.3.0 + resolution: "p-limit@npm:2.3.0" + dependencies: + p-try: "npm:^2.0.0" + checksum: 10/84ff17f1a38126c3314e91ecfe56aecbf36430940e2873dadaa773ffe072dc23b7af8e46d4b6485d302a11673fe94c6b67ca2cfbb60c989848b02100d0594ac1 + languageName: node + linkType: hard + +"p-limit@npm:^3.0.1, p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": + version: 3.1.0 + resolution: "p-limit@npm:3.1.0" + dependencies: + yocto-queue: "npm:^0.1.0" + checksum: 10/7c3690c4dbf62ef625671e20b7bdf1cbc9534e83352a2780f165b0d3ceba21907e77ad63401708145ca4e25bfc51636588d89a8c0aeb715e6c37d1c066430360 + languageName: node + linkType: hard + +"p-locate@npm:^4.1.0": + version: 4.1.0 + resolution: "p-locate@npm:4.1.0" + dependencies: + p-limit: "npm:^2.2.0" + checksum: 10/513bd14a455f5da4ebfcb819ef706c54adb09097703de6aeaa5d26fe5ea16df92b48d1ac45e01e3944ce1e6aa2a66f7f8894742b8c9d6e276e16cd2049a2b870 + languageName: node + linkType: hard + +"p-locate@npm:^5.0.0": + version: 5.0.0 + resolution: "p-locate@npm:5.0.0" + dependencies: + p-limit: "npm:^3.0.2" + checksum: 10/1623088f36cf1cbca58e9b61c4e62bf0c60a07af5ae1ca99a720837356b5b6c5ba3eb1b2127e47a06865fee59dd0453cad7cc844cda9d5a62ac1a5a51b7c86d3 + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: 10/7ba4a2b1e24c05e1fc14bbaea0fc6d85cf005ae7e9c9425d4575550f37e2e584b1af97bcde78eacd7559208f20995988d52881334db16cf77bc1bcf68e48ed7c + languageName: node + linkType: hard + +"p-memoize@npm:^7.1.1": + version: 7.1.1 + resolution: "p-memoize@npm:7.1.1" + dependencies: + mimic-fn: "npm:^4.0.0" + type-fest: "npm:^3.0.0" + checksum: 10/2fc5b12fc530aed9c1e455f706d8da2a2e6bed4573f611a4e92b2a75f253e8c0acc63ad014b55f155b5d5de110c586b490a4d6612d64bc743106cad626d995fd + languageName: node + linkType: hard + +"p-retry@npm:^6.1.0": + version: 6.1.0 + resolution: "p-retry@npm:6.1.0" + dependencies: + "@types/retry": "npm:0.12.2" + is-network-error: "npm:^1.0.0" + retry: "npm:^0.13.1" + checksum: 10/5366014084c62f3a3acf5f95d0b7ad36817973e57d4a6638c3d1ad35fc710432840b1acc5b3f30b2943407b1532808a57a08cb5835199fdb02157ccc516b96e8 + languageName: node + linkType: hard + +"p-timeout@npm:^3.1.0": + version: 3.2.0 + resolution: "p-timeout@npm:3.2.0" + dependencies: + p-finally: "npm:^1.0.0" + checksum: 10/3dd0eaa048780a6f23e5855df3dd45c7beacff1f820476c1d0d1bcd6648e3298752ba2c877aa1c92f6453c7dd23faaf13d9f5149fc14c0598a142e2c5e8d649c + languageName: node + linkType: hard + +"p-try@npm:^2.0.0": + version: 2.2.0 + resolution: "p-try@npm:2.2.0" + checksum: 10/f8a8e9a7693659383f06aec604ad5ead237c7a261c18048a6e1b5b85a5f8a067e469aa24f5bc009b991ea3b058a87f5065ef4176793a200d4917349881216cae + languageName: node + linkType: hard + +"pako@npm:~1.0.2": + version: 1.0.11 + resolution: "pako@npm:1.0.11" + checksum: 10/1ad07210e894472685564c4d39a08717e84c2a68a70d3c1d9e657d32394ef1670e22972a433cbfe48976cb98b154ba06855dcd3fcfba77f60f1777634bec48c0 + languageName: node + linkType: hard + +"parent-module@npm:^1.0.0": + version: 1.0.1 + resolution: "parent-module@npm:1.0.1" + dependencies: + callsites: "npm:^3.0.0" + checksum: 10/6ba8b255145cae9470cf5551eb74be2d22281587af787a2626683a6c20fbb464978784661478dd2a3f1dad74d1e802d403e1b03c1a31fab310259eec8ac560ff + languageName: node + linkType: hard + +"parse-json@npm:^4.0.0": + version: 4.0.0 + resolution: "parse-json@npm:4.0.0" + dependencies: + error-ex: "npm:^1.3.1" + json-parse-better-errors: "npm:^1.0.1" + checksum: 10/0fe227d410a61090c247e34fa210552b834613c006c2c64d9a05cfe9e89cf8b4246d1246b1a99524b53b313e9ac024438d0680f67e33eaed7e6f38db64cfe7b5 + languageName: node + linkType: hard + +"parse-json@npm:^5.0.0, parse-json@npm:^5.2.0": + version: 5.2.0 + resolution: "parse-json@npm:5.2.0" + dependencies: + "@babel/code-frame": "npm:^7.0.0" + error-ex: "npm:^1.3.1" + json-parse-even-better-errors: "npm:^2.3.0" + lines-and-columns: "npm:^1.1.6" + checksum: 10/62085b17d64da57f40f6afc2ac1f4d95def18c4323577e1eced571db75d9ab59b297d1d10582920f84b15985cbfc6b6d450ccbf317644cfa176f3ed982ad87e2 + languageName: node + linkType: hard + +"path-exists@npm:^3.0.0": + version: 3.0.0 + resolution: "path-exists@npm:3.0.0" + checksum: 10/96e92643aa34b4b28d0de1cd2eba52a1c5313a90c6542d03f62750d82480e20bfa62bc865d5cfc6165f5fcd5aeb0851043c40a39be5989646f223300021bae0a + languageName: node + linkType: hard + +"path-exists@npm:^4.0.0": + version: 4.0.0 + resolution: "path-exists@npm:4.0.0" + checksum: 10/505807199dfb7c50737b057dd8d351b82c033029ab94cb10a657609e00c1bc53b951cfdbccab8de04c5584d5eff31128ce6afd3db79281874a5ef2adbba55ed1 + languageName: node + linkType: hard + +"path-is-absolute@npm:^1.0.0": + version: 1.0.1 + resolution: "path-is-absolute@npm:1.0.1" + checksum: 10/060840f92cf8effa293bcc1bea81281bd7d363731d214cbe5c227df207c34cd727430f70c6037b5159c8a870b9157cba65e775446b0ab06fd5ecc7e54615a3b8 + languageName: node + linkType: hard + +"path-key@npm:^2.0.1": + version: 2.0.1 + resolution: "path-key@npm:2.0.1" + checksum: 10/6e654864e34386a2a8e6bf72cf664dcabb76574dd54013add770b374384d438aca95f4357bb26935b514a4e4c2c9b19e191f2200b282422a76ee038b9258c5e7 + languageName: node + linkType: hard + +"path-key@npm:^3.0.0, path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 10/55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 + languageName: node + linkType: hard + +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 10/8e6c314ae6d16b83e93032c61020129f6f4484590a777eed709c4a01b50e498822b00f76ceaf94bc64dbd90b327df56ceadce27da3d83393790f1219e07721d7 + languageName: node + linkType: hard + +"path-loader@npm:^1.0.10": + version: 1.0.12 + resolution: "path-loader@npm:1.0.12" + dependencies: + native-promise-only: "npm:^0.8.1" + superagent: "npm:^7.1.6" + checksum: 10/63475f4bd14570eb7ab6298ec2ca19da47bac99bc98d30957a3128552a6c3212b31a8b06a5622d51301d462e6faf8de3683ea07c2f08cd17b9ab6cbc66d0f626 + languageName: node + linkType: hard + +"path-parse@npm:^1.0.7": + version: 1.0.7 + resolution: "path-parse@npm:1.0.7" + checksum: 10/49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a + languageName: node + linkType: hard + +"path-scurry@npm:^1.10.1": + version: 1.10.1 + resolution: "path-scurry@npm:1.10.1" + dependencies: + lru-cache: "npm:^9.1.1 || ^10.0.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: 10/eebfb8304fef1d4f7e1486df987e4fd77413de4fce16508dea69fcf8eb318c09a6b15a7a2f4c22877cec1cb7ecbd3071d18ca9de79eeece0df874a00f1f0bdc8 + languageName: node + linkType: hard + +"path-type@npm:^3.0.0": + version: 3.0.0 + resolution: "path-type@npm:3.0.0" + dependencies: + pify: "npm:^3.0.0" + checksum: 10/735b35e256bad181f38fa021033b1c33cfbe62ead42bb2222b56c210e42938eecb272ae1949f3b6db4ac39597a61b44edd8384623ec4d79bfdc9a9c0f12537a6 + languageName: node + linkType: hard + +"path-type@npm:^4.0.0": + version: 4.0.0 + resolution: "path-type@npm:4.0.0" + checksum: 10/5b1e2daa247062061325b8fdbfd1fb56dde0a448fb1455453276ea18c60685bdad23a445dc148cf87bc216be1573357509b7d4060494a6fd768c7efad833ee45 + languageName: node + linkType: hard + +"path2@npm:^0.1.0": + version: 0.1.0 + resolution: "path2@npm:0.1.0" + checksum: 10/ef63ed39388718f5d446f87555dee6afe0923e3f1aa3b38fd2f90a74b82e19ea72fa14b25cb3b07c0434a503ccdcf1f481c456442ed60a5f8a2ef85823d0fa6a + languageName: node + linkType: hard + +"peek-readable@npm:^4.1.0": + version: 4.1.0 + resolution: "peek-readable@npm:4.1.0" + checksum: 10/97373215dcf382748645c3d22ac5e8dbd31759f7bd0c539d9fdbaaa7d22021838be3e55110ad0ed8f241c489342304b14a50dfee7ef3bcee2987d003b24ecc41 + languageName: node + linkType: hard + +"pend@npm:~1.2.0": + version: 1.2.0 + resolution: "pend@npm:1.2.0" + checksum: 10/6c72f5243303d9c60bd98e6446ba7d30ae29e3d56fdb6fae8767e8ba6386f33ee284c97efe3230a0d0217e2b1723b8ab490b1bbf34fcbb2180dbc8a9de47850d + languageName: node + linkType: hard + +"pg-connection-string@npm:^2.6.1": + version: 2.6.2 + resolution: "pg-connection-string@npm:2.6.2" + checksum: 10/22265882c3b6f2320785378d0760b051294a684989163d5a1cde4009e64e84448d7bf67d9a7b9e7f69440c3ee9e2212f9aa10dd17ad6773f6143c6020cebbcb5 + languageName: node + linkType: hard + +"picocolors@npm:^1.0.0": + version: 1.0.0 + resolution: "picocolors@npm:1.0.0" + checksum: 10/a2e8092dd86c8396bdba9f2b5481032848525b3dc295ce9b57896f931e63fc16f79805144321f72976383fc249584672a75cc18d6777c6b757603f372f745981 + languageName: node + linkType: hard + +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 10/60c2595003b05e4535394d1da94850f5372c9427ca4413b71210f437f7b2ca091dbd611c45e8b37d10036fa8eade25c1b8951654f9d3973bfa66a2ff4d3b08bc + languageName: node + linkType: hard + +"pidtree@npm:^0.3.0": + version: 0.3.1 + resolution: "pidtree@npm:0.3.1" + bin: + pidtree: bin/pidtree.js + checksum: 10/eb85b841cd168151bfadb984f9514d67a884d6962d4a2d250d4e8acf85cf031d7dab080f7272fb2735f9033364e5058c73eeebbee3cf6fd829169a75d19f189a + languageName: node + linkType: hard + +"pify@npm:^2.3.0": + version: 2.3.0 + resolution: "pify@npm:2.3.0" + checksum: 10/9503aaeaf4577acc58642ad1d25c45c6d90288596238fb68f82811c08104c800e5a7870398e9f015d82b44ecbcbef3dc3d4251a1cbb582f6e5959fe09884b2ba + languageName: node + linkType: hard + +"pify@npm:^3.0.0": + version: 3.0.0 + resolution: "pify@npm:3.0.0" + checksum: 10/668c1dc8d9fc1b34b9ce3b16ba59deb39d4dc743527bf2ed908d2b914cb8ba40aa5ba6960b27c417c241531c5aafd0598feeac2d50cb15278cf9863fa6b02a77 + languageName: node + linkType: hard + +"pinkie-promise@npm:^2.0.0": + version: 2.0.1 + resolution: "pinkie-promise@npm:2.0.1" + dependencies: + pinkie: "npm:^2.0.0" + checksum: 10/b53a4a2e73bf56b6f421eef711e7bdcb693d6abb474d57c5c413b809f654ba5ee750c6a96dd7225052d4b96c4d053cdcb34b708a86fceed4663303abee52fcca + languageName: node + linkType: hard + +"pinkie@npm:^2.0.0": + version: 2.0.4 + resolution: "pinkie@npm:2.0.4" + checksum: 10/11d207257a044d1047c3755374d36d84dda883a44d030fe98216bf0ea97da05a5c9d64e82495387edeb9ee4f52c455bca97cdb97629932be65e6f54b29f5aec8 + languageName: node + linkType: hard + +"pirates@npm:^4.0.4": + version: 4.0.6 + resolution: "pirates@npm:4.0.6" + checksum: 10/d02dda76f4fec1cbdf395c36c11cf26f76a644f9f9a1bfa84d3167d0d3154d5289aacc72677aa20d599bb4a6937a471de1b65c995e2aea2d8687cbcd7e43ea5f + languageName: node + linkType: hard + +"pkg-dir@npm:^4.2.0": + version: 4.2.0 + resolution: "pkg-dir@npm:4.2.0" + dependencies: + find-up: "npm:^4.0.0" + checksum: 10/9863e3f35132bf99ae1636d31ff1e1e3501251d480336edb1c211133c8d58906bed80f154a1d723652df1fda91e01c7442c2eeaf9dc83157c7ae89087e43c8d6 + languageName: node + linkType: hard + +"prelude-ls@npm:^1.2.1": + version: 1.2.1 + resolution: "prelude-ls@npm:1.2.1" + checksum: 10/0b9d2c76801ca652a7f64892dd37b7e3fab149a37d2424920099bf894acccc62abb4424af2155ab36dea8744843060a2d8ddc983518d0b1e22265a22324b72ed + languageName: node + linkType: hard + +"prelude-ls@npm:~1.1.2": + version: 1.1.2 + resolution: "prelude-ls@npm:1.1.2" + checksum: 10/946a9f60d3477ca6b7d4c5e8e452ad1b98dc8aaa992cea939a6b926ac16cc4129d7217c79271dc808b5814b1537ad0af37f29a942e2eafbb92cfc5a1c87c38cb + languageName: node + linkType: hard + +"pretty-format@npm:^27.0.0, pretty-format@npm:^27.5.1": + version: 27.5.1 + resolution: "pretty-format@npm:27.5.1" + dependencies: + ansi-regex: "npm:^5.0.1" + ansi-styles: "npm:^5.0.0" + react-is: "npm:^17.0.1" + checksum: 10/248990cbef9e96fb36a3e1ae6b903c551ca4ddd733f8d0912b9cc5141d3d0b3f9f8dfb4d799fb1c6723382c9c2083ffbfa4ad43ff9a0e7535d32d41fd5f01da6 + languageName: node + linkType: hard + +"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": + version: 29.7.0 + resolution: "pretty-format@npm:29.7.0" + dependencies: + "@jest/schemas": "npm:^29.6.3" + ansi-styles: "npm:^5.0.0" + react-is: "npm:^18.0.0" + checksum: 10/dea96bc83c83cd91b2bfc55757b6b2747edcaac45b568e46de29deee80742f17bc76fe8898135a70d904f4928eafd8bb693cd1da4896e8bdd3c5e82cadf1d2bb + languageName: node + linkType: hard + +"process-nextick-args@npm:~2.0.0": + version: 2.0.1 + resolution: "process-nextick-args@npm:2.0.1" + checksum: 10/1d38588e520dab7cea67cbbe2efdd86a10cc7a074c09657635e34f035277b59fbb57d09d8638346bf7090f8e8ebc070c96fa5fd183b777fff4f5edff5e9466cf + languageName: node + linkType: hard + +"process-utils@npm:^4.0.0": + version: 4.0.0 + resolution: "process-utils@npm:4.0.0" + dependencies: + ext: "npm:^1.4.0" + fs2: "npm:^0.3.9" + memoizee: "npm:^0.4.14" + type: "npm:^2.1.0" + checksum: 10/b905df2a57324b219cd99b999c68fd098a1790f60bd553346444424db35634c99f5c0941354017070bf42ca9ca9048034bfc2643054cf3e5034ac4a17d42fbb6 + languageName: node + linkType: hard + +"prom-client@npm:^13.2.0": + version: 13.2.0 + resolution: "prom-client@npm:13.2.0" + dependencies: + tdigest: "npm:^0.1.1" + checksum: 10/46df22a933f35dc60480ef7139c2ea0f315a7f4b3018e9ac045572999a1c2ee92978ef0bfb124c111159718d98d8093de59b81bb3f6fd499f3d0ce576a987703 + languageName: node + linkType: hard + +"promise-inflight@npm:^1.0.1": + version: 1.0.1 + resolution: "promise-inflight@npm:1.0.1" + checksum: 10/1560d413ea20c5a74f3631d39ba8cbd1972b9228072a755d01e1f5ca5110382d9af76a1582d889445adc6e75bb5ac4886b56dc4b6eae51b30145d7bb1ac7505b + languageName: node + linkType: hard + +"promise-queue@npm:^2.2.5": + version: 2.2.5 + resolution: "promise-queue@npm:2.2.5" + checksum: 10/3e3c33d91c4f4afb59d50d18c4d22e820fbb82c912ba73691a2c1d28257dc1f5ca2cc0319a3d4035cf662579a9a020e2948cd375b500422d7c59581004e2dc1a + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 10/96e1a82453c6c96eef53a37a1d6134c9f2482f94068f98a59145d0986ca4e497bf110a410adf73857e588165eab3899f0ebcf7b3890c1b3ce802abc0d65967d4 + languageName: node + linkType: hard + +"prompts@npm:^2.0.1": + version: 2.4.2 + resolution: "prompts@npm:2.4.2" + dependencies: + kleur: "npm:^3.0.3" + sisteransi: "npm:^1.0.5" + checksum: 10/c52536521a4d21eff4f2f2aa4572446cad227464066365a7167e52ccf8d9839c099f9afec1aba0eed3d5a2514b3e79e0b3e7a1dc326b9acde6b75d27ed74b1a9 + languageName: node + linkType: hard + +"proto-list@npm:~1.2.1": + version: 1.2.4 + resolution: "proto-list@npm:1.2.4" + checksum: 10/9cc3b46d613fa0d637033b225db1bc98e914c3c05864f7adc9bee728192e353125ef2e49f71129a413f6333951756000b0e54f299d921f02d3e9e370cc994100 + languageName: node + linkType: hard + +"proto3-json-serializer@npm:^1.0.0": + version: 1.1.1 + resolution: "proto3-json-serializer@npm:1.1.1" + dependencies: + protobufjs: "npm:^7.0.0" + checksum: 10/20c5d28b6e8f4100fbae1eb78ae89019de1f6f19c352366f931d2a1c25ef48c1cfe0bbdb870e484bd8b3a0ea614295b9790e72655f0930ab445c7e10d1d99cec + languageName: node + linkType: hard + +"protobufjs-cli@npm:1.1.1": + version: 1.1.1 + resolution: "protobufjs-cli@npm:1.1.1" + dependencies: + chalk: "npm:^4.0.0" + escodegen: "npm:^1.13.0" + espree: "npm:^9.0.0" + estraverse: "npm:^5.1.0" + glob: "npm:^8.0.0" + jsdoc: "npm:^4.0.0" + minimist: "npm:^1.2.0" + semver: "npm:^7.1.2" + tmp: "npm:^0.2.1" + uglify-js: "npm:^3.7.7" + peerDependencies: + protobufjs: ^7.0.0 + bin: + pbjs: bin/pbjs + pbts: bin/pbts + checksum: 10/8c8672b1f2e0b1e9d33d98059f5d02adf94430d21df53fd307180a7bda69a6eb0546d51b3aa5c4814c9b6ba9fcc1a3a779f8b8a3a73e831e2ca1eb5606ecc23a + languageName: node + linkType: hard + +"protobufjs@npm:7.2.4": + version: 7.2.4 + resolution: "protobufjs@npm:7.2.4" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.2" + "@protobufjs/base64": "npm:^1.1.2" + "@protobufjs/codegen": "npm:^2.0.4" + "@protobufjs/eventemitter": "npm:^1.1.0" + "@protobufjs/fetch": "npm:^1.1.0" + "@protobufjs/float": "npm:^1.0.2" + "@protobufjs/inquire": "npm:^1.1.0" + "@protobufjs/path": "npm:^1.1.2" + "@protobufjs/pool": "npm:^1.1.0" + "@protobufjs/utf8": "npm:^1.1.0" + "@types/node": "npm:>=13.7.0" + long: "npm:^5.0.0" + checksum: 10/6972bd0a372abdbd43e20e14e9692695b92adcd0b746e73e8deb8880ce78abe4a30303a05160f5d0a5fc3dd0b7b6157cc8a06418da364fc7091f965724ca0443 + languageName: node + linkType: hard + +"protobufjs@npm:^7.0.0, protobufjs@npm:^7.2.4, protobufjs@npm:^7.2.5": + version: 7.2.5 + resolution: "protobufjs@npm:7.2.5" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.2" + "@protobufjs/base64": "npm:^1.1.2" + "@protobufjs/codegen": "npm:^2.0.4" + "@protobufjs/eventemitter": "npm:^1.1.0" + "@protobufjs/fetch": "npm:^1.1.0" + "@protobufjs/float": "npm:^1.0.2" + "@protobufjs/inquire": "npm:^1.1.0" + "@protobufjs/path": "npm:^1.1.2" + "@protobufjs/pool": "npm:^1.1.0" + "@protobufjs/utf8": "npm:^1.1.0" + "@types/node": "npm:>=13.7.0" + long: "npm:^5.0.0" + checksum: 10/6c5aa62b61dff843f585f3acd9cb7a82d566de2dbf167a300b39afee91b04298c4b4aec61354b7c00308b40596f5f3f4b07d6246cfb4ee0abeaea25101033315 + languageName: node + linkType: hard + +"proxy-from-env@npm:^1.1.0": + version: 1.1.0 + resolution: "proxy-from-env@npm:1.1.0" + checksum: 10/f0bb4a87cfd18f77bc2fba23ae49c3b378fb35143af16cc478171c623eebe181678f09439707ad80081d340d1593cd54a33a0113f3ccb3f4bc9451488780ee23 + languageName: node + linkType: hard + +"pseudomap@npm:^1.0.1": + version: 1.0.2 + resolution: "pseudomap@npm:1.0.2" + checksum: 10/856c0aae0ff2ad60881168334448e898ad7a0e45fe7386d114b150084254c01e200c957cf378378025df4e052c7890c5bd933939b0e0d2ecfcc1dc2f0b2991f5 + languageName: node + linkType: hard + +"pump@npm:^3.0.0": + version: 3.0.0 + resolution: "pump@npm:3.0.0" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10/e42e9229fba14732593a718b04cb5e1cfef8254544870997e0ecd9732b189a48e1256e4e5478148ecb47c8511dca2b09eae56b4d0aad8009e6fac8072923cfc9 + languageName: node + linkType: hard + +"punycode@npm:1.3.2": + version: 1.3.2 + resolution: "punycode@npm:1.3.2" + checksum: 10/5c57d588c60679fd1b9400c75de06e327723f2b38e21e195027ba7a59006725f7b817dce5b26d47c7f8c1c842d28275aa59955a06d2e467cffeba70b7e0576bb + languageName: node + linkType: hard + +"punycode@npm:^2.1.0": + version: 2.3.0 + resolution: "punycode@npm:2.3.0" + checksum: 10/d4e7fbb96f570c57d64b09a35a1182c879ac32833de7c6926a2c10619632c1377865af3dab5479f59d51da18bcd5035a20a5ef6ceb74020082a3e78025d9a9ca + languageName: node + linkType: hard + +"pure-rand@npm:^6.0.0": + version: 6.0.4 + resolution: "pure-rand@npm:6.0.4" + checksum: 10/34fed0abe99d3db7ddc459c12e1eda6bff05db6a17f2017a1ae12202271ccf276fb223b442653518c719671c1b339bbf97f27ba9276dba0997c89e45c4e6a3bf + languageName: node + linkType: hard + +"qs@npm:^6.10.3, qs@npm:^6.11.0": + version: 6.11.2 + resolution: "qs@npm:6.11.2" + dependencies: + side-channel: "npm:^1.0.4" + checksum: 10/f2321d0796664d0f94e92447ccd3bdfd6b6f3a50b6b762aa79d7f5b1ea3a7a9f94063ba896b82bc2a877ed6a7426d4081e4f16568fdb04f0ee188cca9d8505b4 + languageName: node + linkType: hard + +"querystring@npm:0.2.0": + version: 0.2.0 + resolution: "querystring@npm:0.2.0" + checksum: 10/37b91720be8c8de87b49d1a68f0ceafbbeda6efe6334ce7aad080b0b4111f933a40650b8a6669c1bc629cd8bb37c67cb7b5a42ec0758662efbce44b8faa1766d + languageName: node + linkType: hard + +"querystring@npm:^0.2.1": + version: 0.2.1 + resolution: "querystring@npm:0.2.1" + checksum: 10/5ae2eeb8c6d70263a3d13ffaf234ce9593ae0e95ad8ea04aa540e14ff66679347420817aeb4fe6fdfa2aaa7fac86e311b6f1d3da2187f433082ad9125c808c14 + languageName: node + linkType: hard + +"queue-microtask@npm:^1.2.2": + version: 1.2.3 + resolution: "queue-microtask@npm:1.2.3" + checksum: 10/72900df0616e473e824202113c3df6abae59150dfb73ed13273503127235320e9c8ca4aaaaccfd58cf417c6ca92a6e68ee9a5c3182886ae949a768639b388a7b + languageName: node + linkType: hard + +"quick-lru@npm:^5.1.1": + version: 5.1.1 + resolution: "quick-lru@npm:5.1.1" + checksum: 10/a516faa25574be7947969883e6068dbe4aa19e8ef8e8e0fd96cddd6d36485e9106d85c0041a27153286b0770b381328f4072aa40d3b18a19f5f7d2b78b94b5ed + languageName: node + linkType: hard + +"randombytes@npm:^2.1.0": + version: 2.1.0 + resolution: "randombytes@npm:2.1.0" + dependencies: + safe-buffer: "npm:^5.1.0" + checksum: 10/4efd1ad3d88db77c2d16588dc54c2b52fd2461e70fe5724611f38d283857094fe09040fa2c9776366803c3152cf133171b452ef717592b65631ce5dc3a2bdafc + languageName: node + linkType: hard + +"react-is@npm:^17.0.1": + version: 17.0.2 + resolution: "react-is@npm:17.0.2" + checksum: 10/73b36281e58eeb27c9cc6031301b6ae19ecdc9f18ae2d518bdb39b0ac564e65c5779405d623f1df9abf378a13858b79442480244bd579968afc1faf9a2ce5e05 + languageName: node + linkType: hard + +"react-is@npm:^18.0.0": + version: 18.2.0 + resolution: "react-is@npm:18.2.0" + checksum: 10/200cd65bf2e0be7ba6055f647091b725a45dd2a6abef03bf2380ce701fd5edccee40b49b9d15edab7ac08a762bf83cb4081e31ec2673a5bfb549a36ba21570df + languageName: node + linkType: hard + +"read-pkg@npm:^3.0.0": + version: 3.0.0 + resolution: "read-pkg@npm:3.0.0" + dependencies: + load-json-file: "npm:^4.0.0" + normalize-package-data: "npm:^2.3.2" + path-type: "npm:^3.0.0" + checksum: 10/398903ebae6c7e9965419a1062924436cc0b6f516c42c4679a90290d2f87448ed8f977e7aa2dbba4aa1ac09248628c43e493ac25b2bc76640e946035200e34c6 + languageName: node + linkType: hard + +"readable-stream@npm:2.3.7": + version: 2.3.7 + resolution: "readable-stream@npm:2.3.7" + dependencies: + core-util-is: "npm:~1.0.0" + inherits: "npm:~2.0.3" + isarray: "npm:~1.0.0" + process-nextick-args: "npm:~2.0.0" + safe-buffer: "npm:~5.1.1" + string_decoder: "npm:~1.1.1" + util-deprecate: "npm:~1.0.1" + checksum: 10/d04c677c1705e3fc6283d45859a23f4c05243d0c0f1fc08cb8f995b4d69f0eb7f38ec0ec102f0ee20535c5d999ee27449f40aa2edf6bf30c24d0cc8f8efeb6d7 + languageName: node + linkType: hard + +"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.5, readable-stream@npm:^2.3.0, readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6": + version: 2.3.8 + resolution: "readable-stream@npm:2.3.8" + dependencies: + core-util-is: "npm:~1.0.0" + inherits: "npm:~2.0.3" + isarray: "npm:~1.0.0" + process-nextick-args: "npm:~2.0.0" + safe-buffer: "npm:~5.1.1" + string_decoder: "npm:~1.1.1" + util-deprecate: "npm:~1.0.1" + checksum: 10/8500dd3a90e391d6c5d889256d50ec6026c059fadee98ae9aa9b86757d60ac46fff24fafb7a39fa41d54cb39d8be56cc77be202ebd4cd8ffcf4cb226cbaa40d4 + languageName: node + linkType: hard + +"readable-stream@npm:^3.0.0, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048 + languageName: node + linkType: hard + +"readable-web-to-node-stream@npm:^3.0.0": + version: 3.0.2 + resolution: "readable-web-to-node-stream@npm:3.0.2" + dependencies: + readable-stream: "npm:^3.6.0" + checksum: 10/d3a5bf9d707c01183d546a64864aa63df4d9cb835dfd2bf89ac8305e17389feef2170c4c14415a10d38f9b9bfddf829a57aaef7c53c8b40f11d499844bf8f1a4 + languageName: node + linkType: hard + +"readdir-glob@npm:^1.1.2": + version: 1.1.3 + resolution: "readdir-glob@npm:1.1.3" + dependencies: + minimatch: "npm:^5.1.0" + checksum: 10/ca3a20aa1e715d671302d4ec785a32bf08e59d6d0dd25d5fc03e9e5a39f8c612cdf809ab3e638a79973db7ad6868492edf38504701e313328e767693671447d6 + languageName: node + linkType: hard + +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 10/196b30ef6ccf9b6e18c4e1724b7334f72a093d011a99f3b5920470f0b3406a51770867b3e1ae9711f227ef7a7065982f6ee2ce316746b2cb42c88efe44297fe7 + languageName: node + linkType: hard + +"redis-commands@npm:^1.7.0": + version: 1.7.0 + resolution: "redis-commands@npm:1.7.0" + checksum: 10/c3c86ecefb7552d4333024dba8e0f1f6516568c2a74fd41643768781fb909524c7a581027d75e2456be1f0b7f08505c4c2252c6234abe044626455ef645c9459 + languageName: node + linkType: hard + +"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": + version: 1.2.0 + resolution: "redis-errors@npm:1.2.0" + checksum: 10/001c11f63ddd52d7c80eb4f4ede3a9433d29a458a7eea06b9154cb37c9802a218d93b7988247aa8c958d4b5d274b18354e8853c148f1096fda87c6e675cfd3ee + languageName: node + linkType: hard + +"redis-parser@npm:^3.0.0": + version: 3.0.0 + resolution: "redis-parser@npm:3.0.0" + dependencies: + redis-errors: "npm:^1.0.0" + checksum: 10/b10846844b4267f19ce1a6529465819c3d78c3e89db7eb0c3bb4eb19f83784797ec411274d15a77dbe08038b48f95f76014b83ca366dc955a016a3a0a0234650 + languageName: node + linkType: hard + +"redis@npm:^3.1.2": + version: 3.1.2 + resolution: "redis@npm:3.1.2" + dependencies: + denque: "npm:^1.5.0" + redis-commands: "npm:^1.7.0" + redis-errors: "npm:^1.2.0" + redis-parser: "npm:^3.0.0" + checksum: 10/a8a2973bbfe98b4fd295f4e560d212106887e7adecfa23f9ee3207cefac080f261a06027a867293f8ed78ebc18e2541e132eea8642af7283e75ef7aa61c2a83a + languageName: node + linkType: hard + +"regexp.prototype.flags@npm:^1.5.1": + version: 1.5.1 + resolution: "regexp.prototype.flags@npm:1.5.1" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + set-function-name: "npm:^2.0.0" + checksum: 10/3fa5610b8e411bbc3a43ddfd13162f3a817beb43155fbd8caa24d4fd0ce2f431a8197541808772a5a06e5946cebfb68464c827827115bde0d11720a92fe2981a + languageName: node + linkType: hard + +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10/a72468e2589270d91f06c7d36ec97a88db53ae5d6fe3787fadc943f0b0276b10347f89b363b2a82285f650bdcc135ad4a257c61bdd4d00d6df1fa24875b0ddaf + languageName: node + linkType: hard + +"require-from-string@npm:^2.0.2": + version: 2.0.2 + resolution: "require-from-string@npm:2.0.2" + checksum: 10/839a3a890102a658f4cb3e7b2aa13a1f80a3a976b512020c3d1efc418491c48a886b6e481ea56afc6c4cb5eef678f23b2a4e70575e7534eccadf5e30ed2e56eb + languageName: node + linkType: hard + +"requizzle@npm:^0.2.3": + version: 0.2.4 + resolution: "requizzle@npm:0.2.4" + dependencies: + lodash: "npm:^4.17.21" + checksum: 10/b13ce6d2a45d46be84ab274422b08a3233119e41ea5e1c8cef814abced2e25d15b7d4b995ef5d419f3dc6537314e6b1ba4a5c84d998d4143cf9938a67fc63587 + languageName: node + linkType: hard + +"resolve-alpn@npm:^1.0.0": + version: 1.2.1 + resolution: "resolve-alpn@npm:1.2.1" + checksum: 10/744e87888f0b6fa0b256ab454ca0b9c0b80808715e2ef1f3672773665c92a941f6181194e30ccae4a8cd0adbe0d955d3f133102636d2ee0cca0119fec0bc9aec + languageName: node + linkType: hard + +"resolve-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "resolve-cwd@npm:3.0.0" + dependencies: + resolve-from: "npm:^5.0.0" + checksum: 10/546e0816012d65778e580ad62b29e975a642989108d9a3c5beabfb2304192fa3c9f9146fbdfe213563c6ff51975ae41bac1d3c6e047dd9572c94863a057b4d81 + languageName: node + linkType: hard + +"resolve-from@npm:^4.0.0": + version: 4.0.0 + resolution: "resolve-from@npm:4.0.0" + checksum: 10/91eb76ce83621eea7bbdd9b55121a5c1c4a39e54a9ce04a9ad4517f102f8b5131c2cf07622c738a6683991bf54f2ce178f5a42803ecbd527ddc5105f362cc9e3 + languageName: node + linkType: hard + +"resolve-from@npm:^5.0.0": + version: 5.0.0 + resolution: "resolve-from@npm:5.0.0" + checksum: 10/be18a5e4d76dd711778664829841cde690971d02b6cbae277735a09c1c28f407b99ef6ef3cd585a1e6546d4097b28df40ed32c4a287b9699dcf6d7f208495e23 + languageName: node + linkType: hard + +"resolve.exports@npm:^2.0.0": + version: 2.0.2 + resolution: "resolve.exports@npm:2.0.2" + checksum: 10/f1cc0b6680f9a7e0345d783e0547f2a5110d8336b3c2a4227231dd007271ffd331fd722df934f017af90bae0373920ca0d4005da6f76cb3176c8ae426370f893 + languageName: node + linkType: hard + +"resolve@npm:^1.10.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4": + version: 1.22.6 + resolution: "resolve@npm:1.22.6" + dependencies: + is-core-module: "npm:^2.13.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10/b57acf016c94aded442f3c92dda4c4e9370ebe5b337ca2dbada3c022ce7c75cd20d5e31a855f884321c7379d6f2c7e640852024ae83f976e15367a1c4cf14de5 + languageName: node + linkType: hard + +"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": + version: 1.22.6 + resolution: "resolve@patch:resolve@npm%3A1.22.6#optional!builtin::version=1.22.6&hash=c3c19d" + dependencies: + is-core-module: "npm:^2.13.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10/d63580488eaffef80d16930ed76ffc786d6f51ac02e5821a8fb54a9c7bef4d355472123abdd36fbc0c68704495e09581f0feba75dc4b0b946818f96ece5c3e2a + languageName: node + linkType: hard + +"responselike@npm:^2.0.0": + version: 2.0.1 + resolution: "responselike@npm:2.0.1" + dependencies: + lowercase-keys: "npm:^2.0.0" + checksum: 10/b122535466e9c97b55e69c7f18e2be0ce3823c5d47ee8de0d9c0b114aa55741c6db8bfbfce3766a94d1272e61bfb1ebf0a15e9310ac5629fbb7446a861b4fd3a + languageName: node + linkType: hard + +"restore-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "restore-cursor@npm:3.1.0" + dependencies: + onetime: "npm:^5.1.0" + signal-exit: "npm:^3.0.2" + checksum: 10/f877dd8741796b909f2a82454ec111afb84eb45890eb49ac947d87991379406b3b83ff9673a46012fca0d7844bb989f45cc5b788254cf1a39b6b5a9659de0630 + languageName: node + linkType: hard + +"retry-as-promised@npm:^7.0.4": + version: 7.0.4 + resolution: "retry-as-promised@npm:7.0.4" + checksum: 10/cd9fd20e990c6980a2979348fbc198aa4a065f03242c1cd7782372da7054253927e0803291c843db07255a38d255936cc0f9da55bf826c9f75443a9dedb8bf4b + languageName: node + linkType: hard + +"retry-request@npm:^5.0.0": + version: 5.0.2 + resolution: "retry-request@npm:5.0.2" + dependencies: + debug: "npm:^4.1.1" + extend: "npm:^3.0.2" + checksum: 10/d5045fae567337920ec0d2d5b0206ebf138ca91b5ebcdeca5f084a2d18884dcc5d2a7e1ec7703e3be88b6a2d28dffb7c1ce3eb65bf355821dc40bedda35b2ee2 + languageName: node + linkType: hard + +"retry@npm:0.13.1, retry@npm:^0.13.1": + version: 0.13.1 + resolution: "retry@npm:0.13.1" + checksum: 10/6125ec2e06d6e47e9201539c887defba4e47f63471db304c59e4b82fc63c8e89ca06a77e9d34939a9a42a76f00774b2f46c0d4a4cbb3e287268bd018ed69426d + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10/1f914879f97e7ee931ad05fe3afa629bd55270fc6cf1c1e589b6a99fab96d15daad0fa1a52a00c729ec0078045fe3e399bd4fd0c93bcc906957bdc17f89cb8e6 + languageName: node + linkType: hard + +"reusify@npm:^1.0.4": + version: 1.0.4 + resolution: "reusify@npm:1.0.4" + checksum: 10/14222c9e1d3f9ae01480c50d96057228a8524706db79cdeb5a2ce5bb7070dd9f409a6f84a02cbef8cdc80d39aef86f2dd03d155188a1300c599b05437dcd2ffb + languageName: node + linkType: hard + +"rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: bin.js + checksum: 10/063ffaccaaaca2cfd0ef3beafb12d6a03dd7ff1260d752d62a6077b5dfff6ae81bea571f655bb6b589d366930ec1bdd285d40d560c0dae9b12f125e54eb743d5 + languageName: node + linkType: hard + +"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1, ripemd160@npm:^2.0.2": + version: 2.0.2 + resolution: "ripemd160@npm:2.0.2" + dependencies: + hash-base: "npm:^3.0.0" + inherits: "npm:^2.0.1" + checksum: 10/006accc40578ee2beae382757c4ce2908a826b27e2b079efdcd2959ee544ddf210b7b5d7d5e80467807604244e7388427330f5c6d4cd61e6edaddc5773ccc393 + languageName: node + linkType: hard + +"run-async@npm:^2.4.0": + version: 2.4.1 + resolution: "run-async@npm:2.4.1" + checksum: 10/c79551224dafa26ecc281cb1efad3510c82c79116aaf681f8a931ce70fdf4ca880d58f97d3b930a38992c7aad7955a08e065b32ec194e1dd49d7790c874ece50 + languageName: node + linkType: hard + +"run-parallel-limit@npm:^1.1.0": + version: 1.1.0 + resolution: "run-parallel-limit@npm:1.1.0" + dependencies: + queue-microtask: "npm:^1.2.2" + checksum: 10/672c3b87e7f939c684b9965222b361421db0930223ed1e43ebf0e7e48ccc1a022ea4de080bef4d5468434e2577c33b7681e3f03b7593fdc49ad250a55381123c + languageName: node + linkType: hard + +"run-parallel@npm:^1.1.9": + version: 1.2.0 + resolution: "run-parallel@npm:1.2.0" + dependencies: + queue-microtask: "npm:^1.2.2" + checksum: 10/cb4f97ad25a75ebc11a8ef4e33bb962f8af8516bb2001082ceabd8902e15b98f4b84b4f8a9b222e5d57fc3bd1379c483886ed4619367a7680dad65316993021d + languageName: node + linkType: hard + +"rxjs@npm:^7.5.5": + version: 7.8.1 + resolution: "rxjs@npm:7.8.1" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10/b10cac1a5258f885e9dd1b70d23c34daeb21b61222ee735d2ec40a8685bdca40429000703a44f0e638c27a684ac139e1c37e835d2a0dc16f6fc061a138ae3abb + languageName: node + linkType: hard + +"safe-array-concat@npm:^1.0.1": + version: 1.0.1 + resolution: "safe-array-concat@npm:1.0.1" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.2.1" + has-symbols: "npm:^1.0.3" + isarray: "npm:^2.0.5" + checksum: 10/44f073d85ca12458138e6eff103ac63cec619c8261b6579bd2fa3ae7b6516cf153f02596d68e40c5bbe322a29c930017800efff652734ddcb8c0f33b2a71f89c + languageName: node + linkType: hard + +"safe-buffer@npm:5.1.2, safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": + version: 5.1.2 + resolution: "safe-buffer@npm:5.1.2" + checksum: 10/7eb5b48f2ed9a594a4795677d5a150faa7eb54483b2318b568dc0c4fc94092a6cce5be02c7288a0500a156282f5276d5688bce7259299568d1053b2150ef374a + languageName: node + linkType: hard + +"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 10/32872cd0ff68a3ddade7a7617b8f4c2ae8764d8b7d884c651b74457967a9e0e886267d3ecc781220629c44a865167b61c375d2da6c720c840ecd73f45d5d9451 + languageName: node + linkType: hard + +"safe-regex-test@npm:^1.0.0": + version: 1.0.0 + resolution: "safe-regex-test@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.1.3" + is-regex: "npm:^1.1.4" + checksum: 10/c7248dfa07891aa634c8b9c55da696e246f8589ca50e7fd14b22b154a106e83209ddf061baf2fa45ebfbd485b094dc7297325acfc50724de6afe7138451b42a9 + languageName: node + linkType: hard + +"safe-stable-stringify@npm:^2.3.1": + version: 2.4.3 + resolution: "safe-stable-stringify@npm:2.4.3" + checksum: 10/a6c192bbefe47770a11072b51b500ed29be7b1c15095371c1ee1dc13e45ce48ee3c80330214c56764d006c485b88bd0b24940d868948170dddc16eed312582d8 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 10/7eaf7a0cf37cc27b42fb3ef6a9b1df6e93a1c6d98c6c6702b02fe262d5fcbd89db63320793b99b21cb5348097d0a53de81bd5f4e8b86e20cc9412e3f1cfb4e83 + languageName: node + linkType: hard + +"sax@npm:1.2.1": + version: 1.2.1 + resolution: "sax@npm:1.2.1" + checksum: 10/d64f65291ce127f191eb2c22012f8f608736e306db6a28306e618bb1324cfbc19f6783c49ce0d88e5628fde30878c29189c8fb3c62c83f079b471734e4df455d + languageName: node + linkType: hard + +"sax@npm:>=0.6.0": + version: 1.3.0 + resolution: "sax@npm:1.3.0" + checksum: 10/bb571b31d30ecb0353c2ff5f87b117a03e5fb9eb4c1519141854c1a8fbee0a77ddbe8045f413259e711833aa03da210887df8527d19cdc55f299822dbf4b34de + languageName: node + linkType: hard + +"schema-utils@npm:^3.1.1, schema-utils@npm:^3.2.0": + version: 3.3.0 + resolution: "schema-utils@npm:3.3.0" + dependencies: + "@types/json-schema": "npm:^7.0.8" + ajv: "npm:^6.12.5" + ajv-keywords: "npm:^3.5.2" + checksum: 10/2c7bbb1da967fdfd320e6cea538949006ec6e8c13ea560a4f94ff2c56809a8486fa5ec419e023452501a6befe1ca381e409c2798c24f4993c7c4094d97fdb258 + languageName: node + linkType: hard + +"secp256k1@npm:^3.0.1": + version: 3.8.0 + resolution: "secp256k1@npm:3.8.0" + dependencies: + bindings: "npm:^1.5.0" + bip66: "npm:^1.1.5" + bn.js: "npm:^4.11.8" + create-hash: "npm:^1.2.0" + drbg.js: "npm:^1.0.1" + elliptic: "npm:^6.5.2" + nan: "npm:^2.14.0" + node-gyp: "npm:latest" + safe-buffer: "npm:^5.1.2" + checksum: 10/45e65c68affb228fa253297188ba64c60c39a0f0defc80578ca50e0dda188efb109e9711f9d434672d3e1507860434a9c4bf16bf41a91d67ae50d32f8f6e2059 + languageName: node + linkType: hard + +"seek-bzip@npm:^1.0.5": + version: 1.0.6 + resolution: "seek-bzip@npm:1.0.6" + dependencies: + commander: "npm:^2.8.1" + bin: + seek-bunzip: bin/seek-bunzip + seek-table: bin/seek-bzip-table + checksum: 10/e47967b694ba51b87a4e7b388772f9c9f6826547972c4c0d2f72b6dd9a41825fe63e810ad56be0f1bcba71c90550b7cb3aee53c261b9aebc15af1cd04fae008f + languageName: node + linkType: hard + +"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0, semver@npm:^5.6.0": + version: 5.7.2 + resolution: "semver@npm:5.7.2" + bin: + semver: bin/semver + checksum: 10/fca14418a174d4b4ef1fecb32c5941e3412d52a4d3d85165924ce3a47fbc7073372c26faf7484ceb4bbc2bde25880c6b97e492473dc7e9708fdfb1c6a02d546e + languageName: node + linkType: hard + +"semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": + version: 6.3.1 + resolution: "semver@npm:6.3.1" + bin: + semver: bin/semver.js + checksum: 10/1ef3a85bd02a760c6ef76a45b8c1ce18226de40831e02a00bad78485390b98b6ccaa31046245fc63bba4a47a6a592b6c7eedc65cc47126e60489f9cc1ce3ed7e + languageName: node + linkType: hard + +"semver@npm:^7.1.2, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4": + version: 7.5.4 + resolution: "semver@npm:7.5.4" + dependencies: + lru-cache: "npm:^6.0.0" + bin: + semver: bin/semver.js + checksum: 10/985dec0d372370229a262c737063860fabd4a1c730662c1ea3200a2f649117761a42184c96df62a0e885e76fbd5dace41087d6c1ac0351b13c0df5d6bcb1b5ac + languageName: node + linkType: hard + +"seq-queue@npm:^0.0.5": + version: 0.0.5 + resolution: "seq-queue@npm:0.0.5" + checksum: 10/fa302e3b2aaece644532603ae42d675f9b8750e395a98740dd58dc5e02985ce6f0c2b78715b5984d6f6a807893735a14212a70d6ec591e6fba410397269588a0 + languageName: node + linkType: hard + +"sequelize-cli@npm:^6.6.1": + version: 6.6.1 + resolution: "sequelize-cli@npm:6.6.1" + dependencies: + cli-color: "npm:^2.0.3" + fs-extra: "npm:^9.1.0" + js-beautify: "npm:^1.14.5" + lodash: "npm:^4.17.21" + resolve: "npm:^1.22.1" + umzug: "npm:^2.3.0" + yargs: "npm:^16.2.0" + bin: + sequelize: lib/sequelize + sequelize-cli: lib/sequelize + checksum: 10/bce80ec04d8381736bc36b7fb85d2a83866b516e74eec50d6fa0f9f40d5123ed1fb51064d8a7507e169a015b98383149d964630c129265f9f22231a2b679cfd0 + languageName: node + linkType: hard + +"sequelize-pool@npm:^7.1.0": + version: 7.1.0 + resolution: "sequelize-pool@npm:7.1.0" + checksum: 10/eeb0837451afb245cf3aece5d93c50ef051bd7f4397c4e578f8cbf41ebf485e0acd887c1aa3f4394b80dc874229a32ce5aafeaa2f00ec3ecc5dfcc518bd7bf7e + languageName: node + linkType: hard + +"sequelize@npm:^6.33.0": + version: 6.33.0 + resolution: "sequelize@npm:6.33.0" + dependencies: + "@types/debug": "npm:^4.1.8" + "@types/validator": "npm:^13.7.17" + debug: "npm:^4.3.4" + dottie: "npm:^2.0.6" + inflection: "npm:^1.13.4" + lodash: "npm:^4.17.21" + moment: "npm:^2.29.4" + moment-timezone: "npm:^0.5.43" + pg-connection-string: "npm:^2.6.1" + retry-as-promised: "npm:^7.0.4" + semver: "npm:^7.5.4" + sequelize-pool: "npm:^7.1.0" + toposort-class: "npm:^1.0.1" + uuid: "npm:^8.3.2" + validator: "npm:^13.9.0" + wkx: "npm:^0.5.0" + peerDependenciesMeta: + ibm_db: + optional: true + mariadb: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-hstore: + optional: true + snowflake-sdk: + optional: true + sqlite3: + optional: true + tedious: + optional: true + checksum: 10/b8c04f5affe6df71aa9a8ff86172963373263832884a56710ca37e3bf2fb523b2493c7b99d5c0ba6917fbd73d76176e37444c2996ed55c0b65b9ab891d7d9389 + languageName: node + linkType: hard + +"serialize-javascript@npm:^6.0.1": + version: 6.0.1 + resolution: "serialize-javascript@npm:6.0.1" + dependencies: + randombytes: "npm:^2.1.0" + checksum: 10/f756b1ff34b655b2183c64dd6683d28d4d9b9a80284b264cac9fd421c73890491eafd6c5c2bbe93f1f21bf78b572037c5a18d24b044c317ee1c9dc44d22db94c + languageName: node + linkType: hard + +"serverless-api-gateway-throttling@npm:^1.1.1": + version: 1.2.2 + resolution: "serverless-api-gateway-throttling@npm:1.2.2" + dependencies: + lodash.get: "npm:^4.4.2" + lodash.isempty: "npm:^4.4.0" + checksum: 10/53bef6ce08a18423affa434aa3330d1a8fafbaae24b657e0f22d30d0f69dc9ddc471ce36cb19f34328f2ebbda54d5ebabb469a79c07b091bc3452fec236079fd + languageName: node + linkType: hard + +"serverless-iam-roles-per-function@npm:^3.2.0": + version: 3.2.0 + resolution: "serverless-iam-roles-per-function@npm:3.2.0" + dependencies: + lodash: "npm:^4.17.20" + checksum: 10/8de1f2b77f044b8bce15d4723a1a313854d498ae0679a87886520ee7f239c03bcb1bcb7de3b983f806299b0f0dcce0c9a1c156947c71538617636ec60980ff1c + languageName: node + linkType: hard + +"serverless-mysql@npm:^1.5.4": + version: 1.5.5 + resolution: "serverless-mysql@npm:1.5.5" + dependencies: + "@types/mysql": "npm:^2.15.6" + mysql: "npm:^2.18.1" + dependenciesMeta: + "@types/mysql": + optional: true + checksum: 10/1316056aa3d0651e1825bb4e1e19d30615c45a4bb715fdb313c896573004cd11ce9cdc85ed8e19c9fd0d0280f8f64604aab07a94f544198ddb5b6c7ebd337984 + languageName: node + linkType: hard + +"serverless-offline@npm:^13.1.2": + version: 13.1.2 + resolution: "serverless-offline@npm:13.1.2" + dependencies: + "@aws-sdk/client-lambda": "npm:^3.421.0" + "@hapi/boom": "npm:^10.0.1" + "@hapi/h2o2": "npm:^10.0.4" + "@hapi/hapi": "npm:^21.3.2" + "@serverless/utils": "npm:^6.15.0" + array-unflat-js: "npm:^0.1.3" + boxen: "npm:^7.1.1" + chalk: "npm:^5.3.0" + desm: "npm:^1.3.0" + execa: "npm:^8.0.1" + fs-extra: "npm:^11.1.1" + is-wsl: "npm:^3.1.0" + java-invoke-local: "npm:0.0.6" + jose: "npm:^4.14.6" + js-string-escape: "npm:^1.0.1" + jsonpath-plus: "npm:^7.2.0" + jsonschema: "npm:^1.4.1" + jszip: "npm:^3.10.1" + luxon: "npm:^3.2.0" + node-schedule: "npm:^2.1.1" + p-memoize: "npm:^7.1.1" + p-retry: "npm:^6.1.0" + velocityjs: "npm:^2.0.6" + ws: "npm:^8.14.2" + peerDependencies: + serverless: ^3.2.0 + checksum: 10/bcc5d3cd2dfe2d4b599540f8f6f38533b802325ba291f9e81b8952049ab2d32013b0c1ec874e46ec021fadb5fc1f8474994bb879cd163769c9e1043a0f2b317d + languageName: node + linkType: hard + +"serverless-plugin-aws-alerts@npm:^1.7.5": + version: 1.7.5 + resolution: "serverless-plugin-aws-alerts@npm:1.7.5" + dependencies: + lodash.isobject: "npm:^3.0.2" + lodash.isstring: "npm:^4.0.1" + lodash.merge: "npm:^4.6.2" + lodash.union: "npm:^4.6.0" + lodash.upperfirst: "npm:^4.3.1" + peerDependencies: + serverless: ^2.4.0 || 3 + checksum: 10/8324b48be8477be4b66c011c6f1db3a7b97e623234782a5c02cf57e5522ac5d0830fc067180f2203da5ddde24307798811607aa278723bd72886fd42623efaf2 + languageName: node + linkType: hard + +"serverless-plugin-monorepo@npm:^0.11.0": + version: 0.11.0 + resolution: "serverless-plugin-monorepo@npm:0.11.0" + dependencies: + fs-extra: "npm:^9.0.1" + peerDependencies: + serverless: 1 || 2 || 3 + checksum: 10/bee8f8763ae6dd330d83c20938bf684c5d8ebed5061d4625975524c5b40699703fee62c562b85728467f32a8c7a2ba7117d1338d1e2f37a8bdc6f03af23ee729 + languageName: node + linkType: hard + +"serverless-plugin-warmup@npm:^8.2.1": + version: 8.2.1 + resolution: "serverless-plugin-warmup@npm:8.2.1" + checksum: 10/9404bd15f9f5456ad088c179ae8bba30a58fd89ba6f453f69a4ca20773beb904097e4b12cff9c9cd6aaa6cd178fd31b305f1552e84ac90c463aa4ecaf7912c9c + languageName: node + linkType: hard + +"serverless-prune-plugin@npm:^2.0.2": + version: 2.0.2 + resolution: "serverless-prune-plugin@npm:2.0.2" + dependencies: + bluebird: "npm:^3.7.2" + peerDependencies: + serverless: 1 || 2 || 3 + checksum: 10/d16143faa6da319ba4764438adafafe64174e9065c14b0f3c574d92f875fb41e5c5d5946ca7c94dddb41a6c7a4df01e19d390d9c7c67665dacbbd48d7af73ae4 + languageName: node + linkType: hard + +"serverless-webpack@npm:^5.13.0": + version: 5.13.0 + resolution: "serverless-webpack@npm:5.13.0" + dependencies: + archiver: "npm:^5.3.1" + bluebird: "npm:^3.7.2" + find-yarn-workspace-root: "npm:^2.0.0" + fs-extra: "npm:^11.1.1" + glob: "npm:^8.1.0" + is-builtin-module: "npm:^3.2.1" + lodash: "npm:^4.17.21" + semver: "npm:^7.3.8" + ts-node: "npm:>= 8.3.0" + peerDependencies: + "@types/node": "*" + serverless: 1 || 2 || 3 + typescript: ">=2.0" + webpack: ">= 3.0.0 < 6" + dependenciesMeta: + ts-node: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + typescript: + optional: true + checksum: 10/de11527cda0b41400074cb11445806c5526112391bc1dfa810beb125d997897b128e0b781da77197a3cd03cfdcbab58da4e69a232a14ab9e79bedcfd98e1ed3e + languageName: node + linkType: hard + +"serverless@npm:^3.35.2": + version: 3.35.2 + resolution: "serverless@npm:3.35.2" + dependencies: + "@serverless/dashboard-plugin": "npm:^7.0.2" + "@serverless/platform-client": "npm:^4.4.0" + "@serverless/utils": "npm:^6.13.1" + abort-controller: "npm:^3.0.0" + ajv: "npm:^8.12.0" + ajv-formats: "npm:^2.1.1" + archiver: "npm:^5.3.1" + aws-sdk: "npm:^2.1404.0" + bluebird: "npm:^3.7.2" + cachedir: "npm:^2.3.0" + chalk: "npm:^4.1.2" + child-process-ext: "npm:^2.1.1" + ci-info: "npm:^3.8.0" + cli-progress-footer: "npm:^2.3.2" + d: "npm:^1.0.1" + dayjs: "npm:^1.11.8" + decompress: "npm:^4.2.1" + dotenv: "npm:^16.3.1" + dotenv-expand: "npm:^10.0.0" + essentials: "npm:^1.2.0" + ext: "npm:^1.7.0" + fastest-levenshtein: "npm:^1.0.16" + filesize: "npm:^10.0.7" + fs-extra: "npm:^10.1.0" + get-stdin: "npm:^8.0.0" + globby: "npm:^11.1.0" + graceful-fs: "npm:^4.2.11" + https-proxy-agent: "npm:^5.0.1" + is-docker: "npm:^2.2.1" + js-yaml: "npm:^4.1.0" + json-colorizer: "npm:^2.2.2" + json-cycle: "npm:^1.5.0" + json-refs: "npm:^3.0.15" + lodash: "npm:^4.17.21" + memoizee: "npm:^0.4.15" + micromatch: "npm:^4.0.5" + node-fetch: "npm:^2.6.11" + npm-registry-utilities: "npm:^1.0.0" + object-hash: "npm:^3.0.0" + open: "npm:^8.4.2" + path2: "npm:^0.1.0" + process-utils: "npm:^4.0.0" + promise-queue: "npm:^2.2.5" + require-from-string: "npm:^2.0.2" + semver: "npm:^7.5.3" + signal-exit: "npm:^3.0.7" + stream-buffers: "npm:^3.0.2" + strip-ansi: "npm:^6.0.1" + supports-color: "npm:^8.1.1" + tar: "npm:^6.1.15" + timers-ext: "npm:^0.1.7" + type: "npm:^2.7.2" + untildify: "npm:^4.0.0" + uuid: "npm:^9.0.0" + ws: "npm:^7.5.9" + yaml-ast-parser: "npm:0.0.43" + bin: + serverless: bin/serverless.js + sls: bin/serverless.js + checksum: 10/68a994a885213208e07a699429b6ee65976fcf44f4c62164128d3fc9aaafb02736b4daaf25805204a20538c5c1a86713bb4cc68bbd8a8e7b2bd917bb86a3f03b + languageName: node + linkType: hard + +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 10/8980ebf7ae9eb945bb036b6e283c547ee783a1ad557a82babf758a065e2fb6ea337fd82cac30dd565c1e606e423f30024a19fff7afbf4977d784720c4026a8ef + languageName: node + linkType: hard + +"set-function-name@npm:^2.0.0": + version: 2.0.1 + resolution: "set-function-name@npm:2.0.1" + dependencies: + define-data-property: "npm:^1.0.1" + functions-have-names: "npm:^1.2.3" + has-property-descriptors: "npm:^1.0.0" + checksum: 10/4975d17d90c40168eee2c7c9c59d023429f0a1690a89d75656306481ece0c3c1fb1ebcc0150ea546d1913e35fbd037bace91372c69e543e51fc5d1f31a9fa126 + languageName: node + linkType: hard + +"setimmediate@npm:^1.0.5": + version: 1.0.5 + resolution: "setimmediate@npm:1.0.5" + checksum: 10/76e3f5d7f4b581b6100ff819761f04a984fa3f3990e72a6554b57188ded53efce2d3d6c0932c10f810b7c59414f85e2ab3c11521877d1dea1ce0b56dc906f485 + languageName: node + linkType: hard + +"sha.js@npm:^2.4.0, sha.js@npm:^2.4.8": + version: 2.4.11 + resolution: "sha.js@npm:2.4.11" + dependencies: + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + bin: + sha.js: ./bin.js + checksum: 10/d833bfa3e0a67579a6ce6e1bc95571f05246e0a441dd8c76e3057972f2a3e098465687a4369b07e83a0375a88703577f71b5b2e966809e67ebc340dbedb478c7 + languageName: node + linkType: hard + +"shebang-command@npm:^1.2.0": + version: 1.2.0 + resolution: "shebang-command@npm:1.2.0" + dependencies: + shebang-regex: "npm:^1.0.0" + checksum: 10/9eed1750301e622961ba5d588af2212505e96770ec376a37ab678f965795e995ade7ed44910f5d3d3cb5e10165a1847f52d3348c64e146b8be922f7707958908 + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 10/6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa + languageName: node + linkType: hard + +"shebang-regex@npm:^1.0.0": + version: 1.0.0 + resolution: "shebang-regex@npm:1.0.0" + checksum: 10/404c5a752cd40f94591dfd9346da40a735a05139dac890ffc229afba610854d8799aaa52f87f7e0c94c5007f2c6af55bdcaeb584b56691926c5eaf41dc8f1372 + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 10/1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222 + languageName: node + linkType: hard + +"shell-quote@npm:^1.6.1": + version: 1.8.1 + resolution: "shell-quote@npm:1.8.1" + checksum: 10/af19ab5a1ec30cb4b2f91fd6df49a7442d5c4825a2e269b3712eded10eedd7f9efeaab96d57829880733fc55bcdd8e9b1d8589b4befb06667c731d08145e274d + languageName: node + linkType: hard + +"side-channel@npm:^1.0.4": + version: 1.0.4 + resolution: "side-channel@npm:1.0.4" + dependencies: + call-bind: "npm:^1.0.0" + get-intrinsic: "npm:^1.0.2" + object-inspect: "npm:^1.9.0" + checksum: 10/c4998d9fc530b0e75a7fd791ad868fdc42846f072734f9080ff55cc8dc7d3899abcda24fd896aa6648c3ab7021b4bb478073eb4f44dfd55bce9714bc1a7c5d45 + languageName: node + linkType: hard + +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: 10/a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f + languageName: node + linkType: hard + +"simple-git@npm:^3.16.0": + version: 3.20.0 + resolution: "simple-git@npm:3.20.0" + dependencies: + "@kwsites/file-exists": "npm:^1.1.1" + "@kwsites/promise-deferred": "npm:^1.1.1" + debug: "npm:^4.3.4" + checksum: 10/fabfdbabfec8c7a7484d22d0218fb4ff9c8acdecaadc34c4655cd10f2aacd40bd656284abdf1613831b692d7fe1be58314b23e9f1adfe380f2b910622cc2468e + languageName: node + linkType: hard + +"simple-swizzle@npm:^0.2.2": + version: 0.2.2 + resolution: "simple-swizzle@npm:0.2.2" + dependencies: + is-arrayish: "npm:^0.3.1" + checksum: 10/c6dffff17aaa383dae7e5c056fbf10cf9855a9f79949f20ee225c04f06ddde56323600e0f3d6797e82d08d006e93761122527438ee9531620031c08c9e0d73cc + languageName: node + linkType: hard + +"sisteransi@npm:^1.0.5": + version: 1.0.5 + resolution: "sisteransi@npm:1.0.5" + checksum: 10/aba6438f46d2bfcef94cf112c835ab395172c75f67453fe05c340c770d3c402363018ae1ab4172a1026a90c47eaccf3af7b6ff6fa749a680c2929bd7fa2b37a4 + languageName: node + linkType: hard + +"slash@npm:^3.0.0": + version: 3.0.0 + resolution: "slash@npm:3.0.0" + checksum: 10/94a93fff615f25a999ad4b83c9d5e257a7280c90a32a7cb8b4a87996e4babf322e469c42b7f649fd5796edd8687652f3fb452a86dc97a816f01113183393f11c + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 10/927484aa0b1640fd9473cee3e0a0bcad6fce93fd7bbc18bac9ad0c33686f5d2e2c422fba24b5899c184524af01e11dd2bd051c2bf2b07e47aff8ca72cbfc60d2 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^6.0.0": + version: 6.2.1 + resolution: "socks-proxy-agent@npm:6.2.1" + dependencies: + agent-base: "npm:^6.0.2" + debug: "npm:^4.3.3" + socks: "npm:^2.6.2" + checksum: 10/554749ba3bdba0742ec36493a907261c116dd0dafcd618ea5babdfc90ce5a5ae648d4ee4d2e26e7184afd854973d282372ce0af63e1fc6412bb9fa1a2b1f2d45 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^7.0.0": + version: 7.0.0 + resolution: "socks-proxy-agent@npm:7.0.0" + dependencies: + agent-base: "npm:^6.0.2" + debug: "npm:^4.3.3" + socks: "npm:^2.6.2" + checksum: 10/26c75d9c62a9ed3fd494df60e65e88da442f78e0d4bc19bfd85ac37bd2c67470d6d4bba5202e804561cda6674db52864c9e2a2266775f879bc8d89c1445a5f4c + languageName: node + linkType: hard + +"socks@npm:^2.6.2": + version: 2.7.1 + resolution: "socks@npm:2.7.1" + dependencies: + ip: "npm:^2.0.0" + smart-buffer: "npm:^4.2.0" + checksum: 10/5074f7d6a13b3155fa655191df1c7e7a48ce3234b8ccf99afa2ccb56591c195e75e8bb78486f8e9ea8168e95a29573cbaad55b2b5e195160ae4d2ea6811ba833 + languageName: node + linkType: hard + +"sort-keys-length@npm:^1.0.0": + version: 1.0.1 + resolution: "sort-keys-length@npm:1.0.1" + dependencies: + sort-keys: "npm:^1.0.0" + checksum: 10/f9acac5fb31580a9e3d43b419dc86a1b75e85b79036a084d95dd4d1062b621c9589906588ac31e370a0dd381be46d8dbe900efa306d087ca9c912d7a59b5a590 + languageName: node + linkType: hard + +"sort-keys@npm:^1.0.0": + version: 1.1.2 + resolution: "sort-keys@npm:1.1.2" + dependencies: + is-plain-obj: "npm:^1.0.0" + checksum: 10/0ac2ea2327d92252f07aa7b2f8c7023a1f6ce3306439a3e81638cce9905893c069521d168f530fb316d1a929bdb052b742969a378190afaef1bc64fa69e29576 + languageName: node + linkType: hard + +"sorted-array-functions@npm:^1.3.0": + version: 1.3.0 + resolution: "sorted-array-functions@npm:1.3.0" + checksum: 10/673fd39ca3b6c92644d4483eac1700bb7d7555713a536822a7522a35af559bef3e72f10d89356b75042dc394cd7c2e2ab6f40024385218ec3c85bb7335032857 + languageName: node + linkType: hard + +"source-map-support@npm:0.5.13": + version: 0.5.13 + resolution: "source-map-support@npm:0.5.13" + dependencies: + buffer-from: "npm:^1.0.0" + source-map: "npm:^0.6.0" + checksum: 10/d1514a922ac9c7e4786037eeff6c3322f461cd25da34bb9fefb15387b3490531774e6e31d95ab6d5b84a3e139af9c3a570ccaee6b47bd7ea262691ed3a8bc34e + languageName: node + linkType: hard + +"source-map-support@npm:^0.5.19, source-map-support@npm:~0.5.20": + version: 0.5.21 + resolution: "source-map-support@npm:0.5.21" + dependencies: + buffer-from: "npm:^1.0.0" + source-map: "npm:^0.6.0" + checksum: 10/8317e12d84019b31e34b86d483dd41d6f832f389f7417faf8fc5c75a66a12d9686e47f589a0554a868b8482f037e23df9d040d29387eb16fa14cb85f091ba207 + languageName: node + linkType: hard + +"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.1": + version: 0.6.1 + resolution: "source-map@npm:0.6.1" + checksum: 10/59ef7462f1c29d502b3057e822cdbdae0b0e565302c4dd1a95e11e793d8d9d62006cdc10e0fd99163ca33ff2071360cf50ee13f90440806e7ed57d81cba2f7ff + languageName: node + linkType: hard + +"spdx-correct@npm:^3.0.0": + version: 3.2.0 + resolution: "spdx-correct@npm:3.2.0" + dependencies: + spdx-expression-parse: "npm:^3.0.0" + spdx-license-ids: "npm:^3.0.0" + checksum: 10/cc2e4dbef822f6d12142116557d63f5facf3300e92a6bd24e907e4865e17b7e1abd0ee6b67f305cae6790fc2194175a24dc394bfcc01eea84e2bdad728e9ae9a + languageName: node + linkType: hard + +"spdx-exceptions@npm:^2.1.0": + version: 2.3.0 + resolution: "spdx-exceptions@npm:2.3.0" + checksum: 10/cb69a26fa3b46305637123cd37c85f75610e8c477b6476fa7354eb67c08128d159f1d36715f19be6f9daf4b680337deb8c65acdcae7f2608ba51931540687ac0 + languageName: node + linkType: hard + +"spdx-expression-parse@npm:^3.0.0": + version: 3.0.1 + resolution: "spdx-expression-parse@npm:3.0.1" + dependencies: + spdx-exceptions: "npm:^2.1.0" + spdx-license-ids: "npm:^3.0.0" + checksum: 10/a1c6e104a2cbada7a593eaa9f430bd5e148ef5290d4c0409899855ce8b1c39652bcc88a725259491a82601159d6dc790bedefc9016c7472f7de8de7361f8ccde + languageName: node + linkType: hard + +"spdx-license-ids@npm:^3.0.0": + version: 3.0.15 + resolution: "spdx-license-ids@npm:3.0.15" + checksum: 10/61b0faeae89c168d0e8a41125e5210a8f2b2ed36c0157fb413b337ebb2b3aa046f3c31ada92e5f3a38f97bb800886a3179bde45da2f69b7eec5fab3a5454bfe4 + languageName: node + linkType: hard + +"split2@npm:^3.1.1, split2@npm:^3.2.2": + version: 3.2.2 + resolution: "split2@npm:3.2.2" + dependencies: + readable-stream: "npm:^3.0.0" + checksum: 10/a426e1e6718e2f7e50f102d5ec3525063d885e3d9cec021a81175fd3497fdb8b867a89c99e70bef4daeef4f2f5e544f7b92df8c1a30b4254e10a9cfdcc3dae87 + languageName: node + linkType: hard + +"sprintf-js@npm:~1.0.2": + version: 1.0.3 + resolution: "sprintf-js@npm:1.0.3" + checksum: 10/c34828732ab8509c2741e5fd1af6b767c3daf2c642f267788f933a65b1614943c282e74c4284f4fa749c264b18ee016a0d37a3e5b73aee446da46277d3a85daa + languageName: node + linkType: hard + +"sprintf-kit@npm:^2.0.1": + version: 2.0.1 + resolution: "sprintf-kit@npm:2.0.1" + dependencies: + es5-ext: "npm:^0.10.53" + checksum: 10/fa388720a9cf16d4265f5d28269680e5697b90e3bb8974fafec2c1dcaa971f64431069f4bed9604fd4bc7027f43f768e6c096140d2d8ec8821b15adf8a50b589 + languageName: node + linkType: hard + +"sqlite3@npm:^5.0.2": + version: 5.1.6 + resolution: "sqlite3@npm:5.1.6" + dependencies: + "@mapbox/node-pre-gyp": "npm:^1.0.0" + node-addon-api: "npm:^4.2.0" + node-gyp: "npm:8.x" + tar: "npm:^6.1.11" + peerDependencies: + node-gyp: 8.x + dependenciesMeta: + node-gyp: + optional: true + peerDependenciesMeta: + node-gyp: + optional: true + checksum: 10/343ffefb69c044f256043b5945a91b723b26b9c1ad55eba896e8895a12303307cfd931f00ebe1d39a2b5d475acdf3c4f877539de59877ddc85a22fb9f4c5755d + languageName: node + linkType: hard + +"sqlstring@npm:2.3.1": + version: 2.3.1 + resolution: "sqlstring@npm:2.3.1" + checksum: 10/bc09237002da7e1172098e7d47401ea0ae45c1e4b224619f7ee2905dc921321f5ccc8c5e076994890df01b4a3363b2b5ea295b7a10d32a35181ef25bad158093 + languageName: node + linkType: hard + +"sqlstring@npm:^2.3.2": + version: 2.3.3 + resolution: "sqlstring@npm:2.3.3" + checksum: 10/4e5a25af2d77a031fe00694034bf9fd822ddc3a483c9383124b120aa6b9ae9ab71e173cd29fba9c653998ebfef9e97be668957839960b9b3dc1afcb45f1ddb64 + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.5 + resolution: "ssri@npm:10.0.5" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10/453f9a1c241c13f5dfceca2ab7b4687bcff354c3ccbc932f35452687b9ef0ccf8983fd13b8a3baa5844c1a4882d6e3ddff48b0e7fd21d743809ef33b80616d79 + languageName: node + linkType: hard + +"ssri@npm:^8.0.0, ssri@npm:^8.0.1": + version: 8.0.1 + resolution: "ssri@npm:8.0.1" + dependencies: + minipass: "npm:^3.1.1" + checksum: 10/fde247b7107674d9a424a20f9c1a6e3ad88a139c2636b9d9ffa7df59e85e11a894cdae48fadd0ad6be41eb0d5b847fe094736513d333615c7eebc3d111abe0d2 + languageName: node + linkType: hard + +"stack-trace@npm:0.0.x": + version: 0.0.10 + resolution: "stack-trace@npm:0.0.10" + checksum: 10/7bd633f0e9ac46e81a0b0fe6538482c1d77031959cf94478228731709db4672fbbed59176f5b9a9fd89fec656b5dae03d084ef2d1b0c4c2f5683e05f2dbb1405 + languageName: node + linkType: hard + +"stack-utils@npm:^2.0.3": + version: 2.0.6 + resolution: "stack-utils@npm:2.0.6" + dependencies: + escape-string-regexp: "npm:^2.0.0" + checksum: 10/cdc988acbc99075b4b036ac6014e5f1e9afa7e564482b687da6384eee6a1909d7eaffde85b0a17ffbe186c5247faf6c2b7544e802109f63b72c7be69b13151bb + languageName: node + linkType: hard + +"stream-buffers@npm:^3.0.2": + version: 3.0.2 + resolution: "stream-buffers@npm:3.0.2" + checksum: 10/66e55fb770929527f5cf7798f0e4c3b48e04970bf242b3d200140d9e3c0425ba14da4203d3b877be2f8a981b8f3027a5f5d2ad56f8c9f51cb70b3cbb6ba7c5b3 + languageName: node + linkType: hard + +"stream-events@npm:^1.0.5": + version: 1.0.5 + resolution: "stream-events@npm:1.0.5" + dependencies: + stubs: "npm:^3.0.0" + checksum: 10/969ce82e34bfbef5734629cc06f9d7f3705a9ceb8fcd6a526332f9159f1f8bbfdb1a453f3ced0b728083454f7706adbbe8428bceb788a0287ca48ba2642dc3fc + languageName: node + linkType: hard + +"stream-promise@npm:^3.2.0": + version: 3.2.0 + resolution: "stream-promise@npm:3.2.0" + dependencies: + 2-thenable: "npm:^1.0.0" + es5-ext: "npm:^0.10.49" + is-stream: "npm:^1.1.0" + checksum: 10/a8693a8db38537e6e34a1b75aa5c597ce8f281c16af6fe47c8f4314cb0b5bf6b28d9428cfc91ca3a93030e01f5b660b99658720ad9404735cd5eb9d49851dfc0 + languageName: node + linkType: hard + +"stream-shift@npm:^1.0.0": + version: 1.0.1 + resolution: "stream-shift@npm:1.0.1" + checksum: 10/59b82b44b29ec3699b5519a49b3cedcc6db58c72fb40c04e005525dfdcab1c75c4e0c180b923c380f204bed78211b9bad8faecc7b93dece4d004c3f6ec75737b + languageName: node + linkType: hard + +"string-length@npm:^4.0.1": + version: 4.0.2 + resolution: "string-length@npm:4.0.2" + dependencies: + char-regex: "npm:^1.0.2" + strip-ansi: "npm:^6.0.0" + checksum: 10/ce85533ef5113fcb7e522bcf9e62cb33871aa99b3729cec5595f4447f660b0cefd542ca6df4150c97a677d58b0cb727a3fe09ac1de94071d05526c73579bf505 + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 10/e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: 10/7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 + languageName: node + linkType: hard + +"string.prototype.padend@npm:^3.0.0": + version: 3.1.5 + resolution: "string.prototype.padend@npm:3.1.5" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 10/03ea16c8c3bb25cb014affef2c238baa894b8a6060a5576c3980fe7e0e79e13af3b449f55eadd9e950669aa562ce9a7de8531cbd49b489f50f50e64f7167f8fd + languageName: node + linkType: hard + +"string.prototype.trim@npm:^1.2.8": + version: 1.2.8 + resolution: "string.prototype.trim@npm:1.2.8" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 10/9301f6cb2b6c44f069adde1b50f4048915985170a20a1d64cf7cb2dc53c5cd6b9525b92431f1257f894f94892d6c4ae19b5aa7f577c3589e7e51772dffc9d5a4 + languageName: node + linkType: hard + +"string.prototype.trimend@npm:^1.0.7": + version: 1.0.7 + resolution: "string.prototype.trimend@npm:1.0.7" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 10/3f0d3397ab9bd95cd98ae2fe0943bd3e7b63d333c2ab88f1875cf2e7c958c75dc3355f6fe19ee7c8fca28de6f39f2475e955e103821feb41299a2764a7463ffa + languageName: node + linkType: hard + +"string.prototype.trimstart@npm:^1.0.7": + version: 1.0.7 + resolution: "string.prototype.trimstart@npm:1.0.7" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 10/6e594d3a61b127d243b8be1312e9f78683abe452cfe0bcafa3e0dc62ad6f030ccfb64d87ed3086fb7cb540fda62442c164d237cc5cc4d53c6e3eb659c29a0aeb + languageName: node + linkType: hard + +"string_decoder@npm:^1.1.1": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 10/54d23f4a6acae0e93f999a585e673be9e561b65cd4cca37714af1e893ab8cd8dfa52a9e4f58f48f87b4a44918d3a9254326cb80ed194bf2e4c226e2b21767e56 + languageName: node + linkType: hard + +"string_decoder@npm:~1.1.1": + version: 1.1.1 + resolution: "string_decoder@npm:1.1.1" + dependencies: + safe-buffer: "npm:~5.1.0" + checksum: 10/7c41c17ed4dea105231f6df208002ebddd732e8e9e2d619d133cecd8e0087ddfd9587d2feb3c8caf3213cbd841ada6d057f5142cae68a4e62d3540778d9819b4 + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 10/ae3b5436d34fadeb6096367626ce987057713c566e1e7768818797e00ac5d62023d0f198c4e681eae9e20701721980b26a64a8f5b91238869592a9c6800719a2 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10/475f53e9c44375d6e72807284024ac5d668ee1d06010740dec0b9744f2ddf47de8d7151f80e5f6190fc8f384e802fdf9504b76a7e9020c9faee7103623338be2 + languageName: node + linkType: hard + +"strip-bom@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-bom@npm:3.0.0" + checksum: 10/8d50ff27b7ebe5ecc78f1fe1e00fcdff7af014e73cf724b46fb81ef889eeb1015fc5184b64e81a2efe002180f3ba431bdd77e300da5c6685d702780fbf0c8d5b + languageName: node + linkType: hard + +"strip-bom@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-bom@npm:4.0.0" + checksum: 10/9dbcfbaf503c57c06af15fe2c8176fb1bf3af5ff65003851a102749f875a6dbe0ab3b30115eccf6e805e9d756830d3e40ec508b62b3f1ddf3761a20ebe29d3f3 + languageName: node + linkType: hard + +"strip-dirs@npm:^2.0.0": + version: 2.1.0 + resolution: "strip-dirs@npm:2.1.0" + dependencies: + is-natural-number: "npm:^4.0.1" + checksum: 10/7284fc61cf667e403c54ea515c421094ae641a382a8c8b6019f06658e828556c8e4bb439d5797f7d42247a5342eb6feef200c88ad0582e69b3261e1ec0dbc3a6 + languageName: node + linkType: hard + +"strip-final-newline@npm:^2.0.0": + version: 2.0.0 + resolution: "strip-final-newline@npm:2.0.0" + checksum: 10/69412b5e25731e1938184b5d489c32e340605bb611d6140344abc3421b7f3c6f9984b21dff296dfcf056681b82caa3bb4cc996a965ce37bcfad663e92eae9c64 + languageName: node + linkType: hard + +"strip-final-newline@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-final-newline@npm:3.0.0" + checksum: 10/23ee263adfa2070cd0f23d1ac14e2ed2f000c9b44229aec9c799f1367ec001478469560abefd00c5c99ee6f0b31c137d53ec6029c53e9f32a93804e18c201050 + languageName: node + linkType: hard + +"strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": + version: 3.1.1 + resolution: "strip-json-comments@npm:3.1.1" + checksum: 10/492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 + languageName: node + linkType: hard + +"strip-outer@npm:^1.0.1": + version: 1.0.1 + resolution: "strip-outer@npm:1.0.1" + dependencies: + escape-string-regexp: "npm:^1.0.2" + checksum: 10/f8d65d33ca2b49aabc66bb41d689dda7b8b9959d320e3a40a2ef4d7079ff2f67ffb72db43f179f48dbf9495c2e33742863feab7a584d180fa62505439162c191 + languageName: node + linkType: hard + +"strnum@npm:^1.0.5": + version: 1.0.5 + resolution: "strnum@npm:1.0.5" + checksum: 10/d3117975db8372d4d7b2c07601ed2f65bf21cc48d741f37a8617b76370d228f2ec26336e53791ebc3638264d23ca54e6c241f57f8c69bd4941c63c79440525ca + languageName: node + linkType: hard + +"strtok3@npm:^6.2.4": + version: 6.3.0 + resolution: "strtok3@npm:6.3.0" + dependencies: + "@tokenizer/token": "npm:^0.3.0" + peek-readable: "npm:^4.1.0" + checksum: 10/98fba564d3830202aa3a6bcd5ccaf2cbd849bd87ae79ece91d337e1913916705a8e633c9577138d030a984f8ec987dea51807e01252f995cf5e183fdea35eb2b + languageName: node + linkType: hard + +"stubs@npm:^3.0.0": + version: 3.0.0 + resolution: "stubs@npm:3.0.0" + checksum: 10/dec7b82186e3743317616235c59bfb53284acc312cb9f4c3e97e2205c67a5c158b0ca89db5927e52351582e90a2672822eeaec9db396e23e56893d2a8676e024 + languageName: node + linkType: hard + +"superagent@npm:^7.1.6": + version: 7.1.6 + resolution: "superagent@npm:7.1.6" + dependencies: + component-emitter: "npm:^1.3.0" + cookiejar: "npm:^2.1.3" + debug: "npm:^4.3.4" + fast-safe-stringify: "npm:^2.1.1" + form-data: "npm:^4.0.0" + formidable: "npm:^2.0.1" + methods: "npm:^1.1.2" + mime: "npm:2.6.0" + qs: "npm:^6.10.3" + readable-stream: "npm:^3.6.0" + semver: "npm:^7.3.7" + checksum: 10/a160ccadbea31dd804501fc6b6632265f1d6ef57feca13075e6c828d6d5494e9021545673d6f4dff7fb6c8a290de9ceae8b02881a9ebfb1aed59348b4315590d + languageName: node + linkType: hard + +"supports-color@npm:^5.3.0": + version: 5.5.0 + resolution: "supports-color@npm:5.5.0" + dependencies: + has-flag: "npm:^3.0.0" + checksum: 10/5f505c6fa3c6e05873b43af096ddeb22159831597649881aeb8572d6fe3b81e798cc10840d0c9735e0026b250368851b7f77b65e84f4e4daa820a4f69947f55b + languageName: node + linkType: hard + +"supports-color@npm:^6.1.0": + version: 6.1.0 + resolution: "supports-color@npm:6.1.0" + dependencies: + has-flag: "npm:^3.0.0" + checksum: 10/78a5c43b9e478966ed41ed923a942dfd6209bf3bcc826a01435cfec98d5a17ca5d866effd2b6be438c16cd73b99f4a4397fcbb282e6f653e39046e1335334189 + languageName: node + linkType: hard + +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10/c8bb7afd564e3b26b50ca6ee47572c217526a1389fe018d00345856d4a9b08ffbd61fadaf283a87368d94c3dcdb8f5ffe2650a5a65863e21ad2730ca0f05210a + languageName: node + linkType: hard + +"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10/157b534df88e39c5518c5e78c35580c1eca848d7dbaf31bbe06cdfc048e22c7ff1a9d046ae17b25691128f631a51d9ec373c1b740c12ae4f0de6e292037e4282 + languageName: node + linkType: hard + +"supports-preserve-symlinks-flag@npm:^1.0.0": + version: 1.0.0 + resolution: "supports-preserve-symlinks-flag@npm:1.0.0" + checksum: 10/a9dc19ae2220c952bd2231d08ddeecb1b0328b61e72071ff4000c8384e145cc07c1c0bdb3b5a1cb06e186a7b2790f1dee793418b332f6ddf320de25d9125be7e + languageName: node + linkType: hard + +"sync-daemon@workspace:packages/daemon": + version: 0.0.0-use.local + resolution: "sync-daemon@workspace:packages/daemon" + dependencies: + "@aws-sdk/client-lambda": "npm:^3.474.0" + "@aws-sdk/client-sqs": "npm:^3.474.0" + "@hathor/wallet-lib": "npm:^0.39.0" + "@types/jest": "npm:^29.5.4" + "@types/lodash": "npm:^4.14.199" + "@types/mysql": "npm:^2.15.21" + "@types/node": "npm:^17.0.45" + "@types/ws": "npm:^8.5.5" + "@typescript-eslint/eslint-plugin": "npm:^6.7.3" + "@typescript-eslint/parser": "npm:^6.7.3" + assert: "npm:^2.1.0" + aws-sdk: "npm:^2.1454.0" + axios: "npm:^1.6.2" + dotenv: "npm:^8.2.0" + eslint-config-airbnb-base: "npm:^15.0.0" + eslint-plugin-jest: "npm:^27.4.0" + jest: "npm:^29.6.4" + lodash: "npm:^4.17.21" + mysql2: "npm:^3.5.2" + sequelize: "npm:^6.33.0" + sequelize-cli: "npm:^6.6.1" + ts-jest: "npm:^29.1.1" + tslib: "npm:^2.1.0" + typescript: "npm:^4.9.5" + websocket: "npm:^1.0.33" + winston: "npm:^3.3.3" + ws: "npm:^8.13.0" + xstate: "npm:^4.38.2" + languageName: unknown + linkType: soft + +"tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": + version: 2.2.1 + resolution: "tapable@npm:2.2.1" + checksum: 10/1769336dd21481ae6347611ca5fca47add0962fd8e80466515032125eca0084a4f0ede11e65341b9c0018ef4e1cf1ad820adbb0fba7cc99865c6005734000b0a + languageName: node + linkType: hard + +"tar-stream@npm:^1.5.2": + version: 1.6.2 + resolution: "tar-stream@npm:1.6.2" + dependencies: + bl: "npm:^1.0.0" + buffer-alloc: "npm:^1.2.0" + end-of-stream: "npm:^1.0.0" + fs-constants: "npm:^1.0.0" + readable-stream: "npm:^2.3.0" + to-buffer: "npm:^1.1.1" + xtend: "npm:^4.0.0" + checksum: 10/ac9b850bd40e6d4b251abcf92613bafd9fc9e592c220c781ebcdbb0ba76da22a245d9ea3ea638ad7168910e7e1ae5079333866cd679d2f1ffadb99c403f99d7f + languageName: node + linkType: hard + +"tar-stream@npm:^2.2.0": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: "npm:^4.0.3" + end-of-stream: "npm:^1.4.1" + fs-constants: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + checksum: 10/1a52a51d240c118cbcd30f7368ea5e5baef1eac3e6b793fb1a41e6cd7319296c79c0264ccc5859f5294aa80f8f00b9239d519e627b9aade80038de6f966fec6a + languageName: node + linkType: hard + +"tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.15, tar@npm:^6.1.2": + version: 6.2.0 + resolution: "tar@npm:6.2.0" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: 10/2042bbb14830b5cd0d584007db0eb0a7e933e66d1397e72a4293768d2332449bc3e312c266a0887ec20156dea388d8965e53b4fc5097f42d78593549016da089 + languageName: node + linkType: hard + +"tdigest@npm:^0.1.1": + version: 0.1.2 + resolution: "tdigest@npm:0.1.2" + dependencies: + bintrees: "npm:1.0.2" + checksum: 10/45be99fa52dab74b8edafe150e473cdc45aa1352c75ed516a39905f350a08c3175f6555598111042c3677ba042d7e3cae6b5ce4c663fe609bc634f326aabc9d6 + languageName: node + linkType: hard + +"teeny-request@npm:^8.0.0": + version: 8.0.3 + resolution: "teeny-request@npm:8.0.3" + dependencies: + http-proxy-agent: "npm:^5.0.0" + https-proxy-agent: "npm:^5.0.0" + node-fetch: "npm:^2.6.1" + stream-events: "npm:^1.0.5" + uuid: "npm:^9.0.0" + checksum: 10/17c45c628e6fcfc702b4b3aad5fdf16907190a2b9b6cc760829fe56682720a521e267636888851edf0d4714d8d4b7e6378e54af6e979b4ce6075a230b9ffeb2d + languageName: node + linkType: hard + +"terser-webpack-plugin@npm:^5.3.7": + version: 5.3.9 + resolution: "terser-webpack-plugin@npm:5.3.9" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.17" + jest-worker: "npm:^27.4.5" + schema-utils: "npm:^3.1.1" + serialize-javascript: "npm:^6.0.1" + terser: "npm:^5.16.8" + peerDependencies: + webpack: ^5.1.0 + peerDependenciesMeta: + "@swc/core": + optional: true + esbuild: + optional: true + uglify-js: + optional: true + checksum: 10/339737a407e034b7a9d4a66e31d84d81c10433e41b8eae2ca776f0e47c2048879be482a9aa08e8c27565a2a949bc68f6e07f451bf4d9aa347dd61b3d000f5353 + languageName: node + linkType: hard + +"terser@npm:^5.16.8": + version: 5.21.0 + resolution: "terser@npm:5.21.0" + dependencies: + "@jridgewell/source-map": "npm:^0.3.3" + acorn: "npm:^8.8.2" + commander: "npm:^2.20.0" + source-map-support: "npm:~0.5.20" + bin: + terser: bin/terser + checksum: 10/4660eae8ecef177bfc66f64f3bab008f1604973a76ea68aaf5d64217e2b928f8f53e4097a21cbe90447d911438c9d9c954ac450fa932ef5451dd8db27df9e9aa + languageName: node + linkType: hard + +"test-exclude@npm:^6.0.0": + version: 6.0.0 + resolution: "test-exclude@npm:6.0.0" + dependencies: + "@istanbuljs/schema": "npm:^0.1.2" + glob: "npm:^7.1.4" + minimatch: "npm:^3.0.4" + checksum: 10/8fccb2cb6c8fcb6bb4115394feb833f8b6cf4b9503ec2485c2c90febf435cac62abe882a0c5c51a37b9bbe70640cdd05acf5f45e486ac4583389f4b0855f69e5 + languageName: node + linkType: hard + +"text-decoding@npm:^1.0.0": + version: 1.0.0 + resolution: "text-decoding@npm:1.0.0" + checksum: 10/73c604f285d86d653815a5c32b86ffef9ffe27074bc3e5da3718ac46bba7c04775bb3da7b13c60e3a6525d1acce7cc3442b39e25a3a43f211c866ba9ba414207 + languageName: node + linkType: hard + +"text-hex@npm:1.0.x": + version: 1.0.0 + resolution: "text-hex@npm:1.0.0" + checksum: 10/1138f68adc97bf4381a302a24e2352f04992b7b1316c5003767e9b0d3367ffd0dc73d65001ea02b07cd0ecc2a9d186de0cf02f3c2d880b8a522d4ccb9342244a + languageName: node + linkType: hard + +"text-table@npm:^0.2.0": + version: 0.2.0 + resolution: "text-table@npm:0.2.0" + checksum: 10/4383b5baaeffa9bb4cda2ac33a4aa2e6d1f8aaf811848bf73513a9b88fd76372dc461f6fd6d2e9cb5100f48b473be32c6f95bd983509b7d92bb4d92c10747452 + languageName: node + linkType: hard + +"throat@npm:^5.0.0": + version: 5.0.0 + resolution: "throat@npm:5.0.0" + checksum: 10/00f7197977d433d1c960edfaa6465c1217652999170ef3ecd8dbefa6add6e2304b321480523ae87354df285474ba2c5feff03842e9f398b4bcdd95cfa18cff9c + languageName: node + linkType: hard + +"through@npm:^2.3.6, through@npm:^2.3.8": + version: 2.3.8 + resolution: "through@npm:2.3.8" + checksum: 10/5da78346f70139a7d213b65a0106f3c398d6bc5301f9248b5275f420abc2c4b1e77c2abc72d218dedc28c41efb2e7c312cb76a7730d04f9c2d37d247da3f4198 + languageName: node + linkType: hard + +"timers-ext@npm:^0.1.7": + version: 0.1.7 + resolution: "timers-ext@npm:0.1.7" + dependencies: + es5-ext: "npm:~0.10.46" + next-tick: "npm:1" + checksum: 10/a8fffe2841ed6c3b16b2e72522ee46537c6a758294da45486c7e8ca52ff065931dd023c9f9946b87a13f48ae3dafe12678ab1f9d1ef24b6aea465762e0ffdcae + languageName: node + linkType: hard + +"tiny-secp256k1@npm:^2.2.1": + version: 2.2.3 + resolution: "tiny-secp256k1@npm:2.2.3" + dependencies: + uint8array-tools: "npm:0.0.7" + checksum: 10/9975134c5c86587bb0e9886dd2e66a7a9b79931cb2c3e32b24bcfc2096216781828bb7c5482c0fa18a632ce1b907f6cc86bf12238704ec9cd43d1e81d8af502e + languageName: node + linkType: hard + +"tmp@npm:^0.0.33": + version: 0.0.33 + resolution: "tmp@npm:0.0.33" + dependencies: + os-tmpdir: "npm:~1.0.2" + checksum: 10/09c0abfd165cff29b32be42bc35e80b8c64727d97dedde6550022e88fa9fd39a084660415ed8e3ebaa2aca1ee142f86df8b31d4196d4f81c774a3a20fd4b6abf + languageName: node + linkType: hard + +"tmp@npm:^0.2.1": + version: 0.2.1 + resolution: "tmp@npm:0.2.1" + dependencies: + rimraf: "npm:^3.0.0" + checksum: 10/445148d72df3ce99356bc89a7857a0c5c3b32958697a14e50952c6f7cf0a8016e746ababe9a74c1aa52f04c526661992f14659eba34d3c6701d49ba2f3cf781b + languageName: node + linkType: hard + +"tmpl@npm:1.0.5": + version: 1.0.5 + resolution: "tmpl@npm:1.0.5" + checksum: 10/cd922d9b853c00fe414c5a774817be65b058d54a2d01ebb415840960406c669a0fc632f66df885e24cb022ec812739199ccbdb8d1164c3e513f85bfca5ab2873 + languageName: node + linkType: hard + +"to-buffer@npm:^1.1.1": + version: 1.1.1 + resolution: "to-buffer@npm:1.1.1" + checksum: 10/8ade59fe04239b281496b6067bc83ad0371a3657552276cbd09ffffaeb3ad0018a28306d61b854b83280eabe1829cbc53001ccd761e834c6062cbcc7fee2766a + languageName: node + linkType: hard + +"to-fast-properties@npm:^2.0.0": + version: 2.0.0 + resolution: "to-fast-properties@npm:2.0.0" + checksum: 10/be2de62fe58ead94e3e592680052683b1ec986c72d589e7b21e5697f8744cdbf48c266fa72f6c15932894c10187b5f54573a3bcf7da0bfd964d5caf23d436168 + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: "npm:^7.0.0" + checksum: 10/10dda13571e1f5ad37546827e9b6d4252d2e0bc176c24a101252153ef435d83696e2557fe128c4678e4e78f5f01e83711c703eef9814eb12dab028580d45980a + languageName: node + linkType: hard + +"token-types@npm:^4.1.1": + version: 4.2.1 + resolution: "token-types@npm:4.2.1" + dependencies: + "@tokenizer/token": "npm:^0.3.0" + ieee754: "npm:^1.2.1" + checksum: 10/2995257d246387e773758c3c92a3cc99d0c0bf13cbafe0de5d712e4c35ed298da6704e21545cb123fa1f1b42ad62936c35bbd0611018b735e78c30b8b22b42d9 + languageName: node + linkType: hard + +"toposort-class@npm:^1.0.1": + version: 1.0.1 + resolution: "toposort-class@npm:1.0.1" + checksum: 10/166cb89ecb544383691e69eb9305b637dbc239f30894af0984cb4ee40f266c8c0fcadadd4ee6879c5466d1fd9153b22cfa3b19b649337d0da927b866352f7d75 + languageName: node + linkType: hard + +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 10/8f1f5aa6cb232f9e1bdc86f485f916b7aa38caee8a778b378ffec0b70d9307873f253f5cbadbe2955ece2ac5c83d0dc14a77513166ccd0a0c7fe197e21396695 + languageName: node + linkType: hard + +"traverse@npm:^0.6.6": + version: 0.6.7 + resolution: "traverse@npm:0.6.7" + checksum: 10/b06ea2d1db755ae21d2f5bade6e5ddfc6daf4b571fefe0de343c4fbbb022836a1e9c293b334d04b5c73cc689e9dbbdde33bb41a57508a8b82c73683f76de7a01 + languageName: node + linkType: hard + +"trim-repeated@npm:^1.0.0": + version: 1.0.0 + resolution: "trim-repeated@npm:1.0.0" + dependencies: + escape-string-regexp: "npm:^1.0.2" + checksum: 10/e25c235305b82c43f1d64a67a71226c406b00281755e4c2c4f3b1d0b09c687a535dd3c4483327f949f28bb89dc400a0bc5e5b749054f4b99f49ebfe48ba36496 + languageName: node + linkType: hard + +"triple-beam@npm:^1.3.0": + version: 1.4.1 + resolution: "triple-beam@npm:1.4.1" + checksum: 10/2e881a3e8e076b6f2b85b9ec9dd4a900d3f5016e6d21183ed98e78f9abcc0149e7d54d79a3f432b23afde46b0885bdcdcbff789f39bc75de796316961ec07f61 + languageName: node + linkType: hard + +"ts-api-utils@npm:^1.0.1": + version: 1.0.3 + resolution: "ts-api-utils@npm:1.0.3" + peerDependencies: + typescript: ">=4.2.0" + checksum: 10/1350a5110eb1e534e9a6178f4081fb8a4fcc439749e19f4ad699baec9090fcb90fe532d5e191d91a062dc6e454a14a8d7eb2ad202f57135a30c4a44a3024f039 + languageName: node + linkType: hard + +"ts-jest@npm:^29.1.1": + version: 29.1.1 + resolution: "ts-jest@npm:29.1.1" + dependencies: + bs-logger: "npm:0.x" + fast-json-stable-stringify: "npm:2.x" + jest-util: "npm:^29.0.0" + json5: "npm:^2.2.3" + lodash.memoize: "npm:4.x" + make-error: "npm:1.x" + semver: "npm:^7.5.3" + yargs-parser: "npm:^21.0.1" + peerDependencies: + "@babel/core": ">=7.0.0-beta.0 <8" + "@jest/types": ^29.0.0 + babel-jest: ^29.0.0 + jest: ^29.0.0 + typescript: ">=4.3 <6" + peerDependenciesMeta: + "@babel/core": + optional: true + "@jest/types": + optional: true + babel-jest: + optional: true + esbuild: + optional: true + bin: + ts-jest: cli.js + checksum: 10/30e8259baba95dd786e64f7c18b864e904598f3ba07911be4d9bd29ca9c3c0024bad4ccf8ec0abd2a2fa14b06622cbbadff1b3be822189c657196442d33ee6ca + languageName: node + linkType: hard + +"ts-loader@npm:^9.4.4": + version: 9.4.4 + resolution: "ts-loader@npm:9.4.4" + dependencies: + chalk: "npm:^4.1.0" + enhanced-resolve: "npm:^5.0.0" + micromatch: "npm:^4.0.0" + semver: "npm:^7.3.4" + peerDependencies: + typescript: "*" + webpack: ^5.0.0 + checksum: 10/52302f3540962d779fc346281d8d4f7310f73e129debc4fb55c1fb3f097519009b32f7e2806299904e98961c785784fccb70ceca9076c4bae33f064ad11dd982 + languageName: node + linkType: hard + +"ts-node@npm:>= 8.3.0": + version: 10.9.1 + resolution: "ts-node@npm:10.9.1" + dependencies: + "@cspotcode/source-map-support": "npm:^0.8.0" + "@tsconfig/node10": "npm:^1.0.7" + "@tsconfig/node12": "npm:^1.0.7" + "@tsconfig/node14": "npm:^1.0.0" + "@tsconfig/node16": "npm:^1.0.2" + acorn: "npm:^8.4.1" + acorn-walk: "npm:^8.1.1" + arg: "npm:^4.1.0" + create-require: "npm:^1.1.0" + diff: "npm:^4.0.1" + make-error: "npm:^1.1.1" + v8-compile-cache-lib: "npm:^3.0.1" + yn: "npm:3.1.1" + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-esm: dist/bin-esm.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: 10/bee56d4dc96ccbafc99dfab7b73fbabc62abab2562af53cdea91c874a301b9d11e42bc33c0a032a6ed6d813dbdc9295ec73dde7b73ea4ebde02b0e22006f7e04 + languageName: node + linkType: hard + +"tsconfig-paths@npm:^3.14.2": + version: 3.14.2 + resolution: "tsconfig-paths@npm:3.14.2" + dependencies: + "@types/json5": "npm:^0.0.29" + json5: "npm:^1.0.2" + minimist: "npm:^1.2.6" + strip-bom: "npm:^3.0.0" + checksum: 10/17f23e98612a60cf23b80dc1d3b7b840879e41fcf603868fc3618a30f061ac7b463ef98cad8c28b68733b9bfe0cc40ffa2bcf29e94cf0d26e4f6addf7ac8527d + languageName: node + linkType: hard + +"tslib@npm:^1.11.1, tslib@npm:^1.8.1": + version: 1.14.1 + resolution: "tslib@npm:1.14.1" + checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb + languageName: node + linkType: hard + +"tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.5.0": + version: 2.6.2 + resolution: "tslib@npm:2.6.2" + checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca + languageName: node + linkType: hard + +"tsutils@npm:^3.17.1, tsutils@npm:^3.21.0": + version: 3.21.0 + resolution: "tsutils@npm:3.21.0" + dependencies: + tslib: "npm:^1.8.1" + peerDependencies: + typescript: ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + checksum: 10/ea036bec1dd024e309939ffd49fda7a351c0e87a1b8eb049570dd119d447250e2c56e0e6c00554e8205760e7417793fdebff752a46e573fbe07d4f375502a5b2 + languageName: node + linkType: hard + +"type-check@npm:^0.4.0, type-check@npm:~0.4.0": + version: 0.4.0 + resolution: "type-check@npm:0.4.0" + dependencies: + prelude-ls: "npm:^1.2.1" + checksum: 10/14687776479d048e3c1dbfe58a2409e00367810d6960c0f619b33793271ff2a27f81b52461f14a162f1f89a9b1d8da1b237fc7c99b0e1fdcec28ec63a86b1fec + languageName: node + linkType: hard + +"type-check@npm:~0.3.2": + version: 0.3.2 + resolution: "type-check@npm:0.3.2" + dependencies: + prelude-ls: "npm:~1.1.2" + checksum: 10/11dec0b50d7c3fd2e630b4b074ba36918ed2b1efbc87dfbd40ba9429d49c58d12dad5c415ece69fcf358fa083f33466fc370f23ab91aa63295c45d38b3a60dda + languageName: node + linkType: hard + +"type-detect@npm:4.0.8": + version: 4.0.8 + resolution: "type-detect@npm:4.0.8" + checksum: 10/5179e3b8ebc51fce1b13efb75fdea4595484433f9683bbc2dca6d99789dba4e602ab7922d2656f2ce8383987467f7770131d4a7f06a26287db0615d2f4c4ce7d + languageName: node + linkType: hard + +"type-fest@npm:^0.20.2": + version: 0.20.2 + resolution: "type-fest@npm:0.20.2" + checksum: 10/8907e16284b2d6cfa4f4817e93520121941baba36b39219ea36acfe64c86b9dbc10c9941af450bd60832c8f43464974d51c0957f9858bc66b952b66b6914cbb9 + languageName: node + linkType: hard + +"type-fest@npm:^0.21.3": + version: 0.21.3 + resolution: "type-fest@npm:0.21.3" + checksum: 10/f4254070d9c3d83a6e573bcb95173008d73474ceadbbf620dd32d273940ca18734dff39c2b2480282df9afe5d1675ebed5499a00d791758748ea81f61a38961f + languageName: node + linkType: hard + +"type-fest@npm:^2.13.0": + version: 2.19.0 + resolution: "type-fest@npm:2.19.0" + checksum: 10/7bf9e8fdf34f92c8bb364c0af14ca875fac7e0183f2985498b77be129dc1b3b1ad0a6b3281580f19e48c6105c037fb966ad9934520c69c6434d17fd0af4eed78 + languageName: node + linkType: hard + +"type-fest@npm:^3.0.0": + version: 3.13.1 + resolution: "type-fest@npm:3.13.1" + checksum: 10/9a8a2359ada34c9b3affcaf3a8f73ee14c52779e89950db337ce66fb74c3399776c697c99f2532e9b16e10e61cfdba3b1c19daffb93b338b742f0acd0117ce12 + languageName: node + linkType: hard + +"type@npm:^1.0.1": + version: 1.2.0 + resolution: "type@npm:1.2.0" + checksum: 10/b4d4b27d1926028be45fc5baaca205896e2a1fe9e5d24dc892046256efbe88de6acd0149e7353cd24dad596e1483e48ec60b0912aa47ca078d68cdd198b09885 + languageName: node + linkType: hard + +"type@npm:^2.1.0, type@npm:^2.5.0, type@npm:^2.6.0, type@npm:^2.7.2": + version: 2.7.2 + resolution: "type@npm:2.7.2" + checksum: 10/602f1b369fba60687fa4d0af6fcfb814075bcaf9ed3a87637fb384d9ff849e2ad15bc244a431f341374562e51a76c159527ffdb1f1f24b0f1f988f35a301c41d + languageName: node + linkType: hard + +"typed-array-buffer@npm:^1.0.0": + version: 1.0.0 + resolution: "typed-array-buffer@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.2.1" + is-typed-array: "npm:^1.1.10" + checksum: 10/3e0281c79b2a40cd97fe715db803884301993f4e8c18e8d79d75fd18f796e8cd203310fec8c7fdb5e6c09bedf0af4f6ab8b75eb3d3a85da69328f28a80456bd3 + languageName: node + linkType: hard + +"typed-array-byte-length@npm:^1.0.0": + version: 1.0.0 + resolution: "typed-array-byte-length@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.2" + for-each: "npm:^0.3.3" + has-proto: "npm:^1.0.1" + is-typed-array: "npm:^1.1.10" + checksum: 10/6f376bf5d988f00f98ccee41fd551cafc389095a2a307c18fab30f29da7d1464fc3697139cf254cda98b4128bbcb114f4b557bbabdc6d9c2e5039c515b31decf + languageName: node + linkType: hard + +"typed-array-byte-offset@npm:^1.0.0": + version: 1.0.0 + resolution: "typed-array-byte-offset@npm:1.0.0" + dependencies: + available-typed-arrays: "npm:^1.0.5" + call-bind: "npm:^1.0.2" + for-each: "npm:^0.3.3" + has-proto: "npm:^1.0.1" + is-typed-array: "npm:^1.1.10" + checksum: 10/2d81747faae31ca79f6c597dc18e15ae3d5b7e97f7aaebce3b31f46feeb2a6c1d6c92b9a634d901c83731ffb7ec0b74d05c6ff56076f5ae39db0cd19b16a3f92 + languageName: node + linkType: hard + +"typed-array-length@npm:^1.0.4": + version: 1.0.4 + resolution: "typed-array-length@npm:1.0.4" + dependencies: + call-bind: "npm:^1.0.2" + for-each: "npm:^0.3.3" + is-typed-array: "npm:^1.1.9" + checksum: 10/0444658acc110b233176cb0b7689dcb828b0cfa099ab1d377da430e8553b6fdcdce882360b7ffe9ae085b6330e1d39383d7b2c61574d6cd8eef651d3e4a87822 + languageName: node + linkType: hard + +"typedarray-to-buffer@npm:^3.1.5": + version: 3.1.5 + resolution: "typedarray-to-buffer@npm:3.1.5" + dependencies: + is-typedarray: "npm:^1.0.0" + checksum: 10/7c850c3433fbdf4d04f04edfc751743b8f577828b8e1eb93b95a3bce782d156e267d83e20fb32b3b47813e69a69ab5e9b5342653332f7d21c7d1210661a7a72c + languageName: node + linkType: hard + +"typeforce@npm:^1.11.3, typeforce@npm:^1.11.5": + version: 1.18.0 + resolution: "typeforce@npm:1.18.0" + checksum: 10/dbf98c75b1d57e56e33c1e1271d5505e67981f4e6a2e2e6e8e31160b58777fea1726160810b6c606517db16476805b7dce315926ba2d4859b9a56cab05b7a41f + languageName: node + linkType: hard + +"typescript-eslint@npm:0.0.1-alpha.0": + version: 0.0.1-alpha.0 + resolution: "typescript-eslint@npm:0.0.1-alpha.0" + checksum: 10/2896a13f2c77f5193736016abfdb78139c4f781d10c1fa9d4a1b4dbaa6f8696db9e0060bb148d5e9c74eb76e797530b048dcf135b7d4ff303609b2f47a62b206 + languageName: node + linkType: hard + +"typescript@npm:^4.9.3, typescript@npm:^4.9.5": + version: 4.9.5 + resolution: "typescript@npm:4.9.5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/458f7220ab11e0fc191514cc41be1707645ec9a8c2d609448a448e18c522cef9646f58728f6811185a4c35613dacdf6c98cf8965c88b3541d0288c47291e4300 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^4.9.3#optional!builtin, typescript@patch:typescript@npm%3A^4.9.5#optional!builtin": + version: 4.9.5 + resolution: "typescript@patch:typescript@npm%3A4.9.5#optional!builtin::version=4.9.5&hash=289587" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/5659316360b5cc2d6f5931b346401fa534107b68b60179cf14970e27978f0936c1d5c46f4b5b8175f8cba0430f522b3ce355b4b724c0ea36ce6c0347fab25afd + languageName: node + linkType: hard + +"uc.micro@npm:^1.0.1, uc.micro@npm:^1.0.5": + version: 1.0.6 + resolution: "uc.micro@npm:1.0.6" + checksum: 10/6898bb556319a38e9cf175e3628689347bd26fec15fc6b29fa38e0045af63075ff3fea4cf1fdba9db46c9f0cbf07f2348cd8844889dd31ebd288c29fe0d27e7a + languageName: node + linkType: hard + +"uglify-js@npm:^3.7.7": + version: 3.17.4 + resolution: "uglify-js@npm:3.17.4" + bin: + uglifyjs: bin/uglifyjs + checksum: 10/4c0b800e0ff192079d2c3ce8414fd3b656a570028c7c79af5c29c53d5c532b68bbcae4ad47307f89c2ee124d11826fff7a136b59d5c5bb18422bcdf5568afe1e + languageName: node + linkType: hard + +"uint8array-tools@npm:0.0.7": + version: 0.0.7 + resolution: "uint8array-tools@npm:0.0.7" + checksum: 10/6ffc45c7d2136757d63c6e556eb8345f908948618a9de37c805fec1249d989c265187b3fbef6cffc4ce5129083204829025b3c58800a0f24c8548e243d42ba13 + languageName: node + linkType: hard + +"umzug@npm:^2.3.0": + version: 2.3.0 + resolution: "umzug@npm:2.3.0" + dependencies: + bluebird: "npm:^3.7.2" + checksum: 10/f406fa2d00c34ec590e9916b39f62331c3ba60a64e3f62f0eda9acac49be6e0a2564f440399f1656b122aa71ace54074e8435cd9b1a2446d1117b6ba0bc80ae3 + languageName: node + linkType: hard + +"unbox-primitive@npm:^1.0.2": + version: 1.0.2 + resolution: "unbox-primitive@npm:1.0.2" + dependencies: + call-bind: "npm:^1.0.2" + has-bigints: "npm:^1.0.2" + has-symbols: "npm:^1.0.3" + which-boxed-primitive: "npm:^1.0.2" + checksum: 10/06e1ee41c1095e37281cb71a975cb3350f7cb470a0665d2576f02cc9564f623bd90cfc0183693b8a7fdf2d242963dcc3010b509fa3ac683f540c765c0f3e7e43 + languageName: node + linkType: hard + +"unbzip2-stream@npm:^1.0.9": + version: 1.4.3 + resolution: "unbzip2-stream@npm:1.4.3" + dependencies: + buffer: "npm:^5.2.1" + through: "npm:^2.3.8" + checksum: 10/4ffc0e14f4af97400ed0f37be83b112b25309af21dd08fa55c4513e7cb4367333f63712aec010925dbe491ef6e92db1248e1e306e589f9f6a8da8b3a9c4db90b + languageName: node + linkType: hard + +"underscore@npm:~1.13.2": + version: 1.13.6 + resolution: "underscore@npm:1.13.6" + checksum: 10/58cf5dc42cb0ac99c146ae4064792c0a2cc84f3a3c4ad88f5082e79057dfdff3371d896d1ec20379e9ece2450d94fa78f2ef5bfefc199ba320653e32c009bd66 + languageName: node + linkType: hard + +"uni-global@npm:^1.0.0": + version: 1.0.0 + resolution: "uni-global@npm:1.0.0" + dependencies: + type: "npm:^2.5.0" + checksum: 10/df3e622f33fc62aa646d36fd226dd41a5bc76e68a5676b8c8da9f22d9f1b885b7e3ca794866676b055205c01abe8b9eb8365b5da685dc3fbfc9638a14df4d147 + languageName: node + linkType: hard + +"unique-filename@npm:^1.1.1": + version: 1.1.1 + resolution: "unique-filename@npm:1.1.1" + dependencies: + unique-slug: "npm:^2.0.0" + checksum: 10/9b6969d649a2096755f19f793315465c6427453b66d67c2a1bee8f36ca7e1fc40725be2c028e974dec110d365bd30a4248e89b1044dc1dfe29663b6867d071ef + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: "npm:^4.0.0" + checksum: 10/8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df + languageName: node + linkType: hard + +"unique-slug@npm:^2.0.0": + version: 2.0.2 + resolution: "unique-slug@npm:2.0.2" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10/6cfaf91976acc9c125fd0686c561ee9ca0784bb4b2b408972e6cd30e747b4ff0ca50264c01bcf5e711b463535ea611ffb84199e9f73088cd79ac9ddee8154042 + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10/40912a8963fc02fb8b600cf50197df4a275c602c60de4cac4f75879d3c48558cfac48de08a25cc10df8112161f7180b3bbb4d662aadb711568602f9eddee54f0 + languageName: node + linkType: hard + +"universalify@npm:^2.0.0": + version: 2.0.0 + resolution: "universalify@npm:2.0.0" + checksum: 10/2406a4edf4a8830aa6813278bab1f953a8e40f2f63a37873ffa9a3bc8f9745d06cc8e88f3572cb899b7e509013f7f6fcc3e37e8a6d914167a5381d8440518c44 + languageName: node + linkType: hard + +"unorm@npm:^1.4.1": + version: 1.6.0 + resolution: "unorm@npm:1.6.0" + checksum: 10/af09a4c656830173571a547605a185916eb5ee2a684374282edf318ef54882f9b25d00bfd44591b686a292d8130e083755a3317eca62753d56e18616e98e501b + languageName: node + linkType: hard + +"untildify@npm:^4.0.0": + version: 4.0.0 + resolution: "untildify@npm:4.0.0" + checksum: 10/39ced9c418a74f73f0a56e1ba4634b4d959422dff61f4c72a8e39f60b99380c1b45ed776fbaa0a4101b157e4310d873ad7d114e8534ca02609b4916bb4187fb9 + languageName: node + linkType: hard + +"update-browserslist-db@npm:^1.0.13": + version: 1.0.13 + resolution: "update-browserslist-db@npm:1.0.13" + dependencies: + escalade: "npm:^3.1.1" + picocolors: "npm:^1.0.0" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 10/9074b4ef34d2ed931f27d390aafdd391ee7c45ad83c508e8fed6aaae1eb68f81999a768ed8525c6f88d4001a4fbf1b8c0268f099d0e8e72088ec5945ac796acf + languageName: node + linkType: hard + +"uri-js@npm:^4.2.2": + version: 4.4.1 + resolution: "uri-js@npm:4.4.1" + dependencies: + punycode: "npm:^2.1.0" + checksum: 10/b271ca7e3d46b7160222e3afa3e531505161c9a4e097febae9664e4b59912f4cbe94861361a4175edac3a03fee99d91e44b6a58c17a634bc5a664b19fc76fbcb + languageName: node + linkType: hard + +"url@npm:0.10.3": + version: 0.10.3 + resolution: "url@npm:0.10.3" + dependencies: + punycode: "npm:1.3.2" + querystring: "npm:0.2.0" + checksum: 10/8c04e30d65907a1e01569cead632c74ea3af99d1b9b63dfbb2cf636640fe210f7a1bc16990aac04914dbb63ad2bd50effee3e782e0170d5938a11e8aa38358a5 + languageName: node + linkType: hard + +"utf-8-validate@npm:^5.0.2": + version: 5.0.10 + resolution: "utf-8-validate@npm:5.0.10" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10/b89cbc13b4badad04828349ebb7aa2ab1edcb02b46ab12ce0ba5b2d6886d684ad4e93347819e3c8d36224c8742422d2dca69f5cc16c72ae4d7eeecc0c5cb544b + languageName: node + linkType: hard + +"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 10/474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 + languageName: node + linkType: hard + +"util@npm:^0.12.4, util@npm:^0.12.5": + version: 0.12.5 + resolution: "util@npm:0.12.5" + dependencies: + inherits: "npm:^2.0.3" + is-arguments: "npm:^1.0.4" + is-generator-function: "npm:^1.0.7" + is-typed-array: "npm:^1.1.3" + which-typed-array: "npm:^1.1.2" + checksum: 10/61a10de7753353dd4d744c917f74cdd7d21b8b46379c1e48e1c4fd8e83f8190e6bd9978fc4e5102ab6a10ebda6019d1b36572fa4a325e175ec8b789a121f6147 + languageName: node + linkType: hard + +"uuid@npm:8.0.0": + version: 8.0.0 + resolution: "uuid@npm:8.0.0" + bin: + uuid: dist/bin/uuid + checksum: 10/5086c43bbe11e2337d9bb9a3b3a156311e5f5ba5da2de8152da9e00cfd5fbbf626d36e6a2838dde06e2105ac563bc298470acc0e4800c96fa2d50565c5782f8a + languageName: node + linkType: hard + +"uuid@npm:^8.0.0, uuid@npm:^8.3.0, uuid@npm:^8.3.2": + version: 8.3.2 + resolution: "uuid@npm:8.3.2" + bin: + uuid: dist/bin/uuid + checksum: 10/9a5f7aa1d6f56dd1e8d5f2478f855f25c645e64e26e347a98e98d95781d5ed20062d6cca2eecb58ba7c84bc3910be95c0451ef4161906abaab44f9cb68ffbdd1 + languageName: node + linkType: hard + +"uuid@npm:^9.0.0": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" + bin: + uuid: dist/bin/uuid + checksum: 10/9d0b6adb72b736e36f2b1b53da0d559125ba3e39d913b6072f6f033e0c87835b414f0836b45bcfaf2bdf698f92297fea1c3cc19b0b258bc182c9c43cc0fab9f2 + languageName: node + linkType: hard + +"v8-compile-cache-lib@npm:^3.0.1": + version: 3.0.1 + resolution: "v8-compile-cache-lib@npm:3.0.1" + checksum: 10/88d3423a52b6aaf1836be779cab12f7016d47ad8430dffba6edf766695e6d90ad4adaa3d8eeb512cc05924f3e246c4a4ca51e089dccf4402caa536b5e5be8961 + languageName: node + linkType: hard + +"v8-to-istanbul@npm:^9.0.1": + version: 9.1.2 + resolution: "v8-to-istanbul@npm:9.1.2" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.12" + "@types/istanbul-lib-coverage": "npm:^2.0.1" + convert-source-map: "npm:^2.0.0" + checksum: 10/4ce25637ba10746c9f39c28a161e3ad7fb577bf770549e4fec65602eb0d2ce45f8965d334ff1ad5dcf72cca2356cad0a46117c79a765077a6eeabee3628301cc + languageName: node + linkType: hard + +"validate-npm-package-license@npm:^3.0.1": + version: 3.0.4 + resolution: "validate-npm-package-license@npm:3.0.4" + dependencies: + spdx-correct: "npm:^3.0.0" + spdx-expression-parse: "npm:^3.0.0" + checksum: 10/86242519b2538bb8aeb12330edebb61b4eb37fd35ef65220ab0b03a26c0592c1c8a7300d32da3cde5abd08d18d95e8dabfad684b5116336f6de9e6f207eec224 + languageName: node + linkType: hard + +"validate-npm-package-name@npm:^3.0.0": + version: 3.0.0 + resolution: "validate-npm-package-name@npm:3.0.0" + dependencies: + builtins: "npm:^1.0.3" + checksum: 10/6f89bcc91bb0d46e3c756eec2fd33887eeb76c85d20e5d3e452b69fe3ffbd37062704a4e8422735ea82d69fd963451b4f85501a4dc856f384138411ec42608fa + languageName: node + linkType: hard + +"validator@npm:^13.9.0": + version: 13.11.0 + resolution: "validator@npm:13.11.0" + checksum: 10/4bf094641eb71729c06a42d669840e7189597ba655a8264adabac9bf03f95cd6fde5fbc894b0a13ee861bd4a852f56d2afdc9391aeaeb3fc0f9633a974140e12 + languageName: node + linkType: hard + +"varuint-bitcoin@npm:^1.0.1, varuint-bitcoin@npm:^1.1.2": + version: 1.1.2 + resolution: "varuint-bitcoin@npm:1.1.2" + dependencies: + safe-buffer: "npm:^5.1.1" + checksum: 10/1c900bf08f2408ae33a6094dc5d809bdb6673eaf6039062d88c230155873e51e29c760053611f93ccd024854d04ebd92ed95c744720e94a79ca4e1150fcce071 + languageName: node + linkType: hard + +"velocityjs@npm:^2.0.6": + version: 2.0.6 + resolution: "velocityjs@npm:2.0.6" + dependencies: + debug: "npm:^4.3.3" + bin: + velocity: bin/velocity + checksum: 10/a8c538dad5d27426c71b061ed209c13a8685feb8db2a5e9b14884a7b899a46ce5b62729198c91e554e98ca85c7c08f33bba722568b767c2b5e82cd55aaa576bf + languageName: node + linkType: hard + +"walker@npm:^1.0.8": + version: 1.0.8 + resolution: "walker@npm:1.0.8" + dependencies: + makeerror: "npm:1.0.12" + checksum: 10/ad7a257ea1e662e57ef2e018f97b3c02a7240ad5093c392186ce0bcf1f1a60bbadd520d073b9beb921ed99f64f065efb63dfc8eec689a80e569f93c1c5d5e16c + languageName: node + linkType: hard + +"wallet-service@workspace:packages/wallet-service": + version: 0.0.0-use.local + resolution: "wallet-service@workspace:packages/wallet-service" + dependencies: + "@aws-sdk/client-apigatewaymanagementapi": "npm:^3.465.0" + "@aws-sdk/client-lambda": "npm:^3.465.0" + "@aws-sdk/client-sqs": "npm:^3.465.0" + "@hathor/healthcheck-lib": "npm:^0.1.0" + "@hathor/wallet-lib": "npm:^0.39.0" + "@middy/core": "npm:^2.5.7" + "@middy/http-cors": "npm:^2.5.7" + "@types/aws-lambda": "npm:^8.10.95" + "@types/jest": "npm:^27.0.24" + "@types/node": "npm:^18.0.4" + "@types/redis": "npm:^2.8.28" + "@typescript-eslint/eslint-plugin": "npm:^6.7.4" + "@typescript-eslint/parser": "npm:^3.3.0" + aws-lambda: "npm:^1.0.7" + axios: "npm:^0.21.1" + bip32: "npm:^3.0.1" + bitcoinjs-lib: "npm:^6.0.1" + bitcoinjs-message: "npm:^2.2.0" + bitcore-lib: "npm:8.25.10" + bitcore-mnemonic: "npm:8.25.10" + dotenv: "npm:^10.0.0" + eslint: "npm:^8.50.0" + eslint-config-airbnb-base: "npm:^14.2.1" + eslint-import-resolver-alias: "npm:^1.1.2" + eslint-plugin-import: "npm:^2.23.3" + eslint-plugin-jest: "npm:^23.13.2" + eslint-plugin-module-resolver: "npm:^0.16.0" + firebase-admin: "npm:^11.3.0" + fork-ts-checker-webpack-plugin: "npm:^9.0.0" + jest: "npm:^29.7.0" + joi: "npm:^17.4.0" + jsonwebtoken: "npm:^8.5.1" + lodash: "npm:^4.17.21" + mysql: "npm:^2.18.1" + mysql2: "npm:^2.2.5" + npm-run-all: "npm:^4.1.5" + prom-client: "npm:^13.2.0" + redis: "npm:^3.1.2" + serverless: "npm:^3.35.2" + serverless-api-gateway-throttling: "npm:^1.1.1" + serverless-iam-roles-per-function: "npm:^3.2.0" + serverless-mysql: "npm:^1.5.4" + serverless-offline: "npm:^13.1.2" + serverless-plugin-aws-alerts: "npm:^1.7.5" + serverless-plugin-monorepo: "npm:^0.11.0" + serverless-plugin-warmup: "npm:^8.2.1" + serverless-prune-plugin: "npm:^2.0.2" + serverless-webpack: "npm:^5.13.0" + source-map-support: "npm:^0.5.19" + sqlite3: "npm:^5.0.2" + tiny-secp256k1: "npm:^2.2.1" + ts-jest: "npm:^29.1.1" + ts-loader: "npm:^9.4.4" + typescript: "npm:^4.9.3" + typescript-eslint: "npm:0.0.1-alpha.0" + uuid: "npm:^8.3.0" + webpack: "npm:^5.88.2" + webpack-node-externals: "npm:^3.0.0" + winston: "npm:^3.7.2" + languageName: unknown + linkType: soft + +"watchpack@npm:^2.0.0-beta.10, watchpack@npm:^2.4.0": + version: 2.4.0 + resolution: "watchpack@npm:2.4.0" + dependencies: + glob-to-regexp: "npm:^0.4.1" + graceful-fs: "npm:^4.1.2" + checksum: 10/4280b45bc4b5d45d5579113f2a4af93b67ae1b9607cc3d86ae41cdd53ead10db5d9dc3237f24256d05ef88b28c69a02712f78e434cb7ecc8edaca134a56e8cab + languageName: node + linkType: hard + +"wcwidth@npm:^1.0.1": + version: 1.0.1 + resolution: "wcwidth@npm:1.0.1" + dependencies: + defaults: "npm:^1.0.3" + checksum: 10/182ebac8ca0b96845fae6ef44afd4619df6987fe5cf552fdee8396d3daa1fb9b8ec5c6c69855acb7b3c1231571393bd1f0a4cdc4028d421575348f64bb0a8817 + languageName: node + linkType: hard + +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: 10/b65b9f8d6854572a84a5c69615152b63371395f0c5dcd6729c45789052296df54314db2bc3e977df41705eacb8bc79c247cee139a63fa695192f95816ed528ad + languageName: node + linkType: hard + +"webpack-node-externals@npm:^3.0.0": + version: 3.0.0 + resolution: "webpack-node-externals@npm:3.0.0" + checksum: 10/1a08102f73be2d6e787d16cf677f98c413076f35f379d64a4c83aa83769099b38091a4592953fac5b2eb0c7e3eb1977f6b901ef2cba531d458e32665314b8025 + languageName: node + linkType: hard + +"webpack-sources@npm:^3.2.3": + version: 3.2.3 + resolution: "webpack-sources@npm:3.2.3" + checksum: 10/a661f41795d678b7526ae8a88cd1b3d8ce71a7d19b6503da8149b2e667fc7a12f9b899041c1665d39e38245ed3a59ab68de648ea31040c3829aa695a5a45211d + languageName: node + linkType: hard + +"webpack@npm:^5.88.2": + version: 5.88.2 + resolution: "webpack@npm:5.88.2" + dependencies: + "@types/eslint-scope": "npm:^3.7.3" + "@types/estree": "npm:^1.0.0" + "@webassemblyjs/ast": "npm:^1.11.5" + "@webassemblyjs/wasm-edit": "npm:^1.11.5" + "@webassemblyjs/wasm-parser": "npm:^1.11.5" + acorn: "npm:^8.7.1" + acorn-import-assertions: "npm:^1.9.0" + browserslist: "npm:^4.14.5" + chrome-trace-event: "npm:^1.0.2" + enhanced-resolve: "npm:^5.15.0" + es-module-lexer: "npm:^1.2.1" + eslint-scope: "npm:5.1.1" + events: "npm:^3.2.0" + glob-to-regexp: "npm:^0.4.1" + graceful-fs: "npm:^4.2.9" + json-parse-even-better-errors: "npm:^2.3.1" + loader-runner: "npm:^4.2.0" + mime-types: "npm:^2.1.27" + neo-async: "npm:^2.6.2" + schema-utils: "npm:^3.2.0" + tapable: "npm:^2.1.1" + terser-webpack-plugin: "npm:^5.3.7" + watchpack: "npm:^2.4.0" + webpack-sources: "npm:^3.2.3" + peerDependenciesMeta: + webpack-cli: + optional: true + bin: + webpack: bin/webpack.js + checksum: 10/2b26158f091df1d97b85ed8b9c374c673ee91de41e13579a3d5abb76f48fda0e2fe592541e58a96e2630d5bce18d885ce605f6ae767d7e0bc2b5ce3b700a51f0 + languageName: node + linkType: hard + +"websocket-driver@npm:>=0.5.1": + version: 0.7.4 + resolution: "websocket-driver@npm:0.7.4" + dependencies: + http-parser-js: "npm:>=0.5.1" + safe-buffer: "npm:>=5.1.0" + websocket-extensions: "npm:>=0.1.1" + checksum: 10/17197d265d5812b96c728e70fd6fe7d067471e121669768fe0c7100c939d997ddfc807d371a728556e24fc7238aa9d58e630ea4ff5fd4cfbb40f3d0a240ef32d + languageName: node + linkType: hard + +"websocket-extensions@npm:>=0.1.1": + version: 0.1.4 + resolution: "websocket-extensions@npm:0.1.4" + checksum: 10/b5399b487d277c78cdd2aef63764b67764aa9899431e3a2fa272c6ad7236a0fb4549b411d89afa76d5afd664c39d62fc19118582dc937e5bb17deb694f42a0d1 + languageName: node + linkType: hard + +"websocket@npm:^1.0.33": + version: 1.0.34 + resolution: "websocket@npm:1.0.34" + dependencies: + bufferutil: "npm:^4.0.1" + debug: "npm:^2.2.0" + es5-ext: "npm:^0.10.50" + typedarray-to-buffer: "npm:^3.1.5" + utf-8-validate: "npm:^5.0.2" + yaeti: "npm:^0.0.6" + checksum: 10/b72e3dcc3fa92b4a4511f0df89b25feed6ab06979cb9e522d2736f09855f4bf7588d826773b9405fcf3f05698200eb55ba9da7ef333584653d4912a5d3b13c18 + languageName: node + linkType: hard + +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: 10/f95adbc1e80820828b45cc671d97da7cd5e4ef9deb426c31bcd5ab00dc7103042291613b3ef3caec0a2335ed09e0d5ed026c940755dbb6d404e2b27f940fdf07 + languageName: node + linkType: hard + +"which-boxed-primitive@npm:^1.0.2": + version: 1.0.2 + resolution: "which-boxed-primitive@npm:1.0.2" + dependencies: + is-bigint: "npm:^1.0.1" + is-boolean-object: "npm:^1.1.0" + is-number-object: "npm:^1.0.4" + is-string: "npm:^1.0.5" + is-symbol: "npm:^1.0.3" + checksum: 10/9c7ca7855255f25ac47f4ce8b59c4cc33629e713fd7a165c9d77a2bb47bf3d9655a5664660c70337a3221cf96742f3589fae15a3a33639908d33e29aa2941efb + languageName: node + linkType: hard + +"which-typed-array@npm:^1.1.11, which-typed-array@npm:^1.1.2": + version: 1.1.11 + resolution: "which-typed-array@npm:1.1.11" + dependencies: + available-typed-arrays: "npm:^1.0.5" + call-bind: "npm:^1.0.2" + for-each: "npm:^0.3.3" + gopd: "npm:^1.0.1" + has-tostringtag: "npm:^1.0.0" + checksum: 10/bc9e8690e71d6c64893c9d88a7daca33af45918861003013faf77574a6a49cc6194d32ca7826e90de341d2f9ef3ac9e3acbe332a8ae73cadf07f59b9c6c6ecad + languageName: node + linkType: hard + +"which@npm:^1.2.9": + version: 1.3.1 + resolution: "which@npm:1.3.1" + dependencies: + isexe: "npm:^2.0.0" + bin: + which: ./bin/which + checksum: 10/549dcf1752f3ee7fbb64f5af2eead4b9a2f482108b7de3e85c781d6c26d8cf6a52d37cfbe0642a155fa6470483fe892661a859c03157f24c669cf115f3bbab5e + languageName: node + linkType: hard + +"which@npm:^2.0.1, which@npm:^2.0.2": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 10/4782f8a1d6b8fc12c65e968fea49f59752bf6302dc43036c3bf87da718a80710f61a062516e9764c70008b487929a73546125570acea95c5b5dcc8ac3052c70f + languageName: node + linkType: hard + +"wide-align@npm:^1.1.2, wide-align@npm:^1.1.5": + version: 1.1.5 + resolution: "wide-align@npm:1.1.5" + dependencies: + string-width: "npm:^1.0.2 || 2 || 3 || 4" + checksum: 10/d5f8027b9a8255a493a94e4ec1b74a27bff6679d5ffe29316a3215e4712945c84ef73ca4045c7e20ae7d0c72f5f57f296e04a4928e773d4276a2f1222e4c2e99 + languageName: node + linkType: hard + +"widest-line@npm:^4.0.1": + version: 4.0.1 + resolution: "widest-line@npm:4.0.1" + dependencies: + string-width: "npm:^5.0.1" + checksum: 10/64c48cf27171221be5f86fc54b94dd29879165bdff1a7aa92dde723d9a8c99fb108312768a5d62c8c2b80b701fa27bbd36a1ddc58367585cd45c0db7920a0cba + languageName: node + linkType: hard + +"wif@npm:^2.0.6": + version: 2.0.6 + resolution: "wif@npm:2.0.6" + dependencies: + bs58check: "npm:<3.0.0" + checksum: 10/c8d7581664532d9ab6d163ee5194a9bec71b089a6e50d54d6ec57a9bd714fcf84bc8d9f22f4cfc7c297fc6ad10b973b8e83eca5c41240163fc61f44b5154b7da + languageName: node + linkType: hard + +"winston-transport@npm:^4.5.0": + version: 4.5.0 + resolution: "winston-transport@npm:4.5.0" + dependencies: + logform: "npm:^2.3.2" + readable-stream: "npm:^3.6.0" + triple-beam: "npm:^1.3.0" + checksum: 10/3184b7f29fa97aac5b75ff680100656116aff8d164c09bc7459c9b7cb1ce47d02254caf96c2293791ec175c0e76e5ff59b5ed1374733e0b46248cf4f68a182fc + languageName: node + linkType: hard + +"winston@npm:^3.3.3, winston@npm:^3.7.2": + version: 3.10.0 + resolution: "winston@npm:3.10.0" + dependencies: + "@colors/colors": "npm:1.5.0" + "@dabh/diagnostics": "npm:^2.0.2" + async: "npm:^3.2.3" + is-stream: "npm:^2.0.0" + logform: "npm:^2.4.0" + one-time: "npm:^1.0.0" + readable-stream: "npm:^3.4.0" + safe-stable-stringify: "npm:^2.3.1" + stack-trace: "npm:0.0.x" + triple-beam: "npm:^1.3.0" + winston-transport: "npm:^4.5.0" + checksum: 10/3fe855a9b8185f5c75d485bf4b6889c0c4885e85155b6736f783b08319c201fdae11e876ef87c1d333f9a213a4f7fc413fc8c42c720fefb76c59b3abd4ff6406 + languageName: node + linkType: hard + +"wkx@npm:^0.5.0": + version: 0.5.0 + resolution: "wkx@npm:0.5.0" + dependencies: + "@types/node": "npm:*" + checksum: 10/b8975e33f9431380eb82707ec39689767f967a8ce362eea5303399618896c983a2dec3ad72fd7273bdf126181c760067519130434344891300ebd54f5d5cbf4a + languageName: node + linkType: hard + +"word-wrap@npm:~1.2.3": + version: 1.2.5 + resolution: "word-wrap@npm:1.2.5" + checksum: 10/1ec6f6089f205f83037be10d0c4b34c9183b0b63fca0834a5b3cee55dd321429d73d40bb44c8fc8471b5203d6e8f8275717f49a8ff4b2b0ab41d7e1b563e0854 + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10/cebdaeca3a6880da410f75209e68cd05428580de5ad24535f22696d7d9cab134d1f8498599f344c3cf0fb37c1715807a183778d8c648d6cc0cb5ff2bb4236540 + languageName: node + linkType: hard + +"wrap-ansi@npm:^6.0.1": + version: 6.2.0 + resolution: "wrap-ansi@npm:6.2.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10/0d64f2d438e0b555e693b95aee7b2689a12c3be5ac458192a1ce28f542a6e9e59ddfecc37520910c2c88eb1f82a5411260566dba5064e8f9895e76e169e76187 + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 10/7b1e4b35e9bb2312d2ee9ee7dc95b8cb5f8b4b5a89f7dde5543fe66c1e3715663094defa50d75454ac900bd210f702d575f15f3f17fa9ec0291806d2578d1ddf + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 10/159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 + languageName: node + linkType: hard + +"write-file-atomic@npm:^4.0.2": + version: 4.0.2 + resolution: "write-file-atomic@npm:4.0.2" + dependencies: + imurmurhash: "npm:^0.1.4" + signal-exit: "npm:^3.0.7" + checksum: 10/3be1f5508a46c190619d5386b1ac8f3af3dbe951ed0f7b0b4a0961eed6fc626bd84b50cf4be768dabc0a05b672f5d0c5ee7f42daa557b14415d18c3a13c7d246 + languageName: node + linkType: hard + +"ws@npm:^7.2.1, ws@npm:^7.5.3, ws@npm:^7.5.9": + version: 7.5.9 + resolution: "ws@npm:7.5.9" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/171e35012934bd8788150a7f46f963e50bac43a4dc524ee714c20f258693ac4d3ba2abadb00838fdac42a47af9e958c7ae7e6f4bc56db047ba897b8a2268cf7c + languageName: node + linkType: hard + +"ws@npm:^8.13.0, ws@npm:^8.14.2": + version: 8.14.2 + resolution: "ws@npm:8.14.2" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/815ff01d9bc20a249b2228825d9739268a03a4408c2e0b14d49b0e2ae89d7f10847e813b587ba26992bdc33e9d03bed131e4cae73ff996baf789d53e99c31186 + languageName: node + linkType: hard + +"xml2js@npm:0.5.0": + version: 0.5.0 + resolution: "xml2js@npm:0.5.0" + dependencies: + sax: "npm:>=0.6.0" + xmlbuilder: "npm:~11.0.0" + checksum: 10/27c4d759214e99be5ec87ee5cb1290add427fa43df509d3b92d10152b3806fd2f7c9609697a18b158ccf2caa01e96af067cdba93196f69ca10c90e4f79a08896 + languageName: node + linkType: hard + +"xmlbuilder@npm:~11.0.0": + version: 11.0.1 + resolution: "xmlbuilder@npm:11.0.1" + checksum: 10/c8c3d208783718db5b285101a736cd8e6b69a5c265199a0739abaa93d1a1b7de5489fd16df4e776e18b2c98cb91f421a7349e99fd8c1ebeb44ecfed72a25091a + languageName: node + linkType: hard + +"xmlcreate@npm:^2.0.4": + version: 2.0.4 + resolution: "xmlcreate@npm:2.0.4" + checksum: 10/4b508f92848fcc05d98b5c0bee40242de327410dda3d16659bb9d3c88faeba26f4af793111ad443be673a60f300b9fd51a6349875c63f34bcbe61a321b94c7ef + languageName: node + linkType: hard + +"xstate@npm:^4.38.2": + version: 4.38.2 + resolution: "xstate@npm:4.38.2" + checksum: 10/76bbd8d2dd0de9f99376daf41654c3f162b8f14102ab1e7a44a13b05e21e50e6bdd5b3867ac8d252a367e438e3928f8e1b4c56f30fac19708b22167d3fd175b6 + languageName: node + linkType: hard + +"xtend@npm:^4.0.0": + version: 4.0.2 + resolution: "xtend@npm:4.0.2" + checksum: 10/ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a + languageName: node + linkType: hard + +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 10/5f1b5f95e3775de4514edbb142398a2c37849ccfaf04a015be5d75521e9629d3be29bd4432d23c57f37e5b61ade592fb0197022e9993f81a06a5afbdcda9346d + languageName: node + linkType: hard + +"yaeti@npm:^0.0.6": + version: 0.0.6 + resolution: "yaeti@npm:0.0.6" + checksum: 10/6db12c152f7c363b80071086a3ebf5032e03332604eeda988872be50d6c8469e1f13316175544fa320f72edad696c2d83843ad0ff370659045c1a68bcecfcfea + languageName: node + linkType: hard + +"yallist@npm:^2.0.0": + version: 2.1.2 + resolution: "yallist@npm:2.1.2" + checksum: 10/75fc7bee4821f52d1c6e6021b91b3e079276f1a9ce0ad58da3c76b79a7e47d6f276d35e206a96ac16c1cf48daee38a8bb3af0b1522a3d11c8ffe18f898828832 + languageName: node + linkType: hard + +"yallist@npm:^3.0.2": + version: 3.1.1 + resolution: "yallist@npm:3.1.1" + checksum: 10/9af0a4329c3c6b779ac4736c69fae4190ac03029fa27c1aef4e6bcc92119b73dea6fe5db5fe881fb0ce2a0e9539a42cdf60c7c21eda04d1a0b8c082e38509efb + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10/4cb02b42b8a93b5cf50caf5d8e9beb409400a8a4d85e83bb0685c1457e9ac0b7a00819e9f5991ac25ffabb56a78e2f017c1acc010b3a1babfe6de690ba531abd + languageName: node + linkType: hard + +"yaml-ast-parser@npm:0.0.43": + version: 0.0.43 + resolution: "yaml-ast-parser@npm:0.0.43" + checksum: 10/a54d00c8e0716a392c6e76eee965b3b4bba434494196490946e416fc47f20a1d89820461afacd9431edbb8209e28fce33bcff1fb42dd83f90e51fc31e80251c9 + languageName: node + linkType: hard + +"yaml@npm:^1.10.0": + version: 1.10.2 + resolution: "yaml@npm:1.10.2" + checksum: 10/e088b37b4d4885b70b50c9fa1b7e54bd2e27f5c87205f9deaffd1fb293ab263d9c964feadb9817a7b129a5bf30a06582cb08750f810568ecc14f3cdbabb79cb3 + languageName: node + linkType: hard + +"yamljs@npm:^0.3.0": + version: 0.3.0 + resolution: "yamljs@npm:0.3.0" + dependencies: + argparse: "npm:^1.0.7" + glob: "npm:^7.0.5" + bin: + json2yaml: ./bin/json2yaml + yaml2json: ./bin/yaml2json + checksum: 10/041ccb467b04e0ebfa8224fceca03a28fb28666f46d8ac82ba19b2b118d44604566c17def5cb5ae6681fcedd903affbb42f757706b1e5440dcd304d5f802ef3c + languageName: node + linkType: hard + +"yargs-parser@npm:^20.2.2": + version: 20.2.9 + resolution: "yargs-parser@npm:20.2.9" + checksum: 10/0188f430a0f496551d09df6719a9132a3469e47fe2747208b1dd0ab2bb0c512a95d0b081628bbca5400fb20dbf2fabe63d22badb346cecadffdd948b049f3fcc + languageName: node + linkType: hard + +"yargs-parser@npm:^21.0.1, yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: 10/9dc2c217ea3bf8d858041252d43e074f7166b53f3d010a8c711275e09cd3d62a002969a39858b92bbda2a6a63a585c7127014534a560b9c69ed2d923d113406e + languageName: node + linkType: hard + +"yargs@npm:^16.2.0": + version: 16.2.0 + resolution: "yargs@npm:16.2.0" + dependencies: + cliui: "npm:^7.0.2" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.0" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^20.2.2" + checksum: 10/807fa21211d2117135d557f95fcd3c3d390530cda2eca0c840f1d95f0f40209dcfeb5ec18c785a1f3425896e623e3b2681e8bb7b6600060eda1c3f4804e7957e + languageName: node + linkType: hard + +"yargs@npm:^17.3.1, yargs@npm:^17.7.2": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: 10/abb3e37678d6e38ea85485ed86ebe0d1e3464c640d7d9069805ea0da12f69d5a32df8e5625e370f9c96dd1c2dc088ab2d0a4dd32af18222ef3c4224a19471576 + languageName: node + linkType: hard + +"yauzl@npm:^2.4.2": + version: 2.10.0 + resolution: "yauzl@npm:2.10.0" + dependencies: + buffer-crc32: "npm:~0.2.3" + fd-slicer: "npm:~1.1.0" + checksum: 10/1e4c311050dc0cf2ee3dbe8854fe0a6cde50e420b3e561a8d97042526b4cf7a0718d6c8d89e9e526a152f4a9cec55bcea9c3617264115f48bd6704cf12a04445 + languageName: node + linkType: hard + +"yn@npm:3.1.1": + version: 3.1.1 + resolution: "yn@npm:3.1.1" + checksum: 10/2c487b0e149e746ef48cda9f8bad10fc83693cd69d7f9dcd8be4214e985de33a29c9e24f3c0d6bcf2288427040a8947406ab27f7af67ee9456e6b84854f02dd6 + languageName: node + linkType: hard + +"yocto-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "yocto-queue@npm:0.1.0" + checksum: 10/f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 + languageName: node + linkType: hard + +"zip-stream@npm:^4.1.0": + version: 4.1.1 + resolution: "zip-stream@npm:4.1.1" + dependencies: + archiver-utils: "npm:^3.0.4" + compress-commons: "npm:^4.1.2" + readable-stream: "npm:^3.6.0" + checksum: 10/33bd5ee7017656c2ad728b5d4ba510e15bd65ce1ec180c5bbdc7a5f063256353ec482e6a2bc74de7515219d8494147924b9aae16e63fdaaf37cdf7d1ee8df125 + languageName: node + linkType: hard