diff --git a/.codebuild/build.sh b/.codebuild/build.sh index a45243cd..62b05ac0 100644 --- a/.codebuild/build.sh +++ b/.codebuild/build.sh @@ -86,6 +86,44 @@ deploy_hathor_network_account() { send_slack_message "New version deployed to testnet-production: ${GIT_REF_TO_DEPLOY}" + # --- Testnet Hotel --- + # Gets all env vars with `testnethotel_` prefix and re-exports them without the prefix + for var in "${!testnethotel_@}"; do + export ${var#testnethotel_}="${!var}" + done + + make migrate; + make build-daemon; + make deploy-lambdas-testnet-hotel; + # The idea here is that if the lambdas deploy fail, the built image won't be pushed: + make push-daemon; + + # Unsets all the testnet env vars so we make sure they don't leak to other deploys + for var in "${!testnethotel_@}"; do + unset ${var#testnethotel_} + done + + send_slack_message "New version deployed to testnet-hotel: ${GIT_REF_TO_DEPLOY}" + + # --- Testnet India --- + # Gets all env vars with `testnetindia_` prefix and re-exports them without the prefix + for var in "${!testnetindia_@}"; do + export ${var#testnetindia_}="${!var}" + done + + make migrate; + make build-daemon; + make deploy-lambdas-testnet-india; + # The idea here is that if the lambdas deploy fail, the built image won't be pushed: + make push-daemon; + + # Unsets all the testnet env vars so we make sure they don't leak to other deploys + for var in "${!testnetindia_@}"; do + unset ${var#testnetindia_} + done + + send_slack_message "New version deployed to testnet-india: ${GIT_REF_TO_DEPLOY}" + # --- Mainnet --- # Gets all env vars with `mainnet_` prefix and re-exports them without the prefix for var in "${!mainnet_@}"; do @@ -129,19 +167,77 @@ deploy_nano_testnet() { make migrate; make deploy-lambdas-nano-testnet; - send_slack_message "New version deployed to nano-testnet: ${GIT_REF_TO_DEPLOY}" + send_slack_message "New version deployed to nano-testnet-alpha: ${GIT_REF_TO_DEPLOY}" elif expr "${MANUAL_DEPLOY}" : "true" >/dev/null; then make migrate; make deploy-lambdas-nano-testnet; - send_slack_message "Branch manually deployed to nano-testnet: ${GIT_REF_TO_DEPLOY}" + send_slack_message "Branch manually deployed to nano-testnet-alpha: ${GIT_REF_TO_DEPLOY}" elif expr "${ROLLBACK}" : "true" >/dev/null; then make migrate; make deploy-lambdas-nano-testnet; - send_slack_message "Rollback performed on nano-tesnet to: ${GIT_REF_TO_DEPLOY}"; + send_slack_message "Rollback performed on nano-tesnet-alpha to: ${GIT_REF_TO_DEPLOY}"; else - echo "We don't deploy ${GIT_REF_TO_DEPLOY} to nano-testnet. Nothing to do."; + echo "We don't deploy ${GIT_REF_TO_DEPLOY} to nano-testnet-alpha. Nothing to do."; + fi; +} + +deploy_nano_testnet_bravo() { + # Deploys the releases and release-candidates to our nano-testnet-bravo environment + + # We deploy only the Lambdas here, because the image for the daemon used in nano-testnet is + # the same as the one built in the hathor-network account, since it runs there as well + + echo "Building git ref ${GIT_REF_TO_DEPLOY}..." + + # This will match both releases and release-candidates + if expr "${GIT_REF_TO_DEPLOY}" : "v.*" >/dev/null; then + make migrate; + make deploy-lambdas-nano-testnet-bravo; + + send_slack_message "New version deployed to nano-testnet-bravo: ${GIT_REF_TO_DEPLOY}" + elif expr "${MANUAL_DEPLOY}" : "true" >/dev/null; then + make migrate; + make deploy-lambdas-nano-testnet-bravo; + + send_slack_message "Branch manually deployed to nano-testnet-bravo: ${GIT_REF_TO_DEPLOY}" + elif expr "${ROLLBACK}" : "true" >/dev/null; then + make migrate; + make deploy-lambdas-nano-testnet-bravo; + + send_slack_message "Rollback performed on nano-tesnet-bravo to: ${GIT_REF_TO_DEPLOY}"; + else + echo "We don't deploy ${GIT_REF_TO_DEPLOY} to nano-testnet-bravo. Nothing to do."; + fi; +} + +deploy_nano_testnet_hackaton() { + # Deploys the releases and release-candidates to our nano-testnet-hackaton environment + + # We deploy only the Lambdas here, because the daemon used in nano-testnet-hackaton is the same as + # the one built in the hathor-network account, since it runs there as well + + echo "Building git ref ${GIT_REF_TO_DEPLOY}..." + + # This will match both releases and release-candidates + if expr "${GIT_REF_TO_DEPLOY}" : "v.*" >/dev/null; then + make migrate; + make deploy-lambdas-nano-testnet-hackaton; + + send_slack_message "New version deployed to nano-testnet-hackaton: ${GIT_REF_TO_DEPLOY}" + elif expr "${MANUAL_DEPLOY}" : "true" >/dev/null; then + make migrate; + make deploy-lambdas-nano-testnet-hackaton; + + send_slack_message "Branch manually deployed to nano-testnet-hackaton: ${GIT_REF_TO_DEPLOY}" + elif expr "${ROLLBACK}" : "true" >/dev/null; then + make migrate; + make deploy-lambdas-nano-testnet-hackaton; + + send_slack_message "Rollback performed on nano-tesnet-hackaton to: ${GIT_REF_TO_DEPLOY}"; + else + echo "We don't deploy ${GIT_REF_TO_DEPLOY} to nano-testnet-hackaton. Nothing to do."; fi; } @@ -219,6 +315,12 @@ case $option in nano-testnet) deploy_nano_testnet ;; + nano-testnet-bravo) + deploy_nano_testnet_bravo + ;; + nano-testnet-hackaton) + deploy_nano_testnet_hackaton + ;; ekvilibro-testnet) deploy_ekvilibro_testnet ;; diff --git a/.codebuild/buildspec.yml b/.codebuild/buildspec.yml index 1d167ee6..445d5dee 100644 --- a/.codebuild/buildspec.yml +++ b/.codebuild/buildspec.yml @@ -15,8 +15,8 @@ env: VOIDED_TX_OFFSET: 20 TX_HISTORY_MAX_COUNT: 50 CREATE_NFT_MAX_RETRIES: 3 - dev_DEFAULT_SERVER: "https://wallet-service.private-nodes.testnet.hathor.network/v1a/" - dev_WS_DOMAIN: "ws.dev.wallet-service.testnet.hathor.network" + dev_DEFAULT_SERVER: "https://wallet-service.private-nodes.india.testnet.hathor.network/v1a/" + dev_WS_DOMAIN: "ws.dev.wallet-service.india.testnet.hathor.network" dev_NETWORK: "testnet" dev_LOG_LEVEL: "debug" dev_NFT_AUTO_REVIEW_ENABLED: "true" @@ -37,6 +37,29 @@ env: testnet_PUSH_NOTIFICATION_ENABLED: "true" testnet_PUSH_ALLOWED_PROVIDERS: "android,ios" testnet_APPLICATION_NAME: "wallet-service-testnet" + testnethotel_DEFAULT_SERVER: "https://wallet-service.private-nodes.hotel.testnet.hathor.network/v1a/" + testnethotel_WS_DOMAIN: "ws.wallet-service.hotel.testnet.hathor.network" + testnethotel_NETWORK: "testnet" + testnethotel_LOG_LEVEL: "debug" + testnethotel_NFT_AUTO_REVIEW_ENABLED: "true" + testnethotel_EXPLORER_STAGE: "hotel" + testnethotel_EXPLORER_SERVICE_LAMBDA_ENDPOINT: "https://lambda.eu-central-1.amazonaws.com" + testnethotel_WALLET_SERVICE_LAMBDA_ENDPOINT: "https://lambda.eu-central-1.amazonaws.com" + testnethotel_PUSH_NOTIFICATION_ENABLED: "true" + testnethotel_PUSH_ALLOWED_PROVIDERS: "android,ios" + testnethotel_APPLICATION_NAME: "wallet-service-testnet-hotel" + testnetindia_DEFAULT_SERVER: "https://wallet-service.private-nodes.india.testnet.hathor.network/v1a/" + testnetindia_WS_DOMAIN: "ws.wallet-service.india.testnet.hathor.network" + testnetindia_NETWORK: "testnet" + testnetindia_LOG_LEVEL: "debug" + testnetindia_NFT_AUTO_REVIEW_ENABLED: "true" + testnetindia_EXPLORER_STAGE: "india" + testnetindia_EXPLORER_SERVICE_LAMBDA_ENDPOINT: "https://lambda.eu-central-1.amazonaws.com" + testnetindia_WALLET_SERVICE_LAMBDA_ENDPOINT: "https://lambda.eu-central-1.amazonaws.com" + testnetindia_PUSH_NOTIFICATION_ENABLED: "true" + testnetindia_PUSH_ALLOWED_PROVIDERS: "android,ios" + testnetindia_APPLICATION_NAME: "wallet-service-testnet-india" + testnetindia_SERVERLESS_DEPLOY_PREFIX: "wallet-service" 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" @@ -116,6 +139,58 @@ env: testnet_ALERT_MANAGER_REGION: "WalletService/testnet:ALERT_MANAGER_REGION" testnet_ALERT_MANAGER_TOPIC: "WalletService/testnet:ALERT_MANAGER_TOPIC" testnet_ALERT_MANAGER_ACCOUNT_ID: "WalletService/testnet:ALERT_MANAGER_ACCOUNT_ID" + # Testnet Hotel secrets + testnethotel_ACCOUNT_ID: "WalletService/testnet-hotel:account_id" + testnethotel_AUTH_SECRET: "WalletService/testnet-hotel:auth_secret" + testnethotel_AWS_VPC_DEFAULT_SG_ID: "WalletService/testnet-hotel:aws_vpc_default_sg_id" + testnethotel_AWS_SUBNET_ID_1: "WalletService/testnet-hotel:aws_subnet_id_1" + testnethotel_AWS_SUBNET_ID_2: "WalletService/testnet-hotel:aws_subnet_id_2" + testnethotel_AWS_SUBNET_ID_3: "WalletService/testnet-hotel:aws_subnet_id_3" + testnethotel_DB_NAME: "WalletService/rds/testnet-hotel:dbname" + testnethotel_DB_USER: "WalletService/rds/testnet-hotel:username" + testnethotel_DB_PASS: "WalletService/rds/testnet-hotel:password" + testnethotel_DB_ENDPOINT: "WalletService/rds/testnet-hotel:host" + testnethotel_DB_PORT: "WalletService/rds/testnet-hotel:port" + testnethotel_REDIS_URL: "WalletService/redis/testnet-hotel:url" + testnethotel_REDIS_PASSWORD: "WalletService/redis/testnet-hotel:password" + testnethotel_FIREBASE_PROJECT_ID: "WalletService/testnet-hotel:FIREBASE_PROJECT_ID" + testnethotel_FIREBASE_PRIVATE_KEY_ID: "WalletService/testnet-hotel:FIREBASE_PRIVATE_KEY_ID" + testnethotel_FIREBASE_PRIVATE_KEY: "WalletService/testnet-hotel:FIREBASE_PRIVATE_KEY" + testnethotel_FIREBASE_CLIENT_EMAIL: "WalletService/testnet-hotel:FIREBASE_CLIENT_EMAIL" + testnethotel_FIREBASE_CLIENT_ID: "WalletService/testnet-hotel:FIREBASE_CLIENT_ID" + testnethotel_FIREBASE_AUTH_URI: "WalletService/testnet-hotel:FIREBASE_AUTH_URI" + testnethotel_FIREBASE_TOKEN_URI: "WalletService/testnet-hotel:FIREBASE_TOKEN_URI" + testnethotel_FIREBASE_AUTH_PROVIDER_X509_CERT_URL: "WalletService/testnet-hotel:FIREBASE_AUTH_PROVIDER_X509_CERT_URL" + testnethotel_FIREBASE_CLIENT_X509_CERT_URL: "WalletService/testnet-hotel:FIREBASE_CLIENT_X509_CERT_URL" + testnethotel_ALERT_MANAGER_REGION: "WalletService/testnet-hotel:ALERT_MANAGER_REGION" + testnethotel_ALERT_MANAGER_TOPIC: "WalletService/testnet-hotel:ALERT_MANAGER_TOPIC" + testnethotel_ALERT_MANAGER_ACCOUNT_ID: "WalletService/testnet-hotel:ALERT_MANAGER_ACCOUNT_ID" + # Testnet India secrets + testnetindia_ACCOUNT_ID: "WalletService/testnet-india:account_id" + testnetindia_AUTH_SECRET: "WalletService/testnet-india:auth_secret" + testnetindia_AWS_VPC_DEFAULT_SG_ID: "WalletService/testnet-india:aws_vpc_default_sg_id" + testnetindia_AWS_SUBNET_ID_1: "WalletService/testnet-india:aws_subnet_id_1" + testnetindia_AWS_SUBNET_ID_2: "WalletService/testnet-india:aws_subnet_id_2" + testnetindia_AWS_SUBNET_ID_3: "WalletService/testnet-india:aws_subnet_id_3" + testnetindia_DB_NAME: "WalletService/rds/testnet-india:dbname" + testnetindia_DB_USER: "WalletService/rds/testnet-india:username" + testnetindia_DB_PASS: "WalletService/rds/testnet-india:password" + testnetindia_DB_ENDPOINT: "WalletService/rds/testnet-india:host" + testnetindia_DB_PORT: "WalletService/rds/testnet-india:port" + testnetindia_REDIS_URL: "WalletService/redis/testnet-india:url" + testnetindia_REDIS_PASSWORD: "WalletService/redis/testnet-india:password" + testnetindia_FIREBASE_PROJECT_ID: "WalletService/testnet-india:FIREBASE_PROJECT_ID" + testnetindia_FIREBASE_PRIVATE_KEY_ID: "WalletService/testnet-india:FIREBASE_PRIVATE_KEY_ID" + testnetindia_FIREBASE_PRIVATE_KEY: "WalletService/testnet-india:FIREBASE_PRIVATE_KEY" + testnetindia_FIREBASE_CLIENT_EMAIL: "WalletService/testnet-india:FIREBASE_CLIENT_EMAIL" + testnetindia_FIREBASE_CLIENT_ID: "WalletService/testnet-india:FIREBASE_CLIENT_ID" + testnetindia_FIREBASE_AUTH_URI: "WalletService/testnet-india:FIREBASE_AUTH_URI" + testnetindia_FIREBASE_TOKEN_URI: "WalletService/testnet-india:FIREBASE_TOKEN_URI" + testnetindia_FIREBASE_AUTH_PROVIDER_X509_CERT_URL: "WalletService/testnet-india:FIREBASE_AUTH_PROVIDER_X509_CERT_URL" + testnetindia_FIREBASE_CLIENT_X509_CERT_URL: "WalletService/testnet-india:FIREBASE_CLIENT_X509_CERT_URL" + testnetindia_ALERT_MANAGER_REGION: "WalletService/testnet-india:ALERT_MANAGER_REGION" + testnetindia_ALERT_MANAGER_TOPIC: "WalletService/testnet-india:ALERT_MANAGER_TOPIC" + testnetindia_ALERT_MANAGER_ACCOUNT_ID: "WalletService/testnet-india:ALERT_MANAGER_ACCOUNT_ID" # Mainnet Staging secrets mainnet_staging_ACCOUNT_ID: "WalletService/mainnet_staging:account_id" mainnet_staging_AUTH_SECRET: "WalletService/mainnet_staging:auth_secret" @@ -173,12 +248,12 @@ phases: #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 + nodejs: 22 # name: version commands: - npm install -g yarn + # corepack will use the version of yarn specified in package.json - corepack enable - - yarn set version 4.1.0 - yarn install pre_build: commands: diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3ed55f1b..00000000 --- a/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -# 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. - -# Build phase -FROM node:20-alpine AS builder - -WORKDIR /app - -RUN apk update && apk add python3 g++ make py3-setuptools - -COPY . . - -RUN corepack enable - -# Use the same version as flake's -RUN yarn set version 4.1.0 - -# This will install dependencies for all packages, except for the lambdas since -# they are ignored in .dockerignore -RUN yarn install - -RUN yarn workspace sync-daemon run build - -# This will remove all dev dependencies and install production deps only -RUN yarn workspaces focus -A --production - -# Run phase -FROM node:20-alpine AS runner - -WORKDIR /app - -# Copy only the necessary files from the build phase -COPY --from=builder /app . - -WORKDIR /app/packages/daemon/ - -CMD ["node", "dist/index.js"] diff --git a/Makefile b/Makefile index c297758a..2d066fce 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,14 @@ build-and-push-daemon: build-daemon: bash scripts/build-daemon.sh +.PHONY: build-migrator +build-migrator: + bash scripts/build-migrator.sh + +.PHONY: build-service +build-service: + bash scripts/build-service.sh + .PHONY: push-daemon push-daemon: bash scripts/push-daemon.sh @@ -14,6 +22,14 @@ push-daemon: deploy-lambdas-nano-testnet: AWS_SDK_LOAD_CONFIG=1 yarn workspace wallet-service run serverless deploy --stage nano --region eu-central-1 --aws-profile nano-testnet +.PHONY: deploy-lambdas-nano-testnet-bravo +deploy-lambdas-nano-testnet-bravo: + AWS_SDK_LOAD_CONFIG=1 yarn workspace wallet-service run serverless deploy --stage nano-bravo --region eu-central-1 --aws-profile nano-testnet + +.PHONY: deploy-lambdas-nano-testnet-hackaton +deploy-lambdas-nano-testnet-hackaton: + AWS_SDK_LOAD_CONFIG=1 yarn workspace wallet-service run serverless deploy --stage hackaton --region eu-central-1 --aws-profile nano-testnet-hackaton + .PHONY: deploy-lambdas-ekvilibro-testnet deploy-lambdas-ekvilibro-testnet: AWS_SDK_LOAD_CONFIG=1 yarn workspace wallet-service run serverless deploy --stage ekvilibro --region eu-central-1 --aws-profile ekvilibro @@ -30,6 +46,14 @@ deploy-lambdas-dev-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-testnet-hotel +deploy-lambdas-testnet-hotel: + AWS_SDK_LOAD_CONFIG=1 yarn workspace wallet-service run serverless deploy --stage hotel --region eu-central-1 + +.PHONY: deploy-lambdas-testnet-india +deploy-lambdas-testnet-india: + AWS_SDK_LOAD_CONFIG=1 yarn workspace wallet-service run serverless deploy --stage india --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 diff --git a/README.md b/README.md index 2cc44002..cccc56a1 100644 --- a/README.md +++ b/README.md @@ -53,3 +53,85 @@ AWS_SECRET_ACCESS_KEY="..." ``` These are used for communicating with the alert SQS + +## Reseeding the HTR Token After Database Reset + +If you need to reset the database (for example, to re-sync it from scratch), you must re-insert the HTR token into the `token` table. This is handled by a seed script that will automatically calculate the correct transaction count for HTR based on the current state of the database. + +To run the seed and add the HTR token again, you must have a `.env` file in your project root with all required environment variables set. At a minimum, you should include: + +``` +NODE_ENV=production +DB_ENDPOINT= +DB_NAME= +DB_USER= +DB_PORT= +DB_PASS= +``` + +Adjust the values as needed for your environment. For production, ensure `NODE_ENV=production` is set. + +To run the seed and add the HTR token again, use the following command: + +``` +yarn dlx sequelize-cli db:seed:all +``` + +This will ensure the HTR token is present and its transaction count is accurate, even if the database already contains transactions for HTR. + +## Database Cleanup + +If you need to re-sync the database from scratch, you must stop the daemon and clean all database tables before starting the sync process again. This ensures there is no leftover data that could cause inconsistencies. + +Below is a SQL script you can use to clean up (truncate) all tables in the database. This script disables foreign key checks, truncates all tables, and then re-enables foreign key checks. **Be careful: this will delete all data in the database.** + +``` +SET FOREIGN_KEY_CHECKS = 0; + +TRUNCATE TABLE address_balance; +TRUNCATE TABLE address_tx_history; +TRUNCATE TABLE address; +TRUNCATE TABLE miner; +TRUNCATE TABLE push_devices; +TRUNCATE TABLE sync_metadata; +TRUNCATE TABLE token; +TRUNCATE TABLE transaction; +TRUNCATE TABLE tx_output; +TRUNCATE TABLE tx_proposal; +TRUNCATE TABLE wallet; +TRUNCATE TABLE wallet_balance; +TRUNCATE TABLE wallet_tx_history; +TRUNCATE TABLE version_data; + +SET FOREIGN_KEY_CHECKS = 1; +``` + +To use this script, save it as `cleanup.sql` and run: + +``` +mysql -u -p < cleanup.sql +``` + +After cleaning the database, you can reseed the HTR token as described in the previous section. + +## Running Inside Containers +When running these applications inside containers, it's worth noting that there are a few Dockerfiles in this monorepo. + +### 1) The Daemon container +This Dockerfile is located at `./packages/daemon` and is used to build the sync daemon image. It, however,needs a properly migrated database and all the fullnode identifiers to run correctly. + +The fullnode identifiers may be fetched dynamically at startup with the use of the `FETCH_FULLNODE_IDS` environment variable, provided the remaining fullnode connection config is available. Please note that this dynamic fetching is only recommended in development environments, as the identifiers are an additional security measure on production builds. + +Its image can be build using the `make build-daemon` while on the root folder. + +### 2) The Migrator container +The Migrator Dockerfile is located at `./db` and is used to build the migrator image then shut off. This image is responsible for applying database migrations to the database connection passed through the environment variables. + +It's specially important if the database has just been created by the dockerized environment, in which case run this migrator container before starting the daemon. This, again, is only expected in discardable development environments, as production and other more persistent databases should be managed externally. + +Its image can be build using the `make build-migrator` while on the root folder. + +### 3) The Wallet Service container +This is the actual serverless application containing the externally consumed API. Its Dockerfile is located at `./packages/wallet-service` and is used to build the wallet service image. It needs a healthy Daemon to run correctly. + +Its image can be build using the `make build-service` while on the root folder. diff --git a/db/Dockerfile.dev b/db/Dockerfile.dev new file mode 100644 index 00000000..18b976ea --- /dev/null +++ b/db/Dockerfile.dev @@ -0,0 +1,66 @@ +# Copyright 2025 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. + +# ========================================================================= +# This Dockerfile is intended for migrating the database of a dockerized private blockchain +# for the Wallet Service Daemon. +# +# It serves only as a means to run the migration scripts in an isolated environment. The container will run only +# for as long as the migration is running, and then it will exit. +# +# The migration scripts do not cause any impact if it is ran multiple times, so it is safe to run this container +# for each deployment of the Wallet Service Daemon. +# +# The expected image size is about 250MB as of v1.9.0 +# +# Sample usage in a docker-compose.yml: +# ws-migrator: +# image: hathornetwork/hathor-wallet-service-migrator +# restart: "no" # Critical: don't restart migration service +# depends_on: +# mysql: # Replace with your actual mysql service name +# condition: service_healthy +# environment: +# DB_ENDPOINT: "mysql" +# DB_NAME: "wallet_service" +# DB_USER: "wallet_service_user" +# DB_PASS: "password" +# DB_PORT: 3306 +# networks: +# - hathor-privnet + +# This Dockerfile is used to build and run the database migration container. +FROM node:22-alpine + +WORKDIR /app + +# Copy only the necessary files for the migration +COPY ./db ./db +COPY ./db/migrations ./migrations + +# This will install only the exact versions of sequelize, sequelize-cli and mysql2 +# that are already in use in the project, avoiding any unwanted upgrades. +# It will also install the dotenv package, but it's not as critical and we can use the latest version. +# +# Note that this migrator container does not need to have all the dependencies of the main project, +# as it will only be used to run the migration scripts. For this, a new package.json is created +# with only the necessary dependencies, reducing image size. + +ARG SEQUELIZE_VERSION +ARG SEQUELIZE_CLI_VERSION +ARG MYSQL2_VERSION +RUN test -n "$SEQUELIZE_VERSION" \ + && test -n "$SEQUELIZE_CLI_VERSION" \ + || (echo "Both SEQUELIZE_VERSION and SEQUELIZE_CLI_VERSION must be set" && exit 1) +# Avoids potential conflicts with pre-installed v1.x versions of yarn +RUN npm uninstall -g yarn +RUN corepack enable +RUN echo '{"name": "migrator", "private": true}' > package.json +RUN yarn add sequelize@"$SEQUELIZE_VERSION" sequelize-cli@"$SEQUELIZE_CLI_VERSION" mysql2@"$MYSQL2_VERSION" dotenv + +# Run the migration scripts +RUN cp /app/db/migration-entrypoint.sh /app; +ENTRYPOINT ["/bin/sh", "/app/migration-entrypoint.sh"] diff --git a/db/config.js b/db/config.js index 7d201df9..a804ab39 100644 --- a/db/config.js +++ b/db/config.js @@ -5,10 +5,11 @@ module.exports = { username: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, - host: '127.0.0.1', + host: process.env.DB_ENDPOINT || '127.0.0.1', port: process.env.DB_PORT || 3306, dialect: 'mysql', dialectOptions: { + supportBigNumbers: true, bigNumberStrings: true, }, }, @@ -20,6 +21,7 @@ module.exports = { port: process.env.CI_DB_PORT || 3306, dialect: 'mysql', dialectOptions: { + supportBigNumbers: true, bigNumberStrings: true, }, }, @@ -31,6 +33,7 @@ module.exports = { port: process.env.DB_PORT, dialect: 'mysql', dialectOptions: { + supportBigNumbers: true, bigNumberStrings: true, }, }, diff --git a/db/migration-entrypoint.sh b/db/migration-entrypoint.sh new file mode 100755 index 00000000..0fcc25ed --- /dev/null +++ b/db/migration-entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +# Run the migration script. +# Note that this is supposed to run from the root of the repository, inside a container +corepack enable +yarn sequelize db:migrate --config db/config.js + +# Run the command passed to the entrypoint (if any). +exec "$@" diff --git a/db/migrations/20250723213141-add_seqnum_column.js b/db/migrations/20250723213141-add_seqnum_column.js new file mode 100644 index 00000000..030c6fcf --- /dev/null +++ b/db/migrations/20250723213141-add_seqnum_column.js @@ -0,0 +1,16 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.addColumn('address', 'seqnum', { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.removeColumn('address', 'seqnum') + } +}; diff --git a/db/seeders/20250416132150-add-htr-token.js b/db/seeders/20250416132150-add-htr-token.js new file mode 100644 index 00000000..97480149 --- /dev/null +++ b/db/seeders/20250416132150-add-htr-token.js @@ -0,0 +1,23 @@ +'use strict'; + +module.exports = { + up: async (queryInterface) => { + // Count unique transactions for HTR + const [results] = await queryInterface.sequelize.query( + "SELECT COUNT(DISTINCT tx_id) AS count FROM tx_output WHERE token_id = '00'" + ); + const htrTxCount = results[0]?.count || 0; + + // Insert HTR token with the correct transaction count + await queryInterface.bulkInsert('token', [{ + id: '00', + name: 'Hathor', + symbol: 'HTR', + transactions: htrTxCount, + }]); + }, + + down: async (queryInterface) => { + await queryInterface.bulkDelete('token', { id: '00' }); + } +}; diff --git a/flake.lock b/flake.lock index 1da0f831..9df99fe4 100644 --- a/flake.lock +++ b/flake.lock @@ -2,15 +2,14 @@ "nodes": { "devshell": { "inputs": { - "nixpkgs": "nixpkgs", - "systems": "systems" + "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1695973661, - "narHash": "sha256-BP2H4c42GThPIhERtTpV1yCtwQHYHEKdRu7pjrmQAwo=", + "lastModified": 1741473158, + "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", "owner": "numtide", "repo": "devshell", - "rev": "cd4e2fda3150dd2f689caeac07b7f47df5197c31", + "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", "type": "github" }, "original": { @@ -20,12 +19,15 @@ } }, "flake-utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1649676176, - "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -36,11 +38,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1677383253, - "narHash": "sha256-UfpzWfSxkfXHnb4boXZNaKsAcUrZT9Hw+tao1oZxd08=", + "lastModified": 1722073938, + "narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9952d6bc395f5841262b006fbace8dd7e143b634", + "rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae", "type": "github" }, "original": { @@ -52,10 +54,12 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 0, - "narHash": "sha256-GheQGRNYAhHsvPxWVOhAmg9lZKkis22UPbEHlmZMthg=", - "path": "/nix/store/xnws56qm6sg7yc7dbw62c1y24s9jhcpf-source", - "type": "path" + "lastModified": 1743320628, + "narHash": "sha256-FurMxmjEEqEMld11eX2vgfAx0Rz0JhoFm8UgxbfCZa8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "63158b9cbb6ec93d26255871c447b0f01da81619", + "type": "github" }, "original": { "id": "nixpkgs", @@ -87,11 +91,11 @@ }, "unstableNixPkgs": { "locked": { - "lastModified": 1708118438, - "narHash": "sha256-kk9/0nuVgA220FcqH/D2xaN6uGyHp/zoxPNUmPCMmEE=", + "lastModified": 1743315132, + "narHash": "sha256-6hl6L/tRnwubHcA4pfUUtk542wn2Om+D4UnDhlDW9BE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5863c27340ba4de8f83e7e3c023b9599c3cb3c80", + "rev": "52faf482a3889b7619003c0daec593a1912fddc1", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 81ee0a39..7f182f77 100644 --- a/flake.nix +++ b/flake.nix @@ -15,7 +15,7 @@ inherit (packages) nodePackages; in { - nodejs = final.nodejs_20; + nodejs = final.nodejs_22; nodePackages = prev.nodePackages; yarn = (import unstableNixPkgs { system = final.system; }).yarn-berry; }; @@ -34,7 +34,7 @@ devShell = pkgs.devshell.mkShell { packages = with pkgs; [ nixpkgs-fmt - nodejs_20 + nodejs_22 yarn docker-compose ]; diff --git a/package.json b/package.json index a18fb01d..0acc2910 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "hathor-wallet-service", - "version": "1.8.2", + "version": "1.10.0", "workspaces": [ "packages/common", "packages/daemon", "packages/wallet-service" ], "engines": { - "node": ">=18" + "node": ">=22" }, "nohoist": [ "**" @@ -29,12 +29,12 @@ "sequelize-cli": "^6.6.2", "typescript": "^5.8.2" }, - "packageManager": "yarn@4.1.0", + "packageManager": "yarn@4.7.0", "dependencies": { "@aws-sdk/client-apigatewaymanagementapi": "3.540.0", "@aws-sdk/client-lambda": "3.540.0", "@aws-sdk/client-sqs": "3.540.0", - "@hathor/wallet-lib": "1.15.0", + "@hathor/wallet-lib": "2.8.3", "@wallet-service/common": "1.5.0", "bip32": "^4.0.0", "bitcoinjs-lib": "^6.1.5", diff --git a/packages/common/__tests__/events/nftCreationTx.ts b/packages/common/__tests__/events/nftCreationTx.ts index 4fcd98bb..f07cdd15 100644 --- a/packages/common/__tests__/events/nftCreationTx.ts +++ b/packages/common/__tests__/events/nftCreationTx.ts @@ -28,7 +28,7 @@ export const nftCreationTx = { is_voided: false, inputs: [ { - value: 100, + value: 100n, token_data: 0, script: 'dqkUaf+xVJ8uAPML/AzwuSB+2W9/M7qIrA==', decoded: { @@ -43,7 +43,7 @@ export const nftCreationTx = { ], outputs: [ { - value: 1, + value: 1n, token_data: 0, // Decoded script: 5ipfs://QmPCSXNDyPdhU9oQFpxFsNN3nTjg9ZoqESKY5n9Gp1XSJc script: 'NWlwZnM6Ly9RbVBDU1hORHlQZGhVOW9RRnB4RnNOTjNuVGpnOVpvcUVTS1k1bjlHcDFYU0pjrA==', @@ -53,7 +53,7 @@ export const nftCreationTx = { selected_as_input: false, }, { - value: 98, + value: 98n, token_data: 0, script: 'dqkUQcQx/3rV1s5VZXqZPc1dkQbPo6eIrA==', decoded: { @@ -65,7 +65,7 @@ export const nftCreationTx = { spent_by: null, }, { - value: 1, + value: 1n, token_data: 1, script: 'dqkUQcQx/3rV1s5VZXqZPc1dkQbPo6eIrA==', decoded: { @@ -77,7 +77,7 @@ export const nftCreationTx = { spent_by: null, }, { - value: 1, + value: 1n, token_data: 129, script: 'dqkU1YP+t130UoYD+3ys9MYt1zkWeY6IrA==', decoded: { @@ -89,7 +89,7 @@ export const nftCreationTx = { spent_by: null, }, { - value: 2, + value: 2n, token_data: 129, script: 'dqkULlcsARvA+pQS8qytBr6Ryjc/SLeIrA==', decoded: { diff --git a/packages/common/__tests__/utils/nft.utils.test.ts b/packages/common/__tests__/utils/nft.utils.test.ts index 99f2e5e2..6194e038 100644 --- a/packages/common/__tests__/utils/nft.utils.test.ts +++ b/packages/common/__tests__/utils/nft.utils.test.ts @@ -46,7 +46,7 @@ const logger = new Logger(); // Real event data from production const REAL_NFT_EVENT_DATA = { 'hash': '000041f860a327969fa03685ed05cf316fc941708c53801cf81f426ac4a55866', - 'nonce': 257857, + 'nonce': 257857n, 'timestamp': 1741649846, 'signal_bits': 0, 'version': 2, @@ -56,7 +56,7 @@ const REAL_NFT_EVENT_DATA = { 'tx_id': '00000000ba6f3fc01a3e8561f2905c50c98422e7112604a8971bdaba1535e797', 'index': 1, 'spent_output': { - 'value': 4, + 'value': 4n, 'token_data': 0, 'script': 'dqkUWDMJLPqtb9X+jPcBSP6WLg6NIC6IrA==', 'decoded': { @@ -69,13 +69,13 @@ const REAL_NFT_EVENT_DATA = { ], 'outputs': [ { - 'value': 1, + 'value': 1n, 'token_data': 0, 'script': 'C2lwZnM6Ly8xMTExrA==', 'decoded': null }, { - 'value': 2, + 'value': 2n, 'token_data': 0, 'script': 'dqkUFUs/hBsLnxy5Jd94WWV24BCmIhmIrA==', 'decoded': { @@ -85,7 +85,7 @@ const REAL_NFT_EVENT_DATA = { } }, { - 'value': 1, + 'value': 1n, 'token_data': 1, 'script': 'dqkUhM3YhAjNc5p/oqX+yqEYcX+miNmIrA==', 'decoded': { @@ -95,6 +95,7 @@ const REAL_NFT_EVENT_DATA = { } } ], + 'headers': [], 'tokens': [ '000041f860a327969fa03685ed05cf316fc941708c53801cf81f426ac4a55866' ], @@ -378,7 +379,7 @@ describe('invokeNftHandlerLambda', () => { const mLambdaClient = new LambdaClientMock({}); (mLambdaClient.send as jest.Mocked).mockImplementationOnce(async () => expectedLambdaResponse); - const result = await NftUtils.invokeNftHandlerLambda('test-tx-id', 'local', logger); + const result = await NftUtils.invokeNftHandlerLambda('test-tx-id', 'local', 'hathor-wallet-service', logger); // Method should return void expect(result).toBeUndefined(); @@ -404,7 +405,7 @@ describe('invokeNftHandlerLambda', () => { const mLambdaClient = new LambdaClientMock({}); (mLambdaClient.send as jest.Mocked).mockResolvedValueOnce(expectedLambdaResponse); - await expect(NftUtils.invokeNftHandlerLambda('test-tx-id', 'local', logger)) + await expect(NftUtils.invokeNftHandlerLambda('test-tx-id', 'local', 'hathor-wallet-service', logger)) .rejects.toThrow('onNewNftEvent lambda invoke failed for tx: test-tx-id'); // Verify alert was added @@ -423,7 +424,7 @@ describe('invokeNftHandlerLambda', () => { // Disable NFT auto review process.env.NFT_AUTO_REVIEW_ENABLED = 'false'; - const result = await NftUtils.invokeNftHandlerLambda('test-tx-id', 'local', logger); + const result = await NftUtils.invokeNftHandlerLambda('test-tx-id', 'local', 'hathor-wallet-service', logger); // Method should return void expect(result).toBeUndefined(); @@ -475,7 +476,7 @@ describe('transaction transformation compatibility', () => { index: 0, spent_output: { token_data: (1 & hathorLib.constants.TOKEN_INDEX_MASK) + 1, // First token - value: 100, + value: 100n, script: 'script1', decoded: { type: 'P2PKH', @@ -486,7 +487,7 @@ describe('transaction transformation compatibility', () => { }], outputs: [{ token_data: (0 & hathorLib.constants.TOKEN_INDEX_MASK) + 1, // HTR token - value: 100, + value: 100n, script: 'script2', decoded: { type: 'P2PKH', @@ -494,7 +495,8 @@ describe('transaction transformation compatibility', () => { timelock: null, } }], - nonce: 0, + headers: [], + nonce: 0n, signal_bits: 1, timestamp: 0, weight: 18.2, @@ -622,7 +624,7 @@ describe('processNftEvent', () => { // Real event data from production const eventData = { hash: '000041f860a327969fa03685ed05cf316fc941708c53801cf81f426ac4a55866', - nonce: 257857, + nonce: 257857n, timestamp: 1741649846, signal_bits: 0, version: 2, @@ -632,7 +634,7 @@ describe('processNftEvent', () => { tx_id: '00000000ba6f3fc01a3e8561f2905c50c98422e7112604a8971bdaba1535e797', index: 1, spent_output: { - value: 4, + value: 4n, token_data: 0, script: 'dqkUWDMJLPqtb9X+jPcBSP6WLg6NIC6IrA==', decoded: { @@ -645,13 +647,13 @@ describe('processNftEvent', () => { ], outputs: [ { - value: 1, + value: 1n, token_data: 0, script: 'C2lwZnM6Ly8xMTExrA==', decoded: null }, { - value: 2, + value: 2n, token_data: 0, script: 'dqkUFUs/hBsLnxy5Jd94WWV24BCmIhmIrA==', decoded: { @@ -661,7 +663,7 @@ describe('processNftEvent', () => { } }, { - value: 1, + value: 1n, token_data: 1, script: 'dqkUhM3YhAjNc5p/oqX+yqEYcX+miNmIrA==', decoded: { @@ -671,6 +673,7 @@ describe('processNftEvent', () => { } } ], + headers: [], tokens: [ '000041f860a327969fa03685ed05cf316fc941708c53801cf81f426ac4a55866' ], @@ -685,6 +688,7 @@ describe('processNftEvent', () => { const result = await NftUtils.processNftEvent( eventData, 'test-stage', + 'hathor-wallet-service', mockNetwork as unknown as hathorLib.Network, logger ); @@ -715,6 +719,7 @@ describe('processNftEvent', () => { expect(invokeNftLambdaSpy).toHaveBeenCalledWith( eventData.hash, 'test-stage', + 'hathor-wallet-service', logger ); }); @@ -729,6 +734,7 @@ describe('processNftEvent', () => { const result = await NftUtils.processNftEvent( eventData, 'test-stage', + 'hathor-wallet-service', mockNetwork as unknown as hathorLib.Network, logger ); @@ -748,6 +754,7 @@ describe('processNftEvent', () => { const result = await NftUtils.processNftEvent( eventData, 'test-stage', + 'hathor-wallet-service', mockNetwork as unknown as hathorLib.Network, logger ); @@ -773,6 +780,7 @@ describe('processNftEvent', () => { const result = await NftUtils.processNftEvent( eventData, 'test-stage', + 'hathor-wallet-service', mockNetwork as unknown as hathorLib.Network, logger ); @@ -806,6 +814,7 @@ describe('processNftEvent', () => { const result = await NftUtils.processNftEvent( eventData, 'test-stage', + 'hathor-wallet-service', mockNetwork as unknown as hathorLib.Network, logger ); @@ -884,6 +893,7 @@ it('should perform full NFT processing with real event data and no mocks', async const result = await NftUtils.processNftEvent( eventData, 'test-stage', + 'hathor-wallet-service', mockNetwork as unknown as hathorLib.Network, logger ); diff --git a/packages/common/jest.config.js b/packages/common/jest.config.js index c80cb292..e11b9f1f 100644 --- a/packages/common/jest.config.js +++ b/packages/common/jest.config.js @@ -1,5 +1,6 @@ module.exports = { roots: ["/__tests__"], + setupFiles: ['./jestSetup.ts'], testRegex: ".*\\.test\\.ts$", moduleNameMapper: { '^@src/(.*)$': '/src/$1', diff --git a/packages/common/jestSetup.ts b/packages/common/jestSetup.ts new file mode 100644 index 00000000..bc3028f6 --- /dev/null +++ b/packages/common/jestSetup.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. + */ + +import { stopGLLBackgroundTask } from '@hathor/wallet-lib'; +stopGLLBackgroundTask(); + diff --git a/packages/common/package.json b/packages/common/package.json index c8f7a8e7..7d78d5c1 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -8,7 +8,7 @@ "test": "jest --runInBand --collectCoverage --detectOpenHandles --forceExit" }, "peerDependencies": { - "@hathor/wallet-lib": "1.15.0" + "@hathor/wallet-lib": "2.8.3" }, "dependencies": { "@aws-sdk/client-lambda": "3.540.0", diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 55521504..e0376b6f 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -29,7 +29,7 @@ export enum Severity { export interface Transaction { // eslint-disable-next-line camelcase tx_id: string; - nonce: number; + nonce: bigint; timestamp: number; // eslint-disable-next-line camelcase signal_bits: number; @@ -38,6 +38,7 @@ export interface Transaction { parents: string[]; inputs: TxInput[]; outputs: TxOutput[]; + headers?: TxHeader[]; height?: number; voided?: boolean | null; // eslint-disable-next-line camelcase @@ -50,7 +51,7 @@ export interface TxInput { // eslint-disable-next-line camelcase tx_id: string; index: number; - value: number; + value: bigint; // eslint-disable-next-line camelcase token_data: number; script: string; @@ -59,7 +60,7 @@ export interface TxInput { } export interface TxOutput { - value: number; + value: bigint; script: string; token: string; decoded: DecodedOutput; @@ -74,12 +75,22 @@ export interface TxOutputWithIndex extends TxOutput { index: number; } -export interface DecodedOutput { +export type DecodedOutput = { type: string; address: string; timelock: number | null; } +export interface TxNanoHeader { + id: string; + nc_seqnum: number; + nc_id: string; + nc_method: string; + nc_address: string; +} + +export type TxHeader = TxNanoHeader; + export class Authorities { /** * Supporting up to 8 authorities (but we only have mint and melt at the moment) @@ -88,12 +99,12 @@ export class Authorities { array: number[]; - constructor(authorities?: number | number[]) { + constructor(authorities?: bigint | number | number[]) { let tmp: number[] = []; if (authorities instanceof Array) { tmp = authorities; } else if (authorities != null) { - tmp = Authorities.intToArray(authorities); + tmp = Authorities.intToArray(Number(authorities)); } this.array = new Array(Authorities.LENGTH - tmp.length).fill(0).concat(tmp); @@ -190,7 +201,8 @@ export class Authorities { } toJSON(): Record { - const authorities = this.toInteger(); + // TOKEN_MINT_MASK and TOKEN_MELT_MASK are bigint (since they come from the output amount) + const authorities = BigInt(this.toInteger()); return { mint: (authorities & constants.TOKEN_MINT_MASK) > 0, // eslint-disable-line no-bitwise melt: (authorities & constants.TOKEN_MELT_MASK) > 0, // eslint-disable-line no-bitwise @@ -199,19 +211,26 @@ export class Authorities { } export class Balance { - totalAmountSent: number; + totalAmountSent: bigint; - lockedAmount: number; + lockedAmount: bigint; - unlockedAmount: number; + unlockedAmount: bigint; lockedAuthorities: Authorities; unlockedAuthorities: Authorities; - lockExpires: number | null | undefined; + lockExpires: number | null; - constructor(totalAmountSent = 0, unlockedAmount = 0, lockedAmount = 0, lockExpires = null, unlockedAuthorities = null, lockedAuthorities = null) { + constructor( + totalAmountSent = 0n, + unlockedAmount = 0n, + lockedAmount = 0n, + lockExpires: number | null = null, + unlockedAuthorities: Authorities | null = null, + lockedAuthorities: Authorities | null = null + ) { this.totalAmountSent = totalAmountSent; this.unlockedAmount = unlockedAmount; this.lockedAmount = lockedAmount; @@ -225,7 +244,7 @@ export class Balance { * * @returns The total balance */ - total(): number { + total(): bigint { return this.unlockedAmount + this.lockedAmount; } @@ -248,7 +267,6 @@ export class Balance { this.totalAmountSent, this.unlockedAmount, this.lockedAmount, - // @ts-ignore this.lockExpires, this.unlockedAuthorities.clone(), this.lockedAuthorities.clone(), @@ -272,14 +290,12 @@ export class Balance { } 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), @@ -296,7 +312,7 @@ export class TokenBalanceMap { get(tokenId: string): Balance { // if the token is not present, return 0 instead of undefined - return this.map[tokenId] || new Balance(0, 0, 0); + return this.map[tokenId] || new Balance(0n, 0n, 0n); } set(tokenId: string, balance: Balance): void { @@ -335,17 +351,16 @@ export class TokenBalanceMap { * @param tokenBalanceMap - The js object to convert to a TokenBalanceMap * @returns - The new TokenBalanceMap object */ - static fromStringMap(tokenBalanceMap: StringMap>): TokenBalanceMap { + 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, + balance.totalSent as bigint, + balance.unlocked as bigint, + balance.locked as bigint, + balance.lockExpires as number || null, + balance.unlockedAuthorities as Authorities, + balance.lockedAuthorities as Authorities, )); } return obj; @@ -380,22 +395,19 @@ export class TokenBalanceMap { throw new Error('Output has no decoded script'); } const token = output.token; - const value = output.value; + const value = BigInt(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))); + obj.set(token, new Balance(0n, 0n, 0n, output.decoded.timelock, new Authorities(0), new Authorities(output.value))); } else { - // @ts-ignore - obj.set(token, new Balance(value, 0, value, output.decoded.timelock, 0, 0)); + obj.set(token, new Balance(value, 0n, value, output.decoded.timelock, new Authorities(0), new Authorities(0))); } } else if (isAuthority(output.token_data)) { - // @ts-ignore - obj.set(token, new Balance(0, 0, 0, null, new Authorities(output.value), 0)); + obj.set(token, new Balance(0n, 0n, 0n, null, new Authorities(output.value), new Authorities(0))); } else { - obj.set(token, new Balance(value, value, 0, null)); + obj.set(token, new Balance(value, value, 0n, null)); } return obj; @@ -419,18 +431,10 @@ export class TokenBalanceMap { const authorities = new Authorities(input.value); obj.set( token, - new Balance( - 0, - 0, - 0, - null, - // @ts-ignore - authorities.toNegative(), - new Authorities(0), - ), + new Balance(0n, 0n, 0n, null, authorities.toNegative(), new Authorities(0)), ); } else { - obj.set(token, new Balance(0, -input.value, 0, null)); + obj.set(token, new Balance(0n, -BigInt(input.value), 0n, null)); } return obj; } @@ -439,7 +443,7 @@ export class TokenBalanceMap { // The output structure in full node events is similar to TxOutput but with some differences export interface FullNodeOutput extends Omit { // In full node data, decoded can be null - decoded: DecodedOutput | null; + decoded: DecodedOutput | null | {}; } // The input structure in full node events is different - it contains a reference to the spent output diff --git a/packages/common/src/utils/nft.utils.ts b/packages/common/src/utils/nft.utils.ts index 92cae4fb..99651396 100644 --- a/packages/common/src/utils/nft.utils.ts +++ b/packages/common/src/utils/nft.utils.ts @@ -145,7 +145,7 @@ export class NftUtils { * 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, stage: string, logger: Logger): Promise { + static async invokeNftHandlerLambda(txId: string, stage: string, deployPrefix: string, logger: Logger): Promise { // Check for required environment variables if (!process.env.WALLET_SERVICE_LAMBDA_ENDPOINT || !process.env.AWS_REGION) { throw new Error('Environment variables WALLET_SERVICE_LAMBDA_ENDPOINT and AWS_REGION are not set.'); @@ -163,7 +163,7 @@ export class NftUtils { }); // invoke lambda asynchronously to metadata update const command = new InvokeCommand({ - FunctionName: `hathor-wallet-service-${stage}-onNewNftEvent`, + FunctionName: `${deployPrefix}-${stage}-onNewNftEvent`, InvocationType: 'Event', Payload: JSON.stringify({ nftUid: txId }), }); @@ -190,6 +190,7 @@ export class NftUtils { static async processNftEvent( eventData: FullNodeTransaction, stage: string, + deployPrefix: string, network: Network, logger: Logger ): Promise { @@ -215,7 +216,7 @@ export class NftUtils { const txId = eventData.hash; // Invoke the lambda function - await NftUtils.invokeNftHandlerLambda(txId, stage, logger); + await NftUtils.invokeNftHandlerLambda(txId, stage, deployPrefix, logger); return true; } @@ -271,7 +272,8 @@ export class NftUtils { weight: fullNodeData.weight, timestamp: fullNodeData.timestamp, is_voided: !!fullNodeData.voided, - nonce: fullNodeData.nonce, + // XXX: This may have conversion errors but the value is of no consequence + nonce: Number(fullNodeData.nonce), parents: fullNodeData.parents ?? [], }; diff --git a/packages/common/src/utils/wallet.utils.ts b/packages/common/src/utils/wallet.utils.ts index e2edf273..d1fec03d 100644 --- a/packages/common/src/utils/wallet.utils.ts +++ b/packages/common/src/utils/wallet.utils.ts @@ -6,6 +6,7 @@ */ import { constants } from '@hathor/wallet-lib'; +import { DecodedOutput } from '../types'; /** * Checks if a given tokenData has any authority bit set @@ -25,7 +26,7 @@ export const isAuthority = (tokenData: number): boolean => ( * @param requiredKeys - A list of keys to check * @returns true if the decoded object is valid, false otherwise */ -export const isDecodedValid = (decoded: any, requiredKeys: string[] = []): boolean => { +export const isDecodedValid = (decoded: any, requiredKeys: string[] = []): decoded is DecodedOutput => { return (decoded != null && typeof decoded === 'object' && Object.keys(decoded).length > 0) diff --git a/.dockerignore b/packages/daemon/.dockerignore similarity index 100% rename from .dockerignore rename to packages/daemon/.dockerignore diff --git a/packages/daemon/Dockerfile b/packages/daemon/Dockerfile new file mode 100644 index 00000000..0fc152dc --- /dev/null +++ b/packages/daemon/Dockerfile @@ -0,0 +1,83 @@ +# 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. + +# ========================================================================= +# This Dockerfile is used to build and run the Wallet Service Daemon container. +# It requires: +# - A MySQL instance, properly migrated ( see /db/Dockerfile ) +# - A Fullnode instance +# - A Redis instance +# +# The expected image size is about 500MB as of v1.9.0 +# +# See the HathorNetwork / Wallet Lib repository for a live example on how to use this Dockerfile, but in short: +# ws-daemon: +# image: hathornetwork/hathor-wallet-service-sync-daemon +# depends_on: +# ws-migrator: +# condition: service_completed_successfully +# fullnode: +# condition: service_healthy +# mysql: +# condition: service_healthy +# environment: +# ... +# ports: +# - "8081:8081" +# - "8082:8082" +# networks: +# - hathor-privnet + +# Build phase +FROM node:22-alpine AS builder + +WORKDIR /app + +RUN apk update && apk add python3 g++ make py3-setuptools + +COPY . . + +# corepack will use the version of yarn specified in package.json +RUN corepack enable + +# This will install dependencies for all packages, except for the lambdas since +# they are ignored in .dockerignore +RUN yarn install + +RUN yarn workspace sync-daemon run build + +# This will remove all dev dependencies and install production deps only +RUN yarn workspaces focus -A --production + +# Run phase +FROM node:22-alpine AS dev + +WORKDIR /app + +# Copy only the necessary files from the build phase +COPY --from=builder /app . + +WORKDIR /app/packages/daemon/ + +# The script should already be available from the builder stage copy +RUN cp /app/scripts/fetch-fullnode-ids.js ./fetch-fullnode-ids.js +RUN cp /app/scripts/merge-complementary-envs.sh ./merge-complementary-envs.sh +RUN chmod +x ./merge-complementary-envs.sh + +# The daemon could need complementary environment variables dynamically. +# The entrypoint script manages this before actually running the daemon. +ENTRYPOINT ["./merge-complementary-envs.sh"] + +FROM node:22-alpine AS prod + +WORKDIR /app + +# Copy only the necessary files from the build phase +COPY --from=builder /app . + +WORKDIR /app/packages/daemon/ + +CMD ["node", "dist/index.js"] diff --git a/packages/daemon/__tests__/actors/HealthCheckActor.test.ts b/packages/daemon/__tests__/actors/HealthCheckActor.test.ts index 04b50257..5ec26085 100644 --- a/packages/daemon/__tests__/actors/HealthCheckActor.test.ts +++ b/packages/daemon/__tests__/actors/HealthCheckActor.test.ts @@ -3,7 +3,6 @@ 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'); @@ -16,6 +15,7 @@ describe('HealthCheckActor', () => { afterAll(() => { jest.clearAllMocks(); + jest.useRealTimers(); }); it('should not start pinging on initialization', () => { diff --git a/packages/daemon/__tests__/db/index.test.ts b/packages/daemon/__tests__/db/index.test.ts index d61fc123..b5c7c375 100644 --- a/packages/daemon/__tests__/db/index.test.ts +++ b/packages/daemon/__tests__/db/index.test.ts @@ -40,7 +40,8 @@ import { updateTxOutputSpentBy, updateWalletLockedBalance, updateWalletTablesWithTx, - voidTransaction + voidTransaction, + voidAddressTransaction } from '../../src/db'; import { Connection } from 'mysql2/promise'; import { @@ -138,12 +139,12 @@ describe('tx output methods', () => { 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 }, + { value: 5n, address: 'address1', tokenId: 'token1', locked: false }, + { value: 15n, address: 'address1', tokenId: 'token1', locked: false }, + { value: 25n, address: 'address2', tokenId: 'token2', timelock: 500, locked: true }, + { value: 35n, address: 'address2', tokenId: 'token1', locked: false }, // authority utxo - { value: 0b11, address: 'address1', tokenId: 'token1', locked: false, tokenData: 129 }, + { value: 0b11n, address: 'address1', tokenId: 'token1', locked: false, tokenData: 129 }, ]; // empty list should be fine @@ -166,8 +167,8 @@ describe('tx output methods', () => { const { token, decoded } = output; let authorities = 0; if (isAuthority(output.token_data)) { - authorities = value; - value = 0; + authorities = Number(value); + value = 0n; } await expect( checkUtxoTable(mysql, utxos.length, txId, output.index, token, decoded?.address, value, authorities, decoded?.timelock, null, output.locked), @@ -239,8 +240,8 @@ describe('tx output methods', () => { const { token, decoded } = output; let authorities = 0; if (isAuthority(output.token_data)) { - authorities = value; - value = 0; + authorities = Number(value); + value = 0n; } await expect( checkUtxoTable(mysql, utxos.length, txId, index, token, decoded?.address, value, authorities, decoded?.timelock, null, output.locked), @@ -259,7 +260,7 @@ describe('tx output methods', () => { timelock: null, }, script: '', - value: 25, + value: 25n, authorities: 0, timelock: 500, heightlock: null, @@ -300,11 +301,11 @@ describe('tx output methods', () => { 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 }, + { txId, index: 0, tokenId: 'token1', address: 'address1', value: 5n, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null }, + { txId, index: 1, tokenId: 'token1', address: 'address1', value: 15n, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null}, + { txId, index: 2, tokenId: 'token1', address: 'address1', value: 25n, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null }, + { txId, index: 3, tokenId: 'token1', address: 'address1', value: 1n, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null }, + { txId, index: 4, tokenId: 'token1', address: 'address1', value: 3n, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null }, ]; // add to utxo table @@ -332,8 +333,8 @@ describe('tx output methods', () => { 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}, + { txId, index: 0, tokenId: 'token1', address: 'address1', value: 5n, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null }, + { txId, index: 1, tokenId: 'token1', address: 'address1', value: 15n, authorities: 0, timelock: 0, heightlock: null, locked: false, spentBy: null, txProposalIndex: null, txProposalId: null}, ]; // add to utxo table @@ -359,17 +360,17 @@ describe('tx output methods', () => { const txId2 = 'txId2'; const utxos = [ // no locks - { value: 5, address: 'address1', token: 'token1', locked: false }, + { value: 5n, address: 'address1', token: 'token1', locked: false }, // only timelock - { value: 25, address: 'address2', token: 'token2', timelock: 50, locked: false }, + { value: 25n, address: 'address2', token: 'token2', timelock: 50, locked: false }, ]; const utxos2 = [ // only heightlock - { value: 35, address: 'address2', token: 'token1', timelock: null, locked: true }, + { value: 35n, 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 }, + { value: 45n, address: 'address2', token: 'token1', timelock: 100, locked: true }, + { value: 55n, address: 'address2', token: 'token1', timelock: 1000, locked: true }, ]; // add to utxo table @@ -382,15 +383,15 @@ describe('tx output methods', () => { // { 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); + expect(results[0].value).toBe(35n); // 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); + expect([35n, 45n]).toContain(results[0].value); + expect([35n, 45n]).toContain(results[1].value); // fetch on timestamp=100 and heightlock=9. Should return empty results = await getUtxosLockedAtHeight(mysql, 1000, 9); @@ -406,12 +407,12 @@ describe('tx output methods', () => { 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 }, + { value: 5n, address: 'address1', tokenId: 'token1', locked: true }, + { value: 15n, address: 'address1', tokenId: 'token1', locked: true }, + { value: 25n, address: 'address2', tokenId: 'token2', timelock: 100, locked: true }, + { value: 35n, address: 'address2', tokenId: 'token1', timelock: 200, locked: true }, // authority utxo - { value: 0b11, address: 'address1', tokenId: 'token1', timelock: 300, locked: true, tokenData: 129 }, + { value: 0b11n, address: 'address1', tokenId: 'token1', timelock: 300, locked: true, tokenData: 129 }, ]; // empty list should be fine @@ -442,16 +443,16 @@ describe('tx output methods', () => { 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); + expect(unlockedUtxos3[2].authorities).toStrictEqual(Number(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 }, + { value: 5n, address: 'address1', token: 'token1', locked: false }, + { value: 25n, address: 'address2', token: 'token2', timelock: 500, locked: true }, + { value: 35n, address: 'address2', token: 'token1', locked: false }, ]; // add to utxo table @@ -465,7 +466,7 @@ describe('tx output methods', () => { 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); + expect(results[0].value).toBe(25n); }); }); @@ -486,20 +487,20 @@ describe('address and wallet related tests', () => { 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) }, + token1: { unlocked: 10n, locked: 0n }, + token2: { unlocked: 7n, locked: 0n }, + token3: { unlocked: 2n, locked: 0n, unlockedAuthorities: new Authorities(0b01) }, }), - address2: TokenBalanceMap.fromStringMap({ token1: { unlocked: 8, locked: 0, unlockedAuthorities: new Authorities(0b01) } }), + address2: TokenBalanceMap.fromStringMap({ token1: { unlocked: 8n, locked: 0n, 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(checkAddressBalanceTable(mysql, 4, address1, token1, 10n, 0n, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address1, token2, 7n, 0n, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address1, token3, 2n, 0n, null, 1, 0b01, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address2, token1, 8n, 0n, 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); @@ -509,21 +510,21 @@ describe('address and wallet related tests', () => { 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 } }), + address1: TokenBalanceMap.fromStringMap({ token1: { unlocked: -5n, locked: 0n }, + token3: { unlocked: 6n, locked: 0n, unlockedAuthorities: new Authorities([-1]) } }), + address2: TokenBalanceMap.fromStringMap({ token1: { unlocked: 8n, locked: 0n, unlockedAuthorities: new Authorities(0b10) }, + token2: { unlocked: 3n, locked: 0n } }), }; 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); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5n, 0n, null, 2)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token2', 7n, 0n, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token3', 8n, 0n, null, 2, 0, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address2, 'token1', 16n, 0n, null, 2, 0b11, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address2, 'token2', 3n, 0n, 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); @@ -540,10 +541,10 @@ describe('address and wallet related tests', () => { const timestamp3 = 20; const lockExpires = 5000; const addrMap3 = { - address1: TokenBalanceMap.fromStringMap({ token1: { unlocked: 0, locked: 3, lockExpires } }), + address1: TokenBalanceMap.fromStringMap({ token1: { unlocked: 0n, locked: 3n, lockExpires } }), }; await updateAddressTablesWithTx(mysql, txId3, timestamp3, addrMap3); - await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5, 3, lockExpires, 3)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5n, 3n, lockExpires, 3)).resolves.toBe(true); // another tx, with higher timelock const txId4 = 'txId4'; @@ -552,7 +553,7 @@ describe('address and wallet related tests', () => { 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); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5n, 5n, lockExpires, 4)).resolves.toBe(true); // another tx, with lower timelock const txId5 = 'txId5'; @@ -561,7 +562,7 @@ describe('address and wallet related tests', () => { 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); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5n, 7n, lockExpires - 1, 5)).resolves.toBe(true); // We shouldn't throw if the addressBalanceMap is empty: await expect(updateAddressTablesWithTx(mysql, txId5, timestamp5, {})).resolves.not.toThrow(); @@ -584,9 +585,9 @@ describe('address and wallet related tests', () => { 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); + await expect(checkAddressBalanceTable(mysql, 3, addr1, tokenId, 60n, 10n, null, 3, 0b01, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 3, addr2, tokenId, 5n, 0n, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 3, addr1, otherToken, 5n, 5n, 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, [{ @@ -594,7 +595,7 @@ describe('address and wallet related tests', () => { index: 0, tokenId, address: addr1, - value: 0, + value: 0n, authorities: 0b01, timelock: 10000, heightlock: null, @@ -603,7 +604,7 @@ describe('address and wallet related tests', () => { }]); 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); + await expect(checkAddressBalanceTable(mysql, 3, addr1, tokenId, 60n, 10n, null, 3, 0b11, 0b01)).resolves.toBe(true); }); test('updateWalletLockedBalance', async () => { @@ -965,30 +966,30 @@ describe('address and wallet related tests', () => { 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[0].unlockedBalance).toStrictEqual(2n); + expect(addressBalances[0].lockedBalance).toStrictEqual(0n); 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[1].unlockedBalance).toStrictEqual(1n); + expect(addressBalances[1].lockedBalance).toStrictEqual(4n); 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[2].unlockedBalance).toStrictEqual(5n); + expect(addressBalances[2].lockedBalance).toStrictEqual(2n); 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[3].unlockedBalance).toStrictEqual(0n); + expect(addressBalances[3].lockedBalance).toStrictEqual(2n); 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[4].unlockedBalance).toStrictEqual(0n); + expect(addressBalances[4].lockedBalance).toStrictEqual(1n); expect(addressBalances[5].address).toStrictEqual('addr3'); expect(addressBalances[5].tokenId).toStrictEqual('token2'); - expect(addressBalances[5].unlockedBalance).toStrictEqual(10); - expect(addressBalances[5].lockedBalance).toStrictEqual(1); + expect(addressBalances[5].unlockedBalance).toStrictEqual(10n); + expect(addressBalances[5].lockedBalance).toStrictEqual(1n); }); test('fetchAddressTxHistorySum', async () => { @@ -1018,8 +1019,8 @@ describe('address and wallet related tests', () => { const history = await fetchAddressTxHistorySum(mysql, [addr1, addr2]); - expect(history[0].balance).toStrictEqual(60); - expect(history[1].balance).toStrictEqual(50); + expect(history[0].balance).toStrictEqual(60n); + expect(history[1].balance).toStrictEqual(50n); }); }); @@ -1158,12 +1159,12 @@ describe('getTokenSymbols', () => { let tokenIdList = tokensToPersist.map((each: TokenInfo) => each.id); let tokenSymbolMap = await getTokenSymbols(mysql, tokenIdList); - expect(tokenSymbolMap).toBeNull(); + expect(tokenSymbolMap).toStrictEqual({}); tokenIdList = []; tokenSymbolMap = await getTokenSymbols(mysql, tokenIdList); - expect(tokenSymbolMap).toBeNull(); + expect(tokenSymbolMap).toStrictEqual({}); }); }); @@ -1223,10 +1224,11 @@ describe('voidTransaction', () => { }), }; - await voidTransaction(mysql, txId, addressBalance); + await voidTransaction(mysql, txId); + await voidAddressTransaction(mysql, txId, addressBalance, 1); - await expect(checkAddressBalanceTable(mysql, 2, addr1, token2, 1, 0, null, 3)).resolves.toBe(true); - await expect(checkAddressBalanceTable(mysql, 2, addr1, token1, 1, 0, null, 4)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 2, addr1, token2, 1n, 0n, null, 3)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 2, addr1, token1, 1n, 0n, null, 4)).resolves.toBe(true); // Address tx history entry should have been deleted for both tokens: await expect(checkAddressTxHistoryTable(mysql, 0, addr1, txId, token1, -1, 0)).resolves.toBe(true); await expect(checkAddressTxHistoryTable(mysql, 0, addr1, txId, token2, -1, 0)).resolves.toBe(true); @@ -1247,7 +1249,7 @@ describe('voidTransaction', () => { const addressBalance: StringMap = {}; - await expect(voidTransaction(mysql, txId, addressBalance)).resolves.not.toThrow(); + await expect(voidTransaction(mysql, txId)).resolves.not.toThrow(); // Tx should be voided await expect(checkTransactionTable(mysql, 1, txId, 0, constants.BLOCK_VERSION, true, 1)).resolves.toBe(true); }); @@ -1255,7 +1257,7 @@ describe('voidTransaction', () => { it('should throw an error if the transaction is not found in the database', async () => { expect.hasAssertions(); - await expect(voidTransaction(mysql, 'mysterious-transaction', {})).rejects.toThrow('Tried to void a transaction that is not in the database.'); + await expect(voidTransaction(mysql, 'mysterious-transaction')).rejects.toThrow('Tried to void a transaction that is not in the database.'); }); }); diff --git a/packages/daemon/__tests__/guards/guards.test.ts b/packages/daemon/__tests__/guards/guards.test.ts index 77f61376..42ac6a30 100644 --- a/packages/daemon/__tests__/guards/guards.test.ts +++ b/packages/daemon/__tests__/guards/guards.test.ts @@ -1,4 +1,4 @@ -import { Context, Event, FullNodeEventTypes, FullNodeEvent, StandardFullNodeEvent } from '../../src/types'; +import { Context, Event, FullNodeEventTypes, StandardFullNodeEvent } from '../../src/types'; import { metadataIgnore, metadataVoided, @@ -13,6 +13,7 @@ import { unchanged, invalidNetwork, reorgStarted, + hasNewEvents, } from '../../src/guards'; import { EventTypes } from '../../src/types'; @@ -61,7 +62,7 @@ const generateStandardFullNodeEvent = (type: Exclude { expect(websocketDisconnected(mockContext, mockConnectedEvent)).toBe(false); }); }); + +describe('event loss detection guards', () => { + test('hasNewEvents returns true when data.hasNewEvents is true', () => { + const mockEvent = { + data: { + hasNewEvents: true, + events: [{ id: 1 }, { id: 2 }], + }, + }; + + expect(hasNewEvents(mockContext, mockEvent)).toBe(true); + }); + + test('hasNewEvents returns false when data.hasNewEvents is false', () => { + const mockEvent = { + data: { + hasNewEvents: false, + events: [], + }, + }; + + expect(hasNewEvents(mockContext, mockEvent)).toBe(false); + }); + + test('hasNewEvents returns false when data is missing', () => { + const mockEvent = {}; + + expect(hasNewEvents(mockContext, mockEvent)).toBe(false); + }); + + test('hasNewEvents returns false when data is null', () => { + const mockEvent = { + data: null, + }; + + expect(hasNewEvents(mockContext, mockEvent)).toBe(false); + }); + + test('hasNewEvents returns false when hasNewEvents is undefined', () => { + const mockEvent = { + data: { + events: [], + }, + }; + + expect(hasNewEvents(mockContext, mockEvent)).toBe(false); + }); +}); diff --git a/packages/daemon/__tests__/integration/balances.test.ts b/packages/daemon/__tests__/integration/balances.test.ts index be99e9d4..5d0565ac 100644 --- a/packages/daemon/__tests__/integration/balances.test.ts +++ b/packages/daemon/__tests__/integration/balances.test.ts @@ -10,13 +10,27 @@ import { SyncMachine } from '../../src/machines'; import { interpret } from 'xstate'; import { getDbConnection } from '../../src/db'; import { Connection } from 'mysql2/promise'; -import { cleanDatabase, fetchAddressBalances, transitionUntilEvent, validateBalances } from './utils'; +import { + cleanDatabase, + fetchAddressBalances, + fetchWalletBalances, + transitionUntilEvent, + validateBalances, + validateWalletBalances, + performVoidingConsistencyChecks, + validateVoidingConsistency, +} 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 invalidMempoolBalances from './scenario_configs/invalid_mempool_transaction.balances'; import emptyScriptBalances from './scenario_configs/empty_script.balances'; import customScriptBalances from './scenario_configs/custom_script.balances'; +import ncEventsBalances from './scenario_configs/nc_events.balances'; +import transactionVoidingChainBalances from './scenario_configs/transaction_voiding_chain.balances'; +import voidedTokenAuthorityBalances from './scenario_configs/voided_token_authority.balances'; +import singleVoidedCreateTokenTransactionBalances from './scenario_configs/single_voided_create_token_transaction.balances'; +import singleVoidedRegularTransactionBalances from './scenario_configs/single_voided_regular_transaction.balances'; import { DB_NAME, @@ -36,6 +50,16 @@ import { CUSTOM_SCRIPT_LAST_EVENT, EMPTY_SCRIPT_PORT, EMPTY_SCRIPT_LAST_EVENT, + NC_EVENTS_PORT, + NC_EVENTS_LAST_EVENT, + TRANSACTION_VOIDING_CHAIN_PORT, + TRANSACTION_VOIDING_CHAIN_LAST_EVENT, + VOIDED_TOKEN_AUTHORITY_PORT, + VOIDED_TOKEN_AUTHORITY_LAST_EVENT, + SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_PORT, + SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_LAST_EVENT, + SINGLE_VOIDED_REGULAR_TRANSACTION_PORT, + SINGLE_VOIDED_REGULAR_TRANSACTION_LAST_EVENT, } from './config'; jest.mock('../../src/config', () => { @@ -45,9 +69,16 @@ jest.mock('../../src/config', () => { }; }); +jest.mock('../../src/utils/aws', () => { + return { + sendRealtimeTx: jest.fn(), + invokeOnTxPushNotificationRequestedLambda: jest.fn(), + }; +}); + import getConfig from '../../src/config'; -// @ts-ignore +// @ts-expect-error getConfig.mockReturnValue({ NETWORK: 'testnet', SERVICE_NAME: 'daemon-test', @@ -64,34 +95,42 @@ getConfig.mockReturnValue({ DB_USER, DB_PASS, DB_PORT, + ACK_TIMEOUT_MS: 20000, }); -// 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; - } + mysql = await getDbConnection(); + await cleanDatabase(mysql); }); beforeEach(async () => { await cleanDatabase(mysql); }); +afterAll(async () => { + jest.resetAllMocks(); + if (mysql && 'release' in mysql) { + // @ts-expect-error - pooled connection has release method + await mysql.release(); + } +}); + +// Mock checkForMissedEvents since HTTP API is not available in test simulators +jest.spyOn(Services, 'checkForMissedEvents').mockImplementation(async () => ({ + hasNewEvents: false, + events: [], +})); + describe('unvoided transaction scenario', () => { - beforeAll(() => { + beforeAll(async () => { jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); - }); - - afterAll(() => { - jest.resetAllMocks(); + await cleanDatabase(mysql); }); it('should do a full sync and the balances should match', async () => { - // @ts-ignore + // @ts-expect-error getConfig.mockReturnValue({ NETWORK: 'testnet', SERVICE_NAME: 'daemon-test', @@ -108,25 +147,26 @@ describe('unvoided transaction scenario', () => { DB_USER, DB_PASS, DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests }); const machine = interpret(SyncMachine); - // @ts-ignore + // @ts-expect-error await transitionUntilEvent(mysql, machine, UNVOIDED_SCENARIO_LAST_EVENT); const addressBalances = await fetchAddressBalances(mysql); - // @ts-ignore - expect(validateBalances(addressBalances, unvoidedScenarioBalances)); - }); + await expect(validateBalances(addressBalances, unvoidedScenarioBalances.addressBalances)).resolves.not.toThrow(); + }, 30000); }); describe('reorg scenario', () => { - beforeAll(() => { + beforeAll(async () => { jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + await cleanDatabase(mysql); }); it('should do a full sync and the balances should match', async () => { - // @ts-ignore + // @ts-expect-error getConfig.mockReturnValue({ NETWORK: 'testnet', SERVICE_NAME: 'daemon-test', @@ -143,25 +183,26 @@ describe('reorg scenario', () => { DB_USER, DB_PASS, DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests }); const machine = interpret(SyncMachine); - // @ts-ignore + // @ts-expect-error await transitionUntilEvent(mysql, machine, REORG_SCENARIO_LAST_EVENT); const addressBalances = await fetchAddressBalances(mysql); - // @ts-ignore - expect(validateBalances(addressBalances, reorgScenarioBalances)); - }); + await expect(validateBalances(addressBalances, reorgScenarioBalances.addressBalances)).resolves.not.toThrow(); + }, 30000); }); describe('single chain blocks and transactions scenario', () => { - beforeAll(() => { + beforeAll(async () => { jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + await cleanDatabase(mysql); }); it('should do a full sync and the balances should match', async () => { - // @ts-ignore + // @ts-expect-error getConfig.mockReturnValue({ NETWORK: 'testnet', SERVICE_NAME: 'daemon-test', @@ -178,25 +219,26 @@ describe('single chain blocks and transactions scenario', () => { DB_USER, DB_PASS, DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests }); const machine = interpret(SyncMachine); - // @ts-ignore + // @ts-expect-error await transitionUntilEvent(mysql, machine, SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT); const addressBalances = await fetchAddressBalances(mysql); - // @ts-ignore - expect(validateBalances(addressBalances, singleChainBlocksAndTransactionsBalances)); - }); + await expect(validateBalances(addressBalances, singleChainBlocksAndTransactionsBalances.addressBalances)).resolves.not.toThrow(); + }, 30000); }); describe('invalid mempool transactions scenario', () => { - beforeAll(() => { + beforeAll(async () => { jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + await cleanDatabase(mysql); }); it('should do a full sync and the balances should match', async () => { - // @ts-ignore + // @ts-expect-error getConfig.mockReturnValue({ NETWORK: 'testnet', SERVICE_NAME: 'daemon-test', @@ -213,25 +255,26 @@ describe('invalid mempool transactions scenario', () => { DB_USER, DB_PASS, DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests }); const machine = interpret(SyncMachine); - // @ts-ignore + // @ts-expect-error await transitionUntilEvent(mysql, machine, INVALID_MEMPOOL_TRANSACTION_LAST_EVENT); const addressBalances = await fetchAddressBalances(mysql); - // @ts-ignore - expect(validateBalances(addressBalances, invalidMempoolBalances)); - }); + await expect(validateBalances(addressBalances, invalidMempoolBalances.addressBalances)).resolves.not.toThrow(); + }, 30000); }); describe('custom script scenario', () => { - beforeAll(() => { + beforeAll(async () => { jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + await cleanDatabase(mysql); }); it('should do a full sync and the balances should match', async () => { - // @ts-ignore + // @ts-expect-error getConfig.mockReturnValue({ NETWORK: 'testnet', SERVICE_NAME: 'daemon-test', @@ -248,25 +291,26 @@ describe('custom script scenario', () => { DB_USER, DB_PASS, DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests }); const machine = interpret(SyncMachine); - // @ts-ignore + // @ts-expect-error await transitionUntilEvent(mysql, machine, CUSTOM_SCRIPT_LAST_EVENT); const addressBalances = await fetchAddressBalances(mysql); - // @ts-ignore - expect(validateBalances(addressBalances, customScriptBalances)); - }); + await expect(validateBalances(addressBalances, customScriptBalances.addressBalances)).resolves.not.toThrow(); + }, 30000); }); describe('empty script scenario', () => { - beforeAll(() => { + beforeAll(async () => { jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + await cleanDatabase(mysql); }); it('should do a full sync and the balances should match', async () => { - // @ts-ignore + // @ts-expect-error getConfig.mockReturnValue({ NETWORK: 'testnet', SERVICE_NAME: 'daemon-test', @@ -283,15 +327,564 @@ describe('empty script scenario', () => { DB_USER, DB_PASS, DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests }); const machine = interpret(SyncMachine); - // @ts-ignore + // @ts-expect-error await transitionUntilEvent(mysql, machine, EMPTY_SCRIPT_LAST_EVENT); const addressBalances = await fetchAddressBalances(mysql); + await expect(validateBalances(addressBalances, emptyScriptBalances.addressBalances)).resolves.not.toThrow(); + }, 30000); +}); + +describe('nc events scenario', () => { + beforeAll(async () => { + jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + await cleanDatabase(mysql); + }); + + it('should do a full sync and the balances should match', async () => { + // @ts-ignore + getConfig.mockReturnValue({ + NETWORK: 'testnet', + 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: 'unittests', + FULLNODE_HOST: `127.0.0.1:${NC_EVENTS_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests + }); + + const machine = interpret(SyncMachine); + + // @ts-ignore + await transitionUntilEvent(mysql, machine, NC_EVENTS_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + + await expect(validateBalances(addressBalances, ncEventsBalances.addressBalances)).resolves.not.toThrow(); + }, 30000); +}); + +describe('transaction voiding chain scenario', () => { + beforeAll(async () => { + jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + await cleanDatabase(mysql); + }); + + it('should do a full sync and the balances should match after voiding chain', async () => { // @ts-ignore - expect(validateBalances(addressBalances, emptyScriptBalances)); + getConfig.mockReturnValue({ + NETWORK: 'testnet', + 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: 'unittests', + FULLNODE_HOST: `127.0.0.1:${TRANSACTION_VOIDING_CHAIN_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests + }); + + const machine = interpret(SyncMachine); + // @ts-ignore + await transitionUntilEvent(mysql, machine, TRANSACTION_VOIDING_CHAIN_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + + await expect(validateBalances(addressBalances, transactionVoidingChainBalances.addressBalances)).resolves.not.toThrow(); + + // Validate transaction voiding consistency + const voidingChecks = await performVoidingConsistencyChecks(mysql, { + transactions: [ + { txId: '404eeba6f3658028722e665684c42a0c28c3a44b190426971e012c5030bf1903', expectedVoided: false }, // tx1 + { txId: '4412a1f718b51c054193cc0df2d1dd9d16e82684be17760d7169aa6fb22d5ea2', expectedVoided: false }, // tx2 + { txId: '06b20fd4258ae965137203d4e1fd7df7b69e775e5d3f4a4568d1161343b91f02', expectedVoided: true }, // spending_tx (voided) + { txId: 'ada5e728ffd680238306b510899629099d6bbb58a8811b042c249a236f9640cc', expectedVoided: false }, // voiding_tx + ], + utxos: [ + // Spending transaction (voided) UTXOs should be marked as voided + { txId: '06b20fd4258ae965137203d4e1fd7df7b69e775e5d3f4a4568d1161343b91f02', index: 0, expectedValue: 5900, expectedVoided: true, expectedSpentBy: null }, + { txId: '06b20fd4258ae965137203d4e1fd7df7b69e775e5d3f4a4568d1161343b91f02', index: 1, expectedValue: 500, expectedVoided: true, expectedSpentBy: null }, + + // Voiding transaction (valid) UTXOs should not be voided + { txId: 'ada5e728ffd680238306b510899629099d6bbb58a8811b042c249a236f9640cc', index: 0, expectedValue: 5900, expectedVoided: false, expectedSpentBy: null }, + { txId: 'ada5e728ffd680238306b510899629099d6bbb58a8811b042c249a236f9640cc', index: 1, expectedValue: 500, expectedVoided: false, expectedSpentBy: null }, + ], + }); + + // Validate consistency + validateVoidingConsistency(voidingChecks); + }, 30000); // 30 second timeout for transaction voiding chain test +}); + +describe('voided token authority scenario', () => { + + const initializeWallet = async (mysql: Connection): Promise => { + // Insert wallet records + const walletSQL = ` + INSERT INTO wallet ( + id, + xpubkey, + status, + max_gap, + created_at, + ready_at, + retry_count, + auth_xpubkey, + last_used_address_index + ) VALUES + ( + 'deafbeef', + 'xpub6F81iNtH5HVknoJ65cK2XAGA5F3okdJK7WHwVAAPZnSir2sfwbhvB9ffNKQ4wLor75QxPe9p12tqt8xUZSG8i8AAPMpkFho7fbWkBJQ5s1x', + 'ready', + 20, + UNIX_TIMESTAMP(), + UNIX_TIMESTAMP(), + 0, + 'xpub6F81iNtH5HVknoJ65cK2XAGA5F3okdJK7WHwVAAPZnSir2sfwbhvB9ffNKQ4wLor75QxPe9p12tqt8xUZSG8i8AAPMpkFho7fbWkBJQ5s1x', + -1 + ), + ( + 'cafecafe', + 'xpub6F81iNtH5HVknoJ65cK2XAGA5F3okdJK7WHwVAAPZnSir2sfwbhvB9ffNKQ4wLor75QxPe9p12tqt8xUZSG8i8AAPMpkFho7fbWkBJQ5s1x', + 'ready', + 20, + UNIX_TIMESTAMP(), + UNIX_TIMESTAMP(), + 0, + 'xpub6F81iNtH5HVknoJ65cK2XAGA5F3okdJK7WHwVAAPZnSir2sfwbhvB9ffNKQ4wLor75QxPe9p12tqt8xUZSG8i8AAPMpkFho7fbWkBJQ5s1x', + -1 + )`; + + // Insert address records - all addresses with the same wallet_id + const addressSQL = ` + INSERT INTO address (address, \`index\`, wallet_id, transactions, seqnum) VALUES + ('HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh', 0, 'deafbeef', 0, 0), + ('HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns', 0, 'cafecafe', 1, 0), + ('HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs', 1, 'deafbeef', 21, 0), + ('HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ', 2, 'deafbeef', 1, 0)`; + + await mysql.query(walletSQL); + await mysql.query(addressSQL); + }; + + beforeAll(async () => { + jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + await cleanDatabase(mysql); + }); + + afterAll(async () => { + // Clean up wallet data after this test to prevent affecting other tests + await cleanDatabase(mysql); }); + + it('should do a full sync and the balances should match after voiding token authority', async () => { + // @ts-ignore + getConfig.mockReturnValue({ + NETWORK: 'testnet', + 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: 'unittests', + FULLNODE_HOST: `127.0.0.1:${VOIDED_TOKEN_AUTHORITY_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests + }); + + // Initialize wallet before processing events + await initializeWallet(mysql); + + const machine = interpret(SyncMachine); + + // @ts-ignore + await transitionUntilEvent(mysql, machine, VOIDED_TOKEN_AUTHORITY_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + const walletBalances = await fetchWalletBalances(mysql); + + await expect(validateBalances(addressBalances, voidedTokenAuthorityBalances.addressBalances)).resolves.not.toThrow(); + + // Validate wallet balances + await expect(validateWalletBalances(walletBalances, voidedTokenAuthorityBalances.walletBalances)).resolves.not.toThrow(); + + // Validate transaction voiding consistency + const voidingChecks = await performVoidingConsistencyChecks(mysql, { + transactions: [ + { txId: 'efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539', expectedVoided: false }, + { txId: 'deabafb4b3e87a98ee60c5190c63de10809811818f663c040b29eaa0a92463af', expectedVoided: true }, + { txId: '4f5625892f602e191c22fd0aa533bea7764e93e3a03dc498d30cb23932eb462c', expectedVoided: false }, + ], + utxos: [ + // No specific UTXO checks needed for this scenario + ], + }); + + // Validate consistency + validateVoidingConsistency(voidingChecks); + }, 30000); // 30 second timeout for voided token authority test +}); + +describe('single voided create token transaction scenario', () => { + const initializeWallet = async (mysql: Connection): Promise => { + // Insert wallet records + const walletSQL = ` + INSERT INTO wallet ( + id, + xpubkey, + status, + max_gap, + created_at, + ready_at, + retry_count, + auth_xpubkey, + last_used_address_index + ) VALUES + ( + 'test-wallet-voided-token', + 'xpub6F81iNtH5HVknoJ65cK2XAGA5F3okdJK7WHwVAAPZnSir2sfwbhvB9ffNKQ4wLor75QxPe9p12tqt8xUZSG8i8AAPMpkFho7fbWkBJQ5s1x', + 'ready', + 20, + UNIX_TIMESTAMP(), + UNIX_TIMESTAMP(), + 0, + 'xpub6F81iNtH5HVknoJ65cK2XAGA5F3okdJK7WHwVAAPZnSir2sfwbhvB9ffNKQ4wLor75QxPe9p12tqt8xUZSG8i8AAPMpkFho7fbWkBJQ5s1x', + -1 + )`; + + // Insert address records that will receive the voided token + const addressSQL = ` + INSERT INTO address (address, \`index\`, wallet_id, transactions, seqnum) VALUES + ('HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs', 0, 'test-wallet-voided-token', 0, 0)`; + + await mysql.query(walletSQL); + await mysql.query(addressSQL); + }; + + beforeAll(async () => { + jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + await cleanDatabase(mysql); + }); + + it('should do a full sync and the balances should match', async () => { + // @ts-expect-error + getConfig.mockReturnValue({ + NETWORK: 'testnet', + 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: 'unittests', + FULLNODE_HOST: `127.0.0.1:${SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests + }); + + const machine = interpret(SyncMachine); + + // @ts-expect-error + await transitionUntilEvent(mysql, machine, SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + await expect(validateBalances(addressBalances, singleVoidedCreateTokenTransactionBalances.addressBalances)).resolves.not.toThrow(); + }, 30000); + + it('addresses_balance and address_tx_history row length must match after a void transaction scenario', async () => { + // @ts-expect-error + getConfig.mockReturnValue({ + NETWORK: 'testnet', + 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: 'unittests', + FULLNODE_HOST: `127.0.0.1:${SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests + }); + + // Initialize wallet before processing events + await initializeWallet(mysql); + + const machine = interpret(SyncMachine); + + // @ts-expect-error + await transitionUntilEvent(mysql, machine, SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_LAST_EVENT); + + const voidedTokenId = 'efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539'; + + // Check for addresses that have balances for a specific token + const addressBalanceResults = 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 token_id = ? + GROUP BY token_id + ORDER BY token_id + `, [voidedTokenId]); + + // Check for transaction history for the same tokens (excluding voided) + const txHistoryResults = await mysql.query(` + SELECT token_id, + SUM(balance) AS balance, + COUNT(DISTINCT tx_id) AS transactions + FROM address_tx_history + WHERE voided = FALSE + AND token_id = ? + GROUP BY token_id + ORDER BY token_id + `, [voidedTokenId]); + + // Cast to array to access length property + const addressRows = addressBalanceResults[0] as any[]; + const txHistoryRows = txHistoryResults[0] as any[]; + + expect(addressRows.length).toEqual(txHistoryRows.length); + + // Verify that the voided token was removed from the token table + const tokenResults = await mysql.query( + 'SELECT * FROM token WHERE id = ?', + [voidedTokenId] + ); + + // Token should not exist in the database after being voided + expect(tokenResults[0]).toHaveLength(0); + + // Verify that the wallet_balance table doesn't contain the voided token + const walletBalanceResults = await mysql.query( + 'SELECT * FROM wallet_balance WHERE token_id = ?', + [voidedTokenId] + ); + + // Wallet balance should not exist for the voided token + expect(walletBalanceResults[0]).toHaveLength(0); + }, 30000); +}); + +describe('single voided regular transaction scenario', () => { + const initializeWallet = async (mysql: Connection): Promise => { + // Insert wallet records + const walletSQL = ` + INSERT INTO wallet ( + id, + xpubkey, + status, + max_gap, + created_at, + ready_at, + retry_count, + auth_xpubkey, + last_used_address_index + ) VALUES + ( + 'test-wallet-voided-regular', + 'xpub6F81iNtH5HVknoJ65cK2XAGA5F3okdJK7WHwVAAPZnSir2sfwbhvB9ffNKQ4wLor75QxPe9p12tqt8xUZSG8i8AAPMpkFho7fbWkBJQ5s1x', + 'ready', + 20, + UNIX_TIMESTAMP(), + UNIX_TIMESTAMP(), + 0, + 'xpub6F81iNtH5HVknoJ65cK2XAGA5F3okdJK7WHwVAAPZnSir2sfwbhvB9ffNKQ4wLor75QxPe9p12tqt8xUZSG8i8AAPMpkFho7fbWkBJQ5s1x', + -1 + )`; + + // Insert address record for the voided transaction address + const addressSQL = ` + INSERT INTO address (address, \`index\`, wallet_id, transactions, seqnum) VALUES + ('HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh', 0, 'test-wallet-voided-regular', 0, 0)`; + + await mysql.query(walletSQL); + await mysql.query(addressSQL); + }; + + beforeAll(async () => { + jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + await cleanDatabase(mysql); + }); + + it('should do a full sync and the balances should match', async () => { + // @ts-expect-error + getConfig.mockReturnValue({ + NETWORK: 'testnet', + 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: 'unittests', + FULLNODE_HOST: `127.0.0.1:${SINGLE_VOIDED_REGULAR_TRANSACTION_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests + }); + + const machine = interpret(SyncMachine); + + // @ts-expect-error + await transitionUntilEvent(mysql, machine, SINGLE_VOIDED_REGULAR_TRANSACTION_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + await expect(validateBalances(addressBalances, singleVoidedRegularTransactionBalances.addressBalances)).resolves.not.toThrow(); + }, 30000); + + it('addresses_balance and address_tx_history row length must match after a void transaction scenario', async () => { + // @ts-expect-error + getConfig.mockReturnValue({ + NETWORK: 'testnet', + 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: 'unittests', + FULLNODE_HOST: `127.0.0.1:${SINGLE_VOIDED_REGULAR_TRANSACTION_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests + }); + + const machine = interpret(SyncMachine); + + // @ts-expect-error + await transitionUntilEvent(mysql, machine, SINGLE_VOIDED_REGULAR_TRANSACTION_LAST_EVENT); + + const voidedAddress = 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh'; + + // Check for address balances for the specific address + const addressBalanceResults = await mysql.query(` + SELECT address, + COUNT(*) AS balance_rows + FROM address_balance + WHERE address = ? + GROUP BY address + ORDER BY address + `, [voidedAddress]); + + // Check for transaction history for the same address (excluding voided) + const txHistoryResults = await mysql.query(` + SELECT address, + COUNT(*) AS history_rows + FROM address_tx_history + WHERE voided = FALSE + AND address = ? + GROUP BY address + ORDER BY address + `, [voidedAddress]); + + // Cast to array to access length property + const addressRows = addressBalanceResults[0] as any[]; + const txHistoryRows = txHistoryResults[0] as any[]; + + expect(addressRows.length).toEqual(txHistoryRows.length); + }, 30000); + + it('wallet_balance and wallet_tx_history row length must match after a void transaction scenario', async () => { + // @ts-expect-error + getConfig.mockReturnValue({ + NETWORK: 'testnet', + 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: 'unittests', + FULLNODE_HOST: `127.0.0.1:${SINGLE_VOIDED_REGULAR_TRANSACTION_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + ACK_TIMEOUT_MS: 300000, // 5 minutes - long enough to not interfere with tests + }); + + // Initialize wallet before processing events + await initializeWallet(mysql); + + const machine = interpret(SyncMachine); + + // @ts-expect-error + await transitionUntilEvent(mysql, machine, SINGLE_VOIDED_REGULAR_TRANSACTION_LAST_EVENT); + + const walletId = 'test-wallet-voided-regular'; + + // Check for wallet balances for the specific wallet + const walletBalanceResults = await mysql.query(` + SELECT wallet_id, + COUNT(*) AS balance_rows + FROM wallet_balance + WHERE wallet_id = ? + GROUP BY wallet_id + ORDER BY wallet_id + `, [walletId]); + + // Check for wallet transaction history for the same wallet (excluding voided) + const walletTxHistoryResults = await mysql.query(` + SELECT wallet_id, + COUNT(*) AS history_rows + FROM wallet_tx_history + WHERE voided = FALSE + AND wallet_id = ? + GROUP BY wallet_id + ORDER BY wallet_id + `, [walletId]); + + // Cast to array to access length property + const walletBalanceRows = walletBalanceResults[0] as any[]; + const walletTxHistoryRows = walletTxHistoryResults[0] as any[]; + + expect(walletBalanceRows.length).toEqual(walletTxHistoryRows.length); + }, 30000); }); diff --git a/packages/daemon/__tests__/integration/config.ts b/packages/daemon/__tests__/integration/config.ts index 2ffa6812..8a77c6be 100644 --- a/packages/daemon/__tests__/integration/config.ts +++ b/packages/daemon/__tests__/integration/config.ts @@ -11,7 +11,7 @@ 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; +export const UNVOIDED_SCENARIO_LAST_EVENT = 39; // reorg export const REORG_SCENARIO_PORT = 8082; @@ -34,6 +34,21 @@ export const CUSTOM_SCRIPT_LAST_EVENT = 37; export const EMPTY_SCRIPT_PORT = 8087; export const EMPTY_SCRIPT_LAST_EVENT = 37; +export const NC_EVENTS_PORT = 8088; +export const NC_EVENTS_LAST_EVENT = 36; + +export const TRANSACTION_VOIDING_CHAIN_PORT = 8089; +export const TRANSACTION_VOIDING_CHAIN_LAST_EVENT = 52; + +export const VOIDED_TOKEN_AUTHORITY_PORT = 8090; +export const VOIDED_TOKEN_AUTHORITY_LAST_EVENT = 66; + +export const SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_PORT = 8091; +export const SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_LAST_EVENT = 50; + +export const SINGLE_VOIDED_REGULAR_TRANSACTION_PORT = 8092; +export const SINGLE_VOIDED_REGULAR_TRANSACTION_LAST_EVENT = 60; + export const SCENARIOS = [ 'UNVOIDED_SCENARIO', 'REORG_SCENARIO', @@ -41,4 +56,9 @@ export const SCENARIOS = [ 'INVALID_MEMPOOL_TRANSACTION', 'EMPTY_SCRIPT', 'CUSTOM_SCRIPT', + 'NC_EVENTS', + 'TRANSACTION_VOIDING_CHAIN', + 'VOIDED_TOKEN_AUTHORITY', + 'SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION', + 'SINGLE_VOIDED_REGULAR_TRANSACTION', ]; diff --git a/packages/daemon/__tests__/integration/scenario_configs/custom_script.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/custom_script.balances.ts index 6cada8af..e5a5ca5c 100644 --- a/packages/daemon/__tests__/integration/scenario_configs/custom_script.balances.ts +++ b/packages/daemon/__tests__/integration/scenario_configs/custom_script.balances.ts @@ -1,16 +1,19 @@ export default { - 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ': 100000000000, - 'HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns': 6400, - 'HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ': 6400, - 'HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu': 6400, - 'HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW': 6400, - 'HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26': 6400, - 'HQijr325t63VJFdc4vYkaTyd87oeBLpSed': 6400, - 'H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR': 6400, - 'HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ': 6400, - 'HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8': 6400, - 'HJPSMHCFv2dRb78wZPMsAzwLQHSkBpfuLn': 6400, - 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh': 1000, - 'H9hHteu9QdAS5p6X743Mpfue6G19rV9GeY': 5400, - 'HSd6PqXesUmHHv6MoN24aUiMuw7Pdcxrwk': 6400 + addressBalances: { + 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ:00': { unlockedBalance: 0n, lockedBalance: 100000000000n }, + 'HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HQijr325t63VJFdc4vYkaTyd87oeBLpSed:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HJPSMHCFv2dRb78wZPMsAzwLQHSkBpfuLn:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh:00': { unlockedBalance: 1000n, lockedBalance: 0n }, + 'H9hHteu9QdAS5p6X743Mpfue6G19rV9GeY:00': { unlockedBalance: 5400n, lockedBalance: 0n }, + 'HSd6PqXesUmHHv6MoN24aUiMuw7Pdcxrwk:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + }, + walletBalances: {}, } diff --git a/packages/daemon/__tests__/integration/scenario_configs/empty_script.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/empty_script.balances.ts index e59f3324..a6343581 100644 --- a/packages/daemon/__tests__/integration/scenario_configs/empty_script.balances.ts +++ b/packages/daemon/__tests__/integration/scenario_configs/empty_script.balances.ts @@ -1,16 +1,19 @@ export default { - 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ': 100000000000, - 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh': 6400, - 'HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns': 6400, - 'HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ': 6400, - 'HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu': 6400, - 'HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW': 6400, - 'HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26': 6400, - 'HQijr325t63VJFdc4vYkaTyd87oeBLpSed': 6400, - 'H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR': 6400, - 'HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ': 6400, - 'HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8': 6400, - 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs': 1000, - 'HNqTfEASfdx7H4vMUGzfD2HyD3GeKuxjTJ': 5400, - 'HRH8Wbmr1A3BrLswSBhvVE4hhsv4jUdyVA': 6400 + addressBalances: { + 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ:00': { unlockedBalance: 0n, lockedBalance: 100000000000n }, + 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HQijr325t63VJFdc4vYkaTyd87oeBLpSed:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs:00': { unlockedBalance: 1000n, lockedBalance: 0n }, + 'HNqTfEASfdx7H4vMUGzfD2HyD3GeKuxjTJ:00': { unlockedBalance: 5400n, lockedBalance: 0n }, + 'HRH8Wbmr1A3BrLswSBhvVE4hhsv4jUdyVA:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + }, + walletBalances: {}, }; diff --git a/packages/daemon/__tests__/integration/scenario_configs/invalid_mempool_transaction.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/invalid_mempool_transaction.balances.ts index 70c28ab3..13725a9d 100644 --- a/packages/daemon/__tests__/integration/scenario_configs/invalid_mempool_transaction.balances.ts +++ b/packages/daemon/__tests__/integration/scenario_configs/invalid_mempool_transaction.balances.ts @@ -1,15 +1,18 @@ export default { - 'HNqTfEASfdx7H4vMUGzfD2HyD3GeKuxjTJ': 0, - 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ': 100000000000, - 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh': 6400, - 'HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns': 6400, - 'HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ': 6400, - 'HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu': 6400, - 'HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW': 6400, - 'HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26': 6400, - 'HQijr325t63VJFdc4vYkaTyd87oeBLpSed': 6400, - 'H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR': 6400, - 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs': 6400, - 'HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ': 0, - 'HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8': 0 + addressBalances: { + 'HNqTfEASfdx7H4vMUGzfD2HyD3GeKuxjTJ:00': { unlockedBalance: 0n, lockedBalance: 0n }, + 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ:00': { unlockedBalance: 0n, lockedBalance: 100000000000n }, + 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HQijr325t63VJFdc4vYkaTyd87oeBLpSed:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs:00': { unlockedBalance: 6400n, lockedBalance: 0n }, + 'HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ:00': { unlockedBalance: 0n, lockedBalance: 0n }, + 'HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8:00': { unlockedBalance: 0n, lockedBalance: 0n }, + }, + walletBalances: {}, } diff --git a/packages/daemon/__tests__/integration/scenario_configs/nc_events.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/nc_events.balances.ts new file mode 100644 index 00000000..1f620012 --- /dev/null +++ b/packages/daemon/__tests__/integration/scenario_configs/nc_events.balances.ts @@ -0,0 +1,22 @@ +export default { + addressBalances: { + "H92QQ83Ldm8Sgj6kT8ebu2CmqtZrvhZb6k:00": { unlockedBalance: 0n, lockedBalance: 0n }, + "HAoH1xLZXdDByVsBRUcz9t5GeGDoEfMF2H:00": { unlockedBalance: 1n, lockedBalance: 0n }, + "HEV1qK3dZDuXZn4rUTHZvfsU3L78usLh6u:00": { unlockedBalance: 0n, lockedBalance: 6400n }, + "HF6XLDMZVA5KjJejBxDNeg1j8isXpPUVpU:00": { unlockedBalance: 0n, lockedBalance: 6400n }, + "HFnTiBtKmriJ4iFG9VBDZv6Te134b9DMmZ:00": { unlockedBalance: 0n, lockedBalance: 6400n }, + "HHZsdpy6U6vy4DPaAwBT5jUWLvYbSefQ7Z:00": { unlockedBalance: 0n, lockedBalance: 0n }, + "HKpb6ABejTCFWG5nFrAVEvVcSrRuFgFnB4:00": { unlockedBalance: 99999999996n, lockedBalance: 0n }, + "HKZHo7yZK49P1EqT43awxqSwiEbH1SsQVZ:00": { unlockedBalance: 1n, lockedBalance: 0n }, + "HM8RTLw6yfyi7ie7o89LNWhKhh8muocLXQ:00": { unlockedBalance: 0n, lockedBalance: 0n }, + "HNt5kvtThfTkffh1Xj8fxZNWkzTbQTjfvi:00": { unlockedBalance: 0n, lockedBalance: 0n }, + "HPYZgEMAy1yEwk8e6ESxEaszyLF58Keqy5:00": { unlockedBalance: 0n, lockedBalance: 0n }, + "HRhC8zuNtN8BLgGyK8NqYM24HwjA9w45UQ:00": { unlockedBalance: 1n, lockedBalance: 0n }, + "HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ:00": { unlockedBalance: 0n, lockedBalance: 0n }, + "HRZPAkRv8SAmSaQi2aSr6rfWQCUqoSydgr:00": { unlockedBalance: 0n, lockedBalance: 0n }, + "HUxVygk7LBT5HeRxeneEoAWFNGCMg382ZD:00": { unlockedBalance: 0n, lockedBalance: 0n }, + "HVcaqHL5e471jp7gRm7sBYmSMr3K1YQVp1:00": { unlockedBalance: 0n, lockedBalance: 0n }, + "HVCGqDBHXkdhghtaS2v4XKqfixui3HeYs1:00": { unlockedBalance: 1n, lockedBalance: 0n }, + }, + walletBalances: {}, +}; diff --git a/packages/daemon/__tests__/integration/scenario_configs/reorg.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/reorg.balances.ts index 3dffad2d..fe048735 100644 --- a/packages/daemon/__tests__/integration/scenario_configs/reorg.balances.ts +++ b/packages/daemon/__tests__/integration/scenario_configs/reorg.balances.ts @@ -1,6 +1,9 @@ export default { - 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ': 100000000000, - 'HFyF1jYJP9FXfiC3LRqf3q4768TBL1rxbn': 6400, - 'HMbS5P3NTLQ5oR5TfLNvAkeQ7L8MPn9VM3': 6400, - 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs': 0, -} + addressBalances: { + 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ:00': { unlockedBalance: 0n, lockedBalance: 100000000000n }, + 'HFyF1jYJP9FXfiC3LRqf3q4768TBL1rxbn:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HMbS5P3NTLQ5oR5TfLNvAkeQ7L8MPn9VM3:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs:00': { unlockedBalance: 0n, lockedBalance: 0n }, + }, + walletBalances: {}, +}; 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 index b0422a90..b603bf5a 100644 --- 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 @@ -1,16 +1,20 @@ 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, + addressBalances: { + 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HQijr325t63VJFdc4vYkaTyd87oeBLpSed:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs:00': { unlockedBalance: 3000n, lockedBalance: 0n }, + 'HRH8Wbmr1A3BrLswSBhvVE4hhsv4jUdyVA:00': { unlockedBalance: 3400n, lockedBalance: 0n }, + 'HSd6PqXesUmHHv6MoN24aUiMuw7Pdcxrwk:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HNqTfEASfdx7H4vMUGzfD2HyD3GeKuxjTJ:00': { unlockedBalance: 0n, lockedBalance: 0n }, + 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ:00': { unlockedBalance: 0n, lockedBalance: 100000000000n }, + }, + walletBalances: {}, } diff --git a/packages/daemon/__tests__/integration/scenario_configs/single_voided_create_token_transaction.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/single_voided_create_token_transaction.balances.ts new file mode 100644 index 00000000..61fa8e56 --- /dev/null +++ b/packages/daemon/__tests__/integration/scenario_configs/single_voided_create_token_transaction.balances.ts @@ -0,0 +1,18 @@ +/** + * 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 { + addressBalances: { + 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh:00': { unlockedBalance: 6400n, lockedBalance: 12800n }, + 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs:00': { unlockedBalance: 0n, lockedBalance: 64000n }, + 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs:efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539': { + unlockedBalance: 0n, + lockedBalance: 0n, + }, + 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ:00': { unlockedBalance: 0n, lockedBalance: 100000000000n }, + }, +}; \ No newline at end of file diff --git a/packages/daemon/__tests__/integration/scenario_configs/single_voided_regular_transaction.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/single_voided_regular_transaction.balances.ts new file mode 100644 index 00000000..f93384d7 --- /dev/null +++ b/packages/daemon/__tests__/integration/scenario_configs/single_voided_regular_transaction.balances.ts @@ -0,0 +1,14 @@ +/** + * 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 { + addressBalances: { + 'HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns:00': { unlockedBalance: 6400n, lockedBalance: 44800n }, + 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs:00': { unlockedBalance: 0n, lockedBalance: 64000n }, + 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ:00': { unlockedBalance: 0n, lockedBalance: 100000000000n }, + }, +}; diff --git a/packages/daemon/__tests__/integration/scenario_configs/transaction_voiding_chain.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/transaction_voiding_chain.balances.ts new file mode 100644 index 00000000..a94a2a70 --- /dev/null +++ b/packages/daemon/__tests__/integration/scenario_configs/transaction_voiding_chain.balances.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. + */ + +export default { + addressBalances: { + // HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh - token efb08b...: unlocked=0, locked=0, authorities: 0 unlocked + 0 locked + 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh:efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539': { + unlockedBalance: 0n, + lockedBalance: 0n, + authorities: { locked: 0, unlocked: 0 } + }, + // HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns - token efb08b...: unlocked=0, locked=0, authorities: 1 unlocked + 0 locked + 'HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns:efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539': { + unlockedBalance: 0n, + lockedBalance: 0n, + authorities: { locked: 0, unlocked: 1 } + }, + // HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs - HTR (00): unlocked=3500, locked=70400 + 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs:00': { + unlockedBalance: 3500n, + lockedBalance: 70400n + }, + // HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs - token efb08b...: unlocked=0, locked=0, authorities: 2 unlocked + 0 locked + 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs:efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539': { + unlockedBalance: 0n, + lockedBalance: 0n, + authorities: { locked: 0, unlocked: 2 } + }, + // HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ - HTR (00): unlocked=0, locked=100000000000 (genesis tx) + 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ:00': { + unlockedBalance: 0n, + lockedBalance: 100000000000n + }, + // HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26 - HTR (00): unlocked=5900, locked=0 + 'HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26:00': { + unlockedBalance: 5900n, + lockedBalance: 0n + }, + // HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu - HTR (00): unlocked=3400, locked=0 + 'HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu:00': { + unlockedBalance: 3400n, + lockedBalance: 0n + }, + }, + walletBalances: {}, +}; diff --git a/packages/daemon/__tests__/integration/scenario_configs/unvoided_transactions.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/unvoided_transactions.balances.ts index 7fdce785..f9b04db6 100644 --- a/packages/daemon/__tests__/integration/scenario_configs/unvoided_transactions.balances.ts +++ b/packages/daemon/__tests__/integration/scenario_configs/unvoided_transactions.balances.ts @@ -1,16 +1,21 @@ 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, - 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ': 100000000000 + addressBalances: { + 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HQijr325t63VJFdc4vYkaTyd87oeBLpSed:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'H9hHteu9QdAS5p6X743Mpfue6G19rV9GeY:00': { unlockedBalance: 0n, lockedBalance: 6400n }, + 'HNqTfEASfdx7H4vMUGzfD2HyD3GeKuxjTJ:00': { unlockedBalance: 5400n, lockedBalance: 0n }, + 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs:00': { unlockedBalance: 1000n, lockedBalance: 0n }, + 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ:00': { unlockedBalance: 0n, lockedBalance: 100000000000n }, + }, + walletBalances: { + // Add wallet balances when needed + }, }; diff --git a/packages/daemon/__tests__/integration/scenario_configs/voided_token_authority.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/voided_token_authority.balances.ts new file mode 100644 index 00000000..f3459ebd --- /dev/null +++ b/packages/daemon/__tests__/integration/scenario_configs/voided_token_authority.balances.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. + */ + +export default { + addressBalances: { + 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs:00': { unlockedBalance: 6390n, lockedBalance: 115200n }, + 'HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs:efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539': { + unlockedBalance: 1000n, + lockedBalance: 0n, + authorities: { unlocked: 2, locked: 0 } + }, + 'HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ:00': { unlockedBalance: 0n, lockedBalance: 100000000000n }, + 'HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns:efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539': { + unlockedBalance: 0n, + lockedBalance: 0n, + authorities: { unlocked: 1, locked: 0 } + }, + 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh:efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539': { + unlockedBalance: 0n, + lockedBalance: 0n, + authorities: { unlocked: 0, locked: 0 } + } + }, + walletBalances: { + // HTH balance: 6390 unlocked + 115200 locked (HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs) + 0 unlocked + 100000000000 locked (HRXVDmLVdq8pgok1BCUKpiFWdAVAy4a5AJ) + 'deafbeef:00': { unlockedBalance: 6390n, lockedBalance: 100000115200n }, + 'deafbeef:efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539': { + unlockedBalance: 1000n, + lockedBalance: 0n, + authorities: { unlocked: 2, locked: 0 } + }, + 'deadbeef:efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539': { + unlockedBalance: 0n, + lockedBalance: 0n, + authorities: { unlocked: 0, locked: 0 } + }, + 'cafecafe:efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539': { + unlockedBalance: 0n, + lockedBalance: 0n, + authorities: { unlocked: 1, locked: 0 } + } + }, +}; diff --git a/packages/daemon/__tests__/integration/scripts/docker-compose.yml b/packages/daemon/__tests__/integration/scripts/docker-compose.yml index afa5b5d4..572963c8 100644 --- a/packages/daemon/__tests__/integration/scripts/docker-compose.yml +++ b/packages/daemon/__tests__/integration/scripts/docker-compose.yml @@ -9,7 +9,7 @@ services: - "3380:3306" unvoided_transaction: - image: hathornetwork/hathor-core:stable + image: hathornetwork/hathor-core:latest command: [ "events_simulator", "--scenario", "UNVOIDED_TRANSACTION", @@ -18,7 +18,7 @@ services: ports: - "8081:8080" reorg: - image: hathornetwork/hathor-core:stable + image: hathornetwork/hathor-core:latest command: [ "events_simulator", "--scenario", "REORG", @@ -27,7 +27,7 @@ services: ports: - "8082:8080" single_chain_blocks_and_transactions: - image: hathornetwork/hathor-core:stable + image: hathornetwork/hathor-core:latest command: [ "events_simulator", "--scenario", "SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS", @@ -36,7 +36,7 @@ services: ports: - "8083:8080" invalid_mempool_transaction: - image: hathornetwork/hathor-core:stable + image: hathornetwork/hathor-core:latest command: [ "events_simulator", "--scenario", "INVALID_MEMPOOL_TRANSACTION", @@ -46,7 +46,7 @@ services: - "8085:8080" custom_scripts: - image: hathornetwork/hathor-core:stable + image: hathornetwork/hathor-core:latest command: [ "events_simulator", "--scenario", "CUSTOM_SCRIPT", @@ -56,7 +56,7 @@ services: - "8086:8080" empty_script: - image: hathornetwork/hathor-core:stable + image: hathornetwork/hathor-core:latest command: [ "events_simulator", "--scenario", "EMPTY_SCRIPT", @@ -65,5 +65,55 @@ services: ports: - "8087:8080" + nc_events: + image: hathornetwork/hathor-core:stable + command: [ + "events_simulator", + "--scenario", "NC_EVENTS", + "--seed", "1" + ] + ports: + - "8088:8080" + + transaction_voiding_chain: + image: hathornetwork/hathor-core:experimental-single-voided-create-token-tx + command: [ + "events_simulator", + "--scenario", "TRANSACTION_VOIDING_CHAIN", + "--seed", "1" + ] + ports: + - "8089:8080" + + voided_token_authority: + image: hathornetwork/hathor-core:experimental-single-voided-create-token-tx + command: [ + "events_simulator", + "--scenario", "VOIDED_TOKEN_AUTHORITY", + "--seed", "1" + ] + ports: + - "8090:8080" + + single_voided_create_token_transaction: + image: hathornetwork/hathor-core:experimental-single-voided-create-token-tx + command: [ + "events_simulator", + "--scenario", "SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION", + "--seed", "1" + ] + ports: + - "8091:8080" + + single_voided_regular_transaction: + image: hathornetwork/hathor-core:experimental-single-voided-create-token-tx-python3.12 + command: [ + "events_simulator", + "--scenario", "SINGLE_VOIDED_REGULAR_TRANSACTION", + "--seed", "1" + ] + ports: + - "8092:8080" + networks: database: diff --git a/packages/daemon/__tests__/integration/scripts/sequelize-db-config.js b/packages/daemon/__tests__/integration/scripts/sequelize-db-config.js index ee52475a..b2ff043a 100644 --- a/packages/daemon/__tests__/integration/scripts/sequelize-db-config.js +++ b/packages/daemon/__tests__/integration/scripts/sequelize-db-config.js @@ -7,6 +7,7 @@ module.exports = { port: 3380, dialect: 'mysql', dialectOptions: { + supportBigNumbers: true, bigNumberStrings: true, }, }, diff --git a/packages/daemon/__tests__/integration/utils/index.ts b/packages/daemon/__tests__/integration/utils/index.ts index fd38b055..43822aef 100644 --- a/packages/daemon/__tests__/integration/utils/index.ts +++ b/packages/daemon/__tests__/integration/utils/index.ts @@ -7,7 +7,19 @@ import { Connection } from 'mysql2/promise'; import { Interpreter } from 'xstate'; import { getLastSyncedEvent } from '../../../src/db'; -import { AddressBalance, AddressBalanceRow, Context, Event } from '../../../src/types'; +import { AddressBalance, AddressBalanceRow, Context, Event, WalletBalanceRow } from '../../../src/types'; + +export interface WalletBalance { + walletId: string; + tokenId: string; + unlockedBalance: bigint; + lockedBalance: bigint; + unlockedAuthorities: number; + lockedAuthorities: number; + timelockExpires: number | null; + transactions: number; + totalReceived: bigint; +} export const cleanDatabase = async (mysql: Connection): Promise => { const TABLES = [ @@ -33,6 +45,10 @@ export const cleanDatabase = async (mysql: Connection): Promise => { } await mysql.query('SET FOREIGN_KEY_CHECKS = 1'); + + // Ensure all changes are committed and flushed + await mysql.query('COMMIT'); + await mysql.query('FLUSH TABLES'); }; export const fetchAddressBalances = async ( @@ -47,8 +63,8 @@ export const fetchAddressBalances = async ( 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, + unlockedBalance: BigInt(result.unlocked_balance), + lockedBalance: BigInt(result.locked_balance), lockedAuthorities: result.locked_authorities as number, unlockedAuthorities: result.unlocked_authorities as number, timelockExpires: result.timelock_expires as number, @@ -56,21 +72,116 @@ export const fetchAddressBalances = async ( })); }; +export const fetchWalletBalances = async ( + mysql: Connection +): Promise => { + const [results] = await mysql.query( + `SELECT * + FROM \`wallet_balance\` + ORDER BY \`wallet_id\`, \`token_id\``, + ); + + return results.map((result): WalletBalance => ({ + walletId: result.wallet_id as string, + tokenId: result.token_id as string, + unlockedBalance: BigInt(result.unlocked_balance), + lockedBalance: BigInt(result.locked_balance), + unlockedAuthorities: result.unlocked_authorities as number, + lockedAuthorities: result.locked_authorities as number, + timelockExpires: result.timelock_expires as number | null, + transactions: result.transactions as number, + totalReceived: BigInt(result.total_received), + })); +}; + export const validateBalances = async ( balancesA: AddressBalance[], - balancesB: { string: number }, + expectedBalances: Record, +): Promise => { + const expectedAddressTokenKeys = new Set(Object.keys(expectedBalances)); + + // Check for unexpected addresses with non-zero balances or authorities + for (const balance of balancesA) { + const addressTokenKey = `${balance.address}:${balance.tokenId}`; + const totalBalance = balance.lockedBalance + balance.unlockedBalance; + const totalAuthorities = balance.lockedAuthorities + balance.unlockedAuthorities; + + if (!expectedAddressTokenKeys.has(addressTokenKey) && (totalBalance !== BigInt(0) || totalAuthorities !== 0)) { + throw new Error(`Unexpected address:token with non-zero balance or authorities: ${addressTokenKey}, balance: ${totalBalance}, authorities: ${totalAuthorities}`); + } + } + + // Validate all expected addresses + for (const addressTokenKey of expectedAddressTokenKeys) { + const [address, tokenId] = addressTokenKey.split(':'); + const balanceA = balancesA.find(b => b.address === address && b.tokenId === tokenId); + const expected = expectedBalances[addressTokenKey]; + + const actualUnlockedBalance = balanceA ? balanceA.unlockedBalance : BigInt(0); + const actualLockedBalance = balanceA ? balanceA.lockedBalance : BigInt(0); + + if (actualUnlockedBalance !== expected.unlockedBalance) { + throw new Error(`Unlocked balance mismatch for address:token ${addressTokenKey}, expected: ${expected.unlockedBalance}, received: ${actualUnlockedBalance}`); + } + + if (actualLockedBalance !== expected.lockedBalance) { + throw new Error(`Locked balance mismatch for address:token ${addressTokenKey}, expected: ${expected.lockedBalance}, received: ${actualLockedBalance}`); + } + + // Validate authorities if specified + if (expected.authorities && balanceA) { + if (balanceA.lockedAuthorities !== expected.authorities.locked) { + throw new Error(`Locked authorities mismatch for address:token ${addressTokenKey}, expected: ${expected.authorities.locked}, received: ${balanceA.lockedAuthorities}`); + } + if (balanceA.unlockedAuthorities !== expected.authorities.unlocked) { + throw new Error(`Unlocked authorities mismatch for address:token ${addressTokenKey}, expected: ${expected.authorities.unlocked}, received: ${balanceA.unlockedAuthorities}`); + } + } + } +}; + +export const validateWalletBalances = async ( + walletBalances: WalletBalance[], + expectedWalletBalances: Record, ): Promise => { - const length = Math.max(balancesA.length, Object.keys(balancesB).length); + for (const [walletTokenKey, expected] of Object.entries(expectedWalletBalances)) { + const [walletId, tokenId] = walletTokenKey.split(':'); + + const walletBalance = walletBalances.find( + b => b.walletId === walletId && b.tokenId === tokenId + ); + + const actualUnlockedBalance = walletBalance ? walletBalance.unlockedBalance : BigInt(0); + const actualLockedBalance = walletBalance ? walletBalance.lockedBalance : BigInt(0); + + if (actualUnlockedBalance !== expected.unlockedBalance) { + throw new Error( + `Wallet unlocked balance mismatch for wallet ${walletId} token ${tokenId}: expected ${expected.unlockedBalance}, received ${actualUnlockedBalance}` + ); + } - 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 (actualLockedBalance !== expected.lockedBalance) { + throw new Error( + `Wallet locked balance mismatch for wallet ${walletId} token ${tokenId}: expected ${expected.lockedBalance}, received ${actualLockedBalance}` + ); + } - if (totalBalanceA !== balanceB) { - throw new Error(`Balances are not equal for address: ${address}, expected: ${balanceB}, received: ${totalBalanceA}`); + // Validate authorities if specified + if (expected.authorities && walletBalance) { + if (walletBalance.lockedAuthorities !== expected.authorities.locked) { + throw new Error(`Wallet locked authorities mismatch for wallet ${walletId} token ${tokenId}: expected ${expected.authorities.locked}, received ${walletBalance.lockedAuthorities}`); + } + if (walletBalance.unlockedAuthorities !== expected.authorities.unlocked) { + throw new Error(`Wallet unlocked authorities mismatch for wallet ${walletId} token ${tokenId}: expected ${expected.authorities.unlocked}, received ${walletBalance.unlockedAuthorities}`); + } } } }; @@ -79,7 +190,6 @@ export async function transitionUntilEvent(mysql: Connection, machine: Interpret return 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 === eventId) { machine.stop(); @@ -92,3 +202,6 @@ export async function transitionUntilEvent(mysql: Connection, machine: Interpret machine.start(); }); } + + +export * from './voiding-consistency-checks'; diff --git a/packages/daemon/__tests__/integration/utils/voiding-consistency-checks.ts b/packages/daemon/__tests__/integration/utils/voiding-consistency-checks.ts new file mode 100644 index 00000000..a145eef7 --- /dev/null +++ b/packages/daemon/__tests__/integration/utils/voiding-consistency-checks.ts @@ -0,0 +1,135 @@ +/** + * Database consistency checks for transaction voiding scenarios + */ + +import { Connection, RowDataPacket } from 'mysql2/promise'; + +interface TransactionRow extends RowDataPacket { + tx_id: string; + voided: number; // MySQL TINYINT/BOOLEAN comes back as number (0 or 1) +} + +interface UtxoRow extends RowDataPacket { + tx_id: string; + index: number; + value: string; + voided: number; // MySQL TINYINT/BOOLEAN comes back as number (0 or 1) + spent_by: string | null; +} + +export interface VoidingConsistencyCheck { + transactionStatuses: { + txId: string; + voided: boolean; + expected: boolean; + }[]; + utxoStatuses: { + txId: string; + index: number; + value: number; + voided: boolean; + spentBy: string | null; + expectedVoided: boolean; + expectedSpentBy: string | null; + }[]; +} + +export const performVoidingConsistencyChecks = async ( + mysql: Connection, + expectedChecks: { + transactions: { txId: string; expectedVoided: boolean }[]; + utxos: { + txId: string; + index: number; + expectedValue: number; + expectedVoided: boolean; + expectedSpentBy: string | null; + }[]; + } +): Promise => { + const results: VoidingConsistencyCheck = { + transactionStatuses: [], + utxoStatuses: [], + }; + + // Check transaction voiding statuses + for (const expectedTx of expectedChecks.transactions) { + const [rows] = await mysql.query( + 'SELECT tx_id, voided FROM `transaction` WHERE tx_id = ?', + [expectedTx.txId] + ); + + const actualVoided = rows.length > 0 ? rows[0].voided === 1 : false; + results.transactionStatuses.push({ + txId: expectedTx.txId, + voided: actualVoided, + expected: expectedTx.expectedVoided, + }); + } + + // Check UTXO statuses + for (const expectedUtxo of expectedChecks.utxos) { + const [rows] = await mysql.query( + 'SELECT tx_id, `index`, value, voided, spent_by FROM `tx_output` WHERE tx_id = ? AND `index` = ?', + [expectedUtxo.txId, expectedUtxo.index] + ); + + if (rows.length > 0) { + const utxo = rows[0]; + results.utxoStatuses.push({ + txId: expectedUtxo.txId, + index: expectedUtxo.index, + value: parseInt(utxo.value), + voided: utxo.voided === 1, + spentBy: utxo.spent_by, + expectedVoided: expectedUtxo.expectedVoided, + expectedSpentBy: expectedUtxo.expectedSpentBy, + }); + } else { + // UTXO not found + results.utxoStatuses.push({ + txId: expectedUtxo.txId, + index: expectedUtxo.index, + value: 0, + voided: false, + spentBy: null, + expectedVoided: expectedUtxo.expectedVoided, + expectedSpentBy: expectedUtxo.expectedSpentBy, + }); + } + } + + return results; +}; + +export const validateVoidingConsistency = (checks: VoidingConsistencyCheck): void => { + const errors: string[] = []; + + // Validate transaction statuses + for (const txCheck of checks.transactionStatuses) { + if (txCheck.voided !== txCheck.expected) { + errors.push( + `Transaction ${txCheck.txId}: expected voided=${txCheck.expected}, got voided=${txCheck.voided}` + ); + } + } + + // Validate UTXO statuses + for (const utxoCheck of checks.utxoStatuses) { + if (utxoCheck.voided !== utxoCheck.expectedVoided) { + errors.push( + `UTXO ${utxoCheck.txId}:${utxoCheck.index}: expected voided=${utxoCheck.expectedVoided}, got voided=${utxoCheck.voided}` + ); + } + + if (utxoCheck.spentBy !== utxoCheck.expectedSpentBy) { + errors.push( + `UTXO ${utxoCheck.txId}:${utxoCheck.index}: expected spentBy=${utxoCheck.expectedSpentBy}, got spentBy=${utxoCheck.spentBy}` + ); + } + } + + if (errors.length > 0) { + throw new Error(`Voiding consistency check failed:\n${errors.join('\n')}`); + } +}; diff --git a/packages/daemon/__tests__/machines/EventLossDetection.test.ts b/packages/daemon/__tests__/machines/EventLossDetection.test.ts new file mode 100644 index 00000000..52e10c07 --- /dev/null +++ b/packages/daemon/__tests__/machines/EventLossDetection.test.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. + */ + +/** + * @jest-environment node + */ + +import { + CONNECTED_STATES, + SyncMachine, + SYNC_MACHINE_STATES, +} from '../../src/machines'; +import { EventTypes, FullNodeEvent } from '../../src/types'; +import EventFixtures from '../__fixtures__/events'; + +const { NEW_VERTEX_ACCEPTED } = EventFixtures; + +describe('Event Loss Detection', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should transition to checkingForMissedEvents after ACK timeout', () => { + const MockedFetchMachine = SyncMachine.withConfig({ + delays: { + ACK_TIMEOUT: 100, + }, + actions: { + startStream: () => { }, + }, + guards: { + invalidPeerId: () => false, + invalidStreamId: () => false, + invalidNetwork: () => false, + unchanged: () => true, + }, + }); + + let currentState = MockedFetchMachine.initialState; + + currentState = MockedFetchMachine.transition(currentState, { + // @ts-ignore + type: `done.invoke.SyncMachine.${SYNC_MACHINE_STATES.INITIALIZING}:invocation[0]`, + // @ts-ignore + data: { lastEventId: 999 }, + }); + + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.WEBSOCKET_EVENT, + event: { type: 'CONNECTED' }, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.idle}`)).toBeTruthy(); + + // Send an event to trigger ACK + currentState = MockedFetchMachine.transition(currentState, { + type: EventTypes.FULLNODE_EVENT, + event: NEW_VERTEX_ACCEPTED as unknown as FullNodeEvent, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.idle}`)).toBeTruthy(); + + // Simulate ACK timeout + currentState = MockedFetchMachine.transition(currentState, { + // @ts-ignore + type: `xstate.after(ACK_TIMEOUT)#${CONNECTED_STATES.idle}`, + }); + + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.checkingForMissedEvents}`)).toBeTruthy(); + }); + + it('should have checkingForMissedEvents state in machine definition', () => { + const machine = SyncMachine.withConfig({ + actions: { startStream: () => { } }, + }); + + const connectedState = machine.states[SYNC_MACHINE_STATES.CONNECTED]; + expect(connectedState).toBeDefined(); + // @ts-ignore + expect(connectedState.states[CONNECTED_STATES.checkingForMissedEvents]).toBeDefined(); + }); + + it('should configure checkForMissedEvents service in checkingForMissedEvents state', () => { + const machine = SyncMachine.withConfig({ + actions: { startStream: () => { } }, + }); + + const connectedState = machine.states[SYNC_MACHINE_STATES.CONNECTED]; + // @ts-ignore + const checkingState = connectedState.states[CONNECTED_STATES.checkingForMissedEvents]; + + expect(checkingState.invoke).toBeDefined(); + // @ts-ignore + const invokeSrc = Array.isArray(checkingState.invoke) ? checkingState.invoke[0].src : checkingState.invoke.src; + expect(invokeSrc).toBe('checkForMissedEvents'); + }); + + it('should have all required services, guards, and delays configured', () => { + expect(SyncMachine.options.services).toHaveProperty('checkForMissedEvents'); + expect(SyncMachine.options.guards).toHaveProperty('hasNewEvents'); + expect(SyncMachine.options.delays).toHaveProperty('ACK_TIMEOUT'); + }); +}); diff --git a/packages/daemon/__tests__/machines/SyncMachine.test.ts b/packages/daemon/__tests__/machines/SyncMachine.test.ts index abb203d6..2fa7ef47 100644 --- a/packages/daemon/__tests__/machines/SyncMachine.test.ts +++ b/packages/daemon/__tests__/machines/SyncMachine.test.ts @@ -69,7 +69,7 @@ describe('machine initialization', () => { it('should fetch initial state, connect to websocket and validate network before transitioning to idle', () => { const MockedFetchMachine = SyncMachine.withConfig({ actions: { - startStream: () => {}, + startStream: () => { }, }, }); @@ -118,8 +118,10 @@ describe('machine initialization', () => { currentState = MockedFetchMachine.transition(currentState, { type: EventTypes.WEBSOCKET_EVENT, - event: { type: 'DISCONNECTED', - }}); + event: { + type: 'DISCONNECTED', + } + }); expect(currentState.matches(SYNC_MACHINE_STATES.RECONNECTING)).toBeTruthy(); }); @@ -163,7 +165,7 @@ describe('machine initialization', () => { it('should transition to RECONNECTING to reconnect after a failure', () => { const MockedFetchMachine = SyncMachine.withConfig({ actions: { - startStream: () => {}, + startStream: () => { }, }, }); @@ -215,41 +217,92 @@ describe('Event handling', () => { }); it('should validate the peerid on every message', () => { + // Use a guard that checks against a different peer_id than what's in the fixture + // The fixture has peer_id: 'bdf4fa876f5cdba84be0cab53b21fc9eb45fe4b3d6ede99f493119d37df4e560' + // We'll check against 'invalidPeerId', which won't match, so invalidPeerId will return true const MockedFetchMachine = SyncMachine.withConfig({ + actions: { + startStream: () => {}, + storeEvent: () => {}, + }, guards: { - invalidPeerId, - invalidStreamId: () => { - return false; - } + invalidPeerId: (_context, event) => { + if (event.type !== EventTypes.FULLNODE_EVENT) { + throw new Error(`Invalid event type on invalidPeerId guard: ${event.type}`); + } + // Return true if peer_id is invalid (doesn't match expected) + return event.event.peer_id !== 'bdf4fa876f5cdba84be0cab53b21fc9eb45fe4b3d6ede99f493119d37df4e560'; + }, + invalidStreamId: () => false, + invalidNetwork: () => false, }, }); let currentState = untilIdle(MockedFetchMachine); - process.env.FULLNODE_PEER_ID = 'invalidPeerId'; + // Manually initialize txCache since untilIdle doesn't execute entry actions + if (!currentState.context.txCache) { + currentState.context.txCache = new LRU(TX_CACHE_SIZE); + } + + // Send event with different peer_id to trigger invalidPeerId guard + const eventWithDifferentPeerId = { + ...VERTEX_METADATA_CHANGED, + event: { + ...VERTEX_METADATA_CHANGED.event, + peer_id: 'differentPeerId', + }, + }; currentState = MockedFetchMachine.transition(currentState, { type: EventTypes.FULLNODE_EVENT, - event: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + event: eventWithDifferentPeerId as unknown as FullNodeEvent, }); expect(currentState.matches(SYNC_MACHINE_STATES.ERROR)).toBeTruthy(); }); it('should validate the stream id on every message', () => { + // Use a guard that checks against a different stream_id than what's in the fixture + // The fixture has stream_id: 'f7d9157c-9906-4bd2-bc84-cfb9f5b607d1' + // We'll check against that, and send an event with a different stream_id const MockedFetchMachine = SyncMachine.withConfig({ + actions: { + startStream: () => {}, + storeEvent: () => {}, + }, guards: { - invalidStreamId, + invalidPeerId: () => false, + invalidStreamId: (_context, event) => { + if (event.type !== EventTypes.FULLNODE_EVENT) { + throw new Error(`Invalid event type on invalidStreamId guard: ${event.type}`); + } + // Return true if stream_id is invalid (doesn't match expected) + return event.event.stream_id !== 'f7d9157c-9906-4bd2-bc84-cfb9f5b607d1'; + }, + invalidNetwork: () => false, }, }); let currentState = untilIdle(MockedFetchMachine); - process.env.STREAM_ID = 'invalidStreamId'; + // Manually initialize txCache since untilIdle doesn't execute entry actions + if (!currentState.context.txCache) { + currentState.context.txCache = new LRU(TX_CACHE_SIZE); + } + + // Send event with different stream_id to trigger invalidStreamId guard + const eventWithDifferentStreamId = { + ...VERTEX_METADATA_CHANGED, + event: { + ...VERTEX_METADATA_CHANGED.event, + stream_id: 'differentStreamId', + }, + }; currentState = MockedFetchMachine.transition(currentState, { type: EventTypes.FULLNODE_EVENT, - event: VERTEX_METADATA_CHANGED as unknown as FullNodeEvent, + event: eventWithDifferentStreamId as unknown as FullNodeEvent, }); expect(currentState.matches(SYNC_MACHINE_STATES.ERROR)).toBeTruthy(); @@ -268,23 +321,22 @@ describe('Event handling', () => { invalidNetwork: () => false, unchanged: unchangedMock, }, - }).withContext({ - event: null, - socket: null, - healthcheck: null, - retryAttempt: 0, - initialEventId: 0, - txCache: TxCache, }); unchangedMock.mockImplementation(unchanged); let currentState = untilIdle(MockedFetchMachine); + // Manually initialize txCache since untilIdle doesn't execute entry actions + if (!currentState.context.txCache) { + currentState.context.txCache = new LRU(TX_CACHE_SIZE); + } + const machineCache = currentState.context.txCache; + 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); + machineCache.set(VERTEX_METADATA_CHANGED.event.data.hash, hashedTx); currentState = MockedFetchMachine.transition(currentState, { type: EventTypes.FULLNODE_EVENT, @@ -301,7 +353,7 @@ describe('Event handling', () => { // @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(); + machineCache.clear(); currentState = MockedFetchMachine.transition(currentState, { type: EventTypes.FULLNODE_EVENT, diff --git a/packages/daemon/__tests__/services/services.test.ts b/packages/daemon/__tests__/services/services.test.ts index 1c87fc9b..69e07daf 100644 --- a/packages/daemon/__tests__/services/services.test.ts +++ b/packages/daemon/__tests__/services/services.test.ts @@ -15,6 +15,7 @@ import { updateLastSyncedEvent as dbUpdateLastSyncedEvent, getTxOutputsFromTx, voidTransaction, + voidAddressTransaction, getTransactionById, getUtxosLockedAtHeight, addOrUpdateTx, @@ -30,6 +31,7 @@ import { handleVertexAccepted, metadataDiff, handleReorgStarted, + checkForMissedEvents, } from '../../src/services'; import logger from '../../src/logger'; import { @@ -53,6 +55,7 @@ jest.mock('../../src/logger', () => ({ debug: jest.fn(), error: jest.fn(), info: jest.fn(), + warn: jest.fn(), })); jest.mock('axios', () => ({ @@ -65,8 +68,13 @@ jest.mock('../../src/db', () => ({ updateLastSyncedEvent: jest.fn(), addOrUpdateTx: jest.fn(), getTxOutputsFromTx: jest.fn(), + getTxOutput: jest.fn(), voidTransaction: jest.fn(), + voidAddressTransaction: jest.fn(), + voidWalletTransaction: jest.fn(), markUtxosAsVoided: jest.fn(), + unspendUtxos: jest.fn(), + clearTxProposalForVoidedTx: jest.fn(), dbUpdateLastSyncedEvent: jest.fn(), getTransactionById: jest.fn(), getUtxosLockedAtHeight: jest.fn(), @@ -105,6 +113,7 @@ jest.mock('../../src/utils', () => ({ sendMessageSQS: jest.fn(), getWalletBalancesForTx: jest.fn(), generateAddresses: jest.fn(), + retryWithBackoff: jest.fn((fn) => fn()), })); jest.mock('@wallet-service/common', () => { @@ -411,10 +420,11 @@ describe('handleVoidedTx', () => { (prepareInputs as jest.Mock).mockReturnValue([]); (getAddressBalanceMap as jest.Mock).mockReturnValue({}); (getTxOutputsFromTx as jest.Mock).mockResolvedValue([]); + (getAddressWalletInfo as jest.Mock).mockResolvedValue({}); await handleVoidedTx(context as any); - expect(voidTransaction).toHaveBeenCalledWith(expect.any(Object), 'hashValue', {}); + 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(); @@ -544,11 +554,11 @@ describe('handleVertexAccepted', () => { (getAddressBalanceMap as jest.Mock).mockReturnValue({}); (getUtxosLockedAtHeight as jest.Mock).mockResolvedValue([]); (hashTxData as jest.Mock).mockReturnValue('hashedData'); - (getAddressWalletInfo as jest.Mock).mockResolvedValue({ + (getAddressWalletInfo as jest.Mock).mockResolvedValue({ 'address1': { - walletId: 'wallet1', - xpubkey: 'xpubkey1', - maxGap: 10 + walletId: 'wallet1', + xpubkey: 'xpubkey1', + maxGap: 10 }, }); @@ -602,11 +612,11 @@ describe('handleVertexAccepted', () => { (getAddressBalanceMap as jest.Mock).mockReturnValue({}); (getUtxosLockedAtHeight as jest.Mock).mockResolvedValue([]); (hashTxData as jest.Mock).mockReturnValue('hashedData'); - (getAddressWalletInfo as jest.Mock).mockResolvedValue({ + (getAddressWalletInfo as jest.Mock).mockResolvedValue({ 'address1': { - walletId: 'wallet1', - xpubkey: 'xpubkey1', - maxGap: 10 + walletId: 'wallet1', + xpubkey: 'xpubkey1', + maxGap: 10 }, }); (getWalletBalancesForTx as jest.Mock).mockResolvedValue({ 'mockWallet': {} }); @@ -659,11 +669,11 @@ describe('handleVertexAccepted', () => { (getAddressBalanceMap as jest.Mock).mockReturnValue({}); (getUtxosLockedAtHeight as jest.Mock).mockResolvedValue([]); (hashTxData as jest.Mock).mockReturnValue('hashedData'); - (getAddressWalletInfo as jest.Mock).mockResolvedValue({ + (getAddressWalletInfo as jest.Mock).mockResolvedValue({ 'address1': { - walletId: 'wallet1', - xpubkey: 'xpubkey1', - maxGap: 10 + walletId: 'wallet1', + xpubkey: 'xpubkey1', + maxGap: 10 }, }); @@ -1034,3 +1044,188 @@ describe('handleReorgStarted', () => { .toThrow('Invalid event type for REORG_STARTED'); }); }); + +describe('checkForMissedEvents', () => { + beforeEach(() => { + jest.clearAllMocks(); + const mockUrl = 'http://mock-host:8080/v1a'; + (getFullnodeHttpUrl as jest.Mock).mockReturnValue(mockUrl); + }); + + it('should return hasNewEvents=true when API returns events', async () => { + const mockResponse = { + status: 200, + data: { + events: [ + { + id: 115182, + timestamp: 1761758848.1938324, + type: 'VERTEX_METADATA_CHANGED', + data: { hash: 'mockHash' }, + }, + { + id: 115183, + timestamp: 1761758848.196779, + type: 'NEW_VERTEX_ACCEPTED', + data: { hash: 'mockHash' }, + }, + ], + latest_event_id: 115561, + }, + }; + + (axios.get as jest.Mock).mockResolvedValue(mockResponse); + + const context = { + event: { + event: { + id: 115181, + }, + }, + }; + + const result = await checkForMissedEvents(context as any); + + expect(result.hasNewEvents).toBe(true); + expect(result.events).toHaveLength(2); + expect(axios.get).toHaveBeenCalledWith('http://mock-host:8080/v1a/event', { + params: { + last_ack_event_id: 115181, + size: 1, + }, + }); + expect(logger.warn).toHaveBeenCalledWith( + 'Detected 2 missed event(s) after ACK 115181. Will reconnect.' + ); + }); + + it('should return hasNewEvents=false when API returns no events', async () => { + const mockResponse = { + status: 200, + data: { + events: [], + latest_event_id: 115181, + }, + }; + + (axios.get as jest.Mock).mockResolvedValue(mockResponse); + + const context = { + event: { + event: { + id: 115181, + }, + }, + }; + + const result = await checkForMissedEvents(context as any); + + expect(result.hasNewEvents).toBe(false); + expect(result.events).toHaveLength(0); + expect(logger.debug).toHaveBeenCalledWith( + 'No missed events detected after ACK 115181' + ); + }); + + it('should throw error when HTTP request fails', async () => { + (axios.get as jest.Mock).mockResolvedValue({ + status: 500, + data: {}, + }); + + const context = { + event: { + event: { + id: 115181, + }, + }, + }; + + await expect(checkForMissedEvents(context as any)) + .rejects + .toThrow('Failed to check for missed events: HTTP 500'); + + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('HTTP 500') + ); + }); + + it('should throw error when network request fails', async () => { + const networkError = new Error('ECONNREFUSED: Connection refused'); + (axios.get as jest.Mock).mockRejectedValue(networkError); + + const context = { + event: { + event: { + id: 115181, + }, + }, + }; + + await expect(checkForMissedEvents(context as any)) + .rejects + .toThrow('Failed to check for missed events: Network error'); + + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Network error') + ); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('ECONNREFUSED') + ); + }); + + it('should throw error when context has no event', async () => { + const context = {}; + + await expect(checkForMissedEvents(context as any)) + .rejects + .toThrow('No event in context when checking for missed events'); + }); + + it('should handle API response with non-array events field', async () => { + const mockResponse = { + status: 200, + data: { + events: null, + latest_event_id: 115181, + }, + }; + + (axios.get as jest.Mock).mockResolvedValue(mockResponse); + + const context = { + event: { + event: { + id: 115181, + }, + }, + }; + + const result = await checkForMissedEvents(context as any); + + expect(result.hasNewEvents).toBe(false); + }); + + it('should throw error when response data is invalid', async () => { + (axios.get as jest.Mock).mockResolvedValue({ + status: 200, + data: null, + }); + + const context = { + event: { + event: { + id: 115181, + }, + }, + }; + + await expect(checkForMissedEvents(context as any)) + .rejects + .toThrow('Failed to check for missed events: Invalid response structure'); + + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Invalid response data structure') + ); + }); +}); diff --git a/packages/daemon/__tests__/services/services_with_db.test.ts b/packages/daemon/__tests__/services/services_with_db.test.ts index 33b7809e..5ad79238 100644 --- a/packages/daemon/__tests__/services/services_with_db.test.ts +++ b/packages/daemon/__tests__/services/services_with_db.test.ts @@ -6,13 +6,56 @@ */ import * as db from '../../src/db'; -import { handleVoidedTx } from '../../src/services'; +import { handleVoidedTx, voidTx } from '../../src/services'; import { LRU } from '../../src/utils'; +import { + addOrUpdateTx, + addUtxos, + updateTxOutputSpentBy, + getTxOutput, + unspendUtxos, +} from '../../src/db'; +import { + cleanDatabase, + checkUtxoTable, + createOutput, + createInput, + createEventTxInput, + setupWallet, + getWalletBalance, + insertWalletBalance, + getWalletTxHistoryCount, +} from '../utils'; +import { DbTxOutput, EventTxInput } from '../../src/types'; +import { Connection } from 'mysql2/promise'; /** * @jest-environment node */ + +// Use a single mysql connection for all tests +let mysql: Connection; + +beforeAll(async () => { + try { + mysql = await db.getDbConnection(); + } catch (e) { + console.error('Failed to establish db connection', e); + throw e; + } +}); + +afterAll(async () => { + if (mysql) { + await mysql.destroy(); + } +}); + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + describe('handleVoidedTx (db)', () => { beforeEach(() => { jest.clearAllMocks(); @@ -46,14 +89,12 @@ describe('handleVoidedTx (db)', () => { }, }; - const mysql = await db.getDbConnection(); await expect(handleVoidedTx(context as any)).resolves.not.toThrow(); const lastEvent = await db.getLastSyncedEvent(mysql); expect(db.voidTransaction).toHaveBeenCalledWith( expect.any(Object), 'random-hash', - expect.any(Object), ); expect(lastEvent).toStrictEqual({ id: expect.any(Number), @@ -62,3 +103,851 @@ describe('handleVoidedTx (db)', () => { }); }); }); + +describe('voidTransaction with input unspending', () => { + it('should unspent inputs when voiding a transaction', async () => { + expect.hasAssertions(); + + // Create transaction A that creates an output + const txIdA = 'test1-tx-a'; + const addressA = 'test1-address-a'; + const tokenId = '00'; + const outputValue = 100n; + + await addOrUpdateTx(mysql, txIdA, 0, 1, 1, 100); + + // Add output from transaction A + const outputA = createOutput(0, outputValue, addressA, tokenId); + await addUtxos(mysql, txIdA, [outputA], null); + + // Verify the UTXO is unspent + let utxo = await getTxOutput(mysql, txIdA, 0, true); + expect(utxo).not.toBeNull(); + expect(utxo?.spentBy).toBeNull(); + + // Create transaction B that spends the output from transaction A + const txIdB = 'test1-tx-b'; + const addressB = 'test1-address-b'; + + await addOrUpdateTx(mysql, txIdB, 0, 1, 1, 101); + + // Mark the output from A as spent by B + const inputB = createInput(outputValue, addressA, txIdA, 0, tokenId); + await updateTxOutputSpentBy(mysql, [inputB], txIdB); + + // Verify the UTXO is now spent + utxo = await getTxOutput(mysql, txIdA, 0, false); + expect(utxo).not.toBeNull(); + expect(utxo?.spentBy).toBe(txIdB); + + // Add output from transaction B + const outputB = createOutput(0, outputValue, addressB, tokenId); + await addUtxos(mysql, txIdB, [outputB], null); + + + // Now void transaction B using the voidTx service function + const inputs = [createEventTxInput(outputValue, addressA, txIdA, 0)]; + const outputs = [{ + value: outputValue, + locked: false, + decoded: { + type: 'P2PKH' as const, + address: addressB, + timelock: null, + }, + token_data: 0, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + }]; + + await voidTx(mysql, txIdB, inputs, outputs, [tokenId], [], 1); + + // Check if the UTXO from transaction A is unspent again + utxo = await getTxOutput(mysql, txIdA, 0, false); + expect(utxo).not.toBeNull(); + expect(utxo?.spentBy).toBeNull(); + }); + + it('should unspent multiple inputs when voiding a transaction with multiple inputs', async () => { + expect.hasAssertions(); + + // Create transactions A and B that create outputs + const txIdA = 'test2-tx-a'; + const txIdB = 'test2-tx-b'; + const txIdC = 'test2-tx-c'; // The transaction we'll void + const address1 = 'test2-address-1'; + const address2 = 'test2-address-2'; + const address3 = 'test2-address-3'; + const tokenId = '00'; + + // Create two UTXOs + await addOrUpdateTx(mysql, txIdA, 0, 1, 1, 100); + await addOrUpdateTx(mysql, txIdB, 0, 1, 1, 101); + + const outputA = createOutput(0, 50n, address1, tokenId); + const outputB = createOutput(0, 75n, address2, tokenId); + + await addUtxos(mysql, txIdA, [outputA], null); + await addUtxos(mysql, txIdB, [outputB], null); + + // Verify both UTXOs are unspent + let utxoA = await getTxOutput(mysql, txIdA, 0, true); + let utxoB = await getTxOutput(mysql, txIdB, 0, true); + expect(utxoA?.spentBy).toBeNull(); + expect(utxoB?.spentBy).toBeNull(); + + // Create transaction C that spends both outputs + await addOrUpdateTx(mysql, txIdC, 0, 1, 1, 102); + + const inputC1 = createInput(50n, address1, txIdA, 0, tokenId); + const inputC2 = createInput(75n, address2, txIdB, 0, tokenId); + await updateTxOutputSpentBy(mysql, [inputC1, inputC2], txIdC); + + // Verify both UTXOs are now spent by C + utxoA = await getTxOutput(mysql, txIdA, 0, false); + utxoB = await getTxOutput(mysql, txIdB, 0, false); + expect(utxoA?.spentBy).toBe(txIdC); + expect(utxoB?.spentBy).toBe(txIdC); + + // Add output from transaction C + const outputC = createOutput(0, 125n, address3, tokenId); + await addUtxos(mysql, txIdC, [outputC], null); + + // Void transaction C using voidTx service function + const inputs = [ + createEventTxInput(50n, address1, txIdA, 0), + createEventTxInput(75n, address2, txIdB, 0), + ]; + const outputs = [{ + value: 125n, + locked: false, + decoded: { + type: 'P2PKH' as const, + address: address3, + timelock: null, + }, + token_data: 0, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + }]; + + await voidTx(mysql, txIdC, inputs, outputs, [tokenId], [], 1); + + // Check if UTXOs from transactions A and B are unspent again + utxoA = await getTxOutput(mysql, txIdA, 0, true); + utxoB = await getTxOutput(mysql, txIdB, 0, true); + + // Both outputs should be unspent again after voiding C (which was spending them) + expect(utxoA).not.toBeNull(); + expect(utxoA?.spentBy).toBeNull(); // Should pass - should be null + expect(utxoB).not.toBeNull(); + expect(utxoB?.spentBy).toBeNull(); // Should pass - should be null + + // The output from transaction C should be voided (not accessible with getTxOutput) + const utxoC = await getTxOutput(mysql, txIdC, 0, false); + expect(utxoC).toBeNull(); // Should be null because it's voided + }); + + it('should handle voiding a transaction that spends already voided outputs', async () => { + expect.hasAssertions(); + + // Create transaction A that creates an output + const txIdA = 'test3-tx-a'; + const txIdB = 'test3-tx-b'; // Will be voided second + const txIdC = 'test3-tx-c'; // Will be voided first + const address1 = 'test3-address-1'; + const address2 = 'test3-address-2'; + const address3 = 'test3-address-3'; + const tokenId = '00'; + + await addOrUpdateTx(mysql, txIdA, 0, 1, 1, 100); + const outputA = createOutput(0, 100n, address1, tokenId); + await addUtxos(mysql, txIdA, [outputA], null); + + // Transaction B spends A's output + await addOrUpdateTx(mysql, txIdB, 0, 1, 1, 101); + const inputB = createInput(100n, address1, txIdA, 0, tokenId); + await updateTxOutputSpentBy(mysql, [inputB], txIdB); + const outputB = createOutput(0, 100n, address2, tokenId); + await addUtxos(mysql, txIdB, [outputB], null); + + // Only update address transaction counts (not balances) to prevent negative decrements + await mysql.query('INSERT INTO address (address, transactions) VALUES (?, 1) ON DUPLICATE KEY UPDATE transactions = transactions + 1', [address1]); + await mysql.query('INSERT INTO address (address, transactions) VALUES (?, 1) ON DUPLICATE KEY UPDATE transactions = transactions + 1', [address2]); + + // Transaction C spends B's output + await addOrUpdateTx(mysql, txIdC, 0, 1, 1, 102); + const inputC = createInput(100n, address2, txIdB, 0, tokenId); + await updateTxOutputSpentBy(mysql, [inputC], txIdC); + const outputC = createOutput(0, 100n, address3, tokenId); + await addUtxos(mysql, txIdC, [outputC], null); + + // Only update address transaction counts (not balances) to prevent negative decrements + await mysql.query('INSERT INTO address (address, transactions) VALUES (?, 1) ON DUPLICATE KEY UPDATE transactions = transactions + 1', [address2]); + await mysql.query('INSERT INTO address (address, transactions) VALUES (?, 1) ON DUPLICATE KEY UPDATE transactions = transactions + 1', [address3]); + + // First void transaction C + await voidTx(mysql, txIdC, + [createEventTxInput(100n, address2, txIdB, 0)], + [{ + value: 100n, + locked: false, + decoded: { type: 'P2PKH' as const, address: address3, timelock: null }, + token_data: 0, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + }], + [tokenId], + [], + 1 + ); + + // B's output should be unspent now (and it will be with the fix) + let utxoB = await getTxOutput(mysql, txIdB, 0, true); + expect(utxoB).not.toBeNull(); + expect(utxoB?.spentBy).toBeNull(); // Should pass - should be null + + // Now void transaction B + await voidTx(mysql, txIdB, + [createEventTxInput(100n, address1, txIdA, 0)], + [{ + value: 100n, + locked: false, + decoded: { type: 'P2PKH' as const, address: address2, timelock: null }, + token_data: 0, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + }], + [tokenId], + [], + 1 + ); + + // A's output should be unspent now + let utxoA = await getTxOutput(mysql, txIdA, 0, true); + expect(utxoA).not.toBeNull(); + expect(utxoA?.spentBy).toBeNull(); // Should pass - should be null + + // B's output should be voided (not accessible with getTxOutput) + utxoB = await getTxOutput(mysql, txIdB, 0, false); + expect(utxoB).toBeNull(); // Should be null because it's voided + }); + + it('should handle voiding when one input is already spent by another transaction', async () => { + expect.hasAssertions(); + + // This tests a double-spend scenario where we void a transaction + // that claims to spend UTXOs already spent by another transaction. + // This can happen during reorgs, network partitions, or double-spend attacks. + + const txIdA = 'test4-tx-a'; + const txIdB = 'test4-tx-b'; + const txIdC = 'test4-tx-c'; // Will try to spend A's output after B already spent it + const address1 = 'test4-address-1'; + const address2 = 'test4-address-2'; + const tokenId = '00'; + + // Create UTXO + await addOrUpdateTx(mysql, txIdA, 0, 1, 1, 100); + const outputA = createOutput(0, 100n, address1, tokenId); + await addUtxos(mysql, txIdA, [outputA], null); + + // Transaction B spends it + await addOrUpdateTx(mysql, txIdB, 0, 1, 1, 101); + const inputB = createInput(100n, address1, txIdA, 0, tokenId); + await updateTxOutputSpentBy(mysql, [inputB], txIdB); + + // Add output for transaction B + const outputB = createOutput(0, 100n, address2, tokenId); + await addUtxos(mysql, txIdB, [outputB], null); + + // Transaction C also tries to spend the same UTXO (double-spend scenario) + // In reality, this would be detected and prevented, but we're testing edge cases + await addOrUpdateTx(mysql, txIdC, 0, 1, 1, 102); + + // Add output for transaction C (this transaction exists but its input reference is invalid) + const outputC = createOutput(0, 100n, address2, tokenId); + await addUtxos(mysql, txIdC, [outputC], null); + + // Now void transaction C which claims to spend an already-spent output + const inputs = [createEventTxInput(100n, address1, txIdA, 0)]; + const outputs = [{ + value: 100n, + locked: false, + decoded: { type: 'P2PKH' as const, address: address2, timelock: null }, + token_data: 0, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + }]; + + await voidTx(mysql, txIdC, inputs, outputs, [tokenId], [], 1); + + // The UTXO should still be spent by B, not unspent + const utxo = await getTxOutput(mysql, txIdA, 0, false); + expect(utxo).not.toBeNull(); + expect(utxo?.spentBy).toBe(txIdB); // Should remain spent by B + }); + + it('should correctly unspent inputs with different token types', async () => { + expect.hasAssertions(); + + const txIdA = 'test5-tx-a'; + const txIdB = 'test5-tx-b'; + const address1 = 'test5-address-1'; + const address2 = 'test5-address-2'; + const hathorToken = '00'; + const customToken = 'custom-token-id'; + + // Create two UTXOs with different tokens + await addOrUpdateTx(mysql, txIdA, 0, 1, 1, 100); + const outputA1 = createOutput(0, 100n, address1, hathorToken); + const outputA2 = createOutput(1, 50n, address1, customToken); + await addUtxos(mysql, txIdA, [outputA1, outputA2], null); + + // Transaction B spends both + await addOrUpdateTx(mysql, txIdB, 0, 1, 1, 101); + const inputB1 = createInput(100n, address1, txIdA, 0, hathorToken); + const inputB2 = createInput(50n, address1, txIdA, 1, customToken); + await updateTxOutputSpentBy(mysql, [inputB1, inputB2], txIdB); + + // Add outputs for transaction B + const outputB1 = createOutput(0, 100n, address2, hathorToken); + const outputB2 = createOutput(1, 50n, address2, customToken); + await addUtxos(mysql, txIdB, [outputB1, outputB2], null); + + // Void transaction B + const inputs = [ + createEventTxInput(100n, address1, txIdA, 0), + createEventTxInput(50n, address1, txIdA, 1), + ]; + const outputs = [ + { + value: 100n, + locked: false, + decoded: { type: 'P2PKH' as const, address: address2, timelock: null }, + token_data: 0, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + }, + { + value: 50n, + locked: false, + decoded: { type: 'P2PKH' as const, address: address2, timelock: null }, + token_data: 0, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + } + ]; + + await voidTx(mysql, txIdB, inputs, outputs, [hathorToken, customToken], [], 1); + + // Both UTXOs should be unspent + const utxo1 = await getTxOutput(mysql, txIdA, 0, true); + const utxo2 = await getTxOutput(mysql, txIdA, 1, true); + + // These should pass with the implementation + expect(utxo1).not.toBeNull(); + expect(utxo1?.spentBy).toBeNull(); + expect(utxo2).not.toBeNull(); + expect(utxo2?.spentBy).toBeNull(); + }); + + it('should verify the complete flow with balance checks', async () => { + expect.hasAssertions(); + + // Complete integration test + const txIdA = 'test6-tx-a'; + const txIdB = 'test6-tx-b'; + const address1 = 'test6-address-1'; + const address2 = 'test6-address-2'; + const tokenId = '00'; + const value = 200n; + + // Setup initial UTXO + await addOrUpdateTx(mysql, txIdA, 0, 1, 1, 100); + const outputA = createOutput(0, value, address1, tokenId); + await addUtxos(mysql, txIdA, [outputA], null); + + // Verify initial state + await expect(checkUtxoTable(mysql, 1, txIdA, 0, tokenId, address1, value, 0, null, null, false, null)).resolves.toBe(true); + + // Create spending transaction + await addOrUpdateTx(mysql, txIdB, 0, 1, 1, 101); + const inputB = createInput(value, address1, txIdA, 0, tokenId); + await updateTxOutputSpentBy(mysql, [inputB], txIdB); + const outputB = createOutput(0, value, address2, tokenId); + await addUtxos(mysql, txIdB, [outputB], null); + + // Verify spent state + const spentUtxo = await getTxOutput(mysql, txIdA, 0, false); + expect(spentUtxo?.spentBy).toBe(txIdB); + + // Void the spending transaction + const inputs = [createEventTxInput(value, address1, txIdA, 0)]; + const outputs = [{ + value, + locked: false, + decoded: { type: 'P2PKH' as const, address: address2, timelock: null }, + token_data: 0, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + }]; + + await voidTx(mysql, txIdB, inputs, outputs, [tokenId], [], 1); + + // Verify the original UTXO is unspent again + const unspentUtxo = await getTxOutput(mysql, txIdA, 0, true); + expect(unspentUtxo).not.toBeNull(); + expect(unspentUtxo?.spentBy).toBeNull(); // This should pass + + // Also verify that B's outputs are marked as voided + const voidedUtxo = await getTxOutput(mysql, txIdB, 0, false); + expect(voidedUtxo).toBeNull(); // Should be null because it's voided + }); +}); + +describe('unspentTxOutputs function', () => { + it('should correctly unspent transaction outputs', async () => { + expect.hasAssertions(); + + // This tests the unspentTxOutputs function directly + const txIdA = 'test7-tx-a'; + const txIdB = 'test7-tx-b'; + const txIdC = 'test7-tx-c'; + const spendingTx = 'test7-spending-tx'; + const address = 'test7-address'; + const tokenId = '00'; + + // Create multiple UTXOs + await addOrUpdateTx(mysql, txIdA, 0, 1, 1, 100); + await addOrUpdateTx(mysql, txIdB, 0, 1, 1, 101); + await addOrUpdateTx(mysql, txIdC, 0, 1, 1, 102); + + const outputs = [ + createOutput(0, 50n, address, tokenId), + createOutput(0, 75n, address, tokenId), + createOutput(0, 100n, address, tokenId), + ]; + + await addUtxos(mysql, txIdA, [outputs[0]], null); + await addUtxos(mysql, txIdB, [outputs[1]], null); + await addUtxos(mysql, txIdC, [outputs[2]], null); + + // Mark them all as spent + const inputs = [ + createInput(50n, address, txIdA, 0, tokenId), + createInput(75n, address, txIdB, 0, tokenId), + createInput(100n, address, txIdC, 0, tokenId), + ]; + await updateTxOutputSpentBy(mysql, inputs, spendingTx); + + // Verify they are spent + let utxoA = await getTxOutput(mysql, txIdA, 0, false); + let utxoB = await getTxOutput(mysql, txIdB, 0, false); + let utxoC = await getTxOutput(mysql, txIdC, 0, false); + expect(utxoA?.spentBy).toBe(spendingTx); + expect(utxoB?.spentBy).toBe(spendingTx); + expect(utxoC?.spentBy).toBe(spendingTx); + + // Now unspent them + const txOutputsToUnspent: DbTxOutput[] = [ + { txId: txIdA, index: 0, tokenId, address, value: 50n, authorities: 0, timelock: null, heightlock: null, locked: false, spentBy: spendingTx, txProposalId: null, txProposalIndex: null }, + { txId: txIdB, index: 0, tokenId, address, value: 75n, authorities: 0, timelock: null, heightlock: null, locked: false, spentBy: spendingTx, txProposalId: null, txProposalIndex: null }, + { txId: txIdC, index: 0, tokenId, address, value: 100n, authorities: 0, timelock: null, heightlock: null, locked: false, spentBy: spendingTx, txProposalId: null, txProposalIndex: null }, + ]; + + await unspendUtxos(mysql, txOutputsToUnspent); + + // Verify they are unspent + utxoA = await getTxOutput(mysql, txIdA, 0, true); + utxoB = await getTxOutput(mysql, txIdB, 0, true); + utxoC = await getTxOutput(mysql, txIdC, 0, true); + expect(utxoA).not.toBeNull(); + expect(utxoA?.spentBy).toBeNull(); + expect(utxoB).not.toBeNull(); + expect(utxoB?.spentBy).toBeNull(); + expect(utxoC).not.toBeNull(); + expect(utxoC?.spentBy).toBeNull(); + }); +}); + + +describe('wallet balance voiding bug', () => { + it('should demonstrate wallet balance not being updated when voiding a transaction', async () => { + expect.hasAssertions(); + + const walletId = 'test-wallet'; + const address = 'test-address'; + const tokenId = '00'; + const txIdA = 'tx-a'; + const txIdB = 'tx-b'; + const initialValue = 100n; + + // Setup wallet and address + await setupWallet(mysql, walletId, [address]); + + // Create transaction A that creates an output to our wallet address + await addOrUpdateTx(mysql, txIdA, 0, 1, 1, 100); + const outputA = createOutput(0, initialValue, address, tokenId); + await addUtxos(mysql, txIdA, [outputA], null); + + // Manually insert wallet balance (simulating what updateWalletTablesWithTx would do) + await insertWalletBalance(mysql, walletId, tokenId, initialValue, 1); + + // Also insert into wallet_tx_history + await mysql.query( + `INSERT INTO \`wallet_tx_history\` (wallet_id, token_id, tx_id, balance, timestamp) + VALUES (?, ?, ?, ?, ?)`, + [walletId, tokenId, txIdA, initialValue, 100] + ); + + // Verify initial wallet balance + let walletBalance = await getWalletBalance(mysql, walletId, tokenId); + expect(walletBalance).not.toBeNull(); + expect(BigInt(walletBalance.unlocked_balance)).toBe(initialValue); + expect(walletBalance.transactions).toBe(1); + + // Create transaction B that spends the output from A + await addOrUpdateTx(mysql, txIdB, 0, 1, 1, 101); + const inputB = createInput(initialValue, address, txIdA, 0, tokenId); + await updateTxOutputSpentBy(mysql, [inputB], txIdB); + + // Add output for transaction B (sending to same address for simplicity) + const outputB = createOutput(0, initialValue, address, tokenId); + await addUtxos(mysql, txIdB, [outputB], null); + + // Update wallet balance for transaction B (net zero change) + await insertWalletBalance(mysql, walletId, tokenId, 0n, 1); + + // Add to wallet_tx_history + await mysql.query( + `INSERT INTO \`wallet_tx_history\` (wallet_id, token_id, tx_id, balance, timestamp) + VALUES (?, ?, ?, ?, ?)`, + [walletId, tokenId, txIdB, 0, 101] + ); + + // Verify wallet balance after transaction B + walletBalance = await getWalletBalance(mysql, walletId, tokenId); + expect(walletBalance).not.toBeNull(); + expect(BigInt(walletBalance.unlocked_balance)).toBe(initialValue); // Still 100n + expect(walletBalance.transactions).toBe(2); // Now 2 transactions + + // Now void transaction B + const inputs: EventTxInput[] = [createEventTxInput(initialValue, address, txIdA, 0)]; + const outputs = [{ + value: initialValue, + locked: false, + decoded: { + type: 'P2PKH' as const, + address: address, + timelock: null, + }, + token_data: 0, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + }]; + + await voidTx(mysql, txIdB, inputs, outputs, [tokenId], [], 1); + + // Check wallet balance after voiding + walletBalance = await getWalletBalance(mysql, walletId, tokenId); + expect(walletBalance).not.toBeNull(); + + // The transaction count should decrease from 2 to 1 + expect(walletBalance.transactions).toBe(1); // Should be back to 1 transaction + + // Check if wallet_tx_history was cleaned up + const historyCount = await getWalletTxHistoryCount(mysql, walletId, txIdB); + expect(historyCount).toBe(0); // Should be 0 after voiding + }); + + it('should demonstrate wallet balance inconsistency with multiple wallets', async () => { + expect.hasAssertions(); + + const wallet1Id = 'wallet-1'; + const wallet2Id = 'wallet-2'; + const address1 = 'address-1'; + const address2 = 'address-2'; + const tokenId = '00'; + const txId = 'transfer-tx'; + const amount = 150n; + + // Setup two wallets + await setupWallet(mysql, wallet1Id, [address1]); + await setupWallet(mysql, wallet2Id, [address2]); + + // Simulate wallet1 already having some balance + await insertWalletBalance(mysql, wallet1Id, tokenId, 200n, 1); + + // Create a transaction that transfers money from wallet1 to wallet2 + await addOrUpdateTx(mysql, txId, 0, 1, 1, 100); + + // Transaction sends money from wallet1 to wallet2 + const outputs = [ + createOutput(0, amount, address2, tokenId), // To wallet2 + ]; + await addUtxos(mysql, txId, outputs, null); + + // Simulate updating wallet balances for this transaction + // Wallet1 loses money (net -150n) + await mysql.query( + `UPDATE \`wallet_balance\` + SET unlocked_balance = unlocked_balance - ?, transactions = transactions + 1 + WHERE wallet_id = ? AND token_id = ?`, + [amount, wallet1Id, tokenId] + ); + + // Wallet2 gains money (+150n) + await insertWalletBalance(mysql, wallet2Id, tokenId, amount, 1); + + // Add wallet_tx_history entries + await mysql.query( + `INSERT INTO \`wallet_tx_history\` (wallet_id, token_id, tx_id, balance, timestamp) + VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)`, + [wallet1Id, tokenId, txId, -amount, 100, wallet2Id, tokenId, txId, amount, 100] + ); + + // Verify wallet balances before voiding + let wallet1Balance = await getWalletBalance(mysql, wallet1Id, tokenId); + let wallet2Balance = await getWalletBalance(mysql, wallet2Id, tokenId); + + expect(wallet1Balance).not.toBeNull(); + expect(BigInt(wallet1Balance.unlocked_balance)).toBe(50n); // 200 - 150 + expect(wallet1Balance.transactions).toBe(2); + + expect(wallet2Balance).not.toBeNull(); + expect(BigInt(wallet2Balance.unlocked_balance)).toBe(amount); + expect(wallet2Balance.transactions).toBe(1); + + // Now void the transaction + const inputs: EventTxInput[] = [createEventTxInput(amount, address1, 'some-previous-tx', 0)]; + const voidOutputs = [{ + value: amount, + locked: false, + decoded: { + type: 'P2PKH' as const, + address: address2, + timelock: null, + }, + token_data: 0, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + }]; + + await voidTx(mysql, txId, inputs, voidOutputs, [tokenId], [], 1); + + // Check wallet balances after voiding + wallet1Balance = await getWalletBalance(mysql, wallet1Id, tokenId); + wallet2Balance = await getWalletBalance(mysql, wallet2Id, tokenId); + + // Wallet1 should have its balance restored (50 + 150 = 200) + expect(BigInt(wallet1Balance.unlocked_balance)).toBe(200n); + expect(wallet1Balance.transactions).toBe(1); // Should decrease back to 1 + + // Wallet2 should have its balance reduced to 0 + if (wallet2Balance) { + expect(BigInt(wallet2Balance.unlocked_balance)).toBe(0n); + expect(wallet2Balance.transactions).toBe(0); // Should be 0 after voiding + } + + // Check wallet_tx_history cleanup + const wallet1HistoryCount = await getWalletTxHistoryCount(mysql, wallet1Id, txId); + const wallet2HistoryCount = await getWalletTxHistoryCount(mysql, wallet2Id, txId); + + expect(wallet1HistoryCount).toBe(0); + expect(wallet2HistoryCount).toBe(0); + }); + + it('should demonstrate the bug exists even with simple single wallet scenario', async () => { + expect.hasAssertions(); + + const walletId = 'simple-wallet'; + const address = 'simple-address'; + const tokenId = '00'; + const txId = 'simple-tx'; + const value = 50n; + + // Setup wallet + await setupWallet(mysql, walletId, [address]); + + // Create transaction + await addOrUpdateTx(mysql, txId, 0, 1, 1, 100); + const output = createOutput(0, value, address, tokenId); + await addUtxos(mysql, txId, [output], null); + + // Simulate wallet balance update + await insertWalletBalance(mysql, walletId, tokenId, value, 1); + await mysql.query( + `INSERT INTO \`wallet_tx_history\` (wallet_id, token_id, tx_id, balance, timestamp) + VALUES (?, ?, ?, ?, ?)`, + [walletId, tokenId, txId, value, 100] + ); + + // Verify wallet balance exists + let walletBalance = await getWalletBalance(mysql, walletId, tokenId); + expect(walletBalance).not.toBeNull(); + expect(BigInt(walletBalance.unlocked_balance)).toBe(value); + expect(walletBalance.transactions).toBe(1); + + // Void the transaction + const inputs: EventTxInput[] = []; + const outputs = [{ + value: value, + locked: false, + decoded: { + type: 'P2PKH' as const, + address: address, + timelock: null, + }, + token_data: 0, + script: 'dqkUH70YjKeoKdFwMX2TOYvGVbXOrKaIrA==', + }]; + + await voidTx(mysql, txId, inputs, outputs, [tokenId], [], 1); + + // Check wallet balance after voiding + walletBalance = await getWalletBalance(mysql, walletId, tokenId); + + if (walletBalance) { + expect(BigInt(walletBalance.unlocked_balance)).toBe(0n); // Should be 0 after voiding + expect(walletBalance.transactions).toBe(0); // Should be 0 after voiding + } + + // Check wallet_tx_history cleanup + const historyCount = await getWalletTxHistoryCount(mysql, walletId, txId); + expect(historyCount).toBe(0); // Should be 0 after voiding + }); + + test('should clear tx_proposal marks when voiding a transaction', async () => { + expect.hasAssertions(); + await cleanDatabase(mysql); + + const address1 = 'HBCQgVR8Xsyv3L8spWJLQCJkbgj1YABWMU'; + const tokenId = '00'; + const txProposalId = 'test-proposal-123'; + const txProposalIndex = 0; + + // Transaction A: Initial transaction with one output + const txIdA = 'txA'; + const outputsA = [createOutput(0, 100n, address1, tokenId)]; + await addOrUpdateTx(mysql, txIdA, 10, 1000, 1, 10); + await addUtxos(mysql, txIdA, outputsA, null); + + // Mark the UTXO with a tx_proposal (simulating it being selected for a transaction) + await mysql.query( + `UPDATE \`tx_output\` + SET \`tx_proposal\` = ?, + \`tx_proposal_index\` = ? + WHERE tx_id = ? AND \`index\` = ?`, + [txProposalId, txProposalIndex, txIdA, 0] + ); + + // Verify the tx_proposal is set + const utxoBeforeTx = await getTxOutput(mysql, txIdA, 0, false); + expect(utxoBeforeTx).not.toBeNull(); + expect(utxoBeforeTx!.txProposalId).toBe(txProposalId); + expect(utxoBeforeTx!.txProposalIndex).toBe(txProposalIndex); + + // Transaction B: Uses the output from A as input + const txIdB = 'txB'; + const inputsB = [createInput(100n, address1, txIdA, 0, tokenId)]; + const outputsB = [createOutput(0, 100n, address1, tokenId)]; + + await addOrUpdateTx(mysql, txIdB, 11, 1001, 1, 11); + await addUtxos(mysql, txIdB, outputsB, null); + await updateTxOutputSpentBy(mysql, inputsB, txIdB); + + // Verify the UTXO is marked as spent + const utxoAfterSpent = await getTxOutput(mysql, txIdA, 0, false); + expect(utxoAfterSpent).not.toBeNull(); + expect(utxoAfterSpent!.spentBy).toBe(txIdB); + + // Now void transaction B + const inputs = [createEventTxInput(100n, address1, txIdA, 0)]; + + const outputs = [{ + value: 100n, + token_data: 0, + script: 'dqkU', + decoded: { + type: 'P2PKH', + address: address1, + timelock: null, + }, + token: tokenId, + spent_by: null, + }]; + + await voidTx(mysql, txIdB, inputs, outputs, [tokenId], [], 1); + + // Check that the tx_proposal marks have been cleared + const utxoAfterVoid = await getTxOutput(mysql, txIdA, 0, false); + expect(utxoAfterVoid).not.toBeNull(); + expect(utxoAfterVoid!.txProposalId).toBeNull(); // Should be cleared + expect(utxoAfterVoid!.txProposalIndex).toBeNull(); // Should be cleared + expect(utxoAfterVoid!.spentBy).toBeNull(); // Should be unspent again + }); + + test('should clear tx_proposal marks for multiple inputs when voiding', async () => { + expect.hasAssertions(); + await cleanDatabase(mysql); + + const address1 = 'HBCQgVR8Xsyv3L8spWJLQCJkbgj1YABWMU'; + const tokenId = '00'; + const txProposalId = 'test-proposal-456'; + + // Create two initial UTXOs + const txIdA = 'txA'; + const outputsA = [ + createOutput(0, 50n, address1, tokenId), + createOutput(1, 50n, address1, tokenId) + ]; + await addOrUpdateTx(mysql, txIdA, 10, 1000, 1, 10); + await addUtxos(mysql, txIdA, outputsA, null); + + // Mark both UTXOs with the same tx_proposal + await mysql.query( + `UPDATE \`tx_output\` + SET \`tx_proposal\` = ?, + \`tx_proposal_index\` = ? + WHERE tx_id = ?`, + [txProposalId, 0, txIdA] + ); + + // Transaction B: Uses both outputs from A as inputs + const txIdB = 'txB'; + const inputsB = [ + createInput(50n, address1, txIdA, 0, tokenId), + createInput(50n, address1, txIdA, 1, tokenId) + ]; + const outputsB = [createOutput(0, 100n, address1, tokenId)]; + + await addOrUpdateTx(mysql, txIdB, 11, 1001, 1, 11); + await addUtxos(mysql, txIdB, outputsB, null); + await updateTxOutputSpentBy(mysql, inputsB, txIdB); + + // Prepare inputs for voidTx + const inputs = [ + createEventTxInput(50n, address1, txIdA, 0), + createEventTxInput(50n, address1, txIdA, 1) + ]; + + const outputs = [{ + value: 100n, + token_data: 0, + script: 'dqkU', + decoded: { + type: 'P2PKH', + address: address1, + timelock: null, + }, + token: tokenId, + spent_by: null, + }]; + + await voidTx(mysql, txIdB, inputs, outputs, [tokenId], [], 1); + + // Check that tx_proposal marks have been cleared for both inputs + const utxo1AfterVoid = await getTxOutput(mysql, txIdA, 0, false); + expect(utxo1AfterVoid).not.toBeNull(); + expect(utxo1AfterVoid!.txProposalId).toBeNull(); + expect(utxo1AfterVoid!.txProposalIndex).toBeNull(); + expect(utxo1AfterVoid!.spentBy).toBeNull(); + + const utxo2AfterVoid = await getTxOutput(mysql, txIdA, 1, false); + expect(utxo2AfterVoid).not.toBeNull(); + expect(utxo2AfterVoid!.txProposalId).toBeNull(); + expect(utxo2AfterVoid!.txProposalIndex).toBeNull(); + expect(utxo2AfterVoid!.spentBy).toBeNull(); + }); +}); diff --git a/packages/daemon/__tests__/utils.ts b/packages/daemon/__tests__/utils.ts index e70ed8ca..8a4e35f9 100644 --- a/packages/daemon/__tests__/utils.ts +++ b/packages/daemon/__tests__/utils.ts @@ -52,7 +52,7 @@ export const ADDRESSES = [ export const createOutput = ( index: number, - value: number, + value: bigint, address: string, token = '00', timelock: number | null = null, @@ -77,7 +77,7 @@ export const createOutput = ( ); export const createEventTxInput = ( - value: number, + value: bigint, address: string, txId: string, index: number, @@ -102,7 +102,7 @@ export const createEventTxInput = ( ); export const createInput = ( - value: number, + value: bigint, address: string, txId: string, index: number, @@ -132,7 +132,7 @@ export const checkUtxoTable = async ( index?: number, tokenId?: string, address?: string, - value?: number, + value?: bigint, authorities?: number, timelock?: number | null, heightlock?: number | null, @@ -363,8 +363,8 @@ export const checkAddressBalanceTable = async ( totalResults: number, address: string, tokenId: string, - unlocked: number, - locked: number, + unlocked: bigint, + locked: bigint, lockExpires: number | null, transactions: number, unlockedAuthorities = 0, @@ -789,3 +789,55 @@ export const generateFullNodeEvent = (event: any) => ({ data: event.data, }, }); + +// Helper function to create a wallet and addresses +export const setupWallet = async (mysql: MysqlConnection, walletId: string, addresses: string[]) => { + const now = Math.floor(Date.now() / 1000); // Unix timestamp + // Create wallet + await mysql.query( + `INSERT INTO \`wallet\` (id, xpubkey, auth_xpubkey, status, max_gap, created_at, ready_at) + VALUES (?, 'xpub123', 'xpub456', 'ready', 20, ?, ?)`, + [walletId, now, now] + ); + + // Add addresses to the wallet + const addressEntries = addresses.map((address, index) => [address, index, walletId, 1]); + await mysql.query( + `INSERT INTO \`address\` (address, \`index\`, wallet_id, transactions) + VALUES ?`, + [addressEntries] + ); +}; + +// Helper function to get wallet balance +export const getWalletBalance = async (mysql: MysqlConnection, walletId: string, tokenId: string) => { + const [results] = await mysql.query( + `SELECT * FROM \`wallet_balance\` WHERE \`wallet_id\` = ? AND \`token_id\` = ?`, + [walletId, tokenId] + ) as [any[], any]; + return results[0] || null; +}; + +// Helper function to manually insert wallet balance (simulating what should happen) +export const insertWalletBalance = async (mysql: MysqlConnection, walletId: string, tokenId: string, balance: bigint, transactions: number) => { + await mysql.query( + `INSERT INTO \`wallet_balance\` (wallet_id, token_id, unlocked_balance, locked_balance, + unlocked_authorities, locked_authorities, total_received, + transactions, timelock_expires) + VALUES (?, ?, ?, 0, 0, 0, ?, ?, NULL) + ON DUPLICATE KEY UPDATE + unlocked_balance = unlocked_balance + ?, + total_received = total_received + ?, + transactions = transactions + ?`, + [walletId, tokenId, balance, balance, transactions, balance, balance, transactions] + ); +}; + +// Helper function to get wallet transaction history count +export const getWalletTxHistoryCount = async (mysql: MysqlConnection, walletId: string, txId: string) => { + const [results] = await mysql.query( + `SELECT COUNT(*) as count FROM \`wallet_tx_history\` WHERE \`wallet_id\` = ? AND \`tx_id\` = ?`, + [walletId, txId] + ) as [any[], any]; + return results[0].count; +}; diff --git a/packages/daemon/__tests__/utils/retry.test.ts b/packages/daemon/__tests__/utils/retry.test.ts new file mode 100644 index 00000000..b2d4c52c --- /dev/null +++ b/packages/daemon/__tests__/utils/retry.test.ts @@ -0,0 +1,193 @@ +/** + * 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 { retryWithBackoff } from '../../src/utils/retry'; +import logger from '../../src/logger'; + +jest.mock('../../src/logger', () => ({ + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), +})); + +describe('retryWithBackoff', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should succeed on first attempt', async () => { + const mockFn = jest.fn().mockResolvedValue('success'); + + const promise = retryWithBackoff(mockFn); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + expect(mockFn).toHaveBeenCalledTimes(1); + }); + + it('should retry on network error and eventually succeed', async () => { + const mockFn = jest + .fn() + .mockRejectedValueOnce(new Error('Network error')) + .mockRejectedValueOnce(new Error('Network error')) + .mockResolvedValueOnce('success'); + + const promise = retryWithBackoff(mockFn, { + maxRetries: 3, + initialDelayMs: 100, + }); + + // Fast-forward through all timers + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + expect(mockFn).toHaveBeenCalledTimes(3); + expect(logger.warn).toHaveBeenCalledTimes(2); + }); + + it('should retry on 5xx server errors', async () => { + const serverError = { + response: { + status: 500, + }, + message: 'Internal Server Error', + }; + + const mockFn = jest + .fn() + .mockRejectedValueOnce(serverError) + .mockResolvedValueOnce('success'); + + const promise = retryWithBackoff(mockFn, { + maxRetries: 3, + initialDelayMs: 100, + }); + + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + expect(mockFn).toHaveBeenCalledTimes(2); + }); + + it('should not retry on 4xx client errors', async () => { + const clientError = { + response: { + status: 404, + }, + message: 'Not Found', + }; + + const mockFn = jest.fn().mockRejectedValue(clientError); + + await expect( + retryWithBackoff(mockFn, { + maxRetries: 3, + initialDelayMs: 100, + }) + ).rejects.toEqual(clientError); + + expect(mockFn).toHaveBeenCalledTimes(1); + }); + + it('should throw error after exhausting all retries', async () => { + const error = new Error('Network error'); + const mockFn = jest.fn().mockRejectedValue(error); + + const promise = retryWithBackoff(mockFn, { + maxRetries: 2, + initialDelayMs: 100, + }); + + // Run all timers and wait for promise to settle + const resultPromise = promise.catch((e) => e); + await jest.runAllTimersAsync(); + const result = await resultPromise; + + expect(result).toEqual(error); + expect(mockFn).toHaveBeenCalledTimes(3); // Initial + 2 retries + expect(logger.warn).toHaveBeenCalledTimes(2); + expect(logger.error).toHaveBeenCalledWith('All 2 retry attempts exhausted'); + }); + + it('should use exponential backoff', async () => { + const mockFn = jest + .fn() + .mockRejectedValueOnce(new Error('Fail 1')) + .mockRejectedValueOnce(new Error('Fail 2')) + .mockResolvedValueOnce('success'); + + const promise = retryWithBackoff(mockFn, { + maxRetries: 3, + initialDelayMs: 1000, + backoffMultiplier: 2, + }); + + // First retry should wait 1000ms + await jest.advanceTimersByTimeAsync(1000); + expect(mockFn).toHaveBeenCalledTimes(2); + + // Second retry should wait 2000ms + await jest.advanceTimersByTimeAsync(2000); + const result = await promise; + + expect(result).toBe('success'); + expect(mockFn).toHaveBeenCalledTimes(3); + }); + + it('should respect maxDelayMs', async () => { + const mockFn = jest + .fn() + .mockRejectedValueOnce(new Error('Fail 1')) + .mockRejectedValueOnce(new Error('Fail 2')) + .mockRejectedValueOnce(new Error('Fail 3')) + .mockResolvedValueOnce('success'); + + const promise = retryWithBackoff(mockFn, { + maxRetries: 4, + initialDelayMs: 1000, + maxDelayMs: 3000, + backoffMultiplier: 2, + }); + + // First retry: 1000ms + await jest.advanceTimersByTimeAsync(1000); + expect(mockFn).toHaveBeenCalledTimes(2); + + // Second retry: 2000ms + await jest.advanceTimersByTimeAsync(2000); + expect(mockFn).toHaveBeenCalledTimes(3); + + // Third retry: should be capped at 3000ms instead of 4000ms + await jest.advanceTimersByTimeAsync(3000); + const result = await promise; + + expect(result).toBe('success'); + expect(mockFn).toHaveBeenCalledTimes(4); + }); + + it('should use custom retryableErrors function', async () => { + const customError = new Error('Custom non-retryable error'); + const mockFn = jest.fn().mockRejectedValue(customError); + + await expect( + retryWithBackoff(mockFn, { + maxRetries: 3, + retryableErrors: (error) => error.message !== 'Custom non-retryable error', + }) + ).rejects.toThrow('Custom non-retryable error'); + + expect(mockFn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/daemon/__tests__/utils/wallet.test.ts b/packages/daemon/__tests__/utils/wallet.test.ts index b3584a26..e7a79ceb 100644 --- a/packages/daemon/__tests__/utils/wallet.test.ts +++ b/packages/daemon/__tests__/utils/wallet.test.ts @@ -15,13 +15,12 @@ import { prepareOutputs } from '../../src/utils'; describe('prepareOutputs', () => { it('should ignore NFT outputs', () => { const nftOutputs: EventTxOutput[] = [{ - value: 1, + value: 1n, token_data: 0, script: 'OmlwZnM6Ly9pcGZzL1FtTlJtNmhRUDN2MlVMclVOZTJQTTY4V1dRb2EyUmVwY1IxejVUVVdWZmd0bzGs', - // @ts-expect-error: This type is wrong, we should allow null here in the type decoded: null }, { - value: 2116, + value: 2116n, token_data: 0, script: 'dqkUCU1EY3YLi8WURhDOEsspok4Y0XiIrA==', decoded: { @@ -30,7 +29,7 @@ describe('prepareOutputs', () => { timelock: null, } }, { - value: 1, + value: 1n, token_data: 1, script: 'dqkUXO7BFkikXo2qwldGMeJlzyPSbtKIrA==', decoded: { diff --git a/packages/daemon/jest.config.js b/packages/daemon/jest.config.js index d23f73ed..3a1f11f5 100644 --- a/packages/daemon/jest.config.js +++ b/packages/daemon/jest.config.js @@ -1,5 +1,6 @@ module.exports = { roots: ["/__tests__"], + setupFiles: ['./jestSetup.ts'], testRegex: ".*\\.test\\.ts$", transform: { "^.+\\.ts$": ["ts-jest", { @@ -10,5 +11,6 @@ module.exports = { }] }, testPathIgnorePatterns: ['/__tests__/integration/'], - moduleFileExtensions: ["ts", "js", "json", "node"] + moduleFileExtensions: ["ts", "js", "json", "node"], + forceExit: true }; diff --git a/packages/daemon/jestSetup.ts b/packages/daemon/jestSetup.ts new file mode 100644 index 00000000..0e1420b6 --- /dev/null +++ b/packages/daemon/jestSetup.ts @@ -0,0 +1,13 @@ +/** + * 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 { stopGLLBackgroundTask } from '@hathor/wallet-lib'; + +Object.defineProperty(global, '_bitcore', { get() { return undefined; }, set() {} }); + +stopGLLBackgroundTask(); + diff --git a/packages/daemon/jest_integration.config.js b/packages/daemon/jest_integration.config.js index 2e30f587..86722d5e 100644 --- a/packages/daemon/jest_integration.config.js +++ b/packages/daemon/jest_integration.config.js @@ -6,6 +6,7 @@ const mainTestMatch = process.env.SPECIFIC_INTEGRATION_TEST_FILE module.exports = { roots: ["/__tests__"], + setupFiles: ['./jestSetup.ts'], transform: { "^.+\\.ts$": ["ts-jest", { tsconfig: "./tsconfig.json", diff --git a/packages/daemon/package.json b/packages/daemon/package.json index 759a4371..55ca89dd 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -21,7 +21,7 @@ "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": "jest --coverage --runInBand", "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", @@ -46,7 +46,7 @@ "typescript": "4.9.5" }, "peerDependencies": { - "@hathor/wallet-lib": "1.15.0", + "@hathor/wallet-lib": "2.8.3", "@wallet-service/common": "1.5.0" }, "dependencies": { @@ -62,6 +62,7 @@ "websocket": "1.0.33", "winston": "3.13.0", "ws": "8.13.0", - "xstate": "4.38.2" + "xstate": "4.38.2", + "zod": "3.23.8" } } diff --git a/packages/daemon/src/actions/index.ts b/packages/daemon/src/actions/index.ts index 91d6f4bd..dfd949c4 100644 --- a/packages/daemon/src/actions/index.ts +++ b/packages/daemon/src/actions/index.ts @@ -11,6 +11,7 @@ import { get } from 'lodash'; import logger from '../logger'; import { hashTxData } from '../utils'; import { createStartStreamMessage, createSendAckMessage } from '../actors'; +import { bigIntUtils } from '@hathor/wallet-lib'; /* * This action is used to store the initial event id on the context @@ -35,6 +36,7 @@ export const storeInitialState = assign({ * to the original event (that initiated the metadata diff check) */ export const unwrapEvent = assign({ + // @ts-ignore: The return event.event.originalEvent.event is not the correct type for an event. event: (_context: Context, event: Event) => { if (event.type !== 'METADATA_DECIDED') { throw new Error(`Received unhandled ${event.type} on unwrapEvent action`); @@ -164,11 +166,15 @@ export const metadataDecided = raise((_context: Context, event: Event) => ({ * Updates the cache with the last processed event (from the context) */ export const updateCache = (context: Context) => { + if (!context.txCache) { + throw new Error('TxCache was not initialized'); + } + const fullNodeEvent = context.event as StandardFullNodeEvent; if (!fullNodeEvent) { return; } - const { metadata, hash } = fullNodeEvent.event.data; + const { metadata, hash } = fullNodeEvent.event.data; const hashedTxData = hashTxData(metadata); context.txCache.set(hash, hashedTxData); @@ -193,4 +199,4 @@ export const stopHealthcheckPing = sendTo( /* * Logs the event as an error log */ -export const logEventError = (_context: Context, event: Event) => logger.error(JSON.stringify(event)); +export const logEventError = (_context: Context, event: Event) => logger.error(bigIntUtils.JSONBigInt.stringify(event)); diff --git a/packages/daemon/src/actors/WebSocketActor.ts b/packages/daemon/src/actors/WebSocketActor.ts index 8f15b7fd..aeff988c 100644 --- a/packages/daemon/src/actors/WebSocketActor.ts +++ b/packages/daemon/src/actors/WebSocketActor.ts @@ -6,10 +6,11 @@ */ import { WebSocket } from 'ws'; -import { Event } from '../types'; +import { Event, FullNodeEventSchema } from '../types'; import { get } from 'lodash'; import logger from '../logger'; import { getFullnodeWsUrl } from '../utils'; +import { bigIntUtils } from '@hathor/wallet-lib'; const PING_TIMEOUT = 30000; // 30s timeout const PING_INTERVAL = 5000; // Will ping every 5s @@ -23,7 +24,6 @@ export default (callback: any, receive: any) => { 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; @@ -47,7 +47,7 @@ export default (callback: any, receive: any) => { return; } - const payload = JSON.stringify(event.event); + const payload = bigIntUtils.JSONBigInt.stringify(event.event); logger.debug('Sending:') logger.debug(payload); @@ -68,13 +68,20 @@ export default (callback: any, receive: any) => { }; socket.onmessage = (socketEvent) => { - const event = JSON.parse(socketEvent.data.toString()); + const parseResult = FullNodeEventSchema.safeParse( + bigIntUtils.JSONBigInt.parse(socketEvent.data.toString()) + ); + if (!parseResult.success) { + logger.error(`Could not parse event: ${socketEvent.data.toString()}`); + throw new Error(parseResult.error.message); + } + const event = parseResult.data; const type = get(event, 'event.type'); logger.debug(`Received ${type}: ${get(event, 'event.id')} from socket.`, event); if (!type) { - logger.error(JSON.stringify(event)); + logger.error(bigIntUtils.JSONBigInt.stringify(event)); throw new Error('Received an event with no defined type'); } diff --git a/packages/daemon/src/config.ts b/packages/daemon/src/config.ts index 9055dd37..e5538dfc 100644 --- a/packages/daemon/src/config.ts +++ b/packages/daemon/src/config.ts @@ -44,6 +44,7 @@ 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 ?? 'local'; +export const SERVERLESS_DEPLOY_PREFIX = process.env.SERVERLESS_DEPLOY_PREFIX ?? 'hathor-wallet-service'; // Fullnode information, used to make sure we're connected to the same fullnode export const FULLNODE_PEER_ID = process.env.FULLNODE_PEER_ID; @@ -83,6 +84,9 @@ 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 +// ACK timeout configuration (in milliseconds) +export const ACK_TIMEOUT_MS = parseInt(process.env.ACK_TIMEOUT_MS ?? '20000', 10); // 20 seconds + // Other export const USE_SSL = process.env.USE_SSL === 'true'; @@ -111,6 +115,7 @@ export default () => ({ PUSH_NOTIFICATION_ENABLED, WALLET_SERVICE_LAMBDA_ENDPOINT, STAGE, + SERVERLESS_DEPLOY_PREFIX, ACCOUNT_ID, AWS_REGION, ALERT_MANAGER_REGION, @@ -121,6 +126,7 @@ export default () => ({ HEALTHCHECK_SERVER_URL, HEALTHCHECK_SERVER_API_KEY, HEALTHCHECK_PING_INTERVAL, + ACK_TIMEOUT_MS, REORG_SIZE_INFO, REORG_SIZE_MINOR, REORG_SIZE_MAJOR, diff --git a/packages/daemon/src/db/index.ts b/packages/daemon/src/db/index.ts index 7ea1ab56..68190df3 100644 --- a/packages/daemon/src/db/index.ts +++ b/packages/daemon/src/db/index.ts @@ -3,14 +3,13 @@ * * 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, OkPacket, Pool, ResultSetHeader } from 'mysql2/promise'; +*/ +import mysql, { Connection as MysqlConnection, Pool, ResultSetHeader } from 'mysql2/promise'; import { DbTxOutput, StringMap, Wallet, EventTxInput, - GenerateAddresses, AddressIndexMap, LastSyncedEvent, AddressBalance, @@ -20,6 +19,8 @@ import { Miner, TokenSymbolsRow, MaxAddressIndexRow, + AddressesWalletsRow, + AddressRow, } from '../types'; import { TxInput, @@ -27,6 +28,7 @@ import { TxOutputWithIndex, } from '@wallet-service/common'; import { isAuthority } from '@wallet-service/common'; +import { getWalletBalanceMap } from '../utils/wallet'; import { AddressBalanceRow, AddressTxHistorySumRow, @@ -38,6 +40,7 @@ import { TxOutputRow, } from '../types'; import getConfig from '../config'; +import { constants } from '@hathor/wallet-lib'; let pool: Pool; @@ -126,8 +129,8 @@ export const addUtxos = async ( let value = output.value; if (isAuthority(output.token_data)) { - authorities = value; - value = 0; + authorities = Number(value); + value = 0n; } return [ @@ -140,6 +143,7 @@ export const addUtxos = async ( output.decoded?.timelock, heightlock, output.locked, + null, // spent_by - initially null for new UTXOs ]; }, ); @@ -148,7 +152,7 @@ export const addUtxos = async ( await mysql.query( `INSERT INTO \`tx_output\` (\`tx_id\`, \`index\`, \`token_id\`, \`value\`, \`authorities\`, \`address\`, - \`timelock\`, \`heightlock\`, \`locked\`) + \`timelock\`, \`heightlock\`, \`locked\`, \`spent_by\`) VALUES ? ON DUPLICATE KEY UPDATE tx_id=tx_id`, [entries], @@ -220,7 +224,7 @@ export const getTxOutputsFromTx = async ( index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, @@ -244,7 +248,7 @@ export const getTxOutputsFromTx = async ( */ export const getTxOutputs = async ( mysql: any, - inputs: {txId: string, index: number}[], + inputs: { txId: string, index: number }[], ): Promise => { if (inputs.length <= 0) return []; const txIdIndexPair = inputs.map((utxo) => [utxo.txId, utxo.index]); @@ -262,7 +266,7 @@ export const getTxOutputs = async ( index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, @@ -345,12 +349,11 @@ export const getTxOutputsAtHeight = async ( index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, - // @ts-ignore - locked: result.locked > 0, + locked: Number(result.locked) > 0, spentBy: result.spent_by as string, txProposalId: result.tx_proposal as string, txProposalIndex: result.tx_proposal_index as number, @@ -362,39 +365,28 @@ export const getTxOutputsAtHeight = async ( }; /** - * Void a transaction by updating the related address and balance information in the database. + * Void address-related information when voiding a transaction. * * @param mysql - The MySQL connection object * @param txId - The ID of the transaction to be voided. * @param addressBalanceMap - A map where the key is an address and the value is a map of token balances. * The TokenBalanceMap contains information about the total amount sent, unlocked and locked amounts, and authorities. * - * @returns {Promise} - A promise that resolves when the transaction has been voided and the database updated + * @returns {Promise} - A promise that resolves when the address-related data has been updated * * This function performs the following steps: * 1. Inserts addresses with a transaction count of 0 into the `address` table or subtracts 1 from the transaction count if they already exist * 2. Iterates over the addressBalanceMap to update the `address_balance` table with the received token balances. * 3. Deletes the transaction entry from the `address_tx_history` table. - * 4. Updates the transaction entry in the `transaction` table to mark it as voided. * * The function ensures that the authorities are correctly updated and the smallest timelock expiration value is preserved. */ -export const voidTransaction = async ( +export const voidAddressTransaction = async ( mysql: any, txId: string, addressBalanceMap: StringMap, + version: number, ): Promise => { - const [result]: [ResultSetHeader] = await mysql.query( - `UPDATE \`transaction\` - SET \`voided\` = TRUE - WHERE \`tx_id\` = ?`, - [txId], - ); - - if (result.affectedRows !== 1) { - throw new Error('Tried to void a transaction that is not in the database.'); - } - const addressEntries = Object.keys(addressBalanceMap).map((address) => [address, 0]); if (addressEntries.length > 0) { @@ -406,49 +398,57 @@ export const voidTransaction = async ( ); } + // Check if this is a token creation transaction + const isCreateTokenTx = version === constants.CREATE_TOKEN_TX_VERSION; + 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], + // Check if address_balance entry exists first + const [existingRows] = await mysql.query( + 'SELECT * FROM address_balance WHERE address = ? AND token_id = ?', + [address, token] ); + if (existingRows.length > 0) { + // Entry exists, perform UPDATE to subtract values + await mysql.query( + `UPDATE address_balance + SET total_received = total_received - ?, + unlocked_balance = unlocked_balance - ?, + locked_balance = locked_balance - ?, + transactions = transactions - 1, + timelock_expires = CASE + WHEN timelock_expires IS NULL THEN ? + WHEN ? IS NULL THEN timelock_expires + ELSE LEAST(timelock_expires, ?) + END, + unlocked_authorities = (unlocked_authorities | ?), + locked_authorities = locked_authorities | ? + WHERE address = ? AND token_id = ?`, + [ + tokenBalance.totalAmountSent, + tokenBalance.unlockedAmount, + tokenBalance.lockedAmount, + tokenBalance.lockExpires, + tokenBalance.lockExpires, + tokenBalance.lockExpires, + tokenBalance.unlockedAuthorities.toUnsignedInteger(), + tokenBalance.lockedAuthorities.toUnsignedInteger(), + address, + token + ] + ); + } else { + // Entry doesn't exist, this means the balance was never added in the first place + // This shouldn't happen since we receive events in order + console.warn(`warning: Trying to void transaction for address ${address} token ${token} but no balance entry exists`); + } + // 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()) { + if (!tokenBalance.unlockedAuthorities.hasNegativeValue()) { await mysql.query( `UPDATE \`address_balance\` SET \`unlocked_authorities\` = ( @@ -468,6 +468,32 @@ export const voidTransaction = async ( // 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. + + // If the address_balance is now zeroed and the number of transactions + // is also zero, it means that the transaction was removed from address_tx_history + // so we need to remove it from the `address_balance` table. + await mysql.query( + `DELETE FROM address_balance + WHERE address = ? + AND token_id = ? + AND total_received = 0 + AND unlocked_balance = 0 + AND locked_balance = 0 + AND unlocked_authorities = 0 + AND locked_authorities = 0 + AND transactions = 0`, + [address, token] + ); + + if (isCreateTokenTx) { + // The transaction that created the token was voided, so we can remove + // it from the tokens table as well. + await mysql.query( + `DELETE FROM token + WHERE id = ?`, + [token] + ); + } } } @@ -478,6 +504,143 @@ export const voidTransaction = async ( ); }; +/** + * Void a transaction by updating the transaction table to mark it as voided. + * + * @param mysql - The MySQL connection object + * @param txId - The ID of the transaction to be voided. + * + * @returns {Promise} - A promise that resolves when the transaction has been marked as voided + */ +export const voidTransaction = async ( + mysql: any, + txId: string, +): Promise => { + const [result]: [ResultSetHeader] = await mysql.query( + `UPDATE \`transaction\` + SET \`voided\` = TRUE + WHERE \`tx_id\` = ?`, + [txId], + ); + + if (result.affectedRows !== 1) { + throw new Error('Tried to void a transaction that is not in the database.'); + } +}; + +/** + * Void a transaction by updating the related wallet balance and transaction information in the database. + * + * @param mysql - The MySQL connection object + * @param txId - The ID of the transaction to be voided. + * @param addressBalanceMap - A map where the key is an address and the value is a map of token balances. + * The TokenBalanceMap contains information about the total amount sent, unlocked and locked amounts, and authorities. + * + * @returns {Promise} - A promise that resolves when the transaction has been voided and the wallet tables updated + * + * This function performs the following steps: + * 1. Gets wallet information for all affected addresses + * 2. Builds wallet balance map from the address balance changes + * 3. Iterates over the walletBalanceMap to update the `wallet_balance` table by reversing the transaction's balance changes. + * 4. Deletes the transaction entry from the `wallet_tx_history` table. + * 5. Updates authority columns correctly when authorities are removed. + * + * The function ensures that wallet balances are correctly reverted and transaction counts are decremented. + */ +export const voidWalletTransaction = async ( + mysql: MysqlConnection, + txId: string, + addressBalanceMap: StringMap +): Promise => { + // Get wallet information for all affected addresses + const addressWalletMap: StringMap = await getAddressWalletInfo(mysql, Object.keys(addressBalanceMap)); + + if (Object.keys(addressWalletMap).length === 0) { + // No wallets to update + return; + } + + // Build wallet balance map from the address balance changes + const walletBalanceMap: StringMap = getWalletBalanceMap(addressWalletMap, addressBalanceMap); + + if (Object.keys(walletBalanceMap).length === 0) { + // No wallet balances to update + return; + } + + for (const [walletId, tokenMap] of Object.entries(walletBalanceMap)) { + for (const [token, tokenBalance] of tokenMap.iterator()) { + // Update wallet_balance table by reversing the transaction's impact + await mysql.query( + `UPDATE \`wallet_balance\` + SET total_received = total_received - ?, + unlocked_balance = unlocked_balance - ?, + locked_balance = locked_balance - ?, + transactions = transactions - 1, + unlocked_authorities = (unlocked_authorities | ?), + locked_authorities = locked_authorities | ? + WHERE wallet_id = ? AND token_id = ?`, + [ + tokenBalance.totalAmountSent, + tokenBalance.unlockedAmount, + tokenBalance.lockedAmount, + tokenBalance.unlockedAuthorities.toUnsignedInteger(), + tokenBalance.lockedAuthorities.toUnsignedInteger(), + walletId, + token + ], + ); + + // If we're removing any of the authorities, we need to refresh the + // authority columns because we might have more than one, so we need to + // calculate the complete state from the complete wallet point of view, + // not just from a single transaction balance point of view. + + // NOTE: No need to do the same for locked authorities as they can't be + // spent before being unlocked and we trust the fullnode + if (!tokenBalance.unlockedAuthorities.hasNegativeValue()) { + 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], + ); + } + + // If the number of transactions is zero, it means that this transaction + // was removed from the wallet_tx_history as well, so we must delete the + // row + await mysql.query( + `DELETE FROM wallet_balance + WHERE wallet_id = ? + AND token_id = ? + AND total_received = 0 + AND unlocked_balance = 0 + AND locked_balance = 0 + AND unlocked_authorities = 0 + AND locked_authorities = 0 + AND transactions = 0`, + [walletId, token] + ); + } + } + + // Delete wallet transaction history entries for the voided transaction + await mysql.query( + `DELETE FROM \`wallet_tx_history\` + WHERE \`tx_id\` = ?`, + [txId], + ); +}; + /** * Update addresses tables with a new transaction. * @@ -529,7 +692,7 @@ export const updateAddressTablesWithTx = async ( // 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), + unlocked_balance: (tokenBalance.unlockedAmount < 0n ? 0n : 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(), @@ -659,12 +822,11 @@ export const getUtxosLockedAtHeight = async ( index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, - // @ts-ignore - locked: result.locked > 0, + locked: Number(result.locked) > 0, }; utxos.push(utxo); } @@ -715,12 +877,12 @@ export const updateAddressLockedBalance = async ( \`unlocked_authorities\` = (unlocked_authorities | ?) WHERE \`address\` = ? AND \`token_id\` = ?`, [ - tokenBalance.unlockedAmount, - tokenBalance.unlockedAmount, - tokenBalance.unlockedAuthorities.toInteger(), - address, - token, - ], + tokenBalance.unlockedAmount, + tokenBalance.unlockedAmount, + tokenBalance.unlockedAuthorities.toInteger(), + address, + token, + ], ); // if any authority has been unlocked, we have to refresh the locked authorities @@ -756,7 +918,7 @@ export const updateAddressLockedBalance = async ( ) WHERE \`address\` = ? AND \`token_id\` = ?`, - [address, token, address, token]); + [address, token, address, token]); } } } @@ -779,7 +941,7 @@ export const getAddressWalletInfo = async (mysql: MysqlConnection, addresses: st } const addressWalletMap: StringMap = {}; - const [results] = await mysql.query( + const [results, _] = await mysql.query( `SELECT DISTINCT a.\`address\`, a.\`wallet_id\`, w.\`auth_xpubkey\`, @@ -793,15 +955,14 @@ export const getAddressWalletInfo = async (mysql: MysqlConnection, addresses: st [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, + walletId: entry.wallet_id, + authXpubkey: entry.auth_xpubkey, + xpubkey: entry.xpubkey, + maxGap: entry.max_gap, }; - addressWalletMap[entry.address as string] = walletInfo; + addressWalletMap[entry.address] = walletInfo; } return addressWalletMap; }; @@ -832,7 +993,7 @@ export const updateWalletLockedBalance = async ( WHERE \`wallet_id\` = ? AND \`token_id\` = ?`, [tokenBalance.unlockedAmount, tokenBalance.unlockedAmount, - tokenBalance.unlockedAuthorities.toInteger(), walletId, token], + tokenBalance.unlockedAuthorities.toInteger(), walletId, token], ); // if any authority has been unlocked, we have to refresh the locked authorities @@ -957,7 +1118,7 @@ export const mapDbResultToDbTxOutput = (result: TxOutputRow): DbTxOutput => ({ index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, @@ -1024,7 +1185,7 @@ export const getLockedUtxoFromInputs = async (mysql: MysqlConnection, inputs: Ev index: utxo.index as number, tokenId: utxo.token_id as string, address: utxo.address as string, - value: utxo.value as number, + value: BigInt(utxo.value), authorities: utxo.authorities as number, timelock: utxo.timelock as number, heightlock: utxo.heightlock as number, @@ -1234,7 +1395,7 @@ export const getTxOutputsBySpent = async ( index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, @@ -1250,6 +1411,27 @@ export const getTxOutputsBySpent = async ( return utxos; }; +/** + * Get all UTXOs that were spent by a specific transaction + * + * @param mysql - Database connection + * @param spendingTxId - The transaction ID that spent the UTXOs + * @returns A list of DbTxOutput objects that were spent by the transaction + */ +export const getUtxosSpentByTx = async ( + mysql: MysqlConnection, + spendingTxId: string, +): Promise => { + const [results] = await mysql.query( + `SELECT * + FROM \`tx_output\` + WHERE \`spent_by\` = ?`, + [spendingTxId] + ); + + return results.map(mapDbResultToDbTxOutput); +}; + /** * Set a list of tx_outputs as unspent * @@ -1290,7 +1472,7 @@ export const markUtxosAsVoided = async ( UPDATE \`tx_output\` SET \`voided\` = TRUE WHERE \`tx_id\` IN (?)`, - [txIds]); + [txIds]); }; export const updateLastSyncedEvent = async ( @@ -1302,7 +1484,7 @@ export const updateLastSyncedEvent = async ( VALUES (0, ?) ON DUPLICATE KEY UPDATE last_event_id = ?`, - [lastEventId, lastEventId]); + [lastEventId, lastEventId]); }; export const getLastSyncedEvent = async ( @@ -1366,8 +1548,8 @@ export const fetchAddressBalance = async ( 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, + unlockedBalance: BigInt(result.unlocked_balance), + lockedBalance: BigInt(result.locked_balance), lockedAuthorities: result.locked_authorities as number, unlockedAuthorities: result.unlocked_authorities as number, timelockExpires: result.timelock_expires as number, @@ -1405,7 +1587,7 @@ export const fetchAddressTxHistorySum = async ( return results.map((result): AddressTotalBalance => ({ address: result.address as string, tokenId: result.token_id as string, - balance: parseInt(result.balance), + balance: BigInt(result.balance), transactions: parseInt(result.transactions), })); }; @@ -1429,7 +1611,7 @@ export const getTxOutputsHeightUnlockedAtHeight = async ( index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, @@ -1505,12 +1687,40 @@ export const cleanupVoidedTx = async (mysql: MysqlConnection, txId: string): Pro ); }; +/** + * Clear tx_proposal and tx_proposal_index columns for outputs from a voided transaction. + * This is necessary to release UTXOs that were marked for a transaction proposal + * but the transaction ended up being voided. + * + * @param mysql - Database connection + * @param txInputs - The inputs of the voided transaction to be released + */ +export const clearTxProposalForVoidedTx = async ( + mysql: MysqlConnection, + txInputs: TxInput[], +): Promise => { + if (txInputs.length === 0) return; + + // Build the WHERE clause for all input pairs + const whereClauses = txInputs.map(() => '(tx_id = ? AND `index` = ?)').join(' OR '); + const params = txInputs.flatMap(input => [input.tx_id, input.index]); + + await mysql.query( + `UPDATE \`tx_output\` + SET \`tx_proposal\` = NULL, + \`tx_proposal_index\` = NULL + WHERE (${whereClauses}) + AND \`tx_proposal\` IS NOT NULL`, + params, + ); +}; + /** * 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) + * @returns The token information (or empty object if id is not found) * * @todo This method is duplicated from the wallet-service lambdas, * we should have common methods for both packages @@ -1518,17 +1728,16 @@ export const cleanupVoidedTx = async (mysql: MysqlConnection, txId: string): Pro export const getTokenSymbols = async ( mysql: MysqlConnection, tokenIdList: string[], -): Promise | null> => { - if (tokenIdList.length === 0) return null; +): Promise> => { + if (tokenIdList.length === 0) return {}; const [results] = await mysql.query( 'SELECT `id`, `symbol` FROM `token` WHERE `id` IN (?)', [tokenIdList], ); - if (results.length === 0) return null; + if (results.length === 0) return {}; 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; @@ -1560,8 +1769,8 @@ export const getTokenSymbols = async ( */ export const getMaxIndicesForWallets = async ( mysql: MysqlConnection, - walletData: Array<{walletId: string, addresses: string[]}> -): Promise> => { + walletData: Array<{ walletId: string, addresses: string[] }> +): Promise> => { if (walletData.length === 0) { return new Map(); } @@ -1588,3 +1797,57 @@ export const getMaxIndicesForWallets = async ( } ])); }; + +/** + * Get a single address information. + * + * @param mysql - Database connection + * @param address - which address to fetch information from + * @returns Address information if address is known or null + */ +export async function getAddressInfo(mysql: MysqlConnection, address: string): Promise { + const [results] = await mysql.query( + 'SELECT * FROM address WHERE address = ?', [address], + ); + + if (results.length === 0) { + return null; + } + + return results[0]; +} + +/** + * Get an address seqnum. + * + * @param mysql - Database connection + * @param address - which address to fetch information from + */ +export async function getAddressSeqnum(mysql: MysqlConnection, address: string): Promise { + const addressInfo = await getAddressInfo(mysql, address); + if (!addressInfo) { + // If the address does not exist on the database, then its seqnum must be 0. + return 0; + } + + return addressInfo.seqnum; +} + + +/** + * Set an address seqnum. + * + * @param mysql - Database connection + * @param address - which address to fetch information from + * @param seqnum - seqnum value to upsert + */ +export async function setAddressSeqnum(mysql: MysqlConnection, address: string, seqnum: number): Promise { + const entries = [[address, 1, seqnum]]; + + await mysql.query( + `INSERT INTO \`address\` (address, transactions, seqnum) + VALUES ? + ON DUPLICATE KEY UPDATE seqnum = ?`, + [entries, seqnum], + ); +} diff --git a/packages/daemon/src/delays/index.ts b/packages/daemon/src/delays/index.ts index 0ff5a4c9..45ab1ff0 100644 --- a/packages/daemon/src/delays/index.ts +++ b/packages/daemon/src/delays/index.ts @@ -6,6 +6,7 @@ */ import { Context } from '../types'; +import getConfig from '../config'; const RETRY_BACKOFF_INCREASE = 1000; // 1s increase in the backoff strategy const MAX_BACKOFF_RETRIES = 10; // The retry backoff will top at 10s @@ -17,3 +18,9 @@ export const BACKOFF_DELAYED_RECONNECT = (context: Context) => { return context.retryAttempt * RETRY_BACKOFF_INCREASE; }; + +// Timeout to check for missed events after ACK (configurable via ACK_TIMEOUT_MS env var) +export const ACK_TIMEOUT = () => { + const { ACK_TIMEOUT_MS } = getConfig(); + return ACK_TIMEOUT_MS; +}; diff --git a/packages/daemon/src/guards/index.ts b/packages/daemon/src/guards/index.ts index e6162c4a..ba850ae0 100644 --- a/packages/daemon/src/guards/index.ts +++ b/packages/daemon/src/guards/index.ts @@ -195,8 +195,8 @@ export const voided = (_context: Context, event: Event) => { } if (event.event.event.type !== FullNodeEventTypes.VERTEX_METADATA_CHANGED - && event.event.event.type !== FullNodeEventTypes.NEW_VERTEX_ACCEPTED) { - return false; + && event.event.event.type !== FullNodeEventTypes.NEW_VERTEX_ACCEPTED) { + return false; } const fullNodeEvent = event.event.event; @@ -218,7 +218,7 @@ export const unchanged = (context: Context, event: Event) => { } if (event.event.event.type !== FullNodeEventTypes.VERTEX_METADATA_CHANGED - && event.event.event.type !== FullNodeEventTypes.NEW_VERTEX_ACCEPTED) { + && event.event.event.type !== FullNodeEventTypes.NEW_VERTEX_ACCEPTED) { // Not unchanged return false; @@ -227,6 +227,10 @@ export const unchanged = (context: Context, event: Event) => { const { data } = event.event.event; const txCache = context.txCache; + if (!txCache) { + throw new Error('txCache is not initialized in context'); + } + const txHashFromCache = txCache.get(data.hash); // Not on the cache, it's not unchanged. if (!txHashFromCache) { @@ -248,3 +252,15 @@ export const reorgStarted = (_context: Context, event: Event) => { return event.event.event.type === FullNodeEventTypes.REORG_STARTED; }; + +/* + * This guard checks if the checkForMissedEvents service found new events + * that we missed due to network packet loss + */ +export const hasNewEvents = (_context: Context, event: any) => { + if (!event.data) { + return false; + } + + return event.data.hasNewEvents === true; +}; diff --git a/packages/daemon/src/index.ts b/packages/daemon/src/index.ts index 49e3d308..571c6dac 100644 --- a/packages/daemon/src/index.ts +++ b/packages/daemon/src/index.ts @@ -9,6 +9,7 @@ import { interpret } from 'xstate'; import { SyncMachine } from './machines'; import logger from './logger'; import { checkEnvVariables } from './config'; +import { bigIntUtils } from '@hathor/wallet-lib'; const main = async () => { checkEnvVariables(); @@ -16,11 +17,11 @@ const main = async () => { const machine = interpret(SyncMachine); machine.onTransition((state) => { - logger.info(`Transitioned to ${JSON.stringify(state.value)}`); + logger.info(`Transitioned to ${bigIntUtils.JSONBigInt.stringify(state.value)}`); }); machine.onEvent((event) => { - logger.info(`Processing event: ${JSON.stringify(event.type)}`); + logger.info(`Processing event: ${bigIntUtils.JSONBigInt.stringify(event.type)}`); }); machine.start(); diff --git a/packages/daemon/src/machines/SyncMachine.ts b/packages/daemon/src/machines/SyncMachine.ts index 668a002a..7bb50b2c 100644 --- a/packages/daemon/src/machines/SyncMachine.ts +++ b/packages/daemon/src/machines/SyncMachine.ts @@ -26,6 +26,7 @@ import { fetchInitialState, handleUnvoidedTx, handleReorgStarted, + checkForMissedEvents, } from '../services'; import { metadataIgnore, @@ -43,6 +44,7 @@ import { unchanged, vertexRemoved, reorgStarted, + hasNewEvents, } from '../guards'; import { storeInitialState, @@ -58,7 +60,7 @@ import { startHealthcheckPing, stopHealthcheckPing, } from '../actions'; -import { BACKOFF_DELAYED_RECONNECT } from '../delays'; +import { BACKOFF_DELAYED_RECONNECT, ACK_TIMEOUT } from '../delays'; import getConfig from '../config'; export const SYNC_MACHINE_STATES = { @@ -79,6 +81,7 @@ export const CONNECTED_STATES = { handlingUnvoidedTx: 'handlingUnvoidedTx', handlingFirstBlock: 'handlingFirstBlock', handlingReorgStarted: 'handlingReorgStarted', + checkingForMissedEvents: 'checkingForMissedEvents', }; const { TX_CACHE_SIZE } = getConfig(); @@ -92,11 +95,12 @@ export const SyncMachine = Machine({ retryAttempt: 0, event: null, initialEventId: null, - txCache: new LRU(TX_CACHE_SIZE), + txCache: null, }, states: { [SYNC_MACHINE_STATES.INITIALIZING]: { entry: assign({ + txCache: () => new LRU(TX_CACHE_SIZE), healthcheck: () => spawn(HealthCheckActor), }), invoke: { @@ -136,6 +140,11 @@ export const SyncMachine = Machine({ states: { [CONNECTED_STATES.idle]: { id: CONNECTED_STATES.idle, + after: { + ACK_TIMEOUT: { + target: CONNECTED_STATES.checkingForMissedEvents, + }, + }, on: { FULLNODE_EVENT: [{ cond: 'invalidStreamId', @@ -287,6 +296,22 @@ export const SyncMachine = Machine({ onError: `#${SYNC_MACHINE_STATES.ERROR}`, }, }, + [CONNECTED_STATES.checkingForMissedEvents]: { + id: CONNECTED_STATES.checkingForMissedEvents, + invoke: { + src: 'checkForMissedEvents', + onDone: [{ + cond: 'hasNewEvents', + target: `#SyncMachine.${SYNC_MACHINE_STATES.RECONNECTING}`, + }, { + target: CONNECTED_STATES.idle, + }], + onError: { + // Critical failure - we cannot verify event integrity + target: `#${SYNC_MACHINE_STATES.ERROR}`, + }, + }, + }, }, on: { WEBSOCKET_EVENT: [{ @@ -312,6 +337,7 @@ export const SyncMachine = Machine({ fetchInitialState, metadataDiff, updateLastSyncedEvent, + checkForMissedEvents, }, guards: { metadataIgnore, @@ -329,8 +355,9 @@ export const SyncMachine = Machine({ unchanged, vertexRemoved, reorgStarted, + hasNewEvents, }, - delays: { BACKOFF_DELAYED_RECONNECT }, + delays: { BACKOFF_DELAYED_RECONNECT, ACK_TIMEOUT }, actions: { storeInitialState, unwrapEvent, diff --git a/packages/daemon/src/services/index.ts b/packages/daemon/src/services/index.ts index aa8ff9a7..87e5a30f 100644 --- a/packages/daemon/src/services/index.ts +++ b/packages/daemon/src/services/index.ts @@ -23,6 +23,8 @@ import { WalletStatus, FullNodeEventTypes, StandardFullNodeEvent, + EventTxHeader, + isNanoHeader, } from '../types'; import { TxInput, @@ -63,17 +65,25 @@ import { addNewAddresses, updateWalletTablesWithTx, voidTransaction, + voidAddressTransaction, updateLastSyncedEvent as dbUpdateLastSyncedEvent, getLastSyncedEvent, getTxOutputsFromTx, markUtxosAsVoided, cleanupVoidedTx, getMaxIndicesForWallets, + setAddressSeqnum, + getAddressSeqnum, + unspendUtxos, + voidWalletTransaction, + getTxOutput, + clearTxProposalForVoidedTx, } from '../db'; import getConfig from '../config'; import logger from '../logger'; -import { invokeOnTxPushNotificationRequestedLambda } from '../utils'; +import { invokeOnTxPushNotificationRequestedLambda, getDaemonUptime, retryWithBackoff } from '../utils'; import { addAlert, Severity } from '@wallet-service/common'; +import { JSONBigInt } from '@hathor/wallet-lib/lib/utils/bigint'; export const METADATA_DIFF_EVENT_TYPES = { IGNORE: 'IGNORE', @@ -83,6 +93,8 @@ export const METADATA_DIFF_EVENT_TYPES = { TX_FIRST_BLOCK: 'TX_FIRST_BLOCK', }; +const DUPLICATE_TX_ALERT_GRACE_PERIOD = 10; // seconds + export const metadataDiff = async (_context: Context, event: Event) => { const mysql = await getDbConnection(); @@ -164,12 +176,22 @@ export const metadataDiff = async (_context: Context, event: Event) => { export const isBlock = (version: number): boolean => version === hathorLib.constants.BLOCK_VERSION || version === hathorLib.constants.MERGED_MINED_BLOCK_VERSION; +export function isNanoContract(headers: EventTxHeader[]) { + for (const header of headers) { + if (isNanoHeader(header)) { + return true; + } + } + return false; +} + export const handleVertexAccepted = async (context: Context, _event: Event) => { const mysql = await getDbConnection(); await mysql.beginTransaction(); const { NETWORK, STAGE, + SERVERLESS_DEPLOY_PREFIX, PUSH_NOTIFICATION_ENABLED, } = getConfig(); @@ -197,12 +219,20 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { token_name, token_symbol, parents, + headers = [], } = fullNodeData; + const isNano = isNanoContract(headers); + 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`); + const daemonUptime = getDaemonUptime(); + // We do not log if the daemon has just started, because it's expected that + // we receive an initial duplicate transaction from the fullnode in this case. + if (daemonUptime < DUPLICATE_TX_ALERT_GRACE_PERIOD) return; + + logger.error(`Transaction ${hash} already in the database and the daemon has not been recently restarted (uptime of ${daemonUptime} seconds). This is unexpected.`); // This might happen if the service has been recently restarted, // so we should raise the alert and just ignore the tx @@ -240,7 +270,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { // add miner to the miners table if (isDecodedValid(blockRewardOutput.decoded, ['address'])) { - await addMiner(mysql, blockRewardOutput.decoded.address, hash); + await addMiner(mysql, blockRewardOutput.decoded!.address, hash); } // here we check if we have any utxos on our database that is locked but @@ -272,6 +302,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { // Add the transaction logger.debug('Will add the tx with height', height); + // TODO: add is_nanocontract to transaction table? await addOrUpdateTx( mysql, hash, @@ -288,13 +319,14 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { 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) { + // Nano contracts are a special case since they can have an address to update even without inputs/outputs + if (inputs.length > 0 || outputs.length > 0 || isNano) { const tokenList: string[] = getTokenListFromInputsAndOutputs(txInputs, txOutputs); // Update transaction count with the new tx await incrementTokensTxCount(mysql, tokenList); - const addressBalanceMap: StringMap = getAddressBalanceMap(txInputs, txOutputs); + const addressBalanceMap: StringMap = getAddressBalanceMap(txInputs, txOutputs, headers); // update address tables (address, address_balance, address_tx_history) await updateAddressTablesWithTx(mysql, hash, timestamp, addressBalanceMap); @@ -376,6 +408,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { parents, inputs: txInputs, outputs: txOutputs, + headers, height: metadata.height, token_name, token_symbol, @@ -412,10 +445,22 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { // Call to process the data for NFT handling (if applicable) // This process is not critical, so we run it in a fire-and-forget manner, not waiting for the promise. - NftUtils.processNftEvent(fullNodeData, STAGE, network, logger) + NftUtils.processNftEvent(fullNodeData, STAGE, SERVERLESS_DEPLOY_PREFIX, network, logger) .catch((err: unknown) => logger.error('[ALERT] Error processing NFT event', err)); } + // Need to check if there is a nano header and update the nc_address's seqnum if needed + for (const header of headers) { + if (isNanoHeader(header)) { + const txseqnum = header.nc_seqnum; + const cachedSeqnum = await getAddressSeqnum(mysql, header.nc_address); + if (txseqnum > cachedSeqnum) { + // The tx seqnum is higher than the cached one so we need to save the tx deqnum + await setAddressSeqnum(mysql, header.nc_address, header.nc_seqnum); + } + } + } + await dbUpdateLastSyncedEvent(mysql, fullNodeEvent.event.id); await mysql.commit(); @@ -444,6 +489,8 @@ export const handleVertexRemoved = async (context: Context, _event: Event) => { outputs, inputs, tokens, + headers = [], + version, } = fullNodeEvent.event.data; const dbTx: DbTransaction | null = await getTransactionById(mysql, hash); @@ -453,12 +500,15 @@ export const handleVertexRemoved = async (context: Context, _event: Event) => { } logger.info(`[VertexRemoved] Voiding tx: ${hash}`); + await voidTx( mysql, hash, inputs, outputs, tokens, + headers, + version, ); logger.info(`[VertexRemoved] Removing tx from database: ${hash}`); @@ -481,6 +531,8 @@ export const voidTx = async ( inputs: EventTxInput[], outputs: EventTxOutput[], tokens: string[], + headers: EventTxHeader[], + version: number, ) => { const dbTxOutputs: DbTxOutput[] = await getTxOutputsFromTx(mysql, hash); const txOutputs: TxOutputWithIndex[] = prepareOutputs(outputs, tokens); @@ -499,9 +551,55 @@ export const voidTx = async ( }; }); - const addressBalanceMap: StringMap = getAddressBalanceMap(txInputs, txOutputsWithLocked); - await voidTransaction(mysql, hash, addressBalanceMap); + const addressBalanceMap: StringMap = getAddressBalanceMap(txInputs, txOutputsWithLocked, headers); + + await voidTransaction(mysql, hash); + // CRITICAL: markUtxosAsVoided must be called before voidAddressTransaction + // and voidWalletTransaction as those methods recalculate balances based on + // the UTXOs table. await markUtxosAsVoided(mysql, dbTxOutputs); + await voidAddressTransaction(mysql, hash, addressBalanceMap, version); + + // CRITICAL: Unspend the inputs when voiding a transaction + // The inputs of the voided transaction need to be marked as unspent + // But only if they were actually spent by this transaction + if (inputs.length > 0) { + // First, check which inputs were actually spent by this transaction + const inputsSpentByThisTx: DbTxOutput[] = []; + + for (const input of inputs) { + // Get the current state of this output to check if it's spent by our transaction + const currentOutput = await getTxOutput(mysql, input.tx_id, input.index, false); + + + if (currentOutput && currentOutput.spentBy === hash) { + inputsSpentByThisTx.push({ + txId: input.tx_id, + index: input.index, + tokenId: '', // Not needed for unspending + address: '', // Not needed for unspending + value: BigInt(0), // Not needed for unspending + authorities: 0, // Not needed for unspending + timelock: null, // Not needed for unspending + heightlock: null, // Not needed for unspending + locked: false, // Not needed for unspending + spentBy: hash, // This is what we're unsetting + voided: false, // Not needed for unspending + }); + } + } + + if (inputsSpentByThisTx.length > 0) { + await unspendUtxos(mysql, inputsSpentByThisTx); + } + } + + // CRITICAL: Update wallet balances when voiding a transaction + await voidWalletTransaction(mysql, hash, addressBalanceMap); + + // CRITICAL: Clear tx_proposal marks from inputs that were used in this voided transaction + // This ensures the UTXOs can be used in new transactions after the void + await clearTxProposalForVoidedTx(mysql, txInputs); const addresses = Object.keys(addressBalanceMap); await validateAddressBalances(mysql, addresses); @@ -519,6 +617,8 @@ export const handleVoidedTx = async (context: Context) => { outputs, inputs, tokens, + headers = [], + version, } = fullNodeEvent.event.data; logger.debug(`Will handle voided tx for ${hash}`); @@ -527,10 +627,11 @@ export const handleVoidedTx = async (context: Context) => { hash, inputs, outputs, - tokens + tokens, + headers, + version, ); logger.debug(`Voided tx ${hash}`); - await mysql.commit(); await dbUpdateLastSyncedEvent(mysql, fullNodeEvent.event.id); } catch (e) { @@ -619,7 +720,7 @@ export const updateLastSyncedEvent = async (context: Context) => { && lastDbSyncedEvent.last_event_id > lastEventId) { logger.error('Tried to store an event lower than the one on the database', { lastEventId, - lastDbSyncedEvent: JSON.stringify(lastDbSyncedEvent), + lastDbSyncedEvent: JSONBigInt.stringify(lastDbSyncedEvent), }); mysql.destroy(); throw new Error('Event lower than stored one.'); @@ -713,3 +814,75 @@ export const handleReorgStarted = async (context: Context): Promise => { ); } }; + +/** + * Checks the HTTP API for missed events after the last ACK + * This is used to detect if we lost an event due to network packet loss + */ +export const checkForMissedEvents = async (context: Context): Promise<{ hasNewEvents: boolean; events: any[] }> => { + if (!context.event) { + throw new Error('No event in context when checking for missed events'); + } + + const lastAckEventId = context.event.event.id; + const fullnodeUrl = getFullnodeHttpUrl(); + + logger.debug(`Checking for missed events after event ID ${lastAckEventId}`); + + let response; + try { + response = await retryWithBackoff( + async () => { + const res = await axios.get(`${fullnodeUrl}/event`, { + params: { + last_ack_event_id: lastAckEventId, + size: 1, + }, + }); + + // Validate response status + if (res.status !== 200) { + logger.error( + `Failed to check for missed events after ACK ${lastAckEventId}: HTTP ${res.status}. URL: ${fullnodeUrl}/event` + ); + throw new Error(`Failed to check for missed events: HTTP ${res.status}`); + } + + // Validate response structure + if (!res.data || typeof res.data !== 'object') { + logger.error( + `Failed to check for missed events after ACK ${lastAckEventId}: Invalid response data structure. Response: ${JSONBigInt.stringify(res.data)}` + ); + throw new Error('Failed to check for missed events: Invalid response structure'); + } + + return res; + }, + { + // It's possible that the fullnode is under high load or having intermittent issues, + // so we use a higher number of retries to give it a chance to recover + maxRetries: 10, + initialDelayMs: 1000, + maxDelayMs: 10000, + backoffMultiplier: 2, + } + ); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error( + `Failed to check for missed events after ACK ${lastAckEventId}: Network error - ${errorMessage}. URL: ${fullnodeUrl}/event` + ); + throw new Error(`Failed to check for missed events: Network error - ${errorMessage}`); + } + + const { events } = response.data; + const hasNewEvents = Array.isArray(events) && events.length > 0; + + if (hasNewEvents) { + logger.warn(`Detected ${events.length} missed event(s) after ACK ${lastAckEventId}. Will reconnect.`); + } else { + logger.debug(`No missed events detected after ACK ${lastAckEventId}`); + } + + return { hasNewEvents, events }; +}; diff --git a/packages/daemon/src/types/address.ts b/packages/daemon/src/types/address.ts index 3b2eef88..370e573e 100644 --- a/packages/daemon/src/types/address.ts +++ b/packages/daemon/src/types/address.ts @@ -17,8 +17,8 @@ export interface GenerateAddresses { export interface AddressBalance { address: string; tokenId: string; - unlockedBalance: number; - lockedBalance: number; + unlockedBalance: bigint; + lockedBalance: bigint; unlockedAuthorities: number; lockedAuthorities: number; timelockExpires: number; @@ -28,7 +28,7 @@ export interface AddressBalance { export interface AddressTotalBalance { address: string; tokenId: string; - balance: number; + balance: bigint; transactions: number; } diff --git a/packages/daemon/src/types/db.ts b/packages/daemon/src/types/db.ts index b9685685..19748df2 100644 --- a/packages/daemon/src/types/db.ts +++ b/packages/daemon/src/types/db.ts @@ -145,3 +145,19 @@ export interface MaxAddressIndexRow extends RowDataPacket { max_among_addresses: number, max_wallet_index: number } + +export interface AddressesWalletsRow extends RowDataPacket { + address: string, + wallet_id: string, + auth_xpubkey: string, + xpubkey: string, + maxGap: number, +} + +export interface AddressRow extends RowDataPacket { + address: string, + index: number, + wallet_id: string, + transactions: number, + seqnum: number, +} diff --git a/packages/daemon/src/types/event.ts b/packages/daemon/src/types/event.ts index a4bcb2a8..fcf70d8f 100644 --- a/packages/daemon/src/types/event.ts +++ b/packages/daemon/src/types/event.ts @@ -5,20 +5,23 @@ * LICENSE file in the root directory of this source tree. */ +import z from 'zod'; +import { bigIntUtils } from '@hathor/wallet-lib'; + export type WebSocketEvent = | { type: 'CONNECTED' } | { type: 'DISCONNECTED' }; export type WebSocketSendEvent = | { - type: 'START_STREAM'; - window_size: number; - last_ack_event_id?: number; + type: 'START_STREAM'; + window_size: number; + last_ack_event_id?: number; } | { - type: 'ACK'; - window_size: number; - ack_event_id?: number; + type: 'ACK'; + window_size: number; + ack_event_id?: number; }; export type HealthCheckEvent = @@ -40,9 +43,29 @@ export enum FullNodeEventTypes { LOAD_STARTED = 'LOAD_STARTED', LOAD_FINISHED = 'LOAD_FINISHED', REORG_STARTED = 'REORG_STARTED', - REORG_FINISHED= 'REORG_FINISHED', + REORG_FINISHED = 'REORG_FINISHED', + NC_EVENT = 'NC_EVENT', } +/** + * All events with transactions + */ +const StandardFullNodeEvents = z.union([ + z.literal('VERTEX_METADATA_CHANGED'), + z.literal('NEW_VERTEX_ACCEPTED'), +]); + +/** + * Events without data + */ +const EmptyDataFullNodeEvents = z.union([ + z.literal('LOAD_STARTED'), + z.literal('LOAD_FINISHED'), + z.literal('REORG_FINISHED'), +]); + +export const FullNodeEventTypesSchema = z.nativeEnum(FullNodeEventTypes); + export type MetadataDecidedEvent = { type: 'TX_VOIDED' | 'TX_UNVOIDED' | 'TX_NEW' | 'TX_FIRST_BLOCK' | 'IGNORE'; originalEvent: FullNodeEvent; @@ -53,87 +76,170 @@ export type Event = | { 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}; + | { type: EventTypes.HEALTHCHECK_EVENT, event: HealthCheckEvent }; export interface VertexRemovedEventData { vertex_id: string; } -export type FullNodeEventBase = { - stream_id: string; - peer_id: string; - network: string; - type: string; - latest_event_id: number; -}; - -export type StandardFullNodeEvent = FullNodeEventBase & { - event: { - id: number; - timestamp: number; - type: Exclude; // All types except "REORG_STARTED" - 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; - signal_bits: number; - metadata: { - hash: string; - voided_by: string[]; - first_block: null | string; - height: number; - }; - }; - }; -}; - -export type ReorgFullNodeEvent = FullNodeEventBase & { - event: { - id: number; - timestamp: number; - type: "REORG_STARTED"; - data: { - reorg_size: number; - previous_best_block: string; - new_best_block: string; - common_block: string; - }; - group_id: number; - }; -}; - -export type FullNodeEvent = StandardFullNodeEvent | ReorgFullNodeEvent; - -export interface EventTxInput { - tx_id: string; - index: number; - spent_output: EventTxOutput; +export const FullNodeEventBaseSchema = z.object({ + stream_id: z.string(), + peer_id: z.string(), + network: z.string(), + type: z.string(), + latest_event_id: z.number(), +}); + +export type FullNodeEventBase = z.infer; + +export const EventTxOutputSchema = z.object({ + value: bigIntUtils.bigIntCoercibleSchema, + token_data: z.number(), + script: z.string(), + locked: z.boolean().optional(), + decoded: z.union([ + z.object({ + type: z.string(), + address: z.string(), + timelock: z.number().nullable(), + }).passthrough().nullable(), + z.object({ + token_data: z.number().nullable(), + }), + z.object({}).strict(), + ]), +}); +export type EventTxOutput = z.infer; + +export const EventTxInputSchema = z.object({ + tx_id: z.string(), + index: z.number(), + spent_output: EventTxOutputSchema, +}); +export type EventTxInput = z.infer; + +export const EventTxNanoHeaderSchema = z.object({ + id: z.string(), + nc_seqnum: z.number(), + nc_id: z.string(), + nc_method: z.string(), + nc_address: z.string(), +}); +export type EventTxNanoHeader = z.infer; + +// EventTxHeaderSchema should be a union of all possible header schemas. +// But currently only the nano header exists. +export const EventTxHeaderSchema = EventTxNanoHeaderSchema; +export type EventTxHeader = z.infer; + +export function isNanoHeader(header: EventTxHeader): header is EventTxNanoHeader { + return header.id === '10'; } -export interface EventTxOutput { - value: number; - token_data: number; - script: string; - locked?: boolean; - decoded: { - type: string; - address: string; - timelock: number | null; - }; -} +export const TxEventDataWithoutMetaSchema = z.object({ + hash: z.string(), + timestamp: z.number(), + version: z.number(), + weight: z.number(), + nonce: bigIntUtils.bigIntCoercibleSchema, + inputs: EventTxInputSchema.array(), + outputs: EventTxOutputSchema.array(), + headers: EventTxNanoHeaderSchema.array().optional(), + parents: z.string().array(), + tokens: z.string().array(), + token_name: z.string().nullable(), + token_symbol: z.string().nullable(), + signal_bits: z.number(), +}); + +export const TxEventDataSchema = TxEventDataWithoutMetaSchema.extend({ + metadata: z.object({ + hash: z.string(), + voided_by: z.string().array(), + first_block: z.string().nullable(), + height: z.number(), + }), +}); + +export const StandardFullNodeEventSchema = FullNodeEventBaseSchema.extend({ + event: z.object({ + id: z.number(), + timestamp: z.number(), + type: StandardFullNodeEvents, + data: TxEventDataSchema, + }), +}); + +export type StandardFullNodeEvent = z.infer; + +export const ReorgFullNodeEventSchema = FullNodeEventBaseSchema.extend({ + event: z.object({ + id: z.number(), + timestamp: z.number(), + type: z.literal('REORG_STARTED'), + data: z.object({ + reorg_size: z.number(), + previous_best_block: z.string(), + new_best_block: z.string(), + common_block: z.string(), + }), + group_id: z.number(), + }), +}); +export type ReorgFullNodeEvent = z.infer; + +export const EmptyDataFullNodeEventSchema = FullNodeEventBaseSchema.extend({ + event: z.object({ + id: z.number(), + timestamp: z.number(), + type: EmptyDataFullNodeEvents, + data: z.object({}).optional(), + }), +}); + +export const TxDataWithoutMetaFullNodeEventSchema = FullNodeEventBaseSchema.extend({ + event: z.object({ + id: z.number(), + timestamp: z.number(), + type: z.literal('VERTEX_REMOVED'), + data: TxEventDataWithoutMetaSchema, + }), +}); + +export const NcEventSchema = FullNodeEventBaseSchema.extend({ + event: z.object({ + id: z.number(), + timestamp: z.number(), + type: z.literal('NC_EVENT'), + data: z.object({ + vertex_id: z.string(), + nc_id: z.string(), + nc_execution: z.union([ + z.literal('pending'), + z.literal('success'), + z.literal('failure'), + z.literal('skipped'), + ]), + first_block: z.string(), + data_hex: z.string(), + }), + group_id: z.number().nullish(), + }), +}); +export type NcEvent = z.infer; + +export const FullNodeEventSchema = z.union([ + TxDataWithoutMetaFullNodeEventSchema, + StandardFullNodeEventSchema, + ReorgFullNodeEventSchema, + EmptyDataFullNodeEventSchema, + NcEventSchema, +]); +export type FullNodeEvent = z.infer; export interface LastSyncedEvent { id: number; last_event_id: number; updated_at: number; } - diff --git a/packages/daemon/src/types/machine.ts b/packages/daemon/src/types/machine.ts index 75265b69..f987a163 100644 --- a/packages/daemon/src/types/machine.ts +++ b/packages/daemon/src/types/machine.ts @@ -15,6 +15,6 @@ export interface Context { retryAttempt: number; event?: FullNodeEvent | null; initialEventId: null | number; - txCache: LRU; + txCache: LRU | null; rewardMinBlocks?: number | null; } diff --git a/packages/daemon/src/types/token.ts b/packages/daemon/src/types/token.ts index c53da8a9..86a0b658 100644 --- a/packages/daemon/src/types/token.ts +++ b/packages/daemon/src/types/token.ts @@ -22,7 +22,7 @@ export class TokenInfo { this.symbol = symbol; this.transactions = transactions || 0; - // XXX: get config from settings? + // XXX: currently we only support Hathor/HTR as the default token const hathorConfig = constants.DEFAULT_NATIVE_TOKEN_CONFIG; if (this.id === constants.NATIVE_TOKEN_UID) { diff --git a/packages/daemon/src/types/transaction.ts b/packages/daemon/src/types/transaction.ts index a933b6ec..9032da1d 100644 --- a/packages/daemon/src/types/transaction.ts +++ b/packages/daemon/src/types/transaction.ts @@ -10,7 +10,7 @@ export interface DbTxOutput { index: number; tokenId: string; address: string; - value: number; + value: bigint; authorities: number; timelock: number | null; heightlock: number | null; diff --git a/packages/daemon/src/types/wallet.ts b/packages/daemon/src/types/wallet.ts index 15159a8e..2921b06b 100644 --- a/packages/daemon/src/types/wallet.ts +++ b/packages/daemon/src/types/wallet.ts @@ -27,13 +27,13 @@ export interface Wallet { export type TokenBalanceValue = { tokenId: string, tokenSymbol: string, - totalAmountSent: number; - lockedAmount: number; - unlockedAmount: number; + totalAmountSent: bigint; + lockedAmount: bigint; + unlockedAmount: bigint; lockedAuthorities: Record; unlockedAuthorities: Record; lockExpires: number | null; - total: number; + total: bigint; } export interface WalletBalanceValue { diff --git a/packages/daemon/src/utils/aws.ts b/packages/daemon/src/utils/aws.ts index 7a35927b..2cd7b74b 100644 --- a/packages/daemon/src/utils/aws.ts +++ b/packages/daemon/src/utils/aws.ts @@ -5,10 +5,11 @@ import { StringMap } from '../types'; import getConfig from '../config'; import logger from '../logger'; import { addAlert, Transaction } from '@wallet-service/common'; +import { bigIntUtils } from '@hathor/wallet-lib'; export function buildFunctionName(functionName: string): string { - const { STAGE } = getConfig(); - return `hathor-wallet-service-${STAGE}-${functionName}`; + const { STAGE, SERVERLESS_DEPLOY_PREFIX } = getConfig(); + return `${SERVERLESS_DEPLOY_PREFIX}-${STAGE}-${functionName}`; } /** @@ -36,7 +37,7 @@ export const invokeOnTxPushNotificationRequestedLambda = async (walletBalanceVal const command = new InvokeCommand({ FunctionName: ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME, InvocationType: 'Event', - Payload: JSON.stringify(walletBalanceValueMap), + Payload: bigIntUtils.JSONBigInt.stringify(walletBalanceValueMap), }); const response: InvokeCommandOutput = await client.send(command); @@ -69,7 +70,7 @@ export const sendRealtimeTx = async (wallets: string[], tx: Transaction): Promis throw new Error('Queue URL is invalid'); } - await sendMessageSQS(JSON.stringify({ + await sendMessageSQS(bigIntUtils.JSONBigInt.stringify({ wallets, tx, }), NEW_TX_SQS); diff --git a/packages/daemon/src/utils/cache.ts b/packages/daemon/src/utils/cache.ts index 8c813f5f..007a0f56 100644 --- a/packages/daemon/src/utils/cache.ts +++ b/packages/daemon/src/utils/cache.ts @@ -43,7 +43,7 @@ export class LRU { } first(): string { - return this.cache.keys().next().value; + return this.cache.keys().next()?.value ?? ''; } clear(): void { diff --git a/packages/daemon/src/utils/date.ts b/packages/daemon/src/utils/date.ts index 7d6deef1..f73b2fe6 100644 --- a/packages/daemon/src/utils/date.ts +++ b/packages/daemon/src/utils/date.ts @@ -13,3 +13,15 @@ export const getUnixTimestamp = (): number => ( Math.round((new Date()).getTime() / 1000) ); + + +const daemonStartTime = getUnixTimestamp(); + +/** + * Get the daemon uptime in seconds + * + * @returns The daemon uptime in seconds + */ +export const getDaemonUptime = (): number => ( + getUnixTimestamp() - daemonStartTime +); \ No newline at end of file diff --git a/packages/daemon/src/utils/index.ts b/packages/daemon/src/utils/index.ts index c29b6e2d..aa572572 100644 --- a/packages/daemon/src/utils/index.ts +++ b/packages/daemon/src/utils/index.ts @@ -11,3 +11,4 @@ export * from './wallet'; export * from './date'; export * from './helpers'; export * from './aws'; +export * from './retry'; diff --git a/packages/daemon/src/utils/retry.ts b/packages/daemon/src/utils/retry.ts new file mode 100644 index 00000000..191cd892 --- /dev/null +++ b/packages/daemon/src/utils/retry.ts @@ -0,0 +1,106 @@ +/** + * 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 logger from '../logger'; + +export interface RetryOptions { + maxRetries?: number; + initialDelayMs?: number; + maxDelayMs?: number; + backoffMultiplier?: number; + retryableErrors?: (error: any) => boolean; +} + +const DEFAULT_OPTIONS: Required = { + maxRetries: 5, + initialDelayMs: 1000, + maxDelayMs: 10000, + backoffMultiplier: 2, + retryableErrors: (error: any) => { + // Retry on network errors and 5xx server errors + if (error.response) { + // HTTP error response received + return error.response.status >= 500 && error.response.status < 600; + } + // Network error (no response received) + return true; + }, +}; + +/** + * Sleep utility function + */ +const sleep = (ms: number): Promise => new Promise(resolve => { + setTimeout(resolve, ms); +}); + +/** + * Calculate the delay for the next retry attempt using exponential backoff + */ +const calculateDelay = ( + attempt: number, + initialDelayMs: number, + maxDelayMs: number, + backoffMultiplier: number +): number => { + const delay = initialDelayMs * (backoffMultiplier ** attempt); + return Math.min(delay, maxDelayMs); +}; + +/** + * Retry a function with exponential backoff + * + * @param fn - The async function to retry + * @param options - Retry configuration options + * @returns Promise that resolves with the function result or rejects with the last error + */ +export async function retryWithBackoff( + fn: () => Promise, + options: RetryOptions = {} +): Promise { + const config = { ...DEFAULT_OPTIONS, ...options }; + let lastError: any; + + for (let attempt = 0; attempt <= config.maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error; + + // Check if we should retry this error + if (!config.retryableErrors(error)) { + logger.debug('Error is not retryable, throwing immediately'); + throw error; + } + + // Check if we've exhausted all retries + if (attempt === config.maxRetries) { + logger.error(`All ${config.maxRetries} retry attempts exhausted`); + throw error; + } + + // Calculate delay and wait before next retry + const delay = calculateDelay( + attempt, + config.initialDelayMs, + config.maxDelayMs, + config.backoffMultiplier + ); + + const errorMsg = error instanceof Error ? error.message : String(error); + logger.warn( + `Retry attempt ${attempt + 1}/${config.maxRetries} failed. ` + + `Retrying in ${delay}ms. Error: ${errorMsg}` + ); + + await sleep(delay); + } + } + + // This should never be reached, but TypeScript needs it + throw lastError; +} diff --git a/packages/daemon/src/utils/wallet.ts b/packages/daemon/src/utils/wallet.ts index f9752269..9c097389 100644 --- a/packages/daemon/src/utils/wallet.ts +++ b/packages/daemon/src/utils/wallet.ts @@ -5,15 +5,17 @@ * LICENSE file in the root directory of this source tree. */ -import hathorLib, { constants, Output, walletUtils, addressUtils } from '@hathor/wallet-lib'; +import hathorLib, { constants, Output, walletUtils, addressUtils, bigIntUtils } from '@hathor/wallet-lib'; import { Connection as MysqlConnection } from 'mysql2/promise'; import { strict as assert } from 'assert'; import { AddressBalance, AddressTotalBalance, DbTxOutput, + EventTxHeader, EventTxInput, EventTxOutput, + isNanoHeader, StringMap, TokenBalanceValue, Wallet, @@ -103,7 +105,7 @@ export const prepareOutputs = (outputs: EventTxOutput[], tokens: string[]): TxOu }; /** - * Get the map of token balances for each address in the transaction inputs and outputs. + * Get the map of token balances for each address in the transaction inputs, outputs and headers. * * @example * Return map has this format: @@ -121,8 +123,9 @@ export const prepareOutputs = (outputs: EventTxOutput[], tokens: string[]): TxOu export const getAddressBalanceMap = ( inputs: TxInput[], outputs: TxOutput[], + headers: EventTxHeader[], ): StringMap => { - const addressBalanceMap = {}; + const addressBalanceMap: StringMap = {}; for (const input of inputs) { if (!isDecodedValid(input.decoded)) { @@ -131,12 +134,11 @@ export const getAddressBalanceMap = ( continue; } - const address = input.decoded?.address; + 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); } @@ -154,10 +156,28 @@ export const getAddressBalanceMap = ( const tokenBalanceMap = TokenBalanceMap.fromTxOutput(output); // merge it with existing TokenBalanceMap for the address - // @ts-ignore addressBalanceMap[address] = TokenBalanceMap.merge(addressBalanceMap[address], tokenBalanceMap); } + for (const header of headers) { + if (!isNanoHeader(header)) { + // We currently only handle nano contract headers + continue; + } + + const address = header.nc_address; + + if (addressBalanceMap[address]) { + // Already have balance for the nc_address + continue; + } + + // Create an empty balance HTR entry if nc_address did not already have balance on the tx + const emptyHTR = new TokenBalanceMap(); + emptyHTR.set(constants.NATIVE_TOKEN_UID, emptyHTR.get(constants.NATIVE_TOKEN_UID)); + addressBalanceMap[address] = emptyHTR; + } + return addressBalanceMap; }; @@ -179,7 +199,7 @@ export const unlockUtxos = async (mysql: MysqlConnection, utxos: DbTxOutput[], u }; return { - value: utxo.authorities > 0 ? utxo.authorities : utxo.value, + value: utxo.authorities > 0 ? BigInt(utxo.authorities) : utxo.value, token: utxo.tokenId, decoded, locked: false, @@ -202,7 +222,7 @@ export const unlockUtxos = async (mysql: MysqlConnection, utxos: DbTxOutput[], u decoded: null, }))); - const addressBalanceMap: StringMap = getAddressBalanceMap([], outputs); + const addressBalanceMap: StringMap = getAddressBalanceMap([], outputs, []); // update address_balance table await updateAddressLockedBalance(mysql, addressBalanceMap, updateTimelocks); @@ -238,7 +258,7 @@ export const getWalletBalanceMap = ( addressWalletMap: StringMap, addressBalanceMap: StringMap, ): StringMap => { - const walletBalanceMap = {}; + const walletBalanceMap: StringMap = {}; for (const [address, balanceMap] of Object.entries(addressBalanceMap)) { const wallet = addressWalletMap[address]; const walletId = wallet && wallet.walletId; @@ -246,7 +266,6 @@ export const getWalletBalanceMap = ( // if this address is not from a started wallet, ignore if (!walletId) continue; - // @ts-ignore walletBalanceMap[walletId] = TokenBalanceMap.merge(walletBalanceMap[walletId], balanceMap); } return walletBalanceMap; @@ -298,10 +317,10 @@ export const prepareInputs = (inputs: EventTxInput[], tokens: string[]): TxInput script: utxo.script, token, decoded: isDecodedValid(output.decoded, ['type', 'address']) ? { - type: output.decoded.type, - address: output.decoded.address, + type: output.decoded!.type, + address: output.decoded!.address, // timelock might actually be null, so don't pass it to requiredKeys - timelock: output.decoded.timelock, + timelock: output.decoded!.timelock, } : null, }; @@ -373,7 +392,7 @@ export const validateAddressBalances = async (mysql: MysqlConnection, addresses: const addressBalances: AddressBalance[] = await fetchAddressBalance(mysql, addresses); const addressTxHistorySums: AddressTotalBalance[] = await fetchAddressTxHistorySum(mysql, addresses); - logger.debug(`Validating address balances for ${JSON.stringify(addresses)}`); + logger.debug(`Validating address balances for ${bigIntUtils.JSONBigInt.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 @@ -407,7 +426,7 @@ export const validateAddressBalances = async (mysql: MysqlConnection, addresses: * @returns */ export const getWalletBalancesForTx = async (mysql: MysqlConnection, tx: Transaction): Promise> => { - const addressBalanceMap: StringMap = getAddressBalanceMap(tx.inputs, tx.outputs); + const addressBalanceMap: StringMap = getAddressBalanceMap(tx.inputs, tx.outputs, tx.headers ?? []); // return only wallets that were started const addressWalletMap: StringMap = await getAddressWalletInfo(mysql, Object.keys(addressBalanceMap)); @@ -445,7 +464,6 @@ export const getWalletBalancesForTx = async (mysql: MysqlConnection, tx: Transac 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); }; @@ -476,7 +494,10 @@ export class FromTokenBalanceMapToBalanceValueList { } export const sortBalanceValueByAbsTotal = (balanceA: TokenBalanceValue, balanceB: TokenBalanceValue): number => { - if (Math.abs(balanceA.total) - Math.abs(balanceB.total) >= 0) return -1; + function abs(num: bigint) { + return num >= 0n ? num : -num; + } + if (abs(balanceA.total) - abs(balanceB.total) >= 0n) return -1; return 0; }; diff --git a/packages/wallet-service/.dockerignore b/packages/wallet-service/.dockerignore new file mode 100644 index 00000000..1569e075 --- /dev/null +++ b/packages/wallet-service/.dockerignore @@ -0,0 +1,11 @@ +dist/ +__tests__/ +.git/ +../../.github/ +.direnv/ +flake.* +node_modules/ +packages/daemon +packages/wallet-service/dist/ +packages/wallet-service/node_modules/ +packages/common/node_modules/ diff --git a/packages/wallet-service/.nvmrc b/packages/wallet-service/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/packages/wallet-service/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/packages/wallet-service/Dockerfile.dev b/packages/wallet-service/Dockerfile.dev new file mode 100644 index 00000000..1df9ab3e --- /dev/null +++ b/packages/wallet-service/Dockerfile.dev @@ -0,0 +1,72 @@ +# Copyright 2025 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. + +# ========================================================================= +# This Dockerfile is used to build and run the Wallet Service container. +# It requires: +# - A MySQL instance, properly migrated ( see /db/Dockerfile ) +# - A Fullnode instance +# - A started Wallet Service Daemon instance ( see /packages/daemon/Dockerfile ) +# +# The expected image size is about 1800MB as of v1.9.0. This is because `serverless` is a development +# dependency and needs to be installed in the final image, reducing the optimization options for +# reducing this size. +# +# To properly connect to a dockerized private network, in the environment variables, you should set `MOCK_AWS=true` +# to avoid trying to connect to external AWS services, except if the container is being run connected to a publicly +# available network. +# +# See the HathorNetwork / Wallet Lib repository for a live example on how to use this Dockerfile, but in short: +# ws-serverless: +# image: hathornetwork/hathor-wallet-service-service +# depends_on: +# fullnode: +# condition: service_healthy +# mysql: +# condition: service_healthy +# ws-daemon: +# condition: service_started +# environment: +# IS_OFFLINE: true +# ENV MOCK_AWS=true # Necessary to avoid trying to connect to external AWS services +# ... +# ports: +# - "3000:3000" +# - "3001:3001" +# networks: +# - hathor-privnet + +# Build stage +FROM node:22-alpine + +# Install system dependencies needed for native modules +RUN apk add --no-cache \ + python3 \ + g++ \ + make \ + py3-setuptools \ + git + +WORKDIR /app + +# Copy root package files +COPY . . + +# Enable corepack for yarn +RUN corepack enable +RUN yarn install + +WORKDIR /app/packages/wallet-service + +# Expose serverless-offline default port +EXPOSE 3000 +# Expose websocket port +EXPOSE 3001 + +RUN chmod +x ./entrypoint.sh + +# Run serverless offline +ENTRYPOINT ["./entrypoint.sh"] diff --git a/packages/wallet-service/README.md b/packages/wallet-service/README.md index ba29aa41..84e61ba3 100644 --- a/packages/wallet-service/README.md +++ b/packages/wallet-service/README.md @@ -31,7 +31,7 @@ database to get the information. The plugin `serverless-offline` is used to emulate AWS Lambda and API Gateway on a local machine. ### Requirements -1. NodeJS v16 +1. NodeJS v22 ### Local database To setup a local database, you will need: diff --git a/packages/wallet-service/entrypoint.sh b/packages/wallet-service/entrypoint.sh new file mode 100755 index 00000000..3bfa0e06 --- /dev/null +++ b/packages/wallet-service/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e + +# When MOCK_AWS is set to true, copy the mocked AWS credentials fixtures to .aws in the working directory +if [ "$MOCK_AWS" = "true" ]; then + cp -r tests/fixtures/aws ./.aws +fi + +yarn serverless offline start --host 0.0.0.0 --httpPort 3000 diff --git a/packages/wallet-service/package.json b/packages/wallet-service/package.json index cae43da2..e226a934 100644 --- a/packages/wallet-service/package.json +++ b/packages/wallet-service/package.json @@ -6,7 +6,11 @@ "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" + "test": "jest", + "debug:offline": "node --inspect-brk ./node_modules/.bin/serverless offline start --host 0.0.0.0 --httpPort 3000" + }, + "engines": { + "node": "22" }, "author": "Hathor Labs", "license": "MIT", @@ -40,7 +44,7 @@ "winston": "3.13.0" }, "peerDependencies": { - "@hathor/wallet-lib": "1.15.0", + "@hathor/wallet-lib": "2.8.3", "@wallet-service/common": "1.5.0" }, "devDependencies": { @@ -60,11 +64,11 @@ "fork-ts-checker-webpack-plugin": "9.0.0", "jest": "29.7.0", "npm-run-all": "4.1.5", - "serverless": "3.35.2", + "serverless": "3.40.0", "serverless-api-gateway-throttling": "2.0.3", "serverless-better-credentials": "2.0.0", "serverless-iam-roles-per-function": "3.2.0", - "serverless-offline": "13.1.2", + "serverless-offline": "14.4.0", "serverless-plugin-aws-alerts": "1.7.5", "serverless-plugin-monorepo": "0.11.0", "serverless-plugin-warmup": "8.2.1", diff --git a/packages/wallet-service/serverless.yml b/packages/wallet-service/serverless.yml index d0ab53b9..c3a37d17 100644 --- a/packages/wallet-service/serverless.yml +++ b/packages/wallet-service/serverless.yml @@ -1,4 +1,4 @@ -service: hathor-wallet-service +service: ${env:SERVERLESS_DEPLOY_PREFIX, "hathor-wallet-service"} frameworkVersion: '3' useDotenv: true @@ -132,7 +132,7 @@ resources: provider: name: aws - runtime: nodejs18.x + runtime: nodejs22.x region: ${opt:region, 'eu-central-1'} # In MB. This is the memory allocated for the Lambdas, they cannot use more than this # and will break if they try. @@ -187,6 +187,7 @@ provider: REDIS_PASSWORD: ${env:REDIS_PASSWORD} SERVICE_NAME: ${self:service} STAGE: ${self:custom.stage} + SERVERLESS_DEPLOY_PREFIX: ${env:SERVERLESS_DEPLOY_PREFIX, "hathor-wallet-service"} EXPLORER_SERVICE_STAGE: ${self:custom.explorerServiceStage} NFT_AUTO_REVIEW_ENABLED: ${env:NFT_AUTO_REVIEW_ENABLED} VOIDED_TX_OFFSET: ${env:VOIDED_TX_OFFSET} @@ -196,15 +197,15 @@ provider: 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, ''} + FIREBASE_PROJECT_ID: ${env:FIREBASE_PROJECT_ID, null} + FIREBASE_PRIVATE_KEY_ID: ${env:FIREBASE_PRIVATE_KEY_ID, null} + FIREBASE_PRIVATE_KEY: ${env:FIREBASE_PRIVATE_KEY, null} + FIREBASE_CLIENT_EMAIL: ${env:FIREBASE_CLIENT_EMAIL, null} + FIREBASE_CLIENT_ID: ${env:FIREBASE_CLIENT_ID, null} + FIREBASE_AUTH_URI: ${env:FIREBASE_AUTH_URI, null} + FIREBASE_TOKEN_URI: ${env:FIREBASE_TOKEN_URI, null} + FIREBASE_AUTH_PROVIDER_X509_CERT_URL: ${env:FIREBASE_AUTH_PROVIDER_X509_CERT_URL, null} + FIREBASE_CLIENT_X509_CERT_URL: ${env:FIREBASE_CLIENT_X509_CERT_URL, null} LOG_LEVEL: ${env:LOG_LEVEL} ALERT_MANAGER_REGION: ${env:ALERT_MANAGER_REGION} ALERT_MANAGER_TOPIC: ${env:ALERT_MANAGER_TOPIC} @@ -324,15 +325,23 @@ functions: method: get cors: true authorizer: ${self:custom.authorizer.walletBearer} - request: - parameters: - paths: - index: false + warmup: + walletWarmer: + enabled: true + getAddressInfoApi: + handler: src/api/addressInfo.get + events: + - http: + path: wallet/address/info + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} warmup: walletWarmer: enabled: true getNewAddresses: handler: src/api/newAddresses.get + memorySize: 1024 events: - http: path: wallet/addresses/new @@ -463,6 +472,39 @@ functions: warmup: walletWarmer: enabled: false + getStateNCProxyApi: + handler: src/api/nanoProxy.getState + events: + - http: + path: wallet/proxy/nano_contract/state + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: false + getHistoryNCProxyApi: + handler: src/api/nanoProxy.getHistory + events: + - http: + path: wallet/proxy/nano_contract/history + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: false + getBpInfoNCProxyApi: + handler: src/api/nanoProxy.getBlueprintInfo + events: + - http: + path: wallet/proxy/nano_contract/blueprint/info + method: get + cors: true + authorizer: ${self:custom.authorizer.walletBearer} + warmup: + walletWarmer: + enabled: false wsConnect: handler: src/ws/connection.connect timeout: 2 @@ -541,6 +583,7 @@ functions: authTokenApi: handler: src/api/auth.tokenHandler timeout: 6 + memorySize: 1024 events: - http: path: auth/token @@ -548,12 +591,25 @@ functions: cors: true warmup: walletWarmer: - enabled: false + enabled: true + authROTokenApi: + handler: src/api/auth.roTokenHandler + timeout: 6 + memorySize: 1024 + events: + - http: + path: auth/token/readonly + method: post + cors: true + warmup: + walletWarmer: + enabled: true bearerAuthorizer: handler: src/api/auth.bearerAuthorizer + memorySize: 1024 warmup: walletWarmer: - enabled: false + enabled: true metrics: handler: src/metrics.getMetrics events: @@ -685,14 +741,15 @@ functions: Fn::GetAtt: [ SendNotificationToDeviceLambdaFunction , Arn ] healthcheck: handler: src/api/healthcheck.getHealthcheck + timeout: 20 # seconds events: - http: private: true path: health method: get throttling: - maxRequestsPerSecond: 1 - maxConcurrentRequests: 1 + maxRequestsPerSecond: 2 + maxConcurrentRequests: 2 deleteStalePushDevices: handler: src/db/cronRoutines.cleanStalePushDevices events: diff --git a/packages/wallet-service/src/api/addressInfo.ts b/packages/wallet-service/src/api/addressInfo.ts new file mode 100644 index 00000000..0bfe84d8 --- /dev/null +++ b/packages/wallet-service/src/api/addressInfo.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 '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, + getWalletAddressDetail, +} 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 errorHandler from '@src/api/middlewares/errorHandler'; + +const mysql = getDbConnection(); + +interface AddressQueryRequest { + address: string, +} + +/** + * Get the address info + * This lambda is called by API Gateway on GET /address/info + */ +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, error } = Joi.object({ + address: Joi.string().regex(/^[A-HJ-NP-Za-km-z1-9]*$/).min(34).max(35).required(), + }).validate(event.queryStringParameters || {}) as { value: AddressQueryRequest, error: ValidationError }; + + if (error) { + const details = error.details.map((err) => ({ + message: err.message, + path: err.path, + })); + + return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); + } + + const { address } = value; + let response = null; + + const info = await getWalletAddressDetail(mysql, walletId, address); + + if (!info) { + // Address not found + return closeDbAndGetError(mysql, ApiError.ADDRESS_NOT_FOUND); + } + + await closeDbConnection(mysql); + + response = { + statusCode: 200, + body: JSON.stringify({ + success: true, + data: info, + }), + }; + + return response; + }), +).use(cors()) + .use(warmupMiddleware()) + .use(errorHandler()); diff --git a/packages/wallet-service/src/api/addresses.ts b/packages/wallet-service/src/api/addresses.ts index a9e31839..52c394bc 100644 --- a/packages/wallet-service/src/api/addresses.ts +++ b/packages/wallet-service/src/api/addresses.ts @@ -39,7 +39,7 @@ class AddressAtIndexValidator { index: Joi.number().min(0).optional(), }); - static validate(payload: unknown): { value: AddressAtIndexRequest, error: ValidationError} { + 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 @@ -92,7 +92,7 @@ export const checkMine: APIGatewayProxyHandler = middy(walletIdProxyHandler(asyn await closeDbConnection(mysql); - const addressBelongMap = sentAddresses.reduce((acc: {string: boolean}, address: string) => { + const addressBelongMap = sentAddresses.reduce((acc: { string: boolean }, address: string) => { acc[address] = walletAddresses.has(address); return acc; @@ -108,7 +108,7 @@ export const checkMine: APIGatewayProxyHandler = middy(walletIdProxyHandler(asyn })).use(cors()) .use(errorHandler()); -/* +/** * 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 @@ -127,7 +127,7 @@ export const get: APIGatewayProxyHandler = middy( return closeDbAndGetError(mysql, ApiError.WALLET_NOT_READY); } - const { value: body, error } = AddressAtIndexValidator.validate(event.pathParameters || {}); + const { value: body, error } = AddressAtIndexValidator.validate(event.queryStringParameters || {}); if (error) { const details = error.details.map((err) => ({ diff --git a/packages/wallet-service/src/api/auth.ts b/packages/wallet-service/src/api/auth.ts index 48a6946d..283bb6d9 100644 --- a/packages/wallet-service/src/api/auth.ts +++ b/packages/wallet-service/src/api/auth.ts @@ -16,7 +16,7 @@ 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 { Wallet, WalletStatus } from '@src/types'; import { getWallet } from '@src/db'; import { verifySignature, @@ -25,7 +25,9 @@ import { getDbConnection, validateAuthTimestamp, AUTH_MAX_TIMESTAMP_SHIFT_IN_SECONDS, + getWalletId, } from '@src/utils'; +import { warmupMiddleware } from '@src/api/utils'; import middy from '@middy/core'; import cors from '@middy/http-cors'; import createDefaultLogger from '@src/logger'; @@ -34,6 +36,7 @@ import config from '@src/config'; import errorHandler from '@src/api/middlewares/errorHandler'; const EXPIRATION_TIME_IN_SECONDS = 1800; +const READONLY_EXPIRATION_TIME_IN_SECONDS = 1800; // 30 minutes const bodySchema = Joi.object({ ts: Joi.number().positive().required(), @@ -42,6 +45,10 @@ const bodySchema = Joi.object({ walletId: Joi.string().required(), }); +const readOnlyBodySchema = Joi.object({ + xpubkey: Joi.string().required(), +}); + function parseBody(body) { try { return JSON.parse(body); @@ -83,6 +90,17 @@ export const tokenHandler: APIGatewayProxyHandler = middy(async (event) => { const authXpubStr = value.xpub; const wallet: Wallet = await getWallet(mysql, value.walletId); + if (!wallet) { + await closeDbConnection(mysql); + return { + statusCode: 400, + body: JSON.stringify({ + success: false, + error: ApiError.WALLET_NOT_FOUND, + }), + }; + } + const [validTimestamp, timestampShift] = validateAuthTimestamp(timestamp, Date.now() / 1000); if (!validTimestamp) { @@ -142,6 +160,7 @@ export const tokenHandler: APIGatewayProxyHandler = middy(async (event) => { ts: timestamp, addr: address.toString(), wid: walletId, + mode: 'full', }, config.authSecret, { @@ -155,25 +174,128 @@ export const tokenHandler: APIGatewayProxyHandler = middy(async (event) => { body: JSON.stringify({ success: true, token }), }; }).use(cors()) + .use(warmupMiddleware()) + .use(errorHandler()); + +export const roTokenHandler: APIGatewayProxyHandler = middy(async (event) => { + const eventBody = parseBody(event.body); + + const { value, error } = readOnlyBodySchema.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 xpubkey = value.xpubkey; + const walletId = getWalletId(xpubkey); + + // Check if wallet exists and is ready + const wallet: Wallet = await getWallet(mysql, walletId); + + if (!wallet) { + await closeDbConnection(mysql); + return { + statusCode: 400, + body: JSON.stringify({ + success: false, + error: ApiError.WALLET_NOT_FOUND, + }), + }; + } + + if (wallet.status !== WalletStatus.READY) { + await closeDbConnection(mysql); + return { + statusCode: 400, + body: JSON.stringify({ + success: false, + error: ApiError.WALLET_NOT_READY, + }), + }; + } + + // Generate JWT with read-only mode + // NOTE: JWT does NOT contain xpubkey, only walletId hash + const token = jwt.sign( + { + wid: walletId, + mode: 'ro', + }, + config.authSecret, + { + expiresIn: READONLY_EXPIRATION_TIME_IN_SECONDS, + jwtid: uuid4(), + }, + ); + + await closeDbConnection(mysql); + + return { + statusCode: 200, + body: JSON.stringify({ success: true, token }), + }; +}).use(cors()) + .use(warmupMiddleware()) .use(errorHandler()); /** * Generates a aws policy document to allow/deny access to the resource */ -const _generatePolicy = (principalId: string, effect: string, resource: string, logger: Logger) => { +const _generatePolicy = (principalId: string, effect: string, resource: string, logger: Logger, mode: string = 'full') => { const resourcePrefix = `${resource.split('/').slice(0, 2).join('/')}/*`; const policyDocument: PolicyDocument = { Version: '2012-10-17', Statement: [], }; + // Define resources based on mode + let allowedResources: string[]; + + if (mode === 'ro') { + // Read-only endpoints + allowedResources = [ + `${resourcePrefix}/wallet/status`, + `${resourcePrefix}/wallet/addresses`, + `${resourcePrefix}/wallet/addresses/new`, + `${resourcePrefix}/wallet/balances`, + `${resourcePrefix}/wallet/tokens`, + `${resourcePrefix}/wallet/tokens/*/details`, + `${resourcePrefix}/wallet/history`, + `${resourcePrefix}/wallet/utxos`, + `${resourcePrefix}/wallet/tx_outputs`, + `${resourcePrefix}/wallet/transactions/*`, + `${resourcePrefix}/wallet/address/info`, + `${resourcePrefix}/wallet/proxy/*`, + ]; + } else { + // Full access + allowedResources = [ + `${resourcePrefix}/wallet/*`, + `${resourcePrefix}/tx/*`, + ]; + } + const statementOne: Statement = { Action: 'execute-api:Invoke', Effect: effect, - Resource: [ - `${resourcePrefix}/wallet/*`, - `${resourcePrefix}/tx/*`, - ], + Resource: allowedResources, }; policyDocument.Statement[0] = statementOne; @@ -181,11 +303,9 @@ const _generatePolicy = (principalId: string, effect: string, resource: string, const authResponse: CustomAuthorizerResult = { policyDocument, principalId, + context: { walletId: principalId, mode }, }; - 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; @@ -220,19 +340,23 @@ export const bearerAuthorizer: APIGatewayTokenAuthorizerHandler = middy(async (e } } - // signature data - const signature = data.sign; - const timestamp = data.ts; - const address = data.addr; const walletId = data.wid; + const mode = data.mode || 'full'; // Default to full for legacy tokens - // header data - const expirationTs = data.exp; - const verified = verifySignature(signature, timestamp, address, walletId); + // For full-access tokens, verify wallet signature (existing logic) + if (mode === 'full') { + const signature = data.sign; + const timestamp = data.ts; + const address = data.addr; + const verified = verifySignature(signature, timestamp, address, walletId); - if (verified && Math.floor(Date.now() / 1000) <= expirationTs) { - return _generatePolicy(walletId, 'Allow', event.methodArn, logger); + if (!verified) { + return _generatePolicy(walletId, 'Deny', event.methodArn, logger, mode); + } } - return _generatePolicy(walletId, 'Deny', event.methodArn, logger); -}).use(cors()); + // For read-only tokens, JWT is already verified above - no wallet signature needed + // Generate appropriate policy based on mode + return _generatePolicy(walletId, 'Allow', event.methodArn, logger, mode); +}).use(cors()) + .use(warmupMiddleware()); diff --git a/packages/wallet-service/src/api/balances.ts b/packages/wallet-service/src/api/balances.ts index bed929cf..e5daac65 100644 --- a/packages/wallet-service/src/api/balances.ts +++ b/packages/wallet-service/src/api/balances.ts @@ -23,6 +23,7 @@ import middy from '@middy/core'; import cors from '@middy/http-cors'; import Joi from 'joi'; import errorHandler from '@src/api/middlewares/errorHandler'; +import { bigIntUtils } from '@hathor/wallet-lib'; const mysql = getDbConnection(); @@ -79,7 +80,7 @@ export const get: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (wal return { statusCode: 200, - body: JSON.stringify({ success: true, balances }), + body: bigIntUtils.JSONBigInt.stringify({ success: true, balances }), }; })).use(cors()) .use(warmupMiddleware()) diff --git a/packages/wallet-service/src/api/healthcheck.ts b/packages/wallet-service/src/api/healthcheck.ts index 30f9b4ec..0d74d69d 100644 --- a/packages/wallet-service/src/api/healthcheck.ts +++ b/packages/wallet-service/src/api/healthcheck.ts @@ -1,11 +1,11 @@ import middy from '@middy/core'; import { - Healthcheck, - HealthcheckInternalComponent, - HealthcheckDatastoreComponent, - HealthcheckHTTPComponent, - HealthcheckCallbackResponse, - HealthcheckStatus, + Healthcheck, + HealthcheckInternalComponent, + HealthcheckDatastoreComponent, + HealthcheckHTTPComponent, + HealthcheckCallbackResponse, + HealthcheckStatus, } from '@hathor/healthcheck-lib'; import { getLatestHeight } from '@src/db'; import fullnode from '@src/fullnode'; diff --git a/packages/wallet-service/src/api/nanoProxy.ts b/packages/wallet-service/src/api/nanoProxy.ts new file mode 100644 index 00000000..90539dbf --- /dev/null +++ b/packages/wallet-service/src/api/nanoProxy.ts @@ -0,0 +1,130 @@ +/** + * 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, { ValidationResult } from 'joi'; +import { APIGatewayProxyHandler } from 'aws-lambda'; +import { STATUS_CODE_TABLE, warmupMiddleware } from '@src/api/utils'; +import middy from '@middy/core'; +import cors from '@middy/http-cors'; +import errorHandler from '@src/api/middlewares/errorHandler'; +import { walletIdProxyHandler } from '@src/commons'; +import { FullnodeGetNCHistoryAPIParams, FullnodeGetNCStateAPIParams } from '@src/types'; +import { ApiError } from './errors'; +import fullnode from '@src/fullnode'; + +const GetNCStateAPIParams = Joi.object({ + id: Joi.string().required(), + fields: Joi.array().items(Joi.string()).required(), + balances: Joi.array().items(Joi.string()).required(), + calls: Joi.array().items(Joi.string()).required(), + block_hash: Joi.string(), + block_height: Joi.number(), + timestamp: Joi.number(), +}); + +const GetNCHistoryAPIParams = Joi.object({ + id: Joi.string().required(), + count: Joi.number().allow(null), + after: Joi.number().allow(null), + before: Joi.number().allow(null), +}); + +const GetNCBpInfoAPIParams = Joi.object({ + blueprint_id: Joi.string().required(), +}); + +/* + * Proxy to fullnode /v1a/nano_contract/state + * + * This lambda is called by API Gateway on POST /wallet/proxy/nano_contract/state + */ +export const getState: APIGatewayProxyHandler = middy(walletIdProxyHandler((async (_walletId, event) => { + const params = event.queryStringParameters || {}; + const validationResult: ValidationResult = GetNCStateAPIParams.validate(params); + + if (validationResult.error) { + const body = { + success: false, + details: validationResult.error.details.map(err => ({ message: err.message, path: err.path})), + }; + return { + statusCode: STATUS_CODE_TABLE[ApiError.INVALID_PAYLOAD], + body: JSON.stringify(body), + }; + } + + const data = await fullnode.getNCState(validationResult.value as FullnodeGetNCStateAPIParams); + + return { + statusCode: 200, + body: JSON.stringify(data), + }; +}))).use(cors()) + .use(warmupMiddleware()) + .use(errorHandler()); + +/* + * Proxy to fullnode /v1a/nano_contract/history + * + * This lambda is called by API Gateway on POST /wallet/proxy/nano_contract/history + */ +export const getHistory: APIGatewayProxyHandler = middy(walletIdProxyHandler((async (_w, event) => { + const params = event.queryStringParameters || {}; + const validationResult: ValidationResult = GetNCHistoryAPIParams.validate(params); + + if (validationResult.error) { + const body = { + success: false, + details: validationResult.error.details.map(err => ({ message: err.message, path: err.path})), + }; + return { + statusCode: STATUS_CODE_TABLE[ApiError.INVALID_PAYLOAD], + body: JSON.stringify(body), + }; + } + + const data = await fullnode.getNCHistory(validationResult.value as FullnodeGetNCHistoryAPIParams); + + return { + statusCode: 200, + body: JSON.stringify(data), + }; +}))).use(cors()) + .use(warmupMiddleware()) + .use(errorHandler()); + +/* + * Proxy to fullnode /v1a/nano_contract/blueprint/info + * + * This lambda is called by API Gateway on POST /wallet/proxy/nano_contract/blueprint/info + */ +export const getBlueprintInfo: APIGatewayProxyHandler = middy(walletIdProxyHandler((async (_w, event) => { + const params = event.queryStringParameters || {}; + const validationResult: ValidationResult = GetNCBpInfoAPIParams.validate(params); + + if (validationResult.error) { + const body = { + success: false, + details: validationResult.error.details.map(err => ({ message: err.message, path: err.path})), + }; + return { + statusCode: STATUS_CODE_TABLE[ApiError.INVALID_PAYLOAD], + body: JSON.stringify(body), + }; + } + + const data = await fullnode.getNCBlueprintInfo(validationResult.value.blueprint_id); + + return { + statusCode: 200, + body: JSON.stringify(data), + }; +}))).use(cors()) + .use(warmupMiddleware()) + .use(errorHandler()); diff --git a/packages/wallet-service/src/api/newAddresses.ts b/packages/wallet-service/src/api/newAddresses.ts index 3158ff9a..2df96664 100644 --- a/packages/wallet-service/src/api/newAddresses.ts +++ b/packages/wallet-service/src/api/newAddresses.ts @@ -30,16 +30,16 @@ const mysql = getDbConnection(); * 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); + const wallet = await getWallet(mysql, walletId); - if (!status) { + if (!wallet) { return closeDbAndGetError(mysql, ApiError.WALLET_NOT_FOUND); } - if (!status.readyAt) { + if (!wallet.readyAt) { return closeDbAndGetError(mysql, ApiError.WALLET_NOT_READY); } - const addresses = await getNewAddresses(mysql, walletId); + const addresses = await getNewAddresses(mysql, wallet); await closeDbConnection(mysql); diff --git a/packages/wallet-service/src/api/tokens.ts b/packages/wallet-service/src/api/tokens.ts index 87d14367..2325a5ae 100644 --- a/packages/wallet-service/src/api/tokens.ts +++ b/packages/wallet-service/src/api/tokens.ts @@ -15,7 +15,7 @@ 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 { bigIntUtils, constants } from '@hathor/wallet-lib'; import middy from '@middy/core'; import cors from '@middy/http-cors'; import errorHandler from '@src/api/middlewares/errorHandler'; @@ -94,13 +94,13 @@ export const getTokenDetails = middy(walletIdProxyHandler(async (walletId, event ] = await Promise.all([ getTotalSupply(mysql, tokenId), getTotalTransactions(mysql, tokenId), - getAuthorityUtxo(mysql, tokenId, constants.TOKEN_MELT_MASK), - getAuthorityUtxo(mysql, tokenId, constants.TOKEN_MINT_MASK), + getAuthorityUtxo(mysql, tokenId, Number(constants.TOKEN_MELT_MASK)), + getAuthorityUtxo(mysql, tokenId, Number(constants.TOKEN_MINT_MASK)), ]); return { statusCode: 200, - body: JSON.stringify({ + body: bigIntUtils.JSONBigInt.stringify({ success: true, details: { tokenInfo, diff --git a/packages/wallet-service/src/api/totalSupply.ts b/packages/wallet-service/src/api/totalSupply.ts index 6088c2c1..ff8a9621 100644 --- a/packages/wallet-service/src/api/totalSupply.ts +++ b/packages/wallet-service/src/api/totalSupply.ts @@ -51,13 +51,13 @@ export const onTotalSupplyRequest: APIGatewayProxyHandler = middy(async (event) } const tokenId = value.tokenId; - const totalSupply: number = await getTotalSupply(mysql, tokenId); + const totalSupply: bigint = await getTotalSupply(mysql, tokenId); await closeDbConnection(mysql); return { statusCode: 200, - body: JSON.stringify({ + body: hathorLib.bigIntUtils.JSONBigInt.stringify({ success: true, totalSupply, }), diff --git a/packages/wallet-service/src/api/txById.ts b/packages/wallet-service/src/api/txById.ts index 98592637..e7791e4a 100644 --- a/packages/wallet-service/src/api/txById.ts +++ b/packages/wallet-service/src/api/txById.ts @@ -15,6 +15,7 @@ import middy from '@middy/core'; import cors from '@middy/http-cors'; import Joi, { ValidationError } from 'joi'; import { TxByIdRequest } from '@src/types'; +import { bigIntUtils } from '@hathor/wallet-lib'; import errorHandler from '@src/api/middlewares/errorHandler'; const mysql = getDbConnection(); @@ -56,7 +57,7 @@ export const get: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (wal return { statusCode: 200, - body: JSON.stringify({ success: true, txTokens }), + body: bigIntUtils.JSONBigInt.stringify({ success: true, txTokens }), }; })) .use(cors()) diff --git a/packages/wallet-service/src/api/txOutputs.ts b/packages/wallet-service/src/api/txOutputs.ts index 55844981..5c840920 100644 --- a/packages/wallet-service/src/api/txOutputs.ts +++ b/packages/wallet-service/src/api/txOutputs.ts @@ -16,13 +16,21 @@ import { } from '@src/types'; import { closeDbAndGetError } from '@src/api/utils'; import { getDbConnection } from '@src/utils'; -import { constants } from '@hathor/wallet-lib'; +import { constants, bigIntUtils, transactionUtils } from '@hathor/wallet-lib'; import middy from '@middy/core'; import cors from '@middy/http-cors'; import errorHandler from '@src/api/middlewares/errorHandler'; const mysql = getDbConnection(); +const positiveBigInt = Joi.custom(value => { + const newVal = BigInt(value); + if (newVal > 0n) { + return newVal; + } + throw new Error('value must be positive'); +}); + const bodySchema = Joi.object({ id: Joi.string().optional(), addresses: Joi.array() @@ -32,8 +40,11 @@ const bodySchema = Joi.object({ 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), + // @ts-ignore : bigint is not considered a basic type for a default value. + biggerThan: positiveBigInt.default(0n), + // @ts-ignore + smallerThan: positiveBigInt.default(constants.MAX_OUTPUT_VALUE + 1n), + totalAmount: positiveBigInt.optional(), maxOutputs: Joi.number().integer().positive().default(constants.MAX_OUTPUTS), skipSpent: Joi.boolean().optional().default(true), txId: Joi.string().optional(), @@ -55,7 +66,7 @@ export const getFilteredUtxos = middy(walletIdProxyHandler(async (walletId, even const eventBody = { id: queryString.id, - addresses: multiQueryString.addresses, + addresses: multiQueryString['addresses[]'], tokenId: queryString.tokenId, authority: queryString.authority, ignoreLocked: queryString.ignoreLocked, @@ -64,6 +75,8 @@ export const getFilteredUtxos = middy(walletIdProxyHandler(async (walletId, even skipSpent: true, // utxo is always unspent txId: queryString.txId, index: queryString.index, + totalAmount: queryString.totalAmount, + maxOutputs: queryString.maxOutputs, }; const { value, error } = bodySchema.validate(eventBody, { @@ -85,11 +98,11 @@ export const getFilteredUtxos = middy(walletIdProxyHandler(async (walletId, even // 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); + const body = bigIntUtils.JSONBigInt.parse(response.body); body.utxos = body.txOutputs; delete body.txOutputs; - response.body = JSON.stringify(body); + response.body = bigIntUtils.JSONBigInt.stringify(body); } return response; @@ -107,7 +120,7 @@ export const getFilteredTxOutputs = middy(walletIdProxyHandler(async (walletId, const eventBody = { id: queryString.id, - addresses: multiQueryString.addresses, + addresses: multiQueryString['addresses[]'], tokenId: queryString.tokenId, authority: queryString.authority, ignoreLocked: queryString.ignoreLocked, @@ -116,6 +129,8 @@ export const getFilteredTxOutputs = middy(walletIdProxyHandler(async (walletId, skipSpent: queryString.skipSpent, txId: queryString.txId, index: queryString.index, + totalAmount: queryString.totalAmount, + maxOutputs: queryString.maxOutputs, }; const { value, error } = bodySchema.validate(eventBody, { @@ -158,7 +173,7 @@ const _getFilteredTxOutputs = async (walletId: string, filters: IFilterTxOutput) return { statusCode: 200, - body: JSON.stringify({ + body: bigIntUtils.JSONBigInt.stringify({ success: true, txOutputs: txOutputList, }), @@ -180,11 +195,37 @@ const _getFilteredTxOutputs = async (walletId: string, filters: IFilterTxOutput) } const txOutputs: DbTxOutput[] = await filterTxOutputs(mysql, newFilters); - const txOutputsWithPath: DbTxOutputWithPath[] = mapTxOutputsWithPath(walletAddresses, txOutputs); + let finalTxOutputs: DbTxOutput[] = txOutputs; + + // Apply totalAmount filter if specified + if (filters.totalAmount) { + try { + const minimalUtxos = txOutputs.map(tx => ({ + ...tx, + authorities: BigInt(tx.authorities), // Convert for compatibility + addressPath: '', // Required by type, but not used by selectUtxos algorithm + })); + + const { utxos } = transactionUtils.selectUtxos(minimalUtxos, filters.totalAmount); + + // Filter original txOutputs to only include the selected ones + const selectedSet = new Set(utxos.map(u => `${u.txId}:${u.index}`)); + finalTxOutputs = txOutputs.filter(tx => selectedSet.has(`${tx.txId}:${tx.index}`)); + } catch (error) { + // If we don't have enough utxos, return empty array + if (error.message && error.message.includes("Don't have enough utxos")) { + finalTxOutputs = []; + } else { + throw error; + } + } + } + + const txOutputsWithPath: DbTxOutputWithPath[] = mapTxOutputsWithPath(walletAddresses, finalTxOutputs); return { statusCode: 200, - body: JSON.stringify({ + body: bigIntUtils.JSONBigInt.stringify({ success: true, txOutputs: txOutputsWithPath, }), diff --git a/packages/wallet-service/src/api/txProposalCreate.ts b/packages/wallet-service/src/api/txProposalCreate.ts index 253f71fd..3fecaa5d 100644 --- a/packages/wallet-service/src/api/txProposalCreate.ts +++ b/packages/wallet-service/src/api/txProposalCreate.ts @@ -119,11 +119,13 @@ export const create = middy(walletIdProxyHandler(async (walletId, event) => { // mark utxos with tx-proposal id const txProposalId = uuidv4(); - await markUtxosWithProposalId(mysql, txProposalId, inputUtxos); - await createTxProposal(mysql, txProposalId, walletId, now); + // Nano contract transactions might have empty inputs + if (inputUtxos.length > 0) { + await markUtxosWithProposalId(mysql, txProposalId, inputUtxos); + } - await closeDbConnection(mysql); + await createTxProposal(mysql, txProposalId, walletId, now); const inputPromises = inputUtxos.map(async (utxo) => { const addressDetail: AddressInfo = await getWalletAddressDetail(mysql, walletId, utxo.address); @@ -136,6 +138,8 @@ export const create = middy(walletIdProxyHandler(async (walletId, event) => { const retInputs = await Promise.all(inputPromises); + await closeDbConnection(mysql); + return { statusCode: 201, body: JSON.stringify({ diff --git a/packages/wallet-service/src/api/txProposalSend.ts b/packages/wallet-service/src/api/txProposalSend.ts index 0ce4cf23..516062cd 100644 --- a/packages/wallet-service/src/api/txProposalSend.ts +++ b/packages/wallet-service/src/api/txProposalSend.ts @@ -10,6 +10,7 @@ import { getTxProposal, getTxProposalInputs, updateTxProposal, + releaseTxProposalUtxos, } from '@src/db'; import { TxProposalStatus, @@ -140,6 +141,8 @@ export const send: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (wa TxProposalStatus.SEND_ERROR, ); + await releaseTxProposalUtxos(mysql, [txProposalId]); + return closeDbAndGetError(mysql, ApiError.TX_PROPOSAL_SEND_ERROR, { message: e.message, txProposalId, diff --git a/packages/wallet-service/src/api/txhistory.ts b/packages/wallet-service/src/api/txhistory.ts index 4c9a0c15..ff5e4bcc 100644 --- a/packages/wallet-service/src/api/txhistory.ts +++ b/packages/wallet-service/src/api/txhistory.ts @@ -6,7 +6,7 @@ */ import 'source-map-support/register'; -import hathorLib from '@hathor/wallet-lib'; +import hathorLib, { bigIntUtils } from '@hathor/wallet-lib'; import { ApiError } from '@src/api/errors'; import { closeDbAndGetError, warmupMiddleware } from '@src/api/utils'; @@ -85,7 +85,7 @@ export const get = middy(walletIdProxyHandler(async (walletId, event) => { return { statusCode: 200, - body: JSON.stringify({ success: true, history, skip, count }), + body: bigIntUtils.JSONBigInt.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 index 83804e89..666e1a1f 100644 --- a/packages/wallet-service/src/api/utils.ts +++ b/packages/wallet-service/src/api/utils.ts @@ -80,10 +80,13 @@ export const closeDbAndGetError = async ( /** * Will return early if the request is a wake-up call from serverless-plugin-warmup + * Generic to work with both APIGatewayProxyResult and APIGatewayAuthorizerResult */ -export const warmupMiddleware = (): middy.MiddlewareObj => { - const warmupBefore = (request: middy.Request): APIGatewayProxyResult | undefined => { +export const warmupMiddleware = (): middy.MiddlewareObj => { + const warmupBefore = (request: middy.Request): TResult | undefined => { + // @ts-expect-error - checking for warmup source property if (request.event.source === 'serverless-plugin-warmup') { + // @ts-expect-error - returning a generic warmup response return { statusCode: 200, body: 'OK', diff --git a/packages/wallet-service/src/api/wallet.ts b/packages/wallet-service/src/api/wallet.ts index 73fdeb88..bbcd3e7c 100644 --- a/packages/wallet-service/src/api/wallet.ts +++ b/packages/wallet-service/src/api/wallet.ts @@ -6,9 +6,10 @@ */ import { APIGatewayProxyHandler, Handler, SNSEvent } from 'aws-lambda'; -import { LambdaClient, InvokeCommand, InvokeCommandOutput } from '@aws-sdk/client-lambda'; +import { InvokeCommand, InvokeCommandOutput } from '@aws-sdk/client-lambda'; import 'source-map-support/register'; +import { createLambdaClient } from '@src/utils/aws.utils'; import { ApiError } from '@src/api/errors'; import { addNewAddresses, @@ -94,7 +95,7 @@ const loadBodySchema = Joi.object({ */ /* istanbul ignore next */ export const invokeLoadWalletAsync = async (xpubkey: string, maxGap: number): Promise => { - const client = new LambdaClient({ + const client = createLambdaClient({ endpoint: config.stage === 'dev' ? 'http://localhost:3002' : `https://lambda.${config.awsRegion}.amazonaws.com`, @@ -124,7 +125,7 @@ export const invokeLoadWalletAsync = async (xpubkey: string, maxGap: number): Pr * @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 + * @param authXpubkeySignature - A string with the signature that proves the user owns the xpub */ export const validateSignatures = ( walletId: string, diff --git a/packages/wallet-service/src/commons.ts b/packages/wallet-service/src/commons.ts index a469e149..b2bb23fa 100644 --- a/packages/wallet-service/src/commons.ts +++ b/packages/wallet-service/src/commons.ts @@ -87,7 +87,7 @@ export const unlockUtxos = async (mysql: ServerlessMysql, utxos: DbTxOutput[], u }; return { - value: utxo.authorities > 0 ? utxo.authorities : utxo.value, + value: utxo.authorities > 0 ? BigInt(utxo.authorities) : utxo.value, token: utxo.tokenId, decoded, locked: false, diff --git a/packages/wallet-service/src/config.ts b/packages/wallet-service/src/config.ts index 78589f31..b5ae34a7 100644 --- a/packages/wallet-service/src/config.ts +++ b/packages/wallet-service/src/config.ts @@ -13,6 +13,7 @@ export function loadEnvConfig(): EnvironmentConfig { const config: EnvironmentConfig = { defaultServer: process.env.DEFAULT_SERVER ?? 'https://node1.mainnet.hathor.network/v1a/', stage: process.env.STAGE, + serverlessDeployPrefix: process.env.SERVERLESS_DEPLOY_PREFIX ?? 'hathor-wallet-service', network: process.env.NETWORK, serviceName: process.env.SERVICE_NAME, maxAddressGap: Number.parseInt(process.env.MAX_ADDRESS_GAP, 10), @@ -31,6 +32,7 @@ export function loadEnvConfig(): EnvironmentConfig { pushNotificationEnabled: process.env.PUSH_NOTIFICATION_ENABLED === 'true', pushAllowedProviders: process.env.PUSH_ALLOWED_PROVIDERS, isOffline: process.env.IS_OFFLINE === 'true', + shouldMockAWS: process.env.MOCK_AWS === 'true', txHistoryMaxCount: Number.parseInt(process.env.TX_HISTORY_MAX_COUNT || '50', 10), healthCheckMaximumHeightDifference: Number.parseInt(process.env.HEALTHCHECK_MAXIMUM_HEIGHT_DIFFERENCE ?? '5', 10), diff --git a/packages/wallet-service/src/db/index.ts b/packages/wallet-service/src/db/index.ts index 796d4b49..cb24261e 100644 --- a/packages/wallet-service/src/db/index.ts +++ b/packages/wallet-service/src/db/index.ts @@ -8,7 +8,7 @@ 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 { constants, bigIntUtils } from '@hathor/wallet-lib'; import { AddressIndexMap, AddressInfo, @@ -64,6 +64,7 @@ const logger: Logger = createDefaultLogger(); const BLOCK_VERSION = [ constants.BLOCK_VERSION, constants.MERGED_MINED_BLOCK_VERSION, + constants.POA_BLOCK_VERSION ]; const BURN_ADDRESS = 'HDeadDeadDeadDeadDeadDeadDeagTPgmn'; @@ -183,7 +184,7 @@ export const generateAddresses = async (mysql: ServerlessMysql, xpubkey: string, 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) { + if (entry.transactions as number > 0 && index > lastUsedAddressIndex) { lastUsedAddressIndex = index; } @@ -289,6 +290,7 @@ export const createWallet = async ( status: WalletStatus.CREATING, created_at: ts, max_gap: maxGap, + last_used_address_index: -1, }; await mysql.query( `INSERT INTO \`wallet\` @@ -304,6 +306,7 @@ export const createWallet = async ( status: WalletStatus.CREATING, createdAt: ts, readyAt: null, + lastUsedAddressIndex: -1, }; }; @@ -426,7 +429,7 @@ export const getWalletAddressDetail = async (mysql: ServerlessMysql, walletId: s FROM \`address\` WHERE \`wallet_id\` = ? AND \`address\` = ?`, - [walletId, address]); + [walletId, address]); if (results.length > 0) { const data = results[0]; @@ -435,6 +438,7 @@ export const getWalletAddressDetail = async (mysql: ServerlessMysql, walletId: s address: data.address as string, index: data.index as number, transactions: data.transactions as number, + seqnum: data.seqnum as number, }; return addressDetail; @@ -536,7 +540,7 @@ export const initWalletBalance = async (mysql: ServerlessMysql, walletId: string 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); + assert.strictEqual(BigInt(row1.unlocked_balance as string) + BigInt(row1.locked_balance as string), BigInt(row2.balance as string)); balanceEntries.push([ walletId, row1.token_id, @@ -683,8 +687,9 @@ export const addUtxos = async ( let value = output.value; if (isAuthority(output.token_data)) { - authorities = value; - value = 0; + // value should be within [0, 255] for authorities to be valid. + authorities = Number(value); + value = 0n; } return [ @@ -890,6 +895,10 @@ export const getUtxos = async ( mysql: ServerlessMysql, utxosInfo: IWalletInput[], ): Promise => { + if (utxosInfo.length <= 0) { + return []; + } + const entries = utxosInfo.map((utxo) => [utxo.txId, utxo.index]); const results: DbSelectResult = await mysql.query( `SELECT * @@ -948,11 +957,11 @@ export const getWalletSortedValueUtxos = async ( index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value as string), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, - locked: result.locked > 0, + locked: result.locked as number > 0, }; utxos.push(utxo); } @@ -1013,11 +1022,11 @@ export const getLockedUtxoFromInputs = async (mysql: ServerlessMysql, inputs: Tx index: utxo.index as number, tokenId: utxo.token_id as string, address: utxo.address as string, - value: utxo.value as number, + value: BigInt(utxo.value as string), authorities: utxo.authorities as number, timelock: utxo.timelock as number, heightlock: utxo.heightlock as number, - locked: (utxo.locked > 0), + locked: (utxo.locked as number > 0), })); } @@ -1160,12 +1169,12 @@ export const updateAddressLockedBalance = async ( \`unlocked_authorities\` = (unlocked_authorities | ?) WHERE \`address\` = ? AND \`token_id\` = ?`, [ - tokenBalance.unlockedAmount, - tokenBalance.unlockedAmount, - tokenBalance.unlockedAuthorities.toInteger(), - address, - token, - ], + tokenBalance.unlockedAmount, + tokenBalance.unlockedAmount, + tokenBalance.unlockedAuthorities.toInteger(), + address, + token, + ], ); // if any authority has been unlocked, we have to refresh the locked authorities @@ -1201,7 +1210,7 @@ export const updateAddressLockedBalance = async ( ) WHERE \`address\` = ? AND \`token_id\` = ?`, - [address, token, address, token]); + [address, token, address, token]); } } } @@ -1233,7 +1242,7 @@ export const updateWalletLockedBalance = async ( WHERE \`wallet_id\` = ? AND \`token_id\` = ?`, [tokenBalance.unlockedAmount, tokenBalance.unlockedAmount, - tokenBalance.unlockedAuthorities.toInteger(), walletId, token], + tokenBalance.unlockedAuthorities.toInteger(), walletId, token], ); // if any authority has been unlocked, we have to refresh the locked authorities @@ -1302,6 +1311,7 @@ export const getWalletAddresses = async (mysql: ServerlessMysql, walletId: strin address: result.address as string, index: result.index as number, transactions: result.transactions as number, + seqnum: result.seqnum as number, }; addresses.push(address); } @@ -1315,32 +1325,27 @@ export const getWalletAddresses = async (mysql: ServerlessMysql, walletId: strin * @param walletId - Wallet id * @returns A list of addresses and their indexes */ -export const getNewAddresses = async (mysql: ServerlessMysql, walletId: string): Promise => { +export const getNewAddresses = async (mysql: ServerlessMysql, wallet: Wallet): 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]); + // 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 ?`, [wallet.walletId, wallet.lastUsedAddressIndex, wallet.maxGap]); - 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); - } + 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; }; @@ -1382,9 +1387,9 @@ 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 totalAmount = BigInt(result.total_received as string); + const unlockedBalance = BigInt(result.unlocked_balance as string); + const lockedBalance = BigInt(result.locked_balance as string); 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; @@ -1418,7 +1423,7 @@ export const getWalletTokens = async ( ); for (const result of results) { - tokenList.push( result.token_id); + tokenList.push(result.token_id); } return tokenList; @@ -1464,14 +1469,14 @@ LEFT OUTER JOIN transaction ON transaction.tx_id = wallet_tx_history.tx_id ORDER BY wallet_tx_history.timestamp DESC LIMIT ?, ?`, - [walletId, tokenId, skip, count]); + [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, + balance: BigInt(result.balance as string), version: result.version, }; history.push(tx); @@ -1516,11 +1521,11 @@ export const getUtxosLockedAtHeight = async ( index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value as string), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, - locked: result.locked > 0, + locked: result.locked as number > 0, }; utxos.push(utxo); } @@ -1570,11 +1575,11 @@ export const getWalletUnlockedUtxos = async ( index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value as string), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, - locked: result.locked > 0, + locked: result.locked as number > 0, }; utxos.push(utxo); } @@ -1592,7 +1597,7 @@ export const updateVersionData = async (mysql: ServerlessMysql, timestamp: numbe const entry = { id: 1, timestamp, - data: JSON.stringify(data), + data: bigIntUtils.JSONBigInt.stringify(data), }; await mysql.query( @@ -1613,7 +1618,7 @@ export const getVersionData = async (mysql: ServerlessMysql): Promise<{ timestam if (results.length > 0) { const data = results[0]; - const entry: FullNodeApiVersionResponse = JSON.parse(data.data as string); + const entry: FullNodeApiVersionResponse = bigIntUtils.JSONBigInt.parse(data.data as string); const { error } = FullnodeVersionSchema.validate(entry); if (error) { throw error; @@ -1934,11 +1939,11 @@ export const getTxOutputs = async ( index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value as string), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, - locked: result.locked > 0, + locked: result.locked as number > 0, txProposalId: result.tx_proposal as string, txProposalIndex: result.tx_proposal_index as number, spentBy: result.spent_by ? result.spent_by as string : null, @@ -2002,11 +2007,11 @@ export const getTxOutputsBySpent = async ( index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value as string), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, - locked: result.locked > 0, + locked: result.locked as number > 0, txProposalId: result.tx_proposal as string, txProposalIndex: result.tx_proposal_index as number, spentBy: result.spent_by ? result.spent_by as string : null, @@ -2074,7 +2079,7 @@ export const markUtxosAsVoided = async ( UPDATE \`tx_output\` SET \`voided\` = TRUE WHERE \`tx_id\` IN (?)`, - [txIds]); + [txIds]); }; /** @@ -2332,8 +2337,8 @@ export const fetchAddressBalance = async ( 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, + unlockedBalance: BigInt(result.unlocked_balance as string), + lockedBalance: BigInt(result.locked_balance as string), lockedAuthorities: result.locked_authorities as number, unlockedAuthorities: result.unlocked_authorities as number, timelockExpires: result.timelock_expires as number, @@ -2367,7 +2372,7 @@ export const fetchAddressTxHistorySum = async ( return results.map((result): AddressTotalBalance => ({ address: result.address as string, tokenId: result.token_id as string, - balance: result.balance as number, + balance: BigInt(result.balance as string), transactions: result.transactions as number, })); }; @@ -2388,8 +2393,8 @@ export const filterTxOutputs = async ( authority: 0, ignoreLocked: false, skipSpent: true, - biggerThan: -1, - smallerThan: constants.MAX_OUTPUT_VALUE + 1, + biggerThan: 0, + smallerThan: constants.MAX_OUTPUT_VALUE + 1n, ...filters, }; @@ -2446,7 +2451,7 @@ export const mapDbResultToDbTxOutput = (result: any): DbTxOutput => ({ index: result.index as number, tokenId: result.token_id as string, address: result.address as string, - value: result.value as number, + value: BigInt(result.value), authorities: result.authorities as number, timelock: result.timelock as number, heightlock: result.heightlock as number, @@ -2549,7 +2554,7 @@ export const getMinersList = async ( address: result.address as string, firstBlock: result.first_block as string, lastBlock: result.last_block as string, - count: result.count as number, + count: Number(result.count), }); } @@ -2566,7 +2571,7 @@ export const getMinersList = async ( export const getTotalSupply = async ( mysql: ServerlessMysql, tokenId: string, -): Promise => { +): Promise => { const results: DbSelectResult = await mysql.query(` SELECT SUM(value) as value FROM tx_output @@ -2588,7 +2593,7 @@ export const getTotalSupply = async ( throw new Error('Total supply query returned no results'); } - return results[0].value as number; + return BigInt(results[0].value as string); }; /** @@ -2647,7 +2652,7 @@ export const getTotalTransactions = async ( throw new Error('Total transactions query returned no results'); } - return results[0].count as number; + return Number(results[0].count as string); }; /** @@ -2707,7 +2712,7 @@ export const getAffectedAddressTxCountFromTxList = async ( const addressTransactions = results.reduce((acc, result) => { const address = result.address as string; - const txCount = result.txCount as number; + const txCount = Number(result.txCount); const tokenId = result.tokenId as string; acc[`${address}_${tokenId}`] = txCount; @@ -2812,7 +2817,7 @@ export const existsPushDevice = async ( mysql: ServerlessMysql, deviceId: string, walletId: string, -) : Promise => { +): Promise => { const [{ count }] = await mysql.query( ` SELECT COUNT(1) as \`count\` @@ -2820,7 +2825,7 @@ export const existsPushDevice = async ( WHERE device_id = ? AND wallet_id = ?`, [deviceId, walletId], - ) as unknown as Array<{count}>; + ) as unknown as Array<{ count }>; return count > 0; }; @@ -2840,7 +2845,7 @@ export const registerPushDevice = async ( enablePush: boolean, enableShowAmounts: boolean, }, -) : Promise => { +): Promise => { await mysql.query( ` INSERT @@ -2889,7 +2894,7 @@ export const updatePushDevice = async ( enablePush: boolean, enableShowAmounts: boolean, }, -) : Promise => { +): Promise => { await mysql.query( ` UPDATE \`push_devices\` @@ -2912,7 +2917,7 @@ export const unregisterPushDevice = async ( mysql: ServerlessMysql, deviceId: string, walletId?: string, -) : Promise => { +): Promise => { if (walletId) { await mysql.query( ` @@ -2964,22 +2969,22 @@ export const getTransactionById = async ( 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 }>; + // 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 = { + const txToken: TxByIdToken = { txId: eachTxToken.tx_id, timestamp: eachTxToken.timestamp, version: eachTxToken.version, voided: !!eachTxToken.voided, weight: eachTxToken.weight, - balance: eachTxToken.balance, + balance: BigInt(eachTxToken.balance), tokenId: eachTxToken.token_id, tokenName: eachTxToken.name, tokenSymbol: eachTxToken.symbol, - } as TxByIdToken; + }; txTokens.push(txToken); }); @@ -2995,7 +3000,7 @@ export const getTransactionById = async ( export const existsWallet = async ( mysql: ServerlessMysql, walletId: string, -) : Promise => { +): Promise => { const [{ count }] = (await mysql.query( ` SELECT COUNT(1) as \`count\` @@ -3016,15 +3021,15 @@ export const existsWallet = async ( export const getPushDevice = async ( mysql: ServerlessMysql, deviceId: string, -) : Promise => { +): 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}>; + // eslint-disable-next-line camelcase + ) as Array<{ wallet_id, device_id, push_provider, enable_push, enable_show_amounts }>; if (!pushDevice) { return null; @@ -3049,7 +3054,7 @@ export const getPushDevice = async ( export const getPushDeviceSettingsList = async ( mysql: ServerlessMysql, walletIdList: string[], -) : Promise => { +): Promise => { const pushDeviceSettingsResult = await mysql.query( ` SELECT wallet_id @@ -3059,8 +3064,8 @@ export const getPushDeviceSettingsList = async ( FROM \`push_devices\` WHERE wallet_id in (?)`, [walletIdList], - // eslint-disable-next-line camelcase - ) as Array<{wallet_id, device_id, enable_push, enable_show_amounts}>; + // 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, @@ -3085,7 +3090,7 @@ export const countStalePushDevices = async (mysql: ServerlessMysql): Promise; - return count; + return Number(count); }; /** @@ -3121,7 +3126,7 @@ export const getTokenSymbols = async ( ); if (results.length === 0) return null; - return results.reduce((prev: Record, token: { id: string, symbol: string}) => { + return results.reduce((prev: Record, token: { id: string, symbol: string }) => { // eslint-disable-next-line no-param-reassign prev[token.id] = token.symbol; return prev; @@ -3171,12 +3176,12 @@ export const getAddressAtIndex = async ( ): Promise => { const addresses = await mysql.query( ` - SELECT \`address\`, \`index\`, \`transactions\` + SELECT \`address\`, \`index\`, \`transactions\`, \`seqnum\` FROM \`address\` pd WHERE \`index\` = ? AND \`wallet_id\` = ? LIMIT 1`, - [walletId, index], + [index, walletId], ); if (addresses.length <= 0) { @@ -3184,8 +3189,9 @@ export const getAddressAtIndex = async ( } return { - address: addresses[0].address as string, - index: addresses[0].index as number, - transactions: addresses[0].transactions as number, - } as AddressInfo; + address: addresses[0].address, + index: addresses[0].index, + transactions: addresses[0].transactions, + seqnum: addresses[0].seqnum, + } }; diff --git a/packages/wallet-service/src/db/utils.ts b/packages/wallet-service/src/db/utils.ts index df7f3442..e66f6011 100644 --- a/packages/wallet-service/src/db/utils.ts +++ b/packages/wallet-service/src/db/utils.ts @@ -75,7 +75,7 @@ export async function transactionDecorator(_mysql: ServerlessMysql, wrapped: Fun * @param result - The result row to map to a Wallet object */ export const getWalletFromDbEntry = (entry: Record): Wallet => ({ - walletId: getWalletId(entry.xpubkey as string), + walletId: entry.id as string, xpubkey: entry.xpubkey as string, authXpubkey: entry.auth_xpubkey as string, status: entry.status as WalletStatus, @@ -83,6 +83,7 @@ export const getWalletFromDbEntry = (entry: Record): Wallet => maxGap: entry.max_gap as number, createdAt: entry.created_at as number, readyAt: entry.ready_at as number, + lastUsedAddressIndex: entry.last_used_address_index as number, }); /** @@ -152,7 +153,10 @@ export class FromTokenBalanceMapToBalanceValueList { } export const sortBalanceValueByAbsTotal = (balanceA: TokenBalanceValue, balanceB: TokenBalanceValue): number => { - if (Math.abs(balanceA.total) - Math.abs(balanceB.total) >= 0) return -1; + function abs(num: bigint) { + return num >= 0n ? num : -num; + } + if (abs(balanceA.total) - abs(balanceB.total) >= 0n) return -1; return 0; }; diff --git a/packages/wallet-service/src/fullnode.ts b/packages/wallet-service/src/fullnode.ts index 3e024ed2..1eb97f69 100644 --- a/packages/wallet-service/src/fullnode.ts +++ b/packages/wallet-service/src/fullnode.ts @@ -6,9 +6,8 @@ */ import axios from 'axios'; -import Joi from 'joi'; import config from '@src/config'; -import { FullNodeApiVersionResponse } from '@src/types'; +import { FullNodeApiVersionResponse, FullnodeGetNCHistoryAPIParams, FullnodeGetNCStateAPIParams } from '@src/types'; import { FullnodeVersionSchema } from '@src/schemas'; export const BASE_URL = config.defaultServer; @@ -89,6 +88,36 @@ export const create = (baseURL = BASE_URL) => { return response.data; } + const getNCState = async (params: FullnodeGetNCStateAPIParams) => { + const response = await api.get('nano_contract/state', { + data: null, + params, + headers: { 'content-type': 'application/json' }, + }); + + return response.data; + } + + const getNCHistory = async (params: FullnodeGetNCHistoryAPIParams) => { + const response = await api.get('nano_contract/history', { + data: null, + params, + headers: { 'content-type': 'application/json' }, + }); + + return response.data; + } + + const getNCBlueprintInfo = async (blueprintId: string) => { + const response = await api.get('nano_contract/blueprint/info', { + data: null, + params: { blueprint_id: blueprintId }, + headers: { 'content-type': 'application/json' }, + }); + + return response.data; + } + return { api, // exported so we can mock it on the tests version, @@ -96,7 +125,10 @@ export const create = (baseURL = BASE_URL) => { getConfirmationData, queryGraphvizNeighbours, getStatus, - getHealth + getHealth, + getNCState, + getNCHistory, + getNCBlueprintInfo, }; }; diff --git a/packages/wallet-service/src/nodeConfig.ts b/packages/wallet-service/src/nodeConfig.ts index 4038dfba..eb21cf30 100644 --- a/packages/wallet-service/src/nodeConfig.ts +++ b/packages/wallet-service/src/nodeConfig.ts @@ -41,6 +41,11 @@ export function convertApiVersionData(data: FullNodeApiVersionResponse): FullNod return { version: data.version, network: data.network, + // NOTE: Due to a bug in older fullnode versions, nano_contracts_enabled may return + // string values ('disabled', 'enabled', 'feature_activation') instead of boolean. + // This will be fixed in future fullnode versions to return boolean only. + // Until then, we need to handle both string and boolean values. + nanoContractsEnabled: data.nano_contracts_enabled === true || data.nano_contracts_enabled === 'enabled' || data.nano_contracts_enabled === 'feature_activation', minWeight: data.min_weight, minTxWeight: data.min_tx_weight, minTxWeightCoefficient: data.min_tx_weight_coefficient, diff --git a/packages/wallet-service/src/redis.ts b/packages/wallet-service/src/redis.ts index 20456b83..2d263ccc 100644 --- a/packages/wallet-service/src/redis.ts +++ b/packages/wallet-service/src/redis.ts @@ -9,8 +9,12 @@ import config from '@src/config'; const redisConfig: RedisConfig = { url: config.redisUrl, - password: config.redisPassword, }; +// Only set password if it is not empty. +// Dockerized environments use passwordless redis instances by default. +if (config.redisPassword) { + redisConfig.password = config.redisPassword; +} export const svcPrefix = 'walletsvc'; diff --git a/packages/wallet-service/src/schemas.ts b/packages/wallet-service/src/schemas.ts index 030de90f..069532c9 100644 --- a/packages/wallet-service/src/schemas.ts +++ b/packages/wallet-service/src/schemas.ts @@ -13,10 +13,17 @@ export const Sha256Schema = Joi.string().hex().length(64); export const FullnodeVersionSchema = Joi.object({ version: Joi.string().min(1).required(), network: Joi.string().min(1).required(), - min_weight: Joi.number().integer().positive().required(), - min_tx_weight: Joi.number().integer().positive().required(), - min_tx_weight_coefficient: Joi.number().positive().required(), - min_tx_weight_k: Joi.number().integer().positive().required(), + // NOTE: Due to a bug in older fullnode versions, this field may be a string + // ('disabled', 'enabled', 'feature_activation') instead of boolean. + // Future fullnode versions will return boolean only. + nano_contracts_enabled: Joi.alternatives().try( + Joi.boolean(), + Joi.string().valid('disabled', 'enabled', 'feature_activation') + ).required(), + min_weight: Joi.number().integer().positive().allow(0).required(), + min_tx_weight: Joi.number().integer().positive().allow(0).required(), + min_tx_weight_coefficient: Joi.number().positive().allow(0).required(), + min_tx_weight_k: Joi.number().integer().positive().allow(0).required(), token_deposit_percentage: Joi.number().positive().required(), reward_spend_min_blocks: Joi.number().integer().positive().required(), max_number_inputs: Joi.number().integer().positive().required(), @@ -34,6 +41,7 @@ export const FullnodeVersionSchema = Joi.object({ export const EnvironmentConfigSchema = Joi.object({ defaultServer: Joi.string().required(), stage: Joi.string().required(), + serverlessDeployPrefix: Joi.string().required(), network: Joi.string().required(), serviceName: Joi.string().required(), maxAddressGap: Joi.number().required(), @@ -52,6 +60,7 @@ export const EnvironmentConfigSchema = Joi.object({ pushNotificationEnabled: Joi.boolean().required(), pushAllowedProviders: Joi.string().required(), isOffline: Joi.boolean().required(), + shouldMockAWS: Joi.boolean().required(), txHistoryMaxCount: Joi.number().required(), healthCheckMaximumHeightDifference: Joi.number().required(), awsRegion: Joi.string().required(), diff --git a/packages/wallet-service/src/types.ts b/packages/wallet-service/src/types.ts index edd97b56..95e18ede 100644 --- a/packages/wallet-service/src/types.ts +++ b/packages/wallet-service/src/types.ts @@ -45,6 +45,7 @@ export enum TxProposalStatus { export interface EnvironmentConfig { defaultServer: string; stage: string; + serverlessDeployPrefix: string; network: string; serviceName: string; maxAddressGap: number; @@ -63,6 +64,7 @@ export interface EnvironmentConfig { pushNotificationEnabled: boolean; pushAllowedProviders: string; isOffline: boolean; + shouldMockAWS: boolean; txHistoryMaxCount: number; healthCheckMaximumHeightDifference: number; awsRegion: string; @@ -74,12 +76,12 @@ export interface EnvironmentConfig { firebaseTokenUri: string; firebaseAuthProviderX509CertUrl: string; firebaseClientX509CertUrl: string; - firebasePrivateKey: string|null; + firebasePrivateKey: string | null; maxLoadWalletRetries: number; logLevel: string; createNftMaxRetries: number; warnMaxReorgSize: number; -}; +} /** * Fullnode converted version data. @@ -87,6 +89,7 @@ export interface EnvironmentConfig { export interface FullNodeVersionData { version: string; network: string; + nanoContractsEnabled: boolean; minWeight: number; minTxWeight: number; minTxWeightCoefficient: number; @@ -106,6 +109,10 @@ export interface FullNodeVersionData { export interface FullNodeApiVersionResponse { version: string; network: string; + // NOTE: Due to a bug in older fullnode versions, this field may be a string + // ('disabled', 'enabled', 'feature_activation') instead of boolean. + // Future fullnode versions will return boolean only. + nano_contracts_enabled?: boolean | 'disabled' | 'enabled' | 'feature_activation'; min_weight: number; min_tx_weight: number; min_tx_weight_coefficient: number; // float @@ -118,7 +125,7 @@ export interface FullNodeApiVersionResponse { genesis_block_hash?: string, genesis_tx1_hash?: string, genesis_tx2_hash?: string, - native_token?: { name: string, symbol: string}; + native_token?: { name: string, symbol: string }; } export interface TxProposal { @@ -144,12 +151,14 @@ export interface Wallet { retryCount?: number; createdAt?: number; readyAt?: number; + lastUsedAddressIndex?: number; } export interface AddressInfo { address: string; index: number; transactions: number; + seqnum: number; } export interface ShortAddressInfo { @@ -204,12 +213,12 @@ export class Authorities { array: number[]; - constructor(authorities?: number | number[]) { + constructor(authorities?: bigint | number | number[]) { let tmp = []; if (authorities instanceof Array) { tmp = authorities; } else if (authorities != null) { - tmp = Authorities.intToArray(authorities); + tmp = Authorities.intToArray(Number(authorities)); } this.array = new Array(Authorities.LENGTH - tmp.length).fill(0).concat(tmp); @@ -306,7 +315,8 @@ export class Authorities { } toJSON(): Record { - const authorities = this.toInteger(); + // TOKEN_MINT_MASK and TOKEN_MELT_MASK are bigint (since they come from the output amount) + const authorities = BigInt(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 @@ -315,11 +325,11 @@ export class Authorities { } export class Balance { - totalAmountSent: number; + totalAmountSent: bigint; - lockedAmount: number; + lockedAmount: bigint; - unlockedAmount: number; + unlockedAmount: bigint; lockedAuthorities: Authorities; @@ -327,7 +337,7 @@ export class Balance { lockExpires: number | null; - constructor(totalAmountSent = 0, unlockedAmount = 0, lockedAmount = 0, lockExpires = null, unlockedAuthorities = null, lockedAuthorities = null) { + constructor(totalAmountSent = 0n, unlockedAmount = 0n, lockedAmount = 0n, lockExpires = null, unlockedAuthorities = null, lockedAuthorities = null) { this.totalAmountSent = totalAmountSent; this.unlockedAmount = unlockedAmount; this.lockedAmount = lockedAmount; @@ -341,7 +351,7 @@ export class Balance { * * @returns The total balance */ - total(): number { + total(): bigint { return this.unlockedAmount + this.lockedAmount; } @@ -403,13 +413,13 @@ export class Balance { export type TokenBalanceValue = { tokenId: string, tokenSymbol: string, - totalAmountSent: number; - lockedAmount: number; - unlockedAmount: number; + totalAmountSent: bigint; + lockedAmount: bigint; + unlockedAmount: bigint; lockedAuthorities: Record; unlockedAuthorities: Record; lockExpires: number | null; - total: number; + total: bigint; } export class WalletTokenBalance { @@ -446,7 +456,7 @@ export interface TxTokenBalance { txId: string; timestamp: number; voided: boolean; - balance: Balance; + balance: bigint; version: number; } @@ -459,7 +469,7 @@ export class TokenBalanceMap { get(tokenId: string): Balance { // if the token is not present, return 0 instead of undefined - return this.map[tokenId] || new Balance(0, 0, 0); + return this.map[tokenId] || new Balance(0n, 0n, 0n); } set(tokenId: string, balance: Balance): void { @@ -498,10 +508,10 @@ export class TokenBalanceMap { * @param tokenBalanceMap - The js object to convert to a TokenBalanceMap * @returns - The new TokenBalanceMap object */ - static fromStringMap(tokenBalanceMap: StringMap>): TokenBalanceMap { + 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, + obj.set(tokenId, new Balance(balance.totalSent as bigint, balance.unlocked as bigint, balance.locked as bigint, balance.lockExpires || null, balance.unlockedAuthorities, balance.lockedAuthorities)); } return obj; @@ -539,14 +549,14 @@ export class 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))); + obj.set(token, new Balance(0n, 0n, 0n, output.decoded.timelock, 0, new Authorities(output.value))); } else { - obj.set(token, new Balance(value, 0, value, output.decoded.timelock, 0, 0)); + obj.set(token, new Balance(value, 0n, 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)); + obj.set(token, new Balance(0n, 0n, 0n, null, new Authorities(output.value), 0)); } else { - obj.set(token, new Balance(value, value, 0, null)); + obj.set(token, new Balance(value, value, 0n, null)); } return obj; @@ -568,9 +578,9 @@ export class 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))); + obj.set(token, new Balance(0n, 0n, 0n, null, authorities.toNegative(), new Authorities(0))); } else { - obj.set(token, new Balance(0, -input.value, 0, null)); + obj.set(token, new Balance(0n, -input.value, 0n, null)); } return obj; } @@ -630,8 +640,8 @@ export interface Tx { export interface AddressBalance { address: string; tokenId: string; - unlockedBalance: number; - lockedBalance: number; + unlockedBalance: bigint; + lockedBalance: bigint; unlockedAuthorities: number; lockedAuthorities: number; timelockExpires: number; @@ -641,7 +651,7 @@ export interface AddressBalance { export interface AddressTotalBalance { address: string; tokenId: string; - balance: number; + balance: bigint; transactions: number; } @@ -650,7 +660,7 @@ export interface DbTxOutput { index: number; tokenId: string; address: string; - value: number; + value: bigint; authorities: number; timelock: number | null; heightlock: number | null; @@ -680,12 +690,13 @@ export interface IFilterTxOutput { tokenId?: string; authority?: number; ignoreLocked?: boolean; - biggerThan?: number; - smallerThan?: number; + biggerThan?: bigint; + smallerThan?: bigint; maxOutputs?: number; skipSpent?: boolean; txId?: string; index?: number; + totalAmount?: bigint; } export enum InputSelectionAlgo { @@ -694,8 +705,8 @@ export enum InputSelectionAlgo { export interface IWalletInsufficientFunds { tokenId: string; - requested: number; - available: number; + requested: bigint; + available: bigint; } export interface DbTxOutputWithPath extends DbTxOutput { @@ -745,7 +756,7 @@ export interface TxByIdToken { version: number; voided: boolean; weight: number; - balance: Balance; + balance: bigint; tokenId: string; tokenName: string; tokenSymbol: string; @@ -817,3 +828,20 @@ export interface WalletBalanceValue { addresses: string[], walletBalanceForTx: TokenBalanceValue[], } + +export interface FullnodeGetNCStateAPIParams { + id: string; + fields: string[]; + balances: string[]; + calls: string[]; + block_hash?: string; + block_height?: number; + timestamp?: number; +} + +export interface FullnodeGetNCHistoryAPIParams { + id: string; + count?: number | null; + after?: number | null; + before?: number | null; +} diff --git a/packages/wallet-service/src/utils.ts b/packages/wallet-service/src/utils.ts index 06809ab1..2d830050 100644 --- a/packages/wallet-service/src/utils.ts +++ b/packages/wallet-service/src/utils.ts @@ -20,6 +20,7 @@ import config from '@src/config'; const bip32 = BIP32Factory(ecc); hathorLib.network.setNetwork(config.network); +hathorLib.config.setServerUrl(config.defaultServer); const libNetwork = hathorLib.network.getNetwork(); const hathorNetwork = { @@ -48,6 +49,7 @@ export const sha256d = (data: string, encoding: BinaryToTextEncoding): string => const hash1 = createHash('sha256'); hash1.update(data); const hash2 = createHash('sha256'); + // @ts-ignore: `digest` returns a Buffer which is not a BinaryLike required by `update` hash2.update(hash1.digest()); return hash2.digest(encoding); }; @@ -86,6 +88,9 @@ export const getDbConnection = (): ServerlessMysql => ( // 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: config.dbPass, + // BIGINT columns should be returned as strings to keep precision on the JS unsafe range. + supportBigNumbers: true, + bigNumberStrings: true, }, }) ); @@ -250,7 +255,7 @@ export const getAddressAtIndex = (xpubkey: string, addressIndex: number): string * @memberof Wallet * @inner */ -export const getAddresses = (xpubkey: string, startIndex: number, quantity: number): {[key: string]: number} => { +export const getAddresses = (xpubkey: string, startIndex: number, quantity: number): { [key: string]: number } => { const addrMap = {}; for (let index = startIndex; index < startIndex + quantity; index++) { diff --git a/packages/wallet-service/src/utils/aws.utils.ts b/packages/wallet-service/src/utils/aws.utils.ts new file mode 100644 index 00000000..6bb33bb0 --- /dev/null +++ b/packages/wallet-service/src/utils/aws.utils.ts @@ -0,0 +1,121 @@ +/** + * 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. + */ + +/** + * AWS Utils and mocker for Local Development + * + * This module provides mock implementations of AWS services when running offline AND in a private network without + * EC2 instances available, for example when running inside a Dockerized private network. + * + * When requested, it prevents the AWS SDK from attempting to connect to EC2 metadata service. + * + * This behavior could be further improved by adding conditional logic to the requests themselves, but this will be + * considered and planned in the future if needed. + */ + +import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'; +import { ApiGatewayManagementApiClient, PostToConnectionCommand } from '@aws-sdk/client-apigatewaymanagementapi'; +import createDefaultLogger from '@src/logger' +import wsConfig from '@src/config'; + +/** + * Mock credentials for offline development + */ +const mockCredentials = { + accessKeyId: 'mock-access-key', + secretAccessKey: 'mock-secret-key', + sessionToken: 'mock-session-token', +}; + +/** + * Create a LambdaClient with proper configuration for offline/online mode + */ +export function createLambdaClient(config: { endpoint?: string; region?: string } = {}) { + const clientConfig: any = { + region: config.region || process.env.AWS_REGION || 'us-east-1', + }; + + // If not mocking, return a real LambdaClient + if (!wsConfig.shouldMockAWS) { + if (config.endpoint) { + clientConfig.endpoint = config.endpoint; + } + return new LambdaClient(clientConfig); + } + + const logger = createDefaultLogger(); + logger.log({level: 'debug', message: '[AWS Mock] Creating mocked LambdaClient for offline development'}); + clientConfig.credentials = mockCredentials; + clientConfig.endpoint = config.endpoint || 'http://localhost:3002'; + + // Create a mock client that doesn't actually call AWS + const mockClient = { + send: async (command: InvokeCommand) => { + logger.log({ + level: 'debug', message: '[AWS Mock] Intercepted Lambda invoke:', invocationData: { + functionName: command.input.FunctionName, + invocationType: command.input.InvocationType, + payload: command.input.Payload ? JSON.parse(command.input.Payload as string) : null, + } + }); + + // Return a successful response for Event invocations + if (command.input.InvocationType === 'Event') { + return {StatusCode: 202}; + } + + // Return a mock response for synchronous invocations + return { + StatusCode: 200, + Payload: JSON.stringify({success: true}), + }; + }, + }; + + return mockClient as any; +} + +/** + * Create an ApiGatewayManagementApiClient with proper configuration for offline/online mode + */ +export function createApiGatewayManagementApiClient(config: { endpoint?: string; region?: string } = {}) { + const clientConfig: any = { + region: config.region || process.env.AWS_REGION || 'us-east-1', + }; + + // If not mocking, return a real ApiGatewayManagementApiClient + if (!wsConfig.shouldMockAWS) { + if (config.endpoint) { + clientConfig.endpoint = config.endpoint; + } + return new ApiGatewayManagementApiClient(clientConfig); + } + + const logger = createDefaultLogger(); + logger.log({ + level: 'debug', + message: '[AWS Mock] Creating mocked ApiGatewayManagementApiClient for offline development' + }); + clientConfig.credentials = mockCredentials; + + const mockClient = { + send: async (command: PostToConnectionCommand) => { + logger.log({ + level: 'debug', + message: '[AWS Mock] Intercepted API Gateway post to connection:', + postData: { + connectionId: command.input.ConnectionId, + data: command.input.Data, + } + }); + + return {StatusCode: 200}; + }, + }; + + return mockClient as any; +} diff --git a/packages/wallet-service/src/utils/pushnotification.utils.ts b/packages/wallet-service/src/utils/pushnotification.utils.ts index ae21b274..46db3c27 100644 --- a/packages/wallet-service/src/utils/pushnotification.utils.ts +++ b/packages/wallet-service/src/utils/pushnotification.utils.ts @@ -13,6 +13,7 @@ import { MulticastMessage } from 'firebase-admin/messaging'; import createDefaultLogger from '@src/logger'; import config from '@src/config'; import { addAlert } from '@wallet-service/common/src/utils/alerting.utils'; +import { bigIntUtils } from '@hathor/wallet-lib'; import { EnvironmentConfigSchema } from '@src/schemas'; const logger = createDefaultLogger(); @@ -31,7 +32,7 @@ const FirebaseConfigSchema = EnvironmentConfigSchema.fork([ ], (schema) => schema.required()); export function buildFunctionName(functionName: string): string { - return `hathor-wallet-service-${config.stage}-${functionName}`; + return `${config.serverlessDeployPrefix}-${config.stage}-${functionName}`; } export enum FunctionName { @@ -117,7 +118,7 @@ if (isPushNotificationEnabled()) { ); throw error; } - + const serviceAccount = { type: 'service_account', project_id: FIREBASE_PROJECT_ID, @@ -242,7 +243,7 @@ export class PushNotificationUtils { const command = new InvokeCommand({ FunctionName: SEND_NOTIFICATION_FUNCTION_NAME, InvocationType: 'Event', - Payload: JSON.stringify(notification), + Payload: bigIntUtils.JSONBigInt.stringify(notification), }); const response: InvokeCommandOutput = await client.send(command); @@ -278,7 +279,7 @@ export class PushNotificationUtils { const command = new InvokeCommand({ FunctionName: ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME, InvocationType: 'Event', - Payload: JSON.stringify(walletBalanceValueMap), + Payload: bigIntUtils.JSONBigInt.stringify(walletBalanceValueMap), }); const response: InvokeCommandOutput = await client.send(command); diff --git a/packages/wallet-service/src/ws/utils.ts b/packages/wallet-service/src/ws/utils.ts index 1c28dc33..02d4571d 100644 --- a/packages/wallet-service/src/ws/utils.ts +++ b/packages/wallet-service/src/ws/utils.ts @@ -14,9 +14,11 @@ import createDefaultLogger from '@src/logger'; import config from '@src/config'; import util from 'util'; +import { createApiGatewayManagementApiClient } from '@src/utils/aws.utils'; import { Severity } from '@wallet-service/common/src/types'; import { WsConnectionInfo } from '@src/types'; import { endWsConnection } from '@src/redis'; +import { bigIntUtils } from '@hathor/wallet-lib'; const logger = createDefaultLogger(); @@ -60,11 +62,11 @@ export const sendMessageToClient = async ( 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({ + const apiGwClient = createApiGatewayManagementApiClient({ endpoint: connInfo.url, }); - const message = JSON.stringify(payload); + const message = bigIntUtils.JSONBigInt.stringify(payload); const command = new PostToConnectionCommand({ ConnectionId: connInfo.id, @@ -107,7 +109,7 @@ export const disconnectClient = async ( client: RedisClient, connInfo: WsConnectionInfo, ): Promise => { // eslint-disable-line @typescript-eslint/no-explicit-any - const apiGwClient = new ApiGatewayManagementApiClient({ + const apiGwClient = createApiGatewayManagementApiClient({ endpoint: connInfo.url, }); diff --git a/packages/wallet-service/tests/api.test.ts b/packages/wallet-service/tests/api.test.ts index ca17a30c..2417d74d 100644 --- a/packages/wallet-service/tests/api.test.ts +++ b/packages/wallet-service/tests/api.test.ts @@ -1,6 +1,7 @@ import { APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda'; import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; +import { get as addressInfoGet } from '@src/api/addressInfo'; 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'; @@ -147,8 +148,8 @@ test('GET /addresses', async () => { }]); const addresses = [ - { address: ADDRESSES[0], index: 0, walletId: 'my-wallet', transactions: 0 }, - { address: ADDRESSES[1], index: 1, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[0], index: 0, walletId: 'my-wallet', transactions: 0, seqnum: 1 }, + { address: ADDRESSES[1], index: 1, walletId: 'my-wallet', transactions: 0, seqnum: 2 }, ]; await addToAddressTable(mysql, addresses); @@ -172,11 +173,13 @@ test('GET /addresses', async () => { address: addresses[0].address, index: addresses[0].index, transactions: addresses[0].transactions, + seqnum: addresses[0].seqnum, }); expect(returnBody.addresses).toContainEqual({ address: addresses[1].address, index: addresses[1].index, transactions: addresses[1].transactions, + seqnum: addresses[1].seqnum, }); // we should error on invalid index parameter @@ -188,8 +191,8 @@ test('GET /addresses', async () => { 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"'); + expect(returnBody.details[0].message.trim()) + .toMatchInlineSnapshot(`""index" must be greater than or equal to 0"`); // we should be able to filter for a specific index event = makeGatewayEventWithAuthorizer('my-wallet', { @@ -205,6 +208,7 @@ test('GET /addresses', async () => { address: addresses[0].address, index: addresses[0].index, transactions: addresses[0].transactions, + seqnum: addresses[0].seqnum, }]); // we should receive ApiError.ADDRESS_NOT_FOUND if the address was not found @@ -458,8 +462,8 @@ test('GET /balances', async () => { await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token1', - unlockedBalance: 10, - lockedBalance: 0, + unlockedBalance: 10n, + lockedBalance: 0n, unlockedAuthorities: 0b01, lockedAuthorities: 0b10, timelockExpires: null, @@ -467,8 +471,8 @@ test('GET /balances', async () => { }, { walletId: 'my-wallet', tokenId: 'token2', - unlockedBalance: 3, - lockedBalance: 2, + unlockedBalance: 3n, + lockedBalance: 2n, unlockedAuthorities: 0b00, lockedAuthorities: 0b11, timelockExpires: lockExpires, @@ -524,8 +528,8 @@ test('GET /balances', async () => { await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token3', - unlockedBalance: 5, - lockedBalance: 1, + unlockedBalance: 5n, + lockedBalance: 1n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: lockExpires2, @@ -536,7 +540,7 @@ test('GET /balances', async () => { index: 0, tokenId: 'token3', address: ADDRESSES[0], - value: 1, + value: 1n, authorities: 0, timelock: lockExpires2, heightlock: null, @@ -562,8 +566,8 @@ test('GET /balances', async () => { await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token4', - unlockedBalance: 10, - lockedBalance: 5, + unlockedBalance: 10n, + lockedBalance: 5n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: lockExpires2, @@ -574,7 +578,7 @@ test('GET /balances', async () => { index: 0, tokenId: 'token4', address: ADDRESSES[0], - value: 3, + value: 3n, authorities: 0, timelock: lockExpires2, heightlock: null, @@ -585,7 +589,7 @@ test('GET /balances', async () => { index: 0, tokenId: 'token4', address: ADDRESSES[0], - value: 2, + value: 2n, authorities: 0, timelock: lockExpires, heightlock: null, @@ -610,8 +614,8 @@ test('GET /balances', async () => { await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: '00', - unlockedBalance: 10, - lockedBalance: 0, + unlockedBalance: 10n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -746,7 +750,7 @@ test('GET /wallet', async () => { expect(result.statusCode).toBe(200); expect(returnBody.success).toBe(true); expect(returnBody.status).toStrictEqual({ - walletId: getWalletId(XPUBKEY), + walletId: 'my-wallet', xpubkey: XPUBKEY, authXpubkey: AUTH_XPUBKEY, status: 'ready', @@ -754,6 +758,7 @@ test('GET /wallet', async () => { retryCount: 0, createdAt: 10000, readyAt: 10001, + lastUsedAddressIndex: -1, }); }); @@ -1478,7 +1483,7 @@ test('GET /wallet/tokens/token_id/details', async () => { index: 0, tokenId: token1.id, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -1490,8 +1495,8 @@ test('GET /wallet/tokens/token_id/details', async () => { index: 1, tokenId: token1.id, address: ADDRESSES[0], - value: 0, - authorities: constants.TOKEN_MINT_MASK, + value: 0n, + authorities: Number(constants.TOKEN_MINT_MASK), timelock: null, heightlock: null, locked: false, @@ -1502,8 +1507,8 @@ test('GET /wallet/tokens/token_id/details', async () => { index: 2, tokenId: token1.id, address: ADDRESSES[0], - value: 0, - authorities: constants.TOKEN_MINT_MASK, + value: 0n, + authorities: Number(constants.TOKEN_MINT_MASK), timelock: null, heightlock: null, locked: false, @@ -1514,7 +1519,7 @@ test('GET /wallet/tokens/token_id/details', async () => { index: 0, tokenId: token2.id, address: ADDRESSES[0], - value: 250, + value: 250n, authorities: 0, timelock: null, heightlock: null, @@ -1526,8 +1531,8 @@ test('GET /wallet/tokens/token_id/details', async () => { index: 1, tokenId: token2.id, address: ADDRESSES[0], - value: 0, - authorities: constants.TOKEN_MINT_MASK, + value: 0n, + authorities: Number(constants.TOKEN_MINT_MASK), timelock: 1000, heightlock: null, locked: true, @@ -1538,8 +1543,8 @@ test('GET /wallet/tokens/token_id/details', async () => { index: 2, tokenId: token2.id, address: ADDRESSES[0], - value: 0, - authorities: constants.TOKEN_MINT_MASK, + value: 0n, + authorities: Number(constants.TOKEN_MINT_MASK), timelock: 1000, heightlock: null, locked: true, @@ -1549,8 +1554,8 @@ test('GET /wallet/tokens/token_id/details', async () => { index: 0, tokenId: token2.id, address: ADDRESSES[0], - value: 0, - authorities: constants.TOKEN_MINT_MASK, + value: 0n, + authorities: Number(constants.TOKEN_MINT_MASK), timelock: null, heightlock: null, locked: false, @@ -1561,8 +1566,8 @@ test('GET /wallet/tokens/token_id/details', async () => { index: 1, tokenId: token2.id, address: ADDRESSES[0], - value: 0, - authorities: constants.TOKEN_MELT_MASK, + value: 0n, + authorities: Number(constants.TOKEN_MELT_MASK), timelock: null, heightlock: null, locked: false, @@ -1570,9 +1575,9 @@ test('GET /wallet/tokens/token_id/details', async () => { }]); 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 }, + { address: ADDRESSES[0], txId: 'txId', tokenId: token1.id, balance: 100n, timestamp: 0 }, + { address: ADDRESSES[0], txId: 'txId2', tokenId: token2.id, balance: 250n, timestamp: 0 }, + { address: ADDRESSES[0], txId: 'txId3', tokenId: token2.id, balance: 0n, timestamp: 0 }, ]); event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: token1.id }); @@ -1669,6 +1674,7 @@ test('GET /version', async () => { const mockData: FullNodeApiVersionResponse = { version: '0.38.0', network: 'mainnet', + nano_contracts_enabled: true, min_weight: 14, min_tx_weight: 14, min_tx_weight_coefficient: 1.6, @@ -1681,7 +1687,7 @@ test('GET /version', async () => { genesis_block_hash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', genesis_tx1_hash: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', genesis_tx2_hash: 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', - native_token: { name: 'Hathor', symbol: 'HTR'}, + native_token: { name: 'Hathor', symbol: 'HTR' }, }; const returnData = convertApiVersionData(mockData); @@ -2264,3 +2270,153 @@ describe('GET /health', () => { }); }); }); + +test('GET /addresses (query string index parameter)', 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, seqnum: 1 }, + { address: ADDRESSES[1], index: 1, walletId: 'my-wallet', transactions: 0, seqnum: 2 }, + ]; + + await addToAddressTable(mysql, addresses); + + // 1. No index parameter (should return all addresses) + 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, + seqnum: addresses[0].seqnum, + }); + expect(returnBody.addresses).toContainEqual({ + address: addresses[1].address, + index: addresses[1].index, + transactions: addresses[1].transactions, + seqnum: addresses[1].seqnum, + }); + + // 2. index as a string (should return the address at that index) + event = makeGatewayEventWithAuthorizer('my-wallet', { index: '1' }); + 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[0].index).toBe(1); + + // 3. index as a number (should return the address at that index) + event = makeGatewayEventWithAuthorizer('my-wallet', { index: '0' }); + 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[0].index).toBe(0); + + // 4. index is invalid (non-numeric) + event = makeGatewayEventWithAuthorizer('my-wallet', { index: 'not-a-number' }); + result = await addressesGet(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[0].message).toMatch(/must be a number/); + + // 5. index is negative (should error) + event = makeGatewayEventWithAuthorizer('my-wallet', { index: '-1' }); + result = await addressesGet(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[0].message).toMatch(/greater than or equal to 0/); + + // 6. index not found (should return ADDRESS_NOT_FOUND) + event = makeGatewayEventWithAuthorizer('my-wallet', { index: '999' }); + 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 /address/info', 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, seqnum: 1 }, + { address: ADDRESSES[1], index: 1, walletId: 'my-wallet', transactions: 0, seqnum: 2 }, + ]; + + await addToAddressTable(mysql, addresses); + + // 1. Send address 0 (should return the address info) + let event = makeGatewayEventWithAuthorizer('my-wallet', { address: ADDRESSES[0] }); + let result = await addressInfoGet(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.data).toEqual({ + address: addresses[0].address, + index: addresses[0].index, + transactions: addresses[0].transactions, + seqnum: addresses[0].seqnum, + }); + + // 2. Send address 1 (should return the address info) + event = makeGatewayEventWithAuthorizer('my-wallet', { address: ADDRESSES[1] }); + result = await addressInfoGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.data).toEqual({ + address: addresses[1].address, + index: addresses[1].index, + transactions: addresses[1].transactions, + seqnum: addresses[1].seqnum, + }); + + // 3. Send invalid address (should return error) + event = makeGatewayEventWithAuthorizer('my-wallet', { address: 'address' }); + result = await addressInfoGet(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[0].message).toBeDefined(); + + // 4. Do not send address (should return error) + event = makeGatewayEventWithAuthorizer('my-wallet', null); + result = await addressInfoGet(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[0].message).toBeDefined(); +}); diff --git a/packages/wallet-service/tests/auth.readonly.test.ts b/packages/wallet-service/tests/auth.readonly.test.ts new file mode 100644 index 00000000..72b63f4b --- /dev/null +++ b/packages/wallet-service/tests/auth.readonly.test.ts @@ -0,0 +1,547 @@ +/** + * 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, APIGatewayTokenAuthorizerEvent, CustomAuthorizerResult } from 'aws-lambda'; +import jwt from 'jsonwebtoken'; +import bitcore from 'bitcore-lib'; + +import { roTokenHandler, bearerAuthorizer, tokenHandler } from '@src/api/auth'; +import { ApiError } from '@src/api/errors'; +import { closeDbConnection, getDbConnection, getWalletId, getAddressFromXpub } from '@src/utils'; +import { WalletStatus } from '@src/types'; +import { + XPUBKEY, + AUTH_XPUBKEY, + addToWalletTable, + cleanDatabase, + makeGatewayEvent, +} from '@tests/utils'; +import config from '@src/config'; + +// 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); +}); + +describe('roTokenHandler', () => { + it('should return a read-only JWT token for a valid ready wallet', async () => { + const walletId = getWalletId(XPUBKEY); + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: XPUBKEY, + authXpubkey: 'xpub-auth', + status: WalletStatus.READY, + maxGap: 20, + createdAt: 10000, + readyAt: 10001, + }]); + + const event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + })); + + const result = await roTokenHandler(event, null, null) as APIGatewayProxyResult; + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(200); + expect(body.success).toBe(true); + expect(body.token).toBeDefined(); + + // Verify JWT structure + const decoded = jwt.verify(body.token, config.authSecret) as any; + expect(decoded.wid).toBe(walletId); + expect(decoded.mode).toBe('ro'); + expect(decoded.jti).toBeDefined(); + expect(decoded.exp).toBeDefined(); + // Should not contain signature data + expect(decoded.sign).toBeUndefined(); + expect(decoded.ts).toBeUndefined(); + expect(decoded.addr).toBeUndefined(); + }); + + it('should return WALLET_NOT_FOUND for non-existent wallet', async () => { + const event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + })); + + const result = await roTokenHandler(event, null, null) as APIGatewayProxyResult; + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(400); + expect(body.success).toBe(false); + expect(body.error).toBe(ApiError.WALLET_NOT_FOUND); + }); + + it('should return WALLET_NOT_READY for wallet not in READY status', async () => { + const walletId = getWalletId(XPUBKEY); + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: XPUBKEY, + authXpubkey: 'xpub-auth', + status: WalletStatus.CREATING, + maxGap: 20, + createdAt: 10000, + readyAt: null, + }]); + + const event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + })); + + const result = await roTokenHandler(event, null, null) as APIGatewayProxyResult; + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(400); + expect(body.success).toBe(false); + expect(body.error).toBe(ApiError.WALLET_NOT_READY); + }); + + it('should return INVALID_PAYLOAD for missing xpubkey', async () => { + const event = makeGatewayEvent({}, JSON.stringify({})); + + const result = await roTokenHandler(event, null, null) as APIGatewayProxyResult; + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(400); + expect(body.success).toBe(false); + expect(body.error).toBe(ApiError.INVALID_PAYLOAD); + expect(body.details).toBeDefined(); + expect(body.details[0].message).toContain('xpubkey'); + }); + + it('should return INVALID_PAYLOAD for invalid request body', async () => { + const event = makeGatewayEvent(null); + event.body = 'invalid json'; + + const result = await roTokenHandler(event, null, null) as APIGatewayProxyResult; + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(400); + expect(body.success).toBe(false); + expect(body.error).toBe(ApiError.INVALID_PAYLOAD); + }); +}); + +describe('tokenHandler (full-access)', () => { + it('should return INVALID_PAYLOAD for missing required fields', async () => { + const event = makeGatewayEvent({}, JSON.stringify({})); + + const result = await tokenHandler(event, null, null) as APIGatewayProxyResult; + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(400); + expect(body.success).toBe(false); + expect(body.error).toBe(ApiError.INVALID_PAYLOAD); + expect(body.details).toBeDefined(); + }); + + it('should return INVALID_PAYLOAD for invalid JSON body', async () => { + const event = makeGatewayEvent(null); + event.body = 'invalid json'; + + const result = await tokenHandler(event, null, null) as APIGatewayProxyResult; + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(400); + expect(body.success).toBe(false); + expect(body.error).toBe(ApiError.INVALID_PAYLOAD); + }); + + it('should return WALLET_NOT_FOUND for non-existent wallet', async () => { + const walletId = getWalletId(XPUBKEY); + const now = Math.floor(Date.now() / 1000); + const address = getAddressFromXpub(AUTH_XPUBKEY); + + // Create a valid signature + const message = `${walletId}:${now}`; + const privateKey = new bitcore.PrivateKey(); + const signature = bitcore.Message(message).sign(privateKey); + + const event = makeGatewayEvent({}, JSON.stringify({ + ts: now, + xpub: AUTH_XPUBKEY, + sign: signature, + walletId, + })); + + const result = await tokenHandler(event, null, null) as APIGatewayProxyResult; + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(400); + expect(body.success).toBe(false); + expect(body.error).toBe(ApiError.WALLET_NOT_FOUND); + }); + + it('should return error for invalid timestamp', async () => { + const walletId = getWalletId(XPUBKEY); + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + status: WalletStatus.READY, + maxGap: 20, + createdAt: 10000, + readyAt: 10001, + }]); + + const invalidTimestamp = Math.floor(Date.now() / 1000) - 100000; // Very old timestamp + const address = getAddressFromXpub(AUTH_XPUBKEY); + const message = `${walletId}:${invalidTimestamp}`; + const privateKey = new bitcore.PrivateKey(); + const signature = bitcore.Message(message).sign(privateKey); + + const event = makeGatewayEvent({}, JSON.stringify({ + ts: invalidTimestamp, + xpub: AUTH_XPUBKEY, + sign: signature, + walletId, + })); + + const result = await tokenHandler(event, null, null) as APIGatewayProxyResult; + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(400); + expect(body.success).toBe(false); + expect(body.error).toBe(ApiError.AUTH_INVALID_SIGNATURE); + expect(body.details).toBeDefined(); + expect(body.details[0].message).toContain('timestamp is shifted'); + }); + + it('should return error for mismatched auth xpubkey', async () => { + const walletId = getWalletId(XPUBKEY); + const wrongAuthXpub = XPUBKEY; // Using XPUBKEY as wrong auth (stored auth is AUTH_XPUBKEY) + + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + status: WalletStatus.READY, + maxGap: 20, + createdAt: 10000, + readyAt: 10001, + }]); + + const now = Math.floor(Date.now() / 1000); + const message = `${walletId}:${now}`; + const privateKey = new bitcore.PrivateKey(); + const signature = bitcore.Message(message).sign(privateKey); + + const event = makeGatewayEvent({}, JSON.stringify({ + ts: now, + xpub: wrongAuthXpub, + sign: signature, + walletId, + })); + + const result = await tokenHandler(event, null, null) as APIGatewayProxyResult; + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(400); + expect(body.success).toBe(false); + expect(body.error).toBe(ApiError.INVALID_PAYLOAD); + expect(body.details).toBeDefined(); + expect(body.details[0].message).toContain('does not match'); + }); + + it('should return error for invalid signature', async () => { + const walletId = getWalletId(XPUBKEY); + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + status: WalletStatus.READY, + maxGap: 20, + createdAt: 10000, + readyAt: 10001, + }]); + + const now = Math.floor(Date.now() / 1000); + + const event = makeGatewayEvent({}, JSON.stringify({ + ts: now, + xpub: AUTH_XPUBKEY, + sign: 'invalid-signature', + walletId, + })); + + const result = await tokenHandler(event, null, null) as APIGatewayProxyResult; + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(400); + expect(body.success).toBe(false); + expect(body.error).toBe(ApiError.AUTH_INVALID_SIGNATURE); + }); + + it('should successfully generate a full-access token with valid signature', async () => { + const walletId = getWalletId(XPUBKEY); + await addToWalletTable(mysql, [{ + id: walletId, + xpubkey: XPUBKEY, + authXpubkey: AUTH_XPUBKEY, + status: WalletStatus.READY, + maxGap: 20, + createdAt: 10000, + readyAt: 10001, + }]); + + const now = Math.floor(Date.now() / 1000); + const address = getAddressFromXpub(AUTH_XPUBKEY); + const message = `${walletId}:${now}`; + + // Create a proper private key and sign + const privateKey = new bitcore.PrivateKey(); + const addressFromPrivateKey = privateKey.toAddress(); + + // We need to mock verifySignature to return true for this test + // since we can't easily create a valid signature that matches the stored auth xpubkey + const originalVerifySignature = require('@src/utils').verifySignature; + jest.spyOn(require('@src/utils'), 'verifySignature').mockReturnValueOnce(true); + + const signature = bitcore.Message(message).sign(privateKey); + + const event = makeGatewayEvent({}, JSON.stringify({ + ts: now, + xpub: AUTH_XPUBKEY, + sign: signature, + walletId, + })); + + const result = await tokenHandler(event, null, null) as APIGatewayProxyResult; + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(200); + expect(body.success).toBe(true); + expect(body.token).toBeDefined(); + + // Verify JWT structure + const decoded = jwt.verify(body.token, config.authSecret) as any; + expect(decoded.wid).toBe(walletId); + expect(decoded.mode).toBe('full'); + expect(decoded.sign).toBeDefined(); + expect(decoded.ts).toBe(now); + expect(decoded.addr).toBeDefined(); + + // Restore original + jest.restoreAllMocks(); + }); +}); + +describe('bearerAuthorizer with read-only mode', () => { + it('should authorize read-only token with correct policy', async () => { + const walletId = getWalletId(XPUBKEY); + + // Generate a read-only JWT token + const token = jwt.sign( + { + wid: walletId, + mode: 'ro', + }, + config.authSecret, + { + expiresIn: 1800, + jwtid: 'test-jti', + }, + ); + + const event: APIGatewayTokenAuthorizerEvent = { + type: 'TOKEN', + methodArn: 'arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/wallet/balances', + authorizationToken: `Bearer ${token}`, + }; + + const result = await bearerAuthorizer(event, null, null) as CustomAuthorizerResult; + + expect(result.principalId).toBe(walletId); + expect(result.context.walletId).toBe(walletId); + expect(result.context.mode).toBe('ro'); + expect(result.policyDocument.Statement[0].Effect).toBe('Allow'); + + // Check that read-only resources are included + const statement = result.policyDocument.Statement[0] as any; + const resources = statement.Resource as string[]; + expect(resources).toContain('arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/*/wallet/balances'); + expect(resources).toContain('arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/*/wallet/status'); + expect(resources).toContain('arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/*/wallet/addresses'); + expect(resources).toContain('arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/*/wallet/history'); + + // Should NOT contain write endpoints + expect(resources).not.toContain('arn:aws:execute-api:us-east-1:123456789012:abcdef123/*/tx/*'); + }); + + it('should authorize full-access token with correct policy', async () => { + const walletId = getWalletId(XPUBKEY); + const now = Math.floor(Date.now() / 1000); + + // Generate a full-access JWT token + const token = jwt.sign( + { + wid: walletId, + mode: 'full', + sign: 'mock-signature', + ts: now, + addr: 'mock-address', + }, + config.authSecret, + { + expiresIn: 1800, + jwtid: 'test-jti', + }, + ); + + const event: APIGatewayTokenAuthorizerEvent = { + type: 'TOKEN', + methodArn: 'arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/POST/tx/proposal', + authorizationToken: `Bearer ${token}`, + }; + + const result = await bearerAuthorizer(event, null, null) as CustomAuthorizerResult; + + expect(result.principalId).toBe(walletId); + expect(result.context.walletId).toBe(walletId); + expect(result.context.mode).toBe('full'); + + // Check that full-access resources are included + const statement = result.policyDocument.Statement[0] as any; + const resources = statement.Resource as string[]; + expect(resources).toContain('arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/*/wallet/*'); + expect(resources).toContain('arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/*/tx/*'); + }); + + it('should default to full mode for legacy tokens without mode field', async () => { + const walletId = getWalletId(XPUBKEY); + const now = Math.floor(Date.now() / 1000); + + // Generate a legacy JWT token without mode field + const token = jwt.sign( + { + wid: walletId, + sign: 'mock-signature', + ts: now, + addr: 'mock-address', + }, + config.authSecret, + { + expiresIn: 1800, + jwtid: 'test-jti', + }, + ); + + const event: APIGatewayTokenAuthorizerEvent = { + type: 'TOKEN', + methodArn: 'arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/wallet/status', + authorizationToken: `Bearer ${token}`, + }; + + const result = await bearerAuthorizer(event, null, null) as CustomAuthorizerResult; + + expect(result.context.mode).toBe('full'); + }); + + it('should reject expired read-only token', async () => { + const walletId = getWalletId(XPUBKEY); + + // Generate an expired token + const token = jwt.sign( + { + wid: walletId, + mode: 'ro', + }, + config.authSecret, + { + expiresIn: -1, // Expired + jwtid: 'test-jti', + }, + ); + + const event: APIGatewayTokenAuthorizerEvent = { + type: 'TOKEN', + methodArn: 'arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/wallet/balances', + authorizationToken: `Bearer ${token}`, + }; + + await expect(bearerAuthorizer(event, null, null)).rejects.toThrow('Unauthorized'); + }); + + it('should reject invalid JWT token', async () => { + const event: APIGatewayTokenAuthorizerEvent = { + type: 'TOKEN', + methodArn: 'arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/wallet/balances', + authorizationToken: 'Bearer invalid-token', + }; + + await expect(bearerAuthorizer(event, null, null)).rejects.toThrow('Unauthorized'); + }); + + it('should reject missing authorization token', async () => { + const event: APIGatewayTokenAuthorizerEvent = { + type: 'TOKEN', + methodArn: 'arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/wallet/balances', + authorizationToken: null, + }; + + await expect(bearerAuthorizer(event, null, null)).rejects.toThrow('Unauthorized'); + }); + + it('should deny access for full-access token with invalid signature', async () => { + const walletId = getWalletId(XPUBKEY); + const now = Math.floor(Date.now() / 1000); + + // Generate a full-access JWT token with invalid signature + const token = jwt.sign( + { + wid: walletId, + mode: 'full', + sign: 'invalid-signature', + ts: now, + addr: 'invalid-address', + }, + config.authSecret, + { + expiresIn: 1800, + jwtid: 'test-jti', + }, + ); + + const event: APIGatewayTokenAuthorizerEvent = { + type: 'TOKEN', + methodArn: 'arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/POST/tx/proposal', + authorizationToken: `Bearer ${token}`, + }; + + const result = await bearerAuthorizer(event, null, null) as CustomAuthorizerResult; + + expect(result.principalId).toBe(walletId); + expect(result.policyDocument.Statement[0].Effect).toBe('Deny'); + }); + + it('should throw for unknown jwt verification error', async () => { + // Mock jwt.verify to throw a custom error + jest.spyOn(jwt, 'verify').mockImplementationOnce(() => { + const error: any = new Error('Some unknown error'); + error.name = 'UnknownError'; + throw error; + }); + + const event: APIGatewayTokenAuthorizerEvent = { + type: 'TOKEN', + methodArn: 'arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/wallet/balances', + authorizationToken: 'Bearer some-token', + }; + + await expect(bearerAuthorizer(event, null, null)).rejects.toThrow('Some unknown error'); + + jest.restoreAllMocks(); + }); +}); diff --git a/packages/wallet-service/tests/commons.test.ts b/packages/wallet-service/tests/commons.test.ts index 5ecb8e8a..ede095f7 100644 --- a/packages/wallet-service/tests/commons.test.ts +++ b/packages/wallet-service/tests/commons.test.ts @@ -82,22 +82,22 @@ test('markLockedOutputs and getAddressBalanceMap', () => { 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'), + createInput(10n, 'address1', 'inputTx', 0, 'token1'), + createInput(5n, 'address1', 'inputTx', 0, 'token1'), + createInput(7n, 'address1', 'inputTx', 1, 'token2'), + createInput(3n, 'address2', 'inputTx', 2, 'token1'), ]; tx.outputs = [ - createOutput(0, 5, 'address1', 'token1'), - createOutput(1, 2, 'address1', 'token3'), - createOutput(2, 11, 'address2', 'token1'), + createOutput(0, 5n, 'address1', 'token1'), + createOutput(1, 2n, 'address1', 'token3'), + createOutput(2, 11n, '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)); + map1.set('token1', new Balance(5n, -10n, 0n)); + map1.set('token2', new Balance(0n, -7n, 0n)); + map1.set('token3', new Balance(2n, 2n, 0n)); const map2 = new TokenBalanceMap(); - map2.set('token1', new Balance(11, 8, 0)); + map2.set('token1', new Balance(11n, 8n, 0n)); const expectedAddrMap = { address1: map1, address2: map2, @@ -123,14 +123,14 @@ test('markLockedOutputs and getAddressBalanceMap', () => { expect(tx.outputs[2].locked).toBe(true); // check balance - map2.set('token1', new Balance(11, -3, 11, now + 1)); + map2.set('token1', new Balance(11n, -3n, 11n, 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'), + createOutput(0, 100n, 'address1', 'token1'), ]; markLockedOutputs(tx.outputs, now, true); for (const output of tx.outputs) { @@ -138,7 +138,7 @@ test('markLockedOutputs and getAddressBalanceMap', () => { } const addrMap3 = getAddressBalanceMap(tx.inputs, tx.outputs); const map3 = new TokenBalanceMap(); - map3.set('token1', new Balance(100, 0, 100)); + map3.set('token1', new Balance(100n, 0n, 100n)); const expectedAddrMap2 = { address1: map3, }; @@ -146,16 +146,16 @@ test('markLockedOutputs and getAddressBalanceMap', () => { // tx with authorities tx.inputs = [ - createInput(0b01, 'address1', 'inputTx', 0, 'token1', null, 129), - createInput(0b10, 'address1', 'inputTx', 1, 'token2', null, 129), + createInput(0b01n, 'address1', 'inputTx', 0, 'token1', null, 129), + createInput(0b10n, 'address1', 'inputTx', 1, 'token2', null, 129), ]; tx.outputs = [ - createOutput(0, 0b01, 'address1', 'token1', null, false, 129), - createOutput(1, 0b10, 'address1', 'token2', 1000, true, 129), + createOutput(0, 0b01n, 'address1', 'token1', null, false, 129), + createOutput(1, 0b10n, '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]))); + map4.set('token1', new Balance(0n, 0n, 0n, null)); + map4.set('token2', new Balance(0n, 0n, 0n, 1000, new Authorities([-1, 0]), new Authorities([1, 0]))); const expectedAddrMap4 = { address1: map4, }; @@ -166,19 +166,19 @@ test('markLockedOutputs and getAddressBalanceMap', () => { 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)); + mapAddress1.set('token1', new Balance(1n, -10n, 0n)); + mapAddress1.set('token2', new Balance(0n, -7n, 0n)); + mapAddress1.set('token3', new Balance(27n, 2n, 0n)); const mapAddress2 = new TokenBalanceMap(); - mapAddress2.set('token1', new Balance(10, 8, 0)); + mapAddress2.set('token1', new Balance(10n, 8n, 0n)); const mapAddress3 = new TokenBalanceMap(); - mapAddress3.set('token2', new Balance(4, 2, 0)); - mapAddress3.set('token3', new Balance(12, 6, 0)); + mapAddress3.set('token2', new Balance(4n, 2n, 0n)); + mapAddress3.set('token3', new Balance(12n, 6n, 0n)); const mapAddress4 = new TokenBalanceMap(); - mapAddress4.set('token1', new Balance(10, 2, 0)); - mapAddress4.set('token2', new Balance(14, 9, 1, 500)); + mapAddress4.set('token1', new Balance(10n, 2n, 0n)); + mapAddress4.set('token2', new Balance(14n, 9n, 1n, 500)); const mapAddress5 = new TokenBalanceMap(); - mapAddress5.set('token1', new Balance(20, 11, 0)); + mapAddress5.set('token1', new Balance(20n, 11n, 0n)); const addressBalanceMap = { address1: mapAddress1, address2: mapAddress2, @@ -193,12 +193,12 @@ test('getWalletBalanceMap', () => { 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)); + mapWallet1.set('token1', new Balance(21n, 0n, 0n)); + mapWallet1.set('token2', new Balance(14n, 2n, 1n, 500)); + mapWallet1.set('token3', new Balance(27n, 2n, 0n)); const mapWallet2 = new TokenBalanceMap(); - mapWallet2.set('token2', new Balance(4, 2, 0)); - mapWallet2.set('token3', new Balance(12, 6, 0)); + mapWallet2.set('token2', new Balance(4n, 2n, 0n)); + mapWallet2.set('token3', new Balance(12n, 6n, 0n)); const expectedWalletBalanceMap = { wallet1: mapWallet1, wallet2: mapWallet2, @@ -213,7 +213,7 @@ test('getWalletBalanceMap', () => { test('unlockUtxos', async () => { expect.hasAssertions(); - const reward = 6400; + const reward = 6400n; const txId1 = 'txId1'; const txId2 = 'txId2'; const txId3 = 'txId3'; @@ -254,7 +254,7 @@ test('unlockUtxos', async () => { index: 0, tokenId: token, address: addr, - value: 2500, + value: 2500n, authorities: 0, timelock: now, heightlock: null, @@ -265,7 +265,7 @@ test('unlockUtxos', async () => { index: 0, tokenId: token, address: addr, - value: 2500, + value: 2500n, authorities: 0, timelock: now * 2, heightlock: null, @@ -276,7 +276,7 @@ test('unlockUtxos', async () => { index: 0, tokenId: token, address: addr, - value: 0, + value: 0n, authorities: 0b10, timelock: now * 3, heightlock: null, @@ -303,14 +303,14 @@ test('unlockUtxos', async () => { }]); await addToAddressBalanceTable(mysql, [ - [addr, token, 0, 2 * reward + 5000, now, 5, 0, 0b10, 4 * reward + 5000], + [addr, token, 0, 2n * reward + 5000n, now, 5, 0, 0b10, 4n * reward + 5000n], ]); await addToWalletBalanceTable(mysql, [{ walletId, tokenId: token, - unlockedBalance: 0, - lockedBalance: 2 * reward + 5000, + unlockedBalance: 0n, + lockedBalance: 2n * reward + 5000n, unlockedAuthorities: 0, lockedAuthorities: 0b10, timelockExpires: now, @@ -334,8 +334,8 @@ test('unlockUtxos', async () => { 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); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, reward, reward + 5000n, now, 5, 0, 0b10)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, reward, reward + 5000n, now, 5, 0, 0b10)).resolves.toBe(true); // unlock txId2 utxo.txId = txId2; @@ -344,36 +344,36 @@ test('unlockUtxos', async () => { 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); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 2n * reward, 5000n, now, 5, 0, 0b10)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 2n * reward, 5000n, now, 5, 0, 0b10)).resolves.toBe(true); // unlock txId3, txId4 is still locked utxo.txId = txId3; - utxo.value = 2500; + utxo.value = 2500n; 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); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 2n * reward + 2500n, 2500n, 2 * now, 5, 0, 0b10)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 2n * reward + 2500n, 2500n, 2 * now, 5, 0, 0b10)).resolves.toBe(true); // unlock txId4 utxo.txId = txId4; - utxo.value = 2500; + utxo.value = 2500n; 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); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 2n * reward + 5000n, 0n, 3 * now, 5, 0, 0b10)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 2n * reward + 5000n, 0n, 3 * now, 5, 0, 0b10)).resolves.toBe(true); // unlock txId5 utxo.txId = txId5; - utxo.value = 0; + utxo.value = 0n; utxo.authorities = 0b10; utxo.timelock = now * 3; utxo.heightlock = null; @@ -381,14 +381,14 @@ test('unlockUtxos', async () => { 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); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 2n * reward + 5000n, 0n, null, 5, 0b10, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 2n * reward + 5000n, 0n, null, 5, 0b10, 0)).resolves.toBe(true); }); test('unlockTimelockedUtxos', async () => { expect.hasAssertions(); - const reward = 6400; + const reward = 6400n; const txId1 = 'txId1'; const txId2 = 'txId2'; const txId3 = 'txId3'; @@ -401,7 +401,7 @@ test('unlockTimelockedUtxos', async () => { index: 0, tokenId: token, address: addr, - value: 2500, + value: 2500n, authorities: 0, timelock: now, heightlock: null, @@ -412,7 +412,7 @@ test('unlockTimelockedUtxos', async () => { index: 0, tokenId: token, address: addr, - value: 2500, + value: 2500n, authorities: 0, timelock: now * 2, heightlock: null, @@ -423,7 +423,7 @@ test('unlockTimelockedUtxos', async () => { index: 0, tokenId: token, address: addr, - value: 0, + value: 0n, authorities: 0b10, timelock: now * 3, heightlock: null, @@ -455,8 +455,8 @@ test('unlockTimelockedUtxos', async () => { await addToWalletBalanceTable(mysql, [{ walletId, tokenId: token, - unlockedBalance: 0, - lockedBalance: 5000, + unlockedBalance: 0n, + lockedBalance: 5000n, unlockedAuthorities: 0, lockedAuthorities: 0b10, timelockExpires: now, @@ -477,31 +477,31 @@ test('unlockTimelockedUtxos', async () => { // unlock txId1, txId2 is still locked utxo.txId = txId1; - utxo.value = 2500; + utxo.value = 2500n; 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); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 2500n, 2500n, 2 * now, 3, 0, 0b10)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 2500n, 2500n, 2 * now, 3, 0, 0b10)).resolves.toBe(true); // unlock txId2 utxo.txId = txId2; - utxo.value = 2500; + utxo.value = 2500n; 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); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 5000n, 0n, 3 * now, 3, 0, 0b10)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 5000n, 0n, 3 * now, 3, 0, 0b10)).resolves.toBe(true); // unlock txId3 utxo.txId = txId3; - utxo.value = 0; + utxo.value = 0n; utxo.authorities = 0b10; utxo.timelock = now * 3; utxo.heightlock = null; @@ -509,8 +509,8 @@ test('unlockTimelockedUtxos', async () => { 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); + await expect(checkAddressBalanceTable(mysql, 1, addr, token, 5000n, 0n, null, 3, 0b10, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 5000n, 0n, null, 3, 0b10, 0)).resolves.toBe(true); }); test('getFullnodeData with an uninitialized version_data database should call the version api', async () => { @@ -519,6 +519,7 @@ test('getFullnodeData with an uninitialized version_data database should call th const mockData = { version: '0.38.0', network: 'mainnet', + nano_contracts_enabled: true, min_weight: 14, min_tx_weight: 14, min_tx_weight_coefficient: 1.6, @@ -561,6 +562,7 @@ test('getFullnodeData with an initialized version_data database should query dat const mockedVersionData: FullNodeApiVersionResponse = { version: '0.38.0', network: 'mainnet', + nano_contracts_enabled: true, min_weight: 14, min_tx_weight: 14, min_tx_weight_coefficient: 1.6, @@ -654,8 +656,8 @@ describe('getWalletBalancesForTx', () => { // 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: 0, value: 5n, address: addr1, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 1, value: 5n, address: addr2, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, ]; // instantiate outputs @@ -684,7 +686,7 @@ describe('getWalletBalancesForTx', () => { const tx = { tx_id: tx1.id, - nonce: 10, + nonce: 10n, timestamp: tx1.timestamp, version: tx1.version, weight: tx1.weight, @@ -738,8 +740,8 @@ describe('getWalletBalancesForTx', () => { // instantiate token balance const balanceToken1 = { - unlocked: 5, - locked: 0, + unlocked: 5n, + locked: 0n, lockExpires: null, transactions: 1, unlockedAuthorities: new Authorities(0b01), @@ -756,8 +758,8 @@ describe('getWalletBalancesForTx', () => { // 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: 0, value: 5n, address: addr1, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 1, value: 5n, address: addr2, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, ]; // instantiate outputs @@ -786,7 +788,7 @@ describe('getWalletBalancesForTx', () => { const tx = { tx_id: tx1.id, - nonce: 10, + nonce: 10n, timestamp: tx1.timestamp, version: tx1.version, weight: tx1.weight, @@ -811,18 +813,18 @@ describe('getWalletBalancesForTx', () => { tokenId: 'token1', tokenSymbol: 'T1', lockExpires: null, - lockedAmount: 0, + lockedAmount: 0n, lockedAuthorities: { melt: false, mint: false, }, - totalAmountSent: 0, - unlockedAmount: -5, + totalAmountSent: 0n, + unlockedAmount: -5n, unlockedAuthorities: { melt: false, mint: false, }, - total: -5, + total: -5n, }, ], }, @@ -873,16 +875,16 @@ describe('getWalletBalancesForTx', () => { // instantiate token balance const balanceToken1 = { - unlocked: 5, - locked: 0, + unlocked: 5n, + locked: 0n, lockExpires: null, transactions: 1, unlockedAuthorities: new Authorities(0b01), lockedAuthorities: 0, }; const balanceToken2 = { - unlocked: 10, - locked: 0, + unlocked: 10n, + locked: 0n, lockExpires: null, transactions: 1, unlockedAuthorities: new Authorities(0b01), @@ -900,9 +902,9 @@ describe('getWalletBalancesForTx', () => { // 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: 0, value: 5n, address: addr1, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 1, value: 5n, address: addr2, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 2, value: 10n, address: addr1, tokenId: token2.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, ]; // instantiate outputs @@ -939,7 +941,7 @@ describe('getWalletBalancesForTx', () => { const tx = { tx_id: tx1.id, - nonce: 10, + nonce: 10n, timestamp: tx1.timestamp, version: tx1.version, weight: tx1.weight, @@ -964,14 +966,14 @@ describe('getWalletBalancesForTx', () => { tokenId: 'token2', tokenSymbol: 'T2', lockExpires: null, - lockedAmount: 0, + lockedAmount: 0n, lockedAuthorities: { melt: false, mint: false, }, - total: -10, - totalAmountSent: 0, - unlockedAmount: -10, + total: -10n, + totalAmountSent: 0n, + unlockedAmount: -10n, unlockedAuthorities: { melt: false, mint: false, @@ -981,18 +983,18 @@ describe('getWalletBalancesForTx', () => { tokenId: 'token1', tokenSymbol: 'T1', lockExpires: null, - lockedAmount: 0, + lockedAmount: 0n, lockedAuthorities: { melt: false, mint: false, }, - totalAmountSent: 0, - unlockedAmount: -5, + totalAmountSent: 0n, + unlockedAmount: -5n, unlockedAuthorities: { melt: false, mint: false, }, - total: -5, + total: -5n, }, ], }, @@ -1043,16 +1045,16 @@ describe('getWalletBalancesForTx', () => { // instantiate token balance const balanceToken1 = { - unlocked: 5, - locked: 0, + unlocked: 5n, + locked: 0n, lockExpires: null, transactions: 1, unlockedAuthorities: new Authorities(0b01), lockedAuthorities: 0, }; const balanceToken2 = { - unlocked: 10, - locked: 0, + unlocked: 10n, + locked: 0n, lockExpires: null, transactions: 1, unlockedAuthorities: new Authorities(0b01), @@ -1070,10 +1072,10 @@ describe('getWalletBalancesForTx', () => { // 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 }, + { index: 0, value: 5n, address: addr1, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 1, value: 5n, address: addr2, tokenId: token1.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 2, value: 10n, address: addr1, tokenId: token2.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, + { index: 3, value: 10n, address: addr2, tokenId: token2.id, locked: false, timelock: null, tokenData: 0, spentBy: null }, ]; // instantiate outputs @@ -1110,7 +1112,7 @@ describe('getWalletBalancesForTx', () => { const tx = { tx_id: tx1.id, - nonce: 10, + nonce: 10n, timestamp: tx1.timestamp, version: tx1.version, weight: tx1.weight, @@ -1135,14 +1137,14 @@ describe('getWalletBalancesForTx', () => { tokenId: 'token2', tokenSymbol: 'T2', lockExpires: null, - lockedAmount: 0, + lockedAmount: 0n, lockedAuthorities: { melt: false, mint: false, }, - total: 10, - totalAmountSent: 10, - unlockedAmount: 10, + total: 10n, + totalAmountSent: 10n, + unlockedAmount: 10n, unlockedAuthorities: { melt: false, mint: false, @@ -1152,18 +1154,18 @@ describe('getWalletBalancesForTx', () => { tokenId: 'token1', tokenSymbol: 'T1', lockExpires: null, - lockedAmount: 0, + lockedAmount: 0n, lockedAuthorities: { melt: false, mint: false, }, - totalAmountSent: 5, - unlockedAmount: 5, + totalAmountSent: 5n, + unlockedAmount: 5n, unlockedAuthorities: { melt: false, mint: false, }, - total: 5, + total: 5n, }, ], }, diff --git a/packages/wallet-service/tests/db.test.ts b/packages/wallet-service/tests/db.test.ts index 33759da5..3404c43a 100644 --- a/packages/wallet-service/tests/db.test.ts +++ b/packages/wallet-service/tests/db.test.ts @@ -427,14 +427,14 @@ test('initWalletTxHistory', async () => { 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 }, + { address: addr1, txId: txId1, tokenId: token1, balance: 10n, timestamp: timestamp1 }, + { address: addr1, txId: txId1, tokenId: token2, balance: 7n, timestamp: timestamp1 }, + { address: addr2, txId: txId1, tokenId: token2, balance: 5n, timestamp: timestamp1 }, + { address: addr3, txId: txId1, tokenId: token1, balance: 3n, timestamp: timestamp1 }, + { address: addr1, txId: txId2, tokenId: token1, balance: -1n, timestamp: timestamp2 }, + { address: addr1, txId: txId2, tokenId: token3, balance: 3n, timestamp: timestamp2 }, + { address: addr2, txId: txId2, tokenId: token2, balance: -5n, timestamp: timestamp2 }, + { address: addr3, txId: txId2, tokenId: token1, balance: 3n, timestamp: timestamp2 }, ]; await addToAddressTxHistoryTable(mysql, entries); @@ -469,14 +469,14 @@ test('initWalletBalance', async () => { * 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 }, + { address: addr1, txId: tx1, tokenId: token1, balance: 10n, timestamp: ts1 }, + { address: addr1, txId: tx2, tokenId: token1, balance: -8n, timestamp: ts2 }, + { address: addr1, txId: tx1, tokenId: token2, balance: 5n, timestamp: ts1 }, + { address: addr2, txId: tx1, tokenId: token1, balance: 3n, timestamp: ts1 }, + { address: addr2, txId: tx3, tokenId: token1, balance: 4n, timestamp: ts3 }, + { address: addr2, txId: tx2, tokenId: token2, balance: 2n, timestamp: ts2 }, + { address: addr3, txId: tx1, tokenId: token1, balance: 1n, timestamp: ts1 }, + { address: addr3, txId: tx3, tokenId: token2, balance: 11n, timestamp: ts3 }, ]; const addressEntries = [ // address, tokenId, unlocked, locked, lockExpires, transactions, unlocked_authorities, locked_authorities, total_received @@ -494,8 +494,8 @@ test('initWalletBalance', async () => { 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); + await expect(checkWalletBalanceTable(mysql, 2, walletId, token1, 7n, 2n, null, 3, 3)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 2, walletId, token2, 1n, 6n, timelock, 2, 2)).resolves.toBe(true); }); test('updateWalletTablesWithTx', async () => { @@ -520,10 +520,10 @@ test('updateWalletTablesWithTx', async () => { // add tx1 const walletBalanceMap1 = { - walletId: TokenBalanceMap.fromStringMap({ token1: { unlocked: 5, locked: 0, unlockedAuthorities: new Authorities(0b01) } }), + walletId: TokenBalanceMap.fromStringMap({ token1: { unlocked: 5n, locked: 0n, 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(checkWalletBalanceTable(mysql, 1, walletId, token1, 5n, 0n, null, 1, 0b01, 0)).resolves.toBe(true); await expect(checkWalletTxHistoryTable(mysql, 1, walletId, token1, tx1, 5, ts1)).resolves.toBe(true); // add tx2 @@ -536,8 +536,8 @@ test('updateWalletTablesWithTx', async () => { ), }; 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(checkWalletBalanceTable(mysql, 2, walletId, token1, 3n, 1n, 500, 2, 0b11, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 2, walletId, token2, 7n, 0n, 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); @@ -558,9 +558,9 @@ test('updateWalletTablesWithTx', async () => { 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(checkWalletBalanceTable(mysql, 3, walletId, token1, 4n, 3n, 200, 3, 0b10, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, walletId, token2, 7n, 0n, null, 1)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, walletId2, token2, 10n, 0n, 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); @@ -573,12 +573,12 @@ test('addUtxos, getUtxos, unlockUtxos, updateTxOutputSpentBy, unspendUtxos, getT 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 }, + { value: 5n, address: 'address1', tokenId: 'token1', locked: false }, + { value: 15n, address: 'address1', tokenId: 'token1', locked: false }, + { value: 25n, address: 'address2', tokenId: 'token2', timelock: 500, locked: true }, + { value: 35n, address: 'address2', tokenId: 'token1', locked: false }, // authority utxo - { value: 0b11, address: 'address1', tokenId: 'token1', locked: false, tokenData: 129 }, + { value: 0b11n, address: 'address1', tokenId: 'token1', locked: false, tokenData: 129 }, ]; // empty list should be fine @@ -601,8 +601,8 @@ test('addUtxos, getUtxos, unlockUtxos, updateTxOutputSpentBy, unspendUtxos, getT const { token, decoded } = output; let authorities = 0; if (isAuthority(output.token_data)) { - authorities = value; - value = 0; + authorities = Number(value); + value = 0n; } await expect( checkUtxoTable(mysql, utxos.length, txId, output.index, token, decoded.address, value, authorities, decoded.timelock, null, output.locked), @@ -680,8 +680,8 @@ test('addUtxos, getUtxos, unlockUtxos, updateTxOutputSpentBy, unspendUtxos, getT const { token, decoded } = output; let authorities = 0; if (isAuthority(output.token_data)) { - authorities = value; - value = 0; + authorities = Number(value); + value = 0n; } await expect( checkUtxoTable(mysql, utxos.length, txId, index, token, decoded.address, value, authorities, decoded.timelock, null, output.locked), @@ -694,7 +694,7 @@ test('addUtxos, getUtxos, unlockUtxos, updateTxOutputSpentBy, unspendUtxos, getT index: 2, tokenId: 'token2', address: 'address2', - value: 25, + value: 25n, authorities: 0, timelock: 500, heightlock: null, @@ -718,9 +718,9 @@ 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 }, + { value: 5n, address: 'address1', token: 'token1', locked: false }, + { value: 25n, address: 'address2', token: 'token2', timelock: 500, locked: true }, + { value: 35n, address: 'address2', token: 'token1', locked: false }, ]; // add to utxo table @@ -734,7 +734,7 @@ test('getLockedUtxoFromInputs', async () => { 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); + expect(results[0].value).toBe(25n); }); test('updateAddressTablesWithTx', async () => { @@ -763,10 +763,10 @@ test('updateAddressTablesWithTx', async () => { 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(checkAddressBalanceTable(mysql, 4, address1, token1, 10n, 0n, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address1, token2, 7n, 0n, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address1, token3, 2n, 0n, null, 1, 0b01, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 4, address2, token1, 8n, 0n, 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); @@ -786,11 +786,11 @@ test('updateAddressTablesWithTx', async () => { 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); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5n, 0n, null, 2)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token2', 7n, 0n, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token3', 8n, 0n, null, 2, 0, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address2, 'token1', 16n, 0n, null, 2, 0b11, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 5, address2, 'token2', 3n, 0n, 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); @@ -810,7 +810,7 @@ test('updateAddressTablesWithTx', async () => { 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); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5n, 3n, lockExpires, 3)).resolves.toBe(true); // another tx, with higher timelock const txId4 = 'txId4'; @@ -819,7 +819,7 @@ test('updateAddressTablesWithTx', async () => { 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); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5n, 5n, lockExpires, 4)).resolves.toBe(true); // another tx, with lower timelock const txId5 = 'txId5'; @@ -828,7 +828,7 @@ test('updateAddressTablesWithTx', async () => { 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); + await expect(checkAddressBalanceTable(mysql, 5, address1, 'token1', 5n, 7n, lockExpires - 1, 5)).resolves.toBe(true); }); test('getWalletTokens', async () => { @@ -937,8 +937,8 @@ test('getWalletBalances', async () => { await addToWalletBalanceTable(mysql, [{ walletId, tokenId: token1.id, - unlockedBalance: 10, - lockedBalance: 4, + unlockedBalance: 10n, + lockedBalance: 4n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: now, @@ -946,8 +946,8 @@ test('getWalletBalances', async () => { }, { walletId, tokenId: token2.id, - unlockedBalance: 20, - lockedBalance: 5, + unlockedBalance: 20n, + lockedBalance: 5n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: now, @@ -955,8 +955,8 @@ test('getWalletBalances', async () => { }, { walletId: 'otherId', tokenId: token1.id, - unlockedBalance: 30, - lockedBalance: 1, + unlockedBalance: 30n, + lockedBalance: 1n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: now, @@ -974,14 +974,14 @@ test('getWalletBalances', async () => { 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.unlockedAmount).toBe(10n); + expect(balance.balance.lockedAmount).toBe(4n); 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.balance.unlockedAmount).toBe(20n); + expect(balance.balance.lockedAmount).toBe(5n); expect(balance.transactions).toBe(2); expect(balance.balance.lockExpires).toBe(now); } @@ -995,8 +995,8 @@ test('getWalletBalances', async () => { 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.unlockedAmount).toBe(20n); + expect(returnedBalances[0].balance.lockedAmount).toBe(5n); expect(returnedBalances[0].balance.lockExpires).toBe(now); expect(returnedBalances[0].transactions).toBe(2); @@ -1012,17 +1012,17 @@ test('getUtxosLockedAtHeight', async () => { const txId2 = 'txId2'; const utxos = [ // no locks - { value: 5, address: 'address1', token: 'token1', locked: false }, + { value: 5n, address: 'address1', token: 'token1', locked: false }, // only timelock - { value: 25, address: 'address2', token: 'token2', timelock: 50, locked: false }, + { value: 25n, address: 'address2', token: 'token2', timelock: 50, locked: false }, ]; const utxos2 = [ // only heightlock - { value: 35, address: 'address2', token: 'token1', timelock: null, locked: true }, + { value: 35n, 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 }, + { value: 45n, address: 'address2', token: 'token1', timelock: 100, locked: true }, + { value: 55n, address: 'address2', token: 'token1', timelock: 1000, locked: true }, ]; // add to utxo table @@ -1035,15 +1035,15 @@ test('getUtxosLockedAtHeight', async () => { // { 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); + expect(results[0].value).toBe(35n); // 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}, + // { value: 35n, address: 'address2', token: 'token1', timelock: null}, + // { value: 45n, 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); + expect([35n, 45n]).toContain(results[0].value); + expect([35n, 45n]).toContain(results[1].value); // fetch on timestamp=100 and heightlock=9. Should return empty results = await getUtxosLockedAtHeight(mysql, 1000, 9); @@ -1071,9 +1071,9 @@ test('updateAddressLockedBalance', async () => { 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); + await expect(checkAddressBalanceTable(mysql, 3, addr1, tokenId, 60n, 10n, null, 3, 0b01, 0)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 3, addr2, tokenId, 5n, 0n, null, 1)).resolves.toBe(true); + await expect(checkAddressBalanceTable(mysql, 3, addr1, otherToken, 5n, 5n, 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, [{ @@ -1081,7 +1081,7 @@ test('updateAddressLockedBalance', async () => { index: 0, tokenId, address: addr1, - value: 0, + value: 0n, authorities: 0b01, timelock: 10000, heightlock: null, @@ -1090,7 +1090,7 @@ test('updateAddressLockedBalance', async () => { }]); 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); + await expect(checkAddressBalanceTable(mysql, 3, addr1, tokenId, 60n, 10n, null, 3, 0b11, 0b01)).resolves.toBe(true); }); test('updateWalletLockedBalance', async () => { @@ -1105,8 +1105,8 @@ test('updateWalletLockedBalance', async () => { const entries = [{ walletId: wallet1, tokenId, - unlockedBalance: 10, - lockedBalance: 20, + unlockedBalance: 10n, + lockedBalance: 20n, unlockedAuthorities: 0b01, lockedAuthorities: 0, timelockExpires: now, @@ -1114,8 +1114,8 @@ test('updateWalletLockedBalance', async () => { }, { walletId: wallet2, tokenId, - unlockedBalance: 0, - lockedBalance: 100, + unlockedBalance: 0n, + lockedBalance: 100n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: now, @@ -1123,8 +1123,8 @@ test('updateWalletLockedBalance', async () => { }, { walletId: wallet1, tokenId: otherToken, - unlockedBalance: 1, - lockedBalance: 2, + unlockedBalance: 1n, + lockedBalance: 2n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -1135,9 +1135,9 @@ test('updateWalletLockedBalance', async () => { 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); + await expect(checkWalletBalanceTable(mysql, 3, wallet1, tokenId, 25n, 5n, now, 5, 0b11, 0)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, wallet2, tokenId, 50n, 50n, now, 4)).resolves.toBe(true); + await expect(checkWalletBalanceTable(mysql, 3, wallet1, otherToken, 1n, 2n, 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, [{ @@ -1149,7 +1149,7 @@ test('updateWalletLockedBalance', async () => { 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); + await expect(checkWalletBalanceTable(mysql, 3, wallet1, tokenId, 25n, 5n, now, 5, 0b11, 0b01)).resolves.toBe(true); }); test('addOrUpdateTx should add weight to a tx', async () => { @@ -1303,7 +1303,7 @@ test('getWalletSortedValueUtxos', async () => { index: 0, tokenId, address: addr1, - value: 0, + value: 0n, authorities: 0b01, timelock: null, heightlock: null, @@ -1316,7 +1316,7 @@ test('getWalletSortedValueUtxos', async () => { index: 1, tokenId, address: addr1, - value: 10, + value: 10n, authorities: 0, timelock: 10000, heightlock: null, @@ -1329,7 +1329,7 @@ test('getWalletSortedValueUtxos', async () => { index: 2, tokenId, address: 'otherAddr', - value: 10, + value: 10n, authorities: 0, timelock: null, heightlock: null, @@ -1342,7 +1342,7 @@ test('getWalletSortedValueUtxos', async () => { index: 3, tokenId: 'tokenId2', address: addr1, - value: 5, + value: 5n, authorities: 0, timelock: null, heightlock: null, @@ -1355,7 +1355,7 @@ test('getWalletSortedValueUtxos', async () => { index: 4, tokenId, address: addr1, - value: 4, + value: 4n, authorities: 0, timelock: null, heightlock: null, @@ -1367,7 +1367,7 @@ test('getWalletSortedValueUtxos', async () => { index: 5, tokenId, address: addr2, - value: 1, + value: 1n, authorities: 0, timelock: null, heightlock: null, @@ -1379,7 +1379,7 @@ test('getWalletSortedValueUtxos', async () => { index: 6, tokenId, address: addr1, - value: 7, + value: 7n, authorities: 0, timelock: null, heightlock: null, @@ -1391,13 +1391,13 @@ test('getWalletSortedValueUtxos', async () => { 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, + txId, index: 6, tokenId, address: addr1, value: 7n, 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, + txId, index: 4, tokenId, address: addr1, value: 4n, 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, + txId, index: 5, tokenId, address: addr2, value: 1n, authorities: 0, timelock: null, heightlock: null, locked: false, }); }); @@ -1436,7 +1436,7 @@ test('markUtxosWithProposalId and getTxProposalInputs', async () => { index: 0, tokenId, address, - value: 5, + value: 5n, authorities: 0, timelock: null, heightlock: null, @@ -1449,7 +1449,7 @@ test('markUtxosWithProposalId and getTxProposalInputs', async () => { index: 1, tokenId, address, - value: 15, + value: 15n, authorities: 0, timelock: null, heightlock: null, @@ -1462,7 +1462,7 @@ test('markUtxosWithProposalId and getTxProposalInputs', async () => { index: 2, tokenId, address, - value: 25, + value: 25n, authorities: 0, timelock: null, heightlock: null, @@ -1542,7 +1542,7 @@ test('createTxProposal, updateTxProposal, getTxProposal, countUnsentTxProposals, index: 0, tokenId: '00', address: 'address1', - value: 5, + value: 5n, authorities: 0, timelock: 0, heightlock: 0, @@ -1555,7 +1555,7 @@ test('createTxProposal, updateTxProposal, getTxProposal, countUnsentTxProposals, index: 0, tokenId: '00', address: 'address1', - value: 5, + value: 5n, authorities: 0, timelock: 0, heightlock: 0, @@ -1568,7 +1568,7 @@ test('createTxProposal, updateTxProposal, getTxProposal, countUnsentTxProposals, index: 0, tokenId: '00', address: 'address1', - value: 5, + value: 5n, authorities: 0, timelock: 0, heightlock: 0, @@ -1614,6 +1614,7 @@ test('updateVersionData', async () => { const mockData: FullNodeApiVersionResponse = { version: '0.38.0', network: 'mainnet', + nano_contracts_enabled: true, min_weight: 14, min_tx_weight: 14, min_tx_weight_coefficient: 1.6, @@ -1655,6 +1656,7 @@ test('getVersionData', async () => { const mockData: FullNodeApiVersionResponse = { version: '0.38.0', network: 'mainnet', + nano_contracts_enabled: true, min_weight: 14, min_tx_weight: 14, min_tx_weight_coefficient: 1.6, @@ -1691,13 +1693,13 @@ test('fetchAddressTxHistorySum', async () => { 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 }, + { address: addr1, txId: txId1, tokenId: token1, balance: 10n, timestamp: timestamp1 }, + { address: addr1, txId: txId2, tokenId: token1, balance: 20n, timestamp: timestamp2 }, + { address: addr1, txId: txId3, tokenId: token1, balance: 30n, 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 }, + { address: addr2, txId: txId1, tokenId: token2, balance: 20n, timestamp: timestamp1 }, + { address: addr2, txId: txId2, tokenId: token2, balance: 20n, timestamp: timestamp2 }, + { address: addr2, txId: txId3, tokenId: token2, balance: 10n, timestamp: timestamp2 }, // total: 50 ]; @@ -1705,8 +1707,8 @@ test('fetchAddressTxHistorySum', async () => { const history = await fetchAddressTxHistorySum(mysql, [addr1, addr2]); - expect(history[0].balance).toStrictEqual(60); - expect(history[1].balance).toStrictEqual(50); + expect(history[0].balance).toStrictEqual(60n); + expect(history[1].balance).toStrictEqual(50n); }); test('fetchAddressBalance', async () => { @@ -1735,30 +1737,30 @@ test('fetchAddressBalance', async () => { 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[0].unlockedBalance).toStrictEqual(2n); + expect(addressBalances[0].lockedBalance).toStrictEqual(0n); 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[1].unlockedBalance).toStrictEqual(1n); + expect(addressBalances[1].lockedBalance).toStrictEqual(4n); 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[2].unlockedBalance).toStrictEqual(5n); + expect(addressBalances[2].lockedBalance).toStrictEqual(2n); 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[3].unlockedBalance).toStrictEqual(0n); + expect(addressBalances[3].lockedBalance).toStrictEqual(2n); 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[4].unlockedBalance).toStrictEqual(0n); + expect(addressBalances[4].lockedBalance).toStrictEqual(1n); expect(addressBalances[5].address).toStrictEqual('addr3'); expect(addressBalances[5].tokenId).toStrictEqual('token2'); - expect(addressBalances[5].unlockedBalance).toStrictEqual(10); - expect(addressBalances[5].lockedBalance).toStrictEqual(1); + expect(addressBalances[5].unlockedBalance).toStrictEqual(10n); + expect(addressBalances[5].lockedBalance).toStrictEqual(1n); }); test('addTx, fetchTx, getTransactionsById and markTxsAsVoided', async () => { @@ -1821,14 +1823,14 @@ test('checkTxWasVoided', async () => { address: address1, txId: tx1, tokenId: '00', - balance: 0, + balance: 0n, timestamp: 1, voided: true, }, { address: address2, txId: tx2, tokenId: '00', - balance: 0, + balance: 0n, timestamp: 1, voided: false, }]); @@ -1850,7 +1852,7 @@ test('cleanupVoidedTx', async () => { index: 0, tokenId, address: addr1, - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -1863,7 +1865,7 @@ test('cleanupVoidedTx', async () => { address: addr1, txId, tokenId, - balance: 0, + balance: 0n, timestamp: 1, voided: true, }]); @@ -1897,7 +1899,7 @@ test('cleanupVoidedTx', async () => { index: 0, tokenId, address: addr1, - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -1914,7 +1916,7 @@ test('cleanupVoidedTx', async () => { timestamp: 1, address: addr1, tokenId, - balance: 0, + balance: 0n, voided: false, }]); @@ -1951,34 +1953,34 @@ test('rebuildAddressBalancesFromUtxos', async () => { 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: 5n, address: addr1, token: token1, locked: false, spentBy: null }, + { value: 15n, address: addr1, token: token1, locked: false, spentBy: null }, + { value: 75n, address: addr2, token: token1, heightlock: 70, locked: true, spentBy: null }, + { value: 150n, address: addr2, token: token1, heightlock: 70, locked: true, spentBy: null }, + { value: 35n, address: addr2, token: token1, locked: false, spentBy: null }, - { value: 25, address: addr2, token: token2, timelock: 500, locked: true, spentBy: null }, + { value: 25n, address: addr2, token: token2, timelock: 500, locked: true, spentBy: null }, // authority utxo - { value: 0b11, address: addr1, token: token1, locked: false, tokenData: 129, spentBy: null }, + { value: 0b11n, 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 }, + { value: 80n, address: addr2, token: token1, heightlock: 70, locked: false, spentBy: null }, + { value: 90n, 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 }, + { value: 5n, address: addr2, token: token1, heightlock: 70, locked: false, spentBy: null }, + { value: 10n, 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 }, + { value: 20n, address: addr1, token: token1, heightlock: 70, locked: false, spentBy: null }, + { value: 1n, address: addr1, token: token1, heightlock: 70, locked: false, spentBy: null }, ]; const mapUtxoListToOutput = (utxoList: any[]) => utxoList.map((utxo, index) => createOutput( @@ -2012,13 +2014,13 @@ test('rebuildAddressBalancesFromUtxos', async () => { 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: addr1, txId, tokenId: token1, balance: 20n, timestamp: timestamp1 }, + { address: addr1, txId: txId4, tokenId: token1, balance: 21n, 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 }, + { address: addr2, txId, tokenId: token1, balance: 260n, timestamp: timestamp1 }, + { address: addr2, txId, tokenId: token2, balance: 25n, timestamp: timestamp1 }, + { address: addr2, txId: txId2, tokenId: token1, balance: 80n, timestamp: timestamp1 }, + { address: addr2, txId: txId3, tokenId: token1, balance: 15n, timestamp: timestamp1, voided: true }, ]; await addToAddressTxHistoryTable(mysql, txHistory); @@ -2041,19 +2043,19 @@ test('rebuildAddressBalancesFromUtxos', async () => { const addressBalances = await fetchAddressBalance(mysql, [addr1, addr2]); - expect(addressBalances[0].unlockedBalance).toStrictEqual(41); + expect(addressBalances[0].unlockedBalance).toStrictEqual(41n); 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].unlockedBalance).toStrictEqual(220n); + expect(addressBalances[1].lockedBalance).toStrictEqual(225n); 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].lockedBalance).toStrictEqual(25n); expect(addressBalances[2].address).toStrictEqual(addr2); expect(addressBalances[2].transactions).toStrictEqual(1); expect(addressBalances[2].tokenId).toStrictEqual('token2'); @@ -2080,13 +2082,13 @@ test('markAddressTxHistoryAsVoided', async () => { 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 }, + { address: addr1, txId: txId1, tokenId: token1, balance: 10n, timestamp: timestamp1 }, + { address: addr1, txId: txId2, tokenId: token1, balance: 20n, timestamp: timestamp2 }, + { address: addr1, txId: txId3, tokenId: token1, balance: 30n, 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 }, + { address: addr2, txId: txId1, tokenId: token2, balance: 20n, timestamp: timestamp1 }, + { address: addr2, txId: txId2, tokenId: token2, balance: 20n, timestamp: timestamp2 }, + { address: addr2, txId: txId3, tokenId: token2, balance: 10n, timestamp: timestamp2 }, // total: 50 ]; @@ -2149,7 +2151,7 @@ test('filterTxOutputs', async () => { index: 0, tokenId: '00', address: addr1, - value: 6000, + value: 6000n, authorities: 0, timelock: null, heightlock: null, @@ -2160,7 +2162,7 @@ test('filterTxOutputs', async () => { index: 0, tokenId, address: addr1, - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -2171,7 +2173,7 @@ test('filterTxOutputs', async () => { index: 0, tokenId, address: addr1, - value: 500, + value: 500n, authorities: 0, timelock: null, heightlock: null, @@ -2182,7 +2184,7 @@ test('filterTxOutputs', async () => { index: 1, tokenId, address: addr1, - value: 1000, + value: 1000n, authorities: 0, timelock: null, heightlock: null, @@ -2194,7 +2196,7 @@ test('filterTxOutputs', async () => { index: 2, tokenId, address: addr2, - value: 1500, + value: 1500n, authorities: 0, timelock: null, heightlock: null, @@ -2206,7 +2208,7 @@ test('filterTxOutputs', async () => { index: 3, tokenId, address: addr2, - value: 0, + value: 0n, authorities: 0b01, timelock: null, heightlock: null, @@ -2218,7 +2220,7 @@ test('filterTxOutputs', async () => { index: 4, tokenId, address: addr2, - value: 0, + value: 0n, authorities: 0b01, timelock: null, heightlock: null, @@ -2243,14 +2245,14 @@ test('filterTxOutputs', async () => { expect(utxos).toHaveLength(2); // filter all utxos between 100 and 1500 - utxos = await filterTxOutputs(mysql, { addresses: [addr1, addr2], tokenId, biggerThan: 100, smallerThan: 1500 }); + utxos = await filterTxOutputs(mysql, { addresses: [addr1, addr2], tokenId, biggerThan: 100n, smallerThan: 1500n }); expect(utxos).toHaveLength(2); expect(utxos[0]).toStrictEqual({ txId: txId2, index: 1, tokenId, address: addr1, - value: 1000, + value: 1000n, authorities: 0, timelock: null, heightlock: null, @@ -2264,7 +2266,7 @@ test('filterTxOutputs', async () => { index: 0, tokenId, address: addr1, - value: 500, + value: 500n, authorities: 0, timelock: null, heightlock: null, @@ -2282,7 +2284,7 @@ test('filterTxOutputs', async () => { index: 2, tokenId, address: addr2, - value: 1500, + value: 1500n, authorities: 0, timelock: null, heightlock: null, @@ -2296,7 +2298,7 @@ test('filterTxOutputs', async () => { index: 1, tokenId, address: addr1, - value: 1000, + value: 1000n, authorities: 0, timelock: null, heightlock: null, @@ -2307,7 +2309,7 @@ test('filterTxOutputs', async () => { }); // 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 }); + utxos = await filterTxOutputs(mysql, { addresses: [addr1, addr2], biggerThan: 0n, smallerThan: 3n, authority: 1, tokenId, maxOutputs: 1 }); expect(utxos).toHaveLength(1); }); @@ -2333,7 +2335,7 @@ test('beginTransaction, commitTransaction, rollbackTransaction', async () => { index: 0, tokenId, address: addr1, - value: 0, + value: 0n, authorities: 0b01, timelock: null, heightlock: null, @@ -2344,7 +2346,7 @@ test('beginTransaction, commitTransaction, rollbackTransaction', async () => { index: 1, tokenId, address: addr1, - value: 10, + value: 10n, authorities: 0, timelock: 10000, heightlock: null, @@ -2355,7 +2357,7 @@ test('beginTransaction, commitTransaction, rollbackTransaction', async () => { index: 2, tokenId, address: 'otherAddr', - value: 10, + value: 10n, authorities: 0, timelock: null, heightlock: null, @@ -2365,9 +2367,9 @@ test('beginTransaction, commitTransaction, rollbackTransaction', async () => { 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 expect(checkUtxoTable(mysql, 3, txId, 0, tokenId, addr1, 0n, 0b01, null, null, false)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 3, txId, 1, tokenId, addr1, 10n, 0, 10000, null, true)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 3, txId, 2, tokenId, 'otherAddr', 10n, 0, null, null, false)).resolves.toBe(true); await beginTransaction(mysql); @@ -2376,7 +2378,7 @@ test('beginTransaction, commitTransaction, rollbackTransaction', async () => { index: 3, tokenId: 'tokenId2', address: addr1, - value: 5, + value: 5n, authorities: 0, timelock: null, heightlock: null, @@ -2387,7 +2389,7 @@ test('beginTransaction, commitTransaction, rollbackTransaction', async () => { index: 4, tokenId, address: addr1, - value: 4, + value: 4n, authorities: 0, timelock: null, heightlock: null, @@ -2398,7 +2400,7 @@ test('beginTransaction, commitTransaction, rollbackTransaction', async () => { index: 5, tokenId, address: addr2, - value: 1, + value: 1n, authorities: 0, timelock: null, heightlock: null, @@ -2409,7 +2411,7 @@ test('beginTransaction, commitTransaction, rollbackTransaction', async () => { index: 6, tokenId, address: addr1, - value: 7, + value: 7n, authorities: 0, timelock: null, heightlock: null, @@ -2420,7 +2422,7 @@ test('beginTransaction, commitTransaction, rollbackTransaction', async () => { 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); + await expect(checkUtxoTable(mysql, 3, txId, 2, tokenId, 'otherAddr', 10n, 0, null, null, false)).resolves.toBe(true); }); test('getMinersList', async () => { @@ -2458,13 +2460,13 @@ test('getTotalSupply', async () => { 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 }, + { value: 500n, address: 'HDeadDeadDeadDeadDeadDeadDeagTPgmn', tokenId: '00', locked: false }, + { value: 5n, address: 'address1', tokenId: '00', locked: false }, + { value: 15n, address: 'address1', tokenId: '00', locked: false }, + { value: 25n, address: 'address2', tokenId: 'token2', timelock: 500, locked: true }, + { value: 35n, address: 'address2', tokenId: 'token1', locked: false }, // authority utxo - { value: 0b11, address: 'address1', tokenId: 'token1', locked: false, tokenData: 129 }, + { value: 0b11n, address: 'address1', tokenId: 'token1', locked: false, tokenData: 129 }, ]; // add to utxo table @@ -2480,9 +2482,9 @@ test('getTotalSupply', async () => { 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); + expect(await getTotalSupply(mysql, '00')).toStrictEqual(20n); + expect(await getTotalSupply(mysql, 'token2')).toStrictEqual(25n); + expect(await getTotalSupply(mysql, 'token1')).toStrictEqual(35n); const mysqlQuerySpy = jest.spyOn(mysql, 'query'); mysqlQuerySpy.mockImplementationOnce(() => Promise.resolve({ length: null })); @@ -2502,12 +2504,12 @@ test('getExpiredTimelocksUtxos', async () => { 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 }, + { value: 5n, address: 'address1', tokenId: 'token1', locked: true }, + { value: 15n, address: 'address1', tokenId: 'token1', locked: true }, + { value: 25n, address: 'address2', tokenId: 'token2', timelock: 100, locked: true }, + { value: 35n, address: 'address2', tokenId: 'token1', timelock: 200, locked: true }, // authority utxo - { value: 0b11, address: 'address1', tokenId: 'token1', timelock: 300, locked: true, tokenData: 129 }, + { value: 0b11n, address: 'address1', tokenId: 'token1', timelock: 300, locked: true, tokenData: 129 }, ]; // empty list should be fine @@ -2538,18 +2540,18 @@ test('getExpiredTimelocksUtxos', async () => { 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); + expect(unlockedUtxos3[2].authorities).toStrictEqual(Number(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 }, + { address: 'address1', txId: 'txId1', tokenId: 'token1', balance: -5n, timestamp: 1000 }, + { address: 'address1', txId: 'txId2', tokenId: 'token1', balance: 5n, timestamp: 1000 }, + { address: 'address1', txId: 'txId3', tokenId: 'token1', balance: 10n, timestamp: 1000 }, + { address: 'address2', txId: 'txId4', tokenId: 'token2', balance: -5n, timestamp: 1000 }, + { address: 'address2', txId: 'txId5', tokenId: 'token2', balance: 50n, timestamp: 1000 }, ]); expect(await getTotalTransactions(mysql, 'token1')).toStrictEqual(3); @@ -2581,7 +2583,7 @@ test('getAvailableAuthorities', async () => { index: 0, tokenId, address: addr1, - value: 0, + value: 0n, authorities: 0b01, timelock: null, heightlock: null, @@ -2592,7 +2594,7 @@ test('getAvailableAuthorities', async () => { index: 1, tokenId, address: addr1, - value: 0, + value: 0n, authorities: 0b11, timelock: 1000, heightlock: null, @@ -2603,7 +2605,7 @@ test('getAvailableAuthorities', async () => { index: 2, tokenId, address: addr1, - value: 0, + value: 0n, authorities: 0b10, timelock: null, heightlock: null, @@ -2614,7 +2616,7 @@ test('getAvailableAuthorities', async () => { index: 3, tokenId: tokenId2, address: addr2, - value: 0, + value: 0n, authorities: 0b01, timelock: null, heightlock: null, @@ -2637,8 +2639,8 @@ test('getUtxo, getAuthorityUtxo', async () => { index: 0, tokenId, address: addr1, - value: 0, - authorities: constants.TOKEN_MINT_MASK, + value: 0n, + authorities: Number(constants.TOKEN_MINT_MASK), timelock: 10000, heightlock: null, locked: true, @@ -2649,8 +2651,8 @@ test('getUtxo, getAuthorityUtxo', async () => { index: 1, tokenId, address: addr1, - value: 0, - authorities: constants.TOKEN_MELT_MASK, + value: 0n, + authorities: Number(constants.TOKEN_MELT_MASK), timelock: 10000, heightlock: null, locked: true, @@ -2663,8 +2665,8 @@ test('getUtxo, getAuthorityUtxo', async () => { index: 0, tokenId, address: addr1, - value: 0, - authorities: constants.TOKEN_MINT_MASK, + value: 0n, + authorities: Number(constants.TOKEN_MINT_MASK), timelock: 10000, heightlock: null, locked: true, @@ -2673,16 +2675,16 @@ test('getUtxo, getAuthorityUtxo', async () => { spentBy: null, }); - const mintUtxo = await getAuthorityUtxo(mysql, tokenId, constants.TOKEN_MINT_MASK); - const meltUtxo = await getAuthorityUtxo(mysql, tokenId, constants.TOKEN_MELT_MASK); + const mintUtxo = await getAuthorityUtxo(mysql, tokenId, Number(constants.TOKEN_MINT_MASK)); + const meltUtxo = await getAuthorityUtxo(mysql, tokenId, Number(constants.TOKEN_MELT_MASK)); expect(mintUtxo).toStrictEqual({ txId: 'txId', index: 0, tokenId, address: addr1, - value: 0, - authorities: constants.TOKEN_MINT_MASK, + value: 0n, + authorities: Number(constants.TOKEN_MINT_MASK), timelock: 10000, heightlock: null, locked: true, @@ -2695,8 +2697,8 @@ test('getUtxo, getAuthorityUtxo', async () => { index: 1, tokenId, address: addr1, - value: 0, - authorities: constants.TOKEN_MELT_MASK, + value: 0n, + authorities: Number(constants.TOKEN_MELT_MASK), timelock: 10000, heightlock: null, locked: true, @@ -2722,14 +2724,14 @@ test('getAffectedAddressTxCountFromTxList', async () => { 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 }, + { address: addr1, txId: txId1, tokenId: token1, balance: 10n, timestamp: timestamp1, voided: true }, + { address: addr1, txId: txId1, tokenId: token2, balance: 7n, timestamp: timestamp1, voided: true }, + { address: addr2, txId: txId1, tokenId: token2, balance: 5n, timestamp: timestamp1, voided: true }, + { address: addr3, txId: txId1, tokenId: token1, balance: 3n, timestamp: timestamp1, voided: true }, + { address: addr1, txId: txId2, tokenId: token1, balance: -1n, timestamp: timestamp2, voided: false }, + { address: addr1, txId: txId2, tokenId: token3, balance: 3n, timestamp: timestamp2, voided: false }, + { address: addr2, txId: txId3, tokenId: token2, balance: -5n, timestamp: timestamp2, voided: true }, + { address: addr3, txId: txId3, tokenId: token1, balance: 3n, timestamp: timestamp2, voided: true }, ]; await addToAddressTxHistoryTable(mysql, entries); @@ -3135,8 +3137,8 @@ describe('getTransactionById', () => { { 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 }, + { address: addr1, txId: txId1, tokenId: token1.id, balance: 10n, timestamp: timestamp1 }, + { address: addr1, txId: txId1, tokenId: token2.id, balance: 7n, timestamp: timestamp1 }, ]; await addToAddressTxHistoryTable(mysql, entries); await initWalletTxHistory(mysql, walletId1, [addr1]); @@ -3147,7 +3149,7 @@ describe('getTransactionById', () => { const [secondToken] = txTokens.filter((eachToken) => eachToken.tokenId === 'token2'); expect(firstToken).toStrictEqual({ - balance: 10, + balance: 10n, timestamp: timestamp1, tokenId: token1.id, tokenName: token1.name, @@ -3158,7 +3160,7 @@ describe('getTransactionById', () => { weight: weight1, }); expect(secondToken).toStrictEqual({ - balance: 7, + balance: 7n, timestamp: timestamp1, tokenId: token2.id, tokenName: token2.name, @@ -3567,7 +3569,7 @@ describe('Clear unsent txProposals utxos', () => { index: 0, tokenId: '00', address: 'address1', - value: 5, + value: 5n, authorities: 0, timelock: 0, heightlock: 0, @@ -3580,7 +3582,7 @@ describe('Clear unsent txProposals utxos', () => { index: 0, tokenId: '00', address: 'address1', - value: 5, + value: 5n, authorities: 0, timelock: 0, heightlock: 0, @@ -3593,7 +3595,7 @@ describe('Clear unsent txProposals utxos', () => { index: 0, tokenId: '00', address: 'address1', - value: 5, + value: 5n, authorities: 0, timelock: 0, heightlock: 0, @@ -3662,12 +3664,14 @@ describe('getAddressByIndex', () => { const walletId = 'walletId'; const index = 0; const transactions = 0; + const seqnum = 3; await addToAddressTable(mysql, [{ address, index, walletId, transactions, + seqnum, }]); await expect(getAddressAtIndex(mysql, walletId, index)) @@ -3676,6 +3680,7 @@ describe('getAddressByIndex', () => { address, index, transactions, + seqnum, }); }); diff --git a/packages/wallet-service/tests/fixtures/aws/config b/packages/wallet-service/tests/fixtures/aws/config new file mode 100644 index 00000000..e69de29b diff --git a/packages/wallet-service/tests/fixtures/aws/credentials b/packages/wallet-service/tests/fixtures/aws/credentials new file mode 100644 index 00000000..79b6da0c --- /dev/null +++ b/packages/wallet-service/tests/fixtures/aws/credentials @@ -0,0 +1,8 @@ +# AWS Credentials for Local Development +# This file provides mock AWS credentials to prevent the serverless framework +# from attempting to fetch credentials from EC2 metadata service + +[default] +aws_access_key_id = fake-access-key-id +aws_secret_access_key = fake-secret-access-key +region = us-east-1 diff --git a/packages/wallet-service/tests/integration.test.ts b/packages/wallet-service/tests/integration.test.ts index e56d2d30..527a4b3d 100644 --- a/packages/wallet-service/tests/integration.test.ts +++ b/packages/wallet-service/tests/integration.test.ts @@ -1,9 +1,8 @@ import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; import { initFirebaseAdminMock } from '@tests/utils/firebase-admin.mock'; import eventTemplate from '@events/eventTemplate.json'; -import { loadWallet, loadWalletFailed } from '@src/api/wallet'; -import { createWallet, getMinersList } from '@src/db'; -import * as txProcessor from '@src/txProcessor'; +import { loadWalletFailed } from '@src/api/wallet'; +import { createWallet } from '@src/db'; import { WalletStatus } from '@src/types'; import { Transaction, TxInput, Severity } from '@wallet-service/common/src/types'; import { closeDbConnection, getDbConnection, getUnixTimestamp, getWalletId } from '@src/utils'; @@ -13,23 +12,17 @@ import { XPUBKEY, AUTH_XPUBKEY, cleanDatabase, - checkAddressTable, - checkAddressBalanceTable, - checkAddressTxHistoryTable, - checkUtxoTable, - checkWalletBalanceTable, checkWalletTable, - checkWalletTxHistoryTable, createOutput, createInput, - addToUtxoTable, + stopWalletLibOpenHandles, } from '@tests/utils'; import { SNSEvent } from 'aws-lambda'; const mysql = getDbConnection(); initFirebaseAdminMock(); -const blockReward = 6400; +const blockReward = 6400n; const htrToken = '00'; const walletId = getWalletId(XPUBKEY); const now = getUnixTimestamp(); @@ -93,8 +86,8 @@ 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]), + createOutput(0, blockReward - 5000n, ADDRESSES[1]), + createOutput(1, 5000n, 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 @@ -106,11 +99,11 @@ const txId4 = 'txId4'; tx2.tx_id = txId4; tx2.timestamp += 20; tx2.inputs = [ - createInput(5000, ADDRESSES[2], txId2, 1), + createInput(5000n, ADDRESSES[2], txId2, 1), ]; tx2.outputs = [ - createOutput(0, 1000, ADDRESSES[6], '00', timelock), // belongs to this wallet - createOutput(1, 4000, 'HCuWC2qgNP47BtWtsTM48PokKitVdR6pch'), // other wallet + createOutput(0, 1000n, ADDRESSES[6], '00', timelock), // belongs to this wallet + createOutput(1, 4000n, 'HCuWC2qgNP47BtWtsTM48PokKitVdR6pch'), // other wallet ]; // tx2Inputs on the format addToUtxoTable expects @@ -138,6 +131,7 @@ beforeAll(async () => { process.env.BLOCK_REWARD_LOCK = '1'; const actualUtils = jest.requireActual('@src/utils'); + await stopWalletLibOpenHandles(); jest.mock('@src/utils', () => { return { ...actualUtils, diff --git a/packages/wallet-service/tests/jestSetup.ts b/packages/wallet-service/tests/jestSetup.ts index 7a477f44..a0aa3ecf 100644 --- a/packages/wallet-service/tests/jestSetup.ts +++ b/packages/wallet-service/tests/jestSetup.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { config } from 'dotenv'; +import { stopGLLBackgroundTask } from '@hathor/wallet-lib'; Object.defineProperty(global, '_bitcore', { get() { return undefined; }, set() {} }); +stopGLLBackgroundTask(); config(); diff --git a/packages/wallet-service/tests/mempool.test.ts b/packages/wallet-service/tests/mempool.test.ts index 311ac682..8a19e786 100644 --- a/packages/wallet-service/tests/mempool.test.ts +++ b/packages/wallet-service/tests/mempool.test.ts @@ -41,7 +41,7 @@ test('onHandleOldVoidedTxs', async () => { index: 0, tokenId: '00', address: ADDRESSES[0], - value: 50, + value: 50n, authorities: 0, timelock: null, heightlock: null, @@ -52,7 +52,7 @@ test('onHandleOldVoidedTxs', async () => { index: 0, tokenId: '00', address: ADDRESSES[1], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -63,7 +63,7 @@ test('onHandleOldVoidedTxs', async () => { index: 0, tokenId: '00', address: ADDRESSES[2], - value: 150, + value: 150n, authorities: 0, timelock: null, heightlock: null, @@ -74,7 +74,7 @@ test('onHandleOldVoidedTxs', async () => { index: 1, tokenId: '00', address: ADDRESSES[3], - value: 200, + value: 200n, authorities: 0, timelock: null, heightlock: null, @@ -83,10 +83,10 @@ test('onHandleOldVoidedTxs', async () => { }]; 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 }, + { address: ADDRESSES[0], txId: TX_IDS[0], tokenId: '00', balance: 50n, timestamp: 10 }, + { address: ADDRESSES[1], txId: TX_IDS[1], tokenId: '00', balance: 100n, timestamp: 10 }, + { address: ADDRESSES[2], txId: TX_IDS[2], tokenId: '00', balance: 150n, timestamp: 10 }, + { address: ADDRESSES[3], txId: TX_IDS[2], tokenId: '00', balance: 200n, timestamp: 10 }, ]; const addressEntries = [ @@ -111,7 +111,7 @@ test('onHandleOldVoidedTxs', async () => { await onHandleOldVoidedTxs(); - await expect(checkUtxoTable(mysql, 4, TX_IDS[0], 0, '00', ADDRESSES[0], 50, 0, null, null, false, null, true)).resolves.toBe(true); + await expect(checkUtxoTable(mysql, 4, TX_IDS[0], 0, '00', ADDRESSES[0], 50n, 0, null, null, false, null, true)).resolves.toBe(true); }); test('onHandleOldVoidedTxs should try to confirm the block by fetching the first_block', async () => { @@ -128,7 +128,7 @@ test('onHandleOldVoidedTxs should try to confirm the block by fetching the first index: 0, tokenId: '00', address: ADDRESSES[0], - value: 50, + value: 50n, authorities: 0, timelock: null, heightlock: null, diff --git a/packages/wallet-service/tests/nodeConfig.test.ts b/packages/wallet-service/tests/nodeConfig.test.ts index 92352adb..064c3d8c 100644 --- a/packages/wallet-service/tests/nodeConfig.test.ts +++ b/packages/wallet-service/tests/nodeConfig.test.ts @@ -10,8 +10,9 @@ import { convertApiVersionData, getRawFullnodeData } from '@src/nodeConfig'; const mysql = getDbConnection(); const VERSION_DATA: FullNodeApiVersionResponse = { - version: '0.63.1', + version: '0.65.2-alpha.1', network: 'mainnet', + nano_contracts_enabled: true, min_weight: 14, min_tx_weight: 14, min_tx_weight_coefficient: 1.6, @@ -63,6 +64,7 @@ test('convertApiVersionData', async () => { expect(convertApiVersionData(OLD_VERSION_DATA)).toStrictEqual({ version: OLD_VERSION_DATA.version, network: OLD_VERSION_DATA.network, + nanoContractsEnabled: false, minWeight: OLD_VERSION_DATA.min_weight, minTxWeight: OLD_VERSION_DATA.min_tx_weight, minTxWeightCoefficient: OLD_VERSION_DATA.min_tx_weight_coefficient, @@ -79,6 +81,7 @@ test('convertApiVersionData', async () => { expect(convertApiVersionData(VERSION_DATA)).toStrictEqual({ version: VERSION_DATA.version, network: VERSION_DATA.network, + nanoContractsEnabled: VERSION_DATA.nano_contracts_enabled, minWeight: VERSION_DATA.min_weight, minTxWeight: VERSION_DATA.min_tx_weight, minTxWeightCoefficient: VERSION_DATA.min_tx_weight_coefficient, @@ -92,3 +95,53 @@ test('convertApiVersionData', async () => { nativeTokenSymbol: VERSION_DATA.native_token.symbol, }); }); + +test('convertApiVersionData handles nano_contracts_enabled correctly', async () => { + // Test with nano_contracts_enabled = false + const versionWithNanoFalse: FullNodeApiVersionResponse = { + ...OLD_VERSION_DATA, + nano_contracts_enabled: false, + }; + expect(convertApiVersionData(versionWithNanoFalse).nanoContractsEnabled).toBe(false); + + // Test with nano_contracts_enabled = true + const versionWithNanoTrue: FullNodeApiVersionResponse = { + ...OLD_VERSION_DATA, + nano_contracts_enabled: true, + }; + expect(convertApiVersionData(versionWithNanoTrue).nanoContractsEnabled).toBe(true); + + // Test with nano_contracts_enabled = 'disabled' (should be false) + const versionWithDisabled: FullNodeApiVersionResponse = { + ...OLD_VERSION_DATA, + nano_contracts_enabled: 'disabled', + }; + expect(convertApiVersionData(versionWithDisabled).nanoContractsEnabled).toBe(false); + + // Test with nano_contracts_enabled = 'enabled' (should be true) + const versionWithEnabled: FullNodeApiVersionResponse = { + ...OLD_VERSION_DATA, + nano_contracts_enabled: 'enabled', + }; + expect(convertApiVersionData(versionWithEnabled).nanoContractsEnabled).toBe(true); + + // Test with nano_contracts_enabled = 'feature_activation' (should be true) + const versionWithFeatureActivation: FullNodeApiVersionResponse = { + ...OLD_VERSION_DATA, + nano_contracts_enabled: 'feature_activation', + }; + expect(convertApiVersionData(versionWithFeatureActivation).nanoContractsEnabled).toBe(true); + + // Test with nano_contracts_enabled = undefined (should default to false) + const versionWithNanoUndefined: FullNodeApiVersionResponse = { + ...OLD_VERSION_DATA, + nano_contracts_enabled: undefined, + }; + expect(convertApiVersionData(versionWithNanoUndefined).nanoContractsEnabled).toBe(false); + + // Test without nano_contracts_enabled field (should default to false) + const versionWithoutNano: FullNodeApiVersionResponse = { + ...OLD_VERSION_DATA, + }; + expect(convertApiVersionData(versionWithoutNano).nanoContractsEnabled).toBe(false); +}); diff --git a/packages/wallet-service/tests/pushSendNotificationToDevice.test.ts b/packages/wallet-service/tests/pushSendNotificationToDevice.test.ts index 7848f429..afd9b946 100644 --- a/packages/wallet-service/tests/pushSendNotificationToDevice.test.ts +++ b/packages/wallet-service/tests/pushSendNotificationToDevice.test.ts @@ -19,6 +19,7 @@ import { makeGatewayEventWithAuthorizer, cleanDatabase, checkPushDevicesTable, + stopWalletLibOpenHandles, } from '@tests/utils'; import { APIGatewayProxyResult, Context } from 'aws-lambda'; import { Severity } from '@wallet-service/common/src/types'; @@ -253,6 +254,7 @@ describe('alert', () => { // allow android and desktop, while test for ios provider process.env.PUSH_ALLOWED_PROVIDERS = 'android,desktop'; await import('@src/api/pushSendNotificationToDevice'); + await stopWalletLibOpenHandles(); await addToWalletTable(mysql, [{ id: 'my-wallet', diff --git a/packages/wallet-service/tests/txById.test.ts b/packages/wallet-service/tests/txById.test.ts index 011de406..baa9d4f7 100644 --- a/packages/wallet-service/tests/txById.test.ts +++ b/packages/wallet-service/tests/txById.test.ts @@ -44,8 +44,8 @@ test('get a transaction given its ID', async () => { { 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 }, + { address: addr1, txId: txId1, tokenId: token1.id, balance: 10n, timestamp: timestamp1 }, + { address: addr1, txId: txId1, tokenId: token2.id, balance: 7n, timestamp: timestamp1 }, ]; await addToAddressTxHistoryTable(mysql, entries); await initWalletTxHistory(mysql, walletId1, [addr1]); diff --git a/packages/wallet-service/tests/txOutputs.test.ts b/packages/wallet-service/tests/txOutputs.test.ts index 40936992..b0f80e47 100644 --- a/packages/wallet-service/tests/txOutputs.test.ts +++ b/packages/wallet-service/tests/txOutputs.test.ts @@ -56,7 +56,7 @@ test('filter utxos api with invalid parameters', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 50, + value: 50n, authorities: 0, timelock: null, heightlock: null, @@ -67,7 +67,7 @@ test('filter utxos api with invalid parameters', async () => { index: 0, tokenId: token1, address: ADDRESSES[1], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -78,7 +78,7 @@ test('filter utxos api with invalid parameters', async () => { index: 0, tokenId: token1, address: ADDRESSES[2], - value: 150, + value: 150n, authorities: 0, timelock: null, heightlock: null, @@ -89,7 +89,7 @@ test('filter utxos api with invalid parameters', async () => { index: 1, tokenId: token1, address: ADDRESSES[3], - value: 200, + value: 200n, authorities: 0, timelock: null, heightlock: null, @@ -186,7 +186,7 @@ test('filter tx_output api with invalid parameters', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 50, + value: 50n, authorities: 0, timelock: null, heightlock: null, @@ -197,7 +197,7 @@ test('filter tx_output api with invalid parameters', async () => { index: 0, tokenId: token1, address: ADDRESSES[1], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -208,7 +208,7 @@ test('filter tx_output api with invalid parameters', async () => { index: 0, tokenId: token1, address: ADDRESSES[2], - value: 150, + value: 150n, authorities: 0, timelock: null, heightlock: null, @@ -219,7 +219,7 @@ test('filter tx_output api with invalid parameters', async () => { index: 1, tokenId: token1, address: ADDRESSES[3], - value: 200, + value: 200n, authorities: 0, timelock: null, heightlock: null, @@ -315,7 +315,7 @@ test('get utxos with wallet id', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 50, + value: 50n, authorities: 0, timelock: null, heightlock: null, @@ -326,7 +326,7 @@ test('get utxos with wallet id', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -337,7 +337,7 @@ test('get utxos with wallet id', async () => { index: 0, tokenId: token1, address: ADDRESSES[1], - value: 150, + value: 150n, authorities: 0, timelock: null, heightlock: null, @@ -348,7 +348,7 @@ test('get utxos with wallet id', async () => { index: 1, tokenId: token1, address: ADDRESSES[0], - value: 200, + value: 200n, authorities: 0, timelock: null, heightlock: null, @@ -372,7 +372,7 @@ test('get utxos with wallet id', async () => { index: utxo.index, tokenId: utxo.tokenId, address: utxo.address, - value: utxo.value, + value: Number(utxo.value), authorities: utxo.authorities, timelock: utxo.timelock, heightlock: utxo.heightlock, @@ -421,7 +421,7 @@ test('get tx outputs with wallet id', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 50, + value: 50n, authorities: 0, timelock: null, heightlock: null, @@ -432,7 +432,7 @@ test('get tx outputs with wallet id', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -443,7 +443,7 @@ test('get tx outputs with wallet id', async () => { index: 0, tokenId: token1, address: ADDRESSES[1], - value: 150, + value: 150n, authorities: 0, timelock: null, heightlock: null, @@ -454,7 +454,7 @@ test('get tx outputs with wallet id', async () => { index: 1, tokenId: token1, address: ADDRESSES[0], - value: 200, + value: 200n, authorities: 0, timelock: null, heightlock: null, @@ -478,7 +478,7 @@ test('get tx outputs with wallet id', async () => { index: utxo.index, tokenId: utxo.tokenId, address: utxo.address, - value: utxo.value, + value: Number(utxo.value), authorities: utxo.authorities, timelock: utxo.timelock, heightlock: utxo.heightlock, @@ -527,7 +527,7 @@ test('get authority utxos', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 0, + value: 0n, authorities: 1, timelock: null, heightlock: null, @@ -538,7 +538,7 @@ test('get authority utxos', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 0, + value: 0n, authorities: 2, timelock: null, heightlock: null, @@ -549,7 +549,7 @@ test('get authority utxos', async () => { index: 0, tokenId: token1, address: ADDRESSES[1], - value: 0, + value: 0n, authorities: 1, timelock: null, heightlock: null, @@ -560,7 +560,7 @@ test('get authority utxos', async () => { index: 1, tokenId: token1, address: ADDRESSES[0], - value: 0, + value: 0n, authorities: 1, timelock: null, heightlock: null, @@ -571,7 +571,7 @@ test('get authority utxos', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 150, + value: 150n, authorities: 0, timelock: null, heightlock: null, @@ -586,7 +586,7 @@ test('get authority utxos', async () => { index: utxo.index, tokenId: utxo.tokenId, address: utxo.address, - value: utxo.value, + value: Number(utxo.value), authorities: utxo.authorities, timelock: utxo.timelock, heightlock: utxo.heightlock, @@ -656,7 +656,7 @@ test('get a specific utxo', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 50, + value: 50n, authorities: 0, timelock: null, heightlock: null, @@ -667,7 +667,7 @@ test('get a specific utxo', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -678,7 +678,7 @@ test('get a specific utxo', async () => { index: 0, tokenId: token1, address: ADDRESSES[1], - value: 150, + value: 150n, authorities: 0, timelock: null, heightlock: null, @@ -689,7 +689,7 @@ test('get a specific utxo', async () => { index: 1, tokenId: token1, address: ADDRESSES[0], - value: 200, + value: 200n, authorities: 0, timelock: null, heightlock: null, @@ -712,7 +712,7 @@ test('get a specific utxo', async () => { index: utxo.index, tokenId: utxo.tokenId, address: utxo.address, - value: utxo.value, + value: Number(utxo.value), authorities: utxo.authorities, timelock: utxo.timelock, heightlock: utxo.heightlock, @@ -760,7 +760,7 @@ test('get utxos from addresses that are not my own should fail with ApiError.ADD index: 0, tokenId: token1, address: ADDRESSES[0], - value: 50, + value: 50n, authorities: 0, timelock: null, heightlock: null, @@ -771,7 +771,7 @@ test('get utxos from addresses that are not my own should fail with ApiError.ADD index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -782,7 +782,7 @@ test('get utxos from addresses that are not my own should fail with ApiError.ADD index: 0, tokenId: token1, address: ADDRESSES[1], - value: 150, + value: 150n, authorities: 0, timelock: null, heightlock: null, @@ -793,7 +793,7 @@ test('get utxos from addresses that are not my own should fail with ApiError.ADD index: 1, tokenId: token1, address: ADDRESSES[1], - value: 200, + value: 200n, authorities: 0, timelock: null, heightlock: null, @@ -804,7 +804,7 @@ test('get utxos from addresses that are not my own should fail with ApiError.ADD await addToUtxoTable(mysql, utxos); const event = makeGatewayEventWithAuthorizer('my-wallet', null, null, { - addresses: [ADDRESSES[1]], + 'addresses[]': [ADDRESSES[1]], }); const result = await getFilteredTxOutputs(event, null, null) as APIGatewayProxyResult; @@ -846,7 +846,7 @@ test('get spent tx_output', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 50, + value: 50n, authorities: 0, timelock: null, heightlock: null, @@ -857,7 +857,7 @@ test('get spent tx_output', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -877,6 +877,7 @@ test('get spent tx_output', async () => { const formatTxOutput = (txOutput, path) => ({ ...txOutput, + value: Number(txOutput.value), txProposalIndex: null, txProposalId: null, addressPath: `m/44'/280'/0'/0/${path}`, @@ -888,3 +889,279 @@ test('get spent tx_output', async () => { expect(returnBody.txOutputs[0]).toStrictEqual(formatTxOutput(txOutputs[1], 0)); expect(returnBody.txOutputs[1]).toStrictEqual(formatTxOutput(txOutputs[0], 0)); }); + +test('filter utxos by addresses and max 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: 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 }, + ]); + + await addToUtxoTable(mysql, [{ + txId: 'txId', + index: 0, + tokenId: '00', + address: ADDRESSES[0], + value: 100n, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: 'txId2', + index: 0, + tokenId: '00', + address: ADDRESSES[0], + value: 50n, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: 'txId3', + index: 0, + tokenId: '00', + address: ADDRESSES[1], + value: 30n, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]); + + const event = makeGatewayEventWithAuthorizer('my-wallet', null, null, null); + const result = await getFilteredTxOutputs(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.txOutputs.length).toStrictEqual(3); + + const event2 = makeGatewayEventWithAuthorizer('my-wallet', null, null, { 'addresses[]': [ADDRESSES[0]] }); + const result2 = await getFilteredTxOutputs(event2, null, null) as APIGatewayProxyResult; + const returnBody2 = JSON.parse(result2.body as string); + + expect(result2.statusCode).toBe(200); + expect(returnBody2.success).toBe(true); + expect(returnBody2.txOutputs.length).toStrictEqual(2); + expect(returnBody2.txOutputs[0].address).toStrictEqual(ADDRESSES[0]); + expect(returnBody2.txOutputs[1].address).toStrictEqual(ADDRESSES[0]); + + const event3 = makeGatewayEventWithAuthorizer('my-wallet', { maxOutputs: '1' }, null, { 'addresses[]': [ADDRESSES[0]] }); + const result3 = await getFilteredTxOutputs(event3, null, null) as APIGatewayProxyResult; + const returnBody3 = JSON.parse(result3.body as string); + + expect(result3.statusCode).toBe(200); + expect(returnBody3.success).toBe(true); + expect(returnBody3.txOutputs.length).toStrictEqual(1); + expect(returnBody3.txOutputs[0].address).toStrictEqual(ADDRESSES[0]); +}); + +test('filter tx_outputs with totalAmount', 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 = '00'; + + const txOutputs = [{ + txId: TX_IDS[0], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 50n, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[1], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100n, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[2], + index: 0, + tokenId: token1, + address: ADDRESSES[1], + value: 200n, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[3], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 300n, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, txOutputs); + + // Test 1: Request exactly what one UTXO can fulfill + let event = makeGatewayEventWithAuthorizer('my-wallet', { + tokenId: token1, + totalAmount: '200', + }, 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(1); + // Should select the 200n UTXO (smallest one that can fulfill the amount) + expect(returnBody.txOutputs[0].txId).toBe(TX_IDS[2]); + expect(returnBody.txOutputs[0].value).toBe(200); + + // Test 2: Request amount that requires multiple UTXOs + event = makeGatewayEventWithAuthorizer('my-wallet', { + tokenId: token1, + totalAmount: '250', + }, 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); + // Should select the 300n UTXO (smallest single UTXO that can fulfill) + expect(returnBody.txOutputs).toHaveLength(1); + expect(returnBody.txOutputs[0].txId).toBe(TX_IDS[3]); + expect(returnBody.txOutputs[0].value).toBe(300); + + // Test 3: Request amount that requires combining UTXOs + event = makeGatewayEventWithAuthorizer('my-wallet', { + tokenId: token1, + totalAmount: '450', + }, 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); + // Should select multiple UTXOs to fulfill the amount + expect(returnBody.txOutputs.length).toBe(2); + const totalValue = returnBody.txOutputs.reduce((sum, utxo) => sum + utxo.value, 0); + expect(totalValue).toBeGreaterThanOrEqual(450); +}); + +test('filter tx_outputs with totalAmount insufficient funds', 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, + }]); + + const token1 = '00'; + + const txOutputs = [{ + txId: TX_IDS[0], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 50n, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }, { + txId: TX_IDS[1], + index: 0, + tokenId: token1, + address: ADDRESSES[0], + value: 100n, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, txOutputs); + + // Request more than available (150n available, request 200n) + const event = makeGatewayEventWithAuthorizer('my-wallet', { + tokenId: token1, + totalAmount: '200', + }, null); + + const result = await getFilteredTxOutputs(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.txOutputs).toHaveLength(0); // Should return empty array when insufficient funds +}); diff --git a/packages/wallet-service/tests/txProposal.test.ts b/packages/wallet-service/tests/txProposal.test.ts index b42a0c3c..b49a6405 100644 --- a/packages/wallet-service/tests/txProposal.test.ts +++ b/packages/wallet-service/tests/txProposal.test.ts @@ -47,10 +47,11 @@ beforeEach(async () => { max_number_inputs: 255, max_number_outputs: 255, decimal_places: 2, + nano_contracts_enabled: true, genesis_block_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', genesis_tx1_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', - native_token: { name: 'Hathor', symbol: 'HTR'}, + native_token: { name: 'Hathor', symbol: 'HTR' }, }; await addToVersionDataTable(mysql, now, versionData); @@ -107,7 +108,7 @@ test('POST /txproposals with utxos that are already used on another txproposal s index: 0, tokenId: token1, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -118,7 +119,7 @@ test('POST /txproposals with utxos that are already used on another txproposal s index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -129,7 +130,7 @@ test('POST /txproposals with utxos that are already used on another txproposal s index: 0, tokenId: token2, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -141,8 +142,8 @@ test('POST /txproposals with utxos that are already used on another txproposal s await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token1', - unlockedBalance: 400, - lockedBalance: 0, + unlockedBalance: 400n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -150,8 +151,8 @@ test('POST /txproposals with utxos that are already used on another txproposal s }, { walletId: 'my-wallet', tokenId: 'token2', - unlockedBalance: 300, - lockedBalance: 0, + unlockedBalance: 300n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -172,10 +173,10 @@ test('POST /txproposals with utxos that are already used on another txproposal s const outputs = [ new hathorLib.Output( - 300, + 300n, p2pkhAddress, { - tokenData: 1, - }, + tokenData: 1, + }, ), ]; const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; @@ -216,12 +217,13 @@ test('POST /txproposals with too many outputs should fail with ApiError.TOO_MANY token_deposit_percentage: 0.01, reward_spend_min_blocks: 300, max_number_inputs: 255, + nano_contracts_enabled: true, max_number_outputs: 2, // mocking to force a failure decimal_places: 2, genesis_block_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', genesis_tx1_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', - native_token: { name: 'Hathor', symbol: 'HTR'}, + native_token: { name: 'Hathor', symbol: 'HTR' }, }); jest.resetModules(); @@ -251,7 +253,7 @@ test('POST /txproposals with too many outputs should fail with ApiError.TOO_MANY ]; const outputs = [...Array(10).keys()].map(() => ( - new hathorLib.Output(300, new hathorLib.P2PKH(new hathorLib.Address(ADDRESSES[0], { + new hathorLib.Output(300n, new hathorLib.P2PKH(new hathorLib.Address(ADDRESSES[0], { network: new hathorLib.Network(process.env.NETWORK), })).createScript(), { tokenData: 1, @@ -295,7 +297,7 @@ test('POST /txproposals with a wallet that is not ready should fail with ApiErro index: 0, tokenId: 'token1', address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -306,7 +308,7 @@ test('POST /txproposals with a wallet that is not ready should fail with ApiErro index: 0, tokenId: 'token1', address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -317,7 +319,7 @@ test('POST /txproposals with a wallet that is not ready should fail with ApiErro index: 0, tokenId: 'token2', address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -329,8 +331,8 @@ test('POST /txproposals with a wallet that is not ready should fail with ApiErro await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token1', - unlockedBalance: 400, - lockedBalance: 0, + unlockedBalance: 400n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -338,8 +340,8 @@ test('POST /txproposals with a wallet that is not ready should fail with ApiErro }, { walletId: 'my-wallet', tokenId: 'token2', - unlockedBalance: 300, - lockedBalance: 0, + unlockedBalance: 300n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -388,7 +390,7 @@ test('PUT /txproposals/{proposalId} with an empty body should fail with ApiError index: 0, tokenId: token1, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -399,7 +401,7 @@ test('PUT /txproposals/{proposalId} with an empty body should fail with ApiError index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -410,7 +412,7 @@ test('PUT /txproposals/{proposalId} with an empty body should fail with ApiError index: 0, tokenId: token2, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -422,8 +424,8 @@ test('PUT /txproposals/{proposalId} with an empty body should fail with ApiError await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token1', - unlockedBalance: 400, - lockedBalance: 0, + unlockedBalance: 400n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -431,8 +433,8 @@ test('PUT /txproposals/{proposalId} with an empty body should fail with ApiError }, { walletId: 'my-wallet', tokenId: 'token2', - unlockedBalance: 300, - lockedBalance: 0, + unlockedBalance: 300n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -449,12 +451,12 @@ test('PUT /txproposals/{proposalId} with an empty body should fail with ApiError // only one output, spending the whole 300 utxo of token1 const outputs = [ new hathorLib.Output( - 300, + 300n, new hathorLib.P2PKH(new hathorLib.Address(ADDRESSES[0], { network: new hathorLib.Network(process.env.NETWORK), })).createScript(), { - tokenData: 1, - }, + tokenData: 1, + }, ), ]; const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; @@ -529,7 +531,7 @@ test('PUT /txproposals/{proposalId} on a proposal which status is not OPEN or SE index: 0, tokenId: token1, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -540,7 +542,7 @@ test('PUT /txproposals/{proposalId} on a proposal which status is not OPEN or SE index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -551,7 +553,7 @@ test('PUT /txproposals/{proposalId} on a proposal which status is not OPEN or SE index: 0, tokenId: token2, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -563,8 +565,8 @@ test('PUT /txproposals/{proposalId} on a proposal which status is not OPEN or SE await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token1', - unlockedBalance: 400, - lockedBalance: 0, + unlockedBalance: 400n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -572,8 +574,8 @@ test('PUT /txproposals/{proposalId} on a proposal which status is not OPEN or SE }, { walletId: 'my-wallet', tokenId: 'token2', - unlockedBalance: 300, - lockedBalance: 0, + unlockedBalance: 300n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -590,16 +592,16 @@ test('PUT /txproposals/{proposalId} on a proposal which status is not OPEN or SE // only one output, spending the whole 300 utxo of token1 const outputs = [ new hathorLib.Output( - 300, + 300n, new hathorLib.P2PKH( new hathorLib.Address( ADDRESSES[0], { - network: new hathorLib.Network(process.env.NETWORK), - }, + network: new hathorLib.Network(process.env.NETWORK), + }, ), ).createScript(), { - tokenData: 1, - }, + tokenData: 1, + }, ), ]; const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; @@ -654,7 +656,7 @@ test('PUT /txproposals/{proposalId} on a proposal which is not owned by the user index: 0, tokenId: token1, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -665,7 +667,7 @@ test('PUT /txproposals/{proposalId} on a proposal which is not owned by the user index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -676,7 +678,7 @@ test('PUT /txproposals/{proposalId} on a proposal which is not owned by the user index: 0, tokenId: token2, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -688,8 +690,8 @@ test('PUT /txproposals/{proposalId} on a proposal which is not owned by the user await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token1', - unlockedBalance: 400, - lockedBalance: 0, + unlockedBalance: 400n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -697,8 +699,8 @@ test('PUT /txproposals/{proposalId} on a proposal which is not owned by the user }, { walletId: 'my-wallet', tokenId: 'token2', - unlockedBalance: 300, - lockedBalance: 0, + unlockedBalance: 300n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -715,14 +717,14 @@ test('PUT /txproposals/{proposalId} on a proposal which is not owned by the user // only one output, spending the whole 300 utxo of token1 const outputs = [ new hathorLib.Output( - 300, + 300n, new hathorLib.P2PKH(new hathorLib.Address( ADDRESSES[0], { - network: new hathorLib.Network(process.env.NETWORK), - }, - )).createScript(), { - tokenData: 1, + network: new hathorLib.Network(process.env.NETWORK), }, + )).createScript(), { + tokenData: 1, + }, ), ]; const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; @@ -769,6 +771,7 @@ test('PUT /txproposals/{proposalId} with an invalid txHex should fail and update success: true, version: '0.38.0', network: 'mainnet', + nano_contracts_enabled: true, min_weight: 14, min_tx_weight: 14, min_tx_weight_coefficient: 1.6, @@ -777,6 +780,11 @@ test('PUT /txproposals/{proposalId} with an invalid txHex should fail and update reward_spend_min_blocks: 300, max_number_inputs: 255, max_number_outputs: 255, + decimal_places: 2, + genesis_block_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx1_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + native_token: { name: 'Hathor', symbol: 'HTR' }, }, }), }); @@ -805,7 +813,7 @@ test('PUT /txproposals/{proposalId} with an invalid txHex should fail and update index: 0, tokenId: token1, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -816,7 +824,7 @@ test('PUT /txproposals/{proposalId} with an invalid txHex should fail and update index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -827,7 +835,7 @@ test('PUT /txproposals/{proposalId} with an invalid txHex should fail and update index: 0, tokenId: token2, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -839,8 +847,8 @@ test('PUT /txproposals/{proposalId} with an invalid txHex should fail and update await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token1', - unlockedBalance: 400, - lockedBalance: 0, + unlockedBalance: 400n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -848,8 +856,8 @@ test('PUT /txproposals/{proposalId} with an invalid txHex should fail and update }, { walletId: 'my-wallet', tokenId: 'token2', - unlockedBalance: 300, - lockedBalance: 0, + unlockedBalance: 300n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -866,14 +874,14 @@ test('PUT /txproposals/{proposalId} with an invalid txHex should fail and update // only one output, spending the whole 300 utxo of token1 const outputs = [ new hathorLib.Output( - 300, + 300n, new hathorLib.P2PKH(new hathorLib.Address( ADDRESSES[0], { - network: new hathorLib.Network(process.env.NETWORK), - }, - )).createScript(), { - tokenData: 1, + network: new hathorLib.Network(process.env.NETWORK), }, + )).createScript(), { + tokenData: 1, + }, ), ]; const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; @@ -913,6 +921,7 @@ test('PUT /txproposals/{proposalId} should update tx_proposal to SEND_ERROR on f success: true, version: '0.38.0', network: 'mainnet', + nano_contracts_enabled: true, min_weight: 14, min_tx_weight: 14, min_tx_weight_coefficient: 1.6, @@ -921,6 +930,11 @@ test('PUT /txproposals/{proposalId} should update tx_proposal to SEND_ERROR on f reward_spend_min_blocks: 300, max_number_inputs: 255, max_number_outputs: 255, + decimal_places: 2, + genesis_block_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx1_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + native_token: { name: 'Hathor', symbol: 'HTR' }, }, }), }); @@ -949,7 +963,7 @@ test('PUT /txproposals/{proposalId} should update tx_proposal to SEND_ERROR on f index: 0, tokenId: token1, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -960,7 +974,7 @@ test('PUT /txproposals/{proposalId} should update tx_proposal to SEND_ERROR on f index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -971,7 +985,7 @@ test('PUT /txproposals/{proposalId} should update tx_proposal to SEND_ERROR on f index: 0, tokenId: token2, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -983,8 +997,8 @@ test('PUT /txproposals/{proposalId} should update tx_proposal to SEND_ERROR on f await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token1', - unlockedBalance: 400, - lockedBalance: 0, + unlockedBalance: 400n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -992,8 +1006,8 @@ test('PUT /txproposals/{proposalId} should update tx_proposal to SEND_ERROR on f }, { walletId: 'my-wallet', tokenId: 'token2', - unlockedBalance: 300, - lockedBalance: 0, + unlockedBalance: 300n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -1010,14 +1024,14 @@ test('PUT /txproposals/{proposalId} should update tx_proposal to SEND_ERROR on f // only one output, spending the whole 300 utxo of token1 const outputs = [ new hathorLib.Output( - 300, + 300n, new hathorLib.P2PKH(new hathorLib.Address( ADDRESSES[0], { - network: new hathorLib.Network(process.env.NETWORK), - }, - )).createScript(), { - tokenData: 1, + network: new hathorLib.Network(process.env.NETWORK), }, + )).createScript(), { + tokenData: 1, + }, ), ]; const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; @@ -1069,7 +1083,7 @@ test('DELETE /txproposals/{proposalId} should delete a tx_proposal and remove th index: 0, tokenId: token1, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -1080,7 +1094,7 @@ test('DELETE /txproposals/{proposalId} should delete a tx_proposal and remove th index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -1091,7 +1105,7 @@ test('DELETE /txproposals/{proposalId} should delete a tx_proposal and remove th index: 0, tokenId: token2, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -1103,8 +1117,8 @@ test('DELETE /txproposals/{proposalId} should delete a tx_proposal and remove th await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token1', - unlockedBalance: 400, - lockedBalance: 0, + unlockedBalance: 400n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -1112,8 +1126,8 @@ test('DELETE /txproposals/{proposalId} should delete a tx_proposal and remove th }, { walletId: 'my-wallet', tokenId: 'token2', - unlockedBalance: 300, - lockedBalance: 0, + unlockedBalance: 300n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -1130,14 +1144,14 @@ test('DELETE /txproposals/{proposalId} should delete a tx_proposal and remove th // only one output, spending the whole 300 utxo of token1 const outputs = [ new hathorLib.Output( - 300, + 300n, new hathorLib.P2PKH(new hathorLib.Address( ADDRESSES[0], { - network: new hathorLib.Network(process.env.NETWORK), - }, - )).createScript(), { - tokenData: 1, + network: new hathorLib.Network(process.env.NETWORK), }, + )).createScript(), { + tokenData: 1, + }, ), ]; const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; @@ -1239,7 +1253,7 @@ test('POST /txproposals one output and input on txHex', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -1250,7 +1264,7 @@ test('POST /txproposals one output and input on txHex', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -1261,7 +1275,7 @@ test('POST /txproposals one output and input on txHex', async () => { index: 0, tokenId: token2, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -1273,8 +1287,8 @@ test('POST /txproposals one output and input on txHex', async () => { await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token1', - unlockedBalance: 400, - lockedBalance: 0, + unlockedBalance: 400n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -1282,8 +1296,8 @@ test('POST /txproposals one output and input on txHex', async () => { }, { walletId: 'my-wallet', tokenId: 'token2', - unlockedBalance: 300, - lockedBalance: 0, + unlockedBalance: 300n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -1300,14 +1314,14 @@ test('POST /txproposals one output and input on txHex', async () => { // only one output, spending the whole 300 utxo of token1 const outputs = [ new hathorLib.Output( - 300, + 300n, new hathorLib.P2PKH(new hathorLib.Address( ADDRESSES[0], { - network: new hathorLib.Network(process.env.NETWORK), - }, - )).createScript(), { - tokenData: 1, + network: new hathorLib.Network(process.env.NETWORK), }, + )).createScript(), { + tokenData: 1, + }, ), ]; const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; @@ -1369,7 +1383,7 @@ test('POST /txproposals with denied utxos', async () => { index: 0, tokenId: token1, address: ADDRESSES[1], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -1380,7 +1394,7 @@ test('POST /txproposals with denied utxos', async () => { index: 0, tokenId: token1, address: ADDRESSES[1], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -1391,7 +1405,7 @@ test('POST /txproposals with denied utxos', async () => { index: 0, tokenId: token2, address: ADDRESSES[1], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -1403,14 +1417,14 @@ test('POST /txproposals with denied utxos', async () => { const outputs = [ new hathorLib.Output( - 300, + 300n, new hathorLib.P2PKH(new hathorLib.Address( ADDRESSES[0], { - network: new hathorLib.Network(process.env.NETWORK), - }, - )).createScript(), { - tokenData: 1, + network: new hathorLib.Network(process.env.NETWORK), }, + )).createScript(), { + tokenData: 1, + }, ), ]; const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; @@ -1450,7 +1464,7 @@ test('POST /txproposals a tx create action on txHex', async () => { index: 0, tokenId: '00', address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -1471,12 +1485,12 @@ test('POST /txproposals a tx create action on txHex', async () => { const outputs = [ // change output 100 htr deposited: new hathorLib.Output( - 200, + 200n, new hathorLib.P2PKH( new hathorLib.Address( ADDRESSES[0], { - network: new hathorLib.Network(process.env.NETWORK), - }, + network: new hathorLib.Network(process.env.NETWORK), + }, ), ).createScript(), { tokenData: 0 }, @@ -1487,8 +1501,8 @@ test('POST /txproposals a tx create action on txHex', async () => { new hathorLib.P2PKH( new hathorLib.Address( ADDRESSES[0], { - network: new hathorLib.Network(process.env.NETWORK), - }, + network: new hathorLib.Network(process.env.NETWORK), + }, ), ).createScript(), { tokenData: 1 | hathorLib.constants.TOKEN_AUTHORITY_MASK }, // eslint-disable-line no-bitwise @@ -1499,20 +1513,20 @@ test('POST /txproposals a tx create action on txHex', async () => { new hathorLib.P2PKH( new hathorLib.Address( ADDRESSES[0], { - network: new hathorLib.Network(process.env.NETWORK), - }, + 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, + 100n * 100n, new hathorLib.P2PKH( new hathorLib.Address( ADDRESSES[0], { - network: new hathorLib.Network(process.env.NETWORK), - }, + network: new hathorLib.Network(process.env.NETWORK), + }, ), ).createScript(), { tokenData: 1 }, @@ -1551,6 +1565,7 @@ test('PUT /txproposals/{proposalId} with txhex', async () => { success: true, version: '0.38.0', network: 'mainnet', + nano_contracts_enabled: true, min_weight: 14, min_tx_weight: 14, min_tx_weight_coefficient: 1.6, @@ -1559,6 +1574,11 @@ test('PUT /txproposals/{proposalId} with txhex', async () => { reward_spend_min_blocks: 300, max_number_inputs: 255, max_number_outputs: 255, + decimal_places: 2, + genesis_block_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx1_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + native_token: { name: 'Hathor', symbol: 'HTR' }, }, }), }); @@ -1587,7 +1607,7 @@ test('PUT /txproposals/{proposalId} with txhex', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -1598,7 +1618,7 @@ test('PUT /txproposals/{proposalId} with txhex', async () => { index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -1609,7 +1629,7 @@ test('PUT /txproposals/{proposalId} with txhex', async () => { index: 0, tokenId: token2, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -1621,8 +1641,8 @@ test('PUT /txproposals/{proposalId} with txhex', async () => { await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token1', - unlockedBalance: 400, - lockedBalance: 0, + unlockedBalance: 400n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -1630,8 +1650,8 @@ test('PUT /txproposals/{proposalId} with txhex', async () => { }, { walletId: 'my-wallet', tokenId: 'token2', - unlockedBalance: 300, - lockedBalance: 0, + unlockedBalance: 300n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -1648,7 +1668,7 @@ test('PUT /txproposals/{proposalId} with txhex', async () => { // only one output, spending the whole 300 utxo of token1 const outputs = [ new hathorLib.Output( - 300, + 300n, new hathorLib.P2PKH( new hathorLib.Address(ADDRESSES[0], { network: new hathorLib.Network(process.env.NETWORK) }), ).createScript(), @@ -1693,6 +1713,7 @@ test('PUT /txproposals/{proposalId} with a different txhex than the one sent in success: true, version: '0.38.0', network: 'mainnet', + nano_contracts_enabled: true, min_weight: 14, min_tx_weight: 14, min_tx_weight_coefficient: 1.6, @@ -1701,6 +1722,11 @@ test('PUT /txproposals/{proposalId} with a different txhex than the one sent in reward_spend_min_blocks: 300, max_number_inputs: 255, max_number_outputs: 255, + decimal_places: 2, + genesis_block_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx1_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + native_token: { name: 'Hathor', symbol: 'HTR' }, }, }), }); @@ -1729,7 +1755,7 @@ test('PUT /txproposals/{proposalId} with a different txhex than the one sent in index: 0, tokenId: token1, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -1740,7 +1766,7 @@ test('PUT /txproposals/{proposalId} with a different txhex than the one sent in index: 0, tokenId: token1, address: ADDRESSES[0], - value: 100, + value: 100n, authorities: 0, timelock: null, heightlock: null, @@ -1751,7 +1777,7 @@ test('PUT /txproposals/{proposalId} with a different txhex than the one sent in index: 0, tokenId: token2, address: ADDRESSES[0], - value: 300, + value: 300n, authorities: 0, timelock: null, heightlock: null, @@ -1763,8 +1789,8 @@ test('PUT /txproposals/{proposalId} with a different txhex than the one sent in await addToWalletBalanceTable(mysql, [{ walletId: 'my-wallet', tokenId: 'token1', - unlockedBalance: 400, - lockedBalance: 0, + unlockedBalance: 400n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -1772,8 +1798,8 @@ test('PUT /txproposals/{proposalId} with a different txhex than the one sent in }, { walletId: 'my-wallet', tokenId: 'token2', - unlockedBalance: 300, - lockedBalance: 0, + unlockedBalance: 300n, + lockedBalance: 0n, unlockedAuthorities: 0, lockedAuthorities: 0, timelockExpires: null, @@ -1790,7 +1816,7 @@ test('PUT /txproposals/{proposalId} with a different txhex than the one sent in // only one output, spending the whole 300 utxo of token1 const outputs = [ new hathorLib.Output( - 300, + 300n, new hathorLib.P2PKH( new hathorLib.Address(ADDRESSES[0], { network: new hathorLib.Network(process.env.NETWORK) }), ).createScript(), @@ -1837,7 +1863,7 @@ test('checkMissingUtxos', async () => { index: 0, tokenId: '00', address: ADDRESSES[0], - value: 0, + value: 0n, authorities: 0, timelock: 0, heightlock: 0, @@ -1851,3 +1877,43 @@ test('checkMissingUtxos', async () => { expect(checkMissingResult).toHaveLength(1); }); + +test('POST /txproposals with empty inputs array should succeed', 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, + }]); + + // Create a transaction with no inputs and no outputs (e.g., for nano contracts) + const outputs = []; // Empty outputs array + const inputs = []; // Empty inputs array + const transaction = new hathorLib.Transaction(inputs, outputs); + + 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(0); + + // Verify that the tx proposal was created + const txProposal = await getTxProposal(mysql, returnBody.txProposalId); + expect(txProposal).not.toBeNull(); +}); diff --git a/packages/wallet-service/tests/txProposalUtxoUnlock.test.ts b/packages/wallet-service/tests/txProposalUtxoUnlock.test.ts new file mode 100644 index 00000000..dba0e8ed --- /dev/null +++ b/packages/wallet-service/tests/txProposalUtxoUnlock.test.ts @@ -0,0 +1,303 @@ +/** + * 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 { APIGatewayProxyResult } from 'aws-lambda'; +import hathorLib from '@hathor/wallet-lib'; +import { + create as txProposalCreate, +} from '@src/api/txProposalCreate'; + +import { + send as txProposalSend, +} from '@src/api/txProposalSend'; + +import { + addToWalletTable, + addToAddressTable, + addToUtxoTable, + addToWalletBalanceTable, + ADDRESSES, + cleanDatabase, + makeGatewayEventWithAuthorizer, +} from '@tests/utils'; + +import { + closeDbConnection, + getDbConnection, +} from '@src/utils'; + +import { + getUtxos, + getTxProposal, +} from '@src/db'; + +import { TxProposalStatus } from '@src/types'; + +const mysql = getDbConnection(); + +beforeEach(async () => { + await cleanDatabase(mysql); +}); + +afterAll(async () => { + await closeDbConnection(mysql); +}); + +describe('TxProposal UTXO unlocking on send failure', () => { + test('UTXOs should be released when txProposalSend fails', async () => { + expect.hasAssertions(); + + // Create the spy to mock wallet-lib to force a failure + const spy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); + spy.mockReturnValue({ + post: () => { + throw new Error('Network error - send failed'); + }, + // @ts-ignore + 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, + }, + }), + }); + + // Setup wallet + await addToWalletTable(mysql, [{ + id: 'test-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'test-wallet', + transactions: 1, + }]); + + const tokenId = '00'; + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId, + address: ADDRESSES[0], + value: 100n, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'test-wallet', + tokenId, + unlockedBalance: 100n, + lockedBalance: 0n, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + // Verify UTXO is initially unlocked (no tx_proposal_id) + let utxoResults = await getUtxos(mysql, [{ txId: utxos[0].txId, index: utxos[0].index }]); + expect(utxoResults).toHaveLength(1); + expect(utxoResults[0].txProposalId).toBeNull(); + expect(utxoResults[0].txProposalIndex).toBeNull(); + + // Create transaction + const outputs = [ + new hathorLib.Output( + 100n, + new hathorLib.P2PKH(new hathorLib.Address( + ADDRESSES[0], + { network: new hathorLib.Network(process.env.NETWORK) } + )).createScript(), + { tokenData: 0 } + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs); + const txHex = transaction.toHex(); + + // Create tx proposal + const createEvent = makeGatewayEventWithAuthorizer('test-wallet', null, JSON.stringify({ txHex })); + const createResult = await txProposalCreate(createEvent, null, null) as APIGatewayProxyResult; + + expect(createResult.statusCode).toBe(201); + const createBody = JSON.parse(createResult.body as string); + expect(createBody.success).toBe(true); + const txProposalId = createBody.txProposalId; + + // Verify UTXO is now locked with tx proposal ID + utxoResults = await getUtxos(mysql, [{ txId: utxos[0].txId, index: utxos[0].index }]); + expect(utxoResults).toHaveLength(1); + expect(utxoResults[0].txProposalId).toBe(txProposalId); + expect(utxoResults[0].txProposalIndex).toBe(0); + + // Attempt to send the transaction (this will fail due to our mock) + const sendEvent = makeGatewayEventWithAuthorizer( + 'test-wallet', + { txProposalId }, + JSON.stringify({ txHex }) + ); + const sendResult = await txProposalSend(sendEvent, null, null) as APIGatewayProxyResult; + + // Verify send failed and proposal status is SEND_ERROR + expect(sendResult.statusCode).toBe(400); + const sendBody = JSON.parse(sendResult.body as string); + expect(sendBody.success).toBe(false); + + const txProposal = await getTxProposal(mysql, txProposalId); + expect(txProposal!.status).toBe(TxProposalStatus.SEND_ERROR); + + utxoResults = await getUtxos(mysql, [{ txId: utxos[0].txId, index: utxos[0].index }]); + expect(utxoResults).toHaveLength(1); + + expect(utxoResults[0].txProposalId).toBeNull(); // Should be null (released) + expect(utxoResults[0].txProposalIndex).toBeNull(); // Should be null (released) + + spy.mockRestore(); + }); + + test('UTXOs should remain locked when txProposalSend succeeds', async () => { + expect.hasAssertions(); + + // Create the spy to mock wallet-lib to force success + const spy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); + spy.mockReturnValue({ + // @ts-ignore + post: () => Promise.resolve({ + data: { success: true, hash: 'mocked-hash' } + }), + // @ts-ignore + 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, + }, + }), + }); + + // Setup wallet (same as above) + await addToWalletTable(mysql, [{ + id: 'test-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + await addToAddressTable(mysql, [{ + address: ADDRESSES[0], + index: 0, + walletId: 'test-wallet', + transactions: 1, + }]); + + const tokenId = '00'; + const utxos = [{ + txId: '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', + index: 0, + tokenId, + address: ADDRESSES[0], + value: 100n, + authorities: 0, + timelock: null, + heightlock: null, + locked: false, + spentBy: null, + }]; + + await addToUtxoTable(mysql, utxos); + await addToWalletBalanceTable(mysql, [{ + walletId: 'test-wallet', + tokenId, + unlockedBalance: 100n, + lockedBalance: 0n, + unlockedAuthorities: 0, + lockedAuthorities: 0, + timelockExpires: null, + transactions: 1, + }]); + + // Create transaction and proposal (same as above) + const outputs = [ + new hathorLib.Output( + 100n, + new hathorLib.P2PKH(new hathorLib.Address( + ADDRESSES[0], + { network: new hathorLib.Network(process.env.NETWORK) } + )).createScript(), + { tokenData: 0 } + ), + ]; + const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)]; + const transaction = new hathorLib.Transaction(inputs, outputs); + const txHex = transaction.toHex(); + + const createEvent = makeGatewayEventWithAuthorizer('test-wallet', null, JSON.stringify({ txHex })); + const createResult = await txProposalCreate(createEvent, null, null) as APIGatewayProxyResult; + const createBody = JSON.parse(createResult.body as string); + const txProposalId = createBody.txProposalId; + + // Send the transaction (this will succeed due to our mock) + const sendEvent = makeGatewayEventWithAuthorizer( + 'test-wallet', + { txProposalId }, + JSON.stringify({ txHex }) + ); + const sendResult = await txProposalSend(sendEvent, null, null) as APIGatewayProxyResult; + + // Verify send succeeded and proposal status is SENT + expect(sendResult.statusCode).toBe(200); + const sendBody = JSON.parse(sendResult.body as string); + expect(sendBody.success).toBe(true); + + const txProposal = await getTxProposal(mysql, txProposalId); + expect(txProposal!.status).toBe(TxProposalStatus.SENT); + + // UTXOs should remain locked when send succeeds (they'll be spent when tx is processed) + const utxoResults = await getUtxos(mysql, [{ txId: utxos[0].txId, index: utxos[0].index }]); + expect(utxoResults).toHaveLength(1); + expect(utxoResults[0].txProposalId).toBe(txProposalId); // Should remain locked + expect(utxoResults[0].txProposalIndex).toBe(0); // Should remain locked + + spy.mockRestore(); + }); +}); diff --git a/packages/wallet-service/tests/types.test.ts b/packages/wallet-service/tests/types.test.ts index 94d4d195..ed9a17ea 100644 --- a/packages/wallet-service/tests/types.test.ts +++ b/packages/wallet-service/tests/types.test.ts @@ -37,24 +37,24 @@ test('Authorities', () => { 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)); + const b1 = new Balance(3n, 1n, 2n, null, new Authorities(0b01), new Authorities(0b00)); + const b2 = new Balance(7n, 3n, 4n, null, new Authorities(0b10), new Authorities(0b11)); + expect(Balance.merge(b1, b2)).toStrictEqual(new Balance(10n, 4n, 6n, null, new Authorities(0b11), new Authorities(0b11))); + + const b3 = new Balance(3n, 1n, 2n, 1000); + const b4 = new Balance(7n, 3n, 4n); + expect(Balance.merge(b3, b4)).toStrictEqual(new Balance(10n, 4n, 6n, 1000)); + expect(Balance.merge(b4, b3)).toStrictEqual(new Balance(10n, 4n, 6n, 1000)); + + const b5 = new Balance(30n, 10n, 20n, 2000); + expect(Balance.merge(b3, b5)).toStrictEqual(new Balance(33n, 11n, 22n, 1000)); + expect(Balance.merge(b5, b3)).toStrictEqual(new Balance(33n, 11n, 22n, 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); + const b = new Balance(3n, 1n, 2n, null, new Authorities(0b01), new Authorities(0b10)); + expect(b.total()).toBe(3n); expect(b.authorities()).toStrictEqual(new Authorities(0b11)); }); @@ -64,7 +64,7 @@ test('TokenBalanceMap basic', () => { // 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); + const b1 = new Balance(14n, 5n, 9n, 1000); t1.set('token1', b1); expect(t1.get('token1')).toStrictEqual(b1); // balance for a different token should still be 0 @@ -74,7 +74,7 @@ test('TokenBalanceMap basic', () => { test('TokenBalanceMap clone', () => { expect.hasAssertions(); const t1 = new TokenBalanceMap(); - t1.set('token1', new Balance(14, 5, 9, 1000)); + t1.set('token1', new Balance(14n, 5n, 9n, 1000)); const t2 = t1.clone(); expect(t1).toStrictEqual(t2); expect(t1).not.toBe(t2); @@ -85,11 +85,11 @@ test('TokenBalanceMap clone', () => { 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)); + t1.set('token1', new Balance(15n, 0n, 15n)); + t1.set('token2', new Balance(5n, 2n, -3n, 1000)); const t2 = TokenBalanceMap.fromStringMap({ - token1: { totalSent: 15, unlocked: 0, locked: 15 }, - token2: { totalSent: 5, unlocked: 2, locked: -3, lockExpires: 1000 }, + token1: { totalSent: 15n, unlocked: 0n, locked: 15n }, + token2: { totalSent: 5n, unlocked: 2n, locked: -3n, lockExpires: 1000 }, }); expect(t2).toStrictEqual(t1); }); @@ -97,17 +97,17 @@ test('TokenBalanceMap fromStringMap', () => { test('TokenBalanceMap merge', () => { expect.hasAssertions(); const t1 = TokenBalanceMap.fromStringMap({ - token1: { totalSent: 10, unlocked: 0, locked: 10 }, - token2: { totalSent: 12, unlocked: 5, locked: 7 }, + token1: { totalSent: 10n, unlocked: 0n, locked: 10n }, + token2: { totalSent: 12n, unlocked: 5n, locked: 7n }, }); const t2 = TokenBalanceMap.fromStringMap({ - token1: { totalSent: 10, unlocked: 2, locked: -3, lockExpires: 1000 }, - token3: { totalSent: 10, unlocked: 9, locked: 0 }, + token1: { totalSent: 10n, unlocked: 2n, locked: -3n, lockExpires: 1000 }, + token3: { totalSent: 10n, unlocked: 9n, locked: 0n }, }); 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)); + merged.set('token1', new Balance(20n, 2n, 7n, 1000)); + merged.set('token2', new Balance(12n, 5n, 7n)); + merged.set('token3', new Balance(10n, 9n, 0n)); expect(TokenBalanceMap.merge(t1, t2)).toStrictEqual(merged); // with null/undefined parameter @@ -128,7 +128,7 @@ test('TokenBalanceMap fromTxOutput fromTxInput', () => { timelock, }; const txOutput: TxOutput = { - value: 200, + value: 200n, token_data: 0, script: 'not-used', token: '00', @@ -139,17 +139,17 @@ test('TokenBalanceMap fromTxOutput fromTxInput', () => { const txInput: TxInput = { tx_id: '00000000000000029411240dc4aea675b672c260f1419c8a3b87cfa203398098', index: 2, - value: 200, + value: 200n, 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 } })); + expect(TokenBalanceMap.fromTxInput(txInput)).toStrictEqual(TokenBalanceMap.fromStringMap({ '00': { totalSent: 0n, unlocked: -txInput.value, locked: 0n } })); + expect(TokenBalanceMap.fromTxOutput(txOutput)).toStrictEqual(TokenBalanceMap.fromStringMap({ '00': { totalSent: 200n, unlocked: txOutput.value, locked: 0n } })); // locked txOutput.locked = true; - expect(TokenBalanceMap.fromTxOutput(txOutput)).toStrictEqual(TokenBalanceMap.fromStringMap({ '00': { totalSent: 200, locked: txOutput.value, unlocked: 0, lockExpires: timelock } })); + expect(TokenBalanceMap.fromTxOutput(txOutput)).toStrictEqual(TokenBalanceMap.fromStringMap({ '00': { totalSent: 200n, locked: txOutput.value, unlocked: 0n, lockExpires: timelock } })); }); diff --git a/packages/wallet-service/tests/types.ts b/packages/wallet-service/tests/types.ts index 2bf07dbe..565f39b1 100644 --- a/packages/wallet-service/tests/types.ts +++ b/packages/wallet-service/tests/types.ts @@ -10,8 +10,8 @@ export interface WalletBalanceEntry { walletId: string; tokenId: string; - unlockedBalance: number; - lockedBalance: number; + unlockedBalance: bigint; + lockedBalance: bigint; unlockedAuthorities: number; lockedAuthorities: number; timelockExpires?: number; @@ -22,7 +22,7 @@ export interface AddressTxHistoryTableEntry { address: string; txId: string; tokenId: string; - balance: number; + balance: bigint; timestamp: number; voided?: boolean; } @@ -32,6 +32,7 @@ export interface AddressTableEntry { index: number; walletId?: string; transactions: number; + seqnum?: number; } export interface TokenTableEntry { diff --git a/packages/wallet-service/tests/utils.ts b/packages/wallet-service/tests/utils.ts index 8342ef85..86b0a683 100644 --- a/packages/wallet-service/tests/utils.ts +++ b/packages/wallet-service/tests/utils.ts @@ -84,7 +84,7 @@ export const cleanDatabase = async (mysql: ServerlessMysql): Promise => { export const createOutput = ( index: number, - value: number, + value: bigint, address: string, token = '00', timelock: number = null, @@ -109,7 +109,7 @@ export const createOutput = ( ); export const createInput = ( - value: number, + value: bigint, address: string, txId: string, index: number, @@ -139,7 +139,7 @@ export const checkUtxoTable = async ( index?: number, tokenId?: string, address?: string, - value?: number, + value?: bigint, authorities?: number, timelock?: number | null, heightlock?: number | null, @@ -239,8 +239,8 @@ export const checkAddressBalanceTable = async ( totalResults: number, address: string, tokenId: string, - unlocked: number, - locked: number, + unlocked: bigint, + locked: bigint, lockExpires: number | null, transactions: number, unlockedAuthorities = 0, @@ -438,8 +438,8 @@ export const checkWalletBalanceTable = async ( totalResults: number, walletId?: string, tokenId?: string, - unlocked?: number, - locked?: number, + unlocked?: bigint, + locked?: bigint, lockExpires?: number | null, transactions?: number, unlockedAuthorities = 0, @@ -563,7 +563,7 @@ export const countTxOutputTable = async ( ); if (results.length > 0) { - return results[0].count as number; + return Number(results[0].count); } return 0; @@ -690,11 +690,12 @@ export const addToAddressTable = async ( entry.index, entry.walletId, entry.transactions, + entry.seqnum ?? 0, ])); await mysql.query(` INSERT INTO \`address\`(\`address\`, \`index\`, - \`wallet_id\`, \`transactions\`) + \`wallet_id\`, \`transactions\`, \`seqnum\`) VALUES ?`, [payload]); }; @@ -1034,14 +1035,14 @@ export const buildWalletBalanceValueMap = ( tokenId: 'token1', tokenSymbol: 'T1', lockExpires: null, - lockedAmount: 0, + lockedAmount: 0n, lockedAuthorities: { melt: false, mint: false, }, - total: 10, - totalAmountSent: 10, - unlockedAmount: 10, + total: 10n, + totalAmountSent: 10n, + unlockedAmount: 10n, unlockedAuthorities: { melt: false, mint: false, @@ -1144,3 +1145,8 @@ export const getXPrivKeyFromSeed = ( const code = new Mnemonic(seed); return code.toHDPrivateKey(passphrase, network.bitcoreNetwork); }; + +export const stopWalletLibOpenHandles = async () => { + const { stopGLLBackgroundTask } = await import('@hathor/wallet-lib'); + stopGLLBackgroundTask(); +}; diff --git a/packages/wallet-service/tests/utils/aws.utils.test.ts b/packages/wallet-service/tests/utils/aws.utils.test.ts new file mode 100644 index 00000000..06b3c773 --- /dev/null +++ b/packages/wallet-service/tests/utils/aws.utils.test.ts @@ -0,0 +1,78 @@ +/** + * @fileoverview Tests for aws-utils.ts + */ +import { createLambdaClient, createApiGatewayManagementApiClient } from '@src/utils/aws.utils'; +import { InvokeCommand } from '@aws-sdk/client-lambda'; +import { PostToConnectionCommand } from '@aws-sdk/client-apigatewaymanagementapi'; +import config, { loadEnvConfig } from '@src/config'; + +describe('aws-utils', () => { + const OLD_ENV = process.env; + beforeEach(() => { + jest.resetModules(); + process.env = { + ...OLD_ENV, + AWS_ACCESS_KEY_ID: 'test-access-key', + AWS_SECRET_ACCESS_KEY: 'test-secret-key', + AWS_REGION: 'us-east-1', + }; + loadEnvConfig(); + }); + afterEach(() => { + process.env = OLD_ENV; + }); + + describe('createLambdaClient', () => { + it('should return a mock LambdaClient when MOCK_AWS is true', async () => { + config.shouldMockAWS = true; + const client = createLambdaClient(); + const command = new InvokeCommand({ + FunctionName: 'test-fn', + InvocationType: 'Event', + Payload: JSON.stringify({ foo: 'bar' }), + }); + const result = await client.send(command); + expect(result.StatusCode).toBe(202); + }); + + it('should return a mock LambdaClient with sync invocation', async () => { + config.shouldMockAWS = true; + const client = createLambdaClient(); + const command = new InvokeCommand({ + FunctionName: 'test-fn', + InvocationType: 'RequestResponse', + Payload: JSON.stringify({ foo: 'bar' }), + }); + const result = await client.send(command); + expect(result.StatusCode).toBe(200); + expect(JSON.parse(result.Payload)).toEqual({ success: true }); + }); + + it('should return a real LambdaClient when MOCK_AWS is not true', () => { + config.shouldMockAWS = false; + const client = createLambdaClient(); + expect(client).toBeInstanceOf(Object); // Not a mock + expect(typeof client.send).toBe('function'); + }); + }); + + describe('createApiGatewayManagementApiClient', () => { + it('should return a mock ApiGatewayManagementApiClient when MOCK_AWS is true', async () => { + config.shouldMockAWS = true; + const client = createApiGatewayManagementApiClient(); + const command = new PostToConnectionCommand({ + ConnectionId: 'abc123', + Data: Buffer.from('hello'), + }); + const result = await client.send(command); + expect(result.StatusCode).toBe(200); + }); + + it('should return a real ApiGatewayManagementApiClient when MOCK_AWS is not true', () => { + config.shouldMockAWS = false; + const client = createApiGatewayManagementApiClient(); + expect(client).toBeInstanceOf(Object); // Not a mock + expect(typeof client.send).toBe('function'); + }); + }); +}); diff --git a/packages/wallet-service/tests/utils/pushnotification.utils.test.ts b/packages/wallet-service/tests/utils/pushnotification.utils.test.ts index 43c80925..0fde2b65 100644 --- a/packages/wallet-service/tests/utils/pushnotification.utils.test.ts +++ b/packages/wallet-service/tests/utils/pushnotification.utils.test.ts @@ -10,7 +10,8 @@ import { SendNotificationToDevice } from '@src/types'; import { Severity } from '@wallet-service/common/src/types'; import { sendMock, lambdaInvokeCommandMock } from '@tests/utils/aws-sdk.mock'; import { LambdaClient } from '@aws-sdk/client-lambda'; -import { buildWalletBalanceValueMap } from '@tests/utils'; +import { buildWalletBalanceValueMap, stopWalletLibOpenHandles } from '@tests/utils'; +import { bigIntUtils } from '@hathor/wallet-lib'; const isFirebaseInitializedMock = jest.spyOn(pushnotificationUtils, 'isFirebaseInitialized'); @@ -60,6 +61,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); 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."'); @@ -76,6 +78,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(mockedAddAlert).toHaveBeenLastCalledWith( 'Lambda missing env variables', @@ -96,6 +99,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(mockedAddAlert).toHaveBeenLastCalledWith( 'Lambda missing env variables', @@ -116,6 +120,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(mockedAddAlert).toHaveBeenLastCalledWith( 'Lambda missing env variables', @@ -136,6 +141,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(mockedAddAlert).toHaveBeenLastCalledWith( 'Lambda missing env variables', @@ -156,6 +162,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(mockedAddAlert).toHaveBeenLastCalledWith( 'Lambda missing env variables', @@ -176,6 +183,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(mockedAddAlert).toHaveBeenLastCalledWith( 'Lambda missing env variables', @@ -196,6 +204,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(mockedAddAlert).toHaveBeenLastCalledWith( 'Lambda missing env variables', @@ -216,6 +225,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(mockedAddAlert).toHaveBeenLastCalledWith( 'Lambda missing env variables', @@ -236,6 +246,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(mockedAddAlert).toHaveBeenLastCalledWith( 'Lambda missing env variables', @@ -256,6 +267,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(mockedAddAlert).toHaveBeenLastCalledWith( 'Lambda missing env variables', @@ -276,6 +288,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(mockedAddAlert).toHaveBeenLastCalledWith( 'Lambda missing env variables', @@ -299,6 +312,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); // No alert should be raised since push notifications are disabled expect(mockedAddAlert).not.toHaveBeenCalled(); @@ -314,6 +328,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(logger.error).toHaveBeenLastCalledWith('[ALERT] Error while parsing the env.FIREBASE_PRIVATE_KEY.'); }); @@ -327,6 +342,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); expect(logger.error).toHaveBeenLastCalledWith('[ALERT] env.PUSH_ALLOWED_PROVIDERS is empty.'); }); @@ -467,6 +483,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); const notification = { deviceId: 'device1', @@ -510,6 +527,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); const notification = { deviceId: 'device1', @@ -541,6 +559,7 @@ describe('PushNotificationUtils', () => { // reload module jest.resetModules(); const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); const notification = { deviceId: 'device1', @@ -569,6 +588,7 @@ describe('PushNotificationUtils', () => { }); jest.resetModules(); const { PushNotificationUtils, buildFunctionName } = await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); const walletMap = buildWalletBalanceValueMap(); const result = await PushNotificationUtils.invokeOnTxPushNotificationRequestedLambda(walletMap); @@ -589,7 +609,7 @@ describe('PushNotificationUtils', () => { expect(lambdaInvokeCommandMock).toHaveBeenCalledWith({ FunctionName: buildFunctionName(FunctionName.ON_TX_PUSH_NOTIFICATION_REQUESTED), InvocationType: 'Event', - Payload: JSON.stringify(walletMap), + Payload: bigIntUtils.JSONBigInt.stringify(walletMap), }); }); @@ -603,6 +623,7 @@ describe('PushNotificationUtils', () => { process.env.PUSH_NOTIFICATION_ENABLED = 'false'; jest.resetModules(); const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); const walletMap = buildWalletBalanceValueMap(); const result = await PushNotificationUtils.invokeOnTxPushNotificationRequestedLambda(walletMap); @@ -633,6 +654,7 @@ describe('PushNotificationUtils', () => { process.env.PUSH_NOTIFICATION_ENABLED = 'true'; jest.resetModules(); const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + await stopWalletLibOpenHandles(); 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/ws.utils.test.ts b/packages/wallet-service/tests/ws.utils.test.ts index 7c6e84bb..2d9cca59 100644 --- a/packages/wallet-service/tests/ws.utils.test.ts +++ b/packages/wallet-service/tests/ws.utils.test.ts @@ -42,6 +42,7 @@ jest.mock('redis', () => ({ })); import { endWsConnection } from '@src/redis'; +import { stopWalletLibOpenHandles } from './utils'; test('connectionInfoFromEvent', async () => { expect.hasAssertions(); @@ -60,6 +61,7 @@ test('connectionInfoFromEvent', async () => { try { const { connectionInfoFromEvent } = await import('@src/ws/utils'); + await stopWalletLibOpenHandles(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const connInfo = connectionInfoFromEvent(event); @@ -90,6 +92,7 @@ test('missing WS_DOMAIN should throw', async () => { try { const { connectionInfoFromEvent } = await import('@src/ws/utils'); + await stopWalletLibOpenHandles(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore expect(() => connectionInfoFromEvent(event)).toThrow('Domain not on env variables'); diff --git a/packages/wallet-service/tsconfig.json b/packages/wallet-service/tsconfig.json index f5098fa9..0cb0e837 100644 --- a/packages/wallet-service/tsconfig.json +++ b/packages/wallet-service/tsconfig.json @@ -1,13 +1,13 @@ { "compilerOptions": { - "lib": ["es2017"], + "lib": ["es2020", "esnext"], "removeComments": true, "moduleResolution": "node", "module": "commonjs", "noUnusedLocals": false, "noUnusedParameters": false, "sourceMap": true, - "target": "es2017", + "target": "es2020", "outDir": "./dist", "inlineSources": true, "esModuleInterop": true, diff --git a/scripts/build-daemon.sh b/scripts/build-daemon.sh index 1d8a29e9..8d185c02 100644 --- a/scripts/build-daemon.sh +++ b/scripts/build-daemon.sh @@ -1,11 +1,24 @@ set -e set -o pipefail +# ACCOUNT_ID is a mandatory env var because the script was built to be used in AWS environments, +# however it's no longer mandatory to publish it there. So we're keeping this variable temporarily and +# implementing a specific behavior in case it's set to "NONE". + if [ -z "$ACCOUNT_ID" ]; then echo "Please export a ACCOUNT_ID env var before running this"; + echo "Set it to NONE if you don't want to publish to AWS ECR"; exit 1; fi +# DYNAMIC_FULLNODE_IDS is an optional env var that, if set to "true", will make the daemon fetch the fullnode +# identifiers dynamically from the fullnode before starting the daemon. This is needed inside containerized private +# networks where the fullnode IDs are not known in advance. +SHOULD_FETCH_IDS=false +if [ "$DYNAMIC_FULLNODE_IDS" = "true" ]; then + SHOULD_FETCH_IDS=true +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 "") @@ -22,6 +35,34 @@ echo $DOCKER_IMAGE_TAG; # 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; +# Handling th Account ID +if [ "$ACCOUNT_ID" = "NONE" ]; then + echo "== Skipping AWS ECR login since ACCOUNT_ID is set to NONE"; + FINAL_TAG="hathornetwork/hathor-wallet-service-sync-daemon:dev"; + SHOULD_FETCH_IDS=true # Always fetch IDs when not using AWS ECR +else + aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.eu-central-1.amazonaws.com; + FINAL_TAG="$ACCOUNT_ID.dkr.ecr.eu-central-1.amazonaws.com/hathor-wallet-service-sync-daemon:$DOCKER_IMAGE_TAG" +fi; + +# Handling the dynamic fullnode IDs to decide the correct build target +if [ "$SHOULD_FETCH_IDS" = true ]; then + echo "== Building the daemon to fetch fullnode IDs dynamically"; + BUILD_TARGET="dev"; +else + echo "== Building the daemon with statically defined fullnode IDs"; + BUILD_TARGET="prod"; +fi; + +# Copying the correct .dockerignore file +cp packages/daemon/.dockerignore .dockerignore; + +# Fetching the daemon Dockerfile to build +docker build \ + -t $FINAL_TAG\ + -f packages/daemon/Dockerfile\ + --target $BUILD_TARGET \ + .; -docker build -t $ACCOUNT_ID.dkr.ecr.eu-central-1.amazonaws.com/hathor-wallet-service-sync-daemon:$DOCKER_IMAGE_TAG .; +# Removing the copied .dockerignore file to avoid confusion with other builds in this monorepo +rm .dockerignore diff --git a/scripts/build-migrator.sh b/scripts/build-migrator.sh new file mode 100755 index 00000000..05c7ea5c --- /dev/null +++ b/scripts/build-migrator.sh @@ -0,0 +1,25 @@ +set -e +set -o pipefail + +# The image will be tagged as latest by default +FINAL_TAG="hathornetwork/hathor-wallet-service-migrator:dev"; + +# Fetching the versions of the critical dependencies from package.json +# to ensure consistency +SEQUELIZE_VERSION=$(cat ./package.json | jq -r '.devDependencies["sequelize"]'); +echo "Using Sequelize version: $SEQUELIZE_VERSION"; + +SEQUELIZE_CLI_VERSION=$(cat ./package.json | jq -r '.devDependencies["sequelize-cli"]'); +echo "Using Sequelize-Cli version: $SEQUELIZE_CLI_VERSION"; + +MYSQL2_VERSION=$(cat ./package.json | jq -r '.devDependencies["mysql2"]'); +echo "Using MySql2 version: $MYSQL2_VERSION"; + +# Build the Docker image, passing the versions as build arguments +docker build \ + --build-arg SEQUELIZE_VERSION=$SEQUELIZE_VERSION \ + --build-arg SEQUELIZE_CLI_VERSION=$SEQUELIZE_CLI_VERSION \ + --build-arg MYSQL2_VERSION=$MYSQL2_VERSION \ + -t $FINAL_TAG \ + -f db/Dockerfile.dev \ + .; diff --git a/scripts/build-service.sh b/scripts/build-service.sh new file mode 100644 index 00000000..d280da6e --- /dev/null +++ b/scripts/build-service.sh @@ -0,0 +1,17 @@ +set -e +set -o pipefail + + +FINAL_TAG="hathornetwork/hathor-wallet-service-lambdas:dev"; + +# Copying the correct .dockerignore file +cp packages/wallet-service/.dockerignore .dockerignore; + +# Fetching the daemon Dockerfile to build +docker build \ + -t $FINAL_TAG\ + -f packages/wallet-service/Dockerfile.dev\ + .; + +# Removing the copied .dockerignore file to avoid confusion with other builds in this monorepo +rm .dockerignore diff --git a/scripts/fetch-fullnode-ids.js b/scripts/fetch-fullnode-ids.js new file mode 100644 index 00000000..3a9f5c41 --- /dev/null +++ b/scripts/fetch-fullnode-ids.js @@ -0,0 +1,125 @@ +// Copyright 2025 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. + +const WebSocket = require('ws'); +const fs = require('fs'); + +/** + * Please note that by default this URL assumes a Docker setup where the fullnode service + * is accessible via the hostname 'fullnode' on port 8080. + * It is possible to adjust this using the FULLNODE_WEBSOCKET_BASEURL environment variable. + * @type {string} + */ +const fullnodeBaseUrl = process.env.FULLNODE_WEBSOCKET_BASEURL || 'fullnode:8080'; + +/** + * Output file name. + * By default, it is 'export-identifiers.sh' in the current directory. + * This can be changed using the FULLNODE_IDENTIFIER_ENVS_FILE environment variable. + * @type {string} + */ +const outputFileName = process.env.FULLNODE_IDENTIFIER_ENVS_FILE || 'export-identifiers.sh'; + +/** + * Fetches identifiers from the fullnode WebSocket endpoint and writes them to an .env file. + * + * This is especially useful when first running a containerized Wallet Service Daemon + * ( see /packages/daemon/Dockerfile ). + */ +async function fetchFullnodeIds() { + const wsUrl = `ws://${fullnodeBaseUrl}/v1a/event_ws`; + const payload = { type: 'START_STREAM', window_size: 1 }; + + console.log('Connecting to fullnode WebSocket...'); + + return new Promise((resolve, reject) => { + const ws = new WebSocket(wsUrl); + + // Set a timeout for the connection + const timeout = setTimeout(() => { + ws.close(); + reject(new Error('WebSocket connection timeout')); + }, 3000); + + ws.on('open', () => { + console.log('WebSocket connected, sending payload...'); + ws.send(JSON.stringify(payload)); + }); + + ws.on('message', (data) => { + clearTimeout(timeout); + console.log('Received response from fullnode'); + + try { + const response = JSON.parse(data.toString()); + console.log('Parsed response:', JSON.stringify(response, null, 2)); + + // Extract the required fields + const streamId = response.stream_id; + const fullnodePeerId = response.peer_id; + + if (!streamId || !fullnodePeerId) { + throw new Error(`Missing required fields in response. stream_id: ${streamId}, peer_id: ${fullnodePeerId}`); + } + + // Create .env file content + const envContent = `export STREAM_ID=${streamId}\nexport FULLNODE_PEER_ID=${fullnodePeerId}\n`; + + // Write to output file + fs.writeFileSync(outputFileName, envContent); + console.warn(`Written identifiers to ${outputFileName}: ${envContent}`); + + ws.close(); + resolve({ streamId, fullnodePeerId }); + + } catch (error) { + ws.close(); + reject(new Error(`Failed to parse response: ${error.message}`)); + } + }); + + ws.on('error', (error) => { + clearTimeout(timeout); + console.error('WebSocket error:', error.message); + reject(new Error(`WebSocket connection failed: ${error.message}`)); + }); + + ws.on('close', (code, reason) => { + clearTimeout(timeout); + if (code !== 1000) { + console.error(`WebSocket closed with code ${code}, reason: ${reason}`); + reject(new Error(`WebSocket closed unexpectedly: ${code} ${reason}`)); + } + }); + }); +} + +/* + * In this script, all console output is directed to stderr except for the final + * .env content which is printed to stdout. This allows the script to be used + * in shell scripts that capture the output directly into environment variables. + */ + +// Main execution +if (require.main === module) { + fetchFullnodeIds() + .then(({ streamId, fullnodePeerId }) => { + console.log(`Successfully fetched identifiers. Exiting.`); + process.exit(0); + }) + .catch((error) => { + console.error('Failed to fetch identifiers with error:', error.message); + + // Create empty .env file as fallback + const fallbackContent = 'STREAM_ID=\nFULLNODE_PEER_ID=\n'; + fs.writeFileSync(outputFileName, fallbackContent); + + console.log(`Created ${outputFileName} with empty values due to error. Exiting.`); + process.exit(1); + }); +} + +module.exports = { fetchFullnodeIds }; diff --git a/scripts/merge-complementary-envs.sh b/scripts/merge-complementary-envs.sh new file mode 100755 index 00000000..6be38c89 --- /dev/null +++ b/scripts/merge-complementary-envs.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +# Copyright 2025 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. + +# ========================================================================= +# This script is meant to be run inside the Wallet Service Daemon container: if the environment variable +# FETCH_FULLNODE_IDS is set, it will fetch the dynamically created fullnode ids and add them to the current +# environment variables. This way, the Wallet Service can connect to the fullnode. +# +# Outside of this containerized scope, it's not advisable to dynamically obtain the fullnode ids, as it +# serves as an additional security layer. The recommended approach is to set the fullnode ids as +# environment variables directly, ensuring that only the trusted fullnode is used. + +set -e + +# Skip the dynamic fetching if the specific environment variable is not set +if [ -z "$FETCH_FULLNODE_IDS" ]; then + echo "FETCH_FULLNODE_IDS not set, skipping merge of complementary environment variables" + # No fetching needed, run the main script for the Wallet Service Daemon + node dist/index.js + exit 0 +fi + +echo "FETCH_FULLNODE_IDS is set, merging complementary environment variables..." +node fetch-fullnode-ids.js + +# Check if the identifiers env file exists +FULLNODE_IDENTIFIER_ENVS_FILE="${FULLNODE_IDENTIFIER_ENVS_FILE:-export-identifiers.sh}" +if [ ! -f "$FULLNODE_IDENTIFIER_ENVS_FILE" ]; then + echo "Warning: $FULLNODE_IDENTIFIER_ENVS_FILE file not found, skipping merge" + # No fetching needed, run the main script for the Wallet Service Daemon + node dist/index.js + exit 0 +fi + +# Export each environment variable from the identifiers env file +echo "Exporting environment variables from $FULLNODE_IDENTIFIER_ENVS_FILE file..." + +source "$FULLNODE_IDENTIFIER_ENVS_FILE" + +echo "Successfully merged and exported complementary environment variables" + +# Finally, run the main script for the Wallet Service Daemon +node dist/index.js diff --git a/scripts/migrate.sh b/scripts/migrate.sh new file mode 100755 index 00000000..757b8c3a --- /dev/null +++ b/scripts/migrate.sh @@ -0,0 +1,5 @@ +set -e +set -o pipefail + +echo "==== Starting migration script" +yarn sequelize db:migrate diff --git a/yarn.lock b/yarn.lock index 21b15ee1..972274fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -43,6 +43,28 @@ __metadata: languageName: node linkType: hard +"@aws-crypto/crc32@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/crc32@npm:5.2.0" + dependencies: + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + tslib: "npm:^2.6.2" + checksum: 10/1b0a56ad4cb44c9512d8b1668dcf9306ab541d3a73829f435ca97abaec8d56f3db953db03ad0d0698754fea16fcd803d11fa42e0889bc7b803c6a030b04c63de + languageName: node + linkType: hard + +"@aws-crypto/crc32c@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/crc32c@npm:5.2.0" + dependencies: + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + tslib: "npm:^2.6.2" + checksum: 10/08bd1db17d7c772fa6e34b38a360ce77ad041164743113eefa8343c2af917a419697daf090c5854129ef19f3a9673ed1fd8446e03eb32c8ed52d2cc409b0dee7 + 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" @@ -52,6 +74,20 @@ __metadata: languageName: node linkType: hard +"@aws-crypto/sha1-browser@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha1-browser@npm:5.2.0" + dependencies: + "@aws-crypto/supports-web-crypto": "npm:^5.2.0" + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + "@aws-sdk/util-locate-window": "npm:^3.0.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.6.2" + checksum: 10/239f4c59cce9abd33c01117b10553fbef868a063e74faf17edb798c250d759a2578841efa2837e5e51854f52ef57dbc40780b073cae20f89ebed6a8cc7fa06f1 + 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" @@ -68,6 +104,21 @@ __metadata: languageName: node linkType: hard +"@aws-crypto/sha256-browser@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha256-browser@npm:5.2.0" + dependencies: + "@aws-crypto/sha256-js": "npm:^5.2.0" + "@aws-crypto/supports-web-crypto": "npm:^5.2.0" + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + "@aws-sdk/util-locate-window": "npm:^3.0.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.6.2" + checksum: 10/2b1b701ca6caa876333b4eb2b96e5187d71ebb51ebf8e2d632690dbcdedeff038202d23adcc97e023437ed42bb1963b7b463e343687edf0635fd4b98b2edad1a + 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" @@ -79,6 +130,17 @@ __metadata: languageName: node linkType: hard +"@aws-crypto/sha256-js@npm:5.2.0, @aws-crypto/sha256-js@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha256-js@npm:5.2.0" + dependencies: + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + tslib: "npm:^2.6.2" + checksum: 10/f46aace7b873c615be4e787ab0efd0148ef7de48f9f12c7d043e05c52e52b75bb0bf6dbcb9b2852d940d7724fab7b6d5ff1469160a3dd024efe7a68b5f70df8c + 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" @@ -88,6 +150,26 @@ __metadata: languageName: node linkType: hard +"@aws-crypto/supports-web-crypto@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/supports-web-crypto@npm:5.2.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/6ed0c7e17f4f6663d057630805c45edb35d5693380c24ab52d4c453ece303c6c8a6ade9ee93c97dda77d9f6cae376ffbb44467057161c513dffa3422250edaf5 + languageName: node + linkType: hard + +"@aws-crypto/util@npm:5.2.0, @aws-crypto/util@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/util@npm:5.2.0" + dependencies: + "@aws-sdk/types": "npm:^3.222.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.6.2" + checksum: 10/f80a174c404e1ad4364741c942f440e75f834c08278fa754349fe23a6edc679d480ea9ced5820774aee58091ed270067022d8059ecf1a7ef452d58134ac7e9e1 + languageName: node + linkType: hard + "@aws-crypto/util@npm:^3.0.0": version: 3.0.0 resolution: "@aws-crypto/util@npm:3.0.0" @@ -99,6 +181,55 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/client-api-gateway@npm:^3.588.0": + version: 3.835.0 + resolution: "@aws-sdk/client-api-gateway@npm:3.835.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/credential-provider-node": "npm:3.835.0" + "@aws-sdk/middleware-host-header": "npm:3.821.0" + "@aws-sdk/middleware-logger": "npm:3.821.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.821.0" + "@aws-sdk/middleware-sdk-api-gateway": "npm:3.821.0" + "@aws-sdk/middleware-user-agent": "npm:3.835.0" + "@aws-sdk/region-config-resolver": "npm:3.821.0" + "@aws-sdk/types": "npm:3.821.0" + "@aws-sdk/util-endpoints": "npm:3.828.0" + "@aws-sdk/util-user-agent-browser": "npm:3.821.0" + "@aws-sdk/util-user-agent-node": "npm:3.835.0" + "@smithy/config-resolver": "npm:^4.1.4" + "@smithy/core": "npm:^3.5.3" + "@smithy/fetch-http-handler": "npm:^5.0.4" + "@smithy/hash-node": "npm:^4.0.4" + "@smithy/invalid-dependency": "npm:^4.0.4" + "@smithy/middleware-content-length": "npm:^4.0.4" + "@smithy/middleware-endpoint": "npm:^4.1.12" + "@smithy/middleware-retry": "npm:^4.1.13" + "@smithy/middleware-serde": "npm:^4.0.8" + "@smithy/middleware-stack": "npm:^4.0.4" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/node-http-handler": "npm:^4.0.6" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/smithy-client": "npm:^4.4.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/url-parser": "npm:^4.0.4" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-body-length-node": "npm:^4.0.0" + "@smithy/util-defaults-mode-browser": "npm:^4.0.20" + "@smithy/util-defaults-mode-node": "npm:^4.0.20" + "@smithy/util-endpoints": "npm:^3.0.6" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-retry": "npm:^4.0.6" + "@smithy/util-stream": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/98f81ab2e6704aee280b5088f19963c96a368afa69d1463fa6cd96837e29dde8997897e0a9a2c3d0d8a552691e0a4a9d41e9ae9f715defccfee161b640440723 + languageName: node + linkType: hard + "@aws-sdk/client-apigatewaymanagementapi@npm:3.540.0": version: 3.540.0 resolution: "@aws-sdk/client-apigatewaymanagementapi@npm:3.540.0" @@ -195,6 +326,149 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/client-cognito-identity-provider@npm:^3.588.0": + version: 3.835.0 + resolution: "@aws-sdk/client-cognito-identity-provider@npm:3.835.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/credential-provider-node": "npm:3.835.0" + "@aws-sdk/middleware-host-header": "npm:3.821.0" + "@aws-sdk/middleware-logger": "npm:3.821.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.821.0" + "@aws-sdk/middleware-user-agent": "npm:3.835.0" + "@aws-sdk/region-config-resolver": "npm:3.821.0" + "@aws-sdk/types": "npm:3.821.0" + "@aws-sdk/util-endpoints": "npm:3.828.0" + "@aws-sdk/util-user-agent-browser": "npm:3.821.0" + "@aws-sdk/util-user-agent-node": "npm:3.835.0" + "@smithy/config-resolver": "npm:^4.1.4" + "@smithy/core": "npm:^3.5.3" + "@smithy/fetch-http-handler": "npm:^5.0.4" + "@smithy/hash-node": "npm:^4.0.4" + "@smithy/invalid-dependency": "npm:^4.0.4" + "@smithy/middleware-content-length": "npm:^4.0.4" + "@smithy/middleware-endpoint": "npm:^4.1.12" + "@smithy/middleware-retry": "npm:^4.1.13" + "@smithy/middleware-serde": "npm:^4.0.8" + "@smithy/middleware-stack": "npm:^4.0.4" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/node-http-handler": "npm:^4.0.6" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/smithy-client": "npm:^4.4.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/url-parser": "npm:^4.0.4" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-body-length-node": "npm:^4.0.0" + "@smithy/util-defaults-mode-browser": "npm:^4.0.20" + "@smithy/util-defaults-mode-node": "npm:^4.0.20" + "@smithy/util-endpoints": "npm:^3.0.6" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-retry": "npm:^4.0.6" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/352cf0bc52a8ca9fdde71db05b96adbf9cd158c5c3ca2677f9acf223f20d264e47f852831992f5e35f72c4a8f416ae686086782d99f89410f4d0c487bca6568d + languageName: node + linkType: hard + +"@aws-sdk/client-eventbridge@npm:^3.588.0": + version: 3.835.0 + resolution: "@aws-sdk/client-eventbridge@npm:3.835.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/credential-provider-node": "npm:3.835.0" + "@aws-sdk/middleware-host-header": "npm:3.821.0" + "@aws-sdk/middleware-logger": "npm:3.821.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.821.0" + "@aws-sdk/middleware-user-agent": "npm:3.835.0" + "@aws-sdk/region-config-resolver": "npm:3.821.0" + "@aws-sdk/signature-v4-multi-region": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@aws-sdk/util-endpoints": "npm:3.828.0" + "@aws-sdk/util-user-agent-browser": "npm:3.821.0" + "@aws-sdk/util-user-agent-node": "npm:3.835.0" + "@smithy/config-resolver": "npm:^4.1.4" + "@smithy/core": "npm:^3.5.3" + "@smithy/fetch-http-handler": "npm:^5.0.4" + "@smithy/hash-node": "npm:^4.0.4" + "@smithy/invalid-dependency": "npm:^4.0.4" + "@smithy/middleware-content-length": "npm:^4.0.4" + "@smithy/middleware-endpoint": "npm:^4.1.12" + "@smithy/middleware-retry": "npm:^4.1.13" + "@smithy/middleware-serde": "npm:^4.0.8" + "@smithy/middleware-stack": "npm:^4.0.4" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/node-http-handler": "npm:^4.0.6" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/smithy-client": "npm:^4.4.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/url-parser": "npm:^4.0.4" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-body-length-node": "npm:^4.0.0" + "@smithy/util-defaults-mode-browser": "npm:^4.0.20" + "@smithy/util-defaults-mode-node": "npm:^4.0.20" + "@smithy/util-endpoints": "npm:^3.0.6" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-retry": "npm:^4.0.6" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/22ee597e86edfbfcfe4a22dfaf7258280ea95d109e7a7db4306e63a3940cb8f53f6fbddc9a6fde9fdad9321bcc528b799889c72a4a53bd0147c8019e93c91f25 + languageName: node + linkType: hard + +"@aws-sdk/client-iam@npm:^3.588.0": + version: 3.835.0 + resolution: "@aws-sdk/client-iam@npm:3.835.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/credential-provider-node": "npm:3.835.0" + "@aws-sdk/middleware-host-header": "npm:3.821.0" + "@aws-sdk/middleware-logger": "npm:3.821.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.821.0" + "@aws-sdk/middleware-user-agent": "npm:3.835.0" + "@aws-sdk/region-config-resolver": "npm:3.821.0" + "@aws-sdk/types": "npm:3.821.0" + "@aws-sdk/util-endpoints": "npm:3.828.0" + "@aws-sdk/util-user-agent-browser": "npm:3.821.0" + "@aws-sdk/util-user-agent-node": "npm:3.835.0" + "@smithy/config-resolver": "npm:^4.1.4" + "@smithy/core": "npm:^3.5.3" + "@smithy/fetch-http-handler": "npm:^5.0.4" + "@smithy/hash-node": "npm:^4.0.4" + "@smithy/invalid-dependency": "npm:^4.0.4" + "@smithy/middleware-content-length": "npm:^4.0.4" + "@smithy/middleware-endpoint": "npm:^4.1.12" + "@smithy/middleware-retry": "npm:^4.1.13" + "@smithy/middleware-serde": "npm:^4.0.8" + "@smithy/middleware-stack": "npm:^4.0.4" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/node-http-handler": "npm:^4.0.6" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/smithy-client": "npm:^4.4.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/url-parser": "npm:^4.0.4" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-body-length-node": "npm:^4.0.0" + "@smithy/util-defaults-mode-browser": "npm:^4.0.20" + "@smithy/util-defaults-mode-node": "npm:^4.0.20" + "@smithy/util-endpoints": "npm:^3.0.6" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-retry": "npm:^4.0.6" + "@smithy/util-utf8": "npm:^4.0.0" + "@smithy/util-waiter": "npm:^4.0.5" + tslib: "npm:^2.6.2" + checksum: 10/1b2b6a66a118e5c2f1a758d25f86bcb4ba6eedd77da6e93b824023035d2d0bfb582c995b0f840e4d2286dc565a9deec4570025614a4332421ee0e2a29d9b6df0 + languageName: node + linkType: hard + "@aws-sdk/client-lambda@npm:3.540.0": version: 3.540.0 resolution: "@aws-sdk/client-lambda@npm:3.540.0" @@ -248,53 +522,173 @@ __metadata: 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 +"@aws-sdk/client-lambda@npm:^3.588.0": + version: 3.835.0 + resolution: "@aws-sdk/client-lambda@npm:3.835.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/credential-provider-node": "npm:3.835.0" + "@aws-sdk/middleware-host-header": "npm:3.821.0" + "@aws-sdk/middleware-logger": "npm:3.821.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.821.0" + "@aws-sdk/middleware-user-agent": "npm:3.835.0" + "@aws-sdk/region-config-resolver": "npm:3.821.0" + "@aws-sdk/types": "npm:3.821.0" + "@aws-sdk/util-endpoints": "npm:3.828.0" + "@aws-sdk/util-user-agent-browser": "npm:3.821.0" + "@aws-sdk/util-user-agent-node": "npm:3.835.0" + "@smithy/config-resolver": "npm:^4.1.4" + "@smithy/core": "npm:^3.5.3" + "@smithy/eventstream-serde-browser": "npm:^4.0.4" + "@smithy/eventstream-serde-config-resolver": "npm:^4.1.2" + "@smithy/eventstream-serde-node": "npm:^4.0.4" + "@smithy/fetch-http-handler": "npm:^5.0.4" + "@smithy/hash-node": "npm:^4.0.4" + "@smithy/invalid-dependency": "npm:^4.0.4" + "@smithy/middleware-content-length": "npm:^4.0.4" + "@smithy/middleware-endpoint": "npm:^4.1.12" + "@smithy/middleware-retry": "npm:^4.1.13" + "@smithy/middleware-serde": "npm:^4.0.8" + "@smithy/middleware-stack": "npm:^4.0.4" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/node-http-handler": "npm:^4.0.6" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/smithy-client": "npm:^4.4.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/url-parser": "npm:^4.0.4" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-body-length-node": "npm:^4.0.0" + "@smithy/util-defaults-mode-browser": "npm:^4.0.20" + "@smithy/util-defaults-mode-node": "npm:^4.0.20" + "@smithy/util-endpoints": "npm:^3.0.6" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-retry": "npm:^4.0.6" + "@smithy/util-stream": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.0.0" + "@smithy/util-waiter": "npm:^4.0.5" + tslib: "npm:^2.6.2" + checksum: 10/909b8287e5f614cc523ce23400467b6cf05adb3022a474202497b0f3fe4a6ed9e018aa38a5ff3fc46f0ab08dcfffa50becebe3823541fb57b6cfe7586f34a1fb + languageName: node + linkType: hard + +"@aws-sdk/client-lambda@npm:^3.636.0": + version: 3.865.0 + resolution: "@aws-sdk/client-lambda@npm:3.865.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.864.0" + "@aws-sdk/credential-provider-node": "npm:3.864.0" + "@aws-sdk/middleware-host-header": "npm:3.862.0" + "@aws-sdk/middleware-logger": "npm:3.862.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.862.0" + "@aws-sdk/middleware-user-agent": "npm:3.864.0" + "@aws-sdk/region-config-resolver": "npm:3.862.0" + "@aws-sdk/types": "npm:3.862.0" + "@aws-sdk/util-endpoints": "npm:3.862.0" + "@aws-sdk/util-user-agent-browser": "npm:3.862.0" + "@aws-sdk/util-user-agent-node": "npm:3.864.0" + "@smithy/config-resolver": "npm:^4.1.5" + "@smithy/core": "npm:^3.8.0" + "@smithy/eventstream-serde-browser": "npm:^4.0.5" + "@smithy/eventstream-serde-config-resolver": "npm:^4.1.3" + "@smithy/eventstream-serde-node": "npm:^4.0.5" + "@smithy/fetch-http-handler": "npm:^5.1.1" + "@smithy/hash-node": "npm:^4.0.5" + "@smithy/invalid-dependency": "npm:^4.0.5" + "@smithy/middleware-content-length": "npm:^4.0.5" + "@smithy/middleware-endpoint": "npm:^4.1.18" + "@smithy/middleware-retry": "npm:^4.1.19" + "@smithy/middleware-serde": "npm:^4.0.9" + "@smithy/middleware-stack": "npm:^4.0.5" + "@smithy/node-config-provider": "npm:^4.1.4" + "@smithy/node-http-handler": "npm:^4.1.1" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/smithy-client": "npm:^4.4.10" + "@smithy/types": "npm:^4.3.2" + "@smithy/url-parser": "npm:^4.0.5" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-body-length-node": "npm:^4.0.0" + "@smithy/util-defaults-mode-browser": "npm:^4.0.26" + "@smithy/util-defaults-mode-node": "npm:^4.0.26" + "@smithy/util-endpoints": "npm:^3.0.7" + "@smithy/util-middleware": "npm:^4.0.5" + "@smithy/util-retry": "npm:^4.0.7" + "@smithy/util-stream": "npm:^4.2.4" + "@smithy/util-utf8": "npm:^4.0.0" + "@smithy/util-waiter": "npm:^4.0.7" + tslib: "npm:^2.6.2" + checksum: 10/0f9e6c04d4ebb5e69358f5e26eee438da504eeb1af0efe3e705c1e94efec93d35e176a8eaa3bba07cbac32f190d189c74e93d71fac87797dfbfaf2b13ab50bde + languageName: node + linkType: hard + +"@aws-sdk/client-s3@npm:^3.588.0": + version: 3.837.0 + resolution: "@aws-sdk/client-s3@npm:3.837.0" + dependencies: + "@aws-crypto/sha1-browser": "npm:5.2.0" + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/credential-provider-node": "npm:3.835.0" + "@aws-sdk/middleware-bucket-endpoint": "npm:3.830.0" + "@aws-sdk/middleware-expect-continue": "npm:3.821.0" + "@aws-sdk/middleware-flexible-checksums": "npm:3.835.0" + "@aws-sdk/middleware-host-header": "npm:3.821.0" + "@aws-sdk/middleware-location-constraint": "npm:3.821.0" + "@aws-sdk/middleware-logger": "npm:3.821.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.821.0" + "@aws-sdk/middleware-sdk-s3": "npm:3.835.0" + "@aws-sdk/middleware-ssec": "npm:3.821.0" + "@aws-sdk/middleware-user-agent": "npm:3.835.0" + "@aws-sdk/region-config-resolver": "npm:3.821.0" + "@aws-sdk/signature-v4-multi-region": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@aws-sdk/util-endpoints": "npm:3.828.0" + "@aws-sdk/util-user-agent-browser": "npm:3.821.0" + "@aws-sdk/util-user-agent-node": "npm:3.835.0" + "@aws-sdk/xml-builder": "npm:3.821.0" + "@smithy/config-resolver": "npm:^4.1.4" + "@smithy/core": "npm:^3.5.3" + "@smithy/eventstream-serde-browser": "npm:^4.0.4" + "@smithy/eventstream-serde-config-resolver": "npm:^4.1.2" + "@smithy/eventstream-serde-node": "npm:^4.0.4" + "@smithy/fetch-http-handler": "npm:^5.0.4" + "@smithy/hash-blob-browser": "npm:^4.0.4" + "@smithy/hash-node": "npm:^4.0.4" + "@smithy/hash-stream-node": "npm:^4.0.4" + "@smithy/invalid-dependency": "npm:^4.0.4" + "@smithy/md5-js": "npm:^4.0.4" + "@smithy/middleware-content-length": "npm:^4.0.4" + "@smithy/middleware-endpoint": "npm:^4.1.12" + "@smithy/middleware-retry": "npm:^4.1.13" + "@smithy/middleware-serde": "npm:^4.0.8" + "@smithy/middleware-stack": "npm:^4.0.4" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/node-http-handler": "npm:^4.0.6" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/smithy-client": "npm:^4.4.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/url-parser": "npm:^4.0.4" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-body-length-node": "npm:^4.0.0" + "@smithy/util-defaults-mode-browser": "npm:^4.0.20" + "@smithy/util-defaults-mode-node": "npm:^4.0.20" + "@smithy/util-endpoints": "npm:^3.0.6" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-retry": "npm:^4.0.6" + "@smithy/util-stream": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.0.0" + "@smithy/util-waiter": "npm:^4.0.5" + "@types/uuid": "npm:^9.0.1" + tslib: "npm:^2.6.2" + uuid: "npm:^9.0.1" + checksum: 10/d2140bd13e65cf62ed6dc14c2dd15fa6dbca25dc3c2ebbed99fc964738c205a0236d16a116ace0f2dc991f615ba90674b69319d923bd165ced12183cad6018e1 languageName: node linkType: hard @@ -485,6 +879,98 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/client-sso@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/client-sso@npm:3.835.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/middleware-host-header": "npm:3.821.0" + "@aws-sdk/middleware-logger": "npm:3.821.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.821.0" + "@aws-sdk/middleware-user-agent": "npm:3.835.0" + "@aws-sdk/region-config-resolver": "npm:3.821.0" + "@aws-sdk/types": "npm:3.821.0" + "@aws-sdk/util-endpoints": "npm:3.828.0" + "@aws-sdk/util-user-agent-browser": "npm:3.821.0" + "@aws-sdk/util-user-agent-node": "npm:3.835.0" + "@smithy/config-resolver": "npm:^4.1.4" + "@smithy/core": "npm:^3.5.3" + "@smithy/fetch-http-handler": "npm:^5.0.4" + "@smithy/hash-node": "npm:^4.0.4" + "@smithy/invalid-dependency": "npm:^4.0.4" + "@smithy/middleware-content-length": "npm:^4.0.4" + "@smithy/middleware-endpoint": "npm:^4.1.12" + "@smithy/middleware-retry": "npm:^4.1.13" + "@smithy/middleware-serde": "npm:^4.0.8" + "@smithy/middleware-stack": "npm:^4.0.4" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/node-http-handler": "npm:^4.0.6" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/smithy-client": "npm:^4.4.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/url-parser": "npm:^4.0.4" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-body-length-node": "npm:^4.0.0" + "@smithy/util-defaults-mode-browser": "npm:^4.0.20" + "@smithy/util-defaults-mode-node": "npm:^4.0.20" + "@smithy/util-endpoints": "npm:^3.0.6" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-retry": "npm:^4.0.6" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/81541ffc1cbe876eb2f90c6361ada7b9f188959e376ead2f9f4b810a4e54b68c2de61f4d7067fa9935189a050f64cf47d1fb99bd81c743cc7bb834a28f76fc22 + languageName: node + linkType: hard + +"@aws-sdk/client-sso@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/client-sso@npm:3.864.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.864.0" + "@aws-sdk/middleware-host-header": "npm:3.862.0" + "@aws-sdk/middleware-logger": "npm:3.862.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.862.0" + "@aws-sdk/middleware-user-agent": "npm:3.864.0" + "@aws-sdk/region-config-resolver": "npm:3.862.0" + "@aws-sdk/types": "npm:3.862.0" + "@aws-sdk/util-endpoints": "npm:3.862.0" + "@aws-sdk/util-user-agent-browser": "npm:3.862.0" + "@aws-sdk/util-user-agent-node": "npm:3.864.0" + "@smithy/config-resolver": "npm:^4.1.5" + "@smithy/core": "npm:^3.8.0" + "@smithy/fetch-http-handler": "npm:^5.1.1" + "@smithy/hash-node": "npm:^4.0.5" + "@smithy/invalid-dependency": "npm:^4.0.5" + "@smithy/middleware-content-length": "npm:^4.0.5" + "@smithy/middleware-endpoint": "npm:^4.1.18" + "@smithy/middleware-retry": "npm:^4.1.19" + "@smithy/middleware-serde": "npm:^4.0.9" + "@smithy/middleware-stack": "npm:^4.0.5" + "@smithy/node-config-provider": "npm:^4.1.4" + "@smithy/node-http-handler": "npm:^4.1.1" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/smithy-client": "npm:^4.4.10" + "@smithy/types": "npm:^4.3.2" + "@smithy/url-parser": "npm:^4.0.5" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-body-length-node": "npm:^4.0.0" + "@smithy/util-defaults-mode-browser": "npm:^4.0.26" + "@smithy/util-defaults-mode-node": "npm:^4.0.26" + "@smithy/util-endpoints": "npm:^3.0.7" + "@smithy/util-middleware": "npm:^4.0.5" + "@smithy/util-retry": "npm:^4.0.7" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/77bf9ec221976d878b053b5731f6d829757dba4bb9376ba40e068352464c4a3508b9a6622b59da7554084f88ec17d68ae98344d0cd76b30cd2fa1161de578d86 + 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" @@ -594,6 +1080,52 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/core@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/core@npm:3.835.0" + dependencies: + "@aws-sdk/types": "npm:3.821.0" + "@aws-sdk/xml-builder": "npm:3.821.0" + "@smithy/core": "npm:^3.5.3" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/signature-v4": "npm:^5.1.2" + "@smithy/smithy-client": "npm:^4.4.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-utf8": "npm:^4.0.0" + fast-xml-parser: "npm:4.4.1" + tslib: "npm:^2.6.2" + checksum: 10/71ebffbea621633092ca54fa4ab7d88b8dd5952959d6cacd5afff58768198727497bb1be118b705ab43487429c9e9a967befa337ef87f2fb24a993469d5e27f9 + languageName: node + linkType: hard + +"@aws-sdk/core@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/core@npm:3.864.0" + dependencies: + "@aws-sdk/types": "npm:3.862.0" + "@aws-sdk/xml-builder": "npm:3.862.0" + "@smithy/core": "npm:^3.8.0" + "@smithy/node-config-provider": "npm:^4.1.4" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/signature-v4": "npm:^5.1.3" + "@smithy/smithy-client": "npm:^4.4.10" + "@smithy/types": "npm:^4.3.2" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.0.5" + "@smithy/util-utf8": "npm:^4.0.0" + fast-xml-parser: "npm:5.2.5" + tslib: "npm:^2.6.2" + checksum: 10/298750f09e54c241a6d0db4e9c836fe352fc0e184d9f08a769efd3817d8982a08e0d312c3ce296565a6708bb7b721a8994c8eb5e8bfe589951da79b329a46c20 + 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" @@ -618,6 +1150,32 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-env@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.835.0" + dependencies: + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/094f7544e3d9bc9731d297e42695b2f0c84d7503fc59d424d755b7b18fce3f4692452595e1f78129e11be24dd923bfd10548131e5640894fa3409a3fe38f3c19 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-env@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.864.0" + dependencies: + "@aws-sdk/core": "npm:3.864.0" + "@aws-sdk/types": "npm:3.862.0" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/ec9439ef54872cebcee4a06495a70b2cfa0ce54215c94b41450d50d3096091d301aef25a4145e8788e60b9696b7907ad33735398c63c2e84f99a9c5e059db686 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-http@npm:3.535.0": version: 3.535.0 resolution: "@aws-sdk/credential-provider-http@npm:3.535.0" @@ -635,6 +1193,42 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-http@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/credential-provider-http@npm:3.835.0" + dependencies: + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@smithy/fetch-http-handler": "npm:^5.0.4" + "@smithy/node-http-handler": "npm:^4.0.6" + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/smithy-client": "npm:^4.4.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-stream": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/49c34f253bb38b5873cf20ac41f5004998f7f8e69c5ac81c470fb67b5675bc3585ced20d8c84e3438001d001c361bf034e8de1edcdd6c1a4d967cd8045a1bb74 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-http@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/credential-provider-http@npm:3.864.0" + dependencies: + "@aws-sdk/core": "npm:3.864.0" + "@aws-sdk/types": "npm:3.862.0" + "@smithy/fetch-http-handler": "npm:^5.1.1" + "@smithy/node-http-handler": "npm:^4.1.1" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/smithy-client": "npm:^4.4.10" + "@smithy/types": "npm:^4.3.2" + "@smithy/util-stream": "npm:^4.2.4" + tslib: "npm:^2.6.2" + checksum: 10/65f529449b07280df715f57e47cf20dc35f00a489cc2c3508507b44bef809457830a3d2ee8189d633f45766c1619bb0031e645f6a10dd8130947c380c7b7325a + 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" @@ -672,6 +1266,48 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-ini@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.835.0" + dependencies: + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/credential-provider-env": "npm:3.835.0" + "@aws-sdk/credential-provider-http": "npm:3.835.0" + "@aws-sdk/credential-provider-process": "npm:3.835.0" + "@aws-sdk/credential-provider-sso": "npm:3.835.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.835.0" + "@aws-sdk/nested-clients": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@smithy/credential-provider-imds": "npm:^4.0.6" + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/shared-ini-file-loader": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/fccd9afc2ae64ceb6deec837924bc25a502849c63841379514fe4781ca1cd39b1f80a93e2ad310de19099ba6c0e74891fda3ae81aebb7f0af307a46020046552 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-ini@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.864.0" + dependencies: + "@aws-sdk/core": "npm:3.864.0" + "@aws-sdk/credential-provider-env": "npm:3.864.0" + "@aws-sdk/credential-provider-http": "npm:3.864.0" + "@aws-sdk/credential-provider-process": "npm:3.864.0" + "@aws-sdk/credential-provider-sso": "npm:3.864.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.864.0" + "@aws-sdk/nested-clients": "npm:3.864.0" + "@aws-sdk/types": "npm:3.862.0" + "@smithy/credential-provider-imds": "npm:^4.0.7" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/shared-ini-file-loader": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/5b779614ea468d96767672c6f2f94fbe50e9252c954d935d1bd52e41308e896b720d0d5d84fbd289dc9ae32c26fd9179ed9f1bd541d5756002486ae4f3b03be3 + 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" @@ -711,6 +1347,46 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-node@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.835.0" + dependencies: + "@aws-sdk/credential-provider-env": "npm:3.835.0" + "@aws-sdk/credential-provider-http": "npm:3.835.0" + "@aws-sdk/credential-provider-ini": "npm:3.835.0" + "@aws-sdk/credential-provider-process": "npm:3.835.0" + "@aws-sdk/credential-provider-sso": "npm:3.835.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@smithy/credential-provider-imds": "npm:^4.0.6" + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/shared-ini-file-loader": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/30beae9e9e04034bb2dc67b94b8b514f14ababe0a515b8ed43f53fd2a98df012cf391261c708e5d7b8e0a2ae09115511b936f48573b557350c298e0ab7128685 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-node@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.864.0" + dependencies: + "@aws-sdk/credential-provider-env": "npm:3.864.0" + "@aws-sdk/credential-provider-http": "npm:3.864.0" + "@aws-sdk/credential-provider-ini": "npm:3.864.0" + "@aws-sdk/credential-provider-process": "npm:3.864.0" + "@aws-sdk/credential-provider-sso": "npm:3.864.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.864.0" + "@aws-sdk/types": "npm:3.862.0" + "@smithy/credential-provider-imds": "npm:^4.0.7" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/shared-ini-file-loader": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/e6e40d909495581e078bcec802588c192f7d38cb9e73b3b7e56c18b3418b7f5cddc0ba94bd5ec64705fe08f99a3a0ee6816279243d6b4a3ace0ca7d1e14976c5 + 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" @@ -737,6 +1413,34 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-process@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.835.0" + dependencies: + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/shared-ini-file-loader": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/86559818b7ba9cb8825c737b8fc02f017c4bab64df8a66a12bcc000794b38889a2ef4d526ac4f37c4b8319414b8f84290febf6f4eca4cc3066ffef83468b4f7d + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-process@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.864.0" + dependencies: + "@aws-sdk/core": "npm:3.864.0" + "@aws-sdk/types": "npm:3.862.0" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/shared-ini-file-loader": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/1cb789a7988841cd55ffcc81c239b9b1f50a00db9c86bbcbbb023b48be9f94332a694118dd03e2d90f3e9a401da513d1960de29e97ca89919d75b8bf38383c8f + 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" @@ -767,6 +1471,38 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-sso@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.835.0" + dependencies: + "@aws-sdk/client-sso": "npm:3.835.0" + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/token-providers": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/shared-ini-file-loader": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/ab4a030f2b881cae307289c6531a45a9cc625a9293f4f98dd0f3d2dc576fdeebd26caebd4269296802f5beb17264f4665685eea6781fbc01b117e656504fdaf6 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-sso@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.864.0" + dependencies: + "@aws-sdk/client-sso": "npm:3.864.0" + "@aws-sdk/core": "npm:3.864.0" + "@aws-sdk/token-providers": "npm:3.864.0" + "@aws-sdk/types": "npm:3.862.0" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/shared-ini-file-loader": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/f30b050083d788deaf481bf6154819401d08925003a88db7c98cc8c3f0d4e3aacdeb4bf70958b7d75f1fafaf8386dbb5671cb899d46424ab59a7e16d17bc6b5a + 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" @@ -792,6 +1528,82 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-web-identity@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.835.0" + dependencies: + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/nested-clients": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/d52e6c2488adcb467e0eb90f77b18897d6174b69aa02900d9a398abbec4efcc766854750e49d50e14469e4664d219776062f4b3cb1f6fe4f6378de5c59eac568 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-web-identity@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.864.0" + dependencies: + "@aws-sdk/core": "npm:3.864.0" + "@aws-sdk/nested-clients": "npm:3.864.0" + "@aws-sdk/types": "npm:3.862.0" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/524cd4c48552707bb8fc87d02c21a9a05b8be224c1a650ed79083cff6c1302bd6f16bfd6893cfbc8568314d724c67095612a452e79a2405f8147eae316b7b187 + languageName: node + linkType: hard + +"@aws-sdk/middleware-bucket-endpoint@npm:3.830.0": + version: 3.830.0 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.830.0" + dependencies: + "@aws-sdk/types": "npm:3.821.0" + "@aws-sdk/util-arn-parser": "npm:3.804.0" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-config-provider": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/e55195c9cb9d0b5d4f346047a2226fc23c8f3e6c345b8122d9d2ee293056046ee5ea5bf097d5d5c96a2daf9da027da551e580c5ec64fb295320c472db6ef8369 + languageName: node + linkType: hard + +"@aws-sdk/middleware-expect-continue@npm:3.821.0": + version: 3.821.0 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.821.0" + dependencies: + "@aws-sdk/types": "npm:3.821.0" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/629659ffa5d388b0963ad32b5d3788e526ceb01e28d562cb018c748736178ea2019a6a6e7eeaf7149e273772fee925c2aeafd6328b3f55bf81909cf9acd05d9d + languageName: node + linkType: hard + +"@aws-sdk/middleware-flexible-checksums@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.835.0" + dependencies: + "@aws-crypto/crc32": "npm:5.2.0" + "@aws-crypto/crc32c": "npm:5.2.0" + "@aws-crypto/util": "npm:5.2.0" + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@smithy/is-array-buffer": "npm:^4.0.0" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-stream": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/595e348e70fae11f85457946d39d6f077f16cdd3340bf22a433a52f8125f0325348d06a3b2e3a9e38c56536dc59f0acf9a094d72ea8b1af8d2b9499be52460da + 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" @@ -816,6 +1628,41 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-host-header@npm:3.821.0": + version: 3.821.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.821.0" + dependencies: + "@aws-sdk/types": "npm:3.821.0" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/20d9e7c1b0f3215de8ad6a85b2ad99a67674100e54fa6c207f1145b96a927282278ba1ca3f900c9d4c528d6925e4a4187d9daac9cf2c1359c5d4bb7c9469b5ce + languageName: node + linkType: hard + +"@aws-sdk/middleware-host-header@npm:3.862.0": + version: 3.862.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.862.0" + dependencies: + "@aws-sdk/types": "npm:3.862.0" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/c1847951c8d573f9411ac15fe83542d37f72fab0b09847b46a4de3bd4335c6f5537ba0cc226f7bcb1f0e30c613e2dc5bc36e71d4b2872f5b021118cc3e09abb1 + languageName: node + linkType: hard + +"@aws-sdk/middleware-location-constraint@npm:3.821.0": + version: 3.821.0 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.821.0" + dependencies: + "@aws-sdk/types": "npm:3.821.0" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/e4b75a4967111fac393a4211e4f86928a4e2c580b91e513a556a01e0a05a325e2ad30cafe0343a82a50aa790d7d0af36a5fcdc626f65619102ee3be829359914 + 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" @@ -838,6 +1685,28 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-logger@npm:3.821.0": + version: 3.821.0 + resolution: "@aws-sdk/middleware-logger@npm:3.821.0" + dependencies: + "@aws-sdk/types": "npm:3.821.0" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/803616fb81eb0d3694baf982ab9133011fc941f2571fdb47123cef5bbd7b9f427f813f2c84f1c71d46f4c60730f1c7ed1c3a23bac30f6736f9222f41b8d0efcf + languageName: node + linkType: hard + +"@aws-sdk/middleware-logger@npm:3.862.0": + version: 3.862.0 + resolution: "@aws-sdk/middleware-logger@npm:3.862.0" + dependencies: + "@aws-sdk/types": "npm:3.862.0" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/8b73ccf8bb66fc1b966354a10d8d868a16b1321075dfb61b9a5406683da7e4da9ba8959409a2261cfddeacd55c357afa92119c8e5b21f4e47cf840610942048a + 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" @@ -862,6 +1731,64 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-recursion-detection@npm:3.821.0": + version: 3.821.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.821.0" + dependencies: + "@aws-sdk/types": "npm:3.821.0" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/7d88950db384a197b5b6da0472a184fa08242a813ab033db8dc538310486c9d8fb40f6b7884bd9654d16309676ded50bb56b1061b0b9820413f70e3dae7250de + languageName: node + linkType: hard + +"@aws-sdk/middleware-recursion-detection@npm:3.862.0": + version: 3.862.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.862.0" + dependencies: + "@aws-sdk/types": "npm:3.862.0" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/3ed38bcfe62d8237ba79f022806482e755b50fe5cee996ea4873251ceae59a5e4ad6c9d43bd23a2a751411fa0809cdc5f9fbb44283fadd5581c9852e0e1f26ed + languageName: node + linkType: hard + +"@aws-sdk/middleware-sdk-api-gateway@npm:3.821.0": + version: 3.821.0 + resolution: "@aws-sdk/middleware-sdk-api-gateway@npm:3.821.0" + dependencies: + "@aws-sdk/types": "npm:3.821.0" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/2052b8e65a37ba0fdeb13ce5155e49ce3b3ad5bdd8f1e899bf2ef57eb2946b09585e4136c7f74006fc08840c6fe92b1c53823ba540be26e9de8e278ab89d097b + languageName: node + linkType: hard + +"@aws-sdk/middleware-sdk-s3@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.835.0" + dependencies: + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@aws-sdk/util-arn-parser": "npm:3.804.0" + "@smithy/core": "npm:^3.5.3" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/signature-v4": "npm:^5.1.2" + "@smithy/smithy-client": "npm:^4.4.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-config-provider": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-stream": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/44e20f57d3420cc1a52ab6b22a08ec77eef29dbc46db48b72c1a073af592b49f7df107146f8f8fcc818569794bc662699ee5f2acc6f26e0ce887fb65f0446dfe + languageName: node + linkType: hard + "@aws-sdk/middleware-sdk-sqs@npm:3.535.0": version: 3.535.0 resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.535.0" @@ -903,6 +1830,17 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-ssec@npm:3.821.0": + version: 3.821.0 + resolution: "@aws-sdk/middleware-ssec@npm:3.821.0" + dependencies: + "@aws-sdk/types": "npm:3.821.0" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/1f700e22e2b8d6af8f85d09d93fe2cd7be197d6b79a22550c7df575522cf29bd4ae4f17d00d37539205b33089c703efd2c5fdb65b702e24422fc15976a9dfcd1 + 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" @@ -929,6 +1867,128 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-user-agent@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.835.0" + dependencies: + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@aws-sdk/util-endpoints": "npm:3.828.0" + "@smithy/core": "npm:^3.5.3" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/b90ab28036c0dc1a2e2aa600d985387b293d00c4c811ce1d6bcae1daf9961a19865865408f7772ce991be180f6910c79fef5250636a105ea2476fdaa7ddf920d + languageName: node + linkType: hard + +"@aws-sdk/middleware-user-agent@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.864.0" + dependencies: + "@aws-sdk/core": "npm:3.864.0" + "@aws-sdk/types": "npm:3.862.0" + "@aws-sdk/util-endpoints": "npm:3.862.0" + "@smithy/core": "npm:^3.8.0" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/c7616eebc06b7c572cbe4277a45855ecce589106fbfe5f321d5bcc22996c03717a277e223e020987bae2dc1326ffed25f96543f7f599aa854e4835ae69561685 + languageName: node + linkType: hard + +"@aws-sdk/nested-clients@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/nested-clients@npm:3.835.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/middleware-host-header": "npm:3.821.0" + "@aws-sdk/middleware-logger": "npm:3.821.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.821.0" + "@aws-sdk/middleware-user-agent": "npm:3.835.0" + "@aws-sdk/region-config-resolver": "npm:3.821.0" + "@aws-sdk/types": "npm:3.821.0" + "@aws-sdk/util-endpoints": "npm:3.828.0" + "@aws-sdk/util-user-agent-browser": "npm:3.821.0" + "@aws-sdk/util-user-agent-node": "npm:3.835.0" + "@smithy/config-resolver": "npm:^4.1.4" + "@smithy/core": "npm:^3.5.3" + "@smithy/fetch-http-handler": "npm:^5.0.4" + "@smithy/hash-node": "npm:^4.0.4" + "@smithy/invalid-dependency": "npm:^4.0.4" + "@smithy/middleware-content-length": "npm:^4.0.4" + "@smithy/middleware-endpoint": "npm:^4.1.12" + "@smithy/middleware-retry": "npm:^4.1.13" + "@smithy/middleware-serde": "npm:^4.0.8" + "@smithy/middleware-stack": "npm:^4.0.4" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/node-http-handler": "npm:^4.0.6" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/smithy-client": "npm:^4.4.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/url-parser": "npm:^4.0.4" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-body-length-node": "npm:^4.0.0" + "@smithy/util-defaults-mode-browser": "npm:^4.0.20" + "@smithy/util-defaults-mode-node": "npm:^4.0.20" + "@smithy/util-endpoints": "npm:^3.0.6" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-retry": "npm:^4.0.6" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/4da257c3464c9fa5927ba910604a5df62729e9e831355510453ac66ab7341353e68585f66819f125ec55d60c4191a8c4c53822cbac4dd97fe3d8e9e49d399897 + languageName: node + linkType: hard + +"@aws-sdk/nested-clients@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/nested-clients@npm:3.864.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.864.0" + "@aws-sdk/middleware-host-header": "npm:3.862.0" + "@aws-sdk/middleware-logger": "npm:3.862.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.862.0" + "@aws-sdk/middleware-user-agent": "npm:3.864.0" + "@aws-sdk/region-config-resolver": "npm:3.862.0" + "@aws-sdk/types": "npm:3.862.0" + "@aws-sdk/util-endpoints": "npm:3.862.0" + "@aws-sdk/util-user-agent-browser": "npm:3.862.0" + "@aws-sdk/util-user-agent-node": "npm:3.864.0" + "@smithy/config-resolver": "npm:^4.1.5" + "@smithy/core": "npm:^3.8.0" + "@smithy/fetch-http-handler": "npm:^5.1.1" + "@smithy/hash-node": "npm:^4.0.5" + "@smithy/invalid-dependency": "npm:^4.0.5" + "@smithy/middleware-content-length": "npm:^4.0.5" + "@smithy/middleware-endpoint": "npm:^4.1.18" + "@smithy/middleware-retry": "npm:^4.1.19" + "@smithy/middleware-serde": "npm:^4.0.9" + "@smithy/middleware-stack": "npm:^4.0.5" + "@smithy/node-config-provider": "npm:^4.1.4" + "@smithy/node-http-handler": "npm:^4.1.1" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/smithy-client": "npm:^4.4.10" + "@smithy/types": "npm:^4.3.2" + "@smithy/url-parser": "npm:^4.0.5" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-body-length-node": "npm:^4.0.0" + "@smithy/util-defaults-mode-browser": "npm:^4.0.26" + "@smithy/util-defaults-mode-node": "npm:^4.0.26" + "@smithy/util-endpoints": "npm:^3.0.7" + "@smithy/util-middleware": "npm:^4.0.5" + "@smithy/util-retry": "npm:^4.0.7" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/3b3e3bcb19fda8b5387fc32fac47522a5867e39a6b3f39c7ea76d998a0fb114c6a2b291babcebe41e14d3fb3577696106e91c1c44d71213137058f5e7bc22ad3 + 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" @@ -956,11 +2016,53 @@ __metadata: 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" +"@aws-sdk/region-config-resolver@npm:3.821.0": + version: 3.821.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.821.0" dependencies: - "@aws-crypto/sha256-browser": "npm:3.0.0" + "@aws-sdk/types": "npm:3.821.0" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-config-provider": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.0.4" + tslib: "npm:^2.6.2" + checksum: 10/399e0ff3e478879bc9250125f59666a7bb9ca75146f109e17455094f65bef45e1ff18d41fd132ec9de79fc3940b3b6da6b9aa8776140dee2facd6e68449dc3f8 + languageName: node + linkType: hard + +"@aws-sdk/region-config-resolver@npm:3.862.0": + version: 3.862.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.862.0" + dependencies: + "@aws-sdk/types": "npm:3.862.0" + "@smithy/node-config-provider": "npm:^4.1.4" + "@smithy/types": "npm:^4.3.2" + "@smithy/util-config-provider": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.0.5" + tslib: "npm:^2.6.2" + checksum: 10/dbe5de1352e3e6329126238cca1f5e894e41039f5baff82a38e213015e8ebc195edd7623ef8ec708d73dd1f302d9da4a2dcaf9f23d9528b0def5648916d8cb74 + languageName: node + linkType: hard + +"@aws-sdk/signature-v4-multi-region@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.835.0" + dependencies: + "@aws-sdk/middleware-sdk-s3": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/signature-v4": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/1e9be6b6215eb12de6845fe734ea692abdc270cdbe4d04a68c33821b53db4645340b02c15e38101eec922e3095d3be3aa064ee5d3d06647aefd874b9b32c1521 + 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" @@ -1013,6 +2115,36 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/token-providers@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/token-providers@npm:3.835.0" + dependencies: + "@aws-sdk/core": "npm:3.835.0" + "@aws-sdk/nested-clients": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/shared-ini-file-loader": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/1b17749deb84405afc97f768e2223085d09ba44d3035076abd6ec20624d1e52083a1809e757179312794be35b4cb8ed318ff515b3517eb060efe1039d0eb6455 + languageName: node + linkType: hard + +"@aws-sdk/token-providers@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/token-providers@npm:3.864.0" + dependencies: + "@aws-sdk/core": "npm:3.864.0" + "@aws-sdk/nested-clients": "npm:3.864.0" + "@aws-sdk/types": "npm:3.862.0" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/shared-ini-file-loader": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/c9600468245072acea9149e2522e6b08a937778fb01501a28dffc86d341897c3af28e29973b47a72c4e4120e00d8613c8e978725b854388168569bbae0de2255 + 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" @@ -1033,6 +2165,35 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/types@npm:3.821.0": + version: 3.821.0 + resolution: "@aws-sdk/types@npm:3.821.0" + dependencies: + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/43642a3c702ef85028d11094c68d9083afdb7f925286ed44325a269fae630c46307c193418591ddc88e0616e59f56f09ce94e29316ae2a6634d9c574bf64335d + languageName: node + linkType: hard + +"@aws-sdk/types@npm:3.862.0": + version: 3.862.0 + resolution: "@aws-sdk/types@npm:3.862.0" + dependencies: + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/056d6712a4782d1c4c9d82d59980afacdaccecb80c64d9044e22a27b505b2e09dd9afca7127262a911ea71f88b0e0acab3d000133ed24bacc3b93175be306114 + languageName: node + linkType: hard + +"@aws-sdk/util-arn-parser@npm:3.804.0": + version: 3.804.0 + resolution: "@aws-sdk/util-arn-parser@npm:3.804.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/3a66cee522fd1de7693eaf9dd8c4bad9efdf0d42f1af86797c138af3f84d38e9e6e38f0dffd963a372a95c5e0de07c570f613c49bbf85f7e03cea6d985fdbe01 + 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" @@ -1055,6 +2216,31 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/util-endpoints@npm:3.828.0": + version: 3.828.0 + resolution: "@aws-sdk/util-endpoints@npm:3.828.0" + dependencies: + "@aws-sdk/types": "npm:3.821.0" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-endpoints": "npm:^3.0.6" + tslib: "npm:^2.6.2" + checksum: 10/e17fc59320f00c230b3f94f808efbd9ef84032fb4769e29b081ec827c2d42743e4903d2ddb3c1bea6810c3dd2cf66e0d7456653bc0aa768c70f52a2704414635 + languageName: node + linkType: hard + +"@aws-sdk/util-endpoints@npm:3.862.0": + version: 3.862.0 + resolution: "@aws-sdk/util-endpoints@npm:3.862.0" + dependencies: + "@aws-sdk/types": "npm:3.862.0" + "@smithy/types": "npm:^4.3.2" + "@smithy/url-parser": "npm:^4.0.5" + "@smithy/util-endpoints": "npm:^3.0.7" + tslib: "npm:^2.6.2" + checksum: 10/4043d87c0e458504392095b5e03047cd2dbadffd4ad49f86b54d4c862cd9f009639f61cad153eacc36204b28ac289e420f52b1c516f45f2efbe0952bf0517d8a + 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" @@ -1088,6 +2274,30 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/util-user-agent-browser@npm:3.821.0": + version: 3.821.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.821.0" + dependencies: + "@aws-sdk/types": "npm:3.821.0" + "@smithy/types": "npm:^4.3.1" + bowser: "npm:^2.11.0" + tslib: "npm:^2.6.2" + checksum: 10/e1b844dcd472c7e431448b8b5d72954e45d528e3bf5d26060b885565767cc89c1fb630545a8653e815891014f94d73dd46201460a6ee900cad87ad8c7d0597c0 + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-browser@npm:3.862.0": + version: 3.862.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.862.0" + dependencies: + "@aws-sdk/types": "npm:3.862.0" + "@smithy/types": "npm:^4.3.2" + bowser: "npm:^2.11.0" + tslib: "npm:^2.6.2" + checksum: 10/8133658cd0622eb48e2fff5d9100f059c37c752a6531f64e3b4e9d5ab32532eb5f22efa3eb2413c26f50df7b5337d10af4cad464c381e4931f532ecaccc4f2e9 + 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" @@ -1122,6 +2332,42 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/util-user-agent-node@npm:3.835.0": + version: 3.835.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.835.0" + dependencies: + "@aws-sdk/middleware-user-agent": "npm:3.835.0" + "@aws-sdk/types": "npm:3.821.0" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + peerDependencies: + aws-crt: ">=1.0.0" + peerDependenciesMeta: + aws-crt: + optional: true + checksum: 10/143fca2d1a23a76853344367512f430e7f7bef7c39c81b0fdee1ba21f07af5fc3082fc79440dd794f139a4d77c23054bc62998a47b012766461095726aa715a6 + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-node@npm:3.864.0": + version: 3.864.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.864.0" + dependencies: + "@aws-sdk/middleware-user-agent": "npm:3.864.0" + "@aws-sdk/types": "npm:3.862.0" + "@smithy/node-config-provider": "npm:^4.1.4" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + peerDependencies: + aws-crt: ">=1.0.0" + peerDependenciesMeta: + aws-crt: + optional: true + checksum: 10/f7434a7bcfb11af1f77ac2f4f4a76d6efd692f0522383ff15602c9cc0ace579a7dccef853143812bfb98ab01800bbef31913808e125ed879e7cd77701fcf212a + 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" @@ -1131,6 +2377,26 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/xml-builder@npm:3.821.0": + version: 3.821.0 + resolution: "@aws-sdk/xml-builder@npm:3.821.0" + dependencies: + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/03e646487bfc59e445670e00ac53fb50060f27821b0c67e9dacaf7ffc8adecd56a54a19267fa66f328e14ba87c77989be8b19c622a21fe22a246433753505a65 + languageName: node + linkType: hard + +"@aws-sdk/xml-builder@npm:3.862.0": + version: 3.862.0 + resolution: "@aws-sdk/xml-builder@npm:3.862.0" + dependencies: + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/96d5958ce5776814c7cebc1694d5bc4aafa230bf69df642239ab3b0c83e868ff12033e5f119b7f16de1ba5d4d79f9323056ddc06fa633381ee3f142dea4986a6 + 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" @@ -1558,6 +2824,188 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/aix-ppc64@npm:0.25.9" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/android-arm64@npm:0.25.9" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/android-arm@npm:0.25.9" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/android-x64@npm:0.25.9" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/darwin-arm64@npm:0.25.9" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/darwin-x64@npm:0.25.9" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/freebsd-arm64@npm:0.25.9" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/freebsd-x64@npm:0.25.9" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/linux-arm64@npm:0.25.9" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/linux-arm@npm:0.25.9" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/linux-ia32@npm:0.25.9" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/linux-loong64@npm:0.25.9" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/linux-mips64el@npm:0.25.9" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/linux-ppc64@npm:0.25.9" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/linux-riscv64@npm:0.25.9" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/linux-s390x@npm:0.25.9" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/linux-x64@npm:0.25.9" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-arm64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/netbsd-arm64@npm:0.25.9" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/netbsd-x64@npm:0.25.9" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/openbsd-arm64@npm:0.25.9" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/openbsd-x64@npm:0.25.9" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/openharmony-arm64@npm:0.25.9" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/sunos-x64@npm:0.25.9" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/win32-arm64@npm:0.25.9" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/win32-ia32@npm:0.25.9" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.25.9": + version: 0.25.9 + resolution: "@esbuild/win32-x64@npm:0.25.9" + conditions: os=win32 & cpu=x64 + 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" @@ -1817,13 +3265,13 @@ __metadata: languageName: node linkType: hard -"@hapi/accept@npm:^6.0.1": - version: 6.0.2 - resolution: "@hapi/accept@npm:6.0.2" +"@hapi/accept@npm:^6.0.3": + version: 6.0.3 + resolution: "@hapi/accept@npm:6.0.3" dependencies: "@hapi/boom": "npm:^10.0.1" "@hapi/hoek": "npm:^11.0.2" - checksum: 10/5511abf491f08a75863527c8eefe88b9508e608926a42b5d622309ab8c3937de857df1ade43fe0054a324bd539e3677ad20e2c28f0a688087b9d38f7f30d5096 + checksum: 10/40134e34c61093835f5bc9616d9a05cff378dd8a4cd9ef23cf21a8961fb2c2b4e2aca487204e3e9331e1f29d6345fadd7c20ef3023a9229a14e2e5980a5c7d2b languageName: node linkType: hard @@ -1864,6 +3312,16 @@ __metadata: languageName: node linkType: hard +"@hapi/bounce@npm:^3.0.2": + version: 3.0.2 + resolution: "@hapi/bounce@npm:3.0.2" + dependencies: + "@hapi/boom": "npm:^10.0.1" + "@hapi/hoek": "npm:^11.0.2" + checksum: 10/5278d41aa2dd4b10ff2ed9cccf41f1b4f663a4d25828fcc7bb0036c7bc99fd98d6e49bb893a16e341833fc515fb09a413445174ed3b837f95c486a0d260528ab + languageName: node + linkType: hard + "@hapi/bourne@npm:^3.0.0": version: 3.0.0 resolution: "@hapi/bourne@npm:3.0.0" @@ -1881,13 +3339,13 @@ __metadata: languageName: node linkType: hard -"@hapi/catbox-memory@npm:^6.0.1": - version: 6.0.1 - resolution: "@hapi/catbox-memory@npm:6.0.1" +"@hapi/catbox-memory@npm:^6.0.2": + version: 6.0.2 + resolution: "@hapi/catbox-memory@npm:6.0.2" dependencies: "@hapi/boom": "npm:^10.0.1" "@hapi/hoek": "npm:^11.0.2" - checksum: 10/e1876b066dcf3f0a1fc779490ef97e98b71f829cf70d263fe1b7958264e5f4b253ab10504b7f819d43a3cf83ff8bc26ea205f42b5f0b006af2bfd6a636cd40f9 + checksum: 10/5edf65ba73583e880c16c78cf9ec80c7016e08d042ebd2108b171e1b75f9d5a510411a140c8851312639aef7aa391414a0a3ebaf527d6dbdfa577f67f2d6e96b languageName: node linkType: hard @@ -1940,29 +3398,29 @@ __metadata: languageName: node linkType: hard -"@hapi/hapi@npm:^21.3.2": - version: 21.3.2 - resolution: "@hapi/hapi@npm:21.3.2" +"@hapi/hapi@npm:^21.3.10": + version: 21.4.3 + resolution: "@hapi/hapi@npm:21.4.3" dependencies: - "@hapi/accept": "npm:^6.0.1" + "@hapi/accept": "npm:^6.0.3" "@hapi/ammo": "npm:^6.0.1" "@hapi/boom": "npm:^10.0.1" - "@hapi/bounce": "npm:^3.0.1" + "@hapi/bounce": "npm:^3.0.2" "@hapi/call": "npm:^9.0.1" "@hapi/catbox": "npm:^12.1.1" - "@hapi/catbox-memory": "npm:^6.0.1" + "@hapi/catbox-memory": "npm:^6.0.2" "@hapi/heavy": "npm:^8.0.1" - "@hapi/hoek": "npm:^11.0.2" + "@hapi/hoek": "npm:^11.0.7" "@hapi/mimos": "npm:^7.0.1" - "@hapi/podium": "npm:^5.0.1" - "@hapi/shot": "npm:^6.0.1" + "@hapi/podium": "npm:^5.0.2" + "@hapi/shot": "npm:^6.0.2" "@hapi/somever": "npm:^4.1.1" - "@hapi/statehood": "npm:^8.1.1" - "@hapi/subtext": "npm:^8.1.0" + "@hapi/statehood": "npm:^8.2.0" + "@hapi/subtext": "npm:^8.1.1" "@hapi/teamwork": "npm:^6.0.0" - "@hapi/topo": "npm:^6.0.1" + "@hapi/topo": "npm:^6.0.2" "@hapi/validate": "npm:^2.0.1" - checksum: 10/202dca65873835eb15fc24b5afe3d8174be8ca673a6c9365f2d46d33ec09883cc7d37c98ff7bedcd381e8c761f35ddf4320127e384d44a4a82232c31d63e4330 + checksum: 10/f23bda02b4337331a6dce4cfa85ecbe877e5e8f81394ed09dde701b3ca7ff88108c9e92b2c1fd7d5c22a9ebd84a8a34278d93ee6ad46a16a834e1f0d207581e8 languageName: node linkType: hard @@ -1984,6 +3442,13 @@ __metadata: languageName: node linkType: hard +"@hapi/hoek@npm:^11.0.7": + version: 11.0.7 + resolution: "@hapi/hoek@npm:11.0.7" + checksum: 10/d0cbacf2edcedac1f06dd1d731c1f8c83ef32f2432630c4b1ebe119729cbf79e3da69fa3e5991f19d80e344f5d246b03b7e96998a032940e18bfda7ba2a56f0e + languageName: node + linkType: hard + "@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0": version: 9.3.0 resolution: "@hapi/hoek@npm:9.3.0" @@ -2037,7 +3502,7 @@ __metadata: languageName: node linkType: hard -"@hapi/podium@npm:^5.0.0, @hapi/podium@npm:^5.0.1": +"@hapi/podium@npm:^5.0.0": version: 5.0.1 resolution: "@hapi/podium@npm:5.0.1" dependencies: @@ -2048,13 +3513,24 @@ __metadata: languageName: node linkType: hard -"@hapi/shot@npm:^6.0.1": - version: 6.0.1 - resolution: "@hapi/shot@npm:6.0.1" +"@hapi/podium@npm:^5.0.2": + version: 5.0.2 + resolution: "@hapi/podium@npm:5.0.2" + dependencies: + "@hapi/hoek": "npm:^11.0.2" + "@hapi/teamwork": "npm:^6.0.0" + "@hapi/validate": "npm:^2.0.1" + checksum: 10/ed709e417f6095675b79c44384f084d5d53df592050127122c4ae241e1c8a42bda2c93ec699b2fc5203d2f0e76cd5b6119b5f5030e7a7a189f09cd20d59d51f5 + languageName: node + linkType: hard + +"@hapi/shot@npm:^6.0.2": + version: 6.0.2 + resolution: "@hapi/shot@npm:6.0.2" dependencies: "@hapi/hoek": "npm:^11.0.2" "@hapi/validate": "npm:^2.0.1" - checksum: 10/6eb387f9c676922c504b042671139aefa943e0460534179501e793e3658741f45be7fc0a45a4972dd2907ba05157a5a3f9b04c19b0f8de71239e2719744d5a43 + checksum: 10/8715f5759c5cc0ac4c5fdb3c932862cd257320a19cfeadaf78d16491515b19b0aa27df95b91273d31d99e1d20d7bf10ed77078f42c844793817365797e6bfdab languageName: node linkType: hard @@ -2068,9 +3544,9 @@ __metadata: languageName: node linkType: hard -"@hapi/statehood@npm:^8.1.1": - version: 8.1.1 - resolution: "@hapi/statehood@npm:8.1.1" +"@hapi/statehood@npm:^8.2.0": + version: 8.2.0 + resolution: "@hapi/statehood@npm:8.2.0" dependencies: "@hapi/boom": "npm:^10.0.1" "@hapi/bounce": "npm:^3.0.1" @@ -2079,13 +3555,13 @@ __metadata: "@hapi/hoek": "npm:^11.0.2" "@hapi/iron": "npm:^7.0.1" "@hapi/validate": "npm:^2.0.1" - checksum: 10/b8259b5470d88064da0f803d39d2ccd244894cd9c20c26f65299398a528eb3a9450c32cde250cb3499d3c80a2e1d9523c609c05658616fcc5be3a8d9f05bbbe1 + checksum: 10/d55129f8bd9fa65d2b5305c922506baed677a1f36a8a3be8c9005d7a9009154a8b783d94603c49f88f1f24b23dd9e9b262cb19be6a4dc6130679bd0a33d63a75 languageName: node linkType: hard -"@hapi/subtext@npm:^8.1.0": - version: 8.1.0 - resolution: "@hapi/subtext@npm:8.1.0" +"@hapi/subtext@npm:^8.1.1": + version: 8.1.1 + resolution: "@hapi/subtext@npm:8.1.1" dependencies: "@hapi/boom": "npm:^10.0.1" "@hapi/bourne": "npm:^3.0.0" @@ -2094,7 +3570,7 @@ __metadata: "@hapi/hoek": "npm:^11.0.2" "@hapi/pez": "npm:^6.1.0" "@hapi/wreck": "npm:^18.0.1" - checksum: 10/3f7bf0c689d67307fa4fd454ce491e210c610823386def3155422c0c45c7e0512429a14d8091f72e10b289ae4e6bf1d449f926945148b9e216238412e6aa6901 + checksum: 10/9ce8251d5ee273e475febf32730d2e77ee5f276dc594a991508ea59916cba9148a00610c0cf942008844a44d6e39e1070bf7be952a35b1599faf1f060bd70a92 languageName: node linkType: hard @@ -2114,7 +3590,7 @@ __metadata: languageName: node linkType: hard -"@hapi/topo@npm:^6.0.1": +"@hapi/topo@npm:^6.0.1, @hapi/topo@npm:^6.0.2": version: 6.0.2 resolution: "@hapi/topo@npm:6.0.2" dependencies: @@ -2160,21 +3636,21 @@ __metadata: languageName: node linkType: hard -"@hathor/wallet-lib@npm:1.15.0": - version: 1.15.0 - resolution: "@hathor/wallet-lib@npm:1.15.0" +"@hathor/wallet-lib@npm:2.8.3": + version: 2.8.3 + resolution: "@hathor/wallet-lib@npm:2.8.3" dependencies: - axios: "npm:1.7.2" + axios: "npm:1.7.7" bitcore-lib: "npm:8.25.10" bitcore-mnemonic: "npm:8.25.10" buffer: "npm:6.0.3" crypto-js: "npm:4.2.0" isomorphic-ws: "npm:5.0.0" lodash: "npm:4.17.21" - long: "npm:5.2.3" queue-microtask: "npm:1.2.3" ws: "npm:8.17.1" - checksum: 10/92babf6327fe589c60f71083fe00cf860e55209c148736bace0a685e504faa3192cf134f098a258d32fb7221398ab3d3f0b5bf9ed8db55d0819cfe28720d0cae + zod: "npm:3.23.8" + checksum: 10/a52b8f8de761a7abdfb206ed536b0fae21c3904cb6b55730cc4dc5c293874ffd89c91babf47536d040c20dc3e2dd542016dfc6bdba282a662e86c3e638291c26 languageName: node linkType: hard @@ -2561,6 +4037,24 @@ __metadata: languageName: node linkType: hard +"@jsep-plugin/assignment@npm:^1.3.0": + version: 1.3.0 + resolution: "@jsep-plugin/assignment@npm:1.3.0" + peerDependencies: + jsep: ^0.4.0||^1.0.0 + checksum: 10/0c93b703d84af95b4be9fb6c23fbdbe7c7b6985b41c98fd10386cd54686ed1eb751cb39f5d54abcb621e4da2a0900a3b2a852e5bf7f2d322b756db3b22e42a45 + languageName: node + linkType: hard + +"@jsep-plugin/regex@npm:^1.0.4": + version: 1.0.4 + resolution: "@jsep-plugin/regex@npm:1.0.4" + peerDependencies: + jsep: ^0.4.0||^1.0.0 + checksum: 10/0ea6ba81f03955972b762fd9fbc8e3fd7e1c1c12e52ce3d4366e23c0a63c8bff8528687b8b3d8f641cf9f626f8bf5a7841efcd31a2489fe967e1900e5738ee3a + languageName: node + linkType: hard + "@kwsites/file-exists@npm:^1.1.1": version: 1.1.1 resolution: "@kwsites/file-exists@npm:1.1.1" @@ -2764,14 +4258,14 @@ __metadata: languageName: node linkType: hard -"@serverless/dashboard-plugin@npm:^7.0.2": - version: 7.0.5 - resolution: "@serverless/dashboard-plugin@npm:7.0.5" +"@serverless/dashboard-plugin@npm:^7.2.0": + version: 7.2.3 + resolution: "@serverless/dashboard-plugin@npm:7.2.3" 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/platform-client": "npm:^4.5.1" "@serverless/utils": "npm:^6.14.0" child-process-ext: "npm:^3.0.1" chokidar: "npm:^3.5.3" @@ -2791,7 +4285,7 @@ __metadata: type: "npm:^2.7.2" uuid: "npm:^8.3.2" yamljs: "npm:^0.3.0" - checksum: 10/f6d3c75ab09936254c23db697fcefa07cc5724b79b544a85c1f15ae1a8975f2e0d03e579239aa66f09107238833ab2ed1a587ee9d9dcd2644071a88f3b8e7eb1 + checksum: 10/c6f569249e2be6ae4b2974944df0f7f21961b359aefe4050c25af61c19e75f6ab51ea217d50ec0c7d00c5f3add140df47d4407165f2ddbb62f7535b91de60c69 languageName: node linkType: hard @@ -2805,13 +4299,13 @@ __metadata: languageName: node linkType: hard -"@serverless/platform-client@npm:^4.4.0": - version: 4.4.0 - resolution: "@serverless/platform-client@npm:4.4.0" +"@serverless/platform-client@npm:^4.5.1": + version: 4.5.1 + resolution: "@serverless/platform-client@npm:4.5.1" dependencies: adm-zip: "npm:^0.5.5" archiver: "npm:^5.3.0" - axios: "npm:^0.21.1" + axios: "npm:^1.6.2" fast-glob: "npm:^3.2.7" https-proxy-agent: "npm:^5.0.0" ignore: "npm:^5.1.8" @@ -2824,11 +4318,11 @@ __metadata: throat: "npm:^5.0.0" traverse: "npm:^0.6.6" ws: "npm:^7.5.3" - checksum: 10/1df46d03318f31ed7e48c67b7a45baa38ae786578cf365aa33ad38bea9ff61c20ed719c9e569e8cbf09c71da1d363bb9e203448474f8bd416898a1438c5472a5 + checksum: 10/8751df64ecbeb78b505a36cf60ecb3e223278b01215c9ff3b947d850a90fe9ee474bbe9ed5f4c3c9d2ab455984fcf9d29551ace350f476b820e3be7341d6f68f languageName: node linkType: hard -"@serverless/utils@npm:^6.13.1, @serverless/utils@npm:^6.14.0, @serverless/utils@npm:^6.15.0": +"@serverless/utils@npm:^6.13.1, @serverless/utils@npm:^6.14.0": version: 6.15.0 resolution: "@serverless/utils@npm:6.15.0" dependencies: @@ -2944,6 +4438,45 @@ __metadata: languageName: node linkType: hard +"@smithy/abort-controller@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/abort-controller@npm:4.0.4" + dependencies: + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/f61c13f0acfecd047cd999d0c7b0ac253da51f2aea5a6e47fd47a2d939232a39979768e3c1a330e00d2da99b3fd5f51cda67c13a75f5b44e777bfe2620e960aa + languageName: node + linkType: hard + +"@smithy/abort-controller@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/abort-controller@npm:4.0.5" + dependencies: + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/6144fd382208d12b9818b0b937a2915cf2c6258854aa0bd860932857c4cafb0324a4c085387372a301854cc0eb4d36e9fe1a5c2400d6e2d3dee396ed6995c3c8 + languageName: node + linkType: hard + +"@smithy/chunked-blob-reader-native@npm:^4.0.0": + version: 4.0.0 + resolution: "@smithy/chunked-blob-reader-native@npm:4.0.0" + dependencies: + "@smithy/util-base64": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/c58c4af5344cb9e2feddc15e020474930dc1a53a71b6dd2b3bd01d5555a5eb30ba964226b0fdac0c7e1f31d0354967a2e0c3c64860d6f0fe36652a7a003a8a19 + languageName: node + linkType: hard + +"@smithy/chunked-blob-reader@npm:^5.0.0": + version: 5.0.0 + resolution: "@smithy/chunked-blob-reader@npm:5.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/d27333cfe68f7d8af6b7b9b3f6edf32c8dea9cac9e4933f2a062b0836b126af4abcec6b908f9607a2f137f86e59f2eee37a57f87dbaea046da95c1f01e44d5ef + 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" @@ -2970,6 +4503,32 @@ __metadata: languageName: node linkType: hard +"@smithy/config-resolver@npm:^4.1.4": + version: 4.1.4 + resolution: "@smithy/config-resolver@npm:4.1.4" + dependencies: + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-config-provider": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.0.4" + tslib: "npm:^2.6.2" + checksum: 10/34cd304c8cfbe85328497137b83c74d534afc440c7d5a1fa8ab960a40c757de62d4e512e974f131f60d8215a7f5a9791ee775ef44422d153c6959173ed166274 + languageName: node + linkType: hard + +"@smithy/config-resolver@npm:^4.1.5": + version: 4.1.5 + resolution: "@smithy/config-resolver@npm:4.1.5" + dependencies: + "@smithy/node-config-provider": "npm:^4.1.4" + "@smithy/types": "npm:^4.3.2" + "@smithy/util-config-provider": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.0.5" + tslib: "npm:^2.6.2" + checksum: 10/c87dcd61c654c840ea820f8955d859d775633c5e39ceed4661353ea199b38e114d11e6a9311fbc49a789986cb75bf0c17705af590ac28a62dd710e75d1e8006f + languageName: node + linkType: hard + "@smithy/core@npm:^1.4.0": version: 1.4.0 resolution: "@smithy/core@npm:1.4.0" @@ -2986,7 +4545,43 @@ __metadata: languageName: node linkType: hard -"@smithy/credential-provider-imds@npm:^2.0.0, @smithy/credential-provider-imds@npm:^2.0.13": +"@smithy/core@npm:^3.5.3, @smithy/core@npm:^3.6.0": + version: 3.6.0 + resolution: "@smithy/core@npm:3.6.0" + dependencies: + "@smithy/middleware-serde": "npm:^4.0.8" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-stream": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/f037808d55fe5b899a58edef20ae2af8f14b81976faba0bdc7888de6e993a8673c9e19e7cbe7f566b16883954e669ae8979502b2a277347371f08080431aabec + languageName: node + linkType: hard + +"@smithy/core@npm:^3.8.0": + version: 3.8.0 + resolution: "@smithy/core@npm:3.8.0" + dependencies: + "@smithy/middleware-serde": "npm:^4.0.9" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/types": "npm:^4.3.2" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-body-length-browser": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.0.5" + "@smithy/util-stream": "npm:^4.2.4" + "@smithy/util-utf8": "npm:^4.0.0" + "@types/uuid": "npm:^9.0.1" + tslib: "npm:^2.6.2" + uuid: "npm:^9.0.1" + checksum: 10/76b86b04fc530285ef73e34e4d47cf4a53b6eaf1b3f54a9a0126990766258670d8686015cee39da797afe3f931347ceb8f6c18a620a6744a7fc901e4822d678a + 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: @@ -3012,6 +4607,32 @@ __metadata: languageName: node linkType: hard +"@smithy/credential-provider-imds@npm:^4.0.6": + version: 4.0.6 + resolution: "@smithy/credential-provider-imds@npm:4.0.6" + dependencies: + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/url-parser": "npm:^4.0.4" + tslib: "npm:^2.6.2" + checksum: 10/59c05c1ea4148781eacf3e41c416c8a696087349d6d3b1a8a3f06cde4747bf446e6482724eaee1e0679d7fa1442ae0fe1f3f01b9c41a0c0160e564bf7652a682 + languageName: node + linkType: hard + +"@smithy/credential-provider-imds@npm:^4.0.7": + version: 4.0.7 + resolution: "@smithy/credential-provider-imds@npm:4.0.7" + dependencies: + "@smithy/node-config-provider": "npm:^4.1.4" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + "@smithy/url-parser": "npm:^4.0.5" + tslib: "npm:^2.6.2" + checksum: 10/e6fe92ac78a562f4f12ab1106bdc201348e356fabbf073027ee02d49dd8d3bd193d06ad9e6a62e995c48f8e4ebdfd76ab532471471a120fafc52301896eaac63 + languageName: node + linkType: hard + "@smithy/eventstream-codec@npm:^2.0.10": version: 2.0.10 resolution: "@smithy/eventstream-codec@npm:2.0.10" @@ -3036,14 +4657,27 @@ __metadata: 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" +"@smithy/eventstream-codec@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/eventstream-codec@npm:4.0.4" dependencies: - "@smithy/eventstream-serde-universal": "npm:^2.0.10" - "@smithy/types": "npm:^2.3.4" - tslib: "npm:^2.5.0" - checksum: 10/b66bd20abb727dba1e4aa45f08222ce88501b8fb55a4eae8d2e8f1d5133979a2d73395a5439908bbe250e2c4201063fbb077eb1c18518ecf3cf01037965fae86 + "@aws-crypto/crc32": "npm:5.2.0" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-hex-encoding": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/278be94e346b3d53a9317f7e6de8c2a6be7013ccd1ea3035447f29491bdada4706cef32c408fb7cdb279462c91cb32dc049d546be981785dc84871b9c307ed2e + languageName: node + linkType: hard + +"@smithy/eventstream-codec@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/eventstream-codec@npm:4.0.5" + dependencies: + "@aws-crypto/crc32": "npm:5.2.0" + "@smithy/types": "npm:^4.3.2" + "@smithy/util-hex-encoding": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/7b3100f60e069e0237ef96921574bb6ee0e459bc4a269d447e7f5c2e83c2e97cf8edf0635aba458658f1c7c278a90036a8eb89f6eee237c4a6c776257eafaf28 languageName: node linkType: hard @@ -3058,13 +4692,25 @@ __metadata: 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" +"@smithy/eventstream-serde-browser@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/eventstream-serde-browser@npm:4.0.4" dependencies: - "@smithy/types": "npm:^2.3.4" - tslib: "npm:^2.5.0" - checksum: 10/bd8a16aa1bf68c9544dc5d74ebbdb3ef450b1e2d46994d047ae54e24b4b8f0de3c014858ea7a4f3afd2b281f13a36882b0fac88e8ab5d3496dc9584b969b989f + "@smithy/eventstream-serde-universal": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/218d94d8dc73e5af14a415b35fe9c0d6a9b869f7f3b5cad59b3cc8b8c7bc51390a25c09999d37225eaca5cd90ef0f4c962f47a0f1db9ca908fea07756ace5726 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-browser@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/eventstream-serde-browser@npm:4.0.5" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/054415f6452ddb85f301cecb1a9ea180522b74fbb434ba295f8df43ed404533a1ad2f6706e5d0fdff142fc18b8f2cee8b10cc308076d3365a93a802b0077c72e languageName: node linkType: hard @@ -3078,14 +4724,23 @@ __metadata: 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" +"@smithy/eventstream-serde-config-resolver@npm:^4.1.2": + version: 4.1.2 + resolution: "@smithy/eventstream-serde-config-resolver@npm:4.1.2" dependencies: - "@smithy/eventstream-serde-universal": "npm:^2.0.10" - "@smithy/types": "npm:^2.3.4" - tslib: "npm:^2.5.0" - checksum: 10/b990642ad9f7c652edf4758e96d800ff998a15edd33de7266c5429669c60dd6d3e45e079a98a1a2f3574089bc9037dc10a02b97f75b359bb5750c45aae26be9c + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/2a11aa813ad0dc4a353d017c87d7155ffb9ff43a985929dbf654c012e1c494c36262fca7282dc119a7b9bf766f1ecd5bfb6f4dff3aef8cccfc9652b43d14882d + languageName: node + linkType: hard + +"@smithy/eventstream-serde-config-resolver@npm:^4.1.3": + version: 4.1.3 + resolution: "@smithy/eventstream-serde-config-resolver@npm:4.1.3" + dependencies: + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/c55ef558afcce6940d7aced9405b376a6844b9e6cd95980b3eb02dd2c9ce468e4e52e2219f9e9fb60c76b07c70e4578d7faeb6f970400d356e81faf125343da5 languageName: node linkType: hard @@ -3100,14 +4755,25 @@ __metadata: 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" +"@smithy/eventstream-serde-node@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/eventstream-serde-node@npm:4.0.4" dependencies: - "@smithy/eventstream-codec": "npm:^2.0.10" - "@smithy/types": "npm:^2.3.4" - tslib: "npm:^2.5.0" - checksum: 10/3ed6a6deb16b5b41e79146f49599fb464392c5d501d2555f259534543508db3479fa6b47d7e435156e6e39303fb87cec27091808efd71d264e27504ca657c2fa + "@smithy/eventstream-serde-universal": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/4a03085450e4e88eb5dcd734bb2ae77260ee46b323e5f7761c8b8421e98be51e4af89bf9d0932e6ed51063092e8050d7b6e5ec077dedcb44b310794a695ef298 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-node@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/eventstream-serde-node@npm:4.0.5" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/10989968e3afa0e41ca19b2bd04283ff41edb4f7d4157a1d2e7582b7299ee3b5f971100bf5d8ce870e81ffd50118c3895568f0ff36dfd6b9d2dc339387119def languageName: node linkType: hard @@ -3122,6 +4788,28 @@ __metadata: languageName: node linkType: hard +"@smithy/eventstream-serde-universal@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/eventstream-serde-universal@npm:4.0.4" + dependencies: + "@smithy/eventstream-codec": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/9a6034a3515034eb1f4556bcac3d8ea3bdd9d45d807bcced0cc91f9e58ac41b42ab2b3b7a1f7a47c5d18cb9d6fc1f264276ee6503a7a0348d669b24cde13e6c1 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-universal@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/eventstream-serde-universal@npm:4.0.5" + dependencies: + "@smithy/eventstream-codec": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/fee891fc18b55225c953602177277f2855361bf167f04aed62ea5433dc54a9a8feaba58962954534de77b7de3c791b0852f236823e89fbc983dbd436b2cc0154 + 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" @@ -3148,6 +4836,44 @@ __metadata: languageName: node linkType: hard +"@smithy/fetch-http-handler@npm:^5.0.4": + version: 5.0.4 + resolution: "@smithy/fetch-http-handler@npm:5.0.4" + dependencies: + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/querystring-builder": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-base64": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/2e607a8ba73385700bc008cfc6b0f98adc06e6113eb9bb0cdb131bd34de24f71c827126a6e3925585287a9f54c0ef87c8a11b683e46ac824c2a4359e02f9b7f2 + languageName: node + linkType: hard + +"@smithy/fetch-http-handler@npm:^5.1.1": + version: 5.1.1 + resolution: "@smithy/fetch-http-handler@npm:5.1.1" + dependencies: + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/querystring-builder": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + "@smithy/util-base64": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/9fecb55396374678a0e662fa4134b20415625e71389b68b42cdb4ad4b523a865ea066a612a53546fc5be8cfb10a3c93aae966170038e8c268bba94c7ad5160e6 + languageName: node + linkType: hard + +"@smithy/hash-blob-browser@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/hash-blob-browser@npm:4.0.4" + dependencies: + "@smithy/chunked-blob-reader": "npm:^5.0.0" + "@smithy/chunked-blob-reader-native": "npm:^4.0.0" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/7458cf730b11ed6339ab6ae2f8ba4d1c1294d21554e3b09b1711e898cfb295efa6270c9d2daf20e37c4de0f807183957618f2316f0ac9e53680b9a4956b71cb3 + languageName: node + linkType: hard + "@smithy/hash-node@npm:^2.0.9": version: 2.0.10 resolution: "@smithy/hash-node@npm:2.0.10" @@ -3172,6 +4898,41 @@ __metadata: languageName: node linkType: hard +"@smithy/hash-node@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/hash-node@npm:4.0.4" + dependencies: + "@smithy/types": "npm:^4.3.1" + "@smithy/util-buffer-from": "npm:^4.0.0" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/c22dbdca891783b39fac3e0bdf6ee260da8138c7f56fccbfee921a4e9479d7d32a98e89e6bc5ebc09760b2785d0473608fccea3fe2017c170f187c5a1d7a2fd1 + languageName: node + linkType: hard + +"@smithy/hash-node@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/hash-node@npm:4.0.5" + dependencies: + "@smithy/types": "npm:^4.3.2" + "@smithy/util-buffer-from": "npm:^4.0.0" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/ccb4a706bc78daaeb9eaa9d3c49a94a14afa50462776656a319b219acb2ab6e8a5de9ffd4df5ee21f57daa7ade92386c307cf52c07603442de00030dbad1e8b9 + languageName: node + linkType: hard + +"@smithy/hash-stream-node@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/hash-stream-node@npm:4.0.4" + dependencies: + "@smithy/types": "npm:^4.3.1" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/5542dff2e2896bba85de83db4c8bfd39766f233f0b0e0bc7098b34ff13e4e4b9233a32dbfc9919c25ea16cedf9f365fed23615d64ee21690630538a5b5c9516b + languageName: node + linkType: hard + "@smithy/invalid-dependency@npm:^2.0.9": version: 2.0.10 resolution: "@smithy/invalid-dependency@npm:2.0.10" @@ -3192,6 +4953,26 @@ __metadata: languageName: node linkType: hard +"@smithy/invalid-dependency@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/invalid-dependency@npm:4.0.4" + dependencies: + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/6941d0f475e96b4a9ad19a06a5a34a7d1c17e31b7782c86c7076c45a12758215a94f5e2b876e0aea062e22437cb2a59c7b5de7ce288d1f79cf647e6e16e7bb00 + languageName: node + linkType: hard + +"@smithy/invalid-dependency@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/invalid-dependency@npm:4.0.5" + dependencies: + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/35611817dc981cf266c55f10e21af53653129effa3915641e357a4d31aebb5b34bd452fed5dcbedd45d8ab3dc2abd1882f3bc971a56d269ac1b636adcf939ed8 + 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" @@ -3210,6 +4991,15 @@ __metadata: languageName: node linkType: hard +"@smithy/is-array-buffer@npm:^4.0.0": + version: 4.0.0 + resolution: "@smithy/is-array-buffer@npm:4.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/3985046ac490968fe86e2d5e87d023d67f29aa4778abebacecb0f7962d07e32507a5612701c7aa7b1fb63b5a6e68086c915cae5229e5f1abfb39419dc07e00c8 + languageName: node + linkType: hard + "@smithy/md5-js@npm:^2.2.0": version: 2.2.0 resolution: "@smithy/md5-js@npm:2.2.0" @@ -3221,6 +5011,17 @@ __metadata: languageName: node linkType: hard +"@smithy/md5-js@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/md5-js@npm:4.0.4" + dependencies: + "@smithy/types": "npm:^4.3.1" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/5452a8b3f98dce6c1226fcd00cbc7a02c3a699364ba06454e970b8fc18a09dab1d583df8e27d85f1b84ce4bb0120168a402fa3e94c909b19cc66fe4c950221d5 + 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" @@ -3243,6 +5044,28 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-content-length@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/middleware-content-length@npm:4.0.4" + dependencies: + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/c81766adc34e801057324da1ae14ae97eb01a788a1ae5be419a8e560805f4eca762e36b4bfaee52f091f867d909a26fdc3d40cbb8a6fcf9cdb21e4d31deea0e7 + languageName: node + linkType: hard + +"@smithy/middleware-content-length@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/middleware-content-length@npm:4.0.5" + dependencies: + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/f5b0c24c1a4a1a627fef2143afbff6e8627bb375b57fe1d8c5a78837856397c92411a92f7ea4eef29546405c3e3f2fb6adcc44bd2665de2a9e2a4ce52559ef0c + languageName: node + linkType: hard + "@smithy/middleware-endpoint@npm:^2.0.9": version: 2.0.10 resolution: "@smithy/middleware-endpoint@npm:2.0.10" @@ -3271,6 +5094,38 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-endpoint@npm:^4.1.12, @smithy/middleware-endpoint@npm:^4.1.13": + version: 4.1.13 + resolution: "@smithy/middleware-endpoint@npm:4.1.13" + dependencies: + "@smithy/core": "npm:^3.6.0" + "@smithy/middleware-serde": "npm:^4.0.8" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/shared-ini-file-loader": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + "@smithy/url-parser": "npm:^4.0.4" + "@smithy/util-middleware": "npm:^4.0.4" + tslib: "npm:^2.6.2" + checksum: 10/19cc518676b34db227c0167a12e12f144594b172e951df97dc05bb7003959215792a04a698842c1939f8f0ea04590234bfecffaa2a1b2cd60c911c243d896531 + languageName: node + linkType: hard + +"@smithy/middleware-endpoint@npm:^4.1.18": + version: 4.1.18 + resolution: "@smithy/middleware-endpoint@npm:4.1.18" + dependencies: + "@smithy/core": "npm:^3.8.0" + "@smithy/middleware-serde": "npm:^4.0.9" + "@smithy/node-config-provider": "npm:^4.1.4" + "@smithy/shared-ini-file-loader": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + "@smithy/url-parser": "npm:^4.0.5" + "@smithy/util-middleware": "npm:^4.0.5" + tslib: "npm:^2.6.2" + checksum: 10/757a7e195a5f1e8d6b04ba3687a3ee530d29bfbe880b9bc2a7c9fc4cc9dc656f2ebcc5c6ff2c7c919e58d616c01bc350f097d8181d72f223a3fa9ab62ef0f1fa + languageName: node + linkType: hard + "@smithy/middleware-retry@npm:^2.0.12": version: 2.0.13 resolution: "@smithy/middleware-retry@npm:2.0.13" @@ -3304,6 +5159,41 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-retry@npm:^4.1.13": + version: 4.1.14 + resolution: "@smithy/middleware-retry@npm:4.1.14" + dependencies: + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/service-error-classification": "npm:^4.0.6" + "@smithy/smithy-client": "npm:^4.4.5" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-retry": "npm:^4.0.6" + tslib: "npm:^2.6.2" + uuid: "npm:^9.0.1" + checksum: 10/e3fb650dbece6bf3565b3ed3f43c38336ae9bc7b68a5fd1dbaa8697470d5e1724a8194bb85129e2b7dc97f70a37be087fa3de264de0e77bcbfed70d5c38955fc + languageName: node + linkType: hard + +"@smithy/middleware-retry@npm:^4.1.19": + version: 4.1.19 + resolution: "@smithy/middleware-retry@npm:4.1.19" + dependencies: + "@smithy/node-config-provider": "npm:^4.1.4" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/service-error-classification": "npm:^4.0.7" + "@smithy/smithy-client": "npm:^4.4.10" + "@smithy/types": "npm:^4.3.2" + "@smithy/util-middleware": "npm:^4.0.5" + "@smithy/util-retry": "npm:^4.0.7" + "@types/uuid": "npm:^9.0.1" + tslib: "npm:^2.6.2" + uuid: "npm:^9.0.1" + checksum: 10/f7ce55a4d66e8b569dacee92796a4096e32ca6c992ba7036a7f62532e1a7e5c570b1a042dca3810d92f196411a85e3a6735f40c7247fc4464e28b4c5c3b997ed + 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" @@ -3324,6 +5214,28 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-serde@npm:^4.0.8": + version: 4.0.8 + resolution: "@smithy/middleware-serde@npm:4.0.8" + dependencies: + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/f3abcdd65b9fae824f30664c5177363f10c7ed785d294b4c2636e549658d20eb4ef303a42ec45f08c18c747a6dabd839dc881e2c956f5f6143ead9358ed07f15 + languageName: node + linkType: hard + +"@smithy/middleware-serde@npm:^4.0.9": + version: 4.0.9 + resolution: "@smithy/middleware-serde@npm:4.0.9" + dependencies: + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/eea044c2f6de809543d1d43d498b3434f8a94e67b8e3c00190a03b9e79f862ac7140d2954e39eee63b18a23b9ec0405bd35910224e22a7b2b74177e2771c9a2c + 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" @@ -3344,6 +5256,26 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-stack@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/middleware-stack@npm:4.0.4" + dependencies: + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/7cfea213e9dafc93061388631828bed7c20a1f87e14c1d32a2c1fbabba802253ca32519277288ce5457189fe015422573c4a9dd4d27ce60b6f0e8f15c127a5be + languageName: node + linkType: hard + +"@smithy/middleware-stack@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/middleware-stack@npm:4.0.5" + dependencies: + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/23c3dbde1d41855b7467dbd9a4342131f6e9446d5ae1ff980c025e849e2e1ad950f344aaa9c2b431df87d968b2a5586c376c3e1f6bc6da8c9da18306dac8c5fb + 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" @@ -3368,6 +5300,30 @@ __metadata: languageName: node linkType: hard +"@smithy/node-config-provider@npm:^4.1.3": + version: 4.1.3 + resolution: "@smithy/node-config-provider@npm:4.1.3" + dependencies: + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/shared-ini-file-loader": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/4b104f6e00446eecb7181233991cd0408fd6fea72e15213514fa12aafe56f70d02ceb544df3b56c5e8c315c0b0a7d26608741212bcc25d8578931173b6935847 + languageName: node + linkType: hard + +"@smithy/node-config-provider@npm:^4.1.4": + version: 4.1.4 + resolution: "@smithy/node-config-provider@npm:4.1.4" + dependencies: + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/shared-ini-file-loader": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/ac411cb19303279c8c0568dbd431cca7c5c1791fe7185b5637cb8540e725ebd7b59852c8d06afa64621d6a070e7fed1109bbcde9856dbe14b09f0b23d58c4564 + 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" @@ -3394,6 +5350,32 @@ __metadata: languageName: node linkType: hard +"@smithy/node-http-handler@npm:^4.0.6": + version: 4.0.6 + resolution: "@smithy/node-http-handler@npm:4.0.6" + dependencies: + "@smithy/abort-controller": "npm:^4.0.4" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/querystring-builder": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/0ab9d5d4c6f0dc86d970fda27d082b565f93dfd381114d1f876b1ebcb495c6f0819fee5cb93289cdc94a5b3ef24450f5ba94f015c064334aaf43869d07b56ae4 + languageName: node + linkType: hard + +"@smithy/node-http-handler@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/node-http-handler@npm:4.1.1" + dependencies: + "@smithy/abort-controller": "npm:^4.0.5" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/querystring-builder": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/0f73f5c11a2efb12688ff8957ef49948926a29a5e3d9c01eafb9621f7bffcbca4eeab3eec23859753965a95d8ccf92b9df0880170773daf012a1ac364573921c + 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" @@ -3414,6 +5396,26 @@ __metadata: languageName: node linkType: hard +"@smithy/property-provider@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/property-provider@npm:4.0.4" + dependencies: + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/5893d1d9f6ac12119d2b316d286fe30eac0e585d7eb43281593da2663d35b8f4faf481d2d8a8385276090c2403fe86b520c781712d482f69329f72ad240ed5f1 + languageName: node + linkType: hard + +"@smithy/property-provider@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/property-provider@npm:4.0.5" + dependencies: + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/f3d204dea729a1189dc90a0938a41a8f5bf20b64e975c8b7698e3d39b1e69a593436d871f084517a28e69ffe690d422fadc4acf1913991bc96ee777f152b1ce6 + 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" @@ -3434,6 +5436,26 @@ __metadata: languageName: node linkType: hard +"@smithy/protocol-http@npm:^5.1.2": + version: 5.1.2 + resolution: "@smithy/protocol-http@npm:5.1.2" + dependencies: + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/02ff9d3e808530fb2d909f292ccb3833bcdae8a0f8b0c702afca8ce8bd2b2dad3341aae7cd77039261bf82202e59583c22ca36f1becc03ba3ae24eec9ef0a909 + languageName: node + linkType: hard + +"@smithy/protocol-http@npm:^5.1.3": + version: 5.1.3 + resolution: "@smithy/protocol-http@npm:5.1.3" + dependencies: + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/582e502fe1f9b52672ddfad3b1f28facfedae517d04fbbdb4ad1bb2bd6ce617f34052682f2738abb7a193eb47d55244abd9005e9f9a4612e820e7bff1056093a + languageName: node + linkType: hard + "@smithy/querystring-builder@npm:^2.0.10": version: 2.0.10 resolution: "@smithy/querystring-builder@npm:2.0.10" @@ -3456,6 +5478,28 @@ __metadata: languageName: node linkType: hard +"@smithy/querystring-builder@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/querystring-builder@npm:4.0.4" + dependencies: + "@smithy/types": "npm:^4.3.1" + "@smithy/util-uri-escape": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/a172a961711bc8a4b69553712c36b1854b8d1d75630197e35fb94877f17c4668f2874f202264155be8ce0789a53ecc97e061c8a55e9ba7335951ba16d3a41919 + languageName: node + linkType: hard + +"@smithy/querystring-builder@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/querystring-builder@npm:4.0.5" + dependencies: + "@smithy/types": "npm:^4.3.2" + "@smithy/util-uri-escape": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/0ffd02b9add467385051deca9e8abb0bf25702b8643265a715752cbb6afd7b40e7d8a6978163dc32f7d478a5b1840b77bc99b5b823cc9f4ca535594b94aa0243 + languageName: node + linkType: hard + "@smithy/querystring-parser@npm:^2.0.10": version: 2.0.10 resolution: "@smithy/querystring-parser@npm:2.0.10" @@ -3476,6 +5520,26 @@ __metadata: languageName: node linkType: hard +"@smithy/querystring-parser@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/querystring-parser@npm:4.0.4" + dependencies: + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/0e476c39995b36831479c73ffae8678be208060a7a932fa05c0a142a1297e7d793377517acb41583c64fb2a171b5176c32d5ac3427f356bc836bcabfcb57ca76 + languageName: node + linkType: hard + +"@smithy/querystring-parser@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/querystring-parser@npm:4.0.5" + dependencies: + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/5a9b65a4ff417aa75e7452eeb393499e57661a0834a60950f23d49522b76a86d2411e06336f4b61364b0ced3639d8162828a13cfa49daf86b87bfa222e819dfd + 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" @@ -3494,6 +5558,24 @@ __metadata: languageName: node linkType: hard +"@smithy/service-error-classification@npm:^4.0.6": + version: 4.0.6 + resolution: "@smithy/service-error-classification@npm:4.0.6" + dependencies: + "@smithy/types": "npm:^4.3.1" + checksum: 10/3800abbc36b20872494a013737f48ff8334571ddc7e63a8e46f5c40235c68f93caee1f6b11d194a61e11666a6b99cc5102bc3ff8c700b33592befee5344ae2b9 + languageName: node + linkType: hard + +"@smithy/service-error-classification@npm:^4.0.7": + version: 4.0.7 + resolution: "@smithy/service-error-classification@npm:4.0.7" + dependencies: + "@smithy/types": "npm:^4.3.2" + checksum: 10/4bc16b0b85576b5e3f2669cd216abff43a46da7f722d3b9a267736beee9365ea011b1b71454dc728a1233b890243fed797e361f32c912f7a2577ba6574ce8f74 + 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" @@ -3514,6 +5596,26 @@ __metadata: languageName: node linkType: hard +"@smithy/shared-ini-file-loader@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/shared-ini-file-loader@npm:4.0.4" + dependencies: + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/45b3198a8ec8f2865f5e81f5c422f1858c6e2966a754a94077cf0e8e8fef09a1b0a22a97963968bd8258c8749e0de5303da631e779ada3ee27976af0f7666a72 + languageName: node + linkType: hard + +"@smithy/shared-ini-file-loader@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/shared-ini-file-loader@npm:4.0.5" + dependencies: + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/06e85753313b90fe4a8800aac0e3a768c1bd064df56503c3309fc6839cb002c40f75f44f5582bd87c13712eb3ed554dc89f36999df70c31a5adb553213ab939a + languageName: node + linkType: hard + "@smithy/signature-v4@npm:^2.0.0": version: 2.0.10 resolution: "@smithy/signature-v4@npm:2.0.10" @@ -3546,6 +5648,38 @@ __metadata: languageName: node linkType: hard +"@smithy/signature-v4@npm:^5.1.2": + version: 5.1.2 + resolution: "@smithy/signature-v4@npm:5.1.2" + dependencies: + "@smithy/is-array-buffer": "npm:^4.0.0" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-hex-encoding": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.0.4" + "@smithy/util-uri-escape": "npm:^4.0.0" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/64ee5ab9266310329be40a7e02c0fb932956067fac1fa16b408d71aeef20de7cfe17f105566823747a6421a5d3e24524fc362f348729ed0d0d5f3d47f16d43f7 + languageName: node + linkType: hard + +"@smithy/signature-v4@npm:^5.1.3": + version: 5.1.3 + resolution: "@smithy/signature-v4@npm:5.1.3" + dependencies: + "@smithy/is-array-buffer": "npm:^4.0.0" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/types": "npm:^4.3.2" + "@smithy/util-hex-encoding": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.0.5" + "@smithy/util-uri-escape": "npm:^4.0.0" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/a8f23ea144535b35c3c9d65f30ee67b78f86eb60b74809d9e3c7f439c6dba32a6a2fc1dfd97eb763c655ae7a69132d90b3397bf7a9d3867e3949ae2720c8b6f9 + 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" @@ -3572,6 +5706,36 @@ __metadata: languageName: node linkType: hard +"@smithy/smithy-client@npm:^4.4.10": + version: 4.4.10 + resolution: "@smithy/smithy-client@npm:4.4.10" + dependencies: + "@smithy/core": "npm:^3.8.0" + "@smithy/middleware-endpoint": "npm:^4.1.18" + "@smithy/middleware-stack": "npm:^4.0.5" + "@smithy/protocol-http": "npm:^5.1.3" + "@smithy/types": "npm:^4.3.2" + "@smithy/util-stream": "npm:^4.2.4" + tslib: "npm:^2.6.2" + checksum: 10/f43302822fd95b71d570db6deba638ca3c02e8a87d00468c3644074e7ebe7627529f12ee13876b7e9a883567ca8b473df2082e184ca9667f4a4dfc3875f5d0c9 + languageName: node + linkType: hard + +"@smithy/smithy-client@npm:^4.4.4, @smithy/smithy-client@npm:^4.4.5": + version: 4.4.5 + resolution: "@smithy/smithy-client@npm:4.4.5" + dependencies: + "@smithy/core": "npm:^3.6.0" + "@smithy/middleware-endpoint": "npm:^4.1.13" + "@smithy/middleware-stack": "npm:^4.0.4" + "@smithy/protocol-http": "npm:^5.1.2" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-stream": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/177d64b5475ef0553a4496494d8d1641a4dc3cebc91667845806f1ce7c32fd63ee99b5322fc1adcff65c1d1d0a64df67c5153489a33f2565f7dc32fb54217916 + languageName: node + linkType: hard + "@smithy/types@npm:^2.12.0": version: 2.12.0 resolution: "@smithy/types@npm:2.12.0" @@ -3590,6 +5754,24 @@ __metadata: languageName: node linkType: hard +"@smithy/types@npm:^4.3.1": + version: 4.3.1 + resolution: "@smithy/types@npm:4.3.1" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/0708b5d66d5a3864816dbd1a0984ce46a7d82e80934a5f08dc5fddd8c7bbfbd3af974acbea7122cf9ad743403459b43b0113c419fae7528cce596432ec5a97ad + languageName: node + linkType: hard + +"@smithy/types@npm:^4.3.2": + version: 4.3.2 + resolution: "@smithy/types@npm:4.3.2" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/2828937ef949b1e5eb45beea2c03f20daec4390db1485e142a7c9c2efc9dfa90430332cf4fb1523b31df3a9a4ff97cb87c157294cc65ef2f0f7e376eab3fd7a8 + 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" @@ -3612,6 +5794,28 @@ __metadata: languageName: node linkType: hard +"@smithy/url-parser@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/url-parser@npm:4.0.4" + dependencies: + "@smithy/querystring-parser": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/0fde2c263f639ae53ab5f73bc4bb9c9a611b159676081c8e58fa21d1a18a5c61c0cec207ec482c4d78c281e6f59dc94bfef9801df373a6936b4e69dbd23c9afe + languageName: node + linkType: hard + +"@smithy/url-parser@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/url-parser@npm:4.0.5" + dependencies: + "@smithy/querystring-parser": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/8874b5c14ec86d4f320c0f58419194dbfe02e34070f224218caf42c55a824cd70d1469fb3a857af88859fe99a655accb1288f0faf3e3cbac77c29aa950fdf1a4 + languageName: node + linkType: hard + "@smithy/util-base64@npm:^2.0.0": version: 2.0.0 resolution: "@smithy/util-base64@npm:2.0.0" @@ -3633,6 +5837,17 @@ __metadata: languageName: node linkType: hard +"@smithy/util-base64@npm:^4.0.0": + version: 4.0.0 + resolution: "@smithy/util-base64@npm:4.0.0" + dependencies: + "@smithy/util-buffer-from": "npm:^4.0.0" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/f495fa8f5be60a1b94f88e2de4b1236df5cfee78f32191840adffcc520f2f55cdc2f287dd7abddcac4759c51970b5326b6b371c60ad65b640992018e95e30d19 + 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" @@ -3651,6 +5866,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-body-length-browser@npm:^4.0.0": + version: 4.0.0 + resolution: "@smithy/util-body-length-browser@npm:4.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/041a5e3c98d5b0a935c992c0217dcc033886798406df803945c994fbf3302eb0d9bdea7f7f8e6abaabf3e547bdffda6f1fb00829be3e93adac6b1949d77b741f + 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" @@ -3669,6 +5893,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-body-length-node@npm:^4.0.0": + version: 4.0.0 + resolution: "@smithy/util-body-length-node@npm:4.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/28d7b25b1465b290507b90be595bb161f9c1de755b35b4b99c3cf752725806b7d1f0c364535007f45a6aba95f2b49c2be9ebabaa4f03b5d36f9fc3287cd9d17a + 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" @@ -3689,6 +5922,16 @@ __metadata: languageName: node linkType: hard +"@smithy/util-buffer-from@npm:^4.0.0": + version: 4.0.0 + resolution: "@smithy/util-buffer-from@npm:4.0.0" + dependencies: + "@smithy/is-array-buffer": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/077fd6fe88b9db69ef0d4e2dfa9946bb1e1ae3d899515d7102f8648d18fb012fcbc87244cce569c0e9e86c5001bfe309b2de874fe508e1a9a591b11540b0a2c8 + 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" @@ -3707,6 +5950,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-config-provider@npm:^4.0.0": + version: 4.0.0 + resolution: "@smithy/util-config-provider@npm:4.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/74f3cb317056f0974b0942c79d43859031cb860fcf6eb5c9244bee369fc6c4b9c823491a40ca4f03f65641f4128d7fa5c2d322860cb7ee8517c0b2e63088ac6f + 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" @@ -3724,12 +5976,38 @@ __metadata: version: 2.2.0 resolution: "@smithy/util-defaults-mode-browser@npm:2.2.0" dependencies: - "@smithy/property-provider": "npm:^2.2.0" - "@smithy/smithy-client": "npm:^2.5.0" - "@smithy/types": "npm:^2.12.0" + "@smithy/property-provider": "npm:^2.2.0" + "@smithy/smithy-client": "npm:^2.5.0" + "@smithy/types": "npm:^2.12.0" + bowser: "npm:^2.11.0" + tslib: "npm:^2.6.2" + checksum: 10/06def0134965de01a35ba1a814d83a464b9d752974109a306588418a643a4205a716635cd4b97a3fc80af4a74c1e82550221f6d1ebea3c8e0d7106d8647e240d + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-browser@npm:^4.0.20": + version: 4.0.21 + resolution: "@smithy/util-defaults-mode-browser@npm:4.0.21" + dependencies: + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/smithy-client": "npm:^4.4.5" + "@smithy/types": "npm:^4.3.1" + bowser: "npm:^2.11.0" + tslib: "npm:^2.6.2" + checksum: 10/6e201e0e5947ddbc3f4521091c3fd95f371b96757b758e85abe657c5fc54717efd74d75dec54d5695ff66800d74583696b7990cdd7ff49ceb0f4026409a1f174 + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-browser@npm:^4.0.26": + version: 4.0.26 + resolution: "@smithy/util-defaults-mode-browser@npm:4.0.26" + dependencies: + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/smithy-client": "npm:^4.4.10" + "@smithy/types": "npm:^4.3.2" bowser: "npm:^2.11.0" tslib: "npm:^2.6.2" - checksum: 10/06def0134965de01a35ba1a814d83a464b9d752974109a306588418a643a4205a716635cd4b97a3fc80af4a74c1e82550221f6d1ebea3c8e0d7106d8647e240d + checksum: 10/fb919771ee1f986f8534f1e28159442941e3eeb464889a9fcb22bd3985c0fb99af771d8ac8fd697df4f69d6c6c81ba2c9b6463d8f4cce022b2f83bbde9ef1fa6 languageName: node linkType: hard @@ -3763,6 +6041,36 @@ __metadata: languageName: node linkType: hard +"@smithy/util-defaults-mode-node@npm:^4.0.20": + version: 4.0.21 + resolution: "@smithy/util-defaults-mode-node@npm:4.0.21" + dependencies: + "@smithy/config-resolver": "npm:^4.1.4" + "@smithy/credential-provider-imds": "npm:^4.0.6" + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/property-provider": "npm:^4.0.4" + "@smithy/smithy-client": "npm:^4.4.5" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/e33f02041592c09263c61bb39dfcca5f43057a03a3e5c7673de7f4d6d5ed546e7c0508d433e23bf9f61a0af4ffa3e7200cc015cfa7b04f47120aa5a67fcb4876 + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-node@npm:^4.0.26": + version: 4.0.26 + resolution: "@smithy/util-defaults-mode-node@npm:4.0.26" + dependencies: + "@smithy/config-resolver": "npm:^4.1.5" + "@smithy/credential-provider-imds": "npm:^4.0.7" + "@smithy/node-config-provider": "npm:^4.1.4" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/smithy-client": "npm:^4.4.10" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/a13dca7b48772ad0373900fe3e6498e7264fa7403af4171898e5950910fd78d945b49e3185397ea06dbb2b65c2b8289f8f522e7b941a3dbb0144e39dd49cc775 + languageName: node + linkType: hard + "@smithy/util-endpoints@npm:^1.2.0": version: 1.2.0 resolution: "@smithy/util-endpoints@npm:1.2.0" @@ -3774,6 +6082,28 @@ __metadata: languageName: node linkType: hard +"@smithy/util-endpoints@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/util-endpoints@npm:3.0.6" + dependencies: + "@smithy/node-config-provider": "npm:^4.1.3" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/509e2c4da31fad91f71b16d7a0cee5c0003b56b4942a4909e24c3257f2622f30a40683d4a3e027a728bb67ebd87092b8dcfd41fdd16248f9158c3b5be769a3f1 + languageName: node + linkType: hard + +"@smithy/util-endpoints@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/util-endpoints@npm:3.0.7" + dependencies: + "@smithy/node-config-provider": "npm:^4.1.4" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/30fab2affea2338abdca833af2da75c7dd5580df4fc990ba234712836cc7600f5f69be76e8728108c0c3eb035265f5044fd890df1ef6e1a903879607d2760a03 + 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" @@ -3792,6 +6122,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-hex-encoding@npm:^4.0.0": + version: 4.0.0 + resolution: "@smithy/util-hex-encoding@npm:4.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/447475cad8510d2727bbdf8490021a7ca8cb52b391f4bfe646c73a3aa1d5678152f1b5c4c2aaeebd9f6650272d973a1739e2d42294bd68c957429e3a30db3546 + 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" @@ -3812,6 +6151,26 @@ __metadata: languageName: node linkType: hard +"@smithy/util-middleware@npm:^4.0.4": + version: 4.0.4 + resolution: "@smithy/util-middleware@npm:4.0.4" + dependencies: + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/02e3aef392fc8d12ce659bc1adb668d0993c4ca61ab2ade855daf6db5f2f050f15d8729a922238fa38eae0dfbcaeeae3d1dfe1ff87c95187198da77baeff44d2 + languageName: node + linkType: hard + +"@smithy/util-middleware@npm:^4.0.5": + version: 4.0.5 + resolution: "@smithy/util-middleware@npm:4.0.5" + dependencies: + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/3138f0524ab8a287657d3a49ac00e83ccd19704d9781bac8835b4bb036dfb2e4196390121852e5d69a45d35372b984d8442f20fdd667559a3e3ec4e11d28985d + 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" @@ -3834,7 +6193,29 @@ __metadata: languageName: node linkType: hard -"@smithy/util-stream@npm:^2.0.12, @smithy/util-stream@npm:^2.0.14": +"@smithy/util-retry@npm:^4.0.6": + version: 4.0.6 + resolution: "@smithy/util-retry@npm:4.0.6" + dependencies: + "@smithy/service-error-classification": "npm:^4.0.6" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/652521cea73bb3ca8d1d2ca90524b1d1e47223960e1961d3f6e865db7b2ebf087287cf1648c609b430a96199eb76d67e299485ca4ca7678e77560deedd802305 + languageName: node + linkType: hard + +"@smithy/util-retry@npm:^4.0.7": + version: 4.0.7 + resolution: "@smithy/util-retry@npm:4.0.7" + dependencies: + "@smithy/service-error-classification": "npm:^4.0.7" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/40f18631bea926bec4c039e56d7a922349d9e52d030d3b60d6d116495241991ed6a7713979af442959aa51c48fd942ff942acb2c19b4ae144c4cc1e022b0b61c + languageName: node + linkType: hard + +"@smithy/util-stream@npm:^2.0.14": version: 2.0.14 resolution: "@smithy/util-stream@npm:2.0.14" dependencies: @@ -3866,6 +6247,38 @@ __metadata: languageName: node linkType: hard +"@smithy/util-stream@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-stream@npm:4.2.2" + dependencies: + "@smithy/fetch-http-handler": "npm:^5.0.4" + "@smithy/node-http-handler": "npm:^4.0.6" + "@smithy/types": "npm:^4.3.1" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-buffer-from": "npm:^4.0.0" + "@smithy/util-hex-encoding": "npm:^4.0.0" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/3b9827115b75c45a4d1b37eed82faecf0373bcfcbd6029add316568c5a768209e1d2dc58e8709f2a7eb636d4a7b0102c02bee8c1c11b169abe8c4736f32f91b6 + languageName: node + linkType: hard + +"@smithy/util-stream@npm:^4.2.4": + version: 4.2.4 + resolution: "@smithy/util-stream@npm:4.2.4" + dependencies: + "@smithy/fetch-http-handler": "npm:^5.1.1" + "@smithy/node-http-handler": "npm:^4.1.1" + "@smithy/types": "npm:^4.3.2" + "@smithy/util-base64": "npm:^4.0.0" + "@smithy/util-buffer-from": "npm:^4.0.0" + "@smithy/util-hex-encoding": "npm:^4.0.0" + "@smithy/util-utf8": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/34cde9fef3c7a6d3e544fdc5026f5745a399e500bf0adf93a0c2e98a85cd5353e08dd3bee02a7a44729999ebd52fe260407f8ff6ab1030230f9014b1ff30c9a7 + 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" @@ -3884,6 +6297,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-uri-escape@npm:^4.0.0": + version: 4.0.0 + resolution: "@smithy/util-uri-escape@npm:4.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/27b71d7c1bc21d9038b86fd55380449a7a1dab52959566372d24a86df027c0ad9190980879cc4903be999dc36a5619f0794acf9cdc789adba5e57e26cd6ce4a6 + languageName: node + linkType: hard + "@smithy/util-utf8@npm:^2.0.0": version: 2.0.0 resolution: "@smithy/util-utf8@npm:2.0.0" @@ -3904,6 +6326,16 @@ __metadata: languageName: node linkType: hard +"@smithy/util-utf8@npm:^4.0.0": + version: 4.0.0 + resolution: "@smithy/util-utf8@npm:4.0.0" + dependencies: + "@smithy/util-buffer-from": "npm:^4.0.0" + tslib: "npm:^2.6.2" + checksum: 10/4de06914d08753ce14ec553cf2dabe4a432cf982e415ec7dec82dfb8a6af793ddd08587fbcaeb889a0f6cc917eecca3a026880cf914082ee8e293f5bfc44e248 + languageName: node + linkType: hard + "@smithy/util-waiter@npm:^2.0.9": version: 2.0.10 resolution: "@smithy/util-waiter@npm:2.0.10" @@ -3926,6 +6358,28 @@ __metadata: languageName: node linkType: hard +"@smithy/util-waiter@npm:^4.0.5": + version: 4.0.6 + resolution: "@smithy/util-waiter@npm:4.0.6" + dependencies: + "@smithy/abort-controller": "npm:^4.0.4" + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10/4f18de5ae347d549b1d9490bad2e826cbb177b0d9a4d54c99abe4ab2a7556daae3037cf62c3d40ce5af1210b19d4ccdd2aee8131073a418382230f09d3ae1c4f + languageName: node + linkType: hard + +"@smithy/util-waiter@npm:^4.0.7": + version: 4.0.7 + resolution: "@smithy/util-waiter@npm:4.0.7" + dependencies: + "@smithy/abort-controller": "npm:^4.0.5" + "@smithy/types": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/335e88f06ebb6d118a69a6c52bed7e39d9586fc97dfb8229d7365dcff8de4ba359981d0d7488844d36ac4e81b55634e891eb19c5d209fab282172508ddd1c2c5 + languageName: node + linkType: hard + "@szmarczak/http-timer@npm:^4.0.5": version: 4.0.6 resolution: "@szmarczak/http-timer@npm:4.0.6" @@ -4410,13 +6864,6 @@ __metadata: 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/semver@npm:^7.3.12, @types/semver@npm:^7.5.0": version: 7.5.3 resolution: "@types/semver@npm:7.5.3" @@ -4466,6 +6913,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^9.0.1": + version: 9.0.8 + resolution: "@types/uuid@npm:9.0.8" + checksum: 10/b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 + languageName: node + linkType: hard + "@types/validator@npm:^13.7.17": version: 13.11.2 resolution: "@types/validator@npm:13.11.2" @@ -5011,7 +7465,7 @@ __metadata: typescript: "npm:5.4.3" winston: "npm:3.13.0" peerDependencies: - "@hathor/wallet-lib": 1.15.0 + "@hathor/wallet-lib": 2.8.3 languageName: unknown linkType: soft @@ -5764,23 +8218,25 @@ __metadata: languageName: node linkType: hard -"axios@npm:1.7.2": - version: 1.7.2 - resolution: "axios@npm:1.7.2" +"axios@npm:1.7.7": + version: 1.7.7 + resolution: "axios@npm:1.7.7" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: 10/6ae80dda9736bb4762ce717f1a26ff997d94672d3a5799ad9941c24d4fb019c1dff45be8272f08d1975d7950bac281f3ba24aff5ecd49ef5a04d872ec428782f + checksum: 10/7f875ea13b9298cd7b40fd09985209f7a38d38321f1118c701520939de2f113c4ba137832fe8e3f811f99a38e12c8225481011023209a77b0c0641270e20cde1 languageName: node linkType: hard -"axios@npm:^0.21.1": - version: 0.21.4 - resolution: "axios@npm:0.21.4" +"axios@npm:^1.6.2": + version: 1.10.0 + resolution: "axios@npm:1.10.0" dependencies: - follow-redirects: "npm:^1.14.0" - checksum: 10/da644592cb6f8f9f8c64fdabd7e1396d6769d7a4c1ea5f8ae8beb5c2eb90a823e3a574352b0b934ac62edc762c0f52647753dc54f7d07279127a7e5c4cd20272 + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 10/d43c80316a45611fd395743e15d16ea69a95f2b7f7095f2bb12cb78f9ca0a905194a02e52a3bf4e0db9f85fd1186d6c690410644c10ecd8bb0a468e57c2040e4 languageName: node linkType: hard @@ -7102,6 +9558,13 @@ __metadata: languageName: node linkType: hard +"data-uri-to-buffer@npm:^4.0.0": + version: 4.0.1 + resolution: "data-uri-to-buffer@npm:4.0.1" + checksum: 10/0d0790b67ffec5302f204c2ccca4494f70b4e2d940fea3d36b09f0bb2b8539c2e86690429eb1f1dc4bcc9e4df0644193073e63d9ee48ac9fce79ec1506e4aa4c + languageName: node + linkType: hard + "data-view-buffer@npm:^1.0.1": version: 1.0.1 resolution: "data-view-buffer@npm:1.0.1" @@ -7374,10 +9837,10 @@ __metadata: languageName: node linkType: hard -"desm@npm:^1.3.0": - version: 1.3.0 - resolution: "desm@npm:1.3.0" - checksum: 10/0954cb5492f787331714dafadd484e8ee4abc7b4ada410b2835408a566ee8f4160eb6b2cd692de68250c3b65db8a1961ac45878137455a01111f18e5b62c5a2c +"desm@npm:^1.3.1": + version: 1.3.1 + resolution: "desm@npm:1.3.1" + checksum: 10/9bf5bfaf863d355b9ea64c71550fc1d7fff9d1e68cd90fa263f0b512f1e520d166f2279a66fc0125633b40ae749e2056d92426f22a7843d8548420ad86332f5d languageName: node linkType: hard @@ -7916,6 +10379,95 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:~0.25.0": + version: 0.25.9 + resolution: "esbuild@npm:0.25.9" + dependencies: + "@esbuild/aix-ppc64": "npm:0.25.9" + "@esbuild/android-arm": "npm:0.25.9" + "@esbuild/android-arm64": "npm:0.25.9" + "@esbuild/android-x64": "npm:0.25.9" + "@esbuild/darwin-arm64": "npm:0.25.9" + "@esbuild/darwin-x64": "npm:0.25.9" + "@esbuild/freebsd-arm64": "npm:0.25.9" + "@esbuild/freebsd-x64": "npm:0.25.9" + "@esbuild/linux-arm": "npm:0.25.9" + "@esbuild/linux-arm64": "npm:0.25.9" + "@esbuild/linux-ia32": "npm:0.25.9" + "@esbuild/linux-loong64": "npm:0.25.9" + "@esbuild/linux-mips64el": "npm:0.25.9" + "@esbuild/linux-ppc64": "npm:0.25.9" + "@esbuild/linux-riscv64": "npm:0.25.9" + "@esbuild/linux-s390x": "npm:0.25.9" + "@esbuild/linux-x64": "npm:0.25.9" + "@esbuild/netbsd-arm64": "npm:0.25.9" + "@esbuild/netbsd-x64": "npm:0.25.9" + "@esbuild/openbsd-arm64": "npm:0.25.9" + "@esbuild/openbsd-x64": "npm:0.25.9" + "@esbuild/openharmony-arm64": "npm:0.25.9" + "@esbuild/sunos-x64": "npm:0.25.9" + "@esbuild/win32-arm64": "npm:0.25.9" + "@esbuild/win32-ia32": "npm:0.25.9" + "@esbuild/win32-x64": "npm:0.25.9" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10/fc174ae7f646ad413adb641c7e46f16be575e462ed209866b55d5954d382e5da839e3f3f89a8e42e2b71d48895cc636ba43523011249fe5ff9c63d8d39d3a364 + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -8629,6 +11181,28 @@ __metadata: languageName: node linkType: hard +"fast-xml-parser@npm:4.4.1": + version: 4.4.1 + resolution: "fast-xml-parser@npm:4.4.1" + dependencies: + strnum: "npm:^1.0.5" + bin: + fxparser: src/cli/cli.js + checksum: 10/0c05ab8703630d8c857fafadbd78d0020d3a8e54310c3842179cd4a0d9d97e96d209ce885e91241f4aa9dd8dfc2fd924a682741a423d65153cad34da2032ec44 + languageName: node + linkType: hard + +"fast-xml-parser@npm:5.2.5": + version: 5.2.5 + resolution: "fast-xml-parser@npm:5.2.5" + dependencies: + strnum: "npm:^2.1.0" + bin: + fxparser: src/cli/cli.js + checksum: 10/305017cff6968a34cbac597317be1516e85c44f650f30d982c84f8c30043e81fd38d39a8810d570136c921399dd43b9ac4775bdfbbbcfee96456f3c086b48bdd + languageName: node + linkType: hard + "fast-xml-parser@npm:^4.4.1": version: 4.5.0 resolution: "fast-xml-parser@npm:4.5.0" @@ -8690,6 +11264,16 @@ __metadata: languageName: node linkType: hard +"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": + version: 3.2.0 + resolution: "fetch-blob@npm:3.2.0" + dependencies: + node-domexception: "npm:^1.0.0" + web-streams-polyfill: "npm:^3.0.3" + checksum: 10/5264ecceb5fdc19eb51d1d0359921f12730941e333019e673e71eb73921146dceabcb0b8f534582be4497312d656508a439ad0f5edeec2b29ab2e10c72a1f86b + languageName: node + linkType: hard + "figures@npm:^3.0.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -8943,7 +11527,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.15.0": +"follow-redirects@npm:^1.15.0": version: 1.15.3 resolution: "follow-redirects@npm:1.15.3" peerDependenciesMeta: @@ -9018,6 +11602,15 @@ __metadata: languageName: node linkType: hard +"formdata-polyfill@npm:^4.0.10": + version: 4.0.10 + resolution: "formdata-polyfill@npm:4.0.10" + dependencies: + fetch-blob: "npm:^3.1.2" + checksum: 10/9b5001d2edef3c9449ac3f48bd4f8cc92e7d0f2e7c1a5c8ba555ad4e77535cc5cf621fabe49e97f304067037282dd9093b9160a3cb533e46420b446c4e6bc06f + languageName: node + linkType: hard + "formidable@npm:^2.0.1": version: 2.1.2 resolution: "formidable@npm:2.1.2" @@ -9059,6 +11652,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.2.0": + version: 11.3.1 + resolution: "fs-extra@npm:11.3.1" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/2b893213411b1da11f9b061ccb0bcff4d6dd66fe90aa8f5b1616219a5e7ca659da869f454ebd8e94aa21c58342730fb43a2e5c98b5c6c5124f0c54a4633f64b0 + 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" @@ -9118,7 +11722,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": +"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -9128,7 +11732,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -9332,6 +11936,15 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:^4.7.5": + version: 4.10.1 + resolution: "get-tsconfig@npm:4.10.1" + dependencies: + resolve-pkg-maps: "npm:^1.0.0" + checksum: 10/04d63f47fdecaefbd1f73ec02949be4ec4db7d6d9fbc8d4e81f9a4bb1c6f876e48943712f2f9236643d3e4d61d9a7b06da08564d08b034631ebe3f5605bef237 + languageName: node + linkType: hard + "github-from-package@npm:0.0.0": version: 0.0.0 resolution: "github-from-package@npm:0.0.0" @@ -9676,7 +12289,7 @@ __metadata: "@aws-sdk/client-apigatewaymanagementapi": "npm:3.540.0" "@aws-sdk/client-lambda": "npm:3.540.0" "@aws-sdk/client-sqs": "npm:3.540.0" - "@hathor/wallet-lib": "npm:1.15.0" + "@hathor/wallet-lib": "npm:2.8.3" "@types/jest": "npm:29.5.13" "@typescript-eslint/eslint-plugin": "npm:^7.4.0" "@typescript-eslint/parser": "npm:^7.4.0" @@ -10253,13 +12866,6 @@ __metadata: 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" @@ -11065,6 +13671,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:^5.7.0": + version: 5.10.0 + resolution: "jose@npm:5.10.0" + checksum: 10/03881d1dfb390dcf50926402edcfe233bf557b5a77321fcb1bdb53453bc1cdd26d2d0a9ab28c7445cbb826881f84fdf5074179700f10c2711ccb9880f51065d7 + languageName: node + linkType: hard + "js-beautify@npm:^1.14.5": version: 1.14.9 resolution: "js-beautify@npm:1.14.9" @@ -11125,6 +13738,13 @@ __metadata: languageName: node linkType: hard +"jsep@npm:^1.4.0": + version: 1.4.0 + resolution: "jsep@npm:1.4.0" + checksum: 10/935824fe6ac28fcff3cd13878f508f99f6c13e7f0f53ec9fca0d3db465e6dd15f8af030bcdc75a38b07c78359c656647435923a26aceb91607027021f00c17f2 + languageName: node + linkType: hard + "jsesc@npm:^2.5.1": version: 2.5.2 resolution: "jsesc@npm:2.5.2" @@ -11220,6 +13840,13 @@ __metadata: languageName: node linkType: hard +"json-stringify-safe@npm:^5.0.1": + version: 5.0.1 + resolution: "json-stringify-safe@npm:5.0.1" + checksum: 10/59169a081e4eeb6f9559ae1f938f656191c000e0512aa6df9f3c8b2437a4ab1823819c6b9fd1818a4e39593ccfd72e9a051fdd3e2d1e340ed913679e888ded8c + languageName: node + linkType: hard + "json5@npm:^0.5.1": version: 0.5.1 resolution: "json5@npm:0.5.1" @@ -11262,10 +13889,17 @@ __metadata: languageName: node linkType: hard -"jsonpath-plus@npm:^7.2.0": - version: 7.2.0 - resolution: "jsonpath-plus@npm:7.2.0" - checksum: 10/f602445b1aa2d55abc2875859fd948f942980ef6400ca2a0362c7a6aa6f912467865262f4d092e04a16889fa74f0dbf6fd67b9dc9583485a5059be6e0a62c6c2 +"jsonpath-plus@npm:^10.2.0": + version: 10.3.0 + resolution: "jsonpath-plus@npm:10.3.0" + dependencies: + "@jsep-plugin/assignment": "npm:^1.3.0" + "@jsep-plugin/regex": "npm:^1.0.4" + jsep: "npm:^1.4.0" + bin: + jsonpath: bin/jsonpath-cli.js + jsonpath-plus: bin/jsonpath-cli.js + checksum: 10/082302334414c7c5ab0cc8239563118f7f14bb2949d001b009f436491d00f94a7a293eed3eaf61ffdaf72f6fda9d25198a4280c4f68a4c403154ca7ed2bd0dc9 languageName: node linkType: hard @@ -11726,13 +14360,6 @@ __metadata: languageName: node linkType: hard -"long@npm:5.2.3, 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 - "long@npm:^4.0.0": version: 4.0.0 resolution: "long@npm:4.0.0" @@ -11740,6 +14367,13 @@ __metadata: 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" @@ -11805,13 +14439,20 @@ __metadata: languageName: node linkType: hard -"luxon@npm:^3.2.0, luxon@npm:^3.2.1": +"luxon@npm:^3.2.1": version: 3.4.3 resolution: "luxon@npm:3.4.3" checksum: 10/b155c9961cf45dadae763b0ec2f5a38d81a2197714154c1dece3ed3a553f1984a34138c1856f248863c998cb623796b27de96b7f7286acdeae68220451e24540 languageName: node linkType: hard +"luxon@npm:^3.5.0": + version: 3.7.1 + resolution: "luxon@npm:3.7.1" + checksum: 10/3582460c0e2d4a88f6f0c11df30cac70c7e09a3d595b66b1d04543759a38afe6e5be28c601c4d81ee73d2e8602c65f825e2c8a8542392cc564624f2bf7d6301f + languageName: node + linkType: hard + "make-dir@npm:^1.0.0": version: 1.3.0 resolution: "make-dir@npm:1.3.0" @@ -12388,6 +15029,17 @@ __metadata: languageName: node linkType: hard +"nock@npm:^13.5.6": + version: 13.5.6 + resolution: "nock@npm:13.5.6" + dependencies: + debug: "npm:^4.1.0" + json-stringify-safe: "npm:^5.0.1" + propagate: "npm:^2.0.0" + checksum: 10/a57c265b75e5f7767e2f8baf058773cdbf357c31c5fea2761386ec03a008a657f9df921899fe2a9502773b47145b708863b32345aef529b3c45cba4019120f88 + languageName: node + linkType: hard + "node-abi@npm:^3.3.0": version: 3.68.0 resolution: "node-abi@npm:3.68.0" @@ -12422,6 +15074,13 @@ __metadata: languageName: node linkType: hard +"node-domexception@npm:^1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10/e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233 + languageName: node + linkType: hard + "node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.8, node-fetch@npm:^2.6.9, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -12436,6 +15095,17 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^3.3.2": + version: 3.3.2 + resolution: "node-fetch@npm:3.3.2" + dependencies: + data-uri-to-buffer: "npm:^4.0.0" + fetch-blob: "npm:^3.1.4" + formdata-polyfill: "npm:^4.0.10" + checksum: 10/24207ca8c81231c7c59151840e3fded461d67a31cf3e3b3968e12201a42f89ce4a0b5fb7079b1fa0a4655957b1ca9257553200f03a9f668b45ebad265ca5593d + languageName: node + linkType: hard + "node-forge@npm:^1.3.1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" @@ -12950,17 +15620,6 @@ __metadata: 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" @@ -13322,6 +15981,13 @@ __metadata: languageName: node linkType: hard +"propagate@npm:^2.0.0": + version: 2.0.1 + resolution: "propagate@npm:2.0.1" + checksum: 10/8c761c16e8232f82f6d015d3e01e8bd4109f47ad804f904d950f6fe319813b448ca112246b6bfdc182b400424b155b0b7c4525a9bb009e6fa950200157569c14 + languageName: node + linkType: hard + "proto-list@npm:~1.2.1": version: 1.2.4 resolution: "proto-list@npm:1.2.4" @@ -13674,6 +16340,13 @@ __metadata: languageName: node linkType: hard +"resolve-pkg-maps@npm:^1.0.0": + version: 1.0.0 + resolution: "resolve-pkg-maps@npm:1.0.0" + checksum: 10/0763150adf303040c304009231314d1e84c6e5ebfa2d82b7d94e96a6e82bacd1dcc0b58ae257315f3c8adb89a91d8d0f12928241cba2df1680fbe6f60bf99b0e + languageName: node + linkType: hard + "resolve.exports@npm:^2.0.0": version: 2.0.2 resolution: "resolve.exports@npm:2.0.2" @@ -13744,7 +16417,7 @@ __metadata: languageName: node linkType: hard -"retry@npm:0.13.1, retry@npm:^0.13.1": +"retry@npm:0.13.1": version: 0.13.1 resolution: "retry@npm:0.13.1" checksum: 10/6125ec2e06d6e47e9201539c887defba4e47f63471db304c59e4b82fc63c8e89ca06a77e9d34939a9a42a76f00774b2f46c0d4a4cbb3e287268bd018ed69426d @@ -14178,37 +16851,39 @@ __metadata: languageName: node linkType: hard -"serverless-offline@npm:13.1.2": - version: 13.1.2 - resolution: "serverless-offline@npm:13.1.2" +"serverless-offline@npm:14.4.0": + version: 14.4.0 + resolution: "serverless-offline@npm:14.4.0" dependencies: - "@aws-sdk/client-lambda": "npm:^3.421.0" + "@aws-sdk/client-lambda": "npm:^3.636.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" + "@hapi/hapi": "npm:^21.3.10" array-unflat-js: "npm:^0.1.3" boxen: "npm:^7.1.1" chalk: "npm:^5.3.0" - desm: "npm:^1.3.0" + desm: "npm:^1.3.1" execa: "npm:^8.0.1" - fs-extra: "npm:^11.1.1" + fs-extra: "npm:^11.2.0" is-wsl: "npm:^3.1.0" java-invoke-local: "npm:0.0.6" - jose: "npm:^4.14.6" + jose: "npm:^5.7.0" js-string-escape: "npm:^1.0.1" - jsonpath-plus: "npm:^7.2.0" + jsonpath-plus: "npm:^10.2.0" jsonschema: "npm:^1.4.1" jszip: "npm:^3.10.1" - luxon: "npm:^3.2.0" + luxon: "npm:^3.5.0" + nock: "npm:^13.5.6" + node-fetch: "npm:^3.3.2" node-schedule: "npm:^2.1.1" p-memoize: "npm:^7.1.1" - p-retry: "npm:^6.1.0" + tree-kill: "npm:^1.2.2" + tsx: "npm:^4.17.0" velocityjs: "npm:^2.0.6" - ws: "npm:^8.14.2" + ws: "npm:^8.18.0" peerDependencies: - serverless: ^3.2.0 - checksum: 10/bcc5d3cd2dfe2d4b599540f8f6f38533b802325ba291f9e81b8952049ab2d32013b0c1ec874e46ec021fadb5fc1f8474994bb879cd163769c9e1043a0f2b317d + serverless: ^4.0.0 + checksum: 10/727948c2ff126374e8b03e203a8134d31e3aca4f3f16f88c1786261c11a53db56fdfde683e11262b7f551529d4548bb06d4112bfdca97e49b6f2920ab96cdde8 languageName: node linkType: hard @@ -14286,12 +16961,18 @@ __metadata: languageName: node linkType: hard -"serverless@npm:3.35.2": - version: 3.35.2 - resolution: "serverless@npm:3.35.2" +"serverless@npm:3.40.0": + version: 3.40.0 + resolution: "serverless@npm:3.40.0" dependencies: - "@serverless/dashboard-plugin": "npm:^7.0.2" - "@serverless/platform-client": "npm:^4.4.0" + "@aws-sdk/client-api-gateway": "npm:^3.588.0" + "@aws-sdk/client-cognito-identity-provider": "npm:^3.588.0" + "@aws-sdk/client-eventbridge": "npm:^3.588.0" + "@aws-sdk/client-iam": "npm:^3.588.0" + "@aws-sdk/client-lambda": "npm:^3.588.0" + "@aws-sdk/client-s3": "npm:^3.588.0" + "@serverless/dashboard-plugin": "npm:^7.2.0" + "@serverless/platform-client": "npm:^4.5.1" "@serverless/utils": "npm:^6.13.1" abort-controller: "npm:^3.0.0" ajv: "npm:^8.12.0" @@ -14349,7 +17030,7 @@ __metadata: bin: serverless: bin/serverless.js sls: bin/serverless.js - checksum: 10/68a994a885213208e07a699429b6ee65976fcf44f4c62164128d3fc9aaafb02736b4daaf25805204a20538c5c1a86713bb4cc68bbd8a8e7b2bd917bb86a3f03b + checksum: 10/5d016a4f4c118fa7d60d25c52b0247b720d459e770e0c9449ae015b02f05c8e193b60bf41c0330f134ca4b0232c5005be33a94f8b805398832b864eedf956855 languageName: node linkType: hard @@ -15015,6 +17696,13 @@ __metadata: languageName: node linkType: hard +"strnum@npm:^2.1.0": + version: 2.1.1 + resolution: "strnum@npm:2.1.1" + checksum: 10/d5fe6e4333cddc17569331048e403e876ffcf629989815f0359b0caf05dae9441b7eef3d7dd07427313ac8b3f05a8f60abc1f61efc15f97245dbc24028362bc9 + languageName: node + linkType: hard + "strtok3@npm:^6.2.4": version: 6.3.0 resolution: "strtok3@npm:6.3.0" @@ -15127,8 +17815,9 @@ __metadata: winston: "npm:3.13.0" ws: "npm:8.13.0" xstate: "npm:4.38.2" + zod: "npm:3.23.8" peerDependencies: - "@hathor/wallet-lib": 1.15.0 + "@hathor/wallet-lib": 2.8.3 "@wallet-service/common": 1.5.0 languageName: unknown linkType: soft @@ -15403,6 +18092,15 @@ __metadata: languageName: node linkType: hard +"tree-kill@npm:^1.2.2": + version: 1.2.2 + resolution: "tree-kill@npm:1.2.2" + bin: + tree-kill: cli.js + checksum: 10/49117f5f410d19c84b0464d29afb9642c863bc5ba40fcb9a245d474c6d5cc64d1b177a6e6713129eb346b40aebb9d4631d967517f9fbe8251c35b21b13cd96c7 + languageName: node + linkType: hard + "trim-repeated@npm:^1.0.0": version: 1.0.0 resolution: "trim-repeated@npm:1.0.0" @@ -15591,6 +18289,22 @@ __metadata: languageName: node linkType: hard +"tsx@npm:^4.17.0": + version: 4.20.4 + resolution: "tsx@npm:4.20.4" + dependencies: + esbuild: "npm:~0.25.0" + fsevents: "npm:~2.3.3" + get-tsconfig: "npm:^4.7.5" + dependenciesMeta: + fsevents: + optional: true + bin: + tsx: dist/cli.mjs + checksum: 10/dc5d7b7a15fc67f9e3bd20a6d0b3322bd798aa544ce6ab1e857134ea5803f2cd7ac8f4a2099e4ca842dd6e22f2d5a5ea0e0057f571f4d2101ba64b676ffaea3d + languageName: node + linkType: hard + "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0" @@ -15842,21 +18556,21 @@ __metadata: "typescript@patch:typescript@npm%3A5.4.3#optional!builtin": version: 5.4.3 - resolution: "typescript@patch:typescript@npm%3A5.4.3#optional!builtin::version=5.4.3&hash=d69c25" + resolution: "typescript@patch:typescript@npm%3A5.4.3#optional!builtin::version=5.4.3&hash=5adc0c" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/3abea475798fdf7ee46e75dafc50c85f30fd1e7061559ec2af61646f23d16c91742703f04f0ac55be52f58ca05c02f77404b7b94bbad16278c9a54c9eeb4f4ea + checksum: 10/5aedd97595582b08aadb8a70e8e3ddebaf5a9c1e5ad4d6503c2fcfc15329b5cf8d01145b09913e9555683ac16c5123a96be32b6d72614098ebd42df520eed9b1 languageName: node linkType: hard "typescript@patch:typescript@npm%3A^5.8.2#optional!builtin": version: 5.8.2 - resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin::version=5.8.2&hash=d69c25" + resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin::version=5.8.2&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/6ae9b2c4d3254ec2eaee6f26ed997e19c02177a212422993209f81e87092b2bb0a4738085549c5b0164982a5609364c047c72aeb281f6c8d802cd0d1c6f0d353 + checksum: 10/97920a082ffc57583b1cb6bc4faa502acc156358e03f54c7fc7fdf0b61c439a717f4c9070c449ee9ee683d4cfc3bb203127c2b9794b2950f66d9d307a4ff262c languageName: node linkType: hard @@ -16201,12 +18915,12 @@ __metadata: npm-run-all: "npm:4.1.5" prom-client: "npm:13.2.0" redis: "npm:3.1.2" - serverless: "npm:3.35.2" + serverless: "npm:3.40.0" serverless-api-gateway-throttling: "npm:2.0.3" serverless-better-credentials: "npm:2.0.0" serverless-iam-roles-per-function: "npm:3.2.0" serverless-mysql: "npm:1.5.4" - serverless-offline: "npm:13.1.2" + serverless-offline: "npm:14.4.0" serverless-plugin-aws-alerts: "npm:1.7.5" serverless-plugin-monorepo: "npm:0.11.0" serverless-plugin-warmup: "npm:8.2.1" @@ -16224,7 +18938,7 @@ __metadata: webpack-node-externals: "npm:3.0.0" winston: "npm:3.13.0" peerDependencies: - "@hathor/wallet-lib": 1.15.0 + "@hathor/wallet-lib": 2.8.3 "@wallet-service/common": 1.5.0 languageName: unknown linkType: soft @@ -16248,6 +18962,13 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:^3.0.3": + version: 3.3.3 + resolution: "web-streams-polyfill@npm:3.3.3" + checksum: 10/8e7e13501b3834094a50abe7c0b6456155a55d7571312b89570012ef47ec2a46d766934768c50aabad10a9c30dd764a407623e8bfcc74fcb58495c29130edea9 + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -16570,9 +19291,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.14.2": - version: 8.14.2 - resolution: "ws@npm:8.14.2" +"ws@npm:^8.18.0": + version: 8.18.3 + resolution: "ws@npm:8.18.3" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -16581,7 +19302,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/815ff01d9bc20a249b2228825d9739268a03a4408c2e0b14d49b0e2ae89d7f10847e813b587ba26992bdc33e9d03bed131e4cae73ff996baf789d53e99c31186 + checksum: 10/725964438d752f0ab0de582cd48d6eeada58d1511c3f613485b5598a83680bedac6187c765b0fe082e2d8cc4341fc57707c813ae780feee82d0c5efe6a4c61b6 languageName: node linkType: hard @@ -16749,3 +19470,10 @@ __metadata: checksum: 10/33bd5ee7017656c2ad728b5d4ba510e15bd65ce1ec180c5bbdc7a5f063256353ec482e6a2bc74de7515219d8494147924b9aae16e63fdaaf37cdf7d1ee8df125 languageName: node linkType: hard + +"zod@npm:3.23.8": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1 + languageName: node + linkType: hard